mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
new titlebar component that'll work on wayland, move about dialog to the settings modal
This commit is contained in:
parent
0b829cbfe2
commit
2413f66c41
21 changed files with 564 additions and 667 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "gerbil",
|
"name": "gerbil",
|
||||||
"productName": "Gerbil",
|
"productName": "Gerbil",
|
||||||
"version": "0.9.1",
|
"version": "0.9.2",
|
||||||
"description": "Run Large Language Models locally",
|
"description": "Run Large Language Models locally",
|
||||||
"main": "out/main/index.js",
|
"main": "out/main/index.js",
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
|
|
@ -80,8 +80,8 @@
|
||||||
"vite": "^7.1.3"
|
"vite": "^7.1.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^8.2.7",
|
"@mantine/core": "^8.2.8",
|
||||||
"@mantine/hooks": "^8.2.7",
|
"@mantine/hooks": "^8.2.8",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"execa": "^9.6.0",
|
"execa": "^9.6.0",
|
||||||
"lucide-react": "^0.542.0",
|
"lucide-react": "^0.542.0",
|
||||||
|
|
|
||||||
26
src/App.tsx
26
src/App.tsx
|
|
@ -8,11 +8,11 @@ import { UpdateAvailableModal } from '@/components/UpdateAvailableModal';
|
||||||
import { SettingsModal } from '@/components/settings/SettingsModal';
|
import { SettingsModal } from '@/components/settings/SettingsModal';
|
||||||
import { EjectConfirmModal } from '@/components/EjectConfirmModal';
|
import { EjectConfirmModal } from '@/components/EjectConfirmModal';
|
||||||
import { ScreenTransition } from '@/components/ScreenTransition';
|
import { ScreenTransition } from '@/components/ScreenTransition';
|
||||||
import { AppHeader } from '@/components/AppHeader';
|
import { TitleBar } from '@/components/TitleBar';
|
||||||
import { useUpdateChecker } from '@/hooks/useUpdateChecker';
|
import { useUpdateChecker } from '@/hooks/useUpdateChecker';
|
||||||
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
||||||
import { UI } from '@/constants';
|
|
||||||
import { Logger } from '@/utils/logger';
|
import { Logger } from '@/utils/logger';
|
||||||
|
import { TITLEBAR_HEIGHT } from '@/constants';
|
||||||
import type { DownloadItem } from '@/types/electron';
|
import type { DownloadItem } from '@/types/electron';
|
||||||
import type { InterfaceTab, FrontendPreference, Screen } from '@/types';
|
import type { InterfaceTab, FrontendPreference, Screen } from '@/types';
|
||||||
|
|
||||||
|
|
@ -149,28 +149,24 @@ export const App = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
header={{ height: currentScreen === 'welcome' ? 0 : UI.HEADER_HEIGHT }}
|
header={{ height: TITLEBAR_HEIGHT }}
|
||||||
padding={currentScreen === 'interface' ? 0 : 'md'}
|
padding={currentScreen === 'interface' ? 0 : 'md'}
|
||||||
>
|
>
|
||||||
{currentScreen !== 'welcome' && (
|
<AppShell.Header style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<AppHeader
|
<TitleBar
|
||||||
currentScreen={currentScreen}
|
currentScreen={currentScreen || 'welcome'}
|
||||||
activeInterfaceTab={activeInterfaceTab}
|
currentTab={activeInterfaceTab}
|
||||||
setActiveInterfaceTab={setActiveInterfaceTab}
|
onTabChange={setActiveInterfaceTab}
|
||||||
isImageGenerationMode={isImageGenerationMode}
|
|
||||||
frontendPreference={frontendPreference}
|
|
||||||
onEject={handleEject}
|
onEject={handleEject}
|
||||||
onSettingsOpen={() => setSettingsOpened(true)}
|
onOpenSettings={() => setSettingsOpened(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
</AppShell.Header>
|
||||||
<AppShell.Main
|
<AppShell.Main
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
minHeight:
|
minHeight:
|
||||||
currentScreen === 'welcome'
|
currentScreen === 'welcome' ? '100vh' : 'calc(100vh - 2rem)',
|
||||||
? '100vh'
|
|
||||||
: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{currentScreen === null ? (
|
{currentScreen === null ? (
|
||||||
|
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import {
|
|
||||||
AppShell,
|
|
||||||
Group,
|
|
||||||
ActionIcon,
|
|
||||||
rem,
|
|
||||||
Button,
|
|
||||||
Select,
|
|
||||||
Title,
|
|
||||||
Image,
|
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { Settings, ArrowLeft, CircleFadingArrowUp } from 'lucide-react';
|
|
||||||
import { soundAssets, playSound, initializeAudio } from '@/utils/sounds';
|
|
||||||
import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
|
|
||||||
import iconUrl from '/icon.png';
|
|
||||||
import { FRONTENDS } from '@/constants';
|
|
||||||
import type { InterfaceTab, FrontendPreference, Screen } from '@/types';
|
|
||||||
|
|
||||||
interface AppHeaderProps {
|
|
||||||
currentScreen: Screen | null;
|
|
||||||
activeInterfaceTab: InterfaceTab;
|
|
||||||
setActiveInterfaceTab: (tab: InterfaceTab) => void;
|
|
||||||
isImageGenerationMode: boolean;
|
|
||||||
frontendPreference: FrontendPreference;
|
|
||||||
onEject: () => void;
|
|
||||||
onSettingsOpen: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AppHeader = ({
|
|
||||||
currentScreen,
|
|
||||||
activeInterfaceTab,
|
|
||||||
setActiveInterfaceTab,
|
|
||||||
isImageGenerationMode,
|
|
||||||
frontendPreference,
|
|
||||||
onEject,
|
|
||||||
onSettingsOpen,
|
|
||||||
}: AppHeaderProps) => {
|
|
||||||
const [logoClickCount, setLogoClickCount] = useState(0);
|
|
||||||
const [isElephantMode, setIsElephantMode] = useState(false);
|
|
||||||
const [isMouseSqueaking, setIsMouseSqueaking] = useState(false);
|
|
||||||
const { hasUpdate, openReleasePage } = useAppUpdateChecker();
|
|
||||||
|
|
||||||
const handleLogoClick = async () => {
|
|
||||||
await initializeAudio();
|
|
||||||
setLogoClickCount((prev) => prev + 1);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (logoClickCount >= 10 && Math.random() < 0.1) {
|
|
||||||
setIsElephantMode(true);
|
|
||||||
await playSound(soundAssets.elephant, 0.6);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsElephantMode(false);
|
|
||||||
}, 1500);
|
|
||||||
} else {
|
|
||||||
setIsMouseSqueaking(true);
|
|
||||||
const squeakNumber = Math.floor(Math.random() * 5);
|
|
||||||
await playSound(soundAssets.mouseSqueaks[squeakNumber], 0.4);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsMouseSqueaking(false);
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
void 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppShell.Header>
|
|
||||||
<Group h="100%" px="md" justify="space-between" align="center">
|
|
||||||
<div style={{ minWidth: '6.25rem' }}>
|
|
||||||
{currentScreen === 'interface' ? (
|
|
||||||
<Button
|
|
||||||
variant="light"
|
|
||||||
color="red"
|
|
||||||
leftSection={<ArrowLeft size={16} />}
|
|
||||||
onClick={onEject}
|
|
||||||
>
|
|
||||||
Eject
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Group gap="sm" align="center">
|
|
||||||
<Image
|
|
||||||
src={iconUrl}
|
|
||||||
alt="Gerbil"
|
|
||||||
w={28}
|
|
||||||
h={28}
|
|
||||||
style={{
|
|
||||||
minWidth: 28,
|
|
||||||
minHeight: 28,
|
|
||||||
cursor: 'pointer',
|
|
||||||
userSelect: 'none',
|
|
||||||
transition: 'transform 0.15s ease-in-out',
|
|
||||||
transform: isElephantMode
|
|
||||||
? 'scale(1.3) rotate(5deg)'
|
|
||||||
: 'scale(1) rotate(0deg)',
|
|
||||||
animation: isElephantMode
|
|
||||||
? 'elephantShake 1.5s ease-in-out'
|
|
||||||
: isMouseSqueaking
|
|
||||||
? 'mouseSqueak 0.3s ease-in-out'
|
|
||||||
: 'none',
|
|
||||||
}}
|
|
||||||
onClick={handleLogoClick}
|
|
||||||
/>
|
|
||||||
<Title order={4} fw={500}>
|
|
||||||
Gerbil
|
|
||||||
</Title>
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{currentScreen === 'interface' && (
|
|
||||||
<Select
|
|
||||||
value={activeInterfaceTab}
|
|
||||||
onChange={(value) =>
|
|
||||||
setActiveInterfaceTab((value || 'terminal') as InterfaceTab)
|
|
||||||
}
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
value: 'chat',
|
|
||||||
label:
|
|
||||||
frontendPreference === 'sillytavern'
|
|
||||||
? FRONTENDS.SILLYTAVERN
|
|
||||||
: isImageGenerationMode
|
|
||||||
? FRONTENDS.STABLE_UI
|
|
||||||
: FRONTENDS.KOBOLDAI_LITE,
|
|
||||||
},
|
|
||||||
{ value: 'terminal', label: 'Terminal' },
|
|
||||||
]}
|
|
||||||
allowDeselect={false}
|
|
||||||
styles={{
|
|
||||||
input: {
|
|
||||||
minWidth: '9.375rem',
|
|
||||||
textAlign: 'center',
|
|
||||||
border: 'none',
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
fontWeight: 500,
|
|
||||||
},
|
|
||||||
dropdown: {
|
|
||||||
minWidth: '9.375rem',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
minWidth: '6.25rem',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
gap: '0.5rem',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{hasUpdate && (
|
|
||||||
<Tooltip label="New release available" position="bottom">
|
|
||||||
<ActionIcon
|
|
||||||
variant="light"
|
|
||||||
color="orange"
|
|
||||||
size="xl"
|
|
||||||
onClick={openReleasePage}
|
|
||||||
aria-label="New release available"
|
|
||||||
>
|
|
||||||
<CircleFadingArrowUp
|
|
||||||
style={{ width: rem(20), height: rem(20) }}
|
|
||||||
/>
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<Tooltip label="Settings" position="bottom">
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
color="gray"
|
|
||||||
size="xl"
|
|
||||||
onClick={onSettingsOpen}
|
|
||||||
aria-label="Open settings"
|
|
||||||
style={{
|
|
||||||
transition: 'all 200ms ease',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Settings style={{ width: rem(20), height: rem(20) }} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</Group>
|
|
||||||
</AppShell.Header>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
255
src/components/TitleBar.tsx
Normal file
255
src/components/TitleBar.tsx
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
import {
|
||||||
|
Group,
|
||||||
|
ActionIcon,
|
||||||
|
Box,
|
||||||
|
Image,
|
||||||
|
Select,
|
||||||
|
useComputedColorScheme,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import {
|
||||||
|
Minus,
|
||||||
|
Square,
|
||||||
|
X,
|
||||||
|
Settings,
|
||||||
|
Copy,
|
||||||
|
CircleFadingArrowUp,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { soundAssets, playSound, initializeAudio } from '@/utils/sounds';
|
||||||
|
import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
|
||||||
|
import iconUrl from '/icon.png';
|
||||||
|
import type { InterfaceTab, Screen } from '@/types';
|
||||||
|
import { PRODUCT_NAME, TITLEBAR_HEIGHT } from '@/constants';
|
||||||
|
|
||||||
|
interface TitleBarProps {
|
||||||
|
currentScreen: Screen;
|
||||||
|
currentTab?: InterfaceTab;
|
||||||
|
onTabChange?: (tab: InterfaceTab) => void;
|
||||||
|
onEject?: () => void;
|
||||||
|
onOpenSettings?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TitleBar = ({
|
||||||
|
currentScreen,
|
||||||
|
currentTab,
|
||||||
|
onTabChange,
|
||||||
|
onEject,
|
||||||
|
onOpenSettings,
|
||||||
|
}: TitleBarProps) => {
|
||||||
|
const computedColorScheme = useComputedColorScheme('light', {
|
||||||
|
getInitialValueInEffect: false,
|
||||||
|
});
|
||||||
|
const { hasUpdate, openReleasePage } = useAppUpdateChecker();
|
||||||
|
const [logoClickCount, setLogoClickCount] = useState(0);
|
||||||
|
const [isElephantMode, setIsElephantMode] = useState(false);
|
||||||
|
const [isMouseSqueaking, setIsMouseSqueaking] = useState(false);
|
||||||
|
const [isMaximized, setIsMaximized] = useState(false);
|
||||||
|
|
||||||
|
const handleMinimize = () => {
|
||||||
|
window.electronAPI.app.minimizeWindow();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMaximize = async () => {
|
||||||
|
await window.electronAPI.app.maximizeWindow();
|
||||||
|
setIsMaximized(!isMaximized);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
window.electronAPI.app.closeWindow();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogoClick = async () => {
|
||||||
|
await initializeAudio();
|
||||||
|
setLogoClickCount((prev) => prev + 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (logoClickCount >= 10 && Math.random() < 0.1) {
|
||||||
|
setIsElephantMode(true);
|
||||||
|
await playSound(soundAssets.elephant, 0.6);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsElephantMode(false);
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
setIsMouseSqueaking(true);
|
||||||
|
const squeakNumber = Math.floor(Math.random() * 5);
|
||||||
|
await playSound(soundAssets.mouseSqueaks[squeakNumber], 0.4);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsMouseSqueaking(false);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
void 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
height: TITLEBAR_HEIGHT,
|
||||||
|
padding: '0.125rem 0.5rem',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
backgroundColor:
|
||||||
|
computedColorScheme === 'dark'
|
||||||
|
? 'var(--mantine-color-dark-7)'
|
||||||
|
: 'var(--mantine-color-gray-0)',
|
||||||
|
borderBottom: '1px solid var(--mantine-color-default-border)',
|
||||||
|
WebkitAppRegion: 'drag',
|
||||||
|
userSelect: 'none',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group gap="0.5rem" align="center" style={{ WebkitAppRegion: 'no-drag' }}>
|
||||||
|
<Image
|
||||||
|
src={iconUrl}
|
||||||
|
alt={PRODUCT_NAME}
|
||||||
|
w={24}
|
||||||
|
h={24}
|
||||||
|
style={{
|
||||||
|
minWidth: 24,
|
||||||
|
minHeight: 24,
|
||||||
|
cursor: 'pointer',
|
||||||
|
userSelect: 'none',
|
||||||
|
transition: 'transform 0.15s ease-in-out',
|
||||||
|
transform: isElephantMode
|
||||||
|
? 'scale(1.3) rotate(5deg)'
|
||||||
|
: 'scale(1) rotate(0deg)',
|
||||||
|
animation: isElephantMode
|
||||||
|
? 'elephantShake 1.5s ease-in-out'
|
||||||
|
: isMouseSqueaking
|
||||||
|
? 'mouseSqueak 0.3s ease-in-out'
|
||||||
|
: 'none',
|
||||||
|
}}
|
||||||
|
onClick={handleLogoClick}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
WebkitAppRegion: 'no-drag',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentScreen === 'interface' && (
|
||||||
|
<Select
|
||||||
|
placeholder="Interface"
|
||||||
|
value={currentTab}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (value === 'eject') {
|
||||||
|
onEject?.();
|
||||||
|
} else {
|
||||||
|
onTabChange?.(value as InterfaceTab);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
value: 'chat',
|
||||||
|
label: 'Chat',
|
||||||
|
},
|
||||||
|
{ value: 'terminal', label: 'Terminal' },
|
||||||
|
{ value: 'eject', label: 'Eject' },
|
||||||
|
]}
|
||||||
|
allowDeselect={false}
|
||||||
|
variant="unstyled"
|
||||||
|
size="sm"
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
minWidth: '120px',
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
textAlign: 'center',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
fontWeight: 500,
|
||||||
|
userSelect: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group gap="0.125rem" style={{ WebkitAppRegion: 'no-drag' }}>
|
||||||
|
{hasUpdate && (
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="orange"
|
||||||
|
size="2rem"
|
||||||
|
onClick={openReleasePage}
|
||||||
|
aria-label="New release available"
|
||||||
|
style={{
|
||||||
|
borderRadius: '0.25rem',
|
||||||
|
margin: '0.125rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircleFadingArrowUp size="1.25rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
size="2rem"
|
||||||
|
onClick={onOpenSettings}
|
||||||
|
aria-label="Open settings"
|
||||||
|
style={{
|
||||||
|
borderRadius: '0.25rem',
|
||||||
|
margin: '0.125rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Settings size="1.25rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: '0.0625rem',
|
||||||
|
height: '1.25rem',
|
||||||
|
backgroundColor: 'var(--mantine-color-default-border)',
|
||||||
|
margin: '0 0.25rem',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
icon: <Minus size="1rem" />,
|
||||||
|
onClick: handleMinimize,
|
||||||
|
color: undefined,
|
||||||
|
label: 'Minimize window',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: isMaximized ? <Copy size="1rem" /> : <Square size="1rem" />,
|
||||||
|
onClick: handleMaximize,
|
||||||
|
color: undefined,
|
||||||
|
label: isMaximized ? 'Restore window' : 'Maximize window',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <X size="1.25rem" />,
|
||||||
|
onClick: handleClose,
|
||||||
|
color: 'red' as const,
|
||||||
|
label: 'Close window',
|
||||||
|
},
|
||||||
|
].map((button, index) => (
|
||||||
|
<ActionIcon
|
||||||
|
key={index}
|
||||||
|
variant="subtle"
|
||||||
|
size="2rem"
|
||||||
|
onClick={button.onClick}
|
||||||
|
color={button.color}
|
||||||
|
aria-label={button.label}
|
||||||
|
style={{
|
||||||
|
borderRadius: '0.25rem',
|
||||||
|
margin: '0.125rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{button.icon}
|
||||||
|
</ActionIcon>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Box, Text, Stack } from '@mantine/core';
|
import { Box, Text, Stack } from '@mantine/core';
|
||||||
import { UI, SILLYTAVERN, FRONTENDS } from '@/constants';
|
import { SILLYTAVERN, FRONTENDS } from '@/constants';
|
||||||
import type { ServerTabMode, FrontendPreference } from '@/types';
|
import type { ServerTabMode, FrontendPreference } from '@/types';
|
||||||
|
|
||||||
interface ServerTabProps {
|
interface ServerTabProps {
|
||||||
|
|
@ -60,7 +60,7 @@ export const ServerTab = ({
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
|
height: 'calc(100vh - 2rem)',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronDown } from 'lucide-react';
|
||||||
import styles from '@/styles/layout.module.css';
|
import styles from '@/styles/layout.module.css';
|
||||||
import { UI, SERVER_READY_SIGNALS } from '@/constants';
|
import { SERVER_READY_SIGNALS } from '@/constants';
|
||||||
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
|
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfigStore';
|
import { useLaunchConfigStore } from '@/stores/launchConfigStore';
|
||||||
import type { FrontendPreference } from '@/types';
|
import type { FrontendPreference } from '@/types';
|
||||||
|
|
@ -104,7 +104,7 @@ export const TerminalTab = ({
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
height: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
|
height: 'calc(100vh - 2rem)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
backgroundColor: isDark
|
backgroundColor: isDark
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,32 @@ export const AdvancedTab = () => {
|
||||||
disabled={!isGpuBackend}
|
disabled={!isGpuBackend}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||||
|
<CheckboxWithTooltip
|
||||||
|
checked={noavx2}
|
||||||
|
onChange={handleNoavx2Change}
|
||||||
|
label="Disable AVX2"
|
||||||
|
tooltip={
|
||||||
|
!backendSupport?.noavx2 && !isLoading
|
||||||
|
? 'This binary does not support the no-AVX2 mode.'
|
||||||
|
: 'Do not use AVX2 instructions, a slower compatibility mode for older devices.'
|
||||||
|
}
|
||||||
|
disabled={isLoading || !backendSupport?.noavx2}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CheckboxWithTooltip
|
||||||
|
checked={failsafe}
|
||||||
|
onChange={handleFailsafeChange}
|
||||||
|
label="Failsafe"
|
||||||
|
tooltip={
|
||||||
|
!backendSupport?.failsafe && !isLoading
|
||||||
|
? 'This binary does not support failsafe mode.'
|
||||||
|
: 'Use failsafe mode, extremely slow CPU only compatibility mode that should work on all devices. Can be combined with useclblast if your device supports OpenCL.'
|
||||||
|
}
|
||||||
|
disabled={isLoading || !backendSupport?.failsafe}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -177,41 +203,6 @@ export const AdvancedTab = () => {
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<Group gap="xs" align="center" mb="md">
|
|
||||||
<Text size="sm" fw={600}>
|
|
||||||
Hardware Compatibility
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
|
||||||
<CheckboxWithTooltip
|
|
||||||
checked={noavx2}
|
|
||||||
onChange={handleNoavx2Change}
|
|
||||||
label="Disable AVX2"
|
|
||||||
tooltip={
|
|
||||||
!backendSupport?.noavx2 && !isLoading
|
|
||||||
? 'This binary does not support the no-AVX2 mode.'
|
|
||||||
: 'Do not use AVX2 instructions, a slower compatibility mode for older devices.'
|
|
||||||
}
|
|
||||||
disabled={isLoading || !backendSupport?.noavx2}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CheckboxWithTooltip
|
|
||||||
checked={failsafe}
|
|
||||||
onChange={handleFailsafeChange}
|
|
||||||
label="Failsafe"
|
|
||||||
tooltip={
|
|
||||||
!backendSupport?.failsafe && !isLoading
|
|
||||||
? 'This binary does not support failsafe mode.'
|
|
||||||
: 'Use failsafe mode, extremely slow CPU only compatibility mode that should work on all devices. Can be combined with useclblast if your device supports OpenCL.'
|
|
||||||
}
|
|
||||||
disabled={isLoading || !backendSupport?.failsafe}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Group gap="xs" align="center" mb="xs">
|
<Group gap="xs" align="center" mb="xs">
|
||||||
<Text size="sm" fw={500}>
|
<Text size="sm" fw={500}>
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ export const GeneralTab = () => {
|
||||||
max={131072}
|
max={131072}
|
||||||
step={1}
|
step={1}
|
||||||
onChange={handleContextSizeChangeWithStep}
|
onChange={handleContextSizeChangeWithStep}
|
||||||
|
style={{ marginBottom: '0.5rem' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
||||||
|
|
@ -314,7 +314,7 @@ export const LaunchScreen = ({
|
||||||
onChange={setActiveTab}
|
onChange={setActiveTab}
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
maxHeight: '22rem',
|
maxHeight: '24rem',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
},
|
},
|
||||||
|
|
@ -350,7 +350,7 @@ export const LaunchScreen = ({
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Group justify="flex-end" pt="md">
|
<Group justify="flex-end">
|
||||||
<WarningDisplay warnings={combinedWarnings}>
|
<WarningDisplay warnings={combinedWarnings}>
|
||||||
<Button
|
<Button
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
Card,
|
Card,
|
||||||
|
|
@ -23,7 +24,7 @@ export const WelcomeScreen = ({ onGetStarted }: WelcomeScreenProps) => (
|
||||||
<Stack gap="lg" align="center">
|
<Stack gap="lg" align="center">
|
||||||
<Stack gap="md" align="center">
|
<Stack gap="md" align="center">
|
||||||
<Title order={1} ta="center">
|
<Title order={1} ta="center">
|
||||||
Gerbil
|
{PRODUCT_NAME}
|
||||||
</Title>
|
</Title>
|
||||||
<Text size="lg" c="dimmed" ta="center" maw={600}>
|
<Text size="lg" c="dimmed" ta="center" maw={600}>
|
||||||
Run Large Language Models locally
|
Run Large Language Models locally
|
||||||
|
|
|
||||||
126
src/components/settings/AboutTab.tsx
Normal file
126
src/components/settings/AboutTab.tsx
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Text,
|
||||||
|
Stack,
|
||||||
|
Anchor,
|
||||||
|
Group,
|
||||||
|
Card,
|
||||||
|
Image,
|
||||||
|
Center,
|
||||||
|
Badge,
|
||||||
|
rem,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { Github } from 'lucide-react';
|
||||||
|
import type { VersionInfo } from '@/types/electron';
|
||||||
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
|
import iconUrl from '/icon.png';
|
||||||
|
export const AboutTab = () => {
|
||||||
|
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const loadVersionInfo = async () => {
|
||||||
|
try {
|
||||||
|
const info = await window.electronAPI.app.getVersionInfo();
|
||||||
|
setVersionInfo(info);
|
||||||
|
} catch (error) {
|
||||||
|
window.electronAPI.logs.logError(
|
||||||
|
'Failed to load version info',
|
||||||
|
error as Error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadVersionInfo();
|
||||||
|
}, []);
|
||||||
|
if (!versionInfo) {
|
||||||
|
return (
|
||||||
|
<Center h="100%">
|
||||||
|
<Text c="dimmed">Loading version information...</Text>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionItems = [
|
||||||
|
{ label: 'App Version', value: versionInfo.appVersion },
|
||||||
|
{ label: 'Electron', value: versionInfo.electronVersion },
|
||||||
|
{ label: 'Node.js', value: versionInfo.nodeVersion },
|
||||||
|
{ label: 'Chromium', value: versionInfo.chromeVersion },
|
||||||
|
{ label: 'V8', value: versionInfo.v8Version },
|
||||||
|
{
|
||||||
|
label: 'Operating System',
|
||||||
|
value: `${versionInfo.platform} ${versionInfo.arch} (${versionInfo.osVersion})`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
<Card withBorder radius="md" p="xs">
|
||||||
|
<Group align="center" gap="lg" wrap="nowrap">
|
||||||
|
<Image
|
||||||
|
src={iconUrl}
|
||||||
|
alt={PRODUCT_NAME}
|
||||||
|
w={64}
|
||||||
|
h={64}
|
||||||
|
style={{ minWidth: 64, minHeight: 64 }}
|
||||||
|
/>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Group align="center" gap="md" wrap="nowrap">
|
||||||
|
<Text size="xl" fw={700}>
|
||||||
|
{PRODUCT_NAME}
|
||||||
|
</Text>
|
||||||
|
<Badge
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
size="lg"
|
||||||
|
style={{ textTransform: 'none' }}
|
||||||
|
>
|
||||||
|
v{versionInfo.appVersion}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
<Text size="sm" c="dimmed" mt="xs">
|
||||||
|
A desktop app to easily run Large Language Models locally.
|
||||||
|
</Text>
|
||||||
|
<Anchor
|
||||||
|
href="https://github.com/lone-cloud/gerbil"
|
||||||
|
target="_blank"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
window.electronAPI.app.openExternal(
|
||||||
|
'https://github.com/lone-cloud/gerbil'
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
style={{ textDecoration: 'none' }}
|
||||||
|
>
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Github style={{ width: rem(16), height: rem(16) }} />
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
GitHub
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Anchor>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card withBorder radius="md" p="xs">
|
||||||
|
<Stack gap="xs">
|
||||||
|
{versionItems.map((item, index) => (
|
||||||
|
<Group key={index} gap="md" align="center" wrap="nowrap">
|
||||||
|
<Text size="sm" fw={500} c="dimmed" style={{ minWidth: '120px' }}>
|
||||||
|
{item.label}:
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
ff="monospace"
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.value}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Modal, Tabs, Text, Group, rem, Button, Box } from '@mantine/core';
|
import { Modal, Tabs, Text, Group, rem, Button, Box } from '@mantine/core';
|
||||||
import { Settings, Palette, SlidersHorizontal, GitBranch } from 'lucide-react';
|
import {
|
||||||
|
Settings,
|
||||||
|
Palette,
|
||||||
|
SlidersHorizontal,
|
||||||
|
GitBranch,
|
||||||
|
Info,
|
||||||
|
} from 'lucide-react';
|
||||||
import { GeneralTab } from '@/components/settings/GeneralTab';
|
import { GeneralTab } from '@/components/settings/GeneralTab';
|
||||||
import { VersionsTab } from '@/components/settings/VersionsTab';
|
import { VersionsTab } from '@/components/settings/VersionsTab';
|
||||||
import { AppearanceTab } from '@/components/settings/AppearanceTab';
|
import { AppearanceTab } from '@/components/settings/AppearanceTab';
|
||||||
|
import { AboutTab } from '@/components/settings/AboutTab';
|
||||||
import type { Screen } from '@/types';
|
import type { Screen } from '@/types';
|
||||||
|
|
||||||
interface SettingsModalProps {
|
interface SettingsModalProps {
|
||||||
|
|
@ -92,6 +99,10 @@ export const SettingsModal = ({
|
||||||
paddingLeft: '24px',
|
paddingLeft: '24px',
|
||||||
paddingRight: '24px',
|
paddingRight: '24px',
|
||||||
},
|
},
|
||||||
|
tabLabel: {
|
||||||
|
textAlign: 'left',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
|
|
@ -121,6 +132,12 @@ export const SettingsModal = ({
|
||||||
>
|
>
|
||||||
Appearance
|
Appearance
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="about"
|
||||||
|
leftSection={<Info style={{ width: rem(16), height: rem(16) }} />}
|
||||||
|
>
|
||||||
|
About
|
||||||
|
</Tabs.Tab>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
|
||||||
<Tabs.Panel value="general">
|
<Tabs.Panel value="general">
|
||||||
|
|
@ -136,12 +153,16 @@ export const SettingsModal = ({
|
||||||
<Tabs.Panel value="appearance">
|
<Tabs.Panel value="appearance">
|
||||||
<AppearanceTab />
|
<AppearanceTab />
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
<Tabs.Panel value="about">
|
||||||
|
<AboutTab />
|
||||||
|
</Tabs.Panel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--mantine-color-body)',
|
backgroundColor: 'var(--mantine-color-body)',
|
||||||
padding: '1.5rem',
|
padding: '0.5rem 1.5rem 1.5rem',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,7 @@ export const PRODUCT_NAME = 'Gerbil';
|
||||||
|
|
||||||
export const CONFIG_FILE_NAME = 'config.json';
|
export const CONFIG_FILE_NAME = 'config.json';
|
||||||
|
|
||||||
export const UI = {
|
export const TITLEBAR_HEIGHT = '2.5rem';
|
||||||
HEADER_HEIGHT: 60,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const { HEADER_HEIGHT } = UI;
|
|
||||||
|
|
||||||
export const SERVER_READY_SIGNALS = {
|
export const SERVER_READY_SIGNALS = {
|
||||||
KOBOLDCPP: 'Please connect to custom endpoint at',
|
KOBOLDCPP: 'Please connect to custom endpoint at',
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,8 @@ export class GerbilApp {
|
||||||
this.hardwareManager,
|
this.hardwareManager,
|
||||||
this.binaryManager,
|
this.binaryManager,
|
||||||
this.logManager,
|
this.logManager,
|
||||||
this.sillyTavernManager
|
this.sillyTavernManager,
|
||||||
|
this.windowManager
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +94,6 @@ export class GerbilApp {
|
||||||
app.setAppUserModelId('com.gerbil.app');
|
app.setAppUserModelId('com.gerbil.app');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.windowManager.setupApplicationMenu();
|
|
||||||
this.windowManager.createMainWindow();
|
this.windowManager.createMainWindow();
|
||||||
this.ipcHandlers.setupHandlers();
|
this.ipcHandlers.setupHandlers();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { ipcMain, shell, app } from 'electron';
|
import { ipcMain, shell, app } from 'electron';
|
||||||
|
import * as os from 'os';
|
||||||
import type { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
import type { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
||||||
import type { ConfigManager } from '@/main/managers/ConfigManager';
|
import type { ConfigManager } from '@/main/managers/ConfigManager';
|
||||||
import type { LogManager } from '@/main/managers/LogManager';
|
import type { LogManager } from '@/main/managers/LogManager';
|
||||||
import type { SillyTavernManager } from '@/main/managers/SillyTavernManager';
|
import type { SillyTavernManager } from '@/main/managers/SillyTavernManager';
|
||||||
|
import type { WindowManager } from '@/main/managers/WindowManager';
|
||||||
import { HardwareManager } from '@/main/managers/HardwareManager';
|
import { HardwareManager } from '@/main/managers/HardwareManager';
|
||||||
import { BinaryManager } from '@/main/managers/BinaryManager';
|
import { BinaryManager } from '@/main/managers/BinaryManager';
|
||||||
|
|
||||||
|
|
@ -13,6 +15,7 @@ export class IPCHandlers {
|
||||||
private sillyTavernManager: SillyTavernManager;
|
private sillyTavernManager: SillyTavernManager;
|
||||||
private hardwareManager: HardwareManager;
|
private hardwareManager: HardwareManager;
|
||||||
private binaryManager: BinaryManager;
|
private binaryManager: BinaryManager;
|
||||||
|
private windowManager: WindowManager;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
koboldManager: KoboldCppManager,
|
koboldManager: KoboldCppManager,
|
||||||
|
|
@ -20,7 +23,8 @@ export class IPCHandlers {
|
||||||
hardwareManager: HardwareManager,
|
hardwareManager: HardwareManager,
|
||||||
binaryManager: BinaryManager,
|
binaryManager: BinaryManager,
|
||||||
logManager: LogManager,
|
logManager: LogManager,
|
||||||
sillyTavernManager: SillyTavernManager
|
sillyTavernManager: SillyTavernManager,
|
||||||
|
windowManager: WindowManager
|
||||||
) {
|
) {
|
||||||
this.koboldManager = koboldManager;
|
this.koboldManager = koboldManager;
|
||||||
this.configManager = configManager;
|
this.configManager = configManager;
|
||||||
|
|
@ -28,6 +32,7 @@ export class IPCHandlers {
|
||||||
this.sillyTavernManager = sillyTavernManager;
|
this.sillyTavernManager = sillyTavernManager;
|
||||||
this.hardwareManager = hardwareManager;
|
this.hardwareManager = hardwareManager;
|
||||||
this.binaryManager = binaryManager;
|
this.binaryManager = binaryManager;
|
||||||
|
this.windowManager = windowManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async launchKoboldCppWithCustomFrontends(args: string[] = []) {
|
private async launchKoboldCppWithCustomFrontends(args: string[] = []) {
|
||||||
|
|
@ -146,6 +151,17 @@ export class IPCHandlers {
|
||||||
|
|
||||||
ipcMain.handle('app:getVersion', () => app.getVersion());
|
ipcMain.handle('app:getVersion', () => app.getVersion());
|
||||||
|
|
||||||
|
ipcMain.handle('app:getVersionInfo', () => ({
|
||||||
|
appVersion: app.getVersion(),
|
||||||
|
electronVersion: process.versions.electron,
|
||||||
|
nodeVersion: process.versions.node,
|
||||||
|
chromeVersion: process.versions.chrome,
|
||||||
|
v8Version: process.versions.v8,
|
||||||
|
osVersion: os.release(),
|
||||||
|
platform: process.platform,
|
||||||
|
arch: process.arch,
|
||||||
|
}));
|
||||||
|
|
||||||
ipcMain.handle('app:openExternal', async (_, url) => {
|
ipcMain.handle('app:openExternal', async (_, url) => {
|
||||||
try {
|
try {
|
||||||
await shell.openExternal(url);
|
await shell.openExternal(url);
|
||||||
|
|
@ -155,6 +171,25 @@ export class IPCHandlers {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('app:minimizeWindow', () => {
|
||||||
|
const mainWindow = this.windowManager.getMainWindow();
|
||||||
|
mainWindow?.minimize();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('app:maximizeWindow', () => {
|
||||||
|
const mainWindow = this.windowManager.getMainWindow();
|
||||||
|
if (mainWindow?.isMaximized()) {
|
||||||
|
mainWindow.restore();
|
||||||
|
} else {
|
||||||
|
mainWindow?.maximize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('app:closeWindow', () => {
|
||||||
|
const mainWindow = this.windowManager.getMainWindow();
|
||||||
|
mainWindow?.close();
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('logs:logError', (_, message: string, error?: Error) => {
|
ipcMain.handle('logs:logError', (_, message: string, error?: Error) => {
|
||||||
this.logManager.logError(message, error);
|
this.logManager.logError(message, error);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import { BrowserWindow, app, Menu, shell, nativeImage, screen } from 'electron';
|
import { BrowserWindow, app, shell, nativeImage, screen, Menu } from 'electron';
|
||||||
import * as os from 'os';
|
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { stripVTControlCharacters } from 'util';
|
import { stripVTControlCharacters } from 'util';
|
||||||
import { GITHUB_API, PRODUCT_NAME } from '../../constants';
|
import { PRODUCT_NAME } from '../../constants';
|
||||||
import type { IPCChannel, IPCChannelPayloads } from '@/types/ipc';
|
import type { IPCChannel, IPCChannelPayloads } from '@/types/ipc';
|
||||||
import { readTextFile } from '@/utils/fs';
|
|
||||||
|
|
||||||
export class WindowManager {
|
export class WindowManager {
|
||||||
private mainWindow: BrowserWindow | null = null;
|
private mainWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
|
private isDevelopment(): boolean {
|
||||||
|
return process.env.NODE_ENV === 'development' || !app.isPackaged;
|
||||||
|
}
|
||||||
|
|
||||||
private getIconPath(): string {
|
private getIconPath(): string {
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
const projectRoot = join(__dirname, '../..');
|
const projectRoot = join(__dirname, '../..');
|
||||||
|
|
@ -17,20 +19,17 @@ export class WindowManager {
|
||||||
return join(process.resourcesPath, 'assets/icon.png');
|
return join(process.resourcesPath, 'assets/icon.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
private isDevelopment(): boolean {
|
|
||||||
return process.env.NODE_ENV === 'development' || !app.isPackaged;
|
|
||||||
}
|
|
||||||
|
|
||||||
createMainWindow(): BrowserWindow {
|
createMainWindow(): BrowserWindow {
|
||||||
const iconPath = this.getIconPath();
|
const iconPath = this.getIconPath();
|
||||||
const iconImage = nativeImage.createFromPath(iconPath);
|
const iconImage = nativeImage.createFromPath(iconPath);
|
||||||
|
|
||||||
const { workAreaSize } = screen.getPrimaryDisplay();
|
const { workAreaSize } = screen.getPrimaryDisplay();
|
||||||
const windowHeight = Math.floor(workAreaSize.height * 0.9);
|
const windowHeight = Math.floor(workAreaSize.height * 0.85);
|
||||||
|
|
||||||
this.mainWindow = new BrowserWindow({
|
this.mainWindow = new BrowserWindow({
|
||||||
width: 1000,
|
width: 1000,
|
||||||
height: windowHeight,
|
height: windowHeight,
|
||||||
|
frame: false,
|
||||||
icon: iconImage,
|
icon: iconImage,
|
||||||
title: PRODUCT_NAME,
|
title: PRODUCT_NAME,
|
||||||
show: false,
|
show: false,
|
||||||
|
|
@ -47,29 +46,6 @@ export class WindowManager {
|
||||||
|
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
this.mainWindow.setIcon(iconImage);
|
this.mainWindow.setIcon(iconImage);
|
||||||
|
|
||||||
if (
|
|
||||||
process.env.WAYLAND_DISPLAY ||
|
|
||||||
process.env.XDG_SESSION_TYPE === 'wayland'
|
|
||||||
) {
|
|
||||||
this.mainWindow.setRepresentedFilename('');
|
|
||||||
|
|
||||||
const retrySetIcon = () => {
|
|
||||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
||||||
this.mainWindow.setIcon(iconImage);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setTimeout(retrySetIcon, 50);
|
|
||||||
setTimeout(retrySetIcon, 100);
|
|
||||||
setTimeout(retrySetIcon, 250);
|
|
||||||
setTimeout(retrySetIcon, 500);
|
|
||||||
setTimeout(retrySetIcon, 1000);
|
|
||||||
|
|
||||||
this.mainWindow.on('show', retrySetIcon);
|
|
||||||
this.mainWindow.on('focus', retrySetIcon);
|
|
||||||
this.mainWindow.on('restore', retrySetIcon);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mainWindow.once('ready-to-show', () => {
|
this.mainWindow.once('ready-to-show', () => {
|
||||||
|
|
@ -119,36 +95,10 @@ export class WindowManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setupContextMenu();
|
this.setupContextMenu();
|
||||||
|
|
||||||
return this.mainWindow;
|
return this.mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMainWindow(): BrowserWindow | null {
|
|
||||||
return this.mainWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
sendToRenderer<T extends IPCChannel>(
|
|
||||||
channel: T,
|
|
||||||
...args: IPCChannelPayloads[T]
|
|
||||||
): void {
|
|
||||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
||||||
this.mainWindow.webContents.send(channel, ...args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sendKoboldOutput(message: string, raw?: boolean): void {
|
|
||||||
const cleanMessage = stripVTControlCharacters(message);
|
|
||||||
this.sendToRenderer(
|
|
||||||
'kobold-output',
|
|
||||||
raw ? cleanMessage : `${cleanMessage}\n`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public cleanup() {
|
|
||||||
if (this.mainWindow) {
|
|
||||||
this.mainWindow.removeAllListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupContextMenu() {
|
private setupContextMenu() {
|
||||||
if (!this.mainWindow) return;
|
if (!this.mainWindow) return;
|
||||||
|
|
||||||
|
|
@ -191,172 +141,30 @@ export class WindowManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setupApplicationMenu() {
|
getMainWindow(): BrowserWindow | null {
|
||||||
const isDev = this.isDevelopment();
|
return this.mainWindow;
|
||||||
|
|
||||||
const template = [
|
|
||||||
{
|
|
||||||
label: 'File',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'Quit',
|
|
||||||
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q',
|
|
||||||
click: () => {
|
|
||||||
app.quit();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Edit',
|
|
||||||
submenu: [
|
|
||||||
{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
|
|
||||||
{ label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' },
|
|
||||||
{ type: 'separator' },
|
|
||||||
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' },
|
|
||||||
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' },
|
|
||||||
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'View',
|
|
||||||
submenu: [
|
|
||||||
...(isDev
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: 'Reload',
|
|
||||||
accelerator: 'CmdOrCtrl+R',
|
|
||||||
role: 'reload' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Force Reload',
|
|
||||||
accelerator: 'CmdOrCtrl+Shift+R',
|
|
||||||
role: 'forceReload' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Toggle Developer Tools',
|
|
||||||
accelerator: 'F12',
|
|
||||||
click: () => {
|
|
||||||
if (this.mainWindow) {
|
|
||||||
this.mainWindow.webContents.toggleDevTools();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: 'separator' as const },
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
label: 'Actual Size',
|
|
||||||
accelerator: 'CmdOrCtrl+0',
|
|
||||||
role: 'resetZoom',
|
|
||||||
},
|
|
||||||
{ label: 'Zoom In', accelerator: 'CmdOrCtrl+Plus', role: 'zoomIn' },
|
|
||||||
{ label: 'Zoom Out', accelerator: 'CmdOrCtrl+-', role: 'zoomOut' },
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: 'Toggle Fullscreen',
|
|
||||||
accelerator: 'F11',
|
|
||||||
role: 'togglefullscreen',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Window',
|
|
||||||
submenu: [
|
|
||||||
{ label: 'Minimize', accelerator: 'CmdOrCtrl+M', role: 'minimize' },
|
|
||||||
{ label: 'Close', accelerator: 'CmdOrCtrl+W', role: 'close' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Help',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'KoboldCpp Wiki',
|
|
||||||
click: () => {
|
|
||||||
shell.openExternal(
|
|
||||||
`https://github.com/${GITHUB_API.KOBOLDCPP_REPO}/wiki`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'View Error Logs',
|
|
||||||
click: () => {
|
|
||||||
const logsDir = join(app.getPath('userData'), 'logs');
|
|
||||||
shell.openPath(logsDir);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: 'About',
|
|
||||||
click: async () => {
|
|
||||||
await this.showAboutDialog();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const menu = Menu.buildFromTemplate(
|
|
||||||
template as Parameters<typeof Menu.buildFromTemplate>[0]
|
|
||||||
);
|
|
||||||
Menu.setApplicationMenu(menu);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async showAboutDialog() {
|
sendToRenderer<T extends IPCChannel>(
|
||||||
const packagePath = join(app.getAppPath(), 'package.json');
|
channel: T,
|
||||||
const packageContent = await readTextFile(packagePath);
|
...args: IPCChannelPayloads[T]
|
||||||
const packageInfo = packageContent ? JSON.parse(packageContent) : {};
|
): void {
|
||||||
const electronVersion = process.versions.electron;
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
const chromeVersion = process.versions.chrome;
|
this.mainWindow.webContents.send(channel, ...args);
|
||||||
const nodeVersion = process.versions.node;
|
}
|
||||||
const v8Version = process.versions.v8;
|
}
|
||||||
const osInfo = `${process.platform} ${process.arch} ${os.release()}`;
|
|
||||||
|
sendKoboldOutput(message: string, raw?: boolean): void {
|
||||||
const versionText = `Version: ${packageInfo.version}
|
const cleanMessage = stripVTControlCharacters(message);
|
||||||
Electron: ${electronVersion}
|
this.sendToRenderer(
|
||||||
Chromium: ${chromeVersion}
|
'kobold-output',
|
||||||
Node.js: ${nodeVersion}
|
raw ? cleanMessage : `${cleanMessage}\n`
|
||||||
V8: ${v8Version}
|
);
|
||||||
OS: ${osInfo}`;
|
}
|
||||||
|
|
||||||
const iconPath = this.getIconPath();
|
public cleanup() {
|
||||||
const iconImage = nativeImage.createFromPath(iconPath);
|
if (this.mainWindow) {
|
||||||
|
this.mainWindow.removeAllListeners();
|
||||||
const aboutWindow = new BrowserWindow({
|
|
||||||
width: 400,
|
|
||||||
height: 450,
|
|
||||||
icon: iconImage,
|
|
||||||
modal: true,
|
|
||||||
parent: this.mainWindow!,
|
|
||||||
resizable: false,
|
|
||||||
minimizable: false,
|
|
||||||
maximizable: false,
|
|
||||||
show: false,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: false,
|
|
||||||
contextIsolation: true,
|
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
aboutWindow.setMenu(null);
|
|
||||||
|
|
||||||
const htmlPath = this.getTemplatePath('about-modal.html');
|
|
||||||
await aboutWindow.loadFile(htmlPath);
|
|
||||||
|
|
||||||
aboutWindow.webContents.executeJavaScript(
|
|
||||||
`setVersionInfo(\`${versionText}\`)`
|
|
||||||
);
|
|
||||||
|
|
||||||
aboutWindow.once('ready-to-show', () => {
|
|
||||||
aboutWindow.show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getTemplatePath(filename: string): string {
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
return join(__dirname, '../../src/main/templates', filename);
|
|
||||||
}
|
}
|
||||||
return join(process.resourcesPath, 'templates', filename);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>About Gerbil</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family:
|
|
||||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 1.25rem;
|
|
||||||
background: #f5f5f5;
|
|
||||||
color: #333;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 25rem;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: white;
|
|
||||||
padding: 1.875rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
box-shadow: 0 0.125rem 0.625rem rgba(0, 0, 0, 0.1);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
margin: 0 0 1.25rem 0;
|
|
||||||
color: #2c3e50;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
.version-info {
|
|
||||||
text-align: left;
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 0.9375rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
margin: 1.25rem 0;
|
|
||||||
font-family: 'Monaco', 'Menlo', monospace;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
.github-link {
|
|
||||||
color: #0366d6;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.github-link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
.buttons {
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
display: flex;
|
|
||||||
gap: 0.625rem;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border: 0.0625rem solid #ddd;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
background: white;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
background: #f0f0f0;
|
|
||||||
}
|
|
||||||
.primary {
|
|
||||||
background: #228be6;
|
|
||||||
color: white;
|
|
||||||
border-color: #228be6;
|
|
||||||
}
|
|
||||||
.primary:hover {
|
|
||||||
background: #1c7ed6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode styles - moved to end to ensure proper override */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body {
|
|
||||||
background: #1a1a1a !important;
|
|
||||||
color: #e0e0e0 !important;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
background: #2d2d2d !important;
|
|
||||||
box-shadow: 0 0.125rem 0.625rem rgba(0, 0, 0, 0.3) !important;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #ffffff !important;
|
|
||||||
}
|
|
||||||
.version-info {
|
|
||||||
background: #3a3a3a !important;
|
|
||||||
color: #e0e0e0 !important;
|
|
||||||
}
|
|
||||||
.github-link {
|
|
||||||
color: #58a6ff !important;
|
|
||||||
}
|
|
||||||
.github-link:hover {
|
|
||||||
color: #79c0ff !important;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background: #3a3a3a !important;
|
|
||||||
border-color: #555 !important;
|
|
||||||
color: #e0e0e0 !important;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
background: #4a4a4a !important;
|
|
||||||
}
|
|
||||||
.primary {
|
|
||||||
background: #228be6 !important;
|
|
||||||
border-color: #228be6 !important;
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
.primary:hover {
|
|
||||||
background: #1c7ed6 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Gerbil</h1>
|
|
||||||
<div class="version-info" id="version-info">
|
|
||||||
<!-- Version information will be injected here -->
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<a href="#" class="github-link" onclick="openGitHub()">
|
|
||||||
View on GitHub →
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<div class="buttons">
|
|
||||||
<button class="primary" onclick="closeDialog()">Close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function setVersionInfo(info) {
|
|
||||||
document.getElementById('version-info').innerHTML = info.replace(
|
|
||||||
/\n/g,
|
|
||||||
'<br>'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openGitHub() {
|
|
||||||
// Send IPC message directly since this is a simple modal
|
|
||||||
if (window.electronAPI && window.electronAPI.app) {
|
|
||||||
window.electronAPI.app.openExternal(
|
|
||||||
'https://github.com/lone-cloud/gerbil'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeDialog() {
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle keyboard shortcuts
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
closeDialog();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -78,6 +78,10 @@ const koboldAPI: KoboldAPI = {
|
||||||
const appAPI: AppAPI = {
|
const appAPI: AppAPI = {
|
||||||
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
|
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
|
||||||
getVersion: () => ipcRenderer.invoke('app:getVersion'),
|
getVersion: () => ipcRenderer.invoke('app:getVersion'),
|
||||||
|
getVersionInfo: () => ipcRenderer.invoke('app:getVersionInfo'),
|
||||||
|
minimizeWindow: () => ipcRenderer.invoke('app:minimizeWindow'),
|
||||||
|
maximizeWindow: () => ipcRenderer.invoke('app:maximizeWindow'),
|
||||||
|
closeWindow: () => ipcRenderer.invoke('app:closeWindow'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const configAPI: ConfigAPI = {
|
const configAPI: ConfigAPI = {
|
||||||
|
|
|
||||||
|
|
@ -45,38 +45,38 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* AppHeader animations */
|
/* TitleBar gerbil icon animations */
|
||||||
@keyframes elephantShake {
|
@keyframes elephantShake {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
transform: scale(1.3) rotate(5deg) translateX(0);
|
transform: scale(1.1) rotate(5deg) translateX(0);
|
||||||
}
|
}
|
||||||
10% {
|
10% {
|
||||||
transform: scale(1.4) rotate(-3deg) translateX(-2px);
|
transform: scale(1.2) rotate(-3deg) translateX(-2px);
|
||||||
}
|
}
|
||||||
20% {
|
20% {
|
||||||
transform: scale(1.5) rotate(8deg) translateX(2px);
|
transform: scale(1.3) rotate(8deg) translateX(2px);
|
||||||
}
|
}
|
||||||
30% {
|
30% {
|
||||||
transform: scale(1.4) rotate(-5deg) translateX(-1px);
|
transform: scale(1.2) rotate(-5deg) translateX(-1px);
|
||||||
}
|
}
|
||||||
40% {
|
40% {
|
||||||
transform: scale(1.6) rotate(10deg) translateX(3px);
|
transform: scale(1.4) rotate(10deg) translateX(3px);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
transform: scale(1.3) rotate(-8deg) translateX(-2px);
|
transform: scale(1.2) rotate(-8deg) translateX(-2px);
|
||||||
}
|
}
|
||||||
60% {
|
60% {
|
||||||
transform: scale(1.5) rotate(6deg) translateX(1px);
|
transform: scale(1.3) rotate(6deg) translateX(1px);
|
||||||
}
|
}
|
||||||
70% {
|
70% {
|
||||||
transform: scale(1.2) rotate(-4deg) translateX(-1px);
|
transform: scale(1) rotate(-4deg) translateX(-1px);
|
||||||
}
|
}
|
||||||
80% {
|
80% {
|
||||||
transform: scale(1.4) rotate(3deg) translateX(2px);
|
transform: scale(1.2) rotate(3deg) translateX(2px);
|
||||||
}
|
}
|
||||||
90% {
|
90% {
|
||||||
transform: scale(1.1) rotate(-2deg) translateX(-1px);
|
transform: scale(1) rotate(-2deg) translateX(-1px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
15
src/types/electron.d.ts
vendored
15
src/types/electron.d.ts
vendored
|
|
@ -131,9 +131,24 @@ export interface KoboldAPI {
|
||||||
removeAllListeners: (channel: string) => void;
|
removeAllListeners: (channel: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VersionInfo {
|
||||||
|
appVersion: string;
|
||||||
|
electronVersion: string;
|
||||||
|
nodeVersion: string;
|
||||||
|
chromeVersion: string;
|
||||||
|
v8Version: string;
|
||||||
|
osVersion: string;
|
||||||
|
platform: string;
|
||||||
|
arch: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppAPI {
|
export interface AppAPI {
|
||||||
openExternal: (url: string) => Promise<void>;
|
openExternal: (url: string) => Promise<void>;
|
||||||
getVersion: () => Promise<string>;
|
getVersion: () => Promise<string>;
|
||||||
|
getVersionInfo: () => Promise<VersionInfo>;
|
||||||
|
minimizeWindow: () => void;
|
||||||
|
maximizeWindow: () => void;
|
||||||
|
closeWindow: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigAPI {
|
export interface ConfigAPI {
|
||||||
|
|
|
||||||
22
yarn.lock
22
yarn.lock
|
|
@ -871,9 +871,9 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@mantine/core@npm:^8.2.7":
|
"@mantine/core@npm:^8.2.8":
|
||||||
version: 8.2.7
|
version: 8.2.8
|
||||||
resolution: "@mantine/core@npm:8.2.7"
|
resolution: "@mantine/core@npm:8.2.8"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/react": "npm:^0.26.28"
|
"@floating-ui/react": "npm:^0.26.28"
|
||||||
clsx: "npm:^2.1.1"
|
clsx: "npm:^2.1.1"
|
||||||
|
|
@ -882,19 +882,19 @@ __metadata:
|
||||||
react-textarea-autosize: "npm:8.5.9"
|
react-textarea-autosize: "npm:8.5.9"
|
||||||
type-fest: "npm:^4.27.0"
|
type-fest: "npm:^4.27.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@mantine/hooks": 8.2.7
|
"@mantine/hooks": 8.2.8
|
||||||
react: ^18.x || ^19.x
|
react: ^18.x || ^19.x
|
||||||
react-dom: ^18.x || ^19.x
|
react-dom: ^18.x || ^19.x
|
||||||
checksum: 10c0/d10ba6ef7ac552b6bd5638b9a16c4d2405391102330d94c8df7a7d87756cdf4ec341a350eab252575eb5990f43dc4122ea13244753a9169338a43d2b996f7594
|
checksum: 10c0/9933ae22ab2faa852d8a941bdbb427c98d160b253f2977f10aad7024cd725fbca56d36a22560f3c75f8716b3d6651a599fa6d996cb131315c997242206cc4ebb
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@mantine/hooks@npm:^8.2.7":
|
"@mantine/hooks@npm:^8.2.8":
|
||||||
version: 8.2.7
|
version: 8.2.8
|
||||||
resolution: "@mantine/hooks@npm:8.2.7"
|
resolution: "@mantine/hooks@npm:8.2.8"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.x || ^19.x
|
react: ^18.x || ^19.x
|
||||||
checksum: 10c0/54dfbc36acad9dbb5e3a29d0944041f7ff31e61416cbd26ecf97c37c3a779a1b0028e05a0e6daa44cf3c3c2406c50b8f2107eb6fee3cfb6647b7524965556686
|
checksum: 10c0/0097d8f05ee684c0e78f6160aa23d431e27ef15d32c66010f369d35e86497571a52dc56824fb659ca1c66ff3dd9e6513fe4c8c9bb3e1d98118ba176b4b9feeeb
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
@ -3706,8 +3706,8 @@ __metadata:
|
||||||
resolution: "gerbil@workspace:."
|
resolution: "gerbil@workspace:."
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint/js": "npm:^9.34.0"
|
"@eslint/js": "npm:^9.34.0"
|
||||||
"@mantine/core": "npm:^8.2.7"
|
"@mantine/core": "npm:^8.2.8"
|
||||||
"@mantine/hooks": "npm:^8.2.7"
|
"@mantine/hooks": "npm:^8.2.8"
|
||||||
"@types/node": "npm:^24.3.0"
|
"@types/node": "npm:^24.3.0"
|
||||||
"@types/react": "npm:^19.1.12"
|
"@types/react": "npm:^19.1.12"
|
||||||
"@types/react-dom": "npm:^19.1.9"
|
"@types/react-dom": "npm:^19.1.9"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue