feat(kon): scaffold hybrid modular workspace

- Cargo workspace with 6 domain crates: core, audio, transcription, ai-formatting, storage, cloud-providers
- Minimal Tauri shell (lib.rs + main.rs) with plugin registration
- Svelte 5 frontend copied from Ramble v0.2
- All crates compile as empty stubs
- App identifier: uk.co.corbel.kon

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jake
2026-03-16 20:21:38 +00:00
commit 9926a42b7a
80 changed files with 13328 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
<script>
import { onMount, onDestroy } from "svelte";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import Card from "$lib/components/Card.svelte";
let { modelSize = "base", onComplete = () => {} } = $props();
let progress = $state(0);
let downloading = $state(false);
let downloaded = $state(0);
let total = $state(0);
let error = $state("");
let unlisten = null;
const modelInfo = {
tiny: { size: "~75 MB", accuracy: "Basic" },
base: { size: "~142 MB", accuracy: "Good" },
small: { size: "~466 MB", accuracy: "Better" },
medium: { size: "~1.5 GB", accuracy: "Best" },
};
onMount(async () => {
unlisten = await listen("model-download-progress", (event) => {
const data = event.payload;
progress = data.progress;
downloaded = data.downloaded;
total = data.total;
});
});
onDestroy(() => {
if (unlisten) unlisten();
});
async function startDownload() {
downloading = true;
error = "";
progress = 0;
try {
await invoke("download_model", { size: modelSize });
onComplete();
} catch (err) {
error = typeof err === "string" ? err : err.message || "Download failed";
downloading = false;
}
}
function formatBytes(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1073741824) return `${(bytes / 1048576).toFixed(1)} MB`;
return `${(bytes / 1073741824).toFixed(2)} GB`;
}
</script>
<div class="flex flex-col items-center justify-center h-full px-8 animate-fade-in">
<Card classes="max-w-md w-full">
<div class="p-8 text-center">
<div class="w-16 h-16 rounded-full bg-accent/10 flex items-center justify-center mx-auto mb-5">
<svg class="w-8 h-8 text-accent" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" />
</svg>
</div>
<h3 class="text-[18px] font-semibold text-text mb-2">Download Whisper Model</h3>
<p class="text-[13px] text-text-secondary mb-1">
Ramble needs the <span class="font-medium text-text">{modelSize}</span> model to transcribe speech.
</p>
<p class="text-[11px] text-text-tertiary mb-6">
{modelInfo[modelSize]?.size ?? "?"} · {modelInfo[modelSize]?.accuracy ?? "?"} accuracy · 100% offline
</p>
{#if downloading}
<div class="mb-4">
<div class="h-[6px] bg-bg-elevated rounded-full overflow-hidden mb-2">
<div
class="h-full bg-accent rounded-full transition-all duration-300 shadow-[0_0_8px_rgba(232,168,124,0.4)]"
style="width: {progress}%"
></div>
</div>
<p class="text-[12px] text-text-secondary">
{progress}% · {formatBytes(downloaded)} / {formatBytes(total)}
</p>
</div>
{:else}
<button
class="px-6 py-2.5 rounded-xl text-[14px] font-medium text-white bg-accent hover:bg-accent-hover
shadow-[0_4px_16px_rgba(232,168,124,0.3)] active:scale-[0.97] transition-all duration-150"
onclick={startDownload}
>
Download Model
</button>
{/if}
{#if error}
<p class="text-[12px] text-danger mt-3">{error}</p>
{/if}
<p class="text-[10px] text-text-tertiary mt-5">
Models are cached locally. No data leaves your machine.
</p>
</div>
</Card>
</div>