import { useState, useEffect, useMemo } from 'react'; import { AppShell, Loader, Center, Stack, Text, useMantineColorScheme, } from '@mantine/core'; import { UpdateAvailableModal } from '@/components/App/UpdateAvailableModal'; import { EjectConfirmModal } from '@/components/App/EjectConfirmModal'; import { BackendCrashModal } from '@/components/App/BackendCrashModal'; import { TitleBar } from '@/components/App/TitleBar'; import { StatusBar } from '@/components/App/StatusBar'; import { ErrorBoundary } from '@/components/App/ErrorBoundary'; import { AppRouter } from '@/components/App/Router'; import { NotepadContainer } from '@/components/Notepad/Container'; import { useUpdateChecker } from '@/hooks/useUpdateChecker'; import { useKoboldBackendsStore } from '@/stores/koboldBackends'; import { usePreferencesStore } from '@/stores/preferences'; import { useLaunchConfigStore } from '@/stores/launchConfig'; import { getDefaultInterfaceTab } from '@/utils/interface'; import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants'; import type { DownloadItem } from '@/types/electron'; import type { InterfaceTab, Screen } from '@/types'; import type { KoboldCrashInfo } from '@/types/ipc'; export const App = () => { const [currentScreen, setCurrentScreen] = useState(null); const [hasInitialized, setHasInitialized] = useState(false); const [activeInterfaceTab, setActiveInterfaceTab] = useState('terminal'); const [isServerReady, setIsServerReady] = useState(false); const [ejectConfirmModalOpen, setEjectConfirmModalOpen] = useState(false); const [crashInfo, setCrashInfo] = useState(null); const isInterfaceScreen = currentScreen === 'interface'; const { resolvedColorScheme: appColorScheme, systemMonitoringEnabled, frontendPreference, imageGenerationFrontendPreference, } = usePreferencesStore(); const { setColorScheme } = useMantineColorScheme(); const { model, sdmodel, isTextMode, isImageGenerationMode } = useLaunchConfigStore(); const defaultInterfaceTab = useMemo( () => getDefaultInterfaceTab({ frontendPreference, imageGenerationFrontendPreference, isTextMode, isImageGenerationMode, }), [ frontendPreference, imageGenerationFrontendPreference, isTextMode, isImageGenerationMode, ] ); useEffect(() => { const cleanup = window.electronAPI.kobold.onServerReady(() => { setIsServerReady(true); setActiveInterfaceTab(defaultInterfaceTab); }); return cleanup; }, [defaultInterfaceTab]); useEffect(() => { setColorScheme(appColorScheme); }, [appColorScheme, setColorScheme]); useEffect(() => { const updateTray = async () => { const config = await window.electronAPI.kobold.getSelectedConfig(); const displayModel = model || sdmodel || null; const modelName = displayModel ? displayModel.split('/').pop() || displayModel : null; window.electronAPI.app.updateTrayState({ screen: currentScreen, model: modelName, config: config || null, monitoringEnabled: systemMonitoringEnabled, }); }; void updateTray(); }, [currentScreen, model, sdmodel, systemMonitoringEnabled]); const performEject = () => { window.electronAPI.kobold.stopKoboldCpp(); setIsServerReady(false); setActiveInterfaceTab('terminal'); setCurrentScreen('launch'); }; useEffect(() => { const ejectCleanup = window.electronAPI.app.onTrayEject(() => { performEject(); }); return () => { ejectCleanup(); }; }, []); useEffect(() => { const crashCleanup = window.electronAPI.kobold.onKoboldCrashed( (crashData) => { setCrashInfo(crashData); } ); return () => { crashCleanup(); }; }, []); const { updateInfo: binaryUpdateInfo, showUpdateModal, checkForUpdates, skipUpdate, closeModal, } = useUpdateChecker(); const { handleDownload, loadingRemote } = useKoboldBackendsStore(); const determineScreen = ( currentVersion: unknown, hasSeenWelcome: boolean ) => { if (!hasSeenWelcome) { setCurrentScreen('welcome'); } else if (currentVersion) { setCurrentScreen('launch'); } else { setCurrentScreen('download'); } }; useEffect(() => { const checkInstallation = async () => { const [currentBackend, hasSeenWelcome] = await Promise.all([ window.electronAPI.kobold.getCurrentBackend(), window.electronAPI.config.get('hasSeenWelcome') as Promise, ]); determineScreen(currentBackend, hasSeenWelcome); setHasInitialized(true); }; checkInstallation(); }, []); useEffect(() => { const runUpdateCheck = async () => { if (loadingRemote || !hasInitialized) return; const currentBackend = await window.electronAPI.kobold.getCurrentBackend(); if (currentBackend) { setTimeout(() => { checkForUpdates(); }, 5000); const interval = setInterval( () => { checkForUpdates(); }, 6 * 60 * 60 * 1000 ); return () => clearInterval(interval); } }; runUpdateCheck(); }, [loadingRemote, hasInitialized, checkForUpdates]); const handleBinaryUpdate = async (download: DownloadItem) => { const currentBackend = await window.electronAPI.kobold.getCurrentBackend(); await handleDownload({ item: download, isUpdate: true, wasCurrentBinary: true, oldBackendPath: currentBackend?.path, }); closeModal(); }; const handleEject = async () => { const skipEjectConfirmation = await window.electronAPI.config.get( 'skipEjectConfirmation' ); if (skipEjectConfirmation) { performEject(); } else { setEjectConfirmModalOpen(true); } }; const handleEjectConfirm = (skipConfirmation: boolean) => { if (skipConfirmation) { window.electronAPI.config.set('skipEjectConfirmation', true); } performEject(); }; const handleWelcomeComplete = async () => { window.electronAPI.config.set('hasSeenWelcome', true); const currentBackend = await window.electronAPI.kobold.getCurrentBackend(); determineScreen(currentBackend, true); }; const handleDownloadComplete = () => setCurrentScreen('launch'); const handleLaunch = () => { setActiveInterfaceTab('terminal'); setCurrentScreen('interface'); }; return ( {currentScreen === null ? (
Loading...
) : ( )}
setEjectConfirmModalOpen(false)} onConfirm={handleEjectConfirm} /> setCrashInfo(null)} crashInfo={crashInfo} />
); };