Added: TypeAnswer replacement

This commit is contained in:
Luc Mcgrady 2025-10-31 12:06:58 +00:00
parent cf9c265570
commit bbf575e491
No known key found for this signature in database
GPG key ID: 4F3D7A0B17CC3D9C
4 changed files with 50 additions and 3 deletions

View file

@ -306,6 +306,7 @@ message NextCardDataResponse {
string css = 5; string css = 5;
string body_class = 6; string body_class = 6;
bool autoplay = 7; bool autoplay = 7;
optional string typed_answer = 12;
repeated card_rendering.AVTag question_av_tags = 8; repeated card_rendering.AVTag question_av_tags = 8;
repeated card_rendering.AVTag answer_av_tags = 9; repeated card_rendering.AVTag answer_av_tags = 9;

View file

@ -4,6 +4,8 @@
mod answering; mod answering;
mod states; mod states;
use std::sync::LazyLock;
use anki_proto::cards; use anki_proto::cards;
use anki_proto::generic; use anki_proto::generic;
use anki_proto::scheduler; use anki_proto::scheduler;
@ -25,6 +27,7 @@ use fsrs::ComputeParametersInput;
use fsrs::FSRSItem; use fsrs::FSRSItem;
use fsrs::FSRSReview; use fsrs::FSRSReview;
use fsrs::FSRS; use fsrs::FSRS;
use regex::Regex;
use crate::backend::Backend; use crate::backend::Backend;
use crate::card_rendering::service::rendered_nodes_to_proto; use crate::card_rendering::service::rendered_nodes_to_proto;
@ -34,7 +37,9 @@ use crate::scheduler::new::NewCardDueOrder;
use crate::scheduler::states::CardState; use crate::scheduler::states::CardState;
use crate::scheduler::states::SchedulingStates; use crate::scheduler::states::SchedulingStates;
use crate::search::SortMode; use crate::search::SortMode;
use crate::services::NotesService;
use crate::stats::studied_today; use crate::stats::studied_today;
use crate::template::RenderedNode;
impl crate::services::SchedulerService for Collection { impl crate::services::SchedulerService for Collection {
/// This behaves like _updateCutoff() in older code - it also unburies at /// This behaves like _updateCutoff() in older code - it also unburies at
@ -411,16 +416,48 @@ impl crate::services::SchedulerService for Collection {
let config = self.deck_config_for_card(&next_card.card)?; let config = self.deck_config_for_card(&next_card.card)?;
// Typed answer replacements
static ANSWER_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\[\[type:(.+?:)?(.+?)\]\]").unwrap());
const ANSWER_HTML: &str = "<center>
<input type=text id=typeans onkeypress=\"_typeAnsPress();\"
style=\"font-family: '{self.typeFont}'; font-size: {self.typeSize}px;\">
</center>";
let mut q_nodes = render.qnodes;
let typed_answer_parent_node = q_nodes.iter_mut().find_map(|node| {
if let RenderedNode::Text { text } = node {
let mut out = None;
*text = ANSWER_REGEX
.replace(text, |cap: &regex::Captures<'_>| {
out = Some(cap[2].to_string());
ANSWER_HTML
})
.to_string();
out
} else {
None
}
});
let typed_answer = typed_answer_parent_node.map(|field| {
let note = self.get_note(next_card.card.note_id.into()).unwrap();
let notetype = self.get_notetype(note.notetype_id.into()).unwrap().unwrap();
note.fields[notetype.get_field_ord(&field).unwrap()].clone()
});
Ok(NextCardDataResponse { Ok(NextCardDataResponse {
next_card: Some(NextCardData { next_card: Some(NextCardData {
queue: Some(queue.into()), queue: Some(queue.into()),
css: render.css.clone(), css: render.css.clone(),
partial_front: rendered_nodes_to_proto(render.qnodes), partial_front: rendered_nodes_to_proto(q_nodes),
partial_back: rendered_nodes_to_proto(render.anodes), partial_back: rendered_nodes_to_proto(render.anodes),
answer_buttons, answer_buttons,
autoplay: !config.inner.disable_autoplay, autoplay: !config.inner.disable_autoplay,
typed_answer,
// Filled by python // Filled by python
front: "".to_string(), front: "".to_string(),

View file

@ -27,6 +27,7 @@ export function updateNightMode() {
export class ReviewerState { export class ReviewerState {
answerHtml = ""; answerHtml = "";
currentTypedAnswer = "";
_cardData: NextCardDataResponse_NextCardData | undefined = undefined; _cardData: NextCardDataResponse_NextCardData | undefined = undefined;
beginAnsweringMs = Date.now(); beginAnsweringMs = Date.now();
readonly cardClass = writable(""); readonly cardClass = writable("");
@ -42,13 +43,16 @@ export class ReviewerState {
addEventListener("message", this.onMessage.bind(this)); addEventListener("message", this.onMessage.bind(this));
} }
onMessage(e: MessageEvent<ReviewerRequest>) { async onMessage(e: MessageEvent<ReviewerRequest>) {
switch (e.data.type) { switch (e.data.type) {
case "audio": { case "audio": {
const tags = get(this.answerShown) ? this._cardData!.answerAvTags : this._cardData!.questionAvTags; const tags = get(this.answerShown) ? this._cardData!.answerAvTags : this._cardData!.questionAvTags;
playAvtags({ tags: [tags[e.data.index]] }); playAvtags({ tags: [tags[e.data.index]] });
break; break;
} }
case "typed": {
this.currentTypedAnswer = e.data.value;
}
} }
} }

View file

@ -6,4 +6,9 @@ interface AudioMessage {
index: number; index: number;
} }
export type ReviewerRequest = AudioMessage; interface CompareTypedAnswerMessage {
type: "typed";
value: string;
}
export type ReviewerRequest = AudioMessage | CompareTypedAnswerMessage;