Added: NextCardData

This commit is contained in:
Luc Mcgrady 2025-10-04 16:29:13 +01:00
parent b6e07f5780
commit aa42d87558
No known key found for this signature in database
GPG key ID: 4F3D7A0B17CC3D9C
4 changed files with 66 additions and 12 deletions

View file

@ -17,6 +17,7 @@ import "anki/deck_config.proto";
service SchedulerService {
rpc GetQueuedCards(GetQueuedCardsRequest) returns (QueuedCards);
rpc AnswerCard(CardAnswer) returns (collection.OpChanges);
rpc NextCardData(NextCardDataRequest) returns (NextCardDataResponse);
rpc SchedTimingToday(generic.Empty) returns (SchedTimingTodayResponse);
rpc StudiedToday(generic.Empty) returns (generic.String);
rpc StudiedTodayMessage(StudiedTodayMessageRequest) returns (generic.String);
@ -285,6 +286,21 @@ message CardAnswer {
uint32 milliseconds_taken = 6;
}
message NextCardDataRequest {
optional CardAnswer answer = 1;
}
message NextCardDataResponse {
message NextCardData {
string front = 1;
string back = 2;
SchedulingStates states = 3;
}
optional NextCardData next_card = 1;
}
message CustomStudyRequest {
message Cram {
enum CramKind {

View file

@ -696,6 +696,7 @@ exposed_backend_list = [
"get_optimal_retention_parameters",
"simulate_fsrs_review",
"simulate_fsrs_workload",
"next_card_data",
# DeckConfigService
"get_ignored_before_count",
"get_retention_workload",
@ -719,7 +720,6 @@ post_handlers = {
for handler in exposed_backend_list
}
def _extract_collection_post_request(path: str) -> DynamicRequest | NotFound:
if not aqt.mw.col:
return NotFound(message=f"collection not open, ignore request for {path}")
@ -766,6 +766,8 @@ def _check_dynamic_request_permissions():
"/_anki/setSchedulingStates",
"/_anki/i18nResources",
"/_anki/congratsInfo",
# TODO: Unsure about this
"/_anki/nextCardData"
):
pass
else:

View file

@ -7,6 +7,7 @@ mod states;
use anki_proto::cards;
use anki_proto::generic;
use anki_proto::scheduler;
use anki_proto::scheduler::next_card_data_response::NextCardData;
use anki_proto::scheduler::ComputeFsrsParamsResponse;
use anki_proto::scheduler::ComputeMemoryStateResponse;
use anki_proto::scheduler::ComputeOptimalRetentionResponse;
@ -14,6 +15,8 @@ use anki_proto::scheduler::FsrsBenchmarkResponse;
use anki_proto::scheduler::FuzzDeltaRequest;
use anki_proto::scheduler::FuzzDeltaResponse;
use anki_proto::scheduler::GetOptimalRetentionParametersResponse;
use anki_proto::scheduler::NextCardDataRequest;
use anki_proto::scheduler::NextCardDataResponse;
use anki_proto::scheduler::SimulateFsrsReviewRequest;
use anki_proto::scheduler::SimulateFsrsReviewResponse;
use anki_proto::scheduler::SimulateFsrsWorkloadResponse;
@ -382,6 +385,30 @@ impl crate::services::SchedulerService for Collection {
delta_days: self.get_fuzz_delta(input.card_id.into(), input.interval)?,
})
}
fn next_card_data(&mut self, req: NextCardDataRequest) -> Result<NextCardDataResponse> {
if let Some(answer) = req.answer {
self.answer_card(&mut answer.into())?;
}
let queue = self.get_queued_cards(1, false)?;
let next_card = queue.cards.first();
if let Some(next_card) = next_card {
let cid = next_card.card.id;
let render = self.render_existing_card(cid, false, false)?;
Ok(NextCardDataResponse {
next_card: Some(NextCardData {
front: render.question().to_string(),
back: render.answer().to_string(),
states: Some(next_card.states.clone().into()),
}),
})
} else {
Ok(NextCardDataResponse::default())
}
}
}
impl crate::services::BackendSchedulerService for Backend {

View file

@ -1,29 +1,38 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { CardAnswer, SchedulingStates } from "@generated/anki/scheduler_pb";
import { nextCardData } from "@generated/backend";
import { bridgeCommand } from "@tslib/bridgecommand";
import { writable } from "svelte/store";
import { preloadAnswerImages } from "../../reviewer/images";
export function setupReviewer(iframe: HTMLIFrameElement) {
const cardClass = writable("");
let answer_html = "";
let states: SchedulingStates | undefined;
function updateHtml(htmlString) {
iframe.contentWindow?.postMessage({ type: "html", value: htmlString }, "*");
}
function showQuestion(q, a, cc) {
updateHtml(q);
// html.set(q);
cardClass.set(cc);
preloadAnswerImages(a);
async function showQuestion(answer: CardAnswer | null) {
let resp = await nextCardData({
answer: answer || undefined,
});
// TODO: "Congratulation screen" logic
const question = resp.nextCard?.front || "";
answer_html = resp.nextCard?.back || "";
states = resp.nextCard?.states;
console.log({ resp });
updateHtml(question);
}
function showAnswer() {
updateHtml(answer_html);
}
function onReady() {
// TODO This should probably be a "ready" command now that it is part of the actual reviewer,
// Currently this depends on the reviewer component mounting after the bottom-reviewer which it should but seems hacky.
// Maybe use a counter with a counter.subscribe($counter == 2 then call("ready"))
bridgeCommand("bottomReady");
iframe.contentWindow?.postMessage({ type: "nightMode", value: true }, "*");
showQuestion(null);
}
iframe?.addEventListener("load", onReady);
@ -46,8 +55,8 @@ export function setupReviewer(iframe: HTMLIFrameElement) {
}
});
globalThis._showAnswer = updateHtml;
globalThis._showQuestion = showQuestion;
globalThis._showAnswer = showAnswer;
return { cardClass };
}