fix(kon): task sidebar overlay, Parakeet model management, conditional engine settings

TaskSidebar:
- Changed from flex layout member to fixed overlay with backdrop
- Click backdrop or press Escape to close
- Content no longer shrinks when sidebar opens — buttons stay accessible
- Positioned at z-50 over main content, top offset for titlebar

Settings — Parakeet model management:
- Added Parakeet model state: download, check, load status
- New downloadParakeet() and loadParakeet() functions
- Listens for parakeet-download-progress events
- Parakeet section shows download/load/status controls

Settings — conditional engine sections:
- Whisper model selector only shows when engine === "whisper"
- Parakeet model controls only show when engine === "parakeet"
- Cleaner UI — only active engine's options visible

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jake
2026-03-16 23:26:27 +00:00
parent 1ad2b55d9c
commit fe3739fdbb
2 changed files with 122 additions and 31 deletions

View File

@@ -16,6 +16,14 @@
let downloadProgress = $state(0);
let unlisten = null;
// Parakeet state
let parakeetStatus = $state("Checking...");
let parakeetOk = $state(false);
let parakeetDownloaded = $state(false);
let parakeetDownloading = $state(false);
let parakeetProgress = $state(0);
let unlistenParakeet = null;
// Profiles & Templates
let showProfiles = $state(false);
let showTemplates = $state(false);
@@ -39,13 +47,27 @@
downloadedModels = await invoke("list_models");
} catch {}
// Parakeet status
try {
parakeetOk = await invoke("check_parakeet_engine");
parakeetDownloaded = await invoke("check_parakeet_model", { name: "ctc-int8" });
parakeetStatus = parakeetOk ? "Model loaded" : parakeetDownloaded ? "Downloaded (not loaded)" : "Not downloaded";
} catch {
parakeetStatus = "Engine not ready";
}
unlisten = await listen("model-download-progress", (event) => {
downloadProgress = event.payload.progress;
downloadProgress = event.payload.percent || event.payload.progress || 0;
});
unlistenParakeet = await listen("parakeet-download-progress", (event) => {
parakeetProgress = event.payload.percent || event.payload.progress || 0;
});
});
onDestroy(() => {
if (unlisten) unlisten();
if (unlistenParakeet) unlistenParakeet();
});
// Auto-save on any change
@@ -105,6 +127,33 @@
Medium: "~1.5GB · best quality, slower",
};
async function downloadParakeet() {
parakeetDownloading = true;
parakeetProgress = 0;
try {
await invoke("download_parakeet_model", { name: "ctc-int8" });
parakeetDownloaded = true;
parakeetDownloading = false;
parakeetStatus = "Downloaded (not loaded)";
} catch {
parakeetDownloading = false;
parakeetStatus = "Download failed";
}
}
async function loadParakeet() {
parakeetStatus = "Loading...";
parakeetOk = false;
try {
await invoke("load_parakeet_model", { name: "ctc-int8" });
parakeetOk = true;
parakeetStatus = "Model loaded";
} catch (err) {
parakeetOk = false;
parakeetStatus = typeof err === "string" ? err : "Load failed";
}
}
// --- Profile management ---
function createProfile() {
if (!newProfileName.trim()) return;
@@ -180,13 +229,13 @@
</p>
</div>
<!-- Whisper model -->
<!-- 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>
<!-- Model status per size -->
<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">
@@ -207,6 +256,37 @@
{/if}
</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">
{#if parakeetOk}
<span class="inline-flex items-center gap-1.5 text-[11px] text-success">
<span class="w-[6px] h-[6px] rounded-full bg-success"></span>
Model loaded
</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">

View File

@@ -98,7 +98,7 @@
});
</script>
<svelte:window onmousemove={handleMouseMove} />
<svelte:window onmousemove={handleMouseMove} onkeydown={(e) => { if (e.key === "Escape" && page.taskSidebarOpen) page.taskSidebarOpen = false; }} />
{#if page.recording}
<div
@@ -113,16 +113,27 @@
{:else}
<div class="flex flex-col h-screen w-screen overflow-hidden grain">
<Titlebar />
<div class="flex flex-1 min-h-0">
<div class="flex flex-1 min-h-0 relative">
{#if page.current !== "first-run"}
<Sidebar />
{/if}
<div class="flex-1 overflow-hidden bg-bg">
{@render children()}
</div>
{#if page.taskSidebarOpen && page.current !== "first-run"}
</div>
</div>
<!-- Task sidebar as overlay — doesn't shrink main content -->
{#if page.taskSidebarOpen && page.current !== "first-run" && !isSecondaryWindow}
<!-- Backdrop -->
<button
class="fixed inset-0 bg-black/20 z-40"
onclick={() => page.taskSidebarOpen = false}
aria-label="Close task sidebar"
></button>
<!-- Sidebar panel -->
<div class="fixed top-[36px] right-0 bottom-0 z-50 animate-slide-in-right">
<TaskSidebar />
{/if}
</div>
</div>
{/if}
{/if}