Makelore 2.0 initial clean snapshot
This commit is contained in:
381
src/App.tsx
Normal file
381
src/App.tsx
Normal file
@@ -0,0 +1,381 @@
|
||||
/**
|
||||
* Root Application Component
|
||||
* Handles routing and global providers
|
||||
*/
|
||||
import { Navigate, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Component, 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 { Models } from './pages/Models';
|
||||
import { Projects } from './pages/Projects';
|
||||
import { Sessions } from './pages/Sessions';
|
||||
import { Chat } from './pages/Chat';
|
||||
import { Home } from './pages/Home';
|
||||
import { PreviewScene } from './pages/NiTu';
|
||||
import { ProjectConfiguration } from './pages/ProjectConfiguration';
|
||||
import { Workbench } from './pages/Workbench';
|
||||
import { ImageCanvas } from './pages/ImageCanvas';
|
||||
import { Settings } from './pages/Settings';
|
||||
import { Setup } from './pages/Setup';
|
||||
import { Login } from './pages/Login';
|
||||
import { ModuleSelection } from './pages/ModuleSelection';
|
||||
import { useSettingsStore } from './stores/settings';
|
||||
import { useProviderStore } from './stores/providers';
|
||||
import { useAuthStore } from './stores/auth';
|
||||
import { useOpencodeStore } from './stores/opencode';
|
||||
import { useProjectConfigStore } from './stores/project-config';
|
||||
import { AI_MODULE_SELECTION_PATH } from './lib/ai-modules';
|
||||
import { useUserSyncStore } from './stores/user-sync';
|
||||
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<div style={{
|
||||
padding: '40px',
|
||||
color: '#991b1b',
|
||||
background: '#F6F8FA',
|
||||
minHeight: '100vh',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
<h1 style={{ fontSize: '24px', marginBottom: '16px' }}>Something went wrong</h1>
|
||||
<pre style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
background: '#FFFFFF',
|
||||
padding: '16px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '14px',
|
||||
color: '#26384D',
|
||||
border: '2px solid #26384D'
|
||||
}}>
|
||||
{this.state.error?.message}
|
||||
{'\n\n'}
|
||||
{this.state.error?.stack}
|
||||
</pre>
|
||||
<button
|
||||
onClick={() => { this.setState({ hasError: false, error: null }); window.location.reload(); }}
|
||||
style={{
|
||||
marginTop: '16px',
|
||||
padding: '8px 16px',
|
||||
background: '#3A5578',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Reload
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function StartupScreen() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background text-sm text-muted-foreground">
|
||||
Makelore
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getReturnPath(location: ReturnType<typeof useLocation>): string {
|
||||
return `${location.pathname}${location.search}`;
|
||||
}
|
||||
|
||||
function ProtectedLayout({
|
||||
authReady,
|
||||
authRequired,
|
||||
imageWorkspaceLocalDevelopment,
|
||||
setupReady,
|
||||
}: {
|
||||
authReady: boolean;
|
||||
authRequired: boolean;
|
||||
imageWorkspaceLocalDevelopment: boolean;
|
||||
setupReady: boolean;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||
const allowsAnonymousImageWorkspace = imageWorkspaceLocalDevelopment
|
||||
&& (location.pathname === '/'
|
||||
|| location.pathname === '/image-canvas'
|
||||
|| location.pathname.startsWith('/image-canvas/'));
|
||||
|
||||
if (!setupReady) {
|
||||
return <StartupScreen />;
|
||||
}
|
||||
|
||||
if (authRequired && !authReady && !allowsAnonymousImageWorkspace) {
|
||||
return <StartupScreen />;
|
||||
}
|
||||
|
||||
if (authRequired && !allowsAnonymousImageWorkspace && !isAuthenticated()) {
|
||||
return (
|
||||
<Navigate
|
||||
to="/login"
|
||||
replace
|
||||
state={{ from: getReturnPath(location) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <MainLayout />;
|
||||
}
|
||||
|
||||
function ProtectedModuleSelection({
|
||||
authReady,
|
||||
authRequired,
|
||||
imageWorkspaceLocalDevelopment,
|
||||
setupReady,
|
||||
}: {
|
||||
authReady: boolean;
|
||||
authRequired: boolean;
|
||||
imageWorkspaceLocalDevelopment: boolean;
|
||||
setupReady: boolean;
|
||||
}) {
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||
|
||||
if (!setupReady) {
|
||||
return <StartupScreen />;
|
||||
}
|
||||
|
||||
if (authRequired && !authReady && !imageWorkspaceLocalDevelopment) {
|
||||
return <StartupScreen />;
|
||||
}
|
||||
|
||||
if (authRequired && !imageWorkspaceLocalDevelopment && !isAuthenticated()) {
|
||||
return <Navigate to="/login" replace state={{ from: AI_MODULE_SELECTION_PATH }} />;
|
||||
}
|
||||
|
||||
return <ModuleSelection />;
|
||||
}
|
||||
|
||||
function ProjectChatRoute() {
|
||||
const activeProject = useOpencodeStore((state) => state.activeProject);
|
||||
const loadProjectList = useOpencodeStore((state) => state.loadProjectList);
|
||||
const loadProjectConfig = useProjectConfigStore((state) => state.load);
|
||||
const [resolvedProjectKey, setResolvedProjectKey] = useState<string | null | undefined>(undefined);
|
||||
const [resolvedRoute, setResolvedRoute] = useState<'chat' | 'config'>('config');
|
||||
const projectKey = activeProject?.id ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
await loadProjectList();
|
||||
|
||||
const project = useOpencodeStore.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 <StartupScreen />;
|
||||
}
|
||||
if (resolvedRoute === 'config') {
|
||||
return <Navigate to="/project-config" replace />;
|
||||
}
|
||||
return <Chat />;
|
||||
}
|
||||
|
||||
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 isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||
const bootstrapUserSync = useUserSyncStore((state) => state.bootstrap);
|
||||
const setupReady = setupComplete || skipSetupForE2E || rendererOnlyPreview;
|
||||
const authRequired = !skipSetupForE2E && !rendererOnlyPreview;
|
||||
const imageWorkspaceLocalDevelopment = window.electron?.imageWorkspaceLocalDevelopment === true;
|
||||
|
||||
useEffect(() => {
|
||||
initAuth();
|
||||
}, [initAuth]);
|
||||
|
||||
useEffect(() => {
|
||||
initSettings();
|
||||
}, [initSettings]);
|
||||
|
||||
// Sync i18n language with persisted settings on mount
|
||||
useEffect(() => {
|
||||
if (language && language !== i18n.language) {
|
||||
i18n.changeLanguage(language);
|
||||
}
|
||||
}, [language]);
|
||||
|
||||
// Initialize provider snapshot only after the user can enter the app.
|
||||
useEffect(() => {
|
||||
if (rendererOnlyPreview) return;
|
||||
if (!setupReady) return;
|
||||
initProviders();
|
||||
}, [initProviders, rendererOnlyPreview, setupReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (rendererOnlyPreview) return;
|
||||
if (!setupReady) return;
|
||||
if (!authInitialized) return;
|
||||
if (!isAuthenticated()) return;
|
||||
void bootstrapUserSync();
|
||||
}, [authInitialized, bootstrapUserSync, isAuthenticated, rendererOnlyPreview, setupReady]);
|
||||
|
||||
// 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 (
|
||||
<ErrorBoundary>
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Routes>
|
||||
{/* Setup wizard (shown on first launch) */}
|
||||
<Route path="/setup/*" element={<Setup />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route
|
||||
path={AI_MODULE_SELECTION_PATH}
|
||||
element={(
|
||||
<ProtectedModuleSelection
|
||||
authReady={authInitialized}
|
||||
authRequired={authRequired}
|
||||
imageWorkspaceLocalDevelopment={imageWorkspaceLocalDevelopment}
|
||||
setupReady={setupReady}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Main application routes */}
|
||||
<Route
|
||||
element={(
|
||||
<ProtectedLayout
|
||||
authReady={authInitialized}
|
||||
authRequired={authRequired}
|
||||
imageWorkspaceLocalDevelopment={imageWorkspaceLocalDevelopment}
|
||||
setupReady={setupReady}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Route
|
||||
path="/"
|
||||
element={<Navigate to={imageWorkspaceLocalDevelopment ? '/image-canvas' : '/opencode-chat'} replace />}
|
||||
/>
|
||||
<Route path="/project-config" element={<ProjectConfiguration />} />
|
||||
<Route path="/nitu-home" element={<Home />} />
|
||||
<Route path="/kangaroo" element={<Navigate to="/project-config" replace />} />
|
||||
<Route path="/subagents" element={<Navigate to="/project-config" replace />} />
|
||||
<Route path="/chat" element={<Navigate to="/opencode-chat" replace />} />
|
||||
<Route path="/deliverables" element={<PreviewScene />} />
|
||||
<Route path="/image-canvas" element={<ImageCanvas />} />
|
||||
<Route path="/workbench/:projectId" element={<Workbench />} />
|
||||
<Route path="/opencode-chat" element={<ProjectChatRoute />} />
|
||||
<Route path="/projects" element={<Projects />} />
|
||||
<Route path="/sessions" element={<Sessions />} />
|
||||
<Route path="/models" element={<Models />} />
|
||||
<Route path="/settings/*" element={<Settings />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
{/* Global toast notifications */}
|
||||
<Toaster
|
||||
position="bottom-right"
|
||||
richColors
|
||||
closeButton
|
||||
style={{ zIndex: 99999 }}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
Reference in New Issue
Block a user