/** * Root Application Component * Handles routing and global providers */ import { Navigate, Routes, Route, useNavigate, useLocation } from 'react-router-dom'; import { Component, lazy, Suspense, useEffect, useState } from 'react'; import type { ErrorInfo, ReactNode } from 'react'; import { Toaster } from 'sonner'; import i18n from './i18n'; import { MainLayout } from './components/layout/MainLayout'; import { TooltipProvider } from '@/components/ui/tooltip'; import { useSettingsStore } from './stores/settings'; import { useProviderStore } from './stores/providers'; import { useAuthStore } from './stores/auth'; import { codingWorkspaceStore, useCodingWorkspaceStore } from './stores/coding-workspace'; import { useProjectConfigStore } from './stores/project-config'; import { AI_MODULE_SELECTION_PATH, getGuardedAiModuleForPath, isAiModuleAllowed, isProgrammingProviderRoute, } from './lib/ai-modules'; import { useUserSyncStore } from './stores/user-sync'; import { flushPendingAgentSessionSync } from '@/lib/agent-session-sync'; import { subscribeHostEvent } from '@/lib/host-events'; import { reportDesktopActivity, type DesktopActivityModule } from '@/lib/host-api'; import { installRendererPerformanceDiagnostics } from '@/lib/performance-diagnostics'; import { resolveSupportedLanguage } from '../shared/language'; const Models = lazy(() => import('./pages/Models').then(({ Models: component }) => ({ default: component }))); const Chat = lazy(() => import('./pages/Chat').then(({ Chat: component }) => ({ default: component }))); const Home = lazy(() => import('./pages/Home').then(({ Home: component }) => ({ default: component }))); const PreviewScene = lazy(() => import('./pages/Makelore').then(({ PreviewScene: component }) => ({ default: component }))); const ProjectConfiguration = lazy(() => import('./pages/ProjectConfiguration').then(({ ProjectConfiguration: component }) => ({ default: component }))); const Workbench = lazy(() => import('./pages/Workbench').then(({ Workbench: component }) => ({ default: component }))); const ImageCanvas = lazy(() => import('./pages/ImageCanvas').then(({ ImageCanvas: component }) => ({ default: component }))); const ImagePromptMuseum = lazy(() => import('./pages/ImagePromptMuseum').then(({ ImagePromptMuseum: component }) => ({ default: component }))); const AiHardware = lazy(() => import('./pages/AiHardware').then(({ AiHardware: component }) => ({ default: component }))); const Settings = lazy(() => import('./pages/Settings').then(({ Settings: component }) => ({ default: component }))); const Setup = lazy(() => import('./pages/Setup').then(({ Setup: component }) => ({ default: component }))); const Login = lazy(() => import('./pages/Login').then(({ Login: component }) => ({ default: component }))); const ModuleSelection = lazy(() => import('./pages/ModuleSelection').then(({ ModuleSelection: component }) => ({ default: component }))); const Learning = lazy(() => import('./pages/Learning').then(({ Learning: component }) => ({ default: component }))); const LearningProjectDetail = lazy(() => import('./pages/Learning/ProjectDetail').then(({ LearningProjectDetail: component }) => ({ default: component }))); /** * Error Boundary to catch and display React rendering errors */ class ErrorBoundary extends Component< { children: ReactNode }, { hasError: boolean; error: Error | null } > { constructor(props: { children: ReactNode }) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } componentDidCatch(error: Error, info: ErrorInfo) { console.error('React Error Boundary caught error:', error, info); } render() { if (this.state.hasError) { return (

Something went wrong

            {this.state.error?.message}
            {'\n\n'}
            {this.state.error?.stack}
          
); } return this.props.children; } } function StartupScreen() { return (
Makelore
); } function getReturnPath(location: ReturnType): string { return `${location.pathname}${location.search}`; } function moduleForPath(pathname: string): DesktopActivityModule | null { const module = getGuardedAiModuleForPath(pathname); if (module === 'programming') return 'programming'; if (module === 'painting') return 'painting'; if (module === 'learning') return 'learning'; if (module === 'robot') return 'robot'; return null; } function publishDesktopActivity(input: { visible: boolean; module: DesktopActivityModule | null; }): void { try { void Promise.resolve(reportDesktopActivity(input)).catch(() => undefined); } catch { // Renderer-only hosts may not expose lifecycle IPC; do not block routing. } } function ProtectedLayout({ authReady, authRequired, imageWorkspaceLocalDevelopment, setupReady, }: { authReady: boolean; authRequired: boolean; imageWorkspaceLocalDevelopment: boolean; setupReady: boolean; }) { const location = useLocation(); const authenticated = useAuthStore((state) => state.isAuthenticated()); const moduleAccess = useAuthStore((state) => state.moduleAccess); const requestedModule = getGuardedAiModuleForPath(location.pathname); const allowsAnonymousImageWorkspace = imageWorkspaceLocalDevelopment && (location.pathname === '/image-canvas' || location.pathname.startsWith('/image-canvas/') || location.pathname === '/image-prompts' || location.pathname.startsWith('/image-prompts/')); if (!setupReady) { return ; } if (authRequired && !authReady && !allowsAnonymousImageWorkspace) { return ; } if (authRequired && !allowsAnonymousImageWorkspace && !authenticated) { return ( ); } if (authenticated && requestedModule && !isAiModuleAllowed(requestedModule, moduleAccess)) { return ; } return ; } function ModuleSelectionRoute({ authReady, authRequired, setupReady, }: { authReady: boolean; authRequired: boolean; setupReady: boolean; }) { if (!setupReady) { return ; } if (authRequired && !authReady) { return ; } return ; } function ProjectChatRoute() { const activeProject = useCodingWorkspaceStore((state) => state.activeProject); const loadProjectList = useCodingWorkspaceStore((state) => state.load); const loadProjectConfig = useProjectConfigStore((state) => state.load); const [resolvedProjectKey, setResolvedProjectKey] = useState(undefined); const [resolvedRoute, setResolvedRoute] = useState<'chat' | 'config'>('config'); const projectKey = activeProject?.id ?? null; useEffect(() => { let cancelled = false; void (async () => { try { await loadProjectList(); const project = codingWorkspaceStore.getState().activeProject; if (!project) { if (!cancelled) { setResolvedProjectKey(null); setResolvedRoute('config'); } return; } const configResult = await loadProjectConfig(project.id); if (!cancelled) { setResolvedProjectKey(project.id); setResolvedRoute(configResult.status === 'valid' && Boolean(configResult.config?.initialized) ? 'chat' : 'config'); } } catch { if (!cancelled) { setResolvedProjectKey(projectKey); setResolvedRoute('config'); } } })(); return () => { cancelled = true; }; }, [loadProjectConfig, loadProjectList, projectKey]); if (resolvedProjectKey === undefined || resolvedProjectKey !== projectKey) { return ; } if (resolvedRoute === 'config') { return ; } return ; } function App() { const navigate = useNavigate(); const location = useLocation(); const rendererOnlyPreview = typeof __NIANCODE_RENDERER_ONLY__ !== 'undefined' && __NIANCODE_RENDERER_ONLY__; const skipSetupForE2E = typeof window !== 'undefined' && new URLSearchParams(window.location.search).get('e2eSkipSetup') === '1'; const initSettings = useSettingsStore((state) => state.init); const language = useSettingsStore((state) => state.language); const setupComplete = useSettingsStore((state) => state.setupComplete); const initProviders = useProviderStore((state) => state.init); const initAuth = useAuthStore((state) => state.init); const authInitialized = useAuthStore((state) => state.initialized); const authenticated = useAuthStore((state) => state.isAuthenticated()); const authAccessToken = useAuthStore((state) => state.accessToken); const programmingModuleAllowed = useAuthStore( (state) => state.moduleAccess.programming, ); const bootstrapUserSync = useUserSyncStore((state) => state.bootstrap); const setupReady = setupComplete || skipSetupForE2E || rendererOnlyPreview; const authRequired = !skipSetupForE2E && !rendererOnlyPreview; const imageWorkspaceLocalDevelopment = window.electron?.imageWorkspaceLocalDevelopment === true; useEffect(() => installRendererPerformanceDiagnostics(), []); useEffect(() => { void initAuth(); }, [initAuth]); useEffect(() => { const unsubscribe = subscribeHostEvent('auth:session-changed', (session) => { useAuthStore.getState().applyMainSession(session); }); return unsubscribe; }, []); useEffect(() => { if (rendererOnlyPreview || !authInitialized || !authAccessToken) return; const recordActivity = (event: Event) => { if (!event.isTrusted) return; void useAuthStore.getState().markActivity(); }; window.addEventListener('pointerdown', recordActivity, { passive: true }); window.addEventListener('touchstart', recordActivity, { passive: true }); window.addEventListener('wheel', recordActivity, { passive: true }); window.addEventListener('keydown', recordActivity); window.addEventListener('focus', recordActivity); return () => { window.removeEventListener('pointerdown', recordActivity); window.removeEventListener('touchstart', recordActivity); window.removeEventListener('wheel', recordActivity); window.removeEventListener('keydown', recordActivity); window.removeEventListener('focus', recordActivity); }; }, [authAccessToken, authInitialized, rendererOnlyPreview]); useEffect(() => { initSettings(); }, [initSettings]); // Sync i18n language with persisted settings on mount useEffect(() => { if (language && language !== i18n.language) { i18n.changeLanguage(language); } }, [language]); // Keep the document language in sync with the persisted setting. useEffect(() => { const resolvedLanguage = resolveSupportedLanguage(language || i18n.language); const root = window.document.documentElement; root.lang = resolvedLanguage === 'zh' ? 'zh-CN' : resolvedLanguage; root.dataset.uiLanguage = resolvedLanguage; }, [language]); // Initialize provider snapshot only after the user can enter the app. useEffect(() => { if (rendererOnlyPreview) return; if (!setupReady) return; if (authRequired && !authInitialized) return; if (authRequired && !authenticated) return; if (!isProgrammingProviderRoute(location.pathname)) return; if ( getGuardedAiModuleForPath(location.pathname) === 'programming' && !programmingModuleAllowed ) return; initProviders(); }, [ authInitialized, authRequired, authenticated, initProviders, location.pathname, programmingModuleAllowed, rendererOnlyPreview, setupReady, ]); useEffect(() => { if (rendererOnlyPreview) return; if (!setupReady) return; if (!authInitialized) return; if (!authenticated) return; void bootstrapUserSync().finally(() => { void flushPendingAgentSessionSync(); }); }, [authenticated, authInitialized, bootstrapUserSync, rendererOnlyPreview, setupReady]); useEffect(() => { if (rendererOnlyPreview) return undefined; const publish = () => { publishDesktopActivity({ visible: document.visibilityState === 'visible', module: moduleForPath(location.pathname), }); }; publish(); document.addEventListener('visibilitychange', publish, { passive: true }); return () => document.removeEventListener('visibilitychange', publish); }, [location.pathname, rendererOnlyPreview]); // Redirect to setup wizard if not complete useEffect(() => { if (rendererOnlyPreview && location.pathname.startsWith('/setup')) { navigate('/'); return; } if (!setupComplete && !skipSetupForE2E && !rendererOnlyPreview && !location.pathname.startsWith('/setup')) { navigate('/setup'); } }, [setupComplete, skipSetupForE2E, rendererOnlyPreview, location.pathname, navigate]); // Listen for navigation events from main process useEffect(() => { if (!window.electron?.ipcRenderer?.on) return; const handleNavigate = (...args: unknown[]) => { const path = args[0]; if (typeof path === 'string') { navigate(path); } }; const unsubscribe = window.electron.ipcRenderer.on('navigate', handleNavigate); return () => { if (typeof unsubscribe === 'function') { unsubscribe(); } }; }, [navigate]); // Apply the product palette. Makelore now uses a single light interface. useEffect(() => { const root = window.document.documentElement; root.classList.remove('light', 'dark'); root.classList.add('light'); }, []); return ( }> {/* Setup wizard (shown on first launch) */} } /> } /> )} /> } /> {/* Main application routes */} )} > } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* Global toast notifications */} ); } export default App;