import { useEffect, useState } from 'react'; import voiceboxLogo from '@/assets/voicebox-logo.png'; import { AudioPlayer } from '@/components/AudioPlayer/AudioPlayer'; // import { GenerationForm } from '@/components/Generation/GenerationForm'; import { FloatingGenerateBox } from '@/components/Generation/FloatingGenerateBox'; import { HistoryTable } from '@/components/History/HistoryTable'; import ShinyText from '@/components/ShinyText'; import { Sidebar } from '@/components/Sidebar'; import { TitleBarDragRegion } from '@/components/TitleBarDragRegion'; import { Toaster } from '@/components/ui/toaster'; import { ProfileList } from '@/components/VoiceProfiles/ProfileList'; import { VoicesTab } from '@/components/VoicesTab/VoicesTab'; import { AudioTab } from '@/components/AudioTab/AudioTab'; import { ServerTab } from '@/components/ServerTab/ServerTab'; import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast'; import { MODEL_DISPLAY_NAMES, useRestoreActiveTasks } from '@/lib/hooks/useRestoreActiveTasks'; import { isMacOS, isTauri, setKeepServerRunning, setupWindowCloseHandler, startServer, } from '@/lib/tauri'; import { usePlayerStore } from '@/stores/playerStore'; import { useServerStore } from '@/stores/serverStore'; // Track if server is starting to prevent duplicate starts let serverStarting = false; const LOADING_MESSAGES = [ 'Warming up tensors...', 'Calibrating synthesizer engine...', 'Initializing voice models...', 'Loading neural networks...', 'Preparing audio pipelines...', 'Optimizing waveform generators...', 'Tuning frequency analyzers...', 'Building voice embeddings...', 'Configuring text-to-speech cores...', 'Syncing audio buffers...', 'Establishing model connections...', 'Preprocessing training data...', 'Validating voice samples...', 'Compiling inference engines...', 'Mapping phoneme sequences...', 'Aligning prosody parameters...', 'Activating speech synthesis...', 'Fine-tuning acoustic models...', 'Preparing voice cloning matrices...', 'Initializing Qwen TTS framework...', ]; function App() { const [activeTab, setActiveTab] = useState('main'); const [serverReady, setServerReady] = useState(false); const [loadingMessageIndex, setLoadingMessageIndex] = useState(0); const audioUrl = usePlayerStore((state) => state.audioUrl); // Monitor active downloads/generations and show toasts for them const activeDownloads = useRestoreActiveTasks(); // Sync stored setting to Rust on startup useEffect(() => { if (isTauri()) { const keepRunning = useServerStore.getState().keepServerRunningOnClose; setKeepServerRunning(keepRunning).catch((error) => { console.error('Failed to sync initial setting to Rust:', error); }); } }, []); // Setup window close handler and auto-start server when running in Tauri (production only) useEffect(() => { if (!isTauri()) { return; } // Setup window close handler to check setting and stop server if needed // This works in both dev and prod, but will only stop server if it was started by the app setupWindowCloseHandler().catch((error) => { console.error('Failed to setup window close handler:', error); }); // Only auto-start server in production mode // In dev mode, user runs server separately if (!import.meta.env?.PROD) { console.log('Dev mode: Skipping auto-start of server (run it separately)'); setServerReady(true); // Mark as ready so UI doesn't show loading screen // Mark that server was not started by app (so we don't try to stop it on close) // @ts-expect-error - adding property to window window.__voiceboxServerStartedByApp = false; return; } // Auto-start server in production if (serverStarting) { return; } serverStarting = true; console.log('Production mode: Starting bundled server...'); startServer(false) .then((serverUrl) => { console.log('Server is ready at:', serverUrl); // Update the server URL in the store with the dynamically assigned port useServerStore.getState().setServerUrl(serverUrl); setServerReady(true); // Mark that we started the server (so we know to stop it on close) // @ts-expect-error - adding property to window window.__voiceboxServerStartedByApp = true; }) .catch((error) => { console.error('Failed to auto-start server:', error); serverStarting = false; // @ts-expect-error - adding property to window window.__voiceboxServerStartedByApp = false; }); // Cleanup: stop server on actual unmount (not StrictMode remount) // Note: Window close is handled separately in Tauri Rust code return () => { // Window close event handles server shutdown based on setting serverStarting = false; }; }, []); // Cycle through loading messages every 3 seconds useEffect(() => { if (!isTauri() || serverReady) { return; } const interval = setInterval(() => { setLoadingMessageIndex((prev) => (prev + 1) % LOADING_MESSAGES.length); }, 3000); return () => clearInterval(interval); }, [serverReady]); // Show loading screen while server is starting in Tauri if (isTauri() && !serverReady) { return (
Voicebox
); } return (
{activeTab === 'main' && ( // Main view: Profiles top left, Generator bottom left, History right
{/* Left Column */}
{/* Profiles - Top Left */}
{/* Generator - Bottom Left */} {/*
*/}
{/* Right Column - History */}
{/* Floating Generate Box */}
)} {activeTab === 'voices' && } {activeTab === 'audio' && } {activeTab === 'server' && }
{/* Audio Player - always visible except on server tab */} {activeTab !== 'server' && } {/* Show download toasts for any active downloads (from anywhere) */} {activeDownloads.map((download) => { const displayName = MODEL_DISPLAY_NAMES[download.model_name] || download.model_name; return ( ); })}
); } /** * Component that restores a download toast for a specific model. */ function DownloadToastRestorer({ modelName, displayName, }: { modelName: string; displayName: string; }) { // Use the download toast hook to restore the toast useModelDownloadToast({ modelName, displayName, enabled: true, }); return null; } export default App;