import { useState, useEffect, useCallback, useMemo, useRef } 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, sortDownloadsByType } from '@/utils/assets'; import { getDisplayNameFromPath, stripAssetExtensions, compareVersions, } from '@/utils/version'; import { Logger } from '@/utils/logger'; import { formatDownloadSize } from '@/utils/download'; import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron'; interface VersionInfo { name: string; version: string; size?: number; isInstalled: boolean; isCurrent: boolean; downloadUrl?: string; installedPath?: string; hasUpdate?: boolean; newerVersion?: string; } export const VersionsTab = () => { const { availableDownloads, loadingPlatform, loadingRemote, downloading, downloadProgress, loadRemoteVersions, handleDownload: sharedHandleDownload, getLatestReleaseWithDownloadStatus, } = useKoboldVersions(); const [installedVersions, setInstalledVersions] = useState< InstalledVersion[] >([]); const [currentVersion, setCurrentVersion] = useState( null ); const [loadingInstalled, setLoadingInstalled] = useState(true); const [latestRelease, setLatestRelease] = useState( null ); const downloadingItemRef = useRef(null); const loadInstalledVersions = useCallback(async () => { setLoadingInstalled(true); await Logger.safeExecute(async () => { 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', ''); } } }, 'Failed to load installed versions:'); setLoadingInstalled(false); }, []); const loadLatestRelease = useCallback(async () => { const release = await Logger.safeExecute( () => getLatestReleaseWithDownloadStatus(), 'Failed to load latest release:' ); if (release) { setLatestRelease(release); } }, [getLatestReleaseWithDownloadStatus]); useEffect(() => { loadInstalledVersions(); loadLatestRelease(); }, [loadInstalledVersions, loadLatestRelease]); const allVersions = useMemo((): VersionInfo[] => { const versions: VersionInfo[] = []; const processedInstalled = new Set(); const sortedDownloads = sortDownloadsByType(availableDownloads); sortedDownloads.forEach((download) => { const downloadBaseName = stripAssetExtensions(download.name); 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, 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, }); } }); 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, }); } }); return versions; }, [availableDownloads, installedVersions, currentVersion]); useEffect(() => { if (downloading && downloadingItemRef.current) { downloadingItemRef.current.scrollIntoView({ behavior: 'smooth', block: 'center', }); } }, [downloading]); const handleDownload = async (version: VersionInfo) => { await Logger.safeExecute(async () => { const download = availableDownloads.find((d) => d.name === version.name); if (!download) { throw new Error('Download not found'); } const success = await sharedHandleDownload({ item: download, isUpdate: false, wasCurrentBinary: false, }); if (success) { await loadInstalledVersions(); } }, 'Failed to download:'); }; const handleUpdate = async (version: VersionInfo) => { await Logger.safeExecute(async () => { const download = availableDownloads.find((d) => d.name === version.name); if (!download) { throw new Error('Download not found'); } const success = await sharedHandleDownload({ item: download, isUpdate: true, wasCurrentBinary: version.isCurrent, }); if (success) { await loadInstalledVersions(); } }, 'Failed to update:'); }; const makeCurrent = async (version: VersionInfo) => { if (!version.installedPath) return; await Logger.safeExecute(async () => { 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); } } }, 'Failed to set current version:'); }; 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 )}
{allVersions.map((version, index) => { const isDownloading = downloading === version.name; return (
{ e.stopPropagation(); handleDownload(version); }} onUpdate={(e) => { e.stopPropagation(); handleUpdate(version); }} onMakeCurrent={() => makeCurrent(version)} />
); })} {allVersions.length === 0 && ( No versions found )} ); };