import { useState, useCallback, useEffect } from 'react'; import { getDisplayNameFromPath, compareVersions } from '@/utils/version'; import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import type { InstalledVersion, DownloadItem } from '@/types/electron'; interface UpdateInfo { currentVersion: InstalledVersion; availableUpdate: DownloadItem; } export const useUpdateChecker = () => { const [updateInfo, setUpdateInfo] = useState(null); const [isChecking, setIsChecking] = useState(false); const [showUpdateModal, setShowUpdateModal] = useState(false); const [dismissedUpdates, setDismissedUpdates] = useState>( new Set() ); const [dismissedUpdatesLoaded, setDismissedUpdatesLoaded] = useState(false); const { availableDownloads: releases, getROCmDownload } = useKoboldVersions(); useEffect(() => { const loadDismissedUpdates = async () => { try { const dismissed = (await window.electronAPI.config.get( 'dismissedUpdates' )) as string[] | undefined; if (dismissed) { setDismissedUpdates(new Set(dismissed)); } setDismissedUpdatesLoaded(true); } catch (error) { window.electronAPI.logs.logError( 'Failed to load dismissed updates:', error as Error ); setDismissedUpdatesLoaded(true); } }; loadDismissedUpdates(); }, []); const saveDismissedUpdates = useCallback(async (updates: Set) => { try { await window.electronAPI.config.set( 'dismissedUpdates', Array.from(updates) ); } catch (error) { window.electronAPI.logs.logError( 'Failed to save dismissed updates:', error as Error ); } }, []); const checkForUpdates = useCallback(async () => { if (!dismissedUpdatesLoaded || releases.length === 0) { return; } setIsChecking(true); try { const [currentBinaryPath, installedVersionsResult, rocmDownload] = await Promise.all([ window.electronAPI.config.get( 'currentKoboldBinary' ) as Promise, window.electronAPI.kobold.getInstalledVersions(), getROCmDownload(), ]); if (!currentBinaryPath || installedVersionsResult.length === 0) { return; } const currentVersion = installedVersionsResult.find( (v: InstalledVersion) => v.path === currentBinaryPath ); if (!currentVersion) { return; } const availableDownloads: DownloadItem[] = [...releases]; if (rocmDownload) { availableDownloads.push(rocmDownload); } const currentDisplayName = getDisplayNameFromPath(currentVersion); const matchingDownload = availableDownloads.find( (download: DownloadItem) => { const downloadBaseName = download.name .replace(/\.(tar\.gz|zip|exe)$/i, '') .replace(/\.packed$/, ''); return downloadBaseName === currentDisplayName; } ); if (matchingDownload && matchingDownload.version) { const hasUpdate = compareVersions(matchingDownload.version, currentVersion.version) > 0; if (hasUpdate) { const updateKey = `${currentVersion.path}-${matchingDownload.version}`; if (!dismissedUpdates.has(updateKey)) { setUpdateInfo({ currentVersion, availableUpdate: matchingDownload, }); setShowUpdateModal(true); } } } } catch (error) { window.electronAPI.logs.logError( 'Failed to check for updates:', error as Error ); } finally { setIsChecking(false); } }, [dismissedUpdates, dismissedUpdatesLoaded, releases, getROCmDownload]); const dismissUpdate = useCallback(async () => { if (updateInfo) { const updateKey = `${updateInfo.currentVersion.path}-${updateInfo.availableUpdate.version}`; const newDismissedUpdates = new Set([...dismissedUpdates, updateKey]); setDismissedUpdates(newDismissedUpdates); await saveDismissedUpdates(newDismissedUpdates); } setShowUpdateModal(false); setUpdateInfo(null); }, [updateInfo, dismissedUpdates, saveDismissedUpdates]); return { updateInfo, showUpdateModal, isChecking, checkForUpdates, dismissUpdate, }; };