diff --git a/src/lib/pages/HistoryPage.svelte b/src/lib/pages/HistoryPage.svelte
index 9b8c54d..da790f8 100644
--- a/src/lib/pages/HistoryPage.svelte
+++ b/src/lib/pages/HistoryPage.svelte
@@ -2,7 +2,13 @@
// @ts-nocheck
import { onDestroy } from "svelte";
import { invoke } from "@tauri-apps/api/core";
- import { history, saveHistory, deleteFromHistory, renameHistoryEntry } from "$lib/stores/page.svelte.js";
+ import {
+ history,
+ saveHistory,
+ saveTranscriptMeta,
+ deleteFromHistory,
+ renameHistoryEntry,
+ } from "$lib/stores/page.svelte.js";
import { toasts } from "$lib/stores/toasts.svelte.js";
import { convertFileSrc } from "@tauri-apps/api/core";
import {
@@ -20,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 } from 'lucide-svelte';
+ import { Search, Clock, Play, Pause, FileText, Mic, ChevronDown, ExternalLink, Star, Tag } from 'lucide-svelte';
const prefs = getPreferences();
const COLLAPSED_ROW_MIN_HEIGHT = 54;
@@ -337,14 +343,79 @@
return;
}
item.manualTags = [...(item.manualTags || []), next];
- saveHistory();
+ saveTranscriptMeta(item.id, { manualTags: item.manualTags });
e.target.value = "";
}
function removeManualTag(item, tag) {
const t = normaliseTag(tag);
item.manualTags = (item.manualTags || []).filter((x) => normaliseTag(x) !== t);
- saveHistory();
+ saveTranscriptMeta(item.id, { manualTags: item.manualTags });
+ }
+
+ // Phase 9 LLM content tagging.
+ let tagging = $state(new Set());
+ let bulkTagging = $state(false);
+ let bulkTaggingProgress = $state("");
+
+ async function tagRow(item) {
+ if (tagging.has(item.id)) return;
+ const next = new Set(tagging);
+ next.add(item.id);
+ tagging = next;
+ try {
+ const result = await invoke("extract_content_tags_cmd", {
+ transcript: item.text || "",
+ });
+ const r = result;
+ item.llmTags = [`topic:${r.topic}`, `intent:${r.intent}`];
+ await saveTranscriptMeta(item.id, { llmTags: item.llmTags });
+ } catch (err) {
+ toasts.error("Tagging failed", String(err));
+ } finally {
+ const rest = new Set(tagging);
+ rest.delete(item.id);
+ tagging = rest;
+ }
+ }
+
+ function promoteLlmTag(item, tag) {
+ const normalised = normaliseTag(tag);
+ const manual = new Set((item.manualTags || []).map(normaliseTag));
+ manual.add(normalised);
+ item.manualTags = [...manual];
+ item.llmTags = (item.llmTags || []).filter((t) => normaliseTag(t) !== normalised);
+ saveTranscriptMeta(item.id, {
+ manualTags: item.manualTags,
+ llmTags: item.llmTags,
+ });
+ }
+
+ async function tagAllUntagged() {
+ if (bulkTagging) return;
+ bulkTagging = true;
+ try {
+ const untagged = history.filter((i) => !i.llmTags || i.llmTags.length === 0);
+ let done = 0;
+ for (const item of untagged) {
+ done += 1;
+ bulkTaggingProgress = `${done} / ${untagged.length}`;
+ try {
+ const result = await invoke("extract_content_tags_cmd", {
+ transcript: item.text || "",
+ });
+ const r = result;
+ item.llmTags = [`topic:${r.topic}`, `intent:${r.intent}`];
+ await saveTranscriptMeta(item.id, { llmTags: item.llmTags });
+ } catch (err) {
+ console.error("bulk tag failed for", item.id, err);
+ }
+ }
+ toasts.success(`Tagged ${untagged.length} transcript${untagged.length === 1 ? "" : "s"}`);
+ } finally {
+ bulkTagging = false;
+ bulkTaggingProgress = "";
+ }
}
async function exportMarkdown(item) {
@@ -459,6 +530,17 @@
Starred
+ {#if history.some((i) => !i.llmTags || i.llmTags.length === 0)}
+
+ {/if}
{#if history.length > 0}
{/each}
+
+ {#each (item.llmTags || []) as t (t)}
+
+ {/each}
Export .md
+
{#if item.audioPath && item.segments && item.segments.length > 0}