re-organize new system info cards into a new System tab, hopefully fix app config re-writing race condition

This commit is contained in:
Egor 2025-09-26 23:16:23 -07:00
parent ec2410c0e4
commit 3c2a07c730
11 changed files with 475 additions and 370 deletions

View file

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

View file

@ -128,7 +128,7 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
))}
</>
) : (
<PerformanceBadge tooltipLabel="System resource manager" iconOnly />
<PerformanceBadge tooltipLabel="Resource manager" iconOnly />
)}
</Group>
</Group>

View file

@ -1,9 +1,4 @@
import { useState, useEffect } from 'react';
import {
createSoftwareItems,
createDriverItems,
createHardwareItems,
} from '@/utils/systemInfo';
import {
Text,
Stack,
@ -15,11 +10,9 @@ import {
Button,
rem,
} from '@mantine/core';
import { Github, FolderOpen, FileText } from 'lucide-react';
import { Github } from 'lucide-react';
import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
import { PRODUCT_NAME, GITHUB_API } from '@/constants';
import { InfoCard } from '@/components/InfoCard';
import type { HardwareInfo } from '@/types/hardware';
import type { SystemVersionInfo } from '@/types/electron';
import icon from '/icon.png';
@ -28,7 +21,6 @@ export const AboutTab = () => {
const [versionInfo, setVersionInfo] = useState<SystemVersionInfo | null>(
null
);
const [hardwareInfo, setHardwareInfo] = useState<HardwareInfo | null>(null);
const { handleLogoClick, getLogoStyles } = useLogoClickSounds();
useEffect(() => {
@ -39,34 +31,7 @@ export const AboutTab = () => {
}
};
const loadHardwareInfo = async () => {
try {
const [cpu, gpu, gpuCapabilities, gpuMemory, systemMemory] =
await Promise.all([
window.electronAPI.kobold.detectCPU(),
window.electronAPI.kobold.detectGPU(),
window.electronAPI.kobold.detectGPUCapabilities(),
window.electronAPI.kobold.detectGPUMemory(),
window.electronAPI.kobold.detectSystemMemory(),
]);
setHardwareInfo({
cpu,
gpu,
gpuCapabilities,
gpuMemory,
systemMemory,
});
} catch (error) {
window.electronAPI.logs.logError(
'Failed to load hardware info',
error as Error
);
}
};
loadVersionInfo();
loadHardwareInfo();
}, []);
if (!versionInfo) {
@ -77,10 +42,6 @@ export const AboutTab = () => {
);
}
const softwareItems = createSoftwareItems(versionInfo);
const driverItems = hardwareInfo ? createDriverItems(hardwareInfo) : [];
const hardwareItems = hardwareInfo ? createHardwareItems(hardwareInfo) : [];
const actionButtons = [
{
icon: Github,
@ -88,16 +49,6 @@ export const AboutTab = () => {
onClick: () =>
window.electronAPI.app.openExternal(GITHUB_API.GERBIL_GITHUB_URL),
},
{
icon: FolderOpen,
label: 'Show Logs',
onClick: () => window.electronAPI.app.showLogsFolder(),
},
{
icon: FileText,
label: 'View Config',
onClick: () => window.electronAPI.app.viewConfigFile(),
},
];
return (
@ -157,15 +108,18 @@ export const AboutTab = () => {
</Group>
</Card>
<InfoCard title="Software" items={softwareItems} />
<InfoCard title="Drivers" items={driverItems} loading={!hardwareInfo} />
<InfoCard
title="Hardware"
items={hardwareItems}
loading={!hardwareInfo}
/>
<Card withBorder radius="md" p="md">
<Text size="lg" fw={500} mb="md">
About {PRODUCT_NAME}
</Text>
<Text size="sm" c="dimmed" mb="md">
{PRODUCT_NAME} is a user-friendly desktop application that makes it
easy to run large language models locally on your machine. Whether
you&apos;re looking to chat with AI models, generate images, or
explore different interfaces like SillyTavern and Open WebUI,{' '}
{PRODUCT_NAME} provides a streamlined experience for local AI.
</Text>
</Card>
</Stack>
);
};

View file

@ -11,14 +11,21 @@ import {
import { Sun, Moon, Monitor } from 'lucide-react';
import { useState, useEffect } from 'react';
import { usePreferencesStore } from '@/stores/preferences';
import { ZOOM } from '@/constants';
import { FrontendInterfaceSelector } from '@/components/settings/FrontendInterfaceSelector';
import {
zoomLevelToPercentage,
percentageToZoomLevel,
isValidZoomPercentage,
} from '@/utils/zoom';
import { ZOOM } from '@/constants';
export const AppearanceTab = () => {
interface AppearanceTabProps {
isOnInterfaceScreen?: boolean;
}
export const AppearanceTab = ({
isOnInterfaceScreen = false,
}: AppearanceTabProps) => {
const {
rawColorScheme,
resolvedColorScheme,
@ -70,6 +77,9 @@ export const AppearanceTab = () => {
return (
<Stack gap="lg" h="100%">
<div>
<FrontendInterfaceSelector isOnInterfaceScreen={isOnInterfaceScreen} />
</div>
<div>
<Text fw={500} mb="sm">
Theme
@ -127,7 +137,6 @@ export const AppearanceTab = () => {
]}
/>
</div>
<div>
<Text fw={500} mb="sm">
Zoom Level

View file

@ -0,0 +1,270 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { Text, Box, Anchor, rem } from '@mantine/core';
import { Monitor } from 'lucide-react';
import { usePreferencesStore } from '@/stores/preferences';
import type { FrontendPreference } from '@/types';
import { FRONTENDS } from '@/constants';
import { Select } from '@/components/Select';
interface FrontendRequirement {
id: string;
name: string;
url: string;
}
interface FrontendConfig {
value: string;
label: string;
badges: string[];
requirements?: FrontendRequirement[];
requirementCheck?: () => Promise<boolean>;
}
interface FrontendInterfaceSelectorProps {
isOnInterfaceScreen?: boolean;
}
export const FrontendInterfaceSelector = ({
isOnInterfaceScreen = false,
}: FrontendInterfaceSelectorProps) => {
const { frontendPreference, setFrontendPreference } = usePreferencesStore();
const [frontendRequirements, setFrontendRequirements] = useState<
Map<string, boolean>
>(new Map());
const frontendConfigs: FrontendConfig[] = useMemo(
() => [
{
value: 'koboldcpp',
label: 'Built-in',
badges: ['Text', 'Image'],
},
{
value: 'sillytavern',
label: FRONTENDS.SILLYTAVERN,
badges: ['Text', 'Image'],
requirements: [
{
id: 'nodejs',
name: 'Node.js',
url: 'https://nodejs.org/',
},
],
requirementCheck: () =>
window.electronAPI.dependencies.isNpxAvailable(),
},
{
value: 'openwebui',
label: FRONTENDS.OPENWEBUI,
badges: ['Text', 'Image'],
requirements: [
{
id: 'uv',
name: 'uv',
url: 'https://docs.astral.sh/uv/getting-started/installation/',
},
],
requirementCheck: () => window.electronAPI.dependencies.isUvAvailable(),
},
{
value: 'comfyui',
label: FRONTENDS.COMFYUI,
badges: ['Image'],
requirements: [
{
id: 'uv',
name: 'uv',
url: 'https://docs.astral.sh/uv/getting-started/installation/',
},
],
requirementCheck: () => window.electronAPI.dependencies.isUvAvailable(),
},
],
[]
);
const checkAllFrontendRequirements = useCallback(async () => {
const requirementResults = new Map<string, boolean>();
for (const config of frontendConfigs) {
if (config.requirementCheck) {
const isAvailable = await config.requirementCheck();
requirementResults.set(config.value, isAvailable);
} else {
requirementResults.set(config.value, true);
}
}
setFrontendRequirements(requirementResults);
const currentFrontendConfig = frontendConfigs.find(
(config) => config.value === frontendPreference
);
if (currentFrontendConfig && !requirementResults.get(frontendPreference)) {
setFrontendPreference('koboldcpp');
}
}, [frontendConfigs, frontendPreference, setFrontendPreference]);
const getSelectedFrontendConfig = () =>
frontendConfigs.find((config) => config.value === frontendPreference);
const getUnmetRequirements = () => {
const selectedConfig = getSelectedFrontendConfig();
if (!selectedConfig || !selectedConfig.requirements) return [];
const isAvailable = frontendRequirements.get(selectedConfig.value) ?? true;
return isAvailable ? [] : selectedConfig.requirements;
};
const getUnmetRequirementsForFrontend = (frontendValue: string) => {
const config = frontendConfigs.find((c) => c.value === frontendValue);
if (!config || !config.requirements) return [];
const isAvailable = frontendRequirements.get(frontendValue) ?? true;
return isAvailable ? [] : config.requirements;
};
const isFrontendAvailable = (frontendValue: string) =>
frontendRequirements.get(frontendValue) ?? true;
const handleFrontendPreferenceChange = (value: string | null) => {
setFrontendPreference(value as FrontendPreference);
};
const renderDisabledFrontendWarnings = () => {
const disabledFrontends = frontendConfigs.filter(
(config) => !isFrontendAvailable(config.value)
);
if (disabledFrontends.length === 0) {
return null;
}
const requirementGroups = new Map<string, string[]>();
disabledFrontends.forEach((config) => {
const unmetReqs = getUnmetRequirementsForFrontend(config.value);
const reqKey = unmetReqs.map((req) => req.id).join(',');
if (!requirementGroups.has(reqKey)) {
requirementGroups.set(reqKey, []);
}
requirementGroups.get(reqKey)!.push(config.label);
});
return (
<Box mt="sm">
{Array.from(requirementGroups.entries()).map(
([reqKey, frontendLabels]) => {
const firstDisabledFrontend = disabledFrontends.find(
(config) =>
getUnmetRequirementsForFrontend(config.value)
.map((req) => req.id)
.join(',') === reqKey
);
const unmetReqs = firstDisabledFrontend
? getUnmetRequirementsForFrontend(firstDisabledFrontend.value)
: [];
const frontendText =
frontendLabels.length === 1
? frontendLabels[0]
: frontendLabels.length === 2
? `${frontendLabels[0]} and ${frontendLabels[1]}`
: `${frontendLabels.slice(0, -1).join(', ')}, and ${frontendLabels[frontendLabels.length - 1]}`;
const isAre = frontendLabels.length === 1 ? 'is' : 'are';
return (
<Text key={reqKey} size="sm" c="orange">
{frontendText} {isAre} disabled - requires{' '}
{unmetReqs.map((req, index) => (
<span key={req.id}>
<Anchor
href={req.url}
target="_blank"
rel="noopener noreferrer"
td="underline"
>
{req.name}
</Anchor>
{index < unmetReqs.length - 1 ? ', ' : ''}
</span>
))}
</Text>
);
}
)}
</Box>
);
};
useEffect(() => {
const initialize = async () => {
await checkAllFrontendRequirements();
};
initialize();
const handleFocus = () => {
checkAllFrontendRequirements();
};
window.addEventListener('focus', handleFocus);
return () => window.removeEventListener('focus', handleFocus);
}, [checkAllFrontendRequirements]);
useEffect(() => {
if (frontendPreference) {
checkAllFrontendRequirements();
}
}, [frontendPreference, checkAllFrontendRequirements]);
return (
<>
<Text fw={500} mb="sm">
Frontend Interface
</Text>
{getUnmetRequirements().length === 0 && (
<Text size="sm" c="dimmed" mb="md">
Choose which frontend interface to use for interacting with AI models
</Text>
)}
{getUnmetRequirements().length > 0 && (
<Text size="sm" c="red" mb="md">
{getSelectedFrontendConfig()?.label} requires{' '}
{getUnmetRequirements().map((req, index) => (
<span key={req.id}>
<Anchor
href={req.url}
target="_blank"
rel="noopener noreferrer"
c="red"
td="underline"
>
{req.name}
</Anchor>
{index < getUnmetRequirements().length - 1 ? ', ' : ''}
</span>
))}{' '}
to be installed on your system
</Text>
)}
<Select
value={frontendPreference}
onChange={handleFrontendPreferenceChange}
disabled={isOnInterfaceScreen}
data={frontendConfigs.map((config) => ({
value: config.value,
label: config.label,
disabled: !isFrontendAvailable(config.value),
}))}
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
/>
{renderDisabledFrontendWarnings()}
</>
);
};

View file

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useState, useEffect } from 'react';
import {
Stack,
Text,
@ -6,165 +6,19 @@ import {
TextInput,
Button,
rem,
Box,
Anchor,
Switch,
} from '@mantine/core';
import { Folder, FolderOpen, Monitor, ExternalLink } from 'lucide-react';
import type { FrontendPreference } from '@/types';
import { usePreferencesStore } from '@/stores/preferences';
import { FRONTENDS } from '@/constants';
import { Select } from '@/components/Select';
interface FrontendRequirement {
id: string;
name: string;
url: string;
}
interface FrontendConfig {
value: string;
label: string;
badges: string[];
requirements?: FrontendRequirement[];
requirementCheck?: () => Promise<boolean>;
}
interface GeneralTabProps {
isOnInterfaceScreen?: boolean;
}
export const GeneralTab = ({
isOnInterfaceScreen = false,
}: GeneralTabProps) => {
export const GeneralTab = () => {
const [installDir, setInstallDir] = useState('');
const {
frontendPreference,
setFrontendPreference,
systemMonitoringEnabled,
setSystemMonitoringEnabled,
} = usePreferencesStore();
const [frontendRequirements, setFrontendRequirements] = useState<
Map<string, boolean>
>(new Map());
const frontendConfigs: FrontendConfig[] = useMemo(
() => [
{
value: 'koboldcpp',
label: 'Built-in',
badges: ['Text', 'Image'],
},
{
value: 'sillytavern',
label: FRONTENDS.SILLYTAVERN,
badges: ['Text', 'Image'],
requirements: [
{
id: 'nodejs',
name: 'Node.js',
url: 'https://nodejs.org/',
},
],
requirementCheck: () =>
window.electronAPI.dependencies.isNpxAvailable(),
},
{
value: 'openwebui',
label: FRONTENDS.OPENWEBUI,
badges: ['Text', 'Image'],
requirements: [
{
id: 'uv',
name: 'uv',
url: 'https://docs.astral.sh/uv/getting-started/installation/',
},
],
requirementCheck: () => window.electronAPI.dependencies.isUvAvailable(),
},
{
value: 'comfyui',
label: FRONTENDS.COMFYUI,
badges: ['Image'],
requirements: [
{
id: 'uv',
name: 'uv',
url: 'https://docs.astral.sh/uv/getting-started/installation/',
},
],
requirementCheck: () => window.electronAPI.dependencies.isUvAvailable(),
},
],
[]
);
const checkAllFrontendRequirements = useCallback(async () => {
const requirementResults = new Map<string, boolean>();
for (const config of frontendConfigs) {
if (config.requirementCheck) {
const isAvailable = await config.requirementCheck();
requirementResults.set(config.value, isAvailable);
} else {
requirementResults.set(config.value, true);
}
}
setFrontendRequirements(requirementResults);
const currentFrontendConfig = frontendConfigs.find(
(config) => config.value === frontendPreference
);
if (currentFrontendConfig && !requirementResults.get(frontendPreference)) {
setFrontendPreference('koboldcpp');
}
}, [frontendConfigs, frontendPreference, setFrontendPreference]);
const { systemMonitoringEnabled, setSystemMonitoringEnabled } =
usePreferencesStore();
useEffect(() => {
const initialize = async () => {
await Promise.all([
loadCurrentInstallDir(),
checkAllFrontendRequirements(),
]);
};
initialize();
const handleFocus = () => {
checkAllFrontendRequirements();
};
window.addEventListener('focus', handleFocus);
return () => window.removeEventListener('focus', handleFocus);
}, [checkAllFrontendRequirements]);
const getSelectedFrontendConfig = () =>
frontendConfigs.find((config) => config.value === frontendPreference);
const getUnmetRequirements = () => {
const selectedConfig = getSelectedFrontendConfig();
if (!selectedConfig || !selectedConfig.requirements) return [];
const isAvailable = frontendRequirements.get(selectedConfig.value) ?? true;
return isAvailable ? [] : selectedConfig.requirements;
};
const getUnmetRequirementsForFrontend = (frontendValue: string) => {
const config = frontendConfigs.find((c) => c.value === frontendValue);
if (!config || !config.requirements) return [];
const isAvailable = frontendRequirements.get(frontendValue) ?? true;
return isAvailable ? [] : config.requirements;
};
const isFrontendAvailable = (frontendValue: string) =>
frontendRequirements.get(frontendValue) ?? true;
useEffect(() => {
if (frontendPreference) {
checkAllFrontendRequirements();
}
}, [frontendPreference, checkAllFrontendRequirements]);
loadCurrentInstallDir();
}, []);
const loadCurrentInstallDir = async () => {
const currentDir = await window.electronAPI.kobold.getCurrentInstallDir();
@ -187,10 +41,6 @@ export const GeneralTab = ({
}
};
const handleFrontendPreferenceChange = (value: string | null) => {
setFrontendPreference(value as FrontendPreference);
};
return (
<Stack gap="lg" h="100%">
<div>
@ -232,117 +82,10 @@ export const GeneralTab = ({
<div>
<Text fw={500} mb="sm">
Frontend Interface
</Text>
{getUnmetRequirements().length === 0 && (
<Text size="sm" c="dimmed" mb="md">
Choose which frontend interface to use for interacting with AI
models
</Text>
)}
{getUnmetRequirements().length > 0 && (
<Text size="sm" c="red" mb="md">
{getSelectedFrontendConfig()?.label} requires{' '}
{getUnmetRequirements().map((req, index) => (
<span key={req.id}>
<Anchor
href={req.url}
target="_blank"
rel="noopener noreferrer"
c="red"
td="underline"
>
{req.name}
</Anchor>
{index < getUnmetRequirements().length - 1 ? ', ' : ''}
</span>
))}{' '}
to be installed on your system
</Text>
)}
<Select
value={frontendPreference}
onChange={handleFrontendPreferenceChange}
disabled={isOnInterfaceScreen}
data={frontendConfigs.map((config) => ({
value: config.value,
label: config.label,
disabled: !isFrontendAvailable(config.value),
}))}
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
/>
<Box mt="sm">
{(() => {
const disabledFrontends = frontendConfigs.filter(
(config) => !isFrontendAvailable(config.value)
);
const requirementGroups = new Map<string, string[]>();
disabledFrontends.forEach((config) => {
const unmetReqs = getUnmetRequirementsForFrontend(config.value);
const reqKey = unmetReqs.map((req) => req.id).join(',');
if (!requirementGroups.has(reqKey)) {
requirementGroups.set(reqKey, []);
}
requirementGroups.get(reqKey)!.push(config.label);
});
return Array.from(requirementGroups.entries()).map(
([reqKey, frontendLabels]) => {
const firstDisabledFrontend = disabledFrontends.find(
(config) =>
getUnmetRequirementsForFrontend(config.value)
.map((req) => req.id)
.join(',') === reqKey
);
const unmetReqs = firstDisabledFrontend
? getUnmetRequirementsForFrontend(firstDisabledFrontend.value)
: [];
const frontendText =
frontendLabels.length === 1
? frontendLabels[0]
: frontendLabels.length === 2
? `${frontendLabels[0]} and ${frontendLabels[1]}`
: `${frontendLabels.slice(0, -1).join(', ')}, and ${frontendLabels[frontendLabels.length - 1]}`;
const isAre = frontendLabels.length === 1 ? 'is' : 'are';
return (
<Text key={reqKey} size="sm" c="orange" mb="xs">
{frontendText} {isAre} disabled - requires{' '}
{unmetReqs.map((req, index) => (
<span key={req.id}>
<Anchor
href={req.url}
target="_blank"
rel="noopener noreferrer"
td="underline"
>
{req.name}
</Anchor>
{index < unmetReqs.length - 1 ? ', ' : ''}
</span>
))}
</Text>
);
}
);
})()}
</Box>
</div>
<div>
<Text fw={500} mb="sm">
System Performance
Status Bar
</Text>
<Text size="sm" c="dimmed" mb="md">
Monitor CPU, memory, and GPU usage in the status bar
Control what information is displayed in the status bar
</Text>
<Switch
label="Show system metrics"
@ -352,6 +95,37 @@ export const GeneralTab = ({
}
/>
</div>
<div>
<Text fw={500} mb="sm">
Troubleshooting
</Text>
<Text size="sm" c="dimmed" mb="md">
Diagnostic tools and configuration access
</Text>
<Group gap="xs">
<Button
variant="outline"
size="compact-sm"
leftSection={
<FolderOpen style={{ width: rem(16), height: rem(16) }} />
}
onClick={() => window.electronAPI.app.showLogsFolder()}
>
Show Logs
</Button>
<Button
variant="outline"
size="compact-sm"
leftSection={
<Monitor style={{ width: rem(16), height: rem(16) }} />
}
onClick={() => window.electronAPI.app.viewConfigFile()}
>
View Config
</Button>
</Group>
</div>
</Stack>
);
};

View file

@ -5,11 +5,13 @@ import {
Palette,
SlidersHorizontal,
GitBranch,
Monitor,
Info,
} from 'lucide-react';
import { GeneralTab } from '@/components/settings/GeneralTab';
import { VersionsTab } from '@/components/settings/VersionsTab';
import { AppearanceTab } from '@/components/settings/AppearanceTab';
import { SystemTab } from '@/components/settings/SystemTab';
import { AboutTab } from '@/components/settings/AboutTab';
import type { Screen } from '@/types';
import { Modal } from '@/components/Modal';
@ -129,6 +131,14 @@ export const SettingsModal = ({
>
Appearance
</Tabs.Tab>
<Tabs.Tab
value="system"
leftSection={
<Monitor style={{ width: rem(16), height: rem(16) }} />
}
>
System
</Tabs.Tab>
<Tabs.Tab
value="about"
leftSection={<Info style={{ width: rem(16), height: rem(16) }} />}
@ -138,7 +148,7 @@ export const SettingsModal = ({
</Tabs.List>
<Tabs.Panel value="general">
<GeneralTab isOnInterfaceScreen={isOnInterfaceScreen} />
<GeneralTab />
</Tabs.Panel>
{showVersionsTab && (
@ -148,7 +158,11 @@ export const SettingsModal = ({
)}
<Tabs.Panel value="appearance">
<AppearanceTab />
<AppearanceTab isOnInterfaceScreen={isOnInterfaceScreen} />
</Tabs.Panel>
<Tabs.Panel value="system">
<SystemTab />
</Tabs.Panel>
<Tabs.Panel value="about">

View file

@ -0,0 +1,81 @@
import { useState, useEffect } from 'react';
import {
createSoftwareItems,
createDriverItems,
createHardwareItems,
} from '@/utils/systemInfo';
import { Stack, Text, Center } from '@mantine/core';
import { InfoCard } from '@/components/InfoCard';
import type { HardwareInfo } from '@/types/hardware';
import type { SystemVersionInfo } from '@/types/electron';
export const SystemTab = () => {
const [versionInfo, setVersionInfo] = useState<SystemVersionInfo | null>(
null
);
const [hardwareInfo, setHardwareInfo] = useState<HardwareInfo | null>(null);
useEffect(() => {
const loadVersionInfo = async () => {
const info = await window.electronAPI.app.getVersionInfo();
if (info) {
setVersionInfo(info);
}
};
const loadHardwareInfo = async () => {
try {
const [cpu, gpu, gpuCapabilities, gpuMemory, systemMemory] =
await Promise.all([
window.electronAPI.kobold.detectCPU(),
window.electronAPI.kobold.detectGPU(),
window.electronAPI.kobold.detectGPUCapabilities(),
window.electronAPI.kobold.detectGPUMemory(),
window.electronAPI.kobold.detectSystemMemory(),
]);
setHardwareInfo({
cpu,
gpu,
gpuCapabilities,
gpuMemory,
systemMemory,
});
} catch (error) {
window.electronAPI.logs.logError(
'Failed to load hardware info',
error as Error
);
}
};
loadVersionInfo();
loadHardwareInfo();
}, []);
if (!versionInfo) {
return (
<Center h="100%">
<Text c="dimmed">Loading system information...</Text>
</Center>
);
}
const softwareItems = createSoftwareItems(versionInfo);
const driverItems = hardwareInfo ? createDriverItems(hardwareInfo) : [];
const hardwareItems = hardwareInfo ? createHardwareItems(hardwareInfo) : [];
return (
<Stack gap="md">
<InfoCard title="Software" items={softwareItems} />
<InfoCard title="Drivers" items={driverItems} loading={!hardwareInfo} />
<InfoCard
title="Hardware"
items={hardwareItems}
loading={!hardwareInfo}
/>
</Stack>
);
};

View file

@ -51,10 +51,6 @@ async function saveConfig() {
return success !== null;
}
function saveConfigAsync() {
saveConfig();
}
export async function initialize() {
configPath = getConfigDir();
config = await loadConfig();
@ -85,9 +81,9 @@ function getDefaultInstallDir() {
export const getInstallDir = () => config.installDir || getDefaultInstallDir();
export function setInstallDir(dir: string) {
export async function setInstallDir(dir: string) {
config.installDir = dir;
saveConfigAsync();
await saveConfig();
}
export function getCurrentKoboldBinary() {
@ -95,23 +91,23 @@ export function getCurrentKoboldBinary() {
return path ? path.trim() : path;
}
export function setCurrentKoboldBinary(binaryPath: string) {
export async function setCurrentKoboldBinary(binaryPath: string) {
config.currentKoboldBinary = binaryPath;
saveConfigAsync();
await saveConfig();
}
export const getSelectedConfig = () => config.selectedConfig;
export function setSelectedConfig(configName: string) {
export async function setSelectedConfig(configName: string) {
config.selectedConfig = configName;
saveConfigAsync();
await saveConfig();
}
export const getColorScheme = () => config.colorScheme || 'auto';
export function setColorScheme(colorScheme: MantineColorScheme) {
export async function setColorScheme(colorScheme: MantineColorScheme) {
config.colorScheme = colorScheme;
saveConfigAsync();
await saveConfig();
}
export function getBackgroundColor() {
@ -128,42 +124,42 @@ export function getBackgroundColor() {
export const getWindowBounds = () => config.windowBounds;
export function setWindowBounds(bounds: WindowBounds) {
export async function setWindowBounds(bounds: WindowBounds) {
config.windowBounds = bounds;
saveConfigAsync();
await saveConfig();
}
export const getFrontendPreference = () => config.frontendPreference;
export function setFrontendPreference(preference: FrontendPreference) {
export async function setFrontendPreference(preference: FrontendPreference) {
config.frontendPreference = preference;
saveConfigAsync();
await saveConfig();
}
export const getHasSeenWelcome = () => config.hasSeenWelcome;
export function setHasSeenWelcome(hasSeenWelcome: boolean) {
export async function setHasSeenWelcome(hasSeenWelcome: boolean) {
config.hasSeenWelcome = hasSeenWelcome;
saveConfigAsync();
await saveConfig();
}
export const getSkipEjectConfirmation = () => config.skipEjectConfirmation;
export function setSkipEjectConfirmation(skipEjectConfirmation: boolean) {
export async function setSkipEjectConfirmation(skipEjectConfirmation: boolean) {
config.skipEjectConfirmation = skipEjectConfirmation;
saveConfigAsync();
await saveConfig();
}
export const getDismissedUpdates = () => config.dismissedUpdates || [];
export function setDismissedUpdates(dismissedUpdates: DismissedUpdate[]) {
export async function setDismissedUpdates(dismissedUpdates: DismissedUpdate[]) {
config.dismissedUpdates = dismissedUpdates;
saveConfigAsync();
await saveConfig();
}
export const getZoomLevel = () => config.zoomLevel;
export function setZoomLevel(zoomLevel: number) {
export async function setZoomLevel(zoomLevel: number) {
config.zoomLevel = zoomLevel;
saveConfigAsync();
await saveConfig();
}

View file

@ -1,5 +1,6 @@
import { readFile, writeFile, access, mkdir } from 'fs/promises';
import { readFile, writeFile, access, mkdir, rename } from 'fs/promises';
import { constants } from 'fs';
import { dirname } from 'path';
export const pathExists = async (path: string) => {
try {
@ -20,8 +21,14 @@ export const readJsonFile = async <T = unknown>(path: string) => {
};
export const writeJsonFile = async (path: string, data: unknown) => {
const dir = dirname(path);
await ensureDir(dir);
const content = JSON.stringify(data, null, 2);
await writeFile(path, content, 'utf-8');
const tempPath = `${path}.tmp`;
await writeFile(tempPath, content, 'utf-8');
await rename(tempPath, path);
};
export const ensureDir = async (path: string) => {

View file

@ -48,6 +48,13 @@ export const createDriverItems = (hardwareInfo: HardwareInfo) => {
];
}
if (gpuCapabilities.cuda.driverVersion) {
items.push({
label: 'NVIDIA',
value: gpuCapabilities.cuda.driverVersion,
});
}
if (gpuCapabilities.cuda.devices.length > 0) {
items.push({
label: 'CUDA',
@ -55,13 +62,13 @@ export const createDriverItems = (hardwareInfo: HardwareInfo) => {
? gpuCapabilities.cuda.version
: 'Available',
});
}
if (gpuCapabilities.cuda.driverVersion) {
items.push({
label: 'NVIDIA',
value: gpuCapabilities.cuda.driverVersion,
});
}
if (gpuCapabilities.rocm.driverVersion) {
items.push({
label: 'AMD',
value: gpuCapabilities.rocm.driverVersion,
});
}
if (gpuCapabilities.rocm.devices.length > 0) {
@ -71,13 +78,6 @@ export const createDriverItems = (hardwareInfo: HardwareInfo) => {
? gpuCapabilities.rocm.version
: 'Available',
});
if (gpuCapabilities.rocm.driverVersion) {
items.push({
label: 'AMD',
value: gpuCapabilities.rocm.driverVersion,
});
}
}
if (gpuCapabilities.vulkan.devices.length > 0) {