mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
re-organize new system info cards into a new System tab, hopefully fix app config re-writing race condition
This commit is contained in:
parent
ec2410c0e4
commit
3c2a07c730
11 changed files with 475 additions and 370 deletions
|
|
@ -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": "./",
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
|
|||
))}
|
||||
</>
|
||||
) : (
|
||||
<PerformanceBadge tooltipLabel="System resource manager" iconOnly />
|
||||
<PerformanceBadge tooltipLabel="Resource manager" iconOnly />
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
|
|
|
|||
|
|
@ -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'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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
270
src/components/settings/FrontendInterfaceSelector.tsx
Normal file
270
src/components/settings/FrontendInterfaceSelector.tsx
Normal 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()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
81
src/components/settings/SystemTab.tsx
Normal file
81
src/components/settings/SystemTab.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue