diff --git a/src/components/DownloadCard.tsx b/src/components/DownloadCard.tsx index 77a05aa..a3a0d36 100644 --- a/src/components/DownloadCard.tsx +++ b/src/components/DownloadCard.tsx @@ -9,7 +9,7 @@ import { Progress, rem, } from '@mantine/core'; -import { Download } from 'lucide-react'; +import { Download, Trash2 } from 'lucide-react'; import { MouseEvent } from 'react'; import { pretifyBinName, isWindowsROCmBuild } from '@/utils/assets'; import { usePreferencesStore } from '@/stores/preferences'; @@ -19,26 +19,28 @@ interface DownloadCardProps { version: VersionInfo; size: string; description?: string; - isDownloading?: boolean; + isLoading?: boolean; downloadProgress?: number; disabled?: boolean; onDownload: (e: MouseEvent) => void; onMakeCurrent?: () => void; onUpdate?: (e: MouseEvent) => void; onRedownload?: (e: MouseEvent) => void; + onDelete?: (e: MouseEvent) => void; } export const DownloadCard = ({ version: versionInfo, size, description, - isDownloading = false, + isLoading = false, downloadProgress = 0, disabled = false, onDownload, onMakeCurrent, onUpdate, onRedownload, + onDelete, }: DownloadCardProps) => { const { resolvedColorScheme: colorScheme } = usePreferencesStore(); const hasVersionMismatch = Boolean( @@ -46,6 +48,8 @@ export const DownloadCard = ({ versionInfo.actualVersion && versionInfo.version !== versionInfo.actualVersion ); + + // eslint-disable-next-line sonarjs/cognitive-complexity const renderActionButtons = () => { const buttons = []; @@ -56,17 +60,17 @@ export const DownloadCard = ({ variant="filled" size="xs" onClick={onDownload} - loading={isDownloading} + loading={isLoading} disabled={disabled} leftSection={ - isDownloading ? ( + isLoading ? ( ) : ( ) } > - {isDownloading ? 'Downloading...' : 'Download'} + {isLoading ? 'Downloading...' : 'Download'} ); } @@ -92,20 +96,18 @@ export const DownloadCard = ({ variant="filled" size="xs" onClick={onUpdate} - loading={isDownloading} + loading={isLoading} disabled={disabled} color="orange" leftSection={ - isDownloading ? ( + isLoading ? ( ) : ( ) } > - {isDownloading - ? 'Updating...' - : `Update to ${versionInfo.newerVersion}`} + {isLoading ? 'Updating...' : `Update to ${versionInfo.newerVersion}`} ); } @@ -117,18 +119,41 @@ export const DownloadCard = ({ variant="filled" size="xs" onClick={onRedownload} - loading={isDownloading} + loading={isLoading} disabled={disabled} color="red" leftSection={ - isDownloading ? ( + isLoading ? ( ) : ( ) } > - {isDownloading ? 'Re-downloading...' : 'Re-download'} + {isLoading ? 'Re-downloading...' : 'Re-download'} + + ); + } + + if (onDelete && versionInfo.isInstalled && !versionInfo.isCurrent) { + buttons.push( + ); } @@ -201,7 +226,7 @@ export const DownloadCard = ({ {renderActionButtons()} - {isDownloading && downloadProgress !== undefined && ( + {isLoading && downloadProgress !== undefined && ( { download.url )} description={getAssetDescription(download.name)} - isDownloading={isDownloading} + isLoading={isDownloading} downloadProgress={ isDownloading ? downloadProgress[download.name] || 0 diff --git a/src/components/screens/Launch/GeneralTab/BackendSelector.tsx b/src/components/screens/Launch/GeneralTab/BackendSelector.tsx index 726e329..32cb741 100644 --- a/src/components/screens/Launch/GeneralTab/BackendSelector.tsx +++ b/src/components/screens/Launch/GeneralTab/BackendSelector.tsx @@ -26,8 +26,10 @@ export const BackendSelector = () => { useEffect(() => { const loadBackends = async () => { setIsLoadingBackends(true); + const backends = await window.electronAPI.kobold.getAvailableBackends(true); + setAvailableBackends(backends || []); setIsLoadingBackends(false); hasInitialized.current = true; diff --git a/src/components/settings/AboutTab.tsx b/src/components/settings/AboutTab.tsx index ba50779..8505900 100644 --- a/src/components/settings/AboutTab.tsx +++ b/src/components/settings/AboutTab.tsx @@ -3,7 +3,6 @@ import { createSoftwareItems, createDriverItems, createHardwareItems, - type HardwareInfo, } from '@/utils/systemInfo'; import { Text, @@ -18,9 +17,10 @@ import { } from '@mantine/core'; import { Github, FolderOpen, FileText } from 'lucide-react'; import { useLogoClickSounds } from '@/hooks/useLogoClickSounds'; -import type { SystemVersionInfo } from '@/types/electron'; import { PRODUCT_NAME, GITHUB_API } from '@/constants'; import { InfoCard } from '@/components/InfoCard'; +import type { HardwareInfo } from '@/types/hardware'; +import type { SystemVersionInfo } from '@/types/electron'; import icon from '/icon.png'; diff --git a/src/components/settings/VersionsTab.tsx b/src/components/settings/VersionsTab.tsx index 1c1c7ec..cd97f40 100644 --- a/src/components/settings/VersionsTab.tsx +++ b/src/components/settings/VersionsTab.tsx @@ -199,6 +199,17 @@ export const VersionsTab = () => { await loadInstalledVersions(); }; + const handleDelete = async (version: VersionInfo) => { + if (!version.installedPath || version.isCurrent) return; + + const result = await window.electronAPI.kobold.deleteRelease( + version.installedPath + ); + if (result.success) { + await loadInstalledVersions(); + } + }; + const makeCurrent = (version: VersionInfo) => { if (!version.installedPath) return; @@ -270,7 +281,7 @@ export const VersionsTab = () => { : '' } description={getAssetDescription(version.name)} - isDownloading={isDownloading} + isLoading={isDownloading} downloadProgress={downloadProgress[version.name]} disabled={downloading !== null} onDownload={(e) => { @@ -285,6 +296,10 @@ export const VersionsTab = () => { e.stopPropagation(); handleRedownload(version); }} + onDelete={(e) => { + e.stopPropagation(); + handleDelete(version); + }} onMakeCurrent={() => makeCurrent(version)} /> diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 01f285b..5c1e867 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -11,6 +11,7 @@ import { getInstalledVersions, getCurrentVersion, setCurrentVersion, + deleteRelease, } from '@/main/modules/koboldcpp/version'; import { getConfigFiles, @@ -138,6 +139,10 @@ export function setupIPCHandlers() { launchKoboldCppWithCustomFrontends(args) ); + ipcMain.handle('kobold:deleteRelease', (_, binaryPath) => + deleteRelease(binaryPath) + ); + ipcMain.handle('kobold:stopKoboldCpp', () => { stopKoboldCpp(); stopSillyTavernFrontend(); diff --git a/src/main/modules/hardware.ts b/src/main/modules/hardware.ts index f4f3eef..ae117c2 100644 --- a/src/main/modules/hardware.ts +++ b/src/main/modules/hardware.ts @@ -110,7 +110,7 @@ async function detectVulkan() { const isIntegrated = gpu.isIntegrated; devices.push({ - name: isIntegrated ? gpu.deviceName : formatDeviceName(gpu.deviceName), + name: isIntegrated ? gpu.name : formatDeviceName(gpu.name), isIntegrated, }); } @@ -192,155 +192,30 @@ async function detectCUDA() { export async function detectROCm() { try { const rocminfoCommand = platform === 'win32' ? 'hipInfo' : 'rocminfo'; - const { stdout } = await execa(rocminfoCommand, [], COMMON_EXEC_OPTIONS); + const [rocminfoResult, vulkanInfo, hipccVersion] = await Promise.all([ + execa(rocminfoCommand, [], COMMON_EXEC_OPTIONS), + getVulkanInfo(), + execa('hipcc', ['--version'], COMMON_EXEC_OPTIONS), + ]); + const { stdout } = rocminfoResult; + const { stdout: hipccOutput } = hipccVersion; + let version: string | undefined; + let driverVersion: string | undefined; + + try { + if (hipccOutput.trim()) { + const hipVersionMatch = hipccOutput.match( + /HIP version:\s*(\d+\.\d+(?:\.\d+)?)/i + ); + + if (hipVersionMatch) { + version = hipVersionMatch[1]; + } + } + } catch {} if (stdout.trim()) { - const devices: GPUDevice[] = []; - - if (platform === 'win32') { - const lines = stdout.split('\n'); - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - if (line.includes('Marketing Name:')) { - const name = line.split('Marketing Name:')[1]?.trim(); - if (name && !name.toLowerCase().includes('cpu')) { - let deviceType = ''; - - const searchRangeLines = 20; - const searchStartIndex = Math.max(0, i - searchRangeLines); - const searchEndIndex = Math.min( - lines.length, - i + searchRangeLines - ); - - for ( - let searchIndex = searchStartIndex; - searchIndex < searchEndIndex; - searchIndex++ - ) { - if (lines[searchIndex].includes('Device Type:')) { - deviceType = - lines[searchIndex].split('Device Type:')[1]?.trim() || ''; - break; - } - } - - if (deviceType !== 'CPU') { - let isIntegrated = true; - try { - const vulkanInfo = await getVulkanInfo(); - const matchingGPU = vulkanInfo.allGPUs.find( - (gpu) => - gpu.deviceName.includes(name) || - name.includes(gpu.deviceName) - ); - isIntegrated = matchingGPU ? matchingGPU.isIntegrated : false; - } catch { - isIntegrated = false; - } - - devices.push({ - name: isIntegrated ? name : formatDeviceName(name), - isIntegrated, - }); - } - } - } - } - } else { - const lines = stdout.split('\n'); - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - if (line.includes('Marketing Name:')) { - const name = line.split('Marketing Name:')[1]?.trim(); - if (name) { - let deviceType = ''; - - const searchRangeLines = 20; - const searchStartIndex = Math.max(0, i - searchRangeLines); - const searchEndIndex = Math.min( - lines.length, - i + searchRangeLines - ); - - for ( - let searchIndex = searchStartIndex; - searchIndex < searchEndIndex; - searchIndex++ - ) { - if (lines[searchIndex].includes('Device Type:')) { - deviceType = - lines[searchIndex].split('Device Type:')[1]?.trim() || ''; - break; - } - } - - if (deviceType !== 'CPU') { - // Check if integrated by cross-referencing with vulkan GPU list - let isIntegrated = true; - try { - const vulkanInfo = await getVulkanInfo(); - const matchingGPU = vulkanInfo.allGPUs.find( - (gpu) => - gpu.deviceName.includes(name) || - name.includes(gpu.deviceName) - ); - isIntegrated = matchingGPU ? matchingGPU.isIntegrated : false; - } catch { - isIntegrated = false; - } - - devices.push({ - name: isIntegrated ? name : formatDeviceName(name), - isIntegrated, - }); - } - } - } - } - } - - let version: string | undefined; - - if (platform === 'linux' || platform === 'darwin') { - try { - const { stdout: amdSmiOutput } = await execa( - 'amd-smi', - COMMON_EXEC_OPTIONS - ); - - 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 {} - } else { - try { - const { stdout: hipccOutput } = await execa( - 'hipcc', - ['--version'], - COMMON_EXEC_OPTIONS - ); - - if (hipccOutput.trim()) { - const hipVersionMatch = hipccOutput.match( - /HIP version:\s*(\d+\.\d+(?:\.\d+)?)/i - ); - if (hipVersionMatch) { - version = hipVersionMatch[1]; - } - } - } catch {} - } - - let driverVersion: string | undefined; + const devices = parseRocmOutput(stdout, vulkanInfo); if (platform === 'win32') { try { @@ -357,10 +232,8 @@ export async function detectROCm() { driverVersion = driverOutput.trim(); } } catch {} - } else if (platform === 'linux') { + } else { try { - const vulkanInfo = await getVulkanInfo(); - for (const gpu of vulkanInfo.allGPUs) { if (gpu.driverInfo && !gpu.isIntegrated) { driverVersion = gpu.driverInfo; @@ -467,6 +340,119 @@ function findComputeUnitsInClInfo(lines: string[], startIndex: number) { const isDiscreteGPU = (computeUnits: number) => computeUnits > 12; +function parseRocmOutput(output: string, vulkanInfo: { allGPUs: GPUDevice[] }) { + const devices: GPUDevice[] = []; + const lines = output.split('\n'); + let currentDevice: Partial | null = null; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmedLine = line.trim(); + + // Handle hipInfo format + if (handleHipInfoLine(trimmedLine, currentDevice, devices)) { + currentDevice = trimmedLine.startsWith('device#') ? {} : currentDevice; + continue; + } + + // Handle rocminfo format + if (line.includes('Marketing Name:')) { + const device = parseRocmInfoDevice(line, lines, i, vulkanInfo); + if (device) { + devices.push(device); + } + } + } + + // Add the last device for hipInfo format + if (currentDevice?.name) { + devices.push( + createDevice(currentDevice.name, currentDevice.isIntegrated || false) + ); + } + + return devices; +} + +function handleHipInfoLine( + trimmedLine: string, + currentDevice: Partial | null, + devices: GPUDevice[] +): boolean { + if (trimmedLine.startsWith('device#')) { + if (currentDevice?.name) { + devices.push( + createDevice(currentDevice.name, currentDevice.isIntegrated || false) + ); + } + return true; + } + + if (currentDevice) { + if (trimmedLine.startsWith('Name:')) { + currentDevice.name = trimmedLine.split('Name:')[1]?.trim(); + } else if (trimmedLine.startsWith('isIntegrated:')) { + const value = trimmedLine.split('isIntegrated:')[1]?.trim(); + currentDevice.isIntegrated = value === '1'; + } + return true; + } + + return false; +} + +function parseRocmInfoDevice( + line: string, + lines: string[], + index: number, + vulkanInfo: { allGPUs: GPUDevice[] } +) { + const name = line.split('Marketing Name:')[1]?.trim(); + if (!name) return null; + + const deviceType = findDeviceType(lines, index); + if (deviceType === 'CPU') return null; + + const isIntegrated = determineIfIntegrated(name, vulkanInfo); + return createDevice(name, isIntegrated); +} + +const createDevice = (name: string, isIntegrated: boolean) => ({ + name: isIntegrated ? name : formatDeviceName(name), + isIntegrated, +}); + +function findDeviceType(lines: string[], startIndex: number) { + const searchRangeLines = 20; + const searchStartIndex = Math.max(0, startIndex - searchRangeLines); + const searchEndIndex = Math.min(lines.length, startIndex + searchRangeLines); + + for ( + let searchIndex = searchStartIndex; + searchIndex < searchEndIndex; + searchIndex++ + ) { + if (lines[searchIndex].includes('Device Type:')) { + return lines[searchIndex].split('Device Type:')[1]?.trim() || ''; + } + } + return ''; +} + +function determineIfIntegrated( + name: string, + vulkanInfo: { allGPUs: GPUDevice[] } +): boolean { + try { + const matchingGPU = vulkanInfo.allGPUs.find( + (gpu) => gpu.name.includes(name) || name.includes(gpu.name) + ); + return matchingGPU ? matchingGPU.isIntegrated : false; + } catch { + return false; + } +} + async function detectCLBlast() { try { const { stdout } = await execa('clinfo', [], COMMON_EXEC_OPTIONS); diff --git a/src/main/modules/koboldcpp/download.ts b/src/main/modules/koboldcpp/download.ts index fa242a5..8b610cf 100644 --- a/src/main/modules/koboldcpp/download.ts +++ b/src/main/modules/koboldcpp/download.ts @@ -15,6 +15,7 @@ import { pathExists } from '@/utils/node/fs'; import { stripAssetExtensions } from '@/utils/version'; import { getLauncherPath } from '@/utils/node/path'; import type { DownloadReleaseOptions, GitHubAsset } from '@/types/electron'; +import { clearVersionCache } from './version'; async function removeDirectoryWithRetry( dirPath: string, @@ -174,6 +175,8 @@ export async function downloadRelease( unpackedDirPath ); + clearVersionCache(launcherPath); + if (options.oldVersionPath && options.isUpdate) { const oldInstallDir = join(options.oldVersionPath, '..'); diff --git a/src/main/modules/koboldcpp/version.ts b/src/main/modules/koboldcpp/version.ts index eb3b316..1c341b5 100644 --- a/src/main/modules/koboldcpp/version.ts +++ b/src/main/modules/koboldcpp/version.ts @@ -13,6 +13,19 @@ import { logError } from '@/utils/node/logging'; import { getLauncherPath } from '@/utils/node/path'; import type { InstalledVersion } from '@/types/electron'; +const versionCache = new Map< + string, + { version: string; actualVersion?: string } | null +>(); + +export function clearVersionCache(path?: string) { + if (path) { + versionCache.delete(path); + } else { + versionCache.clear(); + } +} + export async function getInstalledVersions() { try { const installDir = getInstallDir(); @@ -129,12 +142,48 @@ export async function setCurrentVersion(binaryPath: string) { return false; } +export async function deleteRelease(binaryPath: string) { + try { + if (!(await pathExists(binaryPath))) { + return { success: false, error: 'Release not found' }; + } + + const currentBinaryPath = getCurrentKoboldBinary(); + if (currentBinaryPath === binaryPath) { + return { + success: false, + error: 'Cannot delete the currently active release', + }; + } + + const releaseDir = binaryPath.split(/[/\\]/).slice(0, -1).join('/'); + + if (await pathExists(releaseDir)) { + const { rm } = await import('fs/promises'); + await rm(releaseDir, { recursive: true, force: true }); + + clearVersionCache(binaryPath); + sendToRenderer('versions-updated'); + + return { success: true }; + } + + return { success: false, error: 'Release directory not found' }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } +} + export async function getVersionFromBinary(launcherPath: string) { try { if (!(await pathExists(launcherPath))) { return null; } + if (versionCache.has(launcherPath)) { + return versionCache.get(launcherPath); + } + let folderVersion: string | null = null; let actualVersion: string | null = null; @@ -155,37 +204,31 @@ export async function getVersionFromBinary(launcherPath: string) { }); const allOutput = (result.stdout + result.stderr).trim(); + const lines = allOutput.split('\n').filter((line) => line.trim()); - if (/^\d+\.\d+/.test(allOutput)) { - const versionParts = allOutput.split(/\s+/)[0]; - if (versionParts && /^\d+\.\d+/.test(versionParts)) { - actualVersion = versionParts; - } - } - - 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; - } - } + if (lines.length > 0) { + const lastLine = lines[lines.length - 1].trim(); + const versionMatch = lastLine.match( + /^(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/ + ); + if (versionMatch) { + actualVersion = versionMatch[1]; } } } catch {} - return { + const result = { version: folderVersion || actualVersion || 'unknown', actualVersion: folderVersion && actualVersion && folderVersion !== actualVersion ? actualVersion : undefined, }; + + versionCache.set(launcherPath, result); + return result; } catch { + versionCache.set(launcherPath, null); return null; } } diff --git a/src/preload/index.ts b/src/preload/index.ts index 0bfb6e3..470fdb2 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -36,6 +36,8 @@ const koboldAPI: KoboldAPI = { ipcRenderer.invoke('kobold:selectInstallDirectory'), downloadRelease: (asset, options) => ipcRenderer.invoke('kobold:downloadRelease', asset, options), + deleteRelease: (binaryPath) => + ipcRenderer.invoke('kobold:deleteRelease', binaryPath), launchKoboldCpp: (args) => ipcRenderer.invoke('kobold:launchKoboldCpp', args), getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'), saveConfigFile: (configName, configData) => diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 5a724ce..e172fc8 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -124,6 +124,9 @@ export interface KoboldAPI { asset: GitHubAsset, options: DownloadReleaseOptions ) => Promise; + deleteRelease: ( + binaryPath: string + ) => Promise<{ success: boolean; error?: string }>; launchKoboldCpp: ( args?: string[] ) => Promise<{ success: boolean; pid?: number; error?: string }>; diff --git a/src/types/hardware.d.ts b/src/types/hardware.d.ts index 0f3f2cc..e2380f7 100644 --- a/src/types/hardware.d.ts +++ b/src/types/hardware.d.ts @@ -13,8 +13,8 @@ export interface SystemMemoryInfo { } export interface GPUDevice { - readonly name: string; - readonly isIntegrated: boolean; + name: string; + isIntegrated: boolean; } export interface GPUCapabilities { @@ -52,7 +52,7 @@ export interface HardwareDetectionResult { export interface HardwareInfo { cpu: CPUCapabilities; gpu: BasicGPUInfo; - gpuCapabilities?: GPUCapabilities; - gpuMemory?: GPUMemoryInfo[]; - systemMemory?: SystemMemoryInfo; + gpuCapabilities: GPUCapabilities; + gpuMemory: GPUMemoryInfo[]; + systemMemory: SystemMemoryInfo; } diff --git a/src/utils/node/vulkan.ts b/src/utils/node/vulkan.ts index 85abf4e..fea6ed0 100644 --- a/src/utils/node/vulkan.ts +++ b/src/utils/node/vulkan.ts @@ -4,7 +4,7 @@ import { formatDeviceName } from '@/utils/format'; let vulkanInfoCache: { allGPUs: { - deviceName: string; + name: string; driverInfo?: string; apiVersion?: string; hasAMD: boolean; @@ -27,7 +27,7 @@ export async function getVulkanInfo() { }); const allGPUs: { - deviceName: string; + name: string; driverInfo?: string; apiVersion?: string; hasAMD: boolean; @@ -62,7 +62,7 @@ export async function getVulkanInfo() { 'PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU' ); currentGPU = { - deviceName: '', + name: '', hasAMD: false, hasNVIDIA: false, isIntegrated, @@ -77,7 +77,7 @@ export async function getVulkanInfo() { if (parts.length >= 2) { const name = parts[1]?.trim(); if (name) { - currentGPU.deviceName = name; + currentGPU.name = name; currentGPU.hasAMD = name.toLowerCase().includes('amd') || name.toLowerCase().includes('radeon'); @@ -107,7 +107,7 @@ export async function getVulkanInfo() { } } } else if (foundGPU && currentGPU && line.includes('GPU')) { - if (currentGPU.deviceName) { + if (currentGPU.name) { allGPUs.push(currentGPU); } foundGPU = false; @@ -115,7 +115,7 @@ export async function getVulkanInfo() { } } - if (foundGPU && currentGPU && currentGPU.deviceName) { + if (foundGPU && currentGPU && currentGPU.name) { allGPUs.push(currentGPU); } } @@ -143,7 +143,7 @@ export async function detectGPUViaVulkan() { const gpuInfo: string[] = []; for (const gpu of vulkanInfo.allGPUs.filter((g) => !g.isIntegrated)) { - gpuInfo.push(formatDeviceName(gpu.deviceName)); + gpuInfo.push(formatDeviceName(gpu.name)); if (gpu.hasAMD) { hasAMD = true; diff --git a/src/utils/systemInfo.ts b/src/utils/systemInfo.ts index 662235e..5633dd7 100644 --- a/src/utils/systemInfo.ts +++ b/src/utils/systemInfo.ts @@ -1,21 +1,7 @@ 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; -} +import type { HardwareInfo } from '@/types/hardware'; export const createSoftwareItems = (versionInfo: SystemVersionInfo) => [ {