On first launch, the app now prompts users to download the Python sidecar (CPU or CUDA variant) from Gitea releases, matching the voice-to-notes pattern. On subsequent launches, it auto-launches the sidecar and connects. New Rust module (src-tauri/src/sidecar/): - download_sidecar: streams download with progress events, extracts zip - check_sidecar: verifies installed sidecar binary exists - check_sidecar_update: compares local vs latest release version - SidecarManager: launches binary, waits for ready JSON, manages lifecycle - Dev mode: runs `python -m backend.main_headless` directly - start_sidecar/stop_sidecar/get_sidecar_port: Tauri commands New Svelte component (SidecarSetup.svelte): - First-time setup overlay with CPU/CUDA variant selection - Download progress bar with byte counter - Error state with retry, success state with auto-continue Updated App.svelte state machine: - checking -> needs_setup -> starting -> connected - Falls back to direct connection in browser dev mode Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
385 lines
8.3 KiB
Svelte
385 lines
8.3 KiB
Svelte
<script lang="ts">
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { listen } from "@tauri-apps/api/event";
|
|
import { onMount } from "svelte";
|
|
|
|
interface Props {
|
|
onComplete: () => void;
|
|
}
|
|
|
|
let { onComplete }: Props = $props();
|
|
|
|
type SetupState = "choose" | "downloading" | "error" | "success";
|
|
|
|
let setupState = $state<SetupState>("choose");
|
|
let variant = $state<"cpu" | "cuda">("cpu");
|
|
let progress = $state(0);
|
|
let progressMessage = $state("");
|
|
let errorMessage = $state("");
|
|
|
|
let unlisten: (() => void) | null = null;
|
|
|
|
onMount(() => {
|
|
return () => {
|
|
if (unlisten) {
|
|
unlisten();
|
|
unlisten = null;
|
|
}
|
|
};
|
|
});
|
|
|
|
async function startDownload() {
|
|
setupState = "downloading";
|
|
progress = 0;
|
|
progressMessage = "Starting download...";
|
|
errorMessage = "";
|
|
|
|
try {
|
|
// Listen for progress events from the Tauri backend
|
|
unlisten = await listen<{ progress: number; message: string }>(
|
|
"sidecar-download-progress",
|
|
(event) => {
|
|
progress = event.payload.progress;
|
|
progressMessage = event.payload.message;
|
|
}
|
|
);
|
|
|
|
await invoke("download_sidecar", { variant });
|
|
|
|
// Download complete
|
|
setupState = "success";
|
|
if (unlisten) {
|
|
unlisten();
|
|
unlisten = null;
|
|
}
|
|
|
|
// Brief pause to show success, then proceed
|
|
setTimeout(() => {
|
|
onComplete();
|
|
}, 1500);
|
|
} catch (err) {
|
|
setupState = "error";
|
|
errorMessage = err instanceof Error ? err.message : String(err);
|
|
if (unlisten) {
|
|
unlisten();
|
|
unlisten = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
function retry() {
|
|
setupState = "choose";
|
|
progress = 0;
|
|
progressMessage = "";
|
|
errorMessage = "";
|
|
}
|
|
</script>
|
|
|
|
<div class="setup-overlay">
|
|
<div class="setup-card">
|
|
<div class="setup-header">
|
|
<h1 class="app-title">Local Transcription</h1>
|
|
<h2 class="setup-heading">First-Time Setup</h2>
|
|
</div>
|
|
|
|
{#if setupState === "choose"}
|
|
<p class="setup-description">
|
|
The app needs to download its transcription engine before you can start.
|
|
Choose the version that best fits your hardware.
|
|
</p>
|
|
|
|
<div class="variant-options">
|
|
<label class="variant-option" class:selected={variant === "cpu"}>
|
|
<input
|
|
type="radio"
|
|
name="variant"
|
|
value="cpu"
|
|
bind:group={variant}
|
|
/>
|
|
<div class="variant-info">
|
|
<span class="variant-name">Standard (CPU)</span>
|
|
<span class="variant-desc">Works on all computers (~500 MB download)</span>
|
|
</div>
|
|
</label>
|
|
|
|
<label class="variant-option" class:selected={variant === "cuda"}>
|
|
<input
|
|
type="radio"
|
|
name="variant"
|
|
value="cuda"
|
|
bind:group={variant}
|
|
/>
|
|
<div class="variant-info">
|
|
<span class="variant-name">GPU Accelerated (CUDA)</span>
|
|
<span class="variant-desc">Faster transcription with NVIDIA GPU (~2 GB download)</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<button class="download-btn" onclick={startDownload}>
|
|
Download & Install
|
|
</button>
|
|
|
|
{:else if setupState === "downloading"}
|
|
<div class="progress-section">
|
|
<p class="progress-message">{progressMessage}</p>
|
|
<div class="progress-bar-track">
|
|
<div
|
|
class="progress-bar-fill"
|
|
style="width: {progress}%"
|
|
></div>
|
|
</div>
|
|
<p class="progress-percent">{Math.round(progress)}%</p>
|
|
</div>
|
|
|
|
{:else if setupState === "error"}
|
|
<div class="error-section">
|
|
<div class="error-icon">
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#f44336" stroke-width="2">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<line x1="15" y1="9" x2="9" y2="15"/>
|
|
<line x1="9" y1="9" x2="15" y2="15"/>
|
|
</svg>
|
|
</div>
|
|
<p class="error-title">Download Failed</p>
|
|
<p class="error-message">{errorMessage}</p>
|
|
<button class="retry-btn" onclick={retry}>
|
|
Try Again
|
|
</button>
|
|
</div>
|
|
|
|
{:else if setupState === "success"}
|
|
<div class="success-section">
|
|
<div class="success-icon">
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#4CAF50" stroke-width="2">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<polyline points="16 9 10.5 15 8 12.5"/>
|
|
</svg>
|
|
</div>
|
|
<p class="success-title">Setup Complete</p>
|
|
<p class="success-message">The transcription engine is ready to go.</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.setup-overlay {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
width: 100%;
|
|
background-color: #1e1e1e;
|
|
}
|
|
|
|
.setup-card {
|
|
background-color: #2a2a2a;
|
|
border-radius: 12px;
|
|
padding: 40px;
|
|
max-width: 480px;
|
|
width: 100%;
|
|
margin: 20px;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
}
|
|
|
|
.setup-header {
|
|
text-align: center;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.app-title {
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
color: #e0e0e0;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.setup-heading {
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
color: #a0a0a0;
|
|
}
|
|
|
|
.setup-description {
|
|
font-size: 14px;
|
|
color: #a0a0a0;
|
|
line-height: 1.6;
|
|
text-align: center;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.variant-options {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.variant-option {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 14px 16px;
|
|
border: 2px solid #444;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: border-color 0.15s ease, background-color 0.15s ease;
|
|
}
|
|
|
|
.variant-option:hover {
|
|
background-color: #333;
|
|
border-color: #555;
|
|
}
|
|
|
|
.variant-option.selected {
|
|
border-color: #4CAF50;
|
|
background-color: rgba(76, 175, 80, 0.08);
|
|
}
|
|
|
|
.variant-option input[type="radio"] {
|
|
width: 18px;
|
|
height: 18px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.variant-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.variant-name {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.variant-desc {
|
|
font-size: 12px;
|
|
color: #888;
|
|
}
|
|
|
|
.download-btn {
|
|
display: block;
|
|
width: 100%;
|
|
padding: 12px 24px;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: white;
|
|
background-color: #4CAF50;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: background-color 0.15s ease;
|
|
}
|
|
|
|
.download-btn:hover {
|
|
background-color: #45a049;
|
|
}
|
|
|
|
.download-btn:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
/* Progress state */
|
|
.progress-section {
|
|
text-align: center;
|
|
padding: 20px 0;
|
|
}
|
|
|
|
.progress-message {
|
|
font-size: 14px;
|
|
color: #a0a0a0;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.progress-bar-track {
|
|
width: 100%;
|
|
height: 8px;
|
|
background-color: #3a3a3a;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.progress-bar-fill {
|
|
height: 100%;
|
|
background-color: #4CAF50;
|
|
border-radius: 4px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.progress-percent {
|
|
font-size: 13px;
|
|
color: #707070;
|
|
}
|
|
|
|
/* Error state */
|
|
.error-section {
|
|
text-align: center;
|
|
padding: 10px 0;
|
|
}
|
|
|
|
.error-icon {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.error-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #f44336;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.error-message {
|
|
font-size: 13px;
|
|
color: #a0a0a0;
|
|
margin-bottom: 20px;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.retry-btn {
|
|
display: inline-block;
|
|
padding: 10px 28px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: white;
|
|
background-color: #4CAF50;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: background-color 0.15s ease;
|
|
}
|
|
|
|
.retry-btn:hover {
|
|
background-color: #45a049;
|
|
}
|
|
|
|
/* Success state */
|
|
.success-section {
|
|
text-align: center;
|
|
padding: 20px 0;
|
|
}
|
|
|
|
.success-icon {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.success-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #4CAF50;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.success-message {
|
|
font-size: 14px;
|
|
color: #a0a0a0;
|
|
}
|
|
</style>
|