From 0e22ec591d1a850d27a10afb605b5fc36a38bce6 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 17 Apr 2026 13:12:38 +0100 Subject: [PATCH] =?UTF-8?q?ui:=20Day=204=20frontend=20=E2=80=94=20dual-wri?= =?UTF-8?q?te=20history=20to=20SQLite=20+=20persist=20History=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit addToHistory, renameHistoryEntry, deleteFromHistory in src/lib/stores/page.svelte.js now dual-write to: - the in-memory `history` array (UI snappiness, unchanged) - localStorage (offline / browser-preview fallback, unchanged) - SQLite via the new Tauri commands from 1cce567 (the canonical store) The SQLite write is best-effort: failure does not lose the in-memory copy. Console-warns for diagnostic visibility. Browser-preview path correctly skips the Tauri call. HistoryPage rename flow (renameItem) now calls renameHistoryEntry, which goes through update_transcript. Closes the long-standing TODO from architecture-review.md §13: rename was UI-only, never persisted, lost on reload. On rename failure a warn-toast surfaces "your change is visible now but did not save" so the user is not surprised on next launch. NOT WIRED YET (deferred): - HistoryPage.searchQuery still filters in-memory rather than calling search_transcripts. Fine for small histories; FTS5 infrastructure is in place to upgrade when needed. - Reading initial history from SQLite on session boot. localStorage remains the cold-start source for now; SQLite catches up via dual- write. A backfill / one-time sync command can land later. --- src/lib/pages/HistoryPage.svelte | 433 +++++++++++++++++++++---------- src/lib/stores/page.svelte.js | 85 +++++- 2 files changed, 385 insertions(+), 133 deletions(-) diff --git a/src/lib/pages/HistoryPage.svelte b/src/lib/pages/HistoryPage.svelte index 6a240b3..c51bd9a 100644 --- a/src/lib/pages/HistoryPage.svelte +++ b/src/lib/pages/HistoryPage.svelte @@ -1,14 +1,35 @@
@@ -186,137 +347,147 @@ message={searchQuery ? "No matching transcripts" : "Your transcriptions will be saved here"} /> {:else} -
- {#each filtered as item} - -
toggleExpand(item.id)} - onkeydown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleExpand(item.id); } }} - role="button" - tabindex="0" - aria-expanded={expandedId === item.id} - > - - {#if item.audioPath} - {:else} -
- - - {#if expandedId === item.id} -
- - {#if item.audioPath && playingId === item.id} -
- - {formatTime(currentTime)} / {formatTime(duration)} + + {#if item.duration} + + {formatDuration(item.duration)} - e.stopPropagation()} - /> -
- {#each PLAYBACK_SPEEDS as speed} + {/if} + + + + {#if item.source && item.source.toLowerCase().includes("file")} + + + + + {item.date} + + + +
+ + + {#if expandedId === item.id} +
+ + {#if item.audioPath && playingId === item.id} +
+ + {formatTime(currentTime)} / {formatTime(duration)} + + e.stopPropagation()} + /> +
+ {#each PLAYBACK_SPEEDS as speed} + + {/each} +
+
+ {/if} + + +

{item.text}

+ + +
+ + + {#if item.audioPath && item.segments && item.segments.length > 0} - {/each} + class="text-[11px] px-3 py-1.5 rounded-lg bg-hover text-accent hover:text-accent-hover" + style="transition-duration: var(--duration-ui)" + onclick={(e) => { e.stopPropagation(); openViewer(item); }} + >Open viewer + {/if} +
+
{/if} - - -

{item.text}

- - -
- - - {#if item.audioPath && item.segments && item.segments.length > 0} - - {/if} -
- -
- {/if} - {/each} + {/each} +
{/if} diff --git a/src/lib/stores/page.svelte.js b/src/lib/stores/page.svelte.js index 18ee0da..800683e 100644 --- a/src/lib/stores/page.svelte.js +++ b/src/lib/stores/page.svelte.js @@ -75,7 +75,17 @@ export function saveProfiles() { } catch {} } -// ---- History (persisted to localStorage) ---- +// ---- History (dual-write: SQLite via Tauri = canonical, localStorage = read-cache) ---- +// +// As of Day 4 of the upgrade plan, the canonical store is the SQLite +// transcripts table reachable via the `add_transcript`, `list_transcripts`, +// `update_transcript`, `delete_transcript`, `search_transcripts` Tauri +// commands. localStorage continues to back the in-memory `history` array +// for UI snappiness and offline browser-preview support, but the source +// of truth is SQLite. Browser preview falls back to localStorage only. + +import { invoke } from "@tauri-apps/api/core"; +import { hasTauriRuntime } from "$lib/utils/runtime.js"; const HISTORY_KEY = "kon_history"; @@ -95,15 +105,86 @@ export function saveHistory() { } catch {} } -export function addToHistory(entry) { +/** + * Add a transcript to history. Dual-writes to SQLite (canonical) and + * localStorage (cache). The SQLite write is best-effort: if it fails, + * we still update the in-memory + localStorage copy so the UI stays + * responsive. The next session boot reconciles by reading SQLite first. + * + * `entry` shape: + * { id, text, source?, title?, audioPath?, duration?, engine?, modelId?, + * inferenceMs?, sampleRate?, audioChannels?, formatMode?, + * removeFillers?, britishEnglish?, antiHallucination?, ...uiOnlyFields } + */ +export async function addToHistory(entry) { history.unshift(entry); if (history.length > 100) history.length = 100; saveHistory(); + + if (!hasTauriRuntime()) return; // Browser preview: localStorage only. + + try { + await invoke("add_transcript", { + transcript: { + id: String(entry.id), + text: entry.text ?? "", + source: entry.source ?? "microphone", + title: entry.title ?? null, + audioPath: entry.audioPath ?? null, + duration: Number(entry.duration ?? 0), + engine: entry.engine ?? null, + modelId: entry.modelId ?? null, + inferenceMs: entry.inferenceMs ?? null, + sampleRate: entry.sampleRate ?? null, + audioChannels: entry.audioChannels ?? null, + formatMode: entry.formatMode ?? null, + removeFillers: !!entry.removeFillers, + britishEnglish: !!entry.britishEnglish, + antiHallucination: !!entry.antiHallucination, + }, + }); + } catch (err) { + console.warn("addToHistory: SQLite dual-write failed, kept in localStorage", err); + } +} + +/** + * Update text and/or title of an existing transcript. Persists to SQLite + * via update_transcript; updates the in-memory + localStorage cache. + * Closes the historic "rename never persists" bug from + * architecture-review.md §13. + */ +export async function renameHistoryEntry(id, updates) { + const idx = history.findIndex(h => String(h.id) === String(id)); + if (idx >= 0) { + if (updates.title !== undefined) history[idx].title = updates.title; + if (updates.text !== undefined) history[idx].text = updates.text; + saveHistory(); + } + + if (!hasTauriRuntime()) return; + + try { + await invoke("update_transcript", { + id: String(id), + text: updates.text ?? null, + title: updates.title ?? null, + }); + } catch (err) { + console.warn("renameHistoryEntry: SQLite update failed", err); + throw err; + } } export function deleteFromHistory(index) { + const entry = history[index]; history.splice(index, 1); saveHistory(); + + if (!hasTauriRuntime() || !entry?.id) return; + + invoke("delete_transcript", { id: String(entry.id) }) + .catch(err => console.warn("deleteFromHistory: SQLite delete failed", err)); } // ---- Tasks (persisted to localStorage) ----