import { useState, useEffect, useCallback } from 'react'; import { Stack, Text, Group, Button, Card, Loader, rem, Center, Anchor, } from '@mantine/core'; import { RotateCcw, ExternalLink } from 'lucide-react'; import { DownloadCard } from '@/components/DownloadCard'; import { getAssetDescription, sortAssetsByRecommendation, isAssetRecommended, } from '@/utils/assets'; import { getDisplayNameFromPath, formatFileSizeInMB } from '@/utils'; import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron'; const compareVersions = (versionA: string, versionB: string): number => { const cleanVersion = (version: string): string => version.replace(/^v/, '').replace(/[^0-9.]/g, ''); const parseVersion = (version: string): number[] => cleanVersion(version) .split('.') .map((num) => parseInt(num, 10) || 0); const a = parseVersion(versionA); const b = parseVersion(versionB); const maxLength = Math.max(a.length, b.length); for (let i = 0; i < maxLength; i++) { const aVal = a[i] || 0; const bVal = b[i] || 0; if (aVal !== bVal) { return aVal - bVal; } } return 0; }; interface VersionInfo { name: string; version: string; size?: number; isInstalled: boolean; isCurrent: boolean; downloadUrl?: string; installedPath?: string; isROCm?: boolean; hasUpdate?: boolean; newerVersion?: string; } export const VersionsTab = () => { const { platformInfo, availableDownloads, loadingPlatform, loadingRemote, downloading, downloadProgress, loadRemoteVersions, handleDownload: sharedHandleDownload, } = useKoboldVersions(); const [installedVersions, setInstalledVersions] = useState< InstalledVersion[] >([]); const [currentVersion, setCurrentVersion] = useState( null ); const [loadingInstalled, setLoadingInstalled] = useState(true); const [latestRelease, setLatestRelease] = useState( null ); const loadInstalledVersions = useCallback(async () => { setLoadingInstalled(true); try { const [versions, currentBinaryPath] = await Promise.all([ window.electronAPI.kobold.getInstalledVersions(), window.electronAPI.config.get('currentKoboldBinary') as Promise, ]); setInstalledVersions(versions); if (currentBinaryPath && versions.length > 0) { const current = versions.find((v) => v.path === currentBinaryPath); if (current) { setCurrentVersion(current); } else { setCurrentVersion(versions[0]); await window.electronAPI.config.set( 'currentKoboldBinary', versions[0].path ); } } else if (versions.length > 0) { setCurrentVersion(versions[0]); await window.electronAPI.config.set( 'currentKoboldBinary', versions[0].path ); } else { setCurrentVersion(null); if (currentBinaryPath) { await window.electronAPI.config.set('currentKoboldBinary', ''); } } } catch (error) { window.electronAPI.logs.logError( 'Failed to load installed versions:', error as Error ); } finally { setLoadingInstalled(false); } }, []); const loadLatestRelease = useCallback(async () => { try { const release = await window.electronAPI.kobold.getLatestReleaseWithStatus(); setLatestRelease(release); } catch (error) { window.electronAPI.logs.logError( 'Failed to load latest release:', error as Error ); } }, []); useEffect(() => { loadInstalledVersions(); loadLatestRelease(); }, [loadInstalledVersions, loadLatestRelease]); const getAllVersions = (): VersionInfo[] => { const versions: VersionInfo[] = []; const processedInstalled = new Set(); availableDownloads.forEach((download) => { const downloadBaseName = download.name .replace(/\.(tar\.gz|zip|exe)$/i, '') .replace(/\.packed$/, ''); const installedVersion = installedVersions.find((v) => { const displayName = getDisplayNameFromPath(v); return displayName === downloadBaseName; }); const isCurrent = Boolean( installedVersion && currentVersion && currentVersion.path === installedVersion.path ); if (installedVersion) { processedInstalled.add(installedVersion.path); const hasUpdate = compareVersions( download.version || 'unknown', installedVersion.version ) > 0; versions.push({ name: download.name, version: installedVersion.version, size: undefined, isInstalled: true, isCurrent, downloadUrl: download.url, installedPath: installedVersion.path, isROCm: download.type === 'rocm', hasUpdate, newerVersion: hasUpdate ? download.version : undefined, }); } else { versions.push({ name: download.name, version: download.version || 'unknown', size: download.size, isInstalled: false, isCurrent: false, downloadUrl: download.url, isROCm: download.type === 'rocm', }); } }); installedVersions.forEach((installed) => { if (!processedInstalled.has(installed.path)) { const displayName = getDisplayNameFromPath(installed); const isCurrent = Boolean( currentVersion && currentVersion.path === installed.path ); versions.push({ name: displayName, version: installed.version, size: undefined, isInstalled: true, isCurrent, installedPath: installed.path, isROCm: false, }); } }); return sortAssetsByRecommendation(versions, platformInfo.hasAMDGPU); }; const handleDownload = async (version: VersionInfo) => { try { const download = availableDownloads.find((d) => d.name === version.name); if (!download) { throw new Error('Download not found'); } const downloadType = download.type === 'rocm' ? 'rocm' : 'asset'; const success = await sharedHandleDownload( downloadType, download, false, false ); if (success) { await loadInstalledVersions(); } } catch (error) { window.electronAPI.logs.logError('Failed to download:', error as Error); } }; const handleUpdate = async (version: VersionInfo) => { try { const download = availableDownloads.find((d) => d.name === version.name); if (!download) { throw new Error('Download not found'); } const downloadType = download.type === 'rocm' ? 'rocm' : 'asset'; const wasCurrentBinary = version.isCurrent; const success = await sharedHandleDownload( downloadType, download, true, wasCurrentBinary ); if (success) { await loadInstalledVersions(); } } catch (error) { window.electronAPI.logs.logError('Failed to update:', error as Error); } }; const makeCurrent = async (version: VersionInfo) => { if (!version.installedPath) return; try { const success = await window.electronAPI.kobold.setCurrentVersion( version.installedPath ); if (success) { await window.electronAPI.config.set( 'currentKoboldBinary', version.installedPath ); const newCurrentVersion = installedVersions.find( (v) => v.path === version.installedPath ); if (newCurrentVersion) { setCurrentVersion(newCurrentVersion); } } } catch (error) { window.electronAPI.logs.logError( 'Failed to set current version:', error as Error ); } }; const isLoading = loadingInstalled || loadingPlatform || loadingRemote; if (isLoading) { return (
{loadingInstalled && (loadingPlatform || loadingRemote) ? 'Loading versions...' : loadingInstalled ? 'Scanning installed versions...' : 'Checking for updates...'}
); } return ( <>
Available Versions {latestRelease && ( Latest release: {latestRelease.release.tag_name} { e.preventDefault(); window.electronAPI.app.openExternal( latestRelease.release.html_url ); }} size="sm" c="blue" > Release notes )}
{getAllVersions().map((version, index) => { const isDownloading = downloading === version.name; return (
{ e.stopPropagation(); handleDownload(version); } : undefined } onUpdate={ version.hasUpdate ? (e) => { e.stopPropagation(); handleUpdate(version); } : undefined } onMakeCurrent={ version.isInstalled && !version.isCurrent && !version.hasUpdate ? () => makeCurrent(version) : undefined } />
); })} {getAllVersions().length === 0 && ( No versions found )} ); };