feat(phase9): History LLM tag UI — per-row Tag, chips, promote, batch
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 @@
|
||||
<Star size={14} aria-hidden="true" />
|
||||
<span class="text-[11px]">Starred</span>
|
||||
</button>
|
||||
{#if history.some((i) => !i.llmTags || i.llmTags.length === 0)}
|
||||
<button
|
||||
class="btn-md rounded-lg text-text-tertiary hover:text-text hover:bg-hover disabled:opacity-50 inline-flex items-center gap-1.5"
|
||||
onclick={tagAllUntagged}
|
||||
disabled={bulkTagging}
|
||||
title="Run AI content tagging across every untagged transcript"
|
||||
>
|
||||
<Tag size={13} aria-hidden="true" />
|
||||
<span class="text-[11px]">{bulkTagging ? `Tagging ${bulkTaggingProgress}` : "Tag all untagged"}</span>
|
||||
</button>
|
||||
{/if}
|
||||
{#if history.length > 0}
|
||||
<button
|
||||
class="btn-md rounded-lg text-text-tertiary hover:text-danger hover:bg-hover"
|
||||
@@ -708,6 +790,17 @@
|
||||
>×</button>
|
||||
</span>
|
||||
{/each}
|
||||
<!-- Phase 9 LLM-generated tags. Dashed border + italic
|
||||
distinguishes from manual chips. Click to promote
|
||||
into manualTags. -->
|
||||
{#each (item.llmTags || []) as t (t)}
|
||||
<button
|
||||
class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] italic border border-dashed border-border-subtle text-text-tertiary hover:text-text hover:border-accent"
|
||||
onclick={() => promoteLlmTag(item, t)}
|
||||
title="Click to keep as a manual tag"
|
||||
aria-label={`Promote ${t} to manual tag`}
|
||||
>{t}</button>
|
||||
{/each}
|
||||
<input
|
||||
type="text"
|
||||
class="bg-bg-input border border-border rounded-full px-2 py-0.5 text-[10px]
|
||||
@@ -743,6 +836,17 @@
|
||||
onclick={(e) => { e.stopPropagation(); exportMarkdown(item); }}
|
||||
title="Export this transcript as a Markdown file with YAML frontmatter"
|
||||
>Export .md</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 text-[11px] px-3 py-1.5 rounded-lg bg-hover text-text-secondary hover:text-text disabled:opacity-50"
|
||||
style="transition-duration: var(--duration-ui)"
|
||||
onclick={(e) => { e.stopPropagation(); tagRow(item); }}
|
||||
disabled={tagging.has(item.id)}
|
||||
title="Generate AI content tags (topic and intent)"
|
||||
aria-label="Generate content tags with AI"
|
||||
>
|
||||
<Tag size={11} aria-hidden="true" />
|
||||
{tagging.has(item.id) ? "Tagging…" : "Tag"}
|
||||
</button>
|
||||
{#if item.audioPath && item.segments && item.segments.length > 0}
|
||||
<button
|
||||
class="text-[11px] px-3 py-1.5 rounded-lg bg-hover text-accent hover:text-accent-hover"
|
||||
|
||||
Reference in New Issue
Block a user