gerbil/src/App.tsx

299 lines
9.4 KiB
TypeScript

import { useState, useEffect } from 'react';
import { AppShell, Loader, Center, Stack, Text } from '@mantine/core';
import { DownloadScreen } from '@/components/screens/Download';
import { LaunchScreen } from '@/components/screens/Launch';
import { InterfaceScreen } from '@/components/screens/Interface';
import { WelcomeScreen } from '@/components/screens/Welcome';
import { UpdateAvailableModal } from '@/components/UpdateAvailableModal';
import { SettingsModal } from '@/components/settings/SettingsModal';
import { EjectConfirmModal } from '@/components/EjectConfirmModal';
import { ScreenTransition } from '@/components/ScreenTransition';
import { AppHeader } from '@/components/AppHeader';
import { useUpdateChecker } from '@/hooks/useUpdateChecker';
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import { UI } from '@/constants';
import { Logger } from '@/utils/logger';
import type { DownloadItem } from '@/types/electron';
import type { InterfaceTab, FrontendPreference, Screen } from '@/types';
export const App = () => {
const [currentScreen, setCurrentScreen] = useState<Screen | null>(null);
const [settingsOpened, setSettingsOpened] = useState(false);
const [hasInitialized, setHasInitialized] = useState(false);
const [showEjectModal, setShowEjectModal] = useState(false);
const [activeInterfaceTab, setActiveInterfaceTab] =
useState<InterfaceTab>('terminal');
const [isImageGenerationMode, setIsImageGenerationMode] = useState(false);
const [frontendPreference, setFrontendPreference] =
useState<FrontendPreference>('koboldcpp');
const {
updateInfo: binaryUpdateInfo,
showUpdateModal,
checkForUpdates,
dismissUpdate,
} = useUpdateChecker();
const {
handleDownload: sharedHandleDownload,
downloading,
downloadProgress,
} = useKoboldVersions();
const setCurrentScreenWithTransition = (screen: Screen) => {
setCurrentScreen(screen);
};
useEffect(() => {
const checkInstallation = async () => {
await Logger.safeExecute(async () => {
const [versions, currentBinaryPath, hasSeenWelcome, preference] =
await Promise.all([
window.electronAPI.kobold.getInstalledVersions(),
window.electronAPI.config.get(
'currentKoboldBinary'
) as Promise<string>,
window.electronAPI.config.get('hasSeenWelcome') as Promise<boolean>,
window.electronAPI.config.get(
'frontendPreference'
) as Promise<FrontendPreference>,
]);
setFrontendPreference(preference || 'koboldcpp');
if (!hasSeenWelcome) {
setCurrentScreenWithTransition('welcome');
} else if (versions.length > 0) {
let current = null;
if (currentBinaryPath) {
current = versions.find((v) => v.path === currentBinaryPath);
}
if (!current) {
current = versions[0];
if (current) {
await window.electronAPI.config.set(
'currentKoboldBinary',
current.path
);
}
}
setCurrentScreenWithTransition('launch');
} else {
setCurrentScreenWithTransition('download');
}
if (versions.length > 0) {
setTimeout(() => {
checkForUpdates();
}, 2000);
}
}, 'Error checking installation:');
setHasInitialized(true);
};
checkInstallation();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleBinaryUpdate = async (download: DownloadItem) => {
await Logger.safeExecute(async () => {
const success = await sharedHandleDownload({
item: download,
isUpdate: true,
wasCurrentBinary: true,
});
if (success) {
dismissUpdate();
}
}, 'Failed to update binary:');
};
const handleDownloadComplete = async () => {
await Logger.safeExecute(async () => {
const [versions, currentBinaryPath] = await Promise.all([
window.electronAPI.kobold.getInstalledVersions(),
window.electronAPI.config.get('currentKoboldBinary') as Promise<string>,
]);
if (versions.length > 0) {
let current = null;
if (currentBinaryPath) {
current = versions.find((v) => v.path === currentBinaryPath);
}
if (!current) {
current = versions[0];
if (current) {
await window.electronAPI.config.set(
'currentKoboldBinary',
current.path
);
}
}
}
}, 'Error refreshing versions after download:');
setTimeout(() => {
setCurrentScreenWithTransition('launch');
}, 100);
};
const handleLaunch = () => {
setActiveInterfaceTab('terminal');
setCurrentScreenWithTransition('interface');
};
const handleBackToLaunch = () => {
setCurrentScreenWithTransition('launch');
};
const handleEject = async () => {
const skipEjectConfirmation = await window.electronAPI.config.get(
'skipEjectConfirmation'
);
if (skipEjectConfirmation) {
performEject();
} else {
setShowEjectModal(true);
}
};
const performEject = () => {
window.electronAPI.kobold.stopKoboldCpp();
handleBackToLaunch();
};
const handleEjectConfirm = async (skipConfirmation: boolean) => {
if (skipConfirmation) {
await window.electronAPI.config.set('skipEjectConfirmation', true);
}
performEject();
};
const handleWelcomeComplete = async () => {
await window.electronAPI.config.set('hasSeenWelcome', true);
const versions = await window.electronAPI.kobold.getInstalledVersions();
if (versions.length > 0) {
setCurrentScreenWithTransition('launch');
} else {
setCurrentScreenWithTransition('download');
}
};
return (
<AppShell
header={{ height: currentScreen === 'welcome' ? 0 : UI.HEADER_HEIGHT }}
padding={currentScreen === 'interface' ? 0 : 'md'}
>
{currentScreen !== 'welcome' && (
<AppHeader
currentScreen={currentScreen}
activeInterfaceTab={activeInterfaceTab}
setActiveInterfaceTab={setActiveInterfaceTab}
isImageGenerationMode={isImageGenerationMode}
frontendPreference={frontendPreference}
onEject={handleEject}
onSettingsOpen={() => setSettingsOpened(true)}
/>
)}
<AppShell.Main
style={{
position: 'relative',
overflow: 'hidden',
minHeight:
currentScreen === 'welcome'
? '100vh'
: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
}}
>
{currentScreen === null ? (
<Center h="100%" style={{ minHeight: '25rem' }}>
<Stack align="center" gap="lg">
<Loader size="xl" type="dots" />
<Text c="dimmed" size="lg">
Loading...
</Text>
</Stack>
</Center>
) : (
<>
<ScreenTransition
isActive={currentScreen === 'welcome'}
shouldAnimate={hasInitialized}
>
<WelcomeScreen onGetStarted={handleWelcomeComplete} />
</ScreenTransition>
<ScreenTransition
isActive={currentScreen === 'download'}
shouldAnimate={hasInitialized}
>
<DownloadScreen onDownloadComplete={handleDownloadComplete} />
</ScreenTransition>
<ScreenTransition
isActive={currentScreen === 'launch'}
shouldAnimate={hasInitialized}
>
<LaunchScreen
onLaunch={handleLaunch}
onLaunchModeChange={setIsImageGenerationMode}
/>
</ScreenTransition>
<ScreenTransition
isActive={currentScreen === 'interface'}
shouldAnimate={hasInitialized}
>
<InterfaceScreen
activeTab={activeInterfaceTab}
onTabChange={setActiveInterfaceTab}
isImageGenerationMode={isImageGenerationMode}
frontendPreference={frontendPreference}
/>
</ScreenTransition>
</>
)}
{showUpdateModal && binaryUpdateInfo && (
<UpdateAvailableModal
opened={showUpdateModal}
onClose={dismissUpdate}
currentVersion={binaryUpdateInfo.currentVersion}
availableUpdate={binaryUpdateInfo.availableUpdate}
onUpdate={handleBinaryUpdate}
isDownloading={
downloading === binaryUpdateInfo.availableUpdate.name
}
downloadProgress={
downloadProgress[binaryUpdateInfo.availableUpdate.name] || 0
}
/>
)}
</AppShell.Main>
<SettingsModal
opened={settingsOpened}
onClose={async () => {
setSettingsOpened(false);
const preference = await Logger.safeExecute(
() =>
window.electronAPI.config.get(
'frontendPreference'
) as Promise<FrontendPreference>,
'Failed to load frontend preference:'
);
setFrontendPreference(preference || 'koboldcpp');
}}
currentScreen={currentScreen || undefined}
/>
<EjectConfirmModal
opened={showEjectModal}
onClose={() => setShowEjectModal(false)}
onConfirm={handleEjectConfirm}
/>
</AppShell>
);
};