diff --git a/src/lib/pages/HistoryPage.svelte b/src/lib/pages/HistoryPage.svelte
index 61bbafb..fedadb9 100644
--- a/src/lib/pages/HistoryPage.svelte
+++ b/src/lib/pages/HistoryPage.svelte
@@ -26,7 +26,7 @@
import EmptyState from "$lib/components/EmptyState.svelte";
import { formatTime, formatDuration } from "$lib/utils/time.js";
import { PLAYBACK_SPEEDS } from "$lib/utils/constants.js";
- import { Search, Clock, Play, Pause, FileText, Mic, ChevronDown, ExternalLink, Star, Tag } from 'lucide-svelte';
+ import { Search, Clock, Play, Pause, FileText, Mic, ChevronDown, ExternalLink, Sparkles, Star, Tag } from 'lucide-svelte';
const prefs = getPreferences();
const COLLAPSED_ROW_MIN_HEIGHT = 54;
@@ -439,6 +439,66 @@
}
}
+ // Auto-title flow. Mirrors the tag flow above piece-for-piece — same
+ // per-row + bulk shape, same in-flight Set tracking, same progress
+ // string. The actual generation lives in `generate_title_cmd` in
+ // src-tauri/src/commands/llm.rs; the on-demand button here is the
+ // retry path when auto-title-on-save (in addToHistory) was skipped
+ // because the LLM wasn't loaded yet.
+ let titling = $state(new Set());
+ let bulkTitling = $state(false);
+ let bulkTitlingProgress = $state("");
+
+ async function titleRow(item) {
+ if (titling.has(item.id)) return;
+ const next = new Set(titling);
+ next.add(item.id);
+ titling = next;
+ try {
+ const title = await invoke("generate_title_cmd", { transcript: item.text || "" });
+ const trimmed = (title || "").trim();
+ if (trimmed) {
+ await renameHistoryEntry(item.id, { title: trimmed });
+ } else {
+ toasts.warn("No title produced", "The model returned an empty response.");
+ }
+ } catch (err) {
+ toasts.error("Title generation failed", String(err));
+ } finally {
+ const rest = new Set(titling);
+ rest.delete(item.id);
+ titling = rest;
+ }
+ }
+
+ async function titleAllUntitled() {
+ if (bulkTitling) return;
+ bulkTitling = true;
+ try {
+ const untitled = history.filter((i) => !i.title || !i.title.trim());
+ let done = 0;
+ let written = 0;
+ for (const item of untitled) {
+ done += 1;
+ bulkTitlingProgress = `${done} / ${untitled.length}`;
+ try {
+ const title = await invoke("generate_title_cmd", { transcript: item.text || "" });
+ const trimmed = (title || "").trim();
+ if (trimmed) {
+ await renameHistoryEntry(item.id, { title: trimmed });
+ written += 1;
+ }
+ } catch (err) {
+ console.error("bulk title failed for", item.id, err);
+ }
+ }
+ toasts.success(`Titled ${written} transcript${written === 1 ? "" : "s"}`);
+ } finally {
+ bulkTitling = false;
+ bulkTitlingProgress = "";
+ }
+ }
+
async function exportMarkdown(item) {
await saveTranscriptAsMarkdown(item);
}
@@ -565,6 +625,17 @@
{bulkTagging ? `Tagging ${bulkTaggingProgress}` : "Tag all untagged"}
{/if}
+ {#if history.some((i) => !i.title || !i.title.trim())}
+
+ {/if}
{#if history.length > 0}
{tagging.has(item.id) ? "Tagging…" : "Tag"}
+
{#if item.audioPath && item.segments && item.segments.length > 0}