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):

  • Card remains 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_payload on cards, 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_recall and speech_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 .btn custom-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_review already 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 the review-mode-* rules in flexbox.css) into one tokenized review-card stylesheet. That cluster already descends from this same original design via immersive-design’s set-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 &nbsp; deliberately).
  • Distractor tokens (extras not in the answer) are supported by the data shape (pool vs answer in the JSON payload) but were not shipped originally; keep the shape, defer the feature.
  • Payloads: prompt_payload carries the prompt text/audio reference; answer_payload carries 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):

  1. init: Record visible.
  2. recording: Stop visible; auto-stop after 10s (STOP_RECORDING_INTERVAL).
  3. stopped: Listen + Re-record visible; submit enabled.
  4. 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); >= 65 reads as “mostly correct” (legacy thresholds kept as the starting point, tunable).
  • Persist transcript, pct_correct, and the audio file on the Review (the columns already exist: user_answer, pronunciation_score, pronunciation fields; the controller’s TODO: Implement pronunciation assessment at decks/reviews_controller.rb:114 is 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

  1. Template layer columns + the two template classes, wired through Card#record_review! (card_sources already exists for provenance). The columns (template_key, template_version, prompt_payload, answer_payload) shipped with #411, whose document.word.inference.v1 (CardTemplates::DocumentWordInference, registry in CardTemplate) is the first tenant; it renders through the existing read mode via the Card display-text delegation.
  2. 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).
  3. tokenized_recall for write/listen.
  4. speech_production for speak (needs the transcription service decision).
  5. Session results + repeat loop.

This site uses Just the Docs, a documentation theme for Jekyll.