mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-04 04:04: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",
|
"name": "gerbil",
|
||||||
"productName": "Gerbil",
|
"productName": "Gerbil",
|
||||||
"version": "1.5.5",
|
"version": "1.6.0-beta-1",
|
||||||
"description": "Run Large Language Models locally",
|
"description": "Run Large Language Models locally",
|
||||||
"main": "out/main/index.js",
|
"main": "out/main/index.js",
|
||||||
"homepage": "./",
|
"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 {
|
interface PerformanceBadgeProps {
|
||||||
label: string;
|
label?: string;
|
||||||
value: string;
|
value?: string;
|
||||||
tooltipLabel: string;
|
tooltipLabel: string;
|
||||||
|
iconOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PerformanceBadge = ({
|
export const PerformanceBadge = ({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
tooltipLabel,
|
tooltipLabel,
|
||||||
|
iconOnly = false,
|
||||||
}: PerformanceBadgeProps) => {
|
}: PerformanceBadgeProps) => {
|
||||||
const handlePerformanceClick = async () => {
|
const handlePerformanceClick = async () => {
|
||||||
const result = await window.electronAPI.app.openPerformanceManager();
|
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 (
|
return (
|
||||||
<Tooltip label={tooltipLabel} position="top">
|
<Tooltip label={tooltipLabel} position="top">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -93,37 +93,43 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
{cpuMetrics && memoryMetrics && (
|
{systemMonitoringEnabled ? (
|
||||||
<>
|
<>
|
||||||
<PerformanceBadge
|
{cpuMetrics && memoryMetrics && (
|
||||||
label="CPU"
|
<>
|
||||||
value={`${cpuMetrics.usage}%`}
|
<PerformanceBadge
|
||||||
tooltipLabel={`${cpuMetrics.usage}%${cpuMetrics.temperature ? ` • ${cpuMetrics.temperature}°C` : ''}`}
|
label="CPU"
|
||||||
/>
|
value={`${cpuMetrics.usage}%`}
|
||||||
|
tooltipLabel={`${cpuMetrics.usage}%${cpuMetrics.temperature ? ` • ${cpuMetrics.temperature}°C` : ''}`}
|
||||||
|
/>
|
||||||
|
|
||||||
<PerformanceBadge
|
<PerformanceBadge
|
||||||
label="RAM"
|
label="RAM"
|
||||||
value={`${memoryMetrics.usage}%`}
|
value={`${memoryMetrics.usage}%`}
|
||||||
tooltipLabel={`${memoryMetrics.used.toFixed(2)} GB / ${memoryMetrics.total.toFixed(2)} GB (${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>
|
||||||
</Group>
|
</Group>
|
||||||
</AppShell.Footer>
|
</AppShell.Footer>
|
||||||
|
|
|
||||||
|
|
@ -13,45 +13,43 @@ import { Download } from 'lucide-react';
|
||||||
import { MouseEvent } from 'react';
|
import { MouseEvent } from 'react';
|
||||||
import { pretifyBinName, isWindowsROCmBuild } from '@/utils/assets';
|
import { pretifyBinName, isWindowsROCmBuild } from '@/utils/assets';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
import type { VersionInfo } from '@/types';
|
||||||
|
|
||||||
interface DownloadCardProps {
|
interface DownloadCardProps {
|
||||||
name: string;
|
version: VersionInfo;
|
||||||
size: string;
|
size: string;
|
||||||
version?: string;
|
|
||||||
description?: string;
|
description?: string;
|
||||||
isCurrent?: boolean;
|
|
||||||
isInstalled?: boolean;
|
|
||||||
isDownloading?: boolean;
|
isDownloading?: boolean;
|
||||||
downloadProgress?: number;
|
downloadProgress?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
hasUpdate?: boolean;
|
|
||||||
newerVersion?: string;
|
|
||||||
onDownload: (e: MouseEvent<HTMLButtonElement>) => void;
|
onDownload: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||||
onMakeCurrent?: () => void;
|
onMakeCurrent?: () => void;
|
||||||
onUpdate?: (e: MouseEvent<HTMLButtonElement>) => void;
|
onUpdate?: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
onRedownload?: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DownloadCard = ({
|
export const DownloadCard = ({
|
||||||
name,
|
version: versionInfo,
|
||||||
size,
|
size,
|
||||||
version,
|
|
||||||
description,
|
description,
|
||||||
isCurrent = false,
|
|
||||||
isInstalled = false,
|
|
||||||
isDownloading = false,
|
isDownloading = false,
|
||||||
downloadProgress = 0,
|
downloadProgress = 0,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
hasUpdate = false,
|
|
||||||
newerVersion,
|
|
||||||
onDownload,
|
onDownload,
|
||||||
onMakeCurrent,
|
onMakeCurrent,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
|
onRedownload,
|
||||||
}: DownloadCardProps) => {
|
}: DownloadCardProps) => {
|
||||||
const { resolvedColorScheme: colorScheme } = usePreferencesStore();
|
const { resolvedColorScheme: colorScheme } = usePreferencesStore();
|
||||||
|
const hasVersionMismatch = Boolean(
|
||||||
|
versionInfo.version &&
|
||||||
|
versionInfo.actualVersion &&
|
||||||
|
versionInfo.version !== versionInfo.actualVersion
|
||||||
|
);
|
||||||
const renderActionButtons = () => {
|
const renderActionButtons = () => {
|
||||||
const buttons = [];
|
const buttons = [];
|
||||||
|
|
||||||
if (!isInstalled) {
|
if (!versionInfo.isInstalled) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key="download"
|
key="download"
|
||||||
|
|
@ -73,20 +71,21 @@ export const DownloadCard = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isCurrent && onMakeCurrent) {
|
if (!versionInfo.isCurrent && onMakeCurrent) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<Button
|
<Button
|
||||||
key="makeCurrent"
|
key="makeCurrent"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={onMakeCurrent}
|
onClick={onMakeCurrent}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
Make Current
|
Make Current
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdate && onUpdate) {
|
if (versionInfo.hasUpdate && onUpdate) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<Button
|
<Button
|
||||||
key="update"
|
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>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +141,7 @@ export const DownloadCard = ({
|
||||||
withBorder
|
withBorder
|
||||||
radius="sm"
|
radius="sm"
|
||||||
padding="sm"
|
padding="sm"
|
||||||
{...(isCurrent && {
|
{...(versionInfo.isCurrent && {
|
||||||
bg: colorScheme === 'dark' ? 'dark.6' : 'gray.0',
|
bg: colorScheme === 'dark' ? 'dark.6' : 'gray.0',
|
||||||
bd: `2px solid var(--mantine-color-${colorScheme === 'dark' ? 'blue-4' : 'blue-6'})`,
|
bd: `2px solid var(--mantine-color-${colorScheme === 'dark' ? 'blue-4' : 'blue-6'})`,
|
||||||
})}
|
})}
|
||||||
|
|
@ -126,19 +150,19 @@ export const DownloadCard = ({
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<Group gap="xs" align="center" mb="xs">
|
<Group gap="xs" align="center" mb="xs">
|
||||||
<Text fw={500} size="sm">
|
<Text fw={500} size="sm">
|
||||||
{pretifyBinName(name)}
|
{pretifyBinName(versionInfo.name)}
|
||||||
</Text>
|
</Text>
|
||||||
{isCurrent && (
|
{versionInfo.isCurrent && (
|
||||||
<Badge variant="light" color="blue" size="sm">
|
<Badge variant="light" color="blue" size="sm">
|
||||||
Current
|
Current
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{hasUpdate && (
|
{versionInfo.hasUpdate && (
|
||||||
<Badge variant="light" color="orange" size="sm">
|
<Badge variant="light" color="orange" size="sm">
|
||||||
Update Available
|
Update Available
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{isWindowsROCmBuild(name) && (
|
{isWindowsROCmBuild(versionInfo.name) && (
|
||||||
<Badge variant="light" color="yellow" size="sm">
|
<Badge variant="light" color="yellow" size="sm">
|
||||||
Experimental
|
Experimental
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
@ -150,9 +174,15 @@ export const DownloadCard = ({
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
{version && (
|
{versionInfo.version && (
|
||||||
<Text size="xs" c="dimmed">
|
<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>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{size && (
|
{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}
|
ref={isDownloading ? downloadingItemRef : null}
|
||||||
>
|
>
|
||||||
<DownloadCard
|
<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(
|
size={formatDownloadSize(
|
||||||
download.size,
|
download.size,
|
||||||
download.url
|
download.url
|
||||||
)}
|
)}
|
||||||
version={download.version}
|
|
||||||
description={getAssetDescription(download.name)}
|
description={getAssetDescription(download.name)}
|
||||||
isDownloading={isDownloading}
|
isDownloading={isDownloading}
|
||||||
downloadProgress={
|
downloadProgress={
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
createSoftwareItems,
|
||||||
|
createDriverItems,
|
||||||
|
createHardwareItems,
|
||||||
|
type HardwareInfo,
|
||||||
|
} from '@/utils/systemInfo';
|
||||||
import {
|
import {
|
||||||
Text,
|
Text,
|
||||||
Stack,
|
Stack,
|
||||||
|
|
@ -9,19 +15,22 @@ import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
rem,
|
rem,
|
||||||
ActionIcon,
|
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Github, FolderOpen, Copy, FileText } from 'lucide-react';
|
import { Github, FolderOpen, FileText } from 'lucide-react';
|
||||||
import { safeExecute } from '@/utils/logger';
|
|
||||||
import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
|
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 { PRODUCT_NAME, GITHUB_API } from '@/constants';
|
||||||
|
import { InfoCard } from '@/components/InfoCard';
|
||||||
|
|
||||||
import icon from '/icon.png';
|
import icon from '/icon.png';
|
||||||
|
|
||||||
export const AboutTab = () => {
|
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();
|
const { handleLogoClick, getLogoStyles } = useLogoClickSounds();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadVersionInfo = async () => {
|
const loadVersionInfo = async () => {
|
||||||
const info = await window.electronAPI.app.getVersionInfo();
|
const info = await window.electronAPI.app.getVersionInfo();
|
||||||
|
|
@ -29,7 +38,35 @@ export const AboutTab = () => {
|
||||||
setVersionInfo(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();
|
loadVersionInfo();
|
||||||
|
loadHardwareInfo();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!versionInfo) {
|
if (!versionInfo) {
|
||||||
|
|
@ -40,47 +77,9 @@ export const AboutTab = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionItems = [
|
const softwareItems = createSoftwareItems(versionInfo);
|
||||||
{
|
const driverItems = hardwareInfo ? createDriverItems(hardwareInfo) : [];
|
||||||
label: PRODUCT_NAME,
|
const hardwareItems = hardwareInfo ? createHardwareItems(hardwareInfo) : [];
|
||||||
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 actionButtons = [
|
const actionButtons = [
|
||||||
{
|
{
|
||||||
|
|
@ -158,48 +157,15 @@ export const AboutTab = () => {
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card withBorder radius="md" p="xs" style={{ position: 'relative' }}>
|
<InfoCard title="Software" items={softwareItems} />
|
||||||
<Tooltip label="Copy Info">
|
|
||||||
<ActionIcon
|
<InfoCard title="Drivers" items={driverItems} loading={!hardwareInfo} />
|
||||||
variant="subtle"
|
|
||||||
size="sm"
|
<InfoCard
|
||||||
onClick={copyVersionInfo}
|
title="Hardware"
|
||||||
aria-label="Copy Version Info"
|
items={hardwareItems}
|
||||||
style={{
|
loading={!hardwareInfo}
|
||||||
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>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -20,18 +20,7 @@ import { formatDownloadSize } from '@/utils/format';
|
||||||
|
|
||||||
import { useKoboldVersionsStore } from '@/stores/koboldVersions';
|
import { useKoboldVersionsStore } from '@/stores/koboldVersions';
|
||||||
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron';
|
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron';
|
||||||
|
import type { VersionInfo } from '@/types';
|
||||||
interface VersionInfo {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
size?: number;
|
|
||||||
isInstalled: boolean;
|
|
||||||
isCurrent: boolean;
|
|
||||||
downloadUrl?: string;
|
|
||||||
installedPath?: string;
|
|
||||||
hasUpdate?: boolean;
|
|
||||||
newerVersion?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VersionsTab = () => {
|
export const VersionsTab = () => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -119,6 +108,7 @@ export const VersionsTab = () => {
|
||||||
installedPath: installedVersion.path,
|
installedPath: installedVersion.path,
|
||||||
hasUpdate,
|
hasUpdate,
|
||||||
newerVersion: hasUpdate ? download.version : undefined,
|
newerVersion: hasUpdate ? download.version : undefined,
|
||||||
|
actualVersion: installedVersion.actualVersion,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
versions.push({
|
versions.push({
|
||||||
|
|
@ -146,6 +136,7 @@ export const VersionsTab = () => {
|
||||||
isInstalled: true,
|
isInstalled: true,
|
||||||
isCurrent,
|
isCurrent,
|
||||||
installedPath: installed.path,
|
installedPath: installed.path,
|
||||||
|
actualVersion: installed.actualVersion,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -194,16 +185,31 @@ export const VersionsTab = () => {
|
||||||
await loadInstalledVersions();
|
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;
|
if (!version.installedPath) return;
|
||||||
|
|
||||||
const success = await window.electronAPI.kobold.setCurrentVersion(
|
const targetInstalledVersion = installedVersions.find(
|
||||||
version.installedPath
|
(v) => v.path === version.installedPath
|
||||||
);
|
);
|
||||||
|
if (targetInstalledVersion) {
|
||||||
if (success) {
|
setCurrentVersion(targetInstalledVersion);
|
||||||
await loadInstalledVersions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.electronAPI.kobold.setCurrentVersion(version.installedPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loadingInstalled || loadingPlatform || loadingRemote) {
|
if (loadingInstalled || loadingPlatform || loadingRemote) {
|
||||||
|
|
@ -257,21 +263,16 @@ export const VersionsTab = () => {
|
||||||
ref={isDownloading ? downloadingItemRef : null}
|
ref={isDownloading ? downloadingItemRef : null}
|
||||||
>
|
>
|
||||||
<DownloadCard
|
<DownloadCard
|
||||||
name={version.name}
|
version={version}
|
||||||
size={
|
size={
|
||||||
version.size
|
version.size
|
||||||
? formatDownloadSize(version.size, version.downloadUrl)
|
? formatDownloadSize(version.size, version.downloadUrl)
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
version={version.version}
|
|
||||||
description={getAssetDescription(version.name)}
|
description={getAssetDescription(version.name)}
|
||||||
isCurrent={version.isCurrent}
|
|
||||||
isInstalled={version.isInstalled}
|
|
||||||
isDownloading={isDownloading}
|
isDownloading={isDownloading}
|
||||||
downloadProgress={downloadProgress[version.name]}
|
downloadProgress={downloadProgress[version.name]}
|
||||||
disabled={downloading !== null}
|
disabled={downloading !== null}
|
||||||
hasUpdate={version.hasUpdate}
|
|
||||||
newerVersion={version.newerVersion}
|
|
||||||
onDownload={(e) => {
|
onDownload={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDownload(version);
|
handleDownload(version);
|
||||||
|
|
@ -280,6 +281,10 @@ export const VersionsTab = () => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleUpdate(version);
|
handleUpdate(version);
|
||||||
}}
|
}}
|
||||||
|
onRedownload={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRedownload(version);
|
||||||
|
}}
|
||||||
onMakeCurrent={() => makeCurrent(version)}
|
onMakeCurrent={() => makeCurrent(version)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ const checkVramWarnings = async (backend: string): Promise<Warning[]> => {
|
||||||
|
|
||||||
if (lowVramGpus.length > 0) {
|
if (lowVramGpus.length > 0) {
|
||||||
const memoryDetails = lowVramGpus
|
const memoryDetails = lowVramGpus
|
||||||
.map((gpu) => `${gpu.deviceName}: ${gpu.totalMemoryGB!.toFixed(1)}GB`)
|
.map((gpu) => `${gpu.totalMemoryGB!.toFixed(1)}GB`)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
warnings.push({
|
warnings.push({
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ import {
|
||||||
detectGPUCapabilities,
|
detectGPUCapabilities,
|
||||||
detectGPUMemory,
|
detectGPUMemory,
|
||||||
detectROCm,
|
detectROCm,
|
||||||
|
detectSystemMemory,
|
||||||
} from '@/main/modules/hardware';
|
} from '@/main/modules/hardware';
|
||||||
import {
|
import {
|
||||||
detectBackendSupport,
|
detectBackendSupport,
|
||||||
|
|
@ -121,6 +122,8 @@ export function setupIPCHandlers() {
|
||||||
|
|
||||||
ipcMain.handle('kobold:detectGPUMemory', () => detectGPUMemory());
|
ipcMain.handle('kobold:detectGPUMemory', () => detectGPUMemory());
|
||||||
|
|
||||||
|
ipcMain.handle('kobold:detectSystemMemory', () => detectSystemMemory());
|
||||||
|
|
||||||
ipcMain.handle('kobold:detectROCm', () => detectROCm());
|
ipcMain.handle('kobold:detectROCm', () => detectROCm());
|
||||||
|
|
||||||
ipcMain.handle('kobold:detectBackendSupport', () => detectBackendSupport());
|
ipcMain.handle('kobold:detectBackendSupport', () => detectBackendSupport());
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,14 @@ import type { ChildProcess } from 'child_process';
|
||||||
import yauzl from 'yauzl';
|
import yauzl from 'yauzl';
|
||||||
import { createWriteStream } from 'fs';
|
import { createWriteStream } from 'fs';
|
||||||
|
|
||||||
import { logError, safeExecute } from '@/utils/node/logging';
|
import { logError, safeExecute, tryExecute } from '@/utils/node/logging';
|
||||||
import { sendKoboldOutput } from './window';
|
import { sendKoboldOutput } from './window';
|
||||||
import { getInstallDir } from './config';
|
import { getInstallDir } from './config';
|
||||||
import { COMFYUI, SERVER_READY_SIGNALS, GITHUB_API } from '@/constants';
|
import { COMFYUI, SERVER_READY_SIGNALS, GITHUB_API } from '@/constants';
|
||||||
import { terminateProcess } from '@/utils/node/process';
|
import { terminateProcess } from '@/utils/node/process';
|
||||||
import { parseKoboldConfig } from '@/utils/node/kobold';
|
import { parseKoboldConfig } from '@/utils/node/kobold';
|
||||||
import { getAppVersion, ensureDir } from '@/utils/node/fs';
|
import { getAppVersion, ensureDir } from '@/utils/node/fs';
|
||||||
import { getGPUData } from '@/utils/node/gpu';
|
import { detectGPU } from './hardware';
|
||||||
import { getUvEnvironment } from './dependencies';
|
import { getUvEnvironment } from './dependencies';
|
||||||
|
|
||||||
interface ComfyUIVersionInfo {
|
interface ComfyUIVersionInfo {
|
||||||
|
|
@ -61,15 +61,10 @@ async function saveComfyUIVersion(
|
||||||
workspaceDir: string,
|
workspaceDir: string,
|
||||||
version: ComfyUIVersionInfo
|
version: ComfyUIVersionInfo
|
||||||
) {
|
) {
|
||||||
try {
|
await tryExecute(async () => {
|
||||||
const versionFile = join(workspaceDir, '.version.json');
|
const versionFile = join(workspaceDir, '.version.json');
|
||||||
await writeFile(versionFile, JSON.stringify(version, null, 2));
|
await writeFile(versionFile, JSON.stringify(version, null, 2));
|
||||||
} catch (error) {
|
}, 'Failed to save ComfyUI version info');
|
||||||
logError(
|
|
||||||
'Failed to save ComfyUI version info',
|
|
||||||
error instanceof Error ? error : undefined
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function shouldUpdateComfyUI(workspaceDir: string) {
|
async function shouldUpdateComfyUI(workspaceDir: string) {
|
||||||
|
|
@ -102,9 +97,8 @@ on('SIGTERM', () => {
|
||||||
|
|
||||||
async function shouldForceCPUMode() {
|
async function shouldForceCPUMode() {
|
||||||
try {
|
try {
|
||||||
const gpus = await getGPUData();
|
const gpuInfo = await detectGPU();
|
||||||
const hasAMD = gpus.some((gpu) => gpu.deviceName.includes('AMD'));
|
const { hasAMD, hasNVIDIA } = gpuInfo;
|
||||||
const hasNVIDIA = gpus.some((gpu) => gpu.deviceName.includes('NVIDIA'));
|
|
||||||
const isWindows = platform === 'win32';
|
const isWindows = platform === 'win32';
|
||||||
|
|
||||||
return (hasAMD && isWindows) || (!hasAMD && !hasNVIDIA);
|
return (hasAMD && isWindows) || (!hasAMD && !hasNVIDIA);
|
||||||
|
|
@ -125,9 +119,8 @@ async function getPyTorchInstallArgs(pythonPath: string) {
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const gpus = await getGPUData();
|
const gpuInfo = await detectGPU();
|
||||||
const hasAMD = gpus.some((gpu) => gpu.deviceName.includes('AMD'));
|
const { hasAMD, hasNVIDIA } = gpuInfo;
|
||||||
const hasNVIDIA = gpus.some((gpu) => gpu.deviceName.includes('NVIDIA'));
|
|
||||||
const isWindows = platform === 'win32';
|
const isWindows = platform === 'win32';
|
||||||
|
|
||||||
if (hasAMD && !isWindows) {
|
if (hasAMD && !isWindows) {
|
||||||
|
|
@ -597,10 +590,7 @@ export async function startFrontend(args: string[]) {
|
||||||
|
|
||||||
await waitForComfyUIToStart();
|
await waitForComfyUIToStart();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(
|
logError('Failed to start ComfyUI', error as Error);
|
||||||
'Failed to start ComfyUI',
|
|
||||||
error instanceof Error ? error : undefined
|
|
||||||
);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,9 @@ export async function detectCPU() {
|
||||||
|
|
||||||
const devices: string[] = [];
|
const devices: string[] = [];
|
||||||
if (cpu.brand) {
|
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');
|
const avx = flags.includes('avx') || flags.includes('AVX');
|
||||||
|
|
@ -59,35 +61,28 @@ export async function detectGPU() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await safeExecute(async () => {
|
const result = await safeExecute(async () => {
|
||||||
const gpuData = await getGPUData();
|
const graphics = await si.graphics();
|
||||||
|
|
||||||
let hasAMD = false;
|
let hasAMD = false;
|
||||||
let hasNVIDIA = false;
|
let hasNVIDIA = false;
|
||||||
const gpuInfo: string[] = [];
|
const gpuInfo: string[] = [];
|
||||||
|
|
||||||
for (const gpu of gpuData) {
|
for (const controller of graphics.controllers) {
|
||||||
if (gpu.deviceName) {
|
// Check vendor for AMD
|
||||||
gpuInfo.push(formatDeviceName(gpu.deviceName));
|
|
||||||
}
|
|
||||||
|
|
||||||
const deviceName = gpu.deviceName?.toLowerCase() || '';
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
deviceName.includes('amd') ||
|
controller.vendor?.toLowerCase().includes('amd') ||
|
||||||
deviceName.includes('ati') ||
|
controller.vendor?.toLowerCase().includes('ati')
|
||||||
deviceName.includes('radeon')
|
|
||||||
) {
|
) {
|
||||||
hasAMD = true;
|
hasAMD = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (controller.vendor?.toLowerCase().includes('nvidia')) {
|
||||||
deviceName.includes('nvidia') ||
|
|
||||||
deviceName.includes('geforce') ||
|
|
||||||
deviceName.includes('gtx') ||
|
|
||||||
deviceName.includes('rtx')
|
|
||||||
) {
|
|
||||||
hasNVIDIA = true;
|
hasNVIDIA = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (controller.model) {
|
||||||
|
gpuInfo.push(controller.model);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const basicInfo = {
|
const basicInfo = {
|
||||||
|
|
@ -95,7 +90,6 @@ export async function detectGPU() {
|
||||||
hasNVIDIA,
|
hasNVIDIA,
|
||||||
gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
|
gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
|
||||||
};
|
};
|
||||||
|
|
||||||
basicGPUInfoCache = basicInfo;
|
basicGPUInfoCache = basicInfo;
|
||||||
return basicInfo;
|
return basicInfo;
|
||||||
}, 'GPU detection failed');
|
}, 'GPU detection failed');
|
||||||
|
|
@ -133,7 +127,7 @@ async function detectCUDA() {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execa(
|
const { stdout } = await execa(
|
||||||
'nvidia-smi',
|
'nvidia-smi',
|
||||||
['--query-gpu=name,memory.total,memory.free', '--format=csv,noheader'],
|
['--query-gpu=name,driver_version', '--format=csv,noheader,nounits'],
|
||||||
{
|
{
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
reject: false,
|
reject: false,
|
||||||
|
|
@ -141,19 +135,41 @@ async function detectCUDA() {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (stdout.trim()) {
|
if (stdout.trim()) {
|
||||||
const devices = stdout
|
// Check for error messages that indicate nvidia-smi failed
|
||||||
.trim()
|
const errorPatterns = [
|
||||||
.split('\n')
|
'NVIDIA-SMI has failed',
|
||||||
.map((line) => {
|
'No devices found',
|
||||||
const parts = line.split(',');
|
'Unable to determine the device handle',
|
||||||
const rawName = parts[0]?.trim() || 'Unknown NVIDIA GPU';
|
"couldn't communicate with the NVIDIA driver",
|
||||||
return formatDeviceName(rawName);
|
'No NVIDIA GPU found',
|
||||||
})
|
];
|
||||||
.filter(Boolean);
|
|
||||||
|
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 {
|
return {
|
||||||
supported: devices.length > 0,
|
supported: devices.length > 0,
|
||||||
devices,
|
devices,
|
||||||
|
version,
|
||||||
} as const;
|
} 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 {
|
return {
|
||||||
supported: devices.length > 0,
|
supported: devices.length > 0,
|
||||||
devices,
|
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 {
|
return {
|
||||||
supported: devices.length > 0,
|
supported: devices.length > 0,
|
||||||
devices,
|
devices,
|
||||||
|
version,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { supported: false, devices: [] };
|
return { supported: false, devices: [], version: 'Unknown' };
|
||||||
} catch {
|
} catch {
|
||||||
return { supported: false, devices: [] };
|
return { supported: false, devices: [], version: 'Unknown' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -336,15 +385,34 @@ async function detectCLBlast() {
|
||||||
|
|
||||||
if (stdout.trim()) {
|
if (stdout.trim()) {
|
||||||
const devices = parseClInfoOutput(stdout);
|
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 {
|
return {
|
||||||
supported: devices.length > 0,
|
supported: devices.length > 0,
|
||||||
devices,
|
devices,
|
||||||
|
version,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { supported: false, devices: [] };
|
return { supported: false, devices: [], version: 'Unknown' };
|
||||||
} catch {
|
} catch {
|
||||||
return { supported: false, devices: [] };
|
return { supported: false, devices: [], version: 'Unknown' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -358,18 +426,15 @@ export async function detectGPUMemory() {
|
||||||
const memoryInfo: GPUMemoryInfo[] = [];
|
const memoryInfo: GPUMemoryInfo[] = [];
|
||||||
|
|
||||||
for (const gpu of gpuData) {
|
for (const gpu of gpuData) {
|
||||||
if (gpu.deviceName) {
|
let vram: number | null = gpu.memoryTotal;
|
||||||
let vram: number | null = gpu.memoryTotal;
|
|
||||||
|
|
||||||
if (!vram || vram <= 1) {
|
if (!vram || vram <= 1) {
|
||||||
vram = null;
|
vram = null;
|
||||||
}
|
|
||||||
|
|
||||||
memoryInfo.push({
|
|
||||||
deviceName: formatDeviceName(gpu.deviceName),
|
|
||||||
totalMemoryGB: vram,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
memoryInfo.push({
|
||||||
|
totalMemoryGB: vram,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return memoryInfo;
|
return memoryInfo;
|
||||||
|
|
@ -378,3 +443,44 @@ export async function detectGPUMemory() {
|
||||||
gpuMemoryInfoCache = result || [];
|
gpuMemoryInfoCache = result || [];
|
||||||
return gpuMemoryInfoCache;
|
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) => {
|
const versionPromises = launchers.map(async (launcher) => {
|
||||||
try {
|
try {
|
||||||
const detectedVersion = await getVersionFromBinary(launcher.path);
|
const versionInfo = await getVersionFromBinary(launcher.path);
|
||||||
const version = detectedVersion || 'unknown';
|
|
||||||
|
if (!versionInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version,
|
version: versionInfo.version,
|
||||||
path: launcher.path,
|
path: launcher.path,
|
||||||
filename: launcher.filename,
|
filename: launcher.filename,
|
||||||
size: launcher.size,
|
size: launcher.size,
|
||||||
|
actualVersion: versionInfo.actualVersion,
|
||||||
} as InstalledVersion;
|
} as InstalledVersion;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(
|
logError(
|
||||||
|
|
@ -131,42 +135,56 @@ export async function getVersionFromBinary(launcherPath: string) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let folderVersion: string | null = null;
|
||||||
|
let actualVersion: string | null = null;
|
||||||
|
|
||||||
const folderName = launcherPath.split(/[/\\]/).slice(-2, -1)[0];
|
const folderName = launcherPath.split(/[/\\]/).slice(-2, -1)[0];
|
||||||
if (folderName) {
|
if (folderName) {
|
||||||
const versionMatch = folderName.match(
|
const versionMatch = folderName.match(
|
||||||
/-(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/
|
/-(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/
|
||||||
);
|
);
|
||||||
if (versionMatch) {
|
if (versionMatch) {
|
||||||
return versionMatch[1];
|
folderVersion = versionMatch[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await execa(launcherPath, ['--version'], {
|
try {
|
||||||
timeout: 30000,
|
const result = await execa(launcherPath, ['--version'], {
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
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)) {
|
if (/^\d+\.\d+/.test(allOutput)) {
|
||||||
const versionParts = allOutput.split(/\s+/)[0];
|
const versionParts = allOutput.split(/\s+/)[0];
|
||||||
if (versionParts && /^\d+\.\d+/.test(versionParts)) {
|
if (versionParts && /^\d+\.\d+/.test(versionParts)) {
|
||||||
return versionParts;
|
actualVersion = 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { BrowserWindow } from 'electron';
|
||||||
import { platform } from 'process';
|
import { platform } from 'process';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { getGPUData } from '@/utils/node/gpu';
|
import { getGPUData } from '@/utils/node/gpu';
|
||||||
|
import { detectGPU } from './hardware';
|
||||||
import { tryExecute, safeExecute } from '@/utils/node/logging';
|
import { tryExecute, safeExecute } from '@/utils/node/logging';
|
||||||
|
|
||||||
export interface CpuMetrics {
|
export interface CpuMetrics {
|
||||||
|
|
@ -134,18 +135,18 @@ async function collectAndSendMemoryMetrics() {
|
||||||
|
|
||||||
async function collectAndSendGpuMetrics() {
|
async function collectAndSendGpuMetrics() {
|
||||||
await tryExecute(async () => {
|
await tryExecute(async () => {
|
||||||
const gpuData = await getGPUData();
|
const [gpuData, gpuInfo] = await Promise.all([getGPUData(), detectGPU()]);
|
||||||
const metrics: GpuMetrics = {
|
const metrics: GpuMetrics = {
|
||||||
gpus: gpuData.map((gpuInfo) => ({
|
gpus: gpuData.map((gpuData, index) => ({
|
||||||
name: gpuInfo.deviceName,
|
name: gpuInfo.gpuInfo[index] || `GPU ${index}`,
|
||||||
usage: Math.round(gpuInfo.usage),
|
usage: Math.round(gpuData.usage),
|
||||||
memoryUsed: gpuInfo.memoryUsed,
|
memoryUsed: gpuData.memoryUsed,
|
||||||
memoryTotal: gpuInfo.memoryTotal,
|
memoryTotal: gpuData.memoryTotal,
|
||||||
memoryUsage:
|
memoryUsage:
|
||||||
gpuInfo.memoryTotal > 0
|
gpuData.memoryTotal > 0
|
||||||
? Math.round((gpuInfo.memoryUsed / gpuInfo.memoryTotal) * 100)
|
? Math.round((gpuData.memoryUsed / gpuData.memoryTotal) * 100)
|
||||||
: 0,
|
: 0,
|
||||||
temperature: gpuInfo.temperature,
|
temperature: gpuData.temperature,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,7 @@ import { getUvEnvironment } from './dependencies';
|
||||||
|
|
||||||
let openWebUIProcess: ChildProcess | null = null;
|
let openWebUIProcess: ChildProcess | null = null;
|
||||||
|
|
||||||
const OPENWEBUI_BASE_ARGS = [
|
const OPENWEBUI_BASE_ARGS = ['--python', '3.11', 'open-webui@latest', 'serve'];
|
||||||
'--python',
|
|
||||||
'3.11',
|
|
||||||
'--with',
|
|
||||||
'itsdangerous',
|
|
||||||
'open-webui@latest',
|
|
||||||
'serve',
|
|
||||||
];
|
|
||||||
|
|
||||||
on('SIGINT', () => {
|
on('SIGINT', () => {
|
||||||
void stopFrontend();
|
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: () =>
|
detectGPUCapabilities: () =>
|
||||||
ipcRenderer.invoke('kobold:detectGPUCapabilities'),
|
ipcRenderer.invoke('kobold:detectGPUCapabilities'),
|
||||||
detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'),
|
detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'),
|
||||||
|
detectSystemMemory: () => ipcRenderer.invoke('kobold:detectSystemMemory'),
|
||||||
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
|
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
|
||||||
detectBackendSupport: () => ipcRenderer.invoke('kobold:detectBackendSupport'),
|
detectBackendSupport: () => ipcRenderer.invoke('kobold:detectBackendSupport'),
|
||||||
getAvailableBackends: (includeDisabled) =>
|
getAvailableBackends: (includeDisabled) =>
|
||||||
|
|
|
||||||
7
src/types/electron.d.ts
vendored
7
src/types/electron.d.ts
vendored
|
|
@ -3,6 +3,7 @@ import type {
|
||||||
GPUCapabilities,
|
GPUCapabilities,
|
||||||
BasicGPUInfo,
|
BasicGPUInfo,
|
||||||
GPUMemoryInfo,
|
GPUMemoryInfo,
|
||||||
|
SystemMemoryInfo,
|
||||||
} from '@/types/hardware';
|
} from '@/types/hardware';
|
||||||
import type { BackendOption, BackendSupport } from '@/types';
|
import type { BackendOption, BackendSupport } from '@/types';
|
||||||
import type { MantineColorScheme } from '@mantine/core';
|
import type { MantineColorScheme } from '@mantine/core';
|
||||||
|
|
@ -55,6 +56,7 @@ export interface InstalledVersion {
|
||||||
path: string;
|
path: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
|
actualVersion?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadItem {
|
export interface DownloadItem {
|
||||||
|
|
@ -112,6 +114,7 @@ export interface KoboldAPI {
|
||||||
detectCPU: () => Promise<CPUCapabilities>;
|
detectCPU: () => Promise<CPUCapabilities>;
|
||||||
detectGPUCapabilities: () => Promise<GPUCapabilities>;
|
detectGPUCapabilities: () => Promise<GPUCapabilities>;
|
||||||
detectGPUMemory: () => Promise<GPUMemoryInfo[]>;
|
detectGPUMemory: () => Promise<GPUMemoryInfo[]>;
|
||||||
|
detectSystemMemory: () => Promise<SystemMemoryInfo>;
|
||||||
detectROCm: () => Promise<{ supported: boolean; devices: string[] }>;
|
detectROCm: () => Promise<{ supported: boolean; devices: string[] }>;
|
||||||
detectBackendSupport: () => Promise<BackendSupport | null>;
|
detectBackendSupport: () => Promise<BackendSupport | null>;
|
||||||
getAvailableBackends: (includeDisabled?: boolean) => Promise<BackendOption[]>;
|
getAvailableBackends: (includeDisabled?: boolean) => Promise<BackendOption[]>;
|
||||||
|
|
@ -141,7 +144,7 @@ export interface KoboldAPI {
|
||||||
onKoboldOutput: (callback: (data: string) => void) => () => void;
|
onKoboldOutput: (callback: (data: string) => void) => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VersionInfo {
|
export interface SystemVersionInfo {
|
||||||
appVersion: string;
|
appVersion: string;
|
||||||
electronVersion: string;
|
electronVersion: string;
|
||||||
nodeVersion: string;
|
nodeVersion: string;
|
||||||
|
|
@ -160,7 +163,7 @@ export interface AppAPI {
|
||||||
viewConfigFile: () => Promise<void>;
|
viewConfigFile: () => Promise<void>;
|
||||||
openPath: (path: string) => Promise<void>;
|
openPath: (path: string) => Promise<void>;
|
||||||
getVersion: () => Promise<string>;
|
getVersion: () => Promise<string>;
|
||||||
getVersionInfo: () => Promise<VersionInfo>;
|
getVersionInfo: () => Promise<SystemVersionInfo>;
|
||||||
minimizeWindow: () => void;
|
minimizeWindow: () => void;
|
||||||
maximizeWindow: () => void;
|
maximizeWindow: () => void;
|
||||||
closeWindow: () => 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 {
|
export interface GPUMemoryInfo {
|
||||||
deviceName: string;
|
|
||||||
totalMemoryGB: number | null;
|
totalMemoryGB: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SystemMemoryInfo {
|
||||||
|
totalGB: number;
|
||||||
|
speed?: number;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GPUCapabilities {
|
export interface GPUCapabilities {
|
||||||
cuda: {
|
cuda: {
|
||||||
readonly supported: boolean;
|
readonly supported: boolean;
|
||||||
readonly devices: readonly string[];
|
readonly devices: readonly string[];
|
||||||
|
readonly version?: string;
|
||||||
};
|
};
|
||||||
rocm: {
|
rocm: {
|
||||||
readonly supported: boolean;
|
readonly supported: boolean;
|
||||||
readonly devices: readonly string[];
|
readonly devices: readonly string[];
|
||||||
|
readonly version?: string;
|
||||||
};
|
};
|
||||||
vulkan: {
|
vulkan: {
|
||||||
readonly supported: boolean;
|
readonly supported: boolean;
|
||||||
readonly devices: readonly string[];
|
readonly devices: readonly string[];
|
||||||
|
readonly version?: string;
|
||||||
};
|
};
|
||||||
clblast: {
|
clblast: {
|
||||||
readonly supported: boolean;
|
readonly supported: boolean;
|
||||||
readonly devices: readonly string[];
|
readonly devices: readonly string[];
|
||||||
|
readonly version?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,9 +52,6 @@ export interface HardwareInfo {
|
||||||
cpu: CPUCapabilities;
|
cpu: CPUCapabilities;
|
||||||
gpu: BasicGPUInfo;
|
gpu: BasicGPUInfo;
|
||||||
gpuCapabilities?: GPUCapabilities;
|
gpuCapabilities?: GPUCapabilities;
|
||||||
}
|
gpuMemory?: GPUMemoryInfo[];
|
||||||
|
systemMemory?: SystemMemoryInfo;
|
||||||
export interface SystemCapabilities {
|
|
||||||
hardware: HardwareInfo;
|
|
||||||
platform: string;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
src/types/index.d.ts
vendored
14
src/types/index.d.ts
vendored
|
|
@ -45,6 +45,20 @@ export interface InstalledVersion {
|
||||||
path: string;
|
path: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
size?: number;
|
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 {
|
export interface DismissedUpdate {
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,12 @@ import { join } from 'path';
|
||||||
import { platform } from 'process';
|
import { platform } from 'process';
|
||||||
|
|
||||||
interface CachedGPUInfo {
|
interface CachedGPUInfo {
|
||||||
deviceName: string;
|
|
||||||
devicePath: string;
|
devicePath: string;
|
||||||
memoryTotal: number;
|
memoryTotal: number;
|
||||||
hwmonPath?: string;
|
hwmonPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GPUData {
|
interface GPUData {
|
||||||
deviceName: string;
|
|
||||||
usage: number;
|
usage: number;
|
||||||
memoryUsed: number;
|
memoryUsed: number;
|
||||||
memoryTotal: number;
|
memoryTotal: number;
|
||||||
|
|
@ -38,7 +36,6 @@ async function initializeLinuxGPUCache() {
|
||||||
return linuxCachePromise;
|
return linuxCachePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
linuxCachePromise = (async () => {
|
linuxCachePromise = (async () => {
|
||||||
try {
|
try {
|
||||||
const drmPath = '/sys/class/drm';
|
const drmPath = '/sys/class/drm';
|
||||||
|
|
@ -50,42 +47,6 @@ async function initializeLinuxGPUCache() {
|
||||||
const gpus = [];
|
const gpus = [];
|
||||||
for (const card of cardEntries) {
|
for (const card of cardEntries) {
|
||||||
const devicePath = join(drmPath, card, 'device');
|
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 {
|
try {
|
||||||
const memTotalData = await readFile(
|
const memTotalData = await readFile(
|
||||||
`${devicePath}/mem_info_vram_total`,
|
`${devicePath}/mem_info_vram_total`,
|
||||||
|
|
@ -109,7 +70,6 @@ async function initializeLinuxGPUCache() {
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
gpus.push({
|
gpus.push({
|
||||||
deviceName,
|
|
||||||
devicePath,
|
devicePath,
|
||||||
memoryTotal,
|
memoryTotal,
|
||||||
hwmonPath,
|
hwmonPath,
|
||||||
|
|
@ -167,7 +127,6 @@ async function getLinuxGPUData() {
|
||||||
}
|
}
|
||||||
|
|
||||||
gpus.push({
|
gpus.push({
|
||||||
deviceName: cachedGPU.deviceName,
|
|
||||||
usage,
|
usage,
|
||||||
memoryUsed: parseFloat(memoryUsed.toFixed(2)),
|
memoryUsed: parseFloat(memoryUsed.toFixed(2)),
|
||||||
memoryTotal: parseFloat(cachedGPU.memoryTotal.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