mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
262 lines
8.3 KiB
TypeScript
262 lines
8.3 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 { TitleBar } from '@/components/TitleBar';
|
|
import { useUpdateChecker } from '@/hooks/useUpdateChecker';
|
|
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
|
import { safeExecute } from '@/utils/logger';
|
|
import { TITLEBAR_HEIGHT } from '@/constants';
|
|
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 safeExecute(async () => {
|
|
const [currentVersion, hasSeenWelcome, preference] = await Promise.all([
|
|
window.electronAPI.kobold.getCurrentVersion(),
|
|
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 (currentVersion) {
|
|
setCurrentScreenWithTransition('launch');
|
|
} else {
|
|
setCurrentScreenWithTransition('download');
|
|
}
|
|
|
|
if (currentVersion) {
|
|
setTimeout(() => {
|
|
checkForUpdates();
|
|
}, 2000);
|
|
}
|
|
}, 'Error checking installation:');
|
|
|
|
setHasInitialized(true);
|
|
};
|
|
|
|
checkInstallation();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
const handleBinaryUpdate = async (download: DownloadItem) => {
|
|
await safeExecute(async () => {
|
|
const success = await sharedHandleDownload({
|
|
item: download,
|
|
isUpdate: true,
|
|
wasCurrentBinary: true,
|
|
});
|
|
|
|
if (success) {
|
|
dismissUpdate();
|
|
}
|
|
}, 'Failed to update binary:');
|
|
};
|
|
|
|
const handleDownloadComplete = async () => {
|
|
await safeExecute(async () => {
|
|
await window.electronAPI.kobold.getCurrentVersion();
|
|
}, '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');
|
|
}
|
|
};
|
|
|
|
const isAnyModalOpen = settingsOpened || showEjectModal || showUpdateModal;
|
|
|
|
return (
|
|
<AppShell
|
|
header={{ height: TITLEBAR_HEIGHT }}
|
|
padding={currentScreen === 'interface' ? 0 : 'md'}
|
|
>
|
|
<AppShell.Header style={{ display: 'flex', flexDirection: 'column' }}>
|
|
<TitleBar
|
|
currentScreen={currentScreen || 'welcome'}
|
|
currentTab={activeInterfaceTab}
|
|
onTabChange={setActiveInterfaceTab}
|
|
onEject={handleEject}
|
|
onOpenSettings={() => setSettingsOpened(true)}
|
|
isModalOpen={isAnyModalOpen}
|
|
/>
|
|
</AppShell.Header>
|
|
<AppShell.Main
|
|
style={{
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
minHeight:
|
|
currentScreen === 'welcome' ? '100vh' : 'calc(100vh - 2rem)',
|
|
}}
|
|
>
|
|
{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 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>
|
|
);
|
|
};
|