mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
settings about tab will now display driver and hardware info, allow re-downloading on binary version mismatches, clean up openwebui "itsdangerous" requirements, display a system monitor icon badge when monitoring is off
This commit is contained in:
parent
eaf4a04ce1
commit
61b730c3f7
22 changed files with 719 additions and 323 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "gerbil",
|
||||
"productName": "Gerbil",
|
||||
"version": "1.5.5",
|
||||
"version": "1.6.0-beta-1",
|
||||
"description": "Run Large Language Models locally",
|
||||
"main": "out/main/index.js",
|
||||
"homepage": "./",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
import { Button, Tooltip } from '@mantine/core';
|
||||
import { Button, Tooltip, ActionIcon } from '@mantine/core';
|
||||
import { Activity } from 'lucide-react';
|
||||
|
||||
interface PerformanceBadgeProps {
|
||||
label: string;
|
||||
value: string;
|
||||
label?: string;
|
||||
value?: string;
|
||||
tooltipLabel: string;
|
||||
iconOnly?: boolean;
|
||||
}
|
||||
|
||||
export const PerformanceBadge = ({
|
||||
label,
|
||||
value,
|
||||
tooltipLabel,
|
||||
iconOnly = false,
|
||||
}: PerformanceBadgeProps) => {
|
||||
const handlePerformanceClick = async () => {
|
||||
const result = await window.electronAPI.app.openPerformanceManager();
|
||||
|
|
@ -21,6 +24,16 @@ export const PerformanceBadge = ({
|
|||
}
|
||||
};
|
||||
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<Tooltip label={tooltipLabel} position="top">
|
||||
<ActionIcon size="sm" variant="subtle" onClick={handlePerformanceClick}>
|
||||
<Activity size="1.125rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label={tooltipLabel} position="top">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -93,37 +93,43 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
|
|||
</Group>
|
||||
|
||||
<Group gap="xs">
|
||||
{cpuMetrics && memoryMetrics && (
|
||||
{systemMonitoringEnabled ? (
|
||||
<>
|
||||
<PerformanceBadge
|
||||
label="CPU"
|
||||
value={`${cpuMetrics.usage}%`}
|
||||
tooltipLabel={`${cpuMetrics.usage}%${cpuMetrics.temperature ? ` • ${cpuMetrics.temperature}°C` : ''}`}
|
||||
/>
|
||||
{cpuMetrics && memoryMetrics && (
|
||||
<>
|
||||
<PerformanceBadge
|
||||
label="CPU"
|
||||
value={`${cpuMetrics.usage}%`}
|
||||
tooltipLabel={`${cpuMetrics.usage}%${cpuMetrics.temperature ? ` • ${cpuMetrics.temperature}°C` : ''}`}
|
||||
/>
|
||||
|
||||
<PerformanceBadge
|
||||
label="RAM"
|
||||
value={`${memoryMetrics.usage}%`}
|
||||
tooltipLabel={`${memoryMetrics.used.toFixed(2)} GB / ${memoryMetrics.total.toFixed(2)} GB (${memoryMetrics.usage}%)`}
|
||||
/>
|
||||
<PerformanceBadge
|
||||
label="RAM"
|
||||
value={`${memoryMetrics.usage}%`}
|
||||
tooltipLabel={`${memoryMetrics.used.toFixed(2)} GB / ${memoryMetrics.total.toFixed(2)} GB (${memoryMetrics.usage}%)`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{gpuMetrics?.gpus.map((gpu, index) => (
|
||||
<Group gap="xs" key={`gpu-${index}`}>
|
||||
<PerformanceBadge
|
||||
label={`GPU${gpuMetrics.gpus.length > 1 ? ` ${index + 1}` : ''}`}
|
||||
value={`${gpu.usage}%`}
|
||||
tooltipLabel={`${gpu.usage}%${gpu.temperature ? ` • ${gpu.temperature}°C` : ''}`}
|
||||
/>
|
||||
|
||||
<PerformanceBadge
|
||||
label={`VRAM${gpuMetrics.gpus.length > 1 ? ` ${index + 1}` : ''}`}
|
||||
value={`${gpu.memoryUsage}%`}
|
||||
tooltipLabel={`${gpu.memoryUsed.toFixed(2)} GB / ${gpu.memoryTotal.toFixed(2)} GB (${gpu.memoryUsage}%)`}
|
||||
/>
|
||||
</Group>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<PerformanceBadge tooltipLabel="System resource manager" iconOnly />
|
||||
)}
|
||||
|
||||
{gpuMetrics?.gpus.map((gpu, index) => (
|
||||
<Group gap="xs" key={`gpu-${index}`}>
|
||||
<PerformanceBadge
|
||||
label={`GPU${gpuMetrics.gpus.length > 1 ? ` ${index + 1}` : ''}`}
|
||||
value={`${gpu.usage}%`}
|
||||
tooltipLabel={`${gpu.usage}%${gpu.temperature ? ` • ${gpu.temperature}°C` : ''}`}
|
||||
/>
|
||||
|
||||
<PerformanceBadge
|
||||
label={`VRAM${gpuMetrics.gpus.length > 1 ? ` ${index + 1}` : ''}`}
|
||||
value={`${gpu.memoryUsage}%`}
|
||||
tooltipLabel={`${gpu.memoryUsed.toFixed(2)} GB / ${gpu.memoryTotal.toFixed(2)} GB (${gpu.memoryUsage}%)`}
|
||||
/>
|
||||
</Group>
|
||||
))}
|
||||
</Group>
|
||||
</Group>
|
||||
</AppShell.Footer>
|
||||
|
|
|
|||
|
|
@ -13,45 +13,43 @@ import { Download } from 'lucide-react';
|
|||
import { MouseEvent } from 'react';
|
||||
import { pretifyBinName, isWindowsROCmBuild } from '@/utils/assets';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import type { VersionInfo } from '@/types';
|
||||
|
||||
interface DownloadCardProps {
|
||||
name: string;
|
||||
version: VersionInfo;
|
||||
size: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
isCurrent?: boolean;
|
||||
isInstalled?: boolean;
|
||||
isDownloading?: boolean;
|
||||
downloadProgress?: number;
|
||||
disabled?: boolean;
|
||||
hasUpdate?: boolean;
|
||||
newerVersion?: string;
|
||||
onDownload: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||
onMakeCurrent?: () => void;
|
||||
onUpdate?: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||
onRedownload?: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
export const DownloadCard = ({
|
||||
name,
|
||||
version: versionInfo,
|
||||
size,
|
||||
version,
|
||||
description,
|
||||
isCurrent = false,
|
||||
isInstalled = false,
|
||||
isDownloading = false,
|
||||
downloadProgress = 0,
|
||||
disabled = false,
|
||||
hasUpdate = false,
|
||||
newerVersion,
|
||||
onDownload,
|
||||
onMakeCurrent,
|
||||
onUpdate,
|
||||
onRedownload,
|
||||
}: DownloadCardProps) => {
|
||||
const { resolvedColorScheme: colorScheme } = usePreferencesStore();
|
||||
const hasVersionMismatch = Boolean(
|
||||
versionInfo.version &&
|
||||
versionInfo.actualVersion &&
|
||||
versionInfo.version !== versionInfo.actualVersion
|
||||
);
|
||||
const renderActionButtons = () => {
|
||||
const buttons = [];
|
||||
|
||||
if (!isInstalled) {
|
||||
if (!versionInfo.isInstalled) {
|
||||
return (
|
||||
<Button
|
||||
key="download"
|
||||
|
|
@ -73,20 +71,21 @@ export const DownloadCard = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (!isCurrent && onMakeCurrent) {
|
||||
if (!versionInfo.isCurrent && onMakeCurrent) {
|
||||
buttons.push(
|
||||
<Button
|
||||
key="makeCurrent"
|
||||
variant="filled"
|
||||
size="xs"
|
||||
onClick={onMakeCurrent}
|
||||
disabled={disabled}
|
||||
>
|
||||
Make Current
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasUpdate && onUpdate) {
|
||||
if (versionInfo.hasUpdate && onUpdate) {
|
||||
buttons.push(
|
||||
<Button
|
||||
key="update"
|
||||
|
|
@ -104,7 +103,32 @@ export const DownloadCard = ({
|
|||
)
|
||||
}
|
||||
>
|
||||
{isDownloading ? 'Updating...' : `Update to ${newerVersion}`}
|
||||
{isDownloading
|
||||
? 'Updating...'
|
||||
: `Update to ${versionInfo.newerVersion}`}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasVersionMismatch && onRedownload) {
|
||||
buttons.push(
|
||||
<Button
|
||||
key="redownload"
|
||||
variant="filled"
|
||||
size="xs"
|
||||
onClick={onRedownload}
|
||||
loading={isDownloading}
|
||||
disabled={disabled}
|
||||
color="red"
|
||||
leftSection={
|
||||
isDownloading ? (
|
||||
<Loader size="1rem" />
|
||||
) : (
|
||||
<Download style={{ width: rem(14), height: rem(14) }} />
|
||||
)
|
||||
}
|
||||
>
|
||||
{isDownloading ? 'Re-downloading...' : 'Re-download'}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
@ -117,7 +141,7 @@ export const DownloadCard = ({
|
|||
withBorder
|
||||
radius="sm"
|
||||
padding="sm"
|
||||
{...(isCurrent && {
|
||||
{...(versionInfo.isCurrent && {
|
||||
bg: colorScheme === 'dark' ? 'dark.6' : 'gray.0',
|
||||
bd: `2px solid var(--mantine-color-${colorScheme === 'dark' ? 'blue-4' : 'blue-6'})`,
|
||||
})}
|
||||
|
|
@ -126,19 +150,19 @@ export const DownloadCard = ({
|
|||
<div style={{ flex: 1 }}>
|
||||
<Group gap="xs" align="center" mb="xs">
|
||||
<Text fw={500} size="sm">
|
||||
{pretifyBinName(name)}
|
||||
{pretifyBinName(versionInfo.name)}
|
||||
</Text>
|
||||
{isCurrent && (
|
||||
{versionInfo.isCurrent && (
|
||||
<Badge variant="light" color="blue" size="sm">
|
||||
Current
|
||||
</Badge>
|
||||
)}
|
||||
{hasUpdate && (
|
||||
{versionInfo.hasUpdate && (
|
||||
<Badge variant="light" color="orange" size="sm">
|
||||
Update Available
|
||||
</Badge>
|
||||
)}
|
||||
{isWindowsROCmBuild(name) && (
|
||||
{isWindowsROCmBuild(versionInfo.name) && (
|
||||
<Badge variant="light" color="yellow" size="sm">
|
||||
Experimental
|
||||
</Badge>
|
||||
|
|
@ -150,9 +174,15 @@ export const DownloadCard = ({
|
|||
</Text>
|
||||
)}
|
||||
<Group gap="xs" align="center">
|
||||
{version && (
|
||||
{versionInfo.version && (
|
||||
<Text size="xs" c="dimmed">
|
||||
Version {version}
|
||||
Version {versionInfo.version}
|
||||
{hasVersionMismatch && versionInfo.actualVersion && (
|
||||
<span style={{ color: 'var(--mantine-color-red-6)' }}>
|
||||
{' '}
|
||||
(actual: {versionInfo.actualVersion})
|
||||
</span>
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
{size && (
|
||||
|
|
|
|||
91
src/components/InfoCard.tsx
Normal file
91
src/components/InfoCard.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import {
|
||||
rem,
|
||||
ActionIcon,
|
||||
Tooltip,
|
||||
Card,
|
||||
Stack,
|
||||
Group,
|
||||
Text,
|
||||
} from '@mantine/core';
|
||||
import { Copy } from 'lucide-react';
|
||||
import { safeExecute } from '@/utils/logger';
|
||||
|
||||
export interface InfoItem {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface InfoCardProps {
|
||||
title: string;
|
||||
items: InfoItem[];
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export const InfoCard = ({ title, items, loading = false }: InfoCardProps) => {
|
||||
const copyInfo = async () => {
|
||||
const info = items.map((item) => `${item.label}: ${item.value}`).join('\n');
|
||||
|
||||
await safeExecute(
|
||||
() => navigator.clipboard.writeText(info),
|
||||
`Failed to copy ${title.toLowerCase()}`
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card withBorder radius="md" p="xs">
|
||||
<Text size="md" fw={600} mb="sm">
|
||||
{title}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Loading...
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card withBorder radius="md" p="xs" style={{ position: 'relative' }}>
|
||||
<Tooltip label="Copy">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={copyInfo}
|
||||
aria-label={`Copy ${title}`}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<Copy style={{ width: rem(14), height: rem(14) }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Text size="md" fw={600} mb="sm">
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
<Stack gap="xs">
|
||||
{items.map((item, index) => (
|
||||
<Group key={index} gap="md" align="center" wrap="nowrap">
|
||||
<Text size="sm" fw={500} c="dimmed" style={{ minWidth: '7.5rem' }}>
|
||||
{item.label}:
|
||||
</Text>
|
||||
<Text
|
||||
size="sm"
|
||||
ff="monospace"
|
||||
style={{
|
||||
wordBreak: 'break-all',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{item.value}
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
@ -82,12 +82,19 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
ref={isDownloading ? downloadingItemRef : null}
|
||||
>
|
||||
<DownloadCard
|
||||
name={download.name}
|
||||
version={{
|
||||
name: download.name,
|
||||
version: download.version || '',
|
||||
size: download.size,
|
||||
isInstalled: false,
|
||||
isCurrent: false,
|
||||
downloadUrl: download.url,
|
||||
hasUpdate: false,
|
||||
}}
|
||||
size={formatDownloadSize(
|
||||
download.size,
|
||||
download.url
|
||||
)}
|
||||
version={download.version}
|
||||
description={getAssetDescription(download.name)}
|
||||
isDownloading={isDownloading}
|
||||
downloadProgress={
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
createSoftwareItems,
|
||||
createDriverItems,
|
||||
createHardwareItems,
|
||||
type HardwareInfo,
|
||||
} from '@/utils/systemInfo';
|
||||
import {
|
||||
Text,
|
||||
Stack,
|
||||
|
|
@ -9,19 +15,22 @@ import {
|
|||
Badge,
|
||||
Button,
|
||||
rem,
|
||||
ActionIcon,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { Github, FolderOpen, Copy, FileText } from 'lucide-react';
|
||||
import { safeExecute } from '@/utils/logger';
|
||||
import { Github, FolderOpen, FileText } from 'lucide-react';
|
||||
import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
|
||||
import type { VersionInfo } from '@/types/electron';
|
||||
import type { SystemVersionInfo } from '@/types/electron';
|
||||
import { PRODUCT_NAME, GITHUB_API } from '@/constants';
|
||||
import { InfoCard } from '@/components/InfoCard';
|
||||
|
||||
import icon from '/icon.png';
|
||||
|
||||
export const AboutTab = () => {
|
||||
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null);
|
||||
const [versionInfo, setVersionInfo] = useState<SystemVersionInfo | null>(
|
||||
null
|
||||
);
|
||||
const [hardwareInfo, setHardwareInfo] = useState<HardwareInfo | null>(null);
|
||||
const { handleLogoClick, getLogoStyles } = useLogoClickSounds();
|
||||
|
||||
useEffect(() => {
|
||||
const loadVersionInfo = async () => {
|
||||
const info = await window.electronAPI.app.getVersionInfo();
|
||||
|
|
@ -29,7 +38,35 @@ export const AboutTab = () => {
|
|||
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) {
|
||||
|
|
@ -40,47 +77,9 @@ export const AboutTab = () => {
|
|||
);
|
||||
}
|
||||
|
||||
const versionItems = [
|
||||
{
|
||||
label: PRODUCT_NAME,
|
||||
value: versionInfo.aurPackageVersion
|
||||
? (() => {
|
||||
const pkgrel = versionInfo.aurPackageVersion.split('-')[1];
|
||||
|
||||
return pkgrel
|
||||
? `${versionInfo.appVersion} (AUR${pkgrel !== '1' ? ' r' + pkgrel : ''})`
|
||||
: versionInfo.appVersion;
|
||||
})()
|
||||
: versionInfo.appVersion,
|
||||
},
|
||||
{ label: 'Electron', value: versionInfo.electronVersion },
|
||||
{
|
||||
label: 'Node.js',
|
||||
value: versionInfo.nodeJsSystemVersion
|
||||
? `${versionInfo.nodeVersion} (System: ${versionInfo.nodeJsSystemVersion})`
|
||||
: versionInfo.nodeVersion,
|
||||
},
|
||||
{ label: 'Chromium', value: versionInfo.chromeVersion },
|
||||
{ label: 'V8', value: versionInfo.v8Version },
|
||||
{
|
||||
label: 'OS',
|
||||
value: `${versionInfo.platform} ${versionInfo.arch} (${versionInfo.osVersion})`,
|
||||
},
|
||||
...(versionInfo.uvVersion
|
||||
? [{ label: 'uv', value: versionInfo.uvVersion }]
|
||||
: []),
|
||||
];
|
||||
|
||||
const copyVersionInfo = async () => {
|
||||
const info = versionItems
|
||||
.map((item) => `${item.label}: ${item.value}`)
|
||||
.join('\n');
|
||||
|
||||
await safeExecute(
|
||||
() => navigator.clipboard.writeText(info),
|
||||
'Failed to copy version info'
|
||||
);
|
||||
};
|
||||
const softwareItems = createSoftwareItems(versionInfo);
|
||||
const driverItems = hardwareInfo ? createDriverItems(hardwareInfo) : [];
|
||||
const hardwareItems = hardwareInfo ? createHardwareItems(hardwareInfo) : [];
|
||||
|
||||
const actionButtons = [
|
||||
{
|
||||
|
|
@ -158,48 +157,15 @@ export const AboutTab = () => {
|
|||
</Group>
|
||||
</Card>
|
||||
|
||||
<Card withBorder radius="md" p="xs" style={{ position: 'relative' }}>
|
||||
<Tooltip label="Copy Info">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={copyVersionInfo}
|
||||
aria-label="Copy Version Info"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<Copy style={{ width: rem(14), height: rem(14) }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<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: '7.5rem' }}
|
||||
>
|
||||
{item.label}:
|
||||
</Text>
|
||||
<Text
|
||||
size="sm"
|
||||
ff="monospace"
|
||||
style={{
|
||||
wordBreak: 'break-all',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{item.value}
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
<InfoCard title="Software" items={softwareItems} />
|
||||
|
||||
<InfoCard title="Drivers" items={driverItems} loading={!hardwareInfo} />
|
||||
|
||||
<InfoCard
|
||||
title="Hardware"
|
||||
items={hardwareItems}
|
||||
loading={!hardwareInfo}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,18 +20,7 @@ import { formatDownloadSize } from '@/utils/format';
|
|||
|
||||
import { useKoboldVersionsStore } from '@/stores/koboldVersions';
|
||||
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron';
|
||||
|
||||
interface VersionInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
size?: number;
|
||||
isInstalled: boolean;
|
||||
isCurrent: boolean;
|
||||
downloadUrl?: string;
|
||||
installedPath?: string;
|
||||
hasUpdate?: boolean;
|
||||
newerVersion?: string;
|
||||
}
|
||||
import type { VersionInfo } from '@/types';
|
||||
|
||||
export const VersionsTab = () => {
|
||||
const {
|
||||
|
|
@ -119,6 +108,7 @@ export const VersionsTab = () => {
|
|||
installedPath: installedVersion.path,
|
||||
hasUpdate,
|
||||
newerVersion: hasUpdate ? download.version : undefined,
|
||||
actualVersion: installedVersion.actualVersion,
|
||||
});
|
||||
} else {
|
||||
versions.push({
|
||||
|
|
@ -146,6 +136,7 @@ export const VersionsTab = () => {
|
|||
isInstalled: true,
|
||||
isCurrent,
|
||||
installedPath: installed.path,
|
||||
actualVersion: installed.actualVersion,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -194,16 +185,31 @@ export const VersionsTab = () => {
|
|||
await loadInstalledVersions();
|
||||
};
|
||||
|
||||
const makeCurrent = async (version: VersionInfo) => {
|
||||
const handleRedownload = async (version: VersionInfo) => {
|
||||
const download = availableDownloads.find((d) => d.name === version.name);
|
||||
if (!download) return;
|
||||
|
||||
await handleDownloadFromStore({
|
||||
item: download,
|
||||
isUpdate: true,
|
||||
wasCurrentBinary: version.isCurrent,
|
||||
oldVersionPath: version.installedPath,
|
||||
});
|
||||
|
||||
await loadInstalledVersions();
|
||||
};
|
||||
|
||||
const makeCurrent = (version: VersionInfo) => {
|
||||
if (!version.installedPath) return;
|
||||
|
||||
const success = await window.electronAPI.kobold.setCurrentVersion(
|
||||
version.installedPath
|
||||
const targetInstalledVersion = installedVersions.find(
|
||||
(v) => v.path === version.installedPath
|
||||
);
|
||||
|
||||
if (success) {
|
||||
await loadInstalledVersions();
|
||||
if (targetInstalledVersion) {
|
||||
setCurrentVersion(targetInstalledVersion);
|
||||
}
|
||||
|
||||
window.electronAPI.kobold.setCurrentVersion(version.installedPath);
|
||||
};
|
||||
|
||||
if (loadingInstalled || loadingPlatform || loadingRemote) {
|
||||
|
|
@ -257,21 +263,16 @@ export const VersionsTab = () => {
|
|||
ref={isDownloading ? downloadingItemRef : null}
|
||||
>
|
||||
<DownloadCard
|
||||
name={version.name}
|
||||
version={version}
|
||||
size={
|
||||
version.size
|
||||
? formatDownloadSize(version.size, version.downloadUrl)
|
||||
: ''
|
||||
}
|
||||
version={version.version}
|
||||
description={getAssetDescription(version.name)}
|
||||
isCurrent={version.isCurrent}
|
||||
isInstalled={version.isInstalled}
|
||||
isDownloading={isDownloading}
|
||||
downloadProgress={downloadProgress[version.name]}
|
||||
disabled={downloading !== null}
|
||||
hasUpdate={version.hasUpdate}
|
||||
newerVersion={version.newerVersion}
|
||||
onDownload={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDownload(version);
|
||||
|
|
@ -280,6 +281,10 @@ export const VersionsTab = () => {
|
|||
e.stopPropagation();
|
||||
handleUpdate(version);
|
||||
}}
|
||||
onRedownload={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRedownload(version);
|
||||
}}
|
||||
onMakeCurrent={() => makeCurrent(version)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ const checkVramWarnings = async (backend: string): Promise<Warning[]> => {
|
|||
|
||||
if (lowVramGpus.length > 0) {
|
||||
const memoryDetails = lowVramGpus
|
||||
.map((gpu) => `${gpu.deviceName}: ${gpu.totalMemoryGB!.toFixed(1)}GB`)
|
||||
.map((gpu) => `${gpu.totalMemoryGB!.toFixed(1)}GB`)
|
||||
.join(', ');
|
||||
|
||||
warnings.push({
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import {
|
|||
detectGPUCapabilities,
|
||||
detectGPUMemory,
|
||||
detectROCm,
|
||||
detectSystemMemory,
|
||||
} from '@/main/modules/hardware';
|
||||
import {
|
||||
detectBackendSupport,
|
||||
|
|
@ -121,6 +122,8 @@ export function setupIPCHandlers() {
|
|||
|
||||
ipcMain.handle('kobold:detectGPUMemory', () => detectGPUMemory());
|
||||
|
||||
ipcMain.handle('kobold:detectSystemMemory', () => detectSystemMemory());
|
||||
|
||||
ipcMain.handle('kobold:detectROCm', () => detectROCm());
|
||||
|
||||
ipcMain.handle('kobold:detectBackendSupport', () => detectBackendSupport());
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@ import type { ChildProcess } from 'child_process';
|
|||
import yauzl from 'yauzl';
|
||||
import { createWriteStream } from 'fs';
|
||||
|
||||
import { logError, safeExecute } from '@/utils/node/logging';
|
||||
import { logError, safeExecute, tryExecute } from '@/utils/node/logging';
|
||||
import { sendKoboldOutput } from './window';
|
||||
import { getInstallDir } from './config';
|
||||
import { COMFYUI, SERVER_READY_SIGNALS, GITHUB_API } from '@/constants';
|
||||
import { terminateProcess } from '@/utils/node/process';
|
||||
import { parseKoboldConfig } from '@/utils/node/kobold';
|
||||
import { getAppVersion, ensureDir } from '@/utils/node/fs';
|
||||
import { getGPUData } from '@/utils/node/gpu';
|
||||
import { detectGPU } from './hardware';
|
||||
import { getUvEnvironment } from './dependencies';
|
||||
|
||||
interface ComfyUIVersionInfo {
|
||||
|
|
@ -61,15 +61,10 @@ async function saveComfyUIVersion(
|
|||
workspaceDir: string,
|
||||
version: ComfyUIVersionInfo
|
||||
) {
|
||||
try {
|
||||
await tryExecute(async () => {
|
||||
const versionFile = join(workspaceDir, '.version.json');
|
||||
await writeFile(versionFile, JSON.stringify(version, null, 2));
|
||||
} catch (error) {
|
||||
logError(
|
||||
'Failed to save ComfyUI version info',
|
||||
error instanceof Error ? error : undefined
|
||||
);
|
||||
}
|
||||
}, 'Failed to save ComfyUI version info');
|
||||
}
|
||||
|
||||
async function shouldUpdateComfyUI(workspaceDir: string) {
|
||||
|
|
@ -102,9 +97,8 @@ on('SIGTERM', () => {
|
|||
|
||||
async function shouldForceCPUMode() {
|
||||
try {
|
||||
const gpus = await getGPUData();
|
||||
const hasAMD = gpus.some((gpu) => gpu.deviceName.includes('AMD'));
|
||||
const hasNVIDIA = gpus.some((gpu) => gpu.deviceName.includes('NVIDIA'));
|
||||
const gpuInfo = await detectGPU();
|
||||
const { hasAMD, hasNVIDIA } = gpuInfo;
|
||||
const isWindows = platform === 'win32';
|
||||
|
||||
return (hasAMD && isWindows) || (!hasAMD && !hasNVIDIA);
|
||||
|
|
@ -125,9 +119,8 @@ async function getPyTorchInstallArgs(pythonPath: string) {
|
|||
];
|
||||
|
||||
try {
|
||||
const gpus = await getGPUData();
|
||||
const hasAMD = gpus.some((gpu) => gpu.deviceName.includes('AMD'));
|
||||
const hasNVIDIA = gpus.some((gpu) => gpu.deviceName.includes('NVIDIA'));
|
||||
const gpuInfo = await detectGPU();
|
||||
const { hasAMD, hasNVIDIA } = gpuInfo;
|
||||
const isWindows = platform === 'win32';
|
||||
|
||||
if (hasAMD && !isWindows) {
|
||||
|
|
@ -597,10 +590,7 @@ export async function startFrontend(args: string[]) {
|
|||
|
||||
await waitForComfyUIToStart();
|
||||
} catch (error) {
|
||||
logError(
|
||||
'Failed to start ComfyUI',
|
||||
error instanceof Error ? error : undefined
|
||||
);
|
||||
logError('Failed to start ComfyUI', error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ export async function detectCPU() {
|
|||
|
||||
const devices: string[] = [];
|
||||
if (cpu.brand) {
|
||||
devices.push(formatDeviceName(cpu.brand));
|
||||
devices.push(
|
||||
`${formatDeviceName(cpu.brand)} (${cpu.physicalCores} cores, ${cpu.speed} GHz)`
|
||||
);
|
||||
}
|
||||
|
||||
const avx = flags.includes('avx') || flags.includes('AVX');
|
||||
|
|
@ -59,35 +61,28 @@ export async function detectGPU() {
|
|||
}
|
||||
|
||||
const result = await safeExecute(async () => {
|
||||
const gpuData = await getGPUData();
|
||||
const graphics = await si.graphics();
|
||||
|
||||
let hasAMD = false;
|
||||
let hasNVIDIA = false;
|
||||
const gpuInfo: string[] = [];
|
||||
|
||||
for (const gpu of gpuData) {
|
||||
if (gpu.deviceName) {
|
||||
gpuInfo.push(formatDeviceName(gpu.deviceName));
|
||||
}
|
||||
|
||||
const deviceName = gpu.deviceName?.toLowerCase() || '';
|
||||
|
||||
for (const controller of graphics.controllers) {
|
||||
// Check vendor for AMD
|
||||
if (
|
||||
deviceName.includes('amd') ||
|
||||
deviceName.includes('ati') ||
|
||||
deviceName.includes('radeon')
|
||||
controller.vendor?.toLowerCase().includes('amd') ||
|
||||
controller.vendor?.toLowerCase().includes('ati')
|
||||
) {
|
||||
hasAMD = true;
|
||||
}
|
||||
|
||||
if (
|
||||
deviceName.includes('nvidia') ||
|
||||
deviceName.includes('geforce') ||
|
||||
deviceName.includes('gtx') ||
|
||||
deviceName.includes('rtx')
|
||||
) {
|
||||
if (controller.vendor?.toLowerCase().includes('nvidia')) {
|
||||
hasNVIDIA = true;
|
||||
}
|
||||
|
||||
if (controller.model) {
|
||||
gpuInfo.push(controller.model);
|
||||
}
|
||||
}
|
||||
|
||||
const basicInfo = {
|
||||
|
|
@ -95,7 +90,6 @@ export async function detectGPU() {
|
|||
hasNVIDIA,
|
||||
gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
|
||||
};
|
||||
|
||||
basicGPUInfoCache = basicInfo;
|
||||
return basicInfo;
|
||||
}, 'GPU detection failed');
|
||||
|
|
@ -133,7 +127,7 @@ async function detectCUDA() {
|
|||
try {
|
||||
const { stdout } = await execa(
|
||||
'nvidia-smi',
|
||||
['--query-gpu=name,memory.total,memory.free', '--format=csv,noheader'],
|
||||
['--query-gpu=name,driver_version', '--format=csv,noheader,nounits'],
|
||||
{
|
||||
timeout: 5000,
|
||||
reject: false,
|
||||
|
|
@ -141,19 +135,41 @@ async function detectCUDA() {
|
|||
);
|
||||
|
||||
if (stdout.trim()) {
|
||||
const devices = stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
const parts = line.split(',');
|
||||
const rawName = parts[0]?.trim() || 'Unknown NVIDIA GPU';
|
||||
return formatDeviceName(rawName);
|
||||
})
|
||||
.filter(Boolean);
|
||||
// Check for error messages that indicate nvidia-smi failed
|
||||
const errorPatterns = [
|
||||
'NVIDIA-SMI has failed',
|
||||
'No devices found',
|
||||
'Unable to determine the device handle',
|
||||
"couldn't communicate with the NVIDIA driver",
|
||||
'No NVIDIA GPU found',
|
||||
];
|
||||
|
||||
const hasError = errorPatterns.some((pattern) =>
|
||||
stdout.toLowerCase().includes(pattern.toLowerCase())
|
||||
);
|
||||
|
||||
if (hasError) {
|
||||
return { supported: false, devices: [] } as const;
|
||||
}
|
||||
|
||||
const lines = stdout.trim().split('\n');
|
||||
const devices: string[] = [];
|
||||
let version: string | undefined;
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.split(',');
|
||||
const rawName = parts[0]?.trim() || 'Unknown NVIDIA GPU';
|
||||
devices.push(formatDeviceName(rawName));
|
||||
|
||||
if (!version && parts[1]?.trim()) {
|
||||
version = parts[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
supported: devices.length > 0,
|
||||
devices,
|
||||
version,
|
||||
} as const;
|
||||
}
|
||||
|
||||
|
|
@ -228,9 +244,28 @@ export async function detectROCm() {
|
|||
}
|
||||
}
|
||||
|
||||
let version: string | undefined;
|
||||
try {
|
||||
const { stdout: amdSmiOutput } = await execa('amd-smi', {
|
||||
timeout: 3000,
|
||||
reject: false,
|
||||
});
|
||||
|
||||
if (amdSmiOutput.trim()) {
|
||||
const match =
|
||||
amdSmiOutput.match(/ROCm version:\s*(\d+\.\d+\.\d+)/i) ||
|
||||
amdSmiOutput.match(/version\s*(\d+\.\d+\.\d+)/i) ||
|
||||
amdSmiOutput.match(/(\d+\.\d+\.\d+)/);
|
||||
if (match) {
|
||||
version = match[1];
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
supported: devices.length > 0,
|
||||
devices,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -263,15 +298,29 @@ async function detectVulkan() {
|
|||
}
|
||||
}
|
||||
|
||||
let version = 'Unknown';
|
||||
try {
|
||||
const apiVersionLine = lines.find(
|
||||
(line) => line.includes('apiVersion') && line.includes('=')
|
||||
);
|
||||
if (apiVersionLine) {
|
||||
const match = apiVersionLine.match(/=\s*(\d+\.\d+(?:\.\d+)?)/);
|
||||
if (match) {
|
||||
version = match[1];
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
supported: devices.length > 0,
|
||||
devices,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
return { supported: false, devices: [] };
|
||||
return { supported: false, devices: [], version: 'Unknown' };
|
||||
} catch {
|
||||
return { supported: false, devices: [] };
|
||||
return { supported: false, devices: [], version: 'Unknown' };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -336,15 +385,34 @@ async function detectCLBlast() {
|
|||
|
||||
if (stdout.trim()) {
|
||||
const devices = parseClInfoOutput(stdout);
|
||||
|
||||
let version = 'Unknown';
|
||||
try {
|
||||
const versionLine = stdout
|
||||
.split('\n')
|
||||
.find(
|
||||
(line) =>
|
||||
line.includes('OpenCL C version') ||
|
||||
line.includes('CL_DEVICE_OPENCL_C_VERSION')
|
||||
);
|
||||
if (versionLine) {
|
||||
const match = versionLine.match(/OpenCL C\s+(\d+\.\d+)/);
|
||||
if (match) {
|
||||
version = match[1];
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
supported: devices.length > 0,
|
||||
devices,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
return { supported: false, devices: [] };
|
||||
return { supported: false, devices: [], version: 'Unknown' };
|
||||
} catch {
|
||||
return { supported: false, devices: [] };
|
||||
return { supported: false, devices: [], version: 'Unknown' };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -358,18 +426,15 @@ export async function detectGPUMemory() {
|
|||
const memoryInfo: GPUMemoryInfo[] = [];
|
||||
|
||||
for (const gpu of gpuData) {
|
||||
if (gpu.deviceName) {
|
||||
let vram: number | null = gpu.memoryTotal;
|
||||
let vram: number | null = gpu.memoryTotal;
|
||||
|
||||
if (!vram || vram <= 1) {
|
||||
vram = null;
|
||||
}
|
||||
|
||||
memoryInfo.push({
|
||||
deviceName: formatDeviceName(gpu.deviceName),
|
||||
totalMemoryGB: vram,
|
||||
});
|
||||
if (!vram || vram <= 1) {
|
||||
vram = null;
|
||||
}
|
||||
|
||||
memoryInfo.push({
|
||||
totalMemoryGB: vram,
|
||||
});
|
||||
}
|
||||
|
||||
return memoryInfo;
|
||||
|
|
@ -378,3 +443,44 @@ export async function detectGPUMemory() {
|
|||
gpuMemoryInfoCache = result || [];
|
||||
return gpuMemoryInfoCache;
|
||||
}
|
||||
|
||||
export const detectSystemMemory = async () => {
|
||||
try {
|
||||
const [memInfo, memLayout] = await Promise.all([si.mem(), si.memLayout()]);
|
||||
|
||||
const totalGB = Math.round(memInfo.total / 1024 ** 3);
|
||||
|
||||
let speed: number | undefined;
|
||||
let type: string | undefined;
|
||||
|
||||
if (memLayout && memLayout.length > 0) {
|
||||
const populatedSlot = memLayout.find(
|
||||
(slot) =>
|
||||
slot.size > 0 &&
|
||||
((slot.clockSpeed && slot.clockSpeed > 0) ||
|
||||
(slot.type && slot.type !== ''))
|
||||
);
|
||||
|
||||
if (populatedSlot) {
|
||||
speed =
|
||||
populatedSlot.clockSpeed && populatedSlot.clockSpeed > 0
|
||||
? populatedSlot.clockSpeed
|
||||
: undefined;
|
||||
type =
|
||||
populatedSlot.type && populatedSlot.type !== ''
|
||||
? populatedSlot.type
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalGB,
|
||||
speed,
|
||||
type,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
totalGB: Math.round((await si.mem()).total / 1024 ** 3),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -43,14 +43,18 @@ export async function getInstalledVersions() {
|
|||
|
||||
const versionPromises = launchers.map(async (launcher) => {
|
||||
try {
|
||||
const detectedVersion = await getVersionFromBinary(launcher.path);
|
||||
const version = detectedVersion || 'unknown';
|
||||
const versionInfo = await getVersionFromBinary(launcher.path);
|
||||
|
||||
if (!versionInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
version: versionInfo.version,
|
||||
path: launcher.path,
|
||||
filename: launcher.filename,
|
||||
size: launcher.size,
|
||||
actualVersion: versionInfo.actualVersion,
|
||||
} as InstalledVersion;
|
||||
} catch (error) {
|
||||
logError(
|
||||
|
|
@ -131,42 +135,56 @@ export async function getVersionFromBinary(launcherPath: string) {
|
|||
return null;
|
||||
}
|
||||
|
||||
let folderVersion: string | null = null;
|
||||
let actualVersion: string | null = null;
|
||||
|
||||
const folderName = launcherPath.split(/[/\\]/).slice(-2, -1)[0];
|
||||
if (folderName) {
|
||||
const versionMatch = folderName.match(
|
||||
/-(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/
|
||||
);
|
||||
if (versionMatch) {
|
||||
return versionMatch[1];
|
||||
folderVersion = versionMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
const result = await execa(launcherPath, ['--version'], {
|
||||
timeout: 30000,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
try {
|
||||
const result = await execa(launcherPath, ['--version'], {
|
||||
timeout: 30000,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
const allOutput = (result.stdout + result.stderr).trim();
|
||||
const allOutput = (result.stdout + result.stderr).trim();
|
||||
|
||||
if (/^\d+\.\d+/.test(allOutput)) {
|
||||
const versionParts = allOutput.split(/\s+/)[0];
|
||||
if (versionParts && /^\d+\.\d+/.test(versionParts)) {
|
||||
return versionParts;
|
||||
}
|
||||
}
|
||||
|
||||
const lines = allOutput.split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
if (/^\d+\.\d+/.test(trimmedLine)) {
|
||||
const versionPart = trimmedLine.split(/\s+/)[0];
|
||||
if (versionPart) {
|
||||
return versionPart;
|
||||
if (/^\d+\.\d+/.test(allOutput)) {
|
||||
const versionParts = allOutput.split(/\s+/)[0];
|
||||
if (versionParts && /^\d+\.\d+/.test(versionParts)) {
|
||||
actualVersion = versionParts;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
if (!actualVersion) {
|
||||
const lines = allOutput.split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
if (/^\d+\.\d+/.test(trimmedLine)) {
|
||||
const versionPart = trimmedLine.split(/\s+/)[0];
|
||||
if (versionPart) {
|
||||
actualVersion = versionPart;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
version: folderVersion || actualVersion || 'unknown',
|
||||
actualVersion:
|
||||
folderVersion && actualVersion && folderVersion !== actualVersion
|
||||
? actualVersion
|
||||
: undefined,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { BrowserWindow } from 'electron';
|
|||
import { platform } from 'process';
|
||||
import { spawn } from 'child_process';
|
||||
import { getGPUData } from '@/utils/node/gpu';
|
||||
import { detectGPU } from './hardware';
|
||||
import { tryExecute, safeExecute } from '@/utils/node/logging';
|
||||
|
||||
export interface CpuMetrics {
|
||||
|
|
@ -134,18 +135,18 @@ async function collectAndSendMemoryMetrics() {
|
|||
|
||||
async function collectAndSendGpuMetrics() {
|
||||
await tryExecute(async () => {
|
||||
const gpuData = await getGPUData();
|
||||
const [gpuData, gpuInfo] = await Promise.all([getGPUData(), detectGPU()]);
|
||||
const metrics: GpuMetrics = {
|
||||
gpus: gpuData.map((gpuInfo) => ({
|
||||
name: gpuInfo.deviceName,
|
||||
usage: Math.round(gpuInfo.usage),
|
||||
memoryUsed: gpuInfo.memoryUsed,
|
||||
memoryTotal: gpuInfo.memoryTotal,
|
||||
gpus: gpuData.map((gpuData, index) => ({
|
||||
name: gpuInfo.gpuInfo[index] || `GPU ${index}`,
|
||||
usage: Math.round(gpuData.usage),
|
||||
memoryUsed: gpuData.memoryUsed,
|
||||
memoryTotal: gpuData.memoryTotal,
|
||||
memoryUsage:
|
||||
gpuInfo.memoryTotal > 0
|
||||
? Math.round((gpuInfo.memoryUsed / gpuInfo.memoryTotal) * 100)
|
||||
gpuData.memoryTotal > 0
|
||||
? Math.round((gpuData.memoryUsed / gpuData.memoryTotal) * 100)
|
||||
: 0,
|
||||
temperature: gpuInfo.temperature,
|
||||
temperature: gpuData.temperature,
|
||||
})),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,14 +14,7 @@ import { getUvEnvironment } from './dependencies';
|
|||
|
||||
let openWebUIProcess: ChildProcess | null = null;
|
||||
|
||||
const OPENWEBUI_BASE_ARGS = [
|
||||
'--python',
|
||||
'3.11',
|
||||
'--with',
|
||||
'itsdangerous',
|
||||
'open-webui@latest',
|
||||
'serve',
|
||||
];
|
||||
const OPENWEBUI_BASE_ARGS = ['--python', '3.11', 'open-webui@latest', 'serve'];
|
||||
|
||||
on('SIGINT', () => {
|
||||
void stopFrontend();
|
||||
|
|
|
|||
35
src/main/modules/platform-gpu.ts
Normal file
35
src/main/modules/platform-gpu.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { platform } from 'process';
|
||||
import { detectGPU } from './hardware';
|
||||
import { safeExecute } from '@/utils/node/logging';
|
||||
|
||||
export interface PlatformGPUInfo {
|
||||
hasAMD: boolean;
|
||||
hasNVIDIA: boolean;
|
||||
isWindows: boolean;
|
||||
shouldForceCPU: boolean;
|
||||
}
|
||||
|
||||
export async function getPlatformGPUInfo(): Promise<PlatformGPUInfo> {
|
||||
const result = await safeExecute(async () => {
|
||||
const gpuInfo = await detectGPU();
|
||||
const { hasAMD, hasNVIDIA } = gpuInfo;
|
||||
const isWindows = platform === 'win32';
|
||||
const shouldForceCPU = (hasAMD && isWindows) || (!hasAMD && !hasNVIDIA);
|
||||
|
||||
return {
|
||||
hasAMD,
|
||||
hasNVIDIA,
|
||||
isWindows,
|
||||
shouldForceCPU,
|
||||
};
|
||||
}, 'Failed to detect platform GPU information');
|
||||
|
||||
return (
|
||||
result || {
|
||||
hasAMD: false,
|
||||
hasNVIDIA: false,
|
||||
isWindows: platform === 'win32',
|
||||
shouldForceCPU: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ const koboldAPI: KoboldAPI = {
|
|||
detectGPUCapabilities: () =>
|
||||
ipcRenderer.invoke('kobold:detectGPUCapabilities'),
|
||||
detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'),
|
||||
detectSystemMemory: () => ipcRenderer.invoke('kobold:detectSystemMemory'),
|
||||
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
|
||||
detectBackendSupport: () => ipcRenderer.invoke('kobold:detectBackendSupport'),
|
||||
getAvailableBackends: (includeDisabled) =>
|
||||
|
|
|
|||
7
src/types/electron.d.ts
vendored
7
src/types/electron.d.ts
vendored
|
|
@ -3,6 +3,7 @@ import type {
|
|||
GPUCapabilities,
|
||||
BasicGPUInfo,
|
||||
GPUMemoryInfo,
|
||||
SystemMemoryInfo,
|
||||
} from '@/types/hardware';
|
||||
import type { BackendOption, BackendSupport } from '@/types';
|
||||
import type { MantineColorScheme } from '@mantine/core';
|
||||
|
|
@ -55,6 +56,7 @@ export interface InstalledVersion {
|
|||
path: string;
|
||||
filename: string;
|
||||
size?: number;
|
||||
actualVersion?: string;
|
||||
}
|
||||
|
||||
export interface DownloadItem {
|
||||
|
|
@ -112,6 +114,7 @@ export interface KoboldAPI {
|
|||
detectCPU: () => Promise<CPUCapabilities>;
|
||||
detectGPUCapabilities: () => Promise<GPUCapabilities>;
|
||||
detectGPUMemory: () => Promise<GPUMemoryInfo[]>;
|
||||
detectSystemMemory: () => Promise<SystemMemoryInfo>;
|
||||
detectROCm: () => Promise<{ supported: boolean; devices: string[] }>;
|
||||
detectBackendSupport: () => Promise<BackendSupport | null>;
|
||||
getAvailableBackends: (includeDisabled?: boolean) => Promise<BackendOption[]>;
|
||||
|
|
@ -141,7 +144,7 @@ export interface KoboldAPI {
|
|||
onKoboldOutput: (callback: (data: string) => void) => () => void;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
export interface SystemVersionInfo {
|
||||
appVersion: string;
|
||||
electronVersion: string;
|
||||
nodeVersion: string;
|
||||
|
|
@ -160,7 +163,7 @@ export interface AppAPI {
|
|||
viewConfigFile: () => Promise<void>;
|
||||
openPath: (path: string) => Promise<void>;
|
||||
getVersion: () => Promise<string>;
|
||||
getVersionInfo: () => Promise<VersionInfo>;
|
||||
getVersionInfo: () => Promise<SystemVersionInfo>;
|
||||
minimizeWindow: () => void;
|
||||
maximizeWindow: () => void;
|
||||
closeWindow: () => void;
|
||||
|
|
|
|||
18
src/types/hardware.d.ts
vendored
18
src/types/hardware.d.ts
vendored
|
|
@ -5,26 +5,35 @@ export interface CPUCapabilities {
|
|||
}
|
||||
|
||||
export interface GPUMemoryInfo {
|
||||
deviceName: string;
|
||||
totalMemoryGB: number | null;
|
||||
}
|
||||
|
||||
export interface SystemMemoryInfo {
|
||||
totalGB: number;
|
||||
speed?: number;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface GPUCapabilities {
|
||||
cuda: {
|
||||
readonly supported: boolean;
|
||||
readonly devices: readonly string[];
|
||||
readonly version?: string;
|
||||
};
|
||||
rocm: {
|
||||
readonly supported: boolean;
|
||||
readonly devices: readonly string[];
|
||||
readonly version?: string;
|
||||
};
|
||||
vulkan: {
|
||||
readonly supported: boolean;
|
||||
readonly devices: readonly string[];
|
||||
readonly version?: string;
|
||||
};
|
||||
clblast: {
|
||||
readonly supported: boolean;
|
||||
readonly devices: readonly string[];
|
||||
readonly version?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -43,9 +52,6 @@ export interface HardwareInfo {
|
|||
cpu: CPUCapabilities;
|
||||
gpu: BasicGPUInfo;
|
||||
gpuCapabilities?: GPUCapabilities;
|
||||
}
|
||||
|
||||
export interface SystemCapabilities {
|
||||
hardware: HardwareInfo;
|
||||
platform: string;
|
||||
gpuMemory?: GPUMemoryInfo[];
|
||||
systemMemory?: SystemMemoryInfo;
|
||||
}
|
||||
|
|
|
|||
14
src/types/index.d.ts
vendored
14
src/types/index.d.ts
vendored
|
|
@ -45,6 +45,20 @@ export interface InstalledVersion {
|
|||
path: string;
|
||||
filename: string;
|
||||
size?: number;
|
||||
actualVersion?: string;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
size?: number;
|
||||
isInstalled: boolean;
|
||||
isCurrent: boolean;
|
||||
downloadUrl?: string;
|
||||
installedPath?: string;
|
||||
hasUpdate?: boolean;
|
||||
newerVersion?: string;
|
||||
actualVersion?: string;
|
||||
}
|
||||
|
||||
export interface DismissedUpdate {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@ import { join } from 'path';
|
|||
import { platform } from 'process';
|
||||
|
||||
interface CachedGPUInfo {
|
||||
deviceName: string;
|
||||
devicePath: string;
|
||||
memoryTotal: number;
|
||||
hwmonPath?: string;
|
||||
}
|
||||
|
||||
interface GPUData {
|
||||
deviceName: string;
|
||||
usage: number;
|
||||
memoryUsed: number;
|
||||
memoryTotal: number;
|
||||
|
|
@ -38,7 +36,6 @@ async function initializeLinuxGPUCache() {
|
|||
return linuxCachePromise;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
linuxCachePromise = (async () => {
|
||||
try {
|
||||
const drmPath = '/sys/class/drm';
|
||||
|
|
@ -50,42 +47,6 @@ async function initializeLinuxGPUCache() {
|
|||
const gpus = [];
|
||||
for (const card of cardEntries) {
|
||||
const devicePath = join(drmPath, card, 'device');
|
||||
|
||||
let deviceName = 'Unknown GPU';
|
||||
|
||||
try {
|
||||
const modalias = await readFile(
|
||||
`${devicePath}/modalias`,
|
||||
'utf8'
|
||||
).catch(() => '');
|
||||
|
||||
if (modalias.includes('amdgpu')) {
|
||||
deviceName = 'AMD GPU';
|
||||
} else if (
|
||||
modalias.includes('nouveau') ||
|
||||
modalias.includes('nvidia')
|
||||
) {
|
||||
deviceName = 'NVIDIA GPU';
|
||||
} else {
|
||||
try {
|
||||
const [vendorData, deviceData] = await Promise.all([
|
||||
readFile(`${devicePath}/vendor`, 'utf8').catch(() => ''),
|
||||
readFile(`${devicePath}/device`, 'utf8').catch(() => ''),
|
||||
]);
|
||||
|
||||
const vendorId = vendorData.trim();
|
||||
const deviceId = deviceData.trim();
|
||||
|
||||
if (vendorId === '0x1002') {
|
||||
deviceName = 'AMD GPU';
|
||||
} else if (vendorId === '0x10de') {
|
||||
deviceName = 'NVIDIA GPU';
|
||||
} else {
|
||||
deviceName = `GPU (${vendorId}:${deviceId})`;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
try {
|
||||
const memTotalData = await readFile(
|
||||
`${devicePath}/mem_info_vram_total`,
|
||||
|
|
@ -109,7 +70,6 @@ async function initializeLinuxGPUCache() {
|
|||
} catch {}
|
||||
|
||||
gpus.push({
|
||||
deviceName,
|
||||
devicePath,
|
||||
memoryTotal,
|
||||
hwmonPath,
|
||||
|
|
@ -167,7 +127,6 @@ async function getLinuxGPUData() {
|
|||
}
|
||||
|
||||
gpus.push({
|
||||
deviceName: cachedGPU.deviceName,
|
||||
usage,
|
||||
memoryUsed: parseFloat(memoryUsed.toFixed(2)),
|
||||
memoryTotal: parseFloat(cachedGPU.memoryTotal.toFixed(2)),
|
||||
|
|
|
|||
149
src/utils/systemInfo.ts
Normal file
149
src/utils/systemInfo.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import type { SystemVersionInfo } from '@/types/electron';
|
||||
import type {
|
||||
CPUCapabilities,
|
||||
GPUCapabilities,
|
||||
BasicGPUInfo,
|
||||
GPUMemoryInfo,
|
||||
SystemMemoryInfo,
|
||||
} from '@/types/hardware';
|
||||
import { PRODUCT_NAME } from '@/constants';
|
||||
import type { InfoItem } from '@/components/InfoCard';
|
||||
|
||||
export interface HardwareInfo {
|
||||
cpu: CPUCapabilities;
|
||||
gpu: BasicGPUInfo;
|
||||
gpuCapabilities: GPUCapabilities;
|
||||
gpuMemory: GPUMemoryInfo[];
|
||||
systemMemory: SystemMemoryInfo;
|
||||
}
|
||||
|
||||
export const createSoftwareItems = (versionInfo: SystemVersionInfo) => [
|
||||
{
|
||||
label: PRODUCT_NAME,
|
||||
value: versionInfo.aurPackageVersion
|
||||
? (() => {
|
||||
const pkgrel = versionInfo.aurPackageVersion.split('-')[1];
|
||||
return pkgrel
|
||||
? `${versionInfo.appVersion} (AUR${pkgrel !== '1' ? ' r' + pkgrel : ''})`
|
||||
: versionInfo.appVersion;
|
||||
})()
|
||||
: versionInfo.appVersion,
|
||||
},
|
||||
{ label: 'Electron', value: versionInfo.electronVersion },
|
||||
{
|
||||
label: 'Node.js',
|
||||
value: versionInfo.nodeJsSystemVersion
|
||||
? `${versionInfo.nodeVersion} (System: ${versionInfo.nodeJsSystemVersion})`
|
||||
: versionInfo.nodeVersion,
|
||||
},
|
||||
{ label: 'Chromium', value: versionInfo.chromeVersion },
|
||||
{ label: 'V8', value: versionInfo.v8Version },
|
||||
{
|
||||
label: 'OS',
|
||||
value: `${versionInfo.platform} ${versionInfo.arch} (${versionInfo.osVersion})`,
|
||||
},
|
||||
...(versionInfo.uvVersion
|
||||
? [{ label: 'uv', value: versionInfo.uvVersion }]
|
||||
: []),
|
||||
];
|
||||
|
||||
export const createDriverItems = (hardwareInfo: HardwareInfo) => {
|
||||
const items: InfoItem[] = [];
|
||||
const { gpuCapabilities } = hardwareInfo;
|
||||
|
||||
if (!gpuCapabilities) {
|
||||
return [
|
||||
{
|
||||
label: 'Driver Support',
|
||||
value: 'Loading...',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (gpuCapabilities.cuda.supported) {
|
||||
items.push({
|
||||
label: 'CUDA',
|
||||
value: gpuCapabilities.cuda.version
|
||||
? `v${gpuCapabilities.cuda.version}`
|
||||
: 'Available',
|
||||
});
|
||||
}
|
||||
|
||||
if (gpuCapabilities.rocm.supported) {
|
||||
items.push({
|
||||
label: 'ROCm',
|
||||
value: gpuCapabilities.rocm.version
|
||||
? `v${gpuCapabilities.rocm.version}`
|
||||
: 'Available',
|
||||
});
|
||||
}
|
||||
|
||||
if (gpuCapabilities.vulkan.supported) {
|
||||
items.push({
|
||||
label: 'Vulkan',
|
||||
value: gpuCapabilities.vulkan.version
|
||||
? `v${gpuCapabilities.vulkan.version}`
|
||||
: 'Available',
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
label: 'Vulkan',
|
||||
value: 'Not available',
|
||||
});
|
||||
}
|
||||
|
||||
if (gpuCapabilities.clblast.supported) {
|
||||
items.push({
|
||||
label: 'CLBlast',
|
||||
value: gpuCapabilities.clblast.version
|
||||
? `v${gpuCapabilities.clblast.version}`
|
||||
: 'Available',
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
label: 'CLBlast',
|
||||
value: 'Not available',
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
export const createHardwareItems = (hardwareInfo: HardwareInfo) => [
|
||||
{
|
||||
label: 'CPU',
|
||||
value:
|
||||
hardwareInfo.cpu.devices.length > 0
|
||||
? hardwareInfo.cpu.devices[0]
|
||||
: 'Unknown',
|
||||
},
|
||||
{
|
||||
label: 'RAM',
|
||||
value:
|
||||
hardwareInfo.systemMemory && hardwareInfo.systemMemory.totalGB > 0
|
||||
? `${hardwareInfo.systemMemory.totalGB.toFixed(1)} GB${
|
||||
hardwareInfo.systemMemory.type
|
||||
? ` ${hardwareInfo.systemMemory.type}`
|
||||
: ''
|
||||
}${
|
||||
hardwareInfo.systemMemory.speed
|
||||
? ` @ ${hardwareInfo.systemMemory.speed} MHz`
|
||||
: ''
|
||||
}`
|
||||
: 'Detecting...',
|
||||
},
|
||||
...(hardwareInfo.gpu.gpuInfo.length > 0
|
||||
? hardwareInfo.gpu.gpuInfo.map((gpu, index) => ({
|
||||
label: `GPU ${index > 0 ? index + 1 : ''}`.trim(),
|
||||
value: gpu,
|
||||
}))
|
||||
: [{ label: 'GPU', value: 'No GPU detected' }]),
|
||||
...(hardwareInfo.gpuMemory && hardwareInfo.gpuMemory.length > 0
|
||||
? hardwareInfo.gpuMemory.map((mem, index) => ({
|
||||
label: `VRAM ${index > 0 ? index + 1 : ''}`.trim(),
|
||||
value: mem.totalMemoryGB
|
||||
? `${mem.totalMemoryGB.toFixed(1)} GB`
|
||||
: 'Unknown',
|
||||
}))
|
||||
: []),
|
||||
];
|
||||
Loading…
Add table
Reference in a new issue