diff --git a/src/lib/pages/SettingsPage.svelte b/src/lib/pages/SettingsPage.svelte index 17896a2..a78d137 100644 --- a/src/lib/pages/SettingsPage.svelte +++ b/src/lib/pages/SettingsPage.svelte @@ -12,6 +12,7 @@ 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 { toasts } from "$lib/stores/toasts.svelte.js"; import { clampTextLines } from "$lib/utils/textMeasure.js"; import { bodyPretextLineHeight, pretextFontShorthand } from "$lib/utils/accessibilityTypography.js"; import { Check, ChevronRight } from "lucide-svelte"; @@ -152,6 +153,9 @@ let vocabularyError = $state(null); let newVocabTerm = $state(""); let newVocabNote = $state(""); + let showBulkVocab = $state(false); + let bulkVocabText = $state(""); + let bulkVocabBusy = $state(false); // Draft held locally so the textarea doesn't thrash the store on every // keystroke. Saved on blur or explicit save. @@ -189,6 +193,61 @@ } } + async function addBulkVocabTerms() { + const raw = bulkVocabText; + // Accept newline-separated OR comma-separated (or mixed) — whichever the + // user pasted. Trim each entry, drop empties, dedupe within the input. + const candidates = Array.from( + new Set( + raw + .split(/[\n,]+/) + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0), + ), + ); + if (candidates.length === 0) { + vocabularyError = "Nothing to import — paste one term per line or separated by commas."; + return; + } + + // Skip terms the profile already has (case-insensitive — Whisper prompts + // don't care about case, and duplicates pollute the Terms list). + const existing = new Set(vocabulary.map((row) => row.term.toLowerCase())); + const toAdd = candidates.filter((term) => !existing.has(term.toLowerCase())); + const skipped = candidates.length - toAdd.length; + + bulkVocabBusy = true; + vocabularyError = null; + const failed = []; + try { + for (const term of toAdd) { + try { + await profilesStore.addTerm(profilesStore.activeProfileId, term, ""); + } catch (err) { + failed.push({ term, message: err?.message || String(err) }); + } + } + } finally { + bulkVocabBusy = false; + } + + await refreshVocabulary(); + bulkVocabText = ""; + showBulkVocab = false; + + const addedCount = toAdd.length - failed.length; + const parts = []; + if (addedCount > 0) parts.push(`Added ${addedCount}`); + if (skipped > 0) parts.push(`skipped ${skipped} duplicate${skipped === 1 ? "" : "s"}`); + if (failed.length > 0) parts.push(`${failed.length} failed`); + if (failed.length > 0) { + vocabularyError = `Some terms failed: ${failed.map((f) => f.term).join(", ")}`; + } + if (parts.length > 0) { + toasts.info("Vocabulary import", parts.join(" · ")); + } + } + async function deleteVocabTerm(id) { try { await profilesStore.deleteTerm(id); @@ -952,6 +1011,43 @@ >Add + +
+ {#if !showBulkVocab} + + {:else} +
+ +
+ + +
+
+ {/if} +
+ {#if vocabularyError}

{vocabularyError}

{/if}