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
|
|
@ -12,11 +12,12 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { applyTheme } from "@/components/ThemeToggle";
|
||||
import { chatHistoryStore, ensureChatsHydrated, replaceChats } from "./chat-store";
|
||||
import { replaceFolders, folderStore } from "./folders";
|
||||
import { homeworkStore, replaceHomework } from "./homework";
|
||||
import { ensureLearningHydrated, learningStore, replaceLearning } from "./history";
|
||||
import { setLocale, t } from "./i18n";
|
||||
import { folderSchema, deckSchema, homeworkRecordSchema, answerRecordSchema, srsRecordSchema } from "./schemas";
|
||||
import { folderSchema, deckSchema, homeworkRecordSchema, answerRecordSchema, srsRecordSchema, chatConversationSchema } from "./schemas";
|
||||
import { saveSettings, settingsStore } from "./settings";
|
||||
import { deckStore, replaceDecks } from "./storage";
|
||||
|
||||
|
|
@ -36,6 +37,8 @@ export const backupDataSchema = z.object({
|
|||
/** Answer log and SRS schedule (IndexedDB). Optional for old backups. */
|
||||
history: z.array(answerRecordSchema).max(20_000).optional(),
|
||||
srs: z.array(srsRecordSchema).max(50_000).optional(),
|
||||
/** Chat conversations (IndexedDB). Optional for old backups. */
|
||||
chats: z.array(chatConversationSchema).max(100).optional(),
|
||||
});
|
||||
|
||||
export const backupSchema = z.object({
|
||||
|
|
@ -54,6 +57,7 @@ export type BackupFile = z.infer<typeof backupSchema>;
|
|||
|
||||
export async function buildBackup(): Promise<BackupFile> {
|
||||
await ensureLearningHydrated();
|
||||
await ensureChatsHydrated();
|
||||
return {
|
||||
app: "freezmo",
|
||||
kind: "backup",
|
||||
|
|
@ -65,6 +69,7 @@ export async function buildBackup(): Promise<BackupFile> {
|
|||
homework: homeworkStore.getSnapshot(),
|
||||
history: learningStore.getAnswersSnapshot(),
|
||||
srs: learningStore.getSrsSnapshot(),
|
||||
chats: chatHistoryStore.getSnapshot(),
|
||||
settings: {
|
||||
haptics: settingsStore.getSnapshot().haptics,
|
||||
dailyGoal: settingsStore.getSnapshot().dailyGoal,
|
||||
|
|
@ -138,6 +143,7 @@ export type ImportPreview = {
|
|||
decks: ImportCounts;
|
||||
folders: ImportCounts;
|
||||
homework: ImportCounts;
|
||||
chats: ImportCounts;
|
||||
};
|
||||
|
||||
export function previewImport(backup: BackupFile): ImportPreview {
|
||||
|
|
@ -145,6 +151,7 @@ export function previewImport(backup: BackupFile): ImportPreview {
|
|||
decks: countMerge(deckStore.getSnapshot(), backup.data.decks),
|
||||
folders: countMerge(folderStore.getSnapshot(), backup.data.folders),
|
||||
homework: countMerge(homeworkStore.getSnapshot(), backup.data.homework),
|
||||
chats: countMerge(chatHistoryStore.getSnapshot(), backup.data.chats ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -184,6 +191,13 @@ export async function applyImport(
|
|||
await replaceLearning(answers, [...srsByKey.values()]);
|
||||
}
|
||||
|
||||
// Chat conversations: merge by id (newest wins) or replace wholesale.
|
||||
await ensureChatsHydrated();
|
||||
const incomingChats = data.chats ?? [];
|
||||
if (incomingChats.length > 0) {
|
||||
replaceChats(mode === "replace" ? incomingChats : mergeById(chatHistoryStore.getSnapshot(), incomingChats));
|
||||
}
|
||||
|
||||
const settings = data.settings;
|
||||
if (settings) {
|
||||
if (typeof settings.haptics === "boolean") saveSettings({ haptics: settings.haptics });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue