Teach it back: the AI becomes the curious student

A new mode where the user explains a topic in their own words and the AI
plays a curious classmate: short reactions, exactly one follow-up question
at a time, never "correct"/"wrong", never lecturing, and everything it knows
comes from the user's own decks and notes.

- Entry points: a sidebar row next to Exam mode and a "Teach it back" action
  in every deck's menu (which prefills the topic and scope).
- Start screen: deck or typed topic, friendly/skeptical student, the same
  context and model chips as Chat (/teach?deck=<id> pre-selects the deck).
- Session: the student prompt is a mode of the existing /api/chat route, so
  model resolution, allowlists, rate limits and the streaming client are the
  same code; the personality lives in lib/prompts.ts (TEACH_*). The deck's
  cards travel as a numbered checklist and the student is told to ask about
  the ideas that were skipped; it must say so rather than guess when the
  notes cover nothing. One constant (TEACH_MAX_EXCHANGES = 10) ends the
  session, or the Finish button does.
- Recap: a teach_summary call (JSON, one repair retry) yields well / gaps
  with card references / next steps; each gap links to the card it means.
  "Add missed cards to review" schedules those activities due now; "Turn
  gaps into cards" generates one flashcard per gap from the deck's notes and
  shows an editable review list before saving.
- Sessions are chat conversations with a mode:"teach" marker + session
  record: same IndexedDB history, backup and sync, filtered out of the Chat
  screens; the recent list reopens a recap or continues a session.
- Reused pieces: MessageRow, Composer (now optional attach/scope chips and a
  mic), Markdown/KaTeX, AiDisclaimer, ContextPicker/ModelPicker, plus a
  shared useChatModels hook and useKeyboardInset extracted from Chat.
- Dictation: Web Speech API recognition (lib/speech-input.ts); the mic only
  renders where the browser supports it, and dictated phrases land in the
  draft.
- All strings in en + cs. teach-test: 44 checks (API prompts and guards,
  both entry points, the session, recap actions, history, exchange limit,
  360/390px layouts, mic on/off). Version 0.8.1, SW v103.
This commit is contained in:
Martin Dvořák 2026-10-16 23:05:24 +00:00
commit 6036238bf0
25 changed files with 2821 additions and 181 deletions

View file

@ -1107,6 +1107,73 @@ const en: Record<string, string> = {
"chat.savedToNotes": "Saved to {deck}'s notes",
"chat.savedToNotesTrimmed": "Saved to {deck}'s notes (trimmed to fit)",
"chat.savedFromChat": "Saved from chat",
"chat.voiceStart": "Dictate",
"chat.voiceStop": "Stop dictating",
"teach.nav": "Teach it back",
"teach.title": "Teach it back",
"teach.subtitle": "You explain. The AI plays your student.",
"teach.intro": "Explain the topic in your own words and the student asks short follow-up questions. Where it keeps digging, that is your gap.",
"teach.topicLabel": "What will you explain?",
"teach.topicPlaceholder": "e.g. Why the sky is blue",
"teach.studentLabel": "What may the student know?",
"teach.difficultyLabel": "Pick a student",
"teach.friendly": "Friendly student",
"teach.friendlyNote": "Patient questions, wants it to click.",
"teach.skeptical": "Skeptical student",
"teach.skepticalNote": "Asks why, looks for the catch and the edges.",
"teach.start": "Start teaching",
"teach.recent": "Recent sessions",
"teach.noSessions": "Nothing here yet — finished sessions and their recaps appear here.",
"teach.inProgress": "In progress",
"teach.recap": "Recap",
"teach.newSession": "New session",
"teach.open": "Open",
"teach.untitled": "Untitled session",
"teach.deleteSession": "Delete session",
"teach.deleted": "Session deleted",
"teach.back": "Back",
"teach.finish": "Finish",
"teach.finishHint": "End the session and get your recap.",
"teach.exchangeCount": "{n}/{max}",
"teach.youExplain": "You explain",
"teach.emptyTitle": "You are the teacher now",
"teach.emptyHint": "Explain the topic the way you would tell a classmate. The student asks one short follow-up at a time — no grading, no lectures.",
"teach.placeholder": "Explain it in your own words…",
"teach.voiceStart": "Dictate your explanation",
"teach.voiceStop": "Stop dictating",
"teach.listening": "Listening…",
"teach.limitReached": "Good place to wrap up — here is your recap.",
"teach.summaryTitle": "Recap",
"teach.summaryBusy": "Writing your recap…",
"teach.summaryFailed": "The recap could not be written. Try again.",
"teach.retry": "Write it again",
"teach.well": "Explained well",
"teach.wellEmpty": "Nothing stood out yet.",
"teach.gaps": "Missed or unclear",
"teach.gapsEmpty": "Nothing was missed.",
"teach.next": "Next steps",
"teach.cardLinkTitle": "Open in {deck}",
"teach.addToReview": "Add missed cards to review",
"teach.reviewAdded.one": "{n} card added to review",
"teach.reviewAdded.few": "{n} cards added to review",
"teach.reviewAdded.many": "{n} cards added to review",
"teach.gapsToCards": "Turn gaps into cards",
"teach.gapDeck": "Add the cards to",
"teach.generate": "Create the cards",
"teach.generating": "Creating the cards…",
"teach.reviewTitle": "Review the cards",
"teach.reviewHint": "Edit anything before saving; uncheck what you do not want.",
"teach.cardFront": "Front",
"teach.cardBack": "Back",
"teach.saveCards.one": "Save {n} card",
"teach.saveCards.few": "Save {n} cards",
"teach.saveCards.many": "Save {n} cards",
"teach.cardsSaved.one": "{n} card added to {deck}",
"teach.cardsSaved.few": "{n} cards added to {deck}",
"teach.cardsSaved.many": "{n} cards added to {deck}",
"teach.generateFailed": "The cards could not be created.",
"teach.noGapDeck": "Pick a deck with notes for the cards.",
"teach.noDecksHint": "No decks are selected — the student has nothing to check against.",
"legal.updated": "Last updated {when}",
"legal.privacy": "Privacy policy",
"legal.terms": "Terms of service",
@ -2197,6 +2264,73 @@ const cs: Record<string, string> = {
"chat.savedToNotes": "Uloženo do poznámek sady {deck}",
"chat.savedToNotesTrimmed": "Uloženo do poznámek sady {deck} (zkráceno, aby se vešlo)",
"chat.savedFromChat": "Uloženo z chatu",
"chat.voiceStart": "Diktovat",
"chat.voiceStop": "Zastavit diktování",
"teach.nav": "Vysvětli to",
"teach.title": "Vysvětli to",
"teach.subtitle": "Ty vysvětluješ. AI hraje studenta.",
"teach.intro": "Vysvětli téma vlastními slovy a student se bude krátce doptávat. Kde bude kopat hlouběji, tam máš mezeru.",
"teach.topicLabel": "Co budeš vysvětlovat?",
"teach.topicPlaceholder": "např. Proč je nebe modré",
"teach.studentLabel": "Co smí student znát?",
"teach.difficultyLabel": "Vyber si studenta",
"teach.friendly": "Přátelský student",
"teach.friendlyNote": "Trpělivé otázky, chce to pochopit.",
"teach.skeptical": "Nedůvěřivý student",
"teach.skepticalNote": "Ptá se proč, hledá háček a okrajové případy.",
"teach.start": "Začít učit",
"teach.recent": "Nedávné výuky",
"teach.noSessions": "Zatím tu nic není — dokončené výuky a jejich přehledy se objeví tady.",
"teach.inProgress": "Probíhá",
"teach.recap": "Přehled",
"teach.newSession": "Nová výuka",
"teach.open": "Otevřít",
"teach.untitled": "Výuka bez názvu",
"teach.deleteSession": "Smazat výuku",
"teach.deleted": "Výuka smazána",
"teach.back": "Zpět",
"teach.finish": "Dokončit",
"teach.finishHint": "Ukončit výuku a zobrazit přehled.",
"teach.exchangeCount": "{n}/{max}",
"teach.youExplain": "Ty vysvětluješ",
"teach.emptyTitle": "Teď učíš ty",
"teach.emptyHint": "Vysvětli téma, jako bys ho vyprávěl spolužákovi. Student se vždy zeptá na jednu krátkou věc — žádné známkování, žádné přednášky.",
"teach.placeholder": "Vysvětli to vlastními slovy…",
"teach.voiceStart": "Nadiktovat vysvětlení",
"teach.voiceStop": "Zastavit diktování",
"teach.listening": "Poslouchám…",
"teach.limitReached": "Tady je dobré místo skončit — tady je přehled.",
"teach.summaryTitle": "Přehled",
"teach.summaryBusy": "Píšu přehled…",
"teach.summaryFailed": "Přehled se nepovedlo napsat. Zkus to znovu.",
"teach.retry": "Napsat znovu",
"teach.well": "Dobře vysvětlené",
"teach.wellEmpty": "Zatím nic nevyniklo.",
"teach.gaps": "Chybělo nebo bylo nejasné",
"teach.gapsEmpty": "Nic nechybělo.",
"teach.next": "Další kroky",
"teach.cardLinkTitle": "Otevřít v sadě {deck}",
"teach.addToReview": "Přidat vynechané kartičky k opakování",
"teach.reviewAdded.one": "{n} kartička přidána k opakování",
"teach.reviewAdded.few": "{n} kartičky přidány k opakování",
"teach.reviewAdded.many": "{n} kartiček přidáno k opakování",
"teach.gapsToCards": "Udělat z mezer kartičky",
"teach.gapDeck": "Přidat kartičky do",
"teach.generate": "Vytvořit kartičky",
"teach.generating": "Vytvářím kartičky…",
"teach.reviewTitle": "Zkontroluj kartičky",
"teach.reviewHint": "Před uložením cokoli uprav; nechtěné odškrtni.",
"teach.cardFront": "Přední strana",
"teach.cardBack": "Zadní strana",
"teach.saveCards.one": "Uložit {n} kartičku",
"teach.saveCards.few": "Uložit {n} kartičky",
"teach.saveCards.many": "Uložit {n} kartiček",
"teach.cardsSaved.one": "{n} kartička přidána do sady {deck}",
"teach.cardsSaved.few": "{n} kartičky přidány do sady {deck}",
"teach.cardsSaved.many": "{n} kartiček přidáno do sady {deck}",
"teach.generateFailed": "Kartičky se nepovedlo vytvořit.",
"teach.noGapDeck": "Vyber sadu s poznámkami pro kartičky.",
"teach.noDecksHint": "Nejsou vybrané žádné sady — student nemá proti čemu ověřovat.",
"legal.updated": "Naposledy upraveno {when}",
"legal.privacy": "Zásady soukromí",
"legal.terms": "Podmínky použití",