diff --git a/src/lib/pages/DictationPage.svelte b/src/lib/pages/DictationPage.svelte index 5126c7d..1341d7d 100644 --- a/src/lib/pages/DictationPage.svelte +++ b/src/lib/pages/DictationPage.svelte @@ -3,6 +3,7 @@ import { onMount, onDestroy } from "svelte"; import { Channel, invoke } from "@tauri-apps/api/core"; import { page, settings, templates, profiles, addToHistory, addTask, tasks } from "$lib/stores/page.svelte.js"; + import { profilesStore } from "$lib/stores/profiles.svelte.ts"; import { toasts } from "$lib/stores/toasts.svelte.js"; import Card from "$lib/components/Card.svelte"; import ModelDownloader from "$lib/components/ModelDownloader.svelte"; @@ -123,18 +124,6 @@ return currentModelIsEnglishOnly() ? "en" : settings.language; } - function buildInitialPrompt() { - if (!page.activeProfile || page.activeProfile === "None") return ""; - const profile = profiles.find((entry) => entry.name === page.activeProfile); - if (!profile?.words) return ""; - const words = profile.words - .split("\n") - .map((word) => word.trim()) - .filter(Boolean); - if (words.length === 0) return ""; - return `Use these terms when they match the audio: ${words.join(", ")}`; - } - async function refreshRuntimeCapabilities() { runtimeCapabilities = await invoke("get_runtime_capabilities"); } @@ -322,7 +311,11 @@ engine: settings.engine, modelId: selectedModelId(), language: effectiveLanguage(), - initialPrompt: buildInitialPrompt(), + // Backend (Task 13) falls back to profile.initial_prompt when this + // is empty. Vocabulary now lives on the active profile, not on the + // old page.activeProfile localStorage bag. + initialPrompt: "", + profileId: profilesStore.activeProfileId, saveAudio: settings.saveAudio, outputFolder: settings.outputFolder || null, removeFillers: settings.removeFillers, diff --git a/src/lib/pages/FilesPage.svelte b/src/lib/pages/FilesPage.svelte index 00d1b7d..83ad601 100644 --- a/src/lib/pages/FilesPage.svelte +++ b/src/lib/pages/FilesPage.svelte @@ -4,6 +4,7 @@ import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { settings, addToHistory } from "$lib/stores/page.svelte.js"; + import { profilesStore } from "$lib/stores/profiles.svelte.ts"; import Card from "$lib/components/Card.svelte"; import EmptyState from "$lib/components/EmptyState.svelte"; import { exportTranscript } from "$lib/utils/export.js"; @@ -76,6 +77,7 @@ path, language: settings.language, initialPrompt: "", + profileId: profilesStore.activeProfileId, removeFillers: settings.removeFillers, britishEnglish: settings.britishEnglish, antiHallucination: settings.antiHallucination, diff --git a/src/lib/pages/SettingsPage.svelte b/src/lib/pages/SettingsPage.svelte index 1e0b932..40427a6 100644 --- a/src/lib/pages/SettingsPage.svelte +++ b/src/lib/pages/SettingsPage.svelte @@ -11,6 +11,7 @@ import ZonePicker from "$lib/components/ZonePicker.svelte"; import AccessibilityControls from "$lib/components/AccessibilityControls.svelte"; import { getPreferences, updatePreferences } from "$lib/stores/preferences.svelte.js"; + import { profilesStore, DEFAULT_PROFILE_ID } from "$lib/stores/profiles.svelte.ts"; import { clampTextLines } from "$lib/utils/textMeasure.js"; import { bodyPretextLineHeight, pretextFontShorthand } from "$lib/utils/accessibilityTypography.js"; import { Check, ChevronRight } from "lucide-svelte"; @@ -103,19 +104,29 @@ } } - // --- Custom vocabulary (dictionary) — Day 5 of upgrade plan --- - // Backed by SQLite `dictionary` table via list_dictionary_command etc. - // Terms get injected into the LLM cleanup prompt so the model preserves - // the user's spellings (e.g. medication names, jargon, place names). + // --- Per-profile vocabulary (Task 15) --- + // Terms and the profile's initial_prompt travel with the active profile. + // The Rust side (Task 13) falls back to profile.initial_prompt whenever + // the invoke payload's initialPrompt is empty. let vocabulary = $state([]); let vocabularyError = $state(null); let newVocabTerm = $state(""); let newVocabNote = $state(""); + // Draft held locally so the textarea doesn't thrash the store on every + // keystroke. Saved on blur or explicit save. + let initialPromptDraft = $state(""); + let initialPromptDirty = $state(false); + let profileRenameDraft = $state(""); + let showNewVocabProfile = $state(false); + let newVocabProfileName = $state(""); + let newVocabProfilePrompt = $state(""); + async function refreshVocabulary() { vocabularyError = null; try { - vocabulary = await invoke("list_dictionary_command"); + const profileId = profilesStore.activeProfileId; + vocabulary = await profilesStore.listTerms(profileId); } catch (err) { vocabularyError = "Could not load vocabulary: " + (err?.message || err); } @@ -125,10 +136,11 @@ const term = newVocabTerm.trim(); if (!term) return; try { - await invoke("add_dictionary_entry_command", { + await profilesStore.addTerm( + profilesStore.activeProfileId, term, - note: newVocabNote.trim() || null, - }); + newVocabNote.trim(), + ); newVocabTerm = ""; newVocabNote = ""; await refreshVocabulary(); @@ -139,13 +151,66 @@ async function deleteVocabTerm(id) { try { - await invoke("delete_dictionary_entry_command", { id }); + await profilesStore.deleteTerm(id); await refreshVocabulary(); } catch (err) { vocabularyError = "Could not delete term: " + (err?.message || err); } } + // Sync the initial-prompt draft + term list when the active profile flips. + $effect(() => { + const active = profilesStore.active; + initialPromptDraft = active?.initialPrompt ?? ""; + initialPromptDirty = false; + profileRenameDraft = active?.name ?? ""; + // Refresh terms whenever the active profile id changes. + void profilesStore.activeProfileId; + refreshVocabulary(); + }); + + function onActiveProfileChange(e) { + const id = e.target.value; + profilesStore.setActive(id); + } + + async function saveInitialPrompt() { + if (!initialPromptDirty) return; + const active = profilesStore.active; + if (!active) return; + await profilesStore.update(active.id, active.name, initialPromptDraft); + initialPromptDirty = false; + } + + async function renameActiveProfile() { + const active = profilesStore.active; + if (!active) return; + const name = profileRenameDraft.trim(); + if (!name || name === active.name) { + profileRenameDraft = active.name; + return; + } + await profilesStore.update(active.id, name, active.initialPrompt); + } + + async function createVocabProfile() { + const name = newVocabProfileName.trim(); + if (!name) return; + const created = await profilesStore.create(name, newVocabProfilePrompt.trim()); + if (created) { + profilesStore.setActive(created.id); + newVocabProfileName = ""; + newVocabProfilePrompt = ""; + showNewVocabProfile = false; + } + } + + async function deleteActiveProfile() { + const active = profilesStore.active; + if (!active || active.id === DEFAULT_PROFILE_ID) return; + await profilesStore.delete(active.id); + } + // --- Diagnostics (Settings → About → Generate diagnostic report) --- // Layer 2 of the diagnostics plan: user previews exactly what would be // shared, then chooses to copy / save. Nothing leaves the machine @@ -290,9 +355,9 @@ // the user opens the Audio section. refreshAudioDevices(); - // Pre-load vocabulary too. Both lists are small; pre-fetching avoids - // a flash of "no data" when the section opens. - refreshVocabulary(); + // Vocabulary is loaded reactively via the $effect that tracks the + // active profile id (set once profilesStore.load() resolves in the + // root layout). No eager fetch needed here. try { downloadedModels = await invoke("list_models"); @@ -533,7 +598,9 @@ {/if} - + +