183 lines
5.2 KiB
TypeScript
183 lines
5.2 KiB
TypeScript
/**
|
|
* Root Application Component
|
|
* Handles routing and global providers
|
|
*/
|
|
import { Routes, Route, useNavigate, useLocation } from 'react-router-dom';
|
|
import { Component, useEffect } 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 { Dashboard } from './pages/Dashboard';
|
|
import { Chat } from './pages/Chat';
|
|
import { Channels } from './pages/Channels';
|
|
import { Skills } from './pages/Skills';
|
|
import { Cron } from './pages/Cron';
|
|
import { Settings } from './pages/Settings';
|
|
import { Setup } from './pages/Setup';
|
|
import { useSettingsStore } from './stores/settings';
|
|
import { useGatewayStore } from './stores/gateway';
|
|
|
|
|
|
/**
|
|
* 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: '#f87171',
|
|
background: '#0f172a',
|
|
minHeight: '100vh',
|
|
fontFamily: 'monospace'
|
|
}}>
|
|
<h1 style={{ fontSize: '24px', marginBottom: '16px' }}>Something went wrong</h1>
|
|
<pre style={{
|
|
whiteSpace: 'pre-wrap',
|
|
wordBreak: 'break-all',
|
|
background: '#1e293b',
|
|
padding: '16px',
|
|
borderRadius: '8px',
|
|
fontSize: '14px'
|
|
}}>
|
|
{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: '#3b82f6',
|
|
color: 'white',
|
|
border: 'none',
|
|
borderRadius: '6px',
|
|
cursor: 'pointer'
|
|
}}
|
|
>
|
|
Reload
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
function App() {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const initSettings = useSettingsStore((state) => state.init);
|
|
const theme = useSettingsStore((state) => state.theme);
|
|
const language = useSettingsStore((state) => state.language);
|
|
const setupComplete = useSettingsStore((state) => state.setupComplete);
|
|
const initGateway = useGatewayStore((state) => state.init);
|
|
|
|
useEffect(() => {
|
|
initSettings();
|
|
}, [initSettings]);
|
|
|
|
// Sync i18n language with persisted settings on mount
|
|
useEffect(() => {
|
|
if (language && language !== i18n.language) {
|
|
i18n.changeLanguage(language);
|
|
}
|
|
}, [language]);
|
|
|
|
// Initialize Gateway connection on mount
|
|
useEffect(() => {
|
|
initGateway();
|
|
}, [initGateway]);
|
|
|
|
// Redirect to setup wizard if not complete
|
|
useEffect(() => {
|
|
if (!setupComplete && !location.pathname.startsWith('/setup')) {
|
|
navigate('/setup');
|
|
}
|
|
}, [setupComplete, location.pathname, navigate]);
|
|
|
|
// Listen for navigation events from main process
|
|
useEffect(() => {
|
|
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 theme
|
|
useEffect(() => {
|
|
const root = window.document.documentElement;
|
|
root.classList.remove('light', 'dark');
|
|
|
|
if (theme === 'system') {
|
|
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
? 'dark'
|
|
: 'light';
|
|
root.classList.add(systemTheme);
|
|
} else {
|
|
root.classList.add(theme);
|
|
}
|
|
}, [theme]);
|
|
|
|
return (
|
|
<ErrorBoundary>
|
|
<TooltipProvider delayDuration={300}>
|
|
<Routes>
|
|
{/* Setup wizard (shown on first launch) */}
|
|
<Route path="/setup/*" element={<Setup />} />
|
|
|
|
{/* Main application routes */}
|
|
<Route element={<MainLayout />}>
|
|
<Route path="/" element={<Chat />} />
|
|
<Route path="/dashboard" element={<Dashboard />} />
|
|
<Route path="/channels" element={<Channels />} />
|
|
<Route path="/skills" element={<Skills />} />
|
|
<Route path="/cron" element={<Cron />} />
|
|
<Route path="/settings/*" element={<Settings />} />
|
|
</Route>
|
|
</Routes>
|
|
|
|
{/* Global toast notifications */}
|
|
<Toaster
|
|
position="bottom-right"
|
|
richColors
|
|
closeButton
|
|
style={{ zIndex: 99999 }}
|
|
/>
|
|
</TooltipProvider>
|
|
</ErrorBoundary>
|
|
);
|
|
}
|
|
|
|
export default App;
|