From 7fc971df05163531a0183a880fee3a5834e59582 Mon Sep 17 00:00:00 2001 From: Jake Date: Sat, 25 Apr 2026 00:13:55 +0100 Subject: [PATCH] =?UTF-8?q?feat(phase9):=20History=20LLM=20tag=20UI=20?= =?UTF-8?q?=E2=80=94=20per-row=20Tag,=20chips,=20promote,=20batch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-row Tag button calls extract_content_tags_cmd, persists via saveTranscriptMeta. Dashed-italic chips render the AI tags distinct from manual; clicking a chip promotes it into manualTags (the LLM tag disappears, the manual one stays). Top toolbar gains "Tag all untagged" for batch tagging across the corpus, with progress text. Existing addManualTag / removeManualTag handlers swap their no-op saveHistory() calls for saveTranscriptMeta — picks up the latent manualTags persistence bug as a side effect. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/pages/HistoryPage.svelte | 112 +++++++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 4 deletions(-) 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 @@