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:
parent
3fb491c54d
commit
6036238bf0
25 changed files with 2821 additions and 181 deletions
|
|
@ -440,6 +440,48 @@ export const chatScopeSchema = z.discriminatedUnion("kind", [
|
|||
]);
|
||||
export type ChatScope = z.infer<typeof chatScopeSchema>;
|
||||
|
||||
/* Teach it back: sessions are chat conversations with a "teach" marker and
|
||||
the session's own record (which cards were checked, the recap). Kept inside
|
||||
the chat schema so history, backup and encrypted sync carry them for free. */
|
||||
|
||||
export const teachDifficultySchema = z.enum(["friendly", "skeptical"]);
|
||||
export type TeachDifficulty = z.infer<typeof teachDifficultySchema>;
|
||||
|
||||
/** One checklist card the student checks the explanation against. */
|
||||
export const teachCardRefSchema = z.object({
|
||||
ref: z.number().int().min(1).max(999),
|
||||
deckId: z.string().min(1).max(80),
|
||||
activityId: z.string().min(1).max(80),
|
||||
deckTitle: z.string().max(200),
|
||||
/** The prompt side of the card, for the recap's links. */
|
||||
label: z.string().max(300),
|
||||
});
|
||||
export type TeachCardRef = z.infer<typeof teachCardRefSchema>;
|
||||
|
||||
export const teachSummarySchema = z.object({
|
||||
well: z.array(z.string().max(800)).max(12),
|
||||
gaps: z
|
||||
.array(
|
||||
z.object({
|
||||
what: z.string().max(800),
|
||||
/** [n] reference into the session's cards, when one covers the gap. */
|
||||
ref: z.number().int().min(1).max(999).nullable().optional(),
|
||||
}),
|
||||
)
|
||||
.max(20),
|
||||
next: z.array(z.string().max(800)).max(6),
|
||||
});
|
||||
export type TeachSummary = z.infer<typeof teachSummarySchema>;
|
||||
|
||||
export const teachSessionSchema = z.object({
|
||||
difficulty: teachDifficultySchema,
|
||||
topic: z.string().max(200),
|
||||
scope: chatScopeSchema,
|
||||
cards: z.array(teachCardRefSchema).max(80),
|
||||
summary: teachSummarySchema.optional(),
|
||||
});
|
||||
export type TeachSession = z.infer<typeof teachSessionSchema>;
|
||||
|
||||
export const chatConversationSchema = z.object({
|
||||
id: z.string().min(1).max(80),
|
||||
title: z.string().max(200),
|
||||
|
|
@ -447,6 +489,9 @@ export const chatConversationSchema = z.object({
|
|||
scope: chatScopeSchema,
|
||||
model: z.string().max(120).optional(),
|
||||
provider: z.string().max(60).optional(),
|
||||
/** "chat" (or absent, for old records) / "teach". */
|
||||
mode: z.enum(["chat", "teach"]).optional(),
|
||||
teach: teachSessionSchema.optional(),
|
||||
createdAt: z.number(),
|
||||
updatedAt: z.number(),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue