diff --git a/frontend/src-tauri/src/audio/transcription/engine.rs b/frontend/src-tauri/src/audio/transcription/engine.rs index 415cdc1f1..e3bd2dd7b 100644 --- a/frontend/src-tauri/src/audio/transcription/engine.rs +++ b/frontend/src-tauri/src/audio/transcription/engine.rs @@ -5,7 +5,7 @@ use super::provider::TranscriptionProvider; use log::{info, warn}; use std::sync::Arc; -use tauri::{AppHandle, Manager, Runtime}; +use tauri::{AppHandle, Emitter, Manager, Runtime}; // ============================================================================ // TRANSCRIPTION ENGINE ENUM @@ -233,6 +233,12 @@ pub async fn get_or_init_whisper( engine_guard.as_ref().cloned() }; + // Emit model loading status event + let _ = app.emit("model-loading-status", serde_json::json!({ + "stage": "checking", + "message": "Checking transcription model..." + })); + if let Some(engine) = existing_engine { // Check if a model is already loaded if engine.is_model_loaded().await { @@ -277,12 +283,20 @@ pub async fn get_or_init_whisper( "✅ Loaded model '{}' matches saved config, reusing", current_model ); + let _ = app.emit("model-loading-status", serde_json::json!({ + "stage": "ready", + "message": format!("Model '{}' ready", current_model) + })); return Ok(engine); } else { info!( "🔄 Loaded model '{}' doesn't match saved config '{}', reloading correct model...", current_model, expected_model ); + let _ = app.emit("model-loading-status", serde_json::json!({ + "stage": "switching", + "message": format!("Switching model from '{}' to '{}'...", current_model, expected_model) + })); // Unload the incorrect model engine.unload_model().await; info!("📉 Unloaded incorrect model '{}'", current_model); @@ -294,6 +308,10 @@ pub async fn get_or_init_whisper( "✅ No specific model configured, using currently loaded model: '{}'", current_model ); + let _ = app.emit("model-loading-status", serde_json::json!({ + "stage": "ready", + "message": format!("Model '{}' ready", current_model) + })); return Ok(engine); } } else { @@ -388,11 +406,19 @@ pub async fn get_or_init_whisper( match model.status { crate::whisper_engine::ModelStatus::Available => { info!("Loading model: {}", model_to_load); + let _ = app.emit("model-loading-status", serde_json::json!({ + "stage": "loading", + "message": format!("Loading model '{}'...", model_to_load) + })); engine .load_model(&model_to_load) .await .map_err(|e| format!("Failed to load model '{}': {}", model_to_load, e))?; info!("✅ Model '{}' loaded successfully", model_to_load); + let _ = app.emit("model-loading-status", serde_json::json!({ + "stage": "ready", + "message": format!("Model '{}' ready", model_to_load) + })); } crate::whisper_engine::ModelStatus::Missing => { return Err(format!( @@ -423,6 +449,10 @@ pub async fn get_or_init_whisper( "Model '{}' not found, falling back to available model: '{}'", model_to_load, fallback_model.name ); + let _ = app.emit("model-loading-status", serde_json::json!({ + "stage": "loading", + "message": format!("Loading fallback model '{}'...", fallback_model.name) + })); engine.load_model(&fallback_model.name).await.map_err(|e| { format!( "Failed to load fallback model '{}': {}", @@ -433,6 +463,10 @@ pub async fn get_or_init_whisper( "✅ Fallback model '{}' loaded successfully", fallback_model.name ); + let _ = app.emit("model-loading-status", serde_json::json!({ + "stage": "ready", + "message": format!("Model '{}' ready", fallback_model.name) + })); } else { return Err(format!("Model '{}' is not supported and no other models are available. Please download a model from the settings.", model_to_load)); } diff --git a/frontend/src-tauri/src/parakeet_engine/commands.rs b/frontend/src-tauri/src/parakeet_engine/commands.rs index 6abfc3626..86ba704d8 100644 --- a/frontend/src-tauri/src/parakeet_engine/commands.rs +++ b/frontend/src-tauri/src/parakeet_engine/commands.rs @@ -236,10 +236,19 @@ pub async fn parakeet_validate_model_ready_with_config( }; if let Some(engine) = engine { + let _ = app.emit("model-loading-status", serde_json::json!({ + "stage": "checking", + "message": "Checking transcription model..." + })); + // Check if a model is currently loaded if engine.is_model_loaded().await { if let Some(current_model) = engine.get_current_model().await { log::info!("Parakeet model already loaded: {}", current_model); + let _ = app.emit("model-loading-status", serde_json::json!({ + "stage": "ready", + "message": format!("Model '{}' ready", current_model) + })); return Ok(current_model); } } @@ -332,11 +341,21 @@ pub async fn parakeet_validate_model_ready_with_config( .clone() }; + let _ = app.emit("model-loading-status", serde_json::json!({ + "stage": "loading", + "message": format!("Loading model '{}'...", model_name) + })); + engine .load_model(&model_name) .await .map_err(|e| format!("Failed to load Parakeet model {}: {}", model_name, e))?; + let _ = app.emit("model-loading-status", serde_json::json!({ + "stage": "ready", + "message": format!("Model '{}' ready", model_name) + })); + Ok(model_name) } else { Err("Parakeet engine not initialized".to_string()) diff --git a/frontend/src-tauri/src/whisper_engine/commands.rs b/frontend/src-tauri/src/whisper_engine/commands.rs index e62c6b0e5..986c18402 100644 --- a/frontend/src-tauri/src/whisper_engine/commands.rs +++ b/frontend/src-tauri/src/whisper_engine/commands.rs @@ -286,10 +286,19 @@ pub async fn whisper_validate_model_ready_with_config( }; if let Some(engine) = engine { + let _ = app.emit("model-loading-status", serde_json::json!({ + "stage": "checking", + "message": "Checking transcription model..." + })); + // Check if a model is currently loaded if engine.is_model_loaded().await { if let Some(current_model) = engine.get_current_model().await { log::info!("Model already loaded: {}", current_model); + let _ = app.emit("model-loading-status", serde_json::json!({ + "stage": "ready", + "message": format!("Model '{}' ready", current_model) + })); return Ok(current_model); } } @@ -373,11 +382,21 @@ pub async fn whisper_validate_model_ready_with_config( available_models[0].name.clone() }; + let _ = app.emit("model-loading-status", serde_json::json!({ + "stage": "loading", + "message": format!("Loading model '{}'...", model_name) + })); + engine .load_model(&model_name) .await .map_err(|e| format!("Failed to load model {}: {}", model_name, e))?; + let _ = app.emit("model-loading-status", serde_json::json!({ + "stage": "ready", + "message": format!("Model '{}' ready", model_name) + })); + Ok(model_name) } else { Err("Whisper engine not initialized".to_string()) diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index a4f5d59dc..dac13a364 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; -import { motion } from 'framer-motion'; +import { motion, AnimatePresence } from 'framer-motion'; import { RecordingControls } from '@/components/RecordingControls'; import { useSidebar } from '@/components/Sidebar/SidebarProvider'; import { usePermissionCheck } from '@/hooks/usePermissionCheck'; @@ -21,6 +21,7 @@ import { TranscriptRecovery } from '@/components/TranscriptRecovery'; import { indexedDBService } from '@/services/indexedDBService'; import { toast } from 'sonner'; import { useRouter } from 'next/navigation'; +import { Loader2 } from 'lucide-react'; export default function Home() { // Local page state (not moved to contexts) @@ -62,6 +63,27 @@ export default function Home() { const router = useRouter(); + // Model loading status + const [modelStatus, setModelStatus] = useState<{ stage: string; message: string } | null>(null); + + useEffect(() => { + let unlisten: (() => void) | undefined; + let mounted = true; + import('@tauri-apps/api/event').then(({ listen }) => { + listen<{ stage: string; message: string }>('model-loading-status', (event) => { + if (!mounted) return; + const { stage, message } = event.payload; + if (stage === 'ready') { + setModelStatus({ stage, message }); + setTimeout(() => { if (mounted) setModelStatus(null); }, 1500); + } else { + setModelStatus({ stage, message }); + } + }).then(fn => { unlisten = fn; }); + }); + return () => { mounted = false; unlisten?.(); }; + }, []); + useEffect(() => { // Track page view Analytics.trackPageView('home'); @@ -225,11 +247,24 @@ export default function Home() { status !== RecordingStatus.SAVING && (
+ + {modelStatus && modelStatus.stage !== 'ready' && ( + + + {modelStatus.message} + + )} +