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
|
|
@ -7,7 +7,7 @@ import { Markdown } from "./Markdown";
|
|||
import { ThinkingRow } from "./ThinkingRow";
|
||||
import { Button } from "@/components/ui";
|
||||
import { ModelErrorActions } from "@/components/ModelErrorActions";
|
||||
import { PencilIcon, RefreshIcon } from "@/components/icons";
|
||||
import { PencilIcon, PlusIcon, RefreshIcon, SparklesIcon } from "@/components/icons";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import type { ChatMessage } from "@/lib/chat";
|
||||
|
||||
|
|
@ -24,6 +24,8 @@ export function MessageRow({
|
|||
onRegenerate,
|
||||
onEdit,
|
||||
onRetry,
|
||||
onToFlashcards,
|
||||
onSaveNotes,
|
||||
}: {
|
||||
message: ChatMessage;
|
||||
streaming?: boolean;
|
||||
|
|
@ -33,6 +35,8 @@ export function MessageRow({
|
|||
onRegenerate?: () => void;
|
||||
onEdit?: () => void;
|
||||
onRetry?: () => void;
|
||||
onToFlashcards?: () => void;
|
||||
onSaveNotes?: () => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
|
||||
|
|
@ -108,6 +112,26 @@ export function MessageRow({
|
|||
{t("chat.regenerate")}
|
||||
</button>
|
||||
) : null}
|
||||
{onToFlashcards ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToFlashcards}
|
||||
className="flex items-center gap-1.5 rounded-xl border border-[var(--glass-border)] bg-[var(--surface-strong)] px-2.5 py-2 text-[12px] font-medium text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]"
|
||||
>
|
||||
<SparklesIcon className="size-3.5" />
|
||||
{t("chat.toFlashcards")}
|
||||
</button>
|
||||
) : null}
|
||||
{onSaveNotes ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSaveNotes}
|
||||
className="flex items-center gap-1.5 rounded-xl border border-[var(--glass-border)] bg-[var(--surface-strong)] px-2.5 py-2 text-[12px] font-medium text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]"
|
||||
>
|
||||
<PlusIcon className="size-3.5" />
|
||||
{t("chat.saveToNotes")}
|
||||
</button>
|
||||
) : null}
|
||||
{message.model ? <span className="text-[11px] text-[var(--text-subtle)]">{message.model}</span> : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue