Chat mode, phase 2: local history, answer actions, motion polish
- Conversations in a versioned IndexedDB store (freezmo-chat v1, zod validated with the same schema backup/sync use, capped at 100 x 300). Settled turns persist (question always; answer when produced, partial stops included); ?c=<id> keeps reloads on the conversation via history.replaceState (no router round-trip mid-chat). - History UI: desktop column, phone sheet (spring, backdrop, Escape), auto titles from the first question, inline rename, delete with an Undo toast, diacritic-folding search over titles and message text. Scope and model are remembered per conversation; history is readable offline. - Assistant actions: Turn into flashcards (New deck with the answer as notes, landing on the options step - deck seed gained step) and Save to notes (appends to the single scoped deck, trimmed at 40k). - Chats travel in the backup export (preview counts them) and in the encrypted sync payload; still never the key. - Motion from lib/motion.ts: message fade-up, sheet springs for the model/context/history sheets, 150ms composer height transition, all skipped under reduced motion. - Tests: chat suite 63 -> 89 checks (history, reload, rename/search/ delete/undo, actions, export, two-device sync round-trip, offline, desktop panel). Full set: 12/12 suites, 513 checks. SW -> freezmo-v33. Also: tests/model-compare.mjs benchmark (diagnostic) for deciding the default text model on numbers; defaults unchanged.
This commit is contained in:
parent
756a6257a2
commit
5ca0de920c
19 changed files with 1107 additions and 113 deletions
|
|
@ -339,3 +339,52 @@ export const srsRecordSchema = z.object({
|
|||
lastCorrect: z.boolean(),
|
||||
});
|
||||
export type SrsRecord = z.infer<typeof srsRecordSchema>;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Chat (history, sources, scopes) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export const chatSourceSchema = z.object({
|
||||
deckId: z.string().min(1).max(120),
|
||||
deckTitle: z.string().max(200),
|
||||
section: z.string().max(200),
|
||||
});
|
||||
export type ChatSource = z.infer<typeof chatSourceSchema>;
|
||||
|
||||
export const chatAttachmentSchema = z.object({
|
||||
name: z.string().max(200),
|
||||
kind: z.enum(["photo", "document", "text"]),
|
||||
chars: z.number().int().min(0).max(10_000_000),
|
||||
});
|
||||
export type ChatAttachment = z.infer<typeof chatAttachmentSchema>;
|
||||
|
||||
export const chatMessageSchema = z.object({
|
||||
id: z.string().min(1).max(80),
|
||||
role: z.enum(["user", "assistant"]),
|
||||
content: z.string().max(20_000),
|
||||
thinking: z.string().max(20_000).optional(),
|
||||
model: z.string().max(120).optional(),
|
||||
sources: z.array(chatSourceSchema).max(40).optional(),
|
||||
attachments: z.array(chatAttachmentSchema).max(10).optional(),
|
||||
at: z.number(),
|
||||
error: z.object({ message: z.string().max(2000), kind: z.string().max(40).optional() }).optional(),
|
||||
});
|
||||
export type ChatMessage = z.infer<typeof chatMessageSchema>;
|
||||
|
||||
export const chatScopeSchema = z.discriminatedUnion("kind", [
|
||||
z.object({ kind: z.literal("all") }),
|
||||
z.object({ kind: z.literal("none") }),
|
||||
z.object({ kind: z.literal("decks"), deckIds: z.array(z.string().max(120)).max(200) }),
|
||||
]);
|
||||
export type ChatScope = z.infer<typeof chatScopeSchema>;
|
||||
|
||||
export const chatConversationSchema = z.object({
|
||||
id: z.string().min(1).max(80),
|
||||
title: z.string().max(200),
|
||||
messages: z.array(chatMessageSchema).max(300),
|
||||
scope: chatScopeSchema,
|
||||
model: z.string().max(120).optional(),
|
||||
createdAt: z.number(),
|
||||
updatedAt: z.number(),
|
||||
});
|
||||
export type ChatConversation = z.infer<typeof chatConversationSchema>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue