Review Card: Design and Behavior Spec
The implementation spec for #322 (review card redesign), extracted from the
original flashcard system in the legacy app (immersive-old, local checkout —
file references below point into it). The original shipped a more complete and
better-designed card than what is live today; this spec pulls its visual layout
and interactions forward onto the current FSRS foundation, rebuilt in the
current design system rather than ported.
Companion sections in immersive_redesign_extension_design.md: “Review and
FSRS Pipeline”, “Core Data Model” (template columns), and the review-card
entries in the Component Inventory.
Architecture: template-driven cards, STI frozen
The legacy app’s own documentation stated the intent
(immersive-old/doc/application/study-sets/decks.md):
“This configuration should be table driven – ie: add new card combinations without coding changes.”
It never got there: 6 STI subclasses x 4 modes produced 69 per-type-per-mode
view and service files, including cross-type inheritance like
CardViews::SentenceCard::Speak::UpdateService <
CardViews::NumberCard::Speak::UpdateService — evidence that the interaction,
not the content type, is the real axis of variation. Tokenized-write for a
sentence and tokenized-write for a number are the same interaction with
different payloads.
Decision (also in the design doc’s Architecture Decisions):
Cardremains the single scheduled memory item: FSRS state, deck membership, language pair, ownership, review history.- Test behavior lives in a template layer:
template_key,template_version,prompt_payload,answer_payloadoncards, with a Ruby template class per interaction owning prompt rendering, answer evaluation, and supported modes. - Existing STI subclasses (
WordCard,SentenceCard,VerbCard,NumberCard,TextCard) stay as compatibility paths. No new subclasses. - The two interactions below are the first template tenants:
tokenized_recallandspeech_production.
Visual design
Base: the original card layout, as shipped
(reference screenshots in immersive_redesign_screenshots/legacy-card-*.png;
view sources in immersive-old/app/views/card_views/).
Layout, top to bottom:
- Thin session progress bar pinned to the very top
(
_progress_bar.html.erb). - Bookmark (top-left) and close (top-right) as quiet icon controls.
- Instruction pill: small amber/highlight badge naming the task (“Read the following sentence”) — localized, template-supplied.
- Prompt block: one large focused instruction or question
(
card-question), generous whitespace. - Content block: the sentence/word with inline audio affordance.
- Answer area (mode-dependent: reveal, token bank, text area, recorder).
- Bottom nav bar: back arrow (left) · “N of M” position · Skip · primary next/submit (right).
Deltas from the original, applied during the rebuild:
- Current design tokens and components throughout: semantic OKLCh colors,
icon()inline SVGs (the originals used FontAwesome), the.btncustom-property variants, container queries for the card’s own responsive behavior. - FSRS grade buttons (Again/Hard/Good/Easy with interval labels) replace the
legacy correct/incorrect-only judgment where self-grading applies;
auto-graded modes (tokens, speech) map their result onto an FSRS rating
the way
Decks::ReviewsController#submit_reviewalready does. - Every string through
config/locales/(the current modes hardcode “Type what you see”, “Show Answer”, “Start Recording”, etc. - #322). - CSS consolidates the current six-file cluster (
set-item.css,set-item-header.css,set-item-text.css,set-item-hr.css,set-message.css, plus thereview-mode-*rules inflexbox.css) into one tokenized review-card stylesheet. That cluster already descends from this same original design viaimmersive-design’sset-item-*templates — this completes a migration that stalled halfway. - EuRom5 hook (north star): the card’s content block reserves a slot for the
contrastive annotations (
bridge-equivalence-table,bridge-grammar-callout) so grammar/document-sourced cards can carry them without a second redesign.
Interaction 1: tokenized answers (tokenized_recall)
Source: immersive-old/app/views/card_views/_tokenized_answer_form.erb,
immersive-old/app/javascript/controllers/answer_controller.js. Used by
write and listen modes in the original; same here.
Behavior:
- The expected answer is split into tokens (words; numbers split into component words), shuffled into a pool of tappable buttons below an empty answer row.
- Tapping a pool token appends it to the answer row and removes it from the pool. Tapping a token in the answer row returns it to the pool (order-preserving append).
- When the pool empties, the answer auto-submits (legacy behavior: enable and trigger the submit control) — no separate confirm tap on the happy path.
- Evaluation compares the assembled sequence against the expected token
sequence server-side; whitespace/nbsp handling preserved
(
answerItems()in the legacy controller kept deliberately). - Distractor tokens (extras not in the answer) are supported by the data
shape (
poolvsanswerin the JSON payload) but were not shipped originally; keep the shape, defer the feature. - Payloads:
prompt_payloadcarries the prompt text/audio reference;answer_payloadcarries the ordered token list (and later, distractors).
Interaction 2: speech production (speech_production)
Source: immersive-old/app/views/card_views/_record_button.html.erb,
record_button_controller.js, modules/record_speech/RecorderManager.js,
and grading in
immersive-old/app/services/card_views/number_card/speak/update_service.rb.
Recorder state machine (four targets: record/stop/listen/re-record):
init: Record visible.recording: Stop visible; auto-stop after 10s (STOP_RECORDING_INTERVAL).stopped: Listen + Re-record visible; submit enabled.- Submit uploads the audio with the review.
Grading pipeline, server-side on submit:
- Transcribe the audio in the card’s target language. The original used a
GCP module (
ML::GCP::Transcribe); the rebuild goes through the current app’s service abstractions (AWS Transcribe via the existing AWS integration, or the agent layer) — provider choice is an implementation detail behind one service object. - Score transcript vs expected answer by Damerau-Levenshtein:
pct_correct = ceil((longest_len - distance) / longest_len * 100);>= 65reads as “mostly correct” (legacy thresholds kept as the starting point, tunable). - Persist transcript,
pct_correct, and the audio file on theReview(the columns already exist:user_answer,pronunciation_score, pronunciation fields; the controller’sTODO: Implement pronunciation assessmentatdecks/reviews_controller.rb:114is exactly this). - Result view: the transcript rendered with success/warning highlighting
(legacy
_speak_result.html.erb), the learner’s own audio replayable next to the reference audio.
Session ergonomics
Source: immersive-old/app/views/card_views/card_lists/ (show,
_results_list, _repeat_buttons).
- Session progress bar across the top of every card (position/total from
the existing
ReviewSession). - End-of-session results list: each card with its result; effort summary (per the business plan, effort is first-class and never framed as loss).
- “Repeat incorrect cards” action creating a follow-up run over the misses. Under FSRS this is a relearning pass, not a new scheduling event per miss — repeats within the session don’t double-log reviews.
Sequencing
- Template layer columns + the two template classes, wired through
Card#record_review!(card_sourcesalready exists for provenance). The columns (template_key,template_version,prompt_payload,answer_payload) shipped with #411, whosedocument.word.inference.v1(CardTemplates::DocumentWordInference, registry inCardTemplate) is the first tenant; it renders through the existing read mode via theCarddisplay-text delegation. - Card shell rebuild (visual design above) with the read mode first —
replaces the
<details>reveal, consolidates the CSS cluster, localizes strings (closes most of #322’s checklist). tokenized_recallfor write/listen.speech_productionfor speak (needs the transcription service decision).- Session results + repeat loop.