settings icon back to the titlebar, fix statusbar-related layout issues

This commit is contained in:
Egor 2025-09-11 00:38:26 -07:00
parent dc922b333d
commit 96b1ecc7be
11 changed files with 225 additions and 212 deletions

View file

@ -1,7 +1,7 @@
{
"name": "gerbil",
"productName": "Gerbil",
"version": "1.1.2",
"version": "1.2.0",
"description": "Run Large Language Models locally",
"main": "out/main/index.js",
"homepage": "./",

View file

@ -25,6 +25,7 @@ export const App = () => {
const [frontendPreference, setFrontendPreference] =
useState<FrontendPreference>('koboldcpp');
const [ejectConfirmModalOpen, setEjectConfirmModalOpen] = useState(false);
const isInterfaceScreen = currentScreen === 'interface';
const {
updateInfo: binaryUpdateInfo,
@ -146,7 +147,7 @@ export const App = () => {
<AppShell
header={{ height: TITLEBAR_HEIGHT }}
footer={{ height: STATUSBAR_HEIGHT }}
padding={currentScreen === 'interface' ? 0 : 'md'}
padding={isInterfaceScreen ? 0 : 'md'}
>
<TitleBar
currentScreen={currentScreen || 'welcome'}
@ -154,9 +155,21 @@ export const App = () => {
onTabChange={setActiveInterfaceTab}
onEject={handleEject}
frontendPreference={frontendPreference}
onFrontendPreferenceChange={setFrontendPreference}
/>
<AppShell.Main>
<AppShell.Main
top={TITLEBAR_HEIGHT}
style={{
height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
minHeight: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
position: 'relative',
overflow: 'auto',
paddingTop: 0,
top: TITLEBAR_HEIGHT,
paddingBottom: isInterfaceScreen ? 0 : '1rem',
}}
>
<ErrorBoundary>
{currentScreen === null ? (
<Center h="100%" style={{ minHeight: '25rem' }}>
@ -191,7 +204,7 @@ export const App = () => {
</ScreenTransition>
<ScreenTransition
isActive={currentScreen === 'interface'}
isActive={isInterfaceScreen}
shouldAnimate={hasInitialized}
>
<InterfaceScreen
@ -220,11 +233,7 @@ export const App = () => {
</AppShell.Main>
<AppShell.Footer>
<StatusBar
currentScreen={currentScreen}
frontendPreference={frontendPreference}
onFrontendPreferenceChange={setFrontendPreference}
/>
<StatusBar />
</AppShell.Footer>
<EjectConfirmModal

View file

@ -9,6 +9,7 @@ import {
Code,
TextInput,
Button,
Box,
} from '@mantine/core';
import { useState } from 'react';
@ -543,68 +544,6 @@ export const CommandLineArgumentsModal = ({
}
};
const renderArgument = (arg: ArgumentInfo) => (
<Stack
key={arg.flag}
gap="xs"
p="sm"
style={{ borderLeft: '3px solid var(--mantine-color-blue-4)' }}
>
<Group gap="xs" wrap="wrap" justify="space-between">
<Group gap="xs" wrap="wrap" style={{ flex: 1 }}>
<Code style={{ fontSize: '0.875em', fontWeight: 600 }}>
{arg.flag}
</Code>
{arg.aliases &&
arg.aliases.map((alias) => (
<Code key={alias} style={{ fontSize: '0.75em' }} c="dimmed">
{alias}
</Code>
))}
{arg.type && (
<Badge size="xs" variant="light" color="blue">
{arg.type}
</Badge>
)}
</Group>
{onAddArgument && (
<Button
size="xs"
variant="light"
onClick={() => handleAddArgument(arg)}
>
Add
</Button>
)}
</Group>
<Text size="sm" c="dimmed">
{arg.description}
</Text>
{(arg.metavar || arg.default !== undefined || arg.choices) && (
<Group gap="lg">
{arg.metavar && (
<Text size="xs" c="dimmed">
<strong>Format:</strong> {arg.metavar}
</Text>
)}
{arg.default !== undefined && (
<Text size="xs" c="dimmed">
<strong>Default:</strong> {String(arg.default)}
</Text>
)}
{arg.choices && (
<Text size="xs" c="dimmed">
<strong>Choices:</strong> {arg.choices.join(', ')}
</Text>
)}
</Group>
)}
</Stack>
);
return (
<Modal
opened={opened}
@ -638,11 +577,97 @@ export const CommandLineArgumentsModal = ({
</Group>
</Accordion.Control>
<Accordion.Panel>
<Stack gap="md">{args.map(renderArgument)}</Stack>
<Stack gap="md">
{args.map((arg) => (
<Stack
key={arg.flag}
gap="xs"
p="sm"
style={{
borderLeft: '3px solid var(--mantine-color-blue-4)',
}}
>
<Group gap="xs" wrap="wrap" justify="space-between">
<Group gap="xs" wrap="wrap" style={{ flex: 1 }}>
<Code
style={{ fontSize: '0.875em', fontWeight: 600 }}
>
{arg.flag}
</Code>
{arg.aliases &&
arg.aliases.map((alias) => (
<Code
key={alias}
style={{ fontSize: '0.75em' }}
c="dimmed"
>
{alias}
</Code>
))}
{arg.type && (
<Badge size="xs" variant="light" color="blue">
{arg.type}
</Badge>
)}
</Group>
{onAddArgument && (
<Button
size="xs"
variant="light"
onClick={() => handleAddArgument(arg)}
>
Add
</Button>
)}
</Group>
<Text size="sm" c="dimmed">
{arg.description}
</Text>
{(arg.metavar ||
arg.default !== undefined ||
arg.choices) && (
<Group gap="lg">
{arg.metavar && (
<Text size="xs" c="dimmed">
<strong>Format:</strong> {arg.metavar}
</Text>
)}
{arg.default !== undefined && (
<Text size="xs" c="dimmed">
<strong>Default:</strong> {String(arg.default)}
</Text>
)}
{arg.choices && (
<Text size="xs" c="dimmed">
<strong>Choices:</strong> {arg.choices.join(', ')}
</Text>
)}
</Group>
)}
</Stack>
))}
</Stack>
</Accordion.Panel>
</Accordion.Item>
))}
</Accordion>
<Box
style={{
backgroundColor: 'var(--mantine-color-body)',
padding: '0.5rem 1.5rem 0',
display: 'flex',
justifyContent: 'flex-end',
flexShrink: 0,
}}
>
<Button onClick={onClose} variant="filled">
Close
</Button>
</Box>
</Stack>
</Modal>
);

View file

@ -1,17 +1,5 @@
import { useEffect, useState } from 'react';
import {
Box,
Flex,
ActionIcon,
Tooltip,
Badge,
Group,
useComputedColorScheme,
} from '@mantine/core';
import { Settings } from 'lucide-react';
import { SettingsModal } from '@/components/settings/SettingsModal';
import { safeExecute } from '@/utils/logger';
import type { FrontendPreference, Screen } from '@/types';
import { Badge, Group, Tooltip, useComputedColorScheme } from '@mantine/core';
import type {
CpuMetrics,
MemoryMetrics,
@ -20,23 +8,14 @@ import type {
interface StatusBarProps {
maxDataPoints?: number;
currentScreen: Screen | null;
frontendPreference: FrontendPreference;
onFrontendPreferenceChange: (preference: FrontendPreference) => void;
}
export const StatusBar = ({
maxDataPoints = 60,
currentScreen,
frontendPreference: _frontendPreference,
onFrontendPreferenceChange,
}: StatusBarProps) => {
export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
const [cpuMetrics, setCpuMetrics] = useState<CpuMetrics | null>(null);
const [memoryMetrics, setMemoryMetrics] = useState<MemoryMetrics | null>(
null
);
const [gpuMetrics, setGpuMetrics] = useState<GpuMetrics | null>(null);
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const colorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: true,
});
@ -74,8 +53,10 @@ export const StatusBar = ({
}, [maxDataPoints]);
return (
<Box
<Group
px="xs"
gap="xs"
justify="flex-end"
style={{
backgroundColor:
colorScheme === 'dark'
@ -83,94 +64,59 @@ export const StatusBar = ({
: 'var(--mantine-color-gray-1)',
}}
>
<Flex align="center" justify="space-between">
<Group gap="xs">
{cpuMetrics && memoryMetrics && (
<>
<Tooltip label={`${cpuMetrics.usage.toFixed(1)}%`} position="top">
<Badge
size="sm"
variant="light"
style={{ minWidth: '5rem', textAlign: 'center' }}
>
CPU: {cpuMetrics.usage.toFixed(1)}%
</Badge>
</Tooltip>
{cpuMetrics && memoryMetrics && (
<>
<Tooltip label={`${cpuMetrics.usage.toFixed(1)}%`} position="top">
<Badge
size="sm"
variant="light"
style={{ minWidth: '5rem', textAlign: 'center' }}
>
CPU: {cpuMetrics.usage.toFixed(1)}%
</Badge>
</Tooltip>
<Tooltip
label={`${memoryMetrics.used.toFixed(1)} GB / ${memoryMetrics.total.toFixed(1)} GB (${memoryMetrics.usage.toFixed(1)}%)`}
position="top"
>
<Badge
size="sm"
variant="light"
style={{ minWidth: '5rem', textAlign: 'center' }}
>
RAM: {memoryMetrics.usage.toFixed(1)}%
</Badge>
</Tooltip>
</>
)}
{gpuMetrics?.gpus.map((gpu, index) => (
<Group gap="xs" key={`gpu-${index}`}>
<Tooltip label={`${gpu.usage.toFixed(1)}%`} position="top">
<Badge
size="sm"
variant="light"
style={{ minWidth: '5rem', textAlign: 'center' }}
>
GPU: {gpu.usage.toFixed(1)}%
</Badge>
</Tooltip>
<Tooltip
label={`${gpu.memoryUsed.toFixed(1)} GB / ${gpu.memoryTotal.toFixed(1)} GB (${gpu.memoryUsage.toFixed(1)}%)`}
position="top"
>
<Badge
size="sm"
variant="light"
style={{ minWidth: '5rem', textAlign: 'center' }}
>
VRAM: {gpu.memoryUsage.toFixed(1)}%
</Badge>
</Tooltip>
</Group>
))}
</Group>
<Tooltip label="Settings" position="top">
<ActionIcon
variant="subtle"
size="sm"
onClick={() => setSettingsModalOpen(true)}
aria-label="Open settings"
style={{
borderRadius: '0.25rem',
}}
<Tooltip
label={`${memoryMetrics.used.toFixed(1)} GB / ${memoryMetrics.total.toFixed(1)} GB (${memoryMetrics.usage.toFixed(1)}%)`}
position="top"
>
<Settings size="1rem" />
</ActionIcon>
</Tooltip>
</Flex>
<Badge
size="sm"
variant="light"
style={{ minWidth: '5rem', textAlign: 'center' }}
>
RAM: {memoryMetrics.usage.toFixed(1)}%
</Badge>
</Tooltip>
</>
)}
<SettingsModal
isOnInterfaceScreen={currentScreen === 'interface'}
opened={settingsModalOpen}
onClose={async () => {
setSettingsModalOpen(false);
const preference = await safeExecute(
() =>
window.electronAPI.config.get(
'frontendPreference'
) as Promise<FrontendPreference>,
'Failed to load frontend preference:'
);
onFrontendPreferenceChange(preference || 'koboldcpp');
}}
currentScreen={currentScreen || undefined}
/>
</Box>
{gpuMetrics?.gpus.map((gpu, index) => (
<Group gap="xs" key={`gpu-${index}`}>
<Tooltip label={`${gpu.usage.toFixed(1)}%`} position="top">
<Badge
size="sm"
variant="light"
style={{ minWidth: '5rem', textAlign: 'center' }}
>
GPU: {gpu.usage.toFixed(1)}%
</Badge>
</Tooltip>
<Tooltip
label={`${gpu.memoryUsed.toFixed(1)} GB / ${gpu.memoryTotal.toFixed(1)} GB (${gpu.memoryUsage.toFixed(1)}%)`}
position="top"
>
<Badge
size="sm"
variant="light"
style={{ minWidth: '5rem', textAlign: 'center' }}
>
VRAM: {gpu.memoryUsage.toFixed(1)}%
</Badge>
</Tooltip>
</Group>
))}
</Group>
);
};

View file

@ -7,11 +7,20 @@ import {
useComputedColorScheme,
AppShell,
} from '@mantine/core';
import { Minus, Square, X, Copy, CircleFadingArrowUp } from 'lucide-react';
import {
Minus,
Square,
X,
Copy,
CircleFadingArrowUp,
Settings,
} from 'lucide-react';
import { useState } from 'react';
import { soundAssets, playSound, initializeAudio } from '@/utils/sounds';
import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
import { useLaunchConfigStore } from '@/stores/launchConfig';
import { SettingsModal } from '@/components/settings/SettingsModal';
import { safeExecute } from '@/utils/logger';
import iconUrl from '/icon.png';
import { FRONTENDS, PRODUCT_NAME, TITLEBAR_HEIGHT } from '@/constants';
import type { FrontendPreference, InterfaceTab, Screen } from '@/types';
@ -22,6 +31,7 @@ interface TitleBarProps {
onTabChange: (tab: InterfaceTab) => void;
onEject: () => void;
frontendPreference: FrontendPreference;
onFrontendPreferenceChange: (preference: FrontendPreference) => void;
}
export const TitleBar = ({
@ -30,6 +40,7 @@ export const TitleBar = ({
onTabChange,
onEject,
frontendPreference,
onFrontendPreferenceChange,
}: TitleBarProps) => {
const computedColorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: true,
@ -41,6 +52,7 @@ export const TitleBar = ({
const [isMouseSqueaking, setIsMouseSqueaking] = useState(false);
const [isMaximized, setIsMaximized] = useState(false);
const [isSelectOpen, setIsSelectOpen] = useState(false);
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const handleMinimize = () => {
window.electronAPI.app.minimizeWindow();
@ -225,6 +237,29 @@ export const TitleBar = ({
</ActionIcon>
)}
<ActionIcon
variant="subtle"
size={TITLEBAR_HEIGHT}
onClick={() => setSettingsModalOpen(true)}
aria-label="Open settings"
tabIndex={-1}
style={{
borderRadius: 0,
margin: 0,
}}
>
<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" />,
@ -263,6 +298,23 @@ export const TitleBar = ({
))}
</Group>
</Box>
<SettingsModal
isOnInterfaceScreen={currentScreen === 'interface'}
opened={settingsModalOpen}
onClose={async () => {
setSettingsModalOpen(false);
const preference = await safeExecute(
() =>
window.electronAPI.config.get(
'frontendPreference'
) as Promise<FrontendPreference>,
'Failed to load frontend preference:'
);
onFrontendPreferenceChange(preference || 'koboldcpp');
}}
currentScreen={currentScreen || undefined}
/>
</AppShell.Header>
);
};

View file

@ -4,6 +4,7 @@ import {
OPENWEBUI,
FRONTENDS,
TITLEBAR_HEIGHT,
STATUSBAR_HEIGHT,
} from '@/constants';
import { useLaunchConfigStore } from '@/stores/launchConfig';
import type { FrontendPreference } from '@/types';
@ -65,7 +66,7 @@ export const ServerTab = ({
<Box
style={{
width: '100%',
height: `calc(100vh - ${TITLEBAR_HEIGHT})`,
height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
overflow: 'hidden',
}}
>

View file

@ -12,7 +12,11 @@ import {
useComputedColorScheme,
} from '@mantine/core';
import { ChevronDown } from 'lucide-react';
import { SERVER_READY_SIGNALS, TITLEBAR_HEIGHT } from '@/constants';
import {
SERVER_READY_SIGNALS,
STATUSBAR_HEIGHT,
TITLEBAR_HEIGHT,
} from '@/constants';
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
import { useLaunchConfigStore } from '@/stores/launchConfig';
import type { FrontendPreference } from '@/types';
@ -127,7 +131,7 @@ export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>(
return (
<Box
style={{
height: `calc(100vh - ${TITLEBAR_HEIGHT})`,
height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
display: 'flex',
flexDirection: 'column',
backgroundColor: isDark

View file

@ -55,7 +55,7 @@ export const BackendSelector = () => {
<Text size="sm" fw={500}>
Backend
</Text>
<InfoTooltip label="Select a backend to use. CUDA runs on NVIDIA GPUs, and is much faster. ROCm is the AMD equivalent. Vulkan and CLBlast work on all GPUs." />
<InfoTooltip label="Select a backend to use to run LLMs. CUDA runs on NVIDIA GPUs, and is much faster. ROCm is the AMD equivalent. Vulkan and CLBlast work on all GPUs." />
</Group>
<Select
placeholder={

View file

@ -48,7 +48,7 @@ export const GeneralTab = ({
() => [
{
value: 'koboldcpp',
label: 'Built-in Interface',
label: 'Built-in',
badges: ['Text', 'Image'],
},
{

View file

@ -72,7 +72,6 @@ export const SettingsModal = ({
styles={{
...MODAL_STYLES_WITH_TITLEBAR,
content: {
...MODAL_STYLES_WITH_TITLEBAR.content,
paddingBottom: 0,
},
body: {

View file

@ -1,28 +1,5 @@
@import '@mantine/core/styles.css';
.mantine-AppShell-main {
overflow: auto !important;
height: calc(100vh - 2.5rem) !important;
position: relative !important;
top: 2.5rem !important;
margin-top: 0 !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
min-height: calc(100vh - 2.5rem) !important;
}
.mantine-AppShell-root {
--app-shell-header-offset: 2.5rem;
--app-shell-navbar-offset: 0rem;
--app-shell-aside-offset: 0rem;
--app-shell-footer-offset: 0rem;
}
/* TODO: something (mantine?) is setting this sometimes to "none" on the terminal tab */
body {
user-select: auto !important;
}
/* Custom scrollbars */
::-webkit-scrollbar {
width: 0.5rem;