import { Anchor, Card, Center, Group, Loader, Stack, Text } from '@mantine/core'; import { ExternalLink } from 'lucide-react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { DownloadCard } from '@/components/DownloadCard'; import { ImportBackendLink } from '@/components/ImportBackendLink'; import { useKoboldBackendsStore } from '@/stores/koboldBackends'; import type { BackendInfo } from '@/types'; import type { InstalledBackend, ReleaseWithStatus } from '@/types/electron'; import { getAssetDescription } from '@/utils/assets'; import { formatDownloadSize } from '@/utils/format'; import { compareVersions, getDisplayNameFromPath, stripAssetExtensions } from '@/utils/version'; export const BackendsTab = () => { const { availableDownloads, loadingPlatform, loadingRemote, downloading, handleDownload: handleDownloadFromStore, getLatestReleaseWithDownloadStatus, refreshDownloads, } = useKoboldBackendsStore(); const [installedBackends, setInstalledBackends] = useState([]); const [currentBackend, setCurrentBackend] = useState(null); const [loadingInstalled, setLoadingInstalled] = useState(true); const [latestRelease, setLatestRelease] = useState(null); const [importing, setImporting] = useState(false); const downloadingItemRef = useRef(null); useEffect(() => { const init = async () => { setLoadingInstalled(true); await refreshDownloads(); const [backends, current] = await Promise.all([ window.electronAPI.kobold.getInstalledBackends(), window.electronAPI.kobold.getCurrentBackend(), ]); setInstalledBackends(backends); setCurrentBackend(current); setLoadingInstalled(false); const release = await getLatestReleaseWithDownloadStatus(); if (release) { setLatestRelease(release); } }; void init(); }, [getLatestReleaseWithDownloadStatus, refreshDownloads]); const loadInstalledBackends = useCallback(async () => { const [backends, current] = await Promise.all([ window.electronAPI.kobold.getInstalledBackends(), window.electronAPI.kobold.getCurrentBackend(), ]); setInstalledBackends(backends); setCurrentBackend(current); }, []); const allBackends = useMemo((): BackendInfo[] => { const backends: BackendInfo[] = []; const processedInstalled = new Set(); availableDownloads.forEach((download) => { const downloadBaseName = stripAssetExtensions(download.name); const installedBackend = installedBackends.find((b) => { const displayName = getDisplayNameFromPath(b); return displayName === downloadBaseName; }); const isCurrent = Boolean( installedBackend && currentBackend && currentBackend.path === installedBackend.path, ); if (installedBackend) { processedInstalled.add(installedBackend.path); const hasUpdate = compareVersions(download.version ?? 'unknown', installedBackend.version) > 0; backends.push({ actualVersion: installedBackend.actualVersion, downloadUrl: download.url, hasUpdate, installedPath: installedBackend.path, isCurrent, isInstalled: true, name: download.name, newerVersion: hasUpdate ? download.version : undefined, size: undefined, version: installedBackend.version, }); } else { backends.push({ downloadUrl: download.url, isCurrent: false, isInstalled: false, name: download.name, size: download.size, version: download.version ?? 'unknown', }); } }); installedBackends.forEach((installed) => { if (!processedInstalled.has(installed.path)) { const displayName = getDisplayNameFromPath(installed); const isCurrent = Boolean(currentBackend && currentBackend.path === installed.path); backends.push({ actualVersion: installed.actualVersion, installedPath: installed.path, isCurrent, isInstalled: true, name: displayName, size: undefined, version: installed.version, }); } }); return backends.toSorted((a, b) => { if (a.isInstalled && !b.isInstalled) { return -1; } if (!a.isInstalled && b.isInstalled) { return 1; } return 0; }); }, [availableDownloads, installedBackends, currentBackend]); useEffect(() => { if (downloading && downloadingItemRef.current) { downloadingItemRef.current.scrollIntoView({ behavior: 'smooth', block: 'center', }); } }, [downloading]); const handleDownload = async (backend: BackendInfo) => { const download = availableDownloads.find((d) => d.name === backend.name); if (!download) { return; } await handleDownloadFromStore({ isUpdate: false, item: download, wasCurrentBinary: false, }); await loadInstalledBackends(); }; const handleUpdate = async (backend: BackendInfo) => { const download = availableDownloads.find((d) => d.name === backend.name); if (!download) { return; } await handleDownloadFromStore({ isUpdate: true, item: download, oldBackendPath: backend.installedPath, wasCurrentBinary: backend.isCurrent, }); await loadInstalledBackends(); }; const handleRedownload = async (backend: BackendInfo) => { const download = availableDownloads.find((d) => d.name === backend.name); if (!download) { return; } await handleDownloadFromStore({ isUpdate: true, item: download, oldBackendPath: backend.installedPath, wasCurrentBinary: backend.isCurrent, }); await loadInstalledBackends(); }; const handleDelete = async (backend: BackendInfo) => { if (!backend.installedPath || backend.isCurrent) { return; } const result = await window.electronAPI.kobold.deleteRelease(backend.installedPath); if (result.success) { await loadInstalledBackends(); } }; const makeCurrent = (backend: BackendInfo) => { if (!backend.installedPath) { return; } const targetBackend = installedBackends.find((b) => b.path === backend.installedPath); if (targetBackend) { setCurrentBackend(targetBackend); } void window.electronAPI.kobold.setCurrentBackend(backend.installedPath); }; if (loadingInstalled) { return (
Scanning installed backends...
); } const isDisabled = downloading !== null || importing; return ( <> {latestRelease && ( Latest release: {latestRelease.release.tag_name} Release notes )} {(loadingPlatform || loadingRemote) && ( Checking for available downloads... )} {allBackends.map((backend, index) => { const isDownloading = downloading === backend.name; return (
{ e.stopPropagation(); void handleDownload(backend); }} onUpdate={(e) => { e.stopPropagation(); void handleUpdate(backend); }} onRedownload={(e) => { e.stopPropagation(); void handleRedownload(backend); }} onDelete={(e) => { e.stopPropagation(); void handleDelete(backend); }} onMakeCurrent={() => makeCurrent(backend)} />
); })} {allBackends.length === 0 && ( No backends found )} ); };