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",
|
||||
"productName": "Gerbil",
|
||||
"version": "0.9.1",
|
||||
"version": "0.9.2",
|
||||
"description": "Run Large Language Models locally",
|
||||
"main": "out/main/index.js",
|
||||
"homepage": "./",
|
||||
|
|
@ -80,8 +80,8 @@
|
|||
"vite": "^7.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/core": "^8.2.7",
|
||||
"@mantine/hooks": "^8.2.7",
|
||||
"@mantine/core": "^8.2.8",
|
||||
"@mantine/hooks": "^8.2.8",
|
||||
"axios": "^1.11.0",
|
||||
"execa": "^9.6.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 { EjectConfirmModal } from '@/components/EjectConfirmModal';
|
||||
import { ScreenTransition } from '@/components/ScreenTransition';
|
||||
import { AppHeader } from '@/components/AppHeader';
|
||||
import { TitleBar } from '@/components/TitleBar';
|
||||
import { useUpdateChecker } from '@/hooks/useUpdateChecker';
|
||||
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
||||
import { UI } from '@/constants';
|
||||
import { Logger } from '@/utils/logger';
|
||||
import { TITLEBAR_HEIGHT } from '@/constants';
|
||||
import type { DownloadItem } from '@/types/electron';
|
||||
import type { InterfaceTab, FrontendPreference, Screen } from '@/types';
|
||||
|
||||
|
|
@ -149,28 +149,24 @@ export const App = () => {
|
|||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: currentScreen === 'welcome' ? 0 : UI.HEADER_HEIGHT }}
|
||||
header={{ height: TITLEBAR_HEIGHT }}
|
||||
padding={currentScreen === 'interface' ? 0 : 'md'}
|
||||
>
|
||||
{currentScreen !== 'welcome' && (
|
||||
<AppHeader
|
||||
currentScreen={currentScreen}
|
||||
activeInterfaceTab={activeInterfaceTab}
|
||||
setActiveInterfaceTab={setActiveInterfaceTab}
|
||||
isImageGenerationMode={isImageGenerationMode}
|
||||
frontendPreference={frontendPreference}
|
||||
<AppShell.Header style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<TitleBar
|
||||
currentScreen={currentScreen || 'welcome'}
|
||||
currentTab={activeInterfaceTab}
|
||||
onTabChange={setActiveInterfaceTab}
|
||||
onEject={handleEject}
|
||||
onSettingsOpen={() => setSettingsOpened(true)}
|
||||
onOpenSettings={() => setSettingsOpened(true)}
|
||||
/>
|
||||
)}
|
||||
</AppShell.Header>
|
||||
<AppShell.Main
|
||||
style={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
minHeight:
|
||||
currentScreen === 'welcome'
|
||||
? '100vh'
|
||||
: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
|
||||
currentScreen === 'welcome' ? '100vh' : 'calc(100vh - 2rem)',
|
||||
}}
|
||||
>
|
||||
{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 { Box, Text, Stack } from '@mantine/core';
|
||||
import { UI, SILLYTAVERN, FRONTENDS } from '@/constants';
|
||||
import { SILLYTAVERN, FRONTENDS } from '@/constants';
|
||||
import type { ServerTabMode, FrontendPreference } from '@/types';
|
||||
|
||||
interface ServerTabProps {
|
||||
|
|
@ -60,7 +60,7 @@ export const ServerTab = ({
|
|||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
height: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
|
||||
height: 'calc(100vh - 2rem)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from '@mantine/core';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
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 { useLaunchConfigStore } from '@/stores/launchConfigStore';
|
||||
import type { FrontendPreference } from '@/types';
|
||||
|
|
@ -104,7 +104,7 @@ export const TerminalTab = ({
|
|||
return (
|
||||
<Box
|
||||
style={{
|
||||
height: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
|
||||
height: 'calc(100vh - 2rem)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: isDark
|
||||
|
|
|
|||
|
|
@ -125,6 +125,32 @@ export const AdvancedTab = () => {
|
|||
disabled={!isGpuBackend}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
|
@ -177,41 +203,6 @@ export const AdvancedTab = () => {
|
|||
</Stack>
|
||||
</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>
|
||||
<Group gap="xs" align="center" mb="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export const GeneralTab = () => {
|
|||
max={131072}
|
||||
step={1}
|
||||
onChange={handleContextSizeChangeWithStep}
|
||||
style={{ marginBottom: '0.5rem' }}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -314,7 +314,7 @@ export const LaunchScreen = ({
|
|||
onChange={setActiveTab}
|
||||
styles={{
|
||||
root: {
|
||||
maxHeight: '22rem',
|
||||
maxHeight: '24rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
|
|
@ -350,7 +350,7 @@ export const LaunchScreen = ({
|
|||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
<Group justify="flex-end" pt="md">
|
||||
<Group justify="flex-end">
|
||||
<WarningDisplay warnings={combinedWarnings}>
|
||||
<Button
|
||||
radius="md"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { PRODUCT_NAME } from '@/constants';
|
||||
import {
|
||||
Container,
|
||||
Card,
|
||||
|
|
@ -23,7 +24,7 @@ export const WelcomeScreen = ({ onGetStarted }: WelcomeScreenProps) => (
|
|||
<Stack gap="lg" align="center">
|
||||
<Stack gap="md" align="center">
|
||||
<Title order={1} ta="center">
|
||||
Gerbil
|
||||
{PRODUCT_NAME}
|
||||
</Title>
|
||||
<Text size="lg" c="dimmed" ta="center" maw={600}>
|
||||
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 { 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 { VersionsTab } from '@/components/settings/VersionsTab';
|
||||
import { AppearanceTab } from '@/components/settings/AppearanceTab';
|
||||
import { AboutTab } from '@/components/settings/AboutTab';
|
||||
import type { Screen } from '@/types';
|
||||
|
||||
interface SettingsModalProps {
|
||||
|
|
@ -92,6 +99,10 @@ export const SettingsModal = ({
|
|||
paddingLeft: '24px',
|
||||
paddingRight: '24px',
|
||||
},
|
||||
tabLabel: {
|
||||
textAlign: 'left',
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tabs.List>
|
||||
|
|
@ -121,6 +132,12 @@ export const SettingsModal = ({
|
|||
>
|
||||
Appearance
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="about"
|
||||
leftSection={<Info style={{ width: rem(16), height: rem(16) }} />}
|
||||
>
|
||||
About
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="general">
|
||||
|
|
@ -136,12 +153,16 @@ export const SettingsModal = ({
|
|||
<Tabs.Panel value="appearance">
|
||||
<AppearanceTab />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="about">
|
||||
<AboutTab />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
<Box
|
||||
style={{
|
||||
backgroundColor: 'var(--mantine-color-body)',
|
||||
padding: '1.5rem',
|
||||
padding: '0.5rem 1.5rem 1.5rem',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
flexShrink: 0,
|
||||
|
|
|
|||
|
|
@ -2,11 +2,7 @@ export const PRODUCT_NAME = 'Gerbil';
|
|||
|
||||
export const CONFIG_FILE_NAME = 'config.json';
|
||||
|
||||
export const UI = {
|
||||
HEADER_HEIGHT: 60,
|
||||
} as const;
|
||||
|
||||
export const { HEADER_HEIGHT } = UI;
|
||||
export const TITLEBAR_HEIGHT = '2.5rem';
|
||||
|
||||
export const SERVER_READY_SIGNALS = {
|
||||
KOBOLDCPP: 'Please connect to custom endpoint at',
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ export class GerbilApp {
|
|||
this.hardwareManager,
|
||||
this.binaryManager,
|
||||
this.logManager,
|
||||
this.sillyTavernManager
|
||||
this.sillyTavernManager,
|
||||
this.windowManager
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +94,6 @@ export class GerbilApp {
|
|||
app.setAppUserModelId('com.gerbil.app');
|
||||
}
|
||||
|
||||
this.windowManager.setupApplicationMenu();
|
||||
this.windowManager.createMainWindow();
|
||||
this.ipcHandlers.setupHandlers();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { ipcMain, shell, app } from 'electron';
|
||||
import * as os from 'os';
|
||||
import type { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
||||
import type { ConfigManager } from '@/main/managers/ConfigManager';
|
||||
import type { LogManager } from '@/main/managers/LogManager';
|
||||
import type { SillyTavernManager } from '@/main/managers/SillyTavernManager';
|
||||
import type { WindowManager } from '@/main/managers/WindowManager';
|
||||
import { HardwareManager } from '@/main/managers/HardwareManager';
|
||||
import { BinaryManager } from '@/main/managers/BinaryManager';
|
||||
|
||||
|
|
@ -13,6 +15,7 @@ export class IPCHandlers {
|
|||
private sillyTavernManager: SillyTavernManager;
|
||||
private hardwareManager: HardwareManager;
|
||||
private binaryManager: BinaryManager;
|
||||
private windowManager: WindowManager;
|
||||
|
||||
constructor(
|
||||
koboldManager: KoboldCppManager,
|
||||
|
|
@ -20,7 +23,8 @@ export class IPCHandlers {
|
|||
hardwareManager: HardwareManager,
|
||||
binaryManager: BinaryManager,
|
||||
logManager: LogManager,
|
||||
sillyTavernManager: SillyTavernManager
|
||||
sillyTavernManager: SillyTavernManager,
|
||||
windowManager: WindowManager
|
||||
) {
|
||||
this.koboldManager = koboldManager;
|
||||
this.configManager = configManager;
|
||||
|
|
@ -28,6 +32,7 @@ export class IPCHandlers {
|
|||
this.sillyTavernManager = sillyTavernManager;
|
||||
this.hardwareManager = hardwareManager;
|
||||
this.binaryManager = binaryManager;
|
||||
this.windowManager = windowManager;
|
||||
}
|
||||
|
||||
private async launchKoboldCppWithCustomFrontends(args: string[] = []) {
|
||||
|
|
@ -146,6 +151,17 @@ export class IPCHandlers {
|
|||
|
||||
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) => {
|
||||
try {
|
||||
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) => {
|
||||
this.logManager.logError(message, error);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { BrowserWindow, app, Menu, shell, nativeImage, screen } from 'electron';
|
||||
import * as os from 'os';
|
||||
import { BrowserWindow, app, shell, nativeImage, screen, Menu } from 'electron';
|
||||
import { join } from 'path';
|
||||
import { stripVTControlCharacters } from 'util';
|
||||
import { GITHUB_API, PRODUCT_NAME } from '../../constants';
|
||||
import { PRODUCT_NAME } from '../../constants';
|
||||
import type { IPCChannel, IPCChannelPayloads } from '@/types/ipc';
|
||||
import { readTextFile } from '@/utils/fs';
|
||||
|
||||
export class WindowManager {
|
||||
private mainWindow: BrowserWindow | null = null;
|
||||
|
||||
private isDevelopment(): boolean {
|
||||
return process.env.NODE_ENV === 'development' || !app.isPackaged;
|
||||
}
|
||||
|
||||
private getIconPath(): string {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const projectRoot = join(__dirname, '../..');
|
||||
|
|
@ -17,20 +19,17 @@ export class WindowManager {
|
|||
return join(process.resourcesPath, 'assets/icon.png');
|
||||
}
|
||||
|
||||
private isDevelopment(): boolean {
|
||||
return process.env.NODE_ENV === 'development' || !app.isPackaged;
|
||||
}
|
||||
|
||||
createMainWindow(): BrowserWindow {
|
||||
const iconPath = this.getIconPath();
|
||||
const iconImage = nativeImage.createFromPath(iconPath);
|
||||
|
||||
const { workAreaSize } = screen.getPrimaryDisplay();
|
||||
const windowHeight = Math.floor(workAreaSize.height * 0.9);
|
||||
const windowHeight = Math.floor(workAreaSize.height * 0.85);
|
||||
|
||||
this.mainWindow = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: windowHeight,
|
||||
frame: false,
|
||||
icon: iconImage,
|
||||
title: PRODUCT_NAME,
|
||||
show: false,
|
||||
|
|
@ -47,29 +46,6 @@ export class WindowManager {
|
|||
|
||||
if (process.platform === 'linux') {
|
||||
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', () => {
|
||||
|
|
@ -119,36 +95,10 @@ export class WindowManager {
|
|||
});
|
||||
|
||||
this.setupContextMenu();
|
||||
|
||||
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() {
|
||||
if (!this.mainWindow) return;
|
||||
|
||||
|
|
@ -191,172 +141,30 @@ export class WindowManager {
|
|||
});
|
||||
}
|
||||
|
||||
setupApplicationMenu() {
|
||||
const isDev = this.isDevelopment();
|
||||
|
||||
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);
|
||||
getMainWindow(): BrowserWindow | null {
|
||||
return this.mainWindow;
|
||||
}
|
||||
|
||||
private async showAboutDialog() {
|
||||
const packagePath = join(app.getAppPath(), 'package.json');
|
||||
const packageContent = await readTextFile(packagePath);
|
||||
const packageInfo = packageContent ? JSON.parse(packageContent) : {};
|
||||
const electronVersion = process.versions.electron;
|
||||
const chromeVersion = process.versions.chrome;
|
||||
const nodeVersion = process.versions.node;
|
||||
const v8Version = process.versions.v8;
|
||||
const osInfo = `${process.platform} ${process.arch} ${os.release()}`;
|
||||
|
||||
const versionText = `Version: ${packageInfo.version}
|
||||
Electron: ${electronVersion}
|
||||
Chromium: ${chromeVersion}
|
||||
Node.js: ${nodeVersion}
|
||||
V8: ${v8Version}
|
||||
OS: ${osInfo}`;
|
||||
|
||||
const iconPath = this.getIconPath();
|
||||
const iconImage = nativeImage.createFromPath(iconPath);
|
||||
|
||||
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);
|
||||
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();
|
||||
}
|
||||
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 = {
|
||||
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
|
||||
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 = {
|
||||
|
|
|
|||
|
|
@ -45,38 +45,38 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
/* AppHeader animations */
|
||||
/* TitleBar gerbil icon animations */
|
||||
@keyframes elephantShake {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1.3) rotate(5deg) translateX(0);
|
||||
transform: scale(1.1) rotate(5deg) translateX(0);
|
||||
}
|
||||
10% {
|
||||
transform: scale(1.4) rotate(-3deg) translateX(-2px);
|
||||
transform: scale(1.2) rotate(-3deg) translateX(-2px);
|
||||
}
|
||||
20% {
|
||||
transform: scale(1.5) rotate(8deg) translateX(2px);
|
||||
transform: scale(1.3) rotate(8deg) translateX(2px);
|
||||
}
|
||||
30% {
|
||||
transform: scale(1.4) rotate(-5deg) translateX(-1px);
|
||||
transform: scale(1.2) rotate(-5deg) translateX(-1px);
|
||||
}
|
||||
40% {
|
||||
transform: scale(1.6) rotate(10deg) translateX(3px);
|
||||
transform: scale(1.4) rotate(10deg) translateX(3px);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.3) rotate(-8deg) translateX(-2px);
|
||||
transform: scale(1.2) rotate(-8deg) translateX(-2px);
|
||||
}
|
||||
60% {
|
||||
transform: scale(1.5) rotate(6deg) translateX(1px);
|
||||
transform: scale(1.3) rotate(6deg) translateX(1px);
|
||||
}
|
||||
70% {
|
||||
transform: scale(1.2) rotate(-4deg) translateX(-1px);
|
||||
transform: scale(1) rotate(-4deg) translateX(-1px);
|
||||
}
|
||||
80% {
|
||||
transform: scale(1.4) rotate(3deg) translateX(2px);
|
||||
transform: scale(1.2) rotate(3deg) translateX(2px);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
appVersion: string;
|
||||
electronVersion: string;
|
||||
nodeVersion: string;
|
||||
chromeVersion: string;
|
||||
v8Version: string;
|
||||
osVersion: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
export interface AppAPI {
|
||||
openExternal: (url: string) => Promise<void>;
|
||||
getVersion: () => Promise<string>;
|
||||
getVersionInfo: () => Promise<VersionInfo>;
|
||||
minimizeWindow: () => void;
|
||||
maximizeWindow: () => void;
|
||||
closeWindow: () => void;
|
||||
}
|
||||
|
||||
export interface ConfigAPI {
|
||||
|
|
|
|||
22
yarn.lock
22
yarn.lock
|
|
@ -871,9 +871,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/core@npm:^8.2.7":
|
||||
version: 8.2.7
|
||||
resolution: "@mantine/core@npm:8.2.7"
|
||||
"@mantine/core@npm:^8.2.8":
|
||||
version: 8.2.8
|
||||
resolution: "@mantine/core@npm:8.2.8"
|
||||
dependencies:
|
||||
"@floating-ui/react": "npm:^0.26.28"
|
||||
clsx: "npm:^2.1.1"
|
||||
|
|
@ -882,19 +882,19 @@ __metadata:
|
|||
react-textarea-autosize: "npm:8.5.9"
|
||||
type-fest: "npm:^4.27.0"
|
||||
peerDependencies:
|
||||
"@mantine/hooks": 8.2.7
|
||||
"@mantine/hooks": 8.2.8
|
||||
react: ^18.x || ^19.x
|
||||
react-dom: ^18.x || ^19.x
|
||||
checksum: 10c0/d10ba6ef7ac552b6bd5638b9a16c4d2405391102330d94c8df7a7d87756cdf4ec341a350eab252575eb5990f43dc4122ea13244753a9169338a43d2b996f7594
|
||||
checksum: 10c0/9933ae22ab2faa852d8a941bdbb427c98d160b253f2977f10aad7024cd725fbca56d36a22560f3c75f8716b3d6651a599fa6d996cb131315c997242206cc4ebb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/hooks@npm:^8.2.7":
|
||||
version: 8.2.7
|
||||
resolution: "@mantine/hooks@npm:8.2.7"
|
||||
"@mantine/hooks@npm:^8.2.8":
|
||||
version: 8.2.8
|
||||
resolution: "@mantine/hooks@npm:8.2.8"
|
||||
peerDependencies:
|
||||
react: ^18.x || ^19.x
|
||||
checksum: 10c0/54dfbc36acad9dbb5e3a29d0944041f7ff31e61416cbd26ecf97c37c3a779a1b0028e05a0e6daa44cf3c3c2406c50b8f2107eb6fee3cfb6647b7524965556686
|
||||
checksum: 10c0/0097d8f05ee684c0e78f6160aa23d431e27ef15d32c66010f369d35e86497571a52dc56824fb659ca1c66ff3dd9e6513fe4c8c9bb3e1d98118ba176b4b9feeeb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3706,8 +3706,8 @@ __metadata:
|
|||
resolution: "gerbil@workspace:."
|
||||
dependencies:
|
||||
"@eslint/js": "npm:^9.34.0"
|
||||
"@mantine/core": "npm:^8.2.7"
|
||||
"@mantine/hooks": "npm:^8.2.7"
|
||||
"@mantine/core": "npm:^8.2.8"
|
||||
"@mantine/hooks": "npm:^8.2.8"
|
||||
"@types/node": "npm:^24.3.0"
|
||||
"@types/react": "npm:^19.1.12"
|
||||
"@types/react-dom": "npm:^19.1.9"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue