refactor(kon): settings page — collapsible sections, first section open by default
- Consolidated 7 separate Card components into a single Card with 8 accordion sections - Added openSection $state variable; 'transcription' open by default, others collapsed - Each section header is a clickable button with +/− indicator; clicking open section closes it - Section headings use font-display italic 18px (vs 26px page title) as accordion headers - All existing functionality preserved: toggles, dropdowns, model download/load, profiles, templates, hotkey recorder, output folder picker - Profiles & Templates remain as nested accordion-within-accordion (existing pattern kept) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,9 @@
|
|||||||
let showNewProfile = $state(false);
|
let showNewProfile = $state(false);
|
||||||
let showNewTemplate = $state(false);
|
let showNewTemplate = $state(false);
|
||||||
|
|
||||||
|
// Accordion state — first section open by default
|
||||||
|
let openSection = $state('transcription');
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
const loaded = await invoke("check_engine");
|
const loaded = await invoke("check_engine");
|
||||||
@@ -202,432 +205,497 @@
|
|||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<h2 class="font-display text-[26px] italic text-text px-7 pt-6 pb-5">Settings</h2>
|
<h2 class="font-display text-[26px] italic text-text px-7 pt-6 pb-5">Settings</h2>
|
||||||
|
|
||||||
<div class="px-7 pb-8 space-y-4">
|
<div class="px-7 pb-8">
|
||||||
<!-- Transcription -->
|
|
||||||
<Card>
|
<Card>
|
||||||
<div class="p-5">
|
<!-- Transcription -->
|
||||||
<h3 class="text-[14px] font-semibold text-text mb-5">Transcription</h3>
|
<div class="border-b border-border-subtle">
|
||||||
|
<button
|
||||||
|
class="flex items-center justify-between w-full py-4 px-5 text-left"
|
||||||
|
onclick={() => openSection = openSection === 'transcription' ? null : 'transcription'}
|
||||||
|
>
|
||||||
|
<h3 class="font-display text-[18px] italic text-text">Transcription</h3>
|
||||||
|
<span class="text-text-tertiary text-[16px] leading-none">{openSection === 'transcription' ? '−' : '+'}</span>
|
||||||
|
</button>
|
||||||
|
{#if openSection === 'transcription'}
|
||||||
|
<div class="px-5 pb-5 animate-fade-in">
|
||||||
|
|
||||||
<!-- Engine selector -->
|
<!-- Engine selector -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">Engine</p>
|
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">Engine</p>
|
||||||
<SegmentedButton options={["whisper", "parakeet"]} bind:value={settings.engine} />
|
<SegmentedButton options={["whisper", "parakeet"]} bind:value={settings.engine} />
|
||||||
<p class="text-[11px] text-text-tertiary mt-2">
|
<p class="text-[11px] text-text-tertiary mt-2">
|
||||||
{settings.engine === "whisper" ? "OpenAI Whisper — 99+ languages, reliable" :
|
{settings.engine === "whisper" ? "OpenAI Whisper — 99+ languages, reliable" :
|
||||||
"Nvidia Parakeet — faster on CPU, English-focused, auto-punctuated"}
|
"Nvidia Parakeet — faster on CPU, English-focused, auto-punctuated"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Format mode -->
|
|
||||||
<div class="mb-6">
|
|
||||||
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">Format Mode</p>
|
|
||||||
<SegmentedButton options={["Raw", "Clean", "Smart"]} bind:value={settings.formatMode} />
|
|
||||||
<p class="text-[11px] text-text-tertiary mt-2">
|
|
||||||
{settings.formatMode === "Raw" ? "Exact Whisper output, no formatting" :
|
|
||||||
settings.formatMode === "Clean" ? "Grouped into paragraphs, punctuation tidied" :
|
|
||||||
"Structured with lists, headings, and sections"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Engine-specific model management -->
|
|
||||||
{#if settings.engine === "whisper"}
|
|
||||||
<div class="mb-6">
|
|
||||||
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">Whisper Model</p>
|
|
||||||
<SegmentedButton options={["Tiny", "Base", "Small", "Medium"]} bind:value={settings.modelSize} />
|
|
||||||
<p class="text-[11px] text-text-tertiary mt-2">{modelDescriptions[settings.modelSize]}</p>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2 mt-3">
|
|
||||||
{#if isModelDownloaded(settings.modelSize)}
|
|
||||||
<span class="inline-flex items-center gap-1.5 text-[11px] text-success">
|
|
||||||
<span class="w-[6px] h-[6px] rounded-full bg-success"></span>
|
|
||||||
Downloaded
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="text-[11px] text-text-tertiary hover:text-accent"
|
|
||||||
onclick={loadSelectedModel}
|
|
||||||
>Load model</button>
|
|
||||||
{:else if downloadingModel === settings.modelSize.toLowerCase()}
|
|
||||||
<span class="text-[11px] text-warning">{downloadProgress}% downloading...</span>
|
|
||||||
{:else}
|
|
||||||
<button
|
|
||||||
class="text-[11px] text-accent hover:text-accent-hover"
|
|
||||||
onclick={() => downloadModel(settings.modelSize.toLowerCase())}
|
|
||||||
>Download {settings.modelSize}</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="mb-6">
|
|
||||||
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">Parakeet Model</p>
|
|
||||||
<p class="text-[11px] text-text-tertiary mb-3">Parakeet CTC 0.6B (int8) — ~613MB, near-instant transcription</p>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<!-- Format mode -->
|
||||||
{#if parakeetOk}
|
<div class="mb-6">
|
||||||
<span class="inline-flex items-center gap-1.5 text-[11px] text-success">
|
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">Format Mode</p>
|
||||||
<span class="w-[6px] h-[6px] rounded-full bg-success"></span>
|
<SegmentedButton options={["Raw", "Clean", "Smart"]} bind:value={settings.formatMode} />
|
||||||
Model loaded
|
<p class="text-[11px] text-text-tertiary mt-2">
|
||||||
</span>
|
{settings.formatMode === "Raw" ? "Exact Whisper output, no formatting" :
|
||||||
{:else if parakeetDownloaded}
|
settings.formatMode === "Clean" ? "Grouped into paragraphs, punctuation tidied" :
|
||||||
<span class="inline-flex items-center gap-1.5 text-[11px] text-text-secondary">
|
"Structured with lists, headings, and sections"}
|
||||||
<span class="w-[6px] h-[6px] rounded-full bg-text-tertiary"></span>
|
</p>
|
||||||
Downloaded
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="text-[11px] text-text-tertiary hover:text-accent"
|
|
||||||
onclick={loadParakeet}
|
|
||||||
>Load model</button>
|
|
||||||
{:else if parakeetDownloading}
|
|
||||||
<span class="text-[11px] text-warning">{parakeetProgress}% downloading...</span>
|
|
||||||
{:else}
|
|
||||||
<button
|
|
||||||
class="text-[11px] text-accent hover:text-accent-hover"
|
|
||||||
onclick={downloadParakeet}
|
|
||||||
>Download Parakeet</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Compute device -->
|
<!-- Engine-specific model management -->
|
||||||
<div class="mb-6">
|
{#if settings.engine === "whisper"}
|
||||||
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">Compute Device</p>
|
<div class="mb-6">
|
||||||
<select
|
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">Whisper Model</p>
|
||||||
class="bg-bg-input border border-border rounded-lg px-3 py-2 text-[13px] text-text
|
<SegmentedButton options={["Tiny", "Base", "Small", "Medium"]} bind:value={settings.modelSize} />
|
||||||
focus:outline-none focus:border-accent focus:shadow-[0_0_0_3px_rgba(232,168,124,0.1)]
|
<p class="text-[11px] text-text-tertiary mt-2">{modelDescriptions[settings.modelSize]}</p>
|
||||||
appearance-none cursor-pointer w-[220px]"
|
|
||||||
bind:value={settings.device}
|
|
||||||
>
|
|
||||||
<option value="auto">Auto (CPU)</option>
|
|
||||||
<option value="cuda">CUDA (NVIDIA GPU)</option>
|
|
||||||
<option value="cpu">CPU</option>
|
|
||||||
</select>
|
|
||||||
<p class="text-[11px] text-text-tertiary mt-2">GPU acceleration requires building with CUDA feature</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Language -->
|
<div class="flex items-center gap-2 mt-3">
|
||||||
<div>
|
{#if isModelDownloaded(settings.modelSize)}
|
||||||
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">Language</p>
|
<span class="inline-flex items-center gap-1.5 text-[11px] text-success">
|
||||||
<div class="flex items-center gap-3">
|
<span class="w-[6px] h-[6px] rounded-full bg-success"></span>
|
||||||
<select
|
Downloaded
|
||||||
class="bg-bg-input border border-border rounded-lg px-3 py-2 text-[13px] text-text
|
</span>
|
||||||
focus:outline-none focus:border-accent focus:shadow-[0_0_0_3px_rgba(232,168,124,0.1)]
|
<button
|
||||||
appearance-none cursor-pointer w-[140px]"
|
class="text-[11px] text-text-tertiary hover:text-accent"
|
||||||
bind:value={settings.language}
|
onclick={loadSelectedModel}
|
||||||
>
|
>Load model</button>
|
||||||
<option value="en">English</option>
|
{:else if downloadingModel === settings.modelSize.toLowerCase()}
|
||||||
<option value="auto">Auto-detect</option>
|
<span class="text-[11px] text-warning">{downloadProgress}% downloading...</span>
|
||||||
<option value="fr">French</option>
|
{:else}
|
||||||
<option value="de">German</option>
|
<button
|
||||||
<option value="es">Spanish</option>
|
class="text-[11px] text-accent hover:text-accent-hover"
|
||||||
<option value="it">Italian</option>
|
onclick={() => downloadModel(settings.modelSize.toLowerCase())}
|
||||||
<option value="pt">Portuguese</option>
|
>Download {settings.modelSize}</button>
|
||||||
<option value="nl">Dutch</option>
|
{/if}
|
||||||
<option value="pl">Polish</option>
|
</div>
|
||||||
<option value="ja">Japanese</option>
|
</div>
|
||||||
<option value="ko">Korean</option>
|
{:else}
|
||||||
<option value="zh">Chinese</option>
|
<div class="mb-6">
|
||||||
</select>
|
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">Parakeet Model</p>
|
||||||
{#if settings.language === "en"}
|
<p class="text-[11px] text-text-tertiary mb-3">Parakeet CTC 0.6B (int8) — ~613MB, near-instant transcription</p>
|
||||||
<button
|
|
||||||
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-full text-[11px] border animate-fade-in
|
<div class="flex items-center gap-2">
|
||||||
{settings.britishEnglish
|
{#if parakeetOk}
|
||||||
? 'bg-accent/10 border-accent/30 text-accent font-medium'
|
<span class="inline-flex items-center gap-1.5 text-[11px] text-success">
|
||||||
: 'bg-bg-input border-border text-text-tertiary hover:text-text-secondary'}"
|
<span class="w-[6px] h-[6px] rounded-full bg-success"></span>
|
||||||
onclick={() => { settings.britishEnglish = !settings.britishEnglish; }}
|
Model loaded
|
||||||
title={settings.britishEnglish ? "British English spelling active" : "Click to enable British English spelling"}
|
</span>
|
||||||
|
{:else if parakeetDownloaded}
|
||||||
|
<span class="inline-flex items-center gap-1.5 text-[11px] text-text-secondary">
|
||||||
|
<span class="w-[6px] h-[6px] rounded-full bg-text-tertiary"></span>
|
||||||
|
Downloaded
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
class="text-[11px] text-text-tertiary hover:text-accent"
|
||||||
|
onclick={loadParakeet}
|
||||||
|
>Load model</button>
|
||||||
|
{:else if parakeetDownloading}
|
||||||
|
<span class="text-[11px] text-warning">{parakeetProgress}% downloading...</span>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="text-[11px] text-accent hover:text-accent-hover"
|
||||||
|
onclick={downloadParakeet}
|
||||||
|
>Download Parakeet</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Compute device -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">Compute Device</p>
|
||||||
|
<select
|
||||||
|
class="bg-bg-input border border-border rounded-lg px-3 py-2 text-[13px] text-text
|
||||||
|
focus:outline-none focus:border-accent focus:shadow-[0_0_0_3px_rgba(232,168,124,0.1)]
|
||||||
|
appearance-none cursor-pointer w-[220px]"
|
||||||
|
bind:value={settings.device}
|
||||||
>
|
>
|
||||||
{#if settings.britishEnglish}
|
<option value="auto">Auto (CPU)</option>
|
||||||
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
<option value="cuda">CUDA (NVIDIA GPU)</option>
|
||||||
<path d="M5 12l5 5L20 7" stroke-linecap="round" stroke-linejoin="round" />
|
<option value="cpu">CPU</option>
|
||||||
</svg>
|
</select>
|
||||||
{/if}
|
<p class="text-[11px] text-text-tertiary mt-2">GPU acceleration requires building with CUDA feature</p>
|
||||||
British English
|
</div>
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- Processing -->
|
<!-- Language -->
|
||||||
<Card>
|
<div>
|
||||||
<div class="p-5">
|
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">Language</p>
|
||||||
<h3 class="text-[14px] font-semibold text-text mb-4">Processing</h3>
|
<div class="flex items-center gap-3">
|
||||||
<div class="space-y-0.5">
|
<select
|
||||||
<Toggle
|
class="bg-bg-input border border-border rounded-lg px-3 py-2 text-[13px] text-text
|
||||||
bind:checked={settings.removeFillers}
|
focus:outline-none focus:border-accent focus:shadow-[0_0_0_3px_rgba(232,168,124,0.1)]
|
||||||
label="Remove filler words"
|
appearance-none cursor-pointer w-[140px]"
|
||||||
description="Strips um, uh, like, you know from output"
|
bind:value={settings.language}
|
||||||
/>
|
>
|
||||||
<Toggle
|
<option value="en">English</option>
|
||||||
bind:checked={settings.antiHallucination}
|
<option value="auto">Auto-detect</option>
|
||||||
label="Anti-hallucination shield"
|
<option value="fr">French</option>
|
||||||
description="Detects phantom phrases Whisper generates during silence"
|
<option value="de">German</option>
|
||||||
/>
|
<option value="es">Spanish</option>
|
||||||
</div>
|
<option value="it">Italian</option>
|
||||||
</div>
|
<option value="pt">Portuguese</option>
|
||||||
</Card>
|
<option value="nl">Dutch</option>
|
||||||
|
<option value="pl">Polish</option>
|
||||||
<!-- AI Assistant (LLM) -->
|
<option value="ja">Japanese</option>
|
||||||
<Card>
|
<option value="ko">Korean</option>
|
||||||
<div class="p-5">
|
<option value="zh">Chinese</option>
|
||||||
<h3 class="text-[14px] font-semibold text-text mb-1">AI Assistant</h3>
|
</select>
|
||||||
<p class="text-[11px] text-text-tertiary mb-4">Local LLM for smart task extraction, transcript cleanup, and formatting. Runs 100% offline.</p>
|
{#if settings.language === "en"}
|
||||||
<div class="bg-bg-input rounded-lg px-3 py-2.5 border border-border-subtle">
|
|
||||||
<p class="text-[12px] text-text-secondary font-medium mb-1">Coming soon</p>
|
|
||||||
<p class="text-[11px] text-text-tertiary">AI-powered cleanup and smart extraction are being rebuilt with a faster engine. Task extraction currently uses rule-based matching, which runs automatically after each recording.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- Profiles & Templates -->
|
|
||||||
<Card>
|
|
||||||
<div class="p-5">
|
|
||||||
<!-- Profiles section -->
|
|
||||||
<button
|
|
||||||
class="flex items-center gap-2 w-full text-left mb-1"
|
|
||||||
onclick={() => showProfiles = !showProfiles}
|
|
||||||
>
|
|
||||||
<svg class="w-3 h-3 text-text-tertiary transition-transform {showProfiles ? 'rotate-90' : ''}" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M8 5l8 7-8 7z" />
|
|
||||||
</svg>
|
|
||||||
<h3 class="text-[14px] font-semibold text-text">Profiles</h3>
|
|
||||||
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-bg-elevated text-text-tertiary">
|
|
||||||
{profiles.length}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<p class="text-[11px] text-text-tertiary mb-3 ml-5">Custom vocabulary to improve transcription accuracy</p>
|
|
||||||
|
|
||||||
{#if showProfiles}
|
|
||||||
<div class="space-y-1.5 animate-fade-in ml-5">
|
|
||||||
{#each profiles as profile, i}
|
|
||||||
<div class="group">
|
|
||||||
<div class="flex items-center gap-2 bg-bg-input rounded-lg px-3 h-[36px]">
|
|
||||||
<span class="text-[12px] text-text flex-1 truncate">{profile.name}</span>
|
|
||||||
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-bg-elevated text-text-tertiary">
|
|
||||||
{profileWordCount(profile.words)} words
|
|
||||||
</span>
|
|
||||||
<button
|
<button
|
||||||
class="text-[10px] text-text-tertiary hover:text-accent opacity-0 group-hover:opacity-100"
|
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-full text-[11px] border animate-fade-in
|
||||||
onclick={() => editingProfile = editingProfile === i ? -1 : i}
|
{settings.britishEnglish
|
||||||
>{editingProfile === i ? "Close" : "Edit"}</button>
|
? 'bg-accent/10 border-accent/30 text-accent font-medium'
|
||||||
<button
|
: 'bg-bg-input border-border text-text-tertiary hover:text-text-secondary'}"
|
||||||
class="text-[10px] text-text-tertiary hover:text-danger opacity-0 group-hover:opacity-100"
|
onclick={() => { settings.britishEnglish = !settings.britishEnglish; }}
|
||||||
onclick={() => deleteProfile(i)}
|
title={settings.britishEnglish ? "British English spelling active" : "Click to enable British English spelling"}
|
||||||
>Delete</button>
|
>
|
||||||
</div>
|
{#if settings.britishEnglish}
|
||||||
{#if editingProfile === i}
|
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||||
<div class="mt-1.5 animate-fade-in">
|
<path d="M5 12l5 5L20 7" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
<textarea
|
</svg>
|
||||||
class="w-full h-[120px] bg-bg-elevated border border-border-subtle rounded-lg px-3 py-2
|
{/if}
|
||||||
text-[12px] text-text leading-relaxed resize-none
|
British English
|
||||||
focus:outline-none focus:border-accent"
|
</button>
|
||||||
placeholder="One word or phrase per line..."
|
|
||||||
bind:value={profile.words}
|
|
||||||
oninput={() => saveProfiles()}
|
|
||||||
data-no-transition
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
{#if showNewProfile}
|
|
||||||
<div class="flex items-center gap-2 animate-fade-in">
|
|
||||||
<input
|
|
||||||
class="flex-1 bg-bg-input border border-border rounded-lg px-3 py-1.5 text-[12px] text-text
|
|
||||||
placeholder:text-text-tertiary focus:outline-none focus:border-accent"
|
|
||||||
placeholder="Profile name..."
|
|
||||||
bind:value={newProfileName}
|
|
||||||
onkeydown={(e) => e.key === "Enter" && createProfile()}
|
|
||||||
data-no-transition
|
|
||||||
/>
|
|
||||||
<button class="text-[11px] text-accent hover:text-accent-hover" onclick={createProfile}>Create</button>
|
|
||||||
<button class="text-[11px] text-text-tertiary" onclick={() => { showNewProfile = false; newProfileName = ""; }}>Cancel</button>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<button class="text-[12px] text-accent hover:text-accent-hover" onclick={() => showNewProfile = true}>+ Add profile</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="my-4 h-px bg-border-subtle"></div>
|
|
||||||
|
|
||||||
<!-- Templates section -->
|
|
||||||
<button
|
|
||||||
class="flex items-center gap-2 w-full text-left mb-1"
|
|
||||||
onclick={() => showTemplates = !showTemplates}
|
|
||||||
>
|
|
||||||
<svg class="w-3 h-3 text-text-tertiary transition-transform {showTemplates ? 'rotate-90' : ''}" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M8 5l8 7-8 7z" />
|
|
||||||
</svg>
|
|
||||||
<h3 class="text-[14px] font-semibold text-text">Templates</h3>
|
|
||||||
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-bg-elevated text-text-tertiary">
|
|
||||||
{templates.length}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<p class="text-[11px] text-text-tertiary mb-3 ml-5">Structured formats for dictation sessions</p>
|
|
||||||
|
|
||||||
{#if showTemplates}
|
|
||||||
<div class="space-y-1.5 animate-fade-in ml-5">
|
|
||||||
{#each templates as template, i}
|
|
||||||
<div class="group">
|
|
||||||
<div class="flex items-center gap-2 bg-bg-input rounded-lg px-3 h-[36px]">
|
|
||||||
<span class="text-[12px] text-text flex-1 truncate">{template.name}</span>
|
|
||||||
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-bg-elevated text-text-tertiary">
|
|
||||||
{template.sections.length} sections
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="text-[10px] text-text-tertiary hover:text-accent opacity-0 group-hover:opacity-100"
|
|
||||||
onclick={() => editingTemplate = editingTemplate === i ? -1 : i}
|
|
||||||
>{editingTemplate === i ? "Close" : "Edit"}</button>
|
|
||||||
<button
|
|
||||||
class="text-[10px] text-text-tertiary hover:text-danger opacity-0 group-hover:opacity-100"
|
|
||||||
onclick={() => deleteTemplate(i)}
|
|
||||||
>Delete</button>
|
|
||||||
</div>
|
|
||||||
{#if editingTemplate === i}
|
|
||||||
<div class="mt-1.5 animate-fade-in">
|
|
||||||
<textarea
|
|
||||||
class="w-full h-[100px] bg-bg-elevated border border-border-subtle rounded-lg px-3 py-2
|
|
||||||
text-[12px] text-text leading-relaxed resize-none
|
|
||||||
focus:outline-none focus:border-accent"
|
|
||||||
placeholder="One section per line..."
|
|
||||||
value={template.sections.join("\n")}
|
|
||||||
oninput={(e) => { template.sections = e.target.value.split("\n").filter((s) => s.trim()); saveTemplates(); }}
|
|
||||||
data-no-transition
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
{#if showNewTemplate}
|
|
||||||
<div class="flex items-center gap-2 animate-fade-in">
|
|
||||||
<input
|
|
||||||
class="flex-1 bg-bg-input border border-border rounded-lg px-3 py-1.5 text-[12px] text-text
|
|
||||||
placeholder:text-text-tertiary focus:outline-none focus:border-accent"
|
|
||||||
placeholder="Template name..."
|
|
||||||
bind:value={newTemplateName}
|
|
||||||
onkeydown={(e) => e.key === "Enter" && createTemplate()}
|
|
||||||
data-no-transition
|
|
||||||
/>
|
|
||||||
<button class="text-[11px] text-accent hover:text-accent-hover" onclick={createTemplate}>Create</button>
|
|
||||||
<button class="text-[11px] text-text-tertiary" onclick={() => { showNewTemplate = false; newTemplateName = ""; }}>Cancel</button>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<button class="text-[12px] text-accent hover:text-accent-hover" onclick={() => showNewTemplate = true}>+ Add template</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- Output -->
|
|
||||||
<Card>
|
|
||||||
<div class="p-5">
|
|
||||||
<h3 class="text-[14px] font-semibold text-text mb-4">Output</h3>
|
|
||||||
<div class="space-y-0.5">
|
|
||||||
<Toggle bind:checked={settings.autoCopy} label="Auto-copy to clipboard" />
|
|
||||||
<Toggle bind:checked={settings.includeTimestamps} label="Include timestamps in exports" />
|
|
||||||
<Toggle
|
|
||||||
bind:checked={settings.saveAudio}
|
|
||||||
label="Save audio recordings"
|
|
||||||
description="Saves raw audio as .wav files (~2MB per minute). Stored locally."
|
|
||||||
/>
|
|
||||||
{#if settings.saveAudio}
|
|
||||||
<div class="ml-[50px] mt-2 animate-fade-in">
|
|
||||||
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-1.5">Output Folder</p>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="flex-1 bg-bg-input rounded-lg px-3 py-1.5 border border-border-subtle">
|
|
||||||
<p class="text-[11px] text-text-secondary truncate">
|
|
||||||
{settings.outputFolder || "Default (app data)"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="text-[11px] text-accent hover:text-accent-hover whitespace-nowrap"
|
|
||||||
onclick={async () => {
|
|
||||||
try {
|
|
||||||
const folder = await open({ directory: true, title: "Select output folder" });
|
|
||||||
if (folder) { settings.outputFolder = folder; saveSettings(); }
|
|
||||||
} catch {}
|
|
||||||
}}
|
|
||||||
>Change</button>
|
|
||||||
{#if settings.outputFolder}
|
|
||||||
<button
|
|
||||||
class="text-[11px] text-text-tertiary hover:text-text-secondary whitespace-nowrap"
|
|
||||||
onclick={() => { settings.outputFolder = ""; saveSettings(); }}
|
|
||||||
>Reset</button>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- Hotkey -->
|
<!-- Processing -->
|
||||||
<Card>
|
<div class="border-b border-border-subtle">
|
||||||
<div class="p-5">
|
<button
|
||||||
<h3 class="text-[14px] font-semibold text-text mb-1">Global Hotkey</h3>
|
class="flex items-center justify-between w-full py-4 px-5 text-left"
|
||||||
<p class="text-[11px] text-text-tertiary mb-4">Toggle recording from anywhere. Click to change.</p>
|
onclick={() => openSection = openSection === 'processing' ? null : 'processing'}
|
||||||
<HotkeyRecorder />
|
>
|
||||||
|
<h3 class="font-display text-[18px] italic text-text">Processing</h3>
|
||||||
|
<span class="text-text-tertiary text-[16px] leading-none">{openSection === 'processing' ? '−' : '+'}</span>
|
||||||
|
</button>
|
||||||
|
{#if openSection === 'processing'}
|
||||||
|
<div class="px-5 pb-5 animate-fade-in">
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<Toggle
|
||||||
|
bind:checked={settings.removeFillers}
|
||||||
|
label="Remove filler words"
|
||||||
|
description="Strips um, uh, like, you know from output"
|
||||||
|
/>
|
||||||
|
<Toggle
|
||||||
|
bind:checked={settings.antiHallucination}
|
||||||
|
label="Anti-hallucination shield"
|
||||||
|
description="Detects phantom phrases Whisper generates during silence"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- Appearance -->
|
<!-- AI Assistant -->
|
||||||
<Card>
|
<div class="border-b border-border-subtle">
|
||||||
<div class="p-5">
|
<button
|
||||||
<h3 class="text-[14px] font-semibold text-text mb-5">Appearance</h3>
|
class="flex items-center justify-between w-full py-4 px-5 text-left"
|
||||||
|
onclick={() => openSection = openSection === 'ai' ? null : 'ai'}
|
||||||
<div class="mb-6">
|
>
|
||||||
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">Theme</p>
|
<h3 class="font-display text-[18px] italic text-text">AI Assistant</h3>
|
||||||
<SegmentedButton options={["Dark", "Light", "System"]} bind:value={settings.theme} />
|
<span class="text-text-tertiary text-[16px] leading-none">{openSection === 'ai' ? '−' : '+'}</span>
|
||||||
</div>
|
</button>
|
||||||
|
{#if openSection === 'ai'}
|
||||||
<div class="mb-6">
|
<div class="px-5 pb-5 animate-fade-in">
|
||||||
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">
|
<p class="text-[11px] text-text-tertiary mb-4">Local LLM for smart task extraction, transcript cleanup, and formatting. Runs 100% offline.</p>
|
||||||
Font Size <span class="font-normal text-text-secondary ml-1">{settings.fontSize}px</span>
|
<div class="bg-bg-input rounded-lg px-3 py-2.5 border border-border-subtle">
|
||||||
</p>
|
<p class="text-[12px] text-text-secondary font-medium mb-1">Coming soon</p>
|
||||||
<input
|
<p class="text-[11px] text-text-tertiary">AI-powered cleanup and smart extraction are being rebuilt with a faster engine. Task extraction currently uses rule-based matching, which runs automatically after each recording.</p>
|
||||||
type="range" min="10" max="24" step="1"
|
</div>
|
||||||
bind:value={settings.fontSize}
|
</div>
|
||||||
class="w-[200px] accent-accent"
|
{/if}
|
||||||
data-no-transition
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- About -->
|
<!-- Profiles & Templates -->
|
||||||
<Card>
|
<div class="border-b border-border-subtle">
|
||||||
<div class="p-5">
|
<button
|
||||||
<h3 class="text-[14px] font-semibold text-text mb-4">About</h3>
|
class="flex items-center justify-between w-full py-4 px-5 text-left"
|
||||||
|
onclick={() => openSection = openSection === 'profiles' ? null : 'profiles'}
|
||||||
<!-- Engine status -->
|
>
|
||||||
<div class="flex items-center gap-2 mb-4">
|
<h3 class="font-display text-[18px] italic text-text">Profiles & Templates</h3>
|
||||||
<span class="w-[7px] h-[7px] rounded-full {engineOk ? 'bg-success' : 'bg-warning'}"></span>
|
<span class="text-text-tertiary text-[16px] leading-none">{openSection === 'profiles' ? '−' : '+'}</span>
|
||||||
<span class="text-[12px] text-text-secondary">{engineStatus}</span>
|
</button>
|
||||||
</div>
|
{#if openSection === 'profiles'}
|
||||||
|
<div class="px-5 pb-5 animate-fade-in">
|
||||||
<div class="space-y-1.5">
|
<!-- Profiles section -->
|
||||||
{#each [
|
<button
|
||||||
"100% offline — all processing on your machine",
|
class="flex items-center gap-2 w-full text-left mb-1"
|
||||||
"No Python required — compiled Whisper engine",
|
onclick={() => showProfiles = !showProfiles}
|
||||||
"No cloud — audio never leaves your computer",
|
>
|
||||||
"No accounts — no sign-up, no tracking",
|
<svg class="w-3 h-3 text-text-tertiary transition-transform {showProfiles ? 'rotate-90' : ''}" viewBox="0 0 24 24" fill="currentColor">
|
||||||
"No telemetry — zero data collection",
|
<path d="M8 5l8 7-8 7z" />
|
||||||
] as item}
|
|
||||||
<div class="flex items-start gap-2">
|
|
||||||
<svg class="w-3.5 h-3.5 text-success mt-0.5 flex-shrink-0" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17Z" />
|
|
||||||
</svg>
|
</svg>
|
||||||
<p class="text-[11px] text-text-secondary">{item}</p>
|
<h3 class="text-[14px] font-semibold text-text">Profiles</h3>
|
||||||
</div>
|
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-bg-elevated text-text-tertiary">
|
||||||
{/each}
|
{profiles.length}
|
||||||
</div>
|
</span>
|
||||||
|
</button>
|
||||||
|
<p class="text-[11px] text-text-tertiary mb-3 ml-5">Custom vocabulary to improve transcription accuracy</p>
|
||||||
|
|
||||||
<p class="text-[11px] text-text-tertiary mt-5">Kon v1.0 · Powered by whisper.cpp · Built by CORBEL Ltd</p>
|
{#if showProfiles}
|
||||||
|
<div class="space-y-1.5 animate-fade-in ml-5">
|
||||||
|
{#each profiles as profile, i}
|
||||||
|
<div class="group">
|
||||||
|
<div class="flex items-center gap-2 bg-bg-input rounded-lg px-3 h-[36px]">
|
||||||
|
<span class="text-[12px] text-text flex-1 truncate">{profile.name}</span>
|
||||||
|
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-bg-elevated text-text-tertiary">
|
||||||
|
{profileWordCount(profile.words)} words
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
class="text-[10px] text-text-tertiary hover:text-accent opacity-0 group-hover:opacity-100"
|
||||||
|
onclick={() => editingProfile = editingProfile === i ? -1 : i}
|
||||||
|
>{editingProfile === i ? "Close" : "Edit"}</button>
|
||||||
|
<button
|
||||||
|
class="text-[10px] text-text-tertiary hover:text-danger opacity-0 group-hover:opacity-100"
|
||||||
|
onclick={() => deleteProfile(i)}
|
||||||
|
>Delete</button>
|
||||||
|
</div>
|
||||||
|
{#if editingProfile === i}
|
||||||
|
<div class="mt-1.5 animate-fade-in">
|
||||||
|
<textarea
|
||||||
|
class="w-full h-[120px] bg-bg-elevated border border-border-subtle rounded-lg px-3 py-2
|
||||||
|
text-[12px] text-text leading-relaxed resize-none
|
||||||
|
focus:outline-none focus:border-accent"
|
||||||
|
placeholder="One word or phrase per line..."
|
||||||
|
bind:value={profile.words}
|
||||||
|
oninput={() => saveProfiles()}
|
||||||
|
data-no-transition
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if showNewProfile}
|
||||||
|
<div class="flex items-center gap-2 animate-fade-in">
|
||||||
|
<input
|
||||||
|
class="flex-1 bg-bg-input border border-border rounded-lg px-3 py-1.5 text-[12px] text-text
|
||||||
|
placeholder:text-text-tertiary focus:outline-none focus:border-accent"
|
||||||
|
placeholder="Profile name..."
|
||||||
|
bind:value={newProfileName}
|
||||||
|
onkeydown={(e) => e.key === "Enter" && createProfile()}
|
||||||
|
data-no-transition
|
||||||
|
/>
|
||||||
|
<button class="text-[11px] text-accent hover:text-accent-hover" onclick={createProfile}>Create</button>
|
||||||
|
<button class="text-[11px] text-text-tertiary" onclick={() => { showNewProfile = false; newProfileName = ""; }}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button class="text-[12px] text-accent hover:text-accent-hover" onclick={() => showNewProfile = true}>+ Add profile</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="my-4 h-px bg-border-subtle"></div>
|
||||||
|
|
||||||
|
<!-- Templates section -->
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2 w-full text-left mb-1"
|
||||||
|
onclick={() => showTemplates = !showTemplates}
|
||||||
|
>
|
||||||
|
<svg class="w-3 h-3 text-text-tertiary transition-transform {showTemplates ? 'rotate-90' : ''}" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M8 5l8 7-8 7z" />
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-[14px] font-semibold text-text">Templates</h3>
|
||||||
|
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-bg-elevated text-text-tertiary">
|
||||||
|
{templates.length}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<p class="text-[11px] text-text-tertiary mb-3 ml-5">Structured formats for dictation sessions</p>
|
||||||
|
|
||||||
|
{#if showTemplates}
|
||||||
|
<div class="space-y-1.5 animate-fade-in ml-5">
|
||||||
|
{#each templates as template, i}
|
||||||
|
<div class="group">
|
||||||
|
<div class="flex items-center gap-2 bg-bg-input rounded-lg px-3 h-[36px]">
|
||||||
|
<span class="text-[12px] text-text flex-1 truncate">{template.name}</span>
|
||||||
|
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-bg-elevated text-text-tertiary">
|
||||||
|
{template.sections.length} sections
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
class="text-[10px] text-text-tertiary hover:text-accent opacity-0 group-hover:opacity-100"
|
||||||
|
onclick={() => editingTemplate = editingTemplate === i ? -1 : i}
|
||||||
|
>{editingTemplate === i ? "Close" : "Edit"}</button>
|
||||||
|
<button
|
||||||
|
class="text-[10px] text-text-tertiary hover:text-danger opacity-0 group-hover:opacity-100"
|
||||||
|
onclick={() => deleteTemplate(i)}
|
||||||
|
>Delete</button>
|
||||||
|
</div>
|
||||||
|
{#if editingTemplate === i}
|
||||||
|
<div class="mt-1.5 animate-fade-in">
|
||||||
|
<textarea
|
||||||
|
class="w-full h-[100px] bg-bg-elevated border border-border-subtle rounded-lg px-3 py-2
|
||||||
|
text-[12px] text-text leading-relaxed resize-none
|
||||||
|
focus:outline-none focus:border-accent"
|
||||||
|
placeholder="One section per line..."
|
||||||
|
value={template.sections.join("\n")}
|
||||||
|
oninput={(e) => { template.sections = e.target.value.split("\n").filter((s) => s.trim()); saveTemplates(); }}
|
||||||
|
data-no-transition
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if showNewTemplate}
|
||||||
|
<div class="flex items-center gap-2 animate-fade-in">
|
||||||
|
<input
|
||||||
|
class="flex-1 bg-bg-input border border-border rounded-lg px-3 py-1.5 text-[12px] text-text
|
||||||
|
placeholder:text-text-tertiary focus:outline-none focus:border-accent"
|
||||||
|
placeholder="Template name..."
|
||||||
|
bind:value={newTemplateName}
|
||||||
|
onkeydown={(e) => e.key === "Enter" && createTemplate()}
|
||||||
|
data-no-transition
|
||||||
|
/>
|
||||||
|
<button class="text-[11px] text-accent hover:text-accent-hover" onclick={createTemplate}>Create</button>
|
||||||
|
<button class="text-[11px] text-text-tertiary" onclick={() => { showNewTemplate = false; newTemplateName = ""; }}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button class="text-[12px] text-accent hover:text-accent-hover" onclick={() => showNewTemplate = true}>+ Add template</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Output -->
|
||||||
|
<div class="border-b border-border-subtle">
|
||||||
|
<button
|
||||||
|
class="flex items-center justify-between w-full py-4 px-5 text-left"
|
||||||
|
onclick={() => openSection = openSection === 'output' ? null : 'output'}
|
||||||
|
>
|
||||||
|
<h3 class="font-display text-[18px] italic text-text">Output</h3>
|
||||||
|
<span class="text-text-tertiary text-[16px] leading-none">{openSection === 'output' ? '−' : '+'}</span>
|
||||||
|
</button>
|
||||||
|
{#if openSection === 'output'}
|
||||||
|
<div class="px-5 pb-5 animate-fade-in">
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<Toggle bind:checked={settings.autoCopy} label="Auto-copy to clipboard" />
|
||||||
|
<Toggle bind:checked={settings.includeTimestamps} label="Include timestamps in exports" />
|
||||||
|
<Toggle
|
||||||
|
bind:checked={settings.saveAudio}
|
||||||
|
label="Save audio recordings"
|
||||||
|
description="Saves raw audio as .wav files (~2MB per minute). Stored locally."
|
||||||
|
/>
|
||||||
|
{#if settings.saveAudio}
|
||||||
|
<div class="ml-[50px] mt-2 animate-fade-in">
|
||||||
|
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-1.5">Output Folder</p>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="flex-1 bg-bg-input rounded-lg px-3 py-1.5 border border-border-subtle">
|
||||||
|
<p class="text-[11px] text-text-secondary truncate">
|
||||||
|
{settings.outputFolder || "Default (app data)"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="text-[11px] text-accent hover:text-accent-hover whitespace-nowrap"
|
||||||
|
onclick={async () => {
|
||||||
|
try {
|
||||||
|
const folder = await open({ directory: true, title: "Select output folder" });
|
||||||
|
if (folder) { settings.outputFolder = folder; saveSettings(); }
|
||||||
|
} catch {}
|
||||||
|
}}
|
||||||
|
>Change</button>
|
||||||
|
{#if settings.outputFolder}
|
||||||
|
<button
|
||||||
|
class="text-[11px] text-text-tertiary hover:text-text-secondary whitespace-nowrap"
|
||||||
|
onclick={() => { settings.outputFolder = ""; saveSettings(); }}
|
||||||
|
>Reset</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hotkey -->
|
||||||
|
<div class="border-b border-border-subtle">
|
||||||
|
<button
|
||||||
|
class="flex items-center justify-between w-full py-4 px-5 text-left"
|
||||||
|
onclick={() => openSection = openSection === 'hotkey' ? null : 'hotkey'}
|
||||||
|
>
|
||||||
|
<h3 class="font-display text-[18px] italic text-text">Global Hotkey</h3>
|
||||||
|
<span class="text-text-tertiary text-[16px] leading-none">{openSection === 'hotkey' ? '−' : '+'}</span>
|
||||||
|
</button>
|
||||||
|
{#if openSection === 'hotkey'}
|
||||||
|
<div class="px-5 pb-5 animate-fade-in">
|
||||||
|
<p class="text-[11px] text-text-tertiary mb-4">Toggle recording from anywhere. Click to change.</p>
|
||||||
|
<HotkeyRecorder />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Appearance -->
|
||||||
|
<div class="border-b border-border-subtle">
|
||||||
|
<button
|
||||||
|
class="flex items-center justify-between w-full py-4 px-5 text-left"
|
||||||
|
onclick={() => openSection = openSection === 'appearance' ? null : 'appearance'}
|
||||||
|
>
|
||||||
|
<h3 class="font-display text-[18px] italic text-text">Appearance</h3>
|
||||||
|
<span class="text-text-tertiary text-[16px] leading-none">{openSection === 'appearance' ? '−' : '+'}</span>
|
||||||
|
</button>
|
||||||
|
{#if openSection === 'appearance'}
|
||||||
|
<div class="px-5 pb-5 animate-fade-in">
|
||||||
|
<div class="mb-6">
|
||||||
|
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">Theme</p>
|
||||||
|
<SegmentedButton options={["Dark", "Light", "System"]} bind:value={settings.theme} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">
|
||||||
|
Font Size <span class="font-normal text-text-secondary ml-1">{settings.fontSize}px</span>
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="range" min="10" max="24" step="1"
|
||||||
|
bind:value={settings.fontSize}
|
||||||
|
class="w-[200px] accent-accent"
|
||||||
|
data-no-transition
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- About -->
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="flex items-center justify-between w-full py-4 px-5 text-left"
|
||||||
|
onclick={() => openSection = openSection === 'about' ? null : 'about'}
|
||||||
|
>
|
||||||
|
<h3 class="font-display text-[18px] italic text-text">About</h3>
|
||||||
|
<span class="text-text-tertiary text-[16px] leading-none">{openSection === 'about' ? '−' : '+'}</span>
|
||||||
|
</button>
|
||||||
|
{#if openSection === 'about'}
|
||||||
|
<div class="px-5 pb-5 animate-fade-in">
|
||||||
|
<!-- Engine status -->
|
||||||
|
<div class="flex items-center gap-2 mb-4">
|
||||||
|
<span class="w-[7px] h-[7px] rounded-full {engineOk ? 'bg-success' : 'bg-warning'}"></span>
|
||||||
|
<span class="text-[12px] text-text-secondary">{engineStatus}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
{#each [
|
||||||
|
"100% offline — all processing on your machine",
|
||||||
|
"No Python required — compiled Whisper engine",
|
||||||
|
"No cloud — audio never leaves your computer",
|
||||||
|
"No accounts — no sign-up, no tracking",
|
||||||
|
"No telemetry — zero data collection",
|
||||||
|
] as item}
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
<svg class="w-3.5 h-3.5 text-success mt-0.5 flex-shrink-0" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17Z" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-[11px] text-text-secondary">{item}</p>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-[11px] text-text-tertiary mt-5">Kon v1.0 · Powered by whisper.cpp · Built by CORBEL Ltd</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user