import { RouterProvider } from '@tanstack/react-router'; import { useEffect, useRef, useState } from 'react'; import voiceboxLogo from '@/assets/voicebox-logo.png'; import ShinyText from '@/components/ShinyText'; import { TitleBarDragRegion } from '@/components/TitleBarDragRegion'; import { useAutoUpdater } from '@/hooks/useAutoUpdater'; import { TOP_SAFE_AREA_PADDING } from '@/lib/constants/ui'; import { cn } from '@/lib/utils/cn'; import { usePlatform } from '@/platform/PlatformContext'; import { router } from '@/router'; import { useLogStore } from '@/stores/logStore'; import { useServerStore } from '@/stores/serverStore'; 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 platform = usePlatform(); const [serverReady, setServerReady] = useState(false); const [loadingMessageIndex, setLoadingMessageIndex] = useState(0); const serverStartingRef = useRef(false); // Automatically check for app updates on startup and show toast notifications useAutoUpdater({ checkOnMount: true, showToast: true }); // Sync stored setting to Rust on startup useEffect(() => { if (platform.metadata.isTauri) { const keepRunning = useServerStore.getState().keepServerRunningOnClose; platform.lifecycle.setKeepServerRunning(keepRunning).catch((error) => { console.error('Failed to sync initial setting to Rust:', error); }); } // Empty dependency array - platform is stable from context, only run once // eslint-disable-next-line react-hooks/exhaustive-deps }, [platform.metadata.isTauri, platform.lifecycle]); // Setup lifecycle callbacks useEffect(() => { platform.lifecycle.onServerReady = () => { setServerReady(true); }; // Empty dependency array - platform is stable from context, only run once // eslint-disable-next-line react-hooks/exhaustive-deps }, [platform.lifecycle]); // Subscribe to server logs useEffect(() => { const unsubscribe = platform.lifecycle.subscribeToServerLogs((entry) => { useLogStore.getState().addEntry(entry); }); return unsubscribe; }, [platform.lifecycle]); // Setup window close handler and auto-start server when running in Tauri (production only) useEffect(() => { if (!platform.metadata.isTauri) { setServerReady(true); // Web assumes server is running 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 platform.lifecycle.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 (serverStartingRef.current) { return; } serverStartingRef.current = true; const isRemote = useServerStore.getState().mode === 'remote'; const customModelsDir = useServerStore.getState().customModelsDir; console.log(`Production mode: Starting bundled server... (remote: ${isRemote})`); platform.lifecycle .startServer(isRemote, customModelsDir) .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); serverStartingRef.current = 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 serverStartingRef.current = false; }; // Empty dependency array - platform is stable from context, only run once // eslint-disable-next-line react-hooks/exhaustive-deps }, [platform.metadata.isTauri, platform.lifecycle]); // Cycle through loading messages every 3 seconds useEffect(() => { if (!platform.metadata.isTauri || serverReady) { return; } const interval = setInterval(() => { setLoadingMessageIndex((prev) => (prev + 1) % LOADING_MESSAGES.length); }, 3000); return () => clearInterval(interval); }, [serverReady, platform.metadata.isTauri]); // Show loading screen while server is starting in Tauri if (platform.metadata.isTauri && !serverReady) { return (
Voicebox
); } return ; } export default App;