+ {#each visibleItems as { item, top, height, preview } (item.id)}
+
+
+
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 playingId === item.id && audioEl && !audioEl.paused}
-
+
+ {#if item.audioPath}
+
{:else}
-
+
{/if}
-
- {:else}
-
- {/if}
-
-
- {item.title || item.preview || item.text.slice(0, 80)}
-
+
+
+
+ {preview?.text || compactPreviewText(item)}
+
+
-
- {#if item.duration}
-
- {formatDuration(item.duration)}
-
- {/if}
-
-
-
- {#if item.source && item.source.toLowerCase().includes("file")}
-
- {:else}
-
- {/if}
-
-
-
-
- {item.date}
-
-
-
-
-
-
-
- {#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")}
+
+ {:else}
+
+ {/if}
+
+
+
+
+ {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) ----