From ba4dc6d1eb8d9bdeeecf8be982ae3686becadec5 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 8 Sep 2025 17:19:37 +0700 Subject: [PATCH] enhancement: update ui dialog update llamacpp backend --- extensions/llamacpp-extension/src/backend.ts | 12 +- .../src/containers/dialogs/BackendUpdater.tsx | 104 ++++++ web-app/src/hooks/useBackendUpdater.ts | 348 ++++++++++++++++++ web-app/src/locales/de-DE/settings.json | 14 + web-app/src/locales/en/settings.json | 18 +- web-app/src/locales/id/settings.json | 14 + web-app/src/locales/pl/settings.json | 14 + web-app/src/locales/vn/settings.json | 14 + web-app/src/locales/zh-CN/settings.json | 14 + web-app/src/locales/zh-TW/settings.json | 16 +- web-app/src/routes/__root.tsx | 6 +- .../settings/providers/$providerName.tsx | 128 ++++++- 12 files changed, 696 insertions(+), 6 deletions(-) create mode 100644 web-app/src/containers/dialogs/BackendUpdater.tsx create mode 100644 web-app/src/hooks/useBackendUpdater.ts diff --git a/extensions/llamacpp-extension/src/backend.ts b/extensions/llamacpp-extension/src/backend.ts index 06f26032b..b5e7a581b 100644 --- a/extensions/llamacpp-extension/src/backend.ts +++ b/extensions/llamacpp-extension/src/backend.ts @@ -28,6 +28,13 @@ export async function getLocalInstalledBackends(): Promise< for (const version of versionDirs) { const versionPath = await joinPath([backendsDir, version]) const versionName = await basename(versionPath) + + // Check if versionPath is actually a directory before reading it + const versionStat = await fs.fileStat(versionPath) + if (!versionStat?.isDirectory) { + continue + } + const backendTypes = await fs.readdirSync(versionPath) // Verify that the backend is really installed @@ -150,8 +157,9 @@ export async function listSupportedBackends(): Promise< supportedBackends.push('macos-arm64') } // get latest backends from Github - const remoteBackendVersions = - await fetchRemoteSupportedBackends(supportedBackends) + const remoteBackendVersions = await fetchRemoteSupportedBackends( + supportedBackends + ) // Get locally installed versions const localBackendVersions = await getLocalInstalledBackends() diff --git a/web-app/src/containers/dialogs/BackendUpdater.tsx b/web-app/src/containers/dialogs/BackendUpdater.tsx new file mode 100644 index 000000000..071559c6c --- /dev/null +++ b/web-app/src/containers/dialogs/BackendUpdater.tsx @@ -0,0 +1,104 @@ +import { useBackendUpdater } from '@/hooks/useBackendUpdater' + +import { IconDownload } from '@tabler/icons-react' +import { Button } from '@/components/ui/button' + +import { useState, useEffect } from 'react' +import { cn } from '@/lib/utils' +import { useTranslation } from '@/i18n/react-i18next-compat' +import { toast } from 'sonner' + +const BackendUpdater = () => { + const { t } = useTranslation() + const { updateState, updateBackend, checkForUpdate, setRemindMeLater } = + useBackendUpdater() + + const handleUpdate = async () => { + try { + await updateBackend() + setRemindMeLater(true) + toast.success(t('settings:backendUpdater.updateSuccess')) + } catch (error) { + console.error('Backend update failed:', error) + toast.error(t('settings:backendUpdater.updateError')) + } + } + + // Check for updates when component mounts + useEffect(() => { + checkForUpdate() + }, [checkForUpdate]) + + const [backendUpdateState, setBackendUpdateState] = useState({ + remindMeLater: false, + isUpdateAvailable: false, + }) + + useEffect(() => { + setBackendUpdateState({ + remindMeLater: updateState.remindMeLater, + isUpdateAvailable: updateState.isUpdateAvailable, + }) + }, [updateState]) + + // Don't show if user clicked remind me later or auto update is enabled + if (backendUpdateState.remindMeLater || updateState.autoUpdateEnabled) + return null + + return ( + <> + {backendUpdateState.isUpdateAvailable && ( +
+
+
+
+ +
+
+ {t('settings:backendUpdater.newBackendVersion', { + version: updateState.updateInfo?.newVersion, + })} +
+
+ {t('settings:backendUpdater.backendUpdateAvailable')} +
+
+
+
+ +
+
+
+ + +
+
+
+
+
+ )} + + ) +} + +export default BackendUpdater diff --git a/web-app/src/hooks/useBackendUpdater.ts b/web-app/src/hooks/useBackendUpdater.ts new file mode 100644 index 000000000..84f778973 --- /dev/null +++ b/web-app/src/hooks/useBackendUpdater.ts @@ -0,0 +1,348 @@ +import { useState, useCallback, useEffect } from 'react' +import { events } from '@janhq/core' +import { ExtensionManager } from '@/lib/extension' + +export interface BackendUpdateInfo { + updateNeeded: boolean + newVersion: string + currentVersion?: string +} + +interface ExtensionSetting { + key: string + controllerProps?: { + value: unknown + } +} + +interface LlamacppExtension { + getSettings?(): Promise + checkBackendForUpdates?(): Promise + updateBackend?( + targetBackend: string + ): Promise<{ wasUpdated: boolean; newBackend: string }> + installBackend?(filePath: string): Promise +} + +export interface BackendUpdateState { + isUpdateAvailable: boolean + updateInfo: BackendUpdateInfo | null + isUpdating: boolean + remindMeLater: boolean + autoUpdateEnabled: boolean +} + +export const useBackendUpdater = () => { + const [updateState, setUpdateState] = useState({ + isUpdateAvailable: false, + updateInfo: null, + isUpdating: false, + remindMeLater: false, + autoUpdateEnabled: false, + }) + + // Listen for backend update state sync events + useEffect(() => { + const handleUpdateStateSync = (newState: Partial) => { + setUpdateState((prev) => ({ + ...prev, + ...newState, + })) + } + + events.on('onBackendUpdateStateSync', handleUpdateStateSync) + + return () => { + events.off('onBackendUpdateStateSync', handleUpdateStateSync) + } + }, []) + + // Check auto update setting from llamacpp extension + useEffect(() => { + const checkAutoUpdateSetting = async () => { + try { + // Get llamacpp extension instance + const allExtensions = ExtensionManager.getInstance().listExtensions() + let llamacppExtension = + ExtensionManager.getInstance().getByName('llamacpp-extension') + + if (!llamacppExtension) { + // Try to find by type or other properties + llamacppExtension = + allExtensions.find( + (ext) => + ext.constructor.name.toLowerCase().includes('llamacpp') || + (ext.type && + ext.type()?.toString().toLowerCase().includes('inference')) + ) || undefined + } + + if (llamacppExtension && 'getSettings' in llamacppExtension) { + const extension = llamacppExtension as LlamacppExtension + const settings = await extension.getSettings?.() + const autoUpdateSetting = settings?.find( + (s) => s.key === 'auto_update_engine' + ) + + setUpdateState((prev) => ({ + ...prev, + autoUpdateEnabled: + autoUpdateSetting?.controllerProps?.value === true, + })) + } + } catch (error) { + console.error('Failed to check auto update setting:', error) + } + } + + checkAutoUpdateSetting() + }, []) + + const syncStateToOtherInstances = useCallback( + (partialState: Partial) => { + // Emit event to sync state across all useBackendUpdater instances + events.emit('onBackendUpdateStateSync', partialState) + }, + [] + ) + + const checkForUpdate = useCallback( + async (resetRemindMeLater = false) => { + try { + // Reset remindMeLater if requested (e.g., when called from settings) + if (resetRemindMeLater) { + const newState = { + remindMeLater: false, + } + setUpdateState((prev) => ({ + ...prev, + ...newState, + })) + syncStateToOtherInstances(newState) + } + + // Get llamacpp extension instance + const allExtensions = ExtensionManager.getInstance().listExtensions() + + const llamacppExtension = + ExtensionManager.getInstance().getByName('llamacpp-extension') + + let extensionToUse = llamacppExtension + + if (!llamacppExtension) { + // Try to find by type or other properties + const possibleExtension = allExtensions.find( + (ext) => + ext.constructor.name.toLowerCase().includes('llamacpp') || + (ext.type && + ext.type()?.toString().toLowerCase().includes('inference')) + ) + + if (!possibleExtension) { + console.error('LlamaCpp extension not found') + return null + } + + extensionToUse = possibleExtension + } + + if (!extensionToUse || !('checkBackendForUpdates' in extensionToUse)) { + console.error( + 'Extension does not support checkBackendForUpdates method' + ) + return null + } + + // Call the extension's checkBackendForUpdates method + const extension = extensionToUse as LlamacppExtension + const updateInfo = await extension.checkBackendForUpdates?.() + + if (updateInfo?.updateNeeded) { + const newState = { + isUpdateAvailable: true, + remindMeLater: false, + updateInfo, + } + setUpdateState((prev) => ({ + ...prev, + ...newState, + })) + syncStateToOtherInstances(newState) + console.log('Backend update available:', updateInfo?.newVersion) + return updateInfo + } else { + // No update available - reset state + const newState = { + isUpdateAvailable: false, + updateInfo: null, + } + setUpdateState((prev) => ({ + ...prev, + ...newState, + })) + syncStateToOtherInstances(newState) + return null + } + } catch (error) { + console.error('Error checking for backend updates:', error) + // Reset state on error + const newState = { + isUpdateAvailable: false, + updateInfo: null, + } + setUpdateState((prev) => ({ + ...prev, + ...newState, + })) + syncStateToOtherInstances(newState) + return null + } + }, + [syncStateToOtherInstances] + ) + + const setRemindMeLater = useCallback( + (remind: boolean) => { + const newState = { + remindMeLater: remind, + } + setUpdateState((prev) => ({ + ...prev, + ...newState, + })) + syncStateToOtherInstances(newState) + }, + [syncStateToOtherInstances] + ) + + const updateBackend = useCallback(async () => { + if (!updateState.updateInfo) return + + try { + setUpdateState((prev) => ({ + ...prev, + isUpdating: true, + })) + + // Get llamacpp extension instance + const allExtensions = ExtensionManager.getInstance().listExtensions() + const llamacppExtension = + ExtensionManager.getInstance().getByName('llamacpp-extension') + + let extensionToUse = llamacppExtension + + if (!llamacppExtension) { + // Try to find by type or other properties + const possibleExtension = allExtensions.find( + (ext) => + ext.constructor.name.toLowerCase().includes('llamacpp') || + (ext.type && + ext.type()?.toString().toLowerCase().includes('inference')) + ) + + if (!possibleExtension) { + throw new Error('LlamaCpp extension not found') + } + + extensionToUse = possibleExtension + } + + if ( + !extensionToUse || + !('getSettings' in extensionToUse) || + !('updateBackend' in extensionToUse) + ) { + throw new Error('Extension does not support backend updates') + } + + // Get current backend version to construct target backend string + const extension = extensionToUse as LlamacppExtension + const settings = await extension.getSettings?.() + const currentBackendSetting = settings?.find( + (s) => s.key === 'version_backend' + ) + const currentBackend = currentBackendSetting?.controllerProps + ?.value as string + + if (!currentBackend) { + throw new Error('Current backend not found') + } + + // Extract backend type from current backend string (e.g., "b3224/cuda12" -> "cuda12") + const [, backendType] = currentBackend.split('/') + const targetBackendString = `${updateState.updateInfo.newVersion}/${backendType}` + + // Call the extension's updateBackend method + const result = await extension.updateBackend?.(targetBackendString) + + if (result?.wasUpdated) { + // Reset update state + const newState = { + isUpdateAvailable: false, + updateInfo: null, + isUpdating: false, + } + setUpdateState((prev) => ({ + ...prev, + ...newState, + })) + syncStateToOtherInstances(newState) + } else { + throw new Error('Backend update failed') + } + } catch (error) { + console.error('Error updating backend:', error) + setUpdateState((prev) => ({ + ...prev, + isUpdating: false, + })) + throw error + } + }, [updateState.updateInfo, syncStateToOtherInstances]) + + const installBackend = useCallback(async (filePath: string) => { + try { + // Get llamacpp extension instance + const allExtensions = ExtensionManager.getInstance().listExtensions() + const llamacppExtension = + ExtensionManager.getInstance().getByName('llamacpp-extension') + + let extensionToUse = llamacppExtension + + if (!llamacppExtension) { + // Try to find by type or other properties + const possibleExtension = allExtensions.find( + (ext) => + ext.constructor.name.toLowerCase().includes('llamacpp') || + (ext.type && + ext.type()?.toString().toLowerCase().includes('inference')) + ) + + if (!possibleExtension) { + throw new Error('LlamaCpp extension not found') + } + + extensionToUse = possibleExtension + } + + if (!extensionToUse || !('installBackend' in extensionToUse)) { + throw new Error('Extension does not support backend installation') + } + + // Call the extension's installBackend method + const extension = extensionToUse as LlamacppExtension + await extension.installBackend?.(filePath) + } catch (error) { + console.error('Error installing backend:', error) + throw error + } + }, []) + + return { + updateState, + checkForUpdate, + updateBackend, + setRemindMeLater, + installBackend, + } +} diff --git a/web-app/src/locales/de-DE/settings.json b/web-app/src/locales/de-DE/settings.json index 0e2b11ca5..e6b1fab0a 100644 --- a/web-app/src/locales/de-DE/settings.json +++ b/web-app/src/locales/de-DE/settings.json @@ -6,6 +6,11 @@ "noUpdateAvailable": "Du verwendest die neueste Version", "devVersion": "Entwicklungsversion erkannt", "updateError": "Fehler beim Suchen nach Updates", + "checkForBackendUpdates": "LlamaCpp Updates prüfen", + "checkForBackendUpdatesDesc": "Prüfe, ob eine neuere Version des LlamaCpp-Backends verfügbar ist.", + "checkingForBackendUpdates": "Suche nach LlamaCpp Updates...", + "noBackendUpdateAvailable": "Du verwendest die neueste LlamaCpp Version", + "backendUpdateError": "Fehler beim Suchen nach LlamaCpp Updates", "changeLocation": "Ort ändern", "copied": "Kopiert", "copyPath": "Pfad kopieren", @@ -244,5 +249,14 @@ "cancel": "Abbrechen", "changeLocation": "Ort ändern" } + }, + "backendUpdater": { + "newBackendVersion": "Neue LlamaCpp Version {{version}}", + "backendUpdateAvailable": "LlamaCpp Update verfügbar", + "remindMeLater": "Später erinnern", + "updating": "Aktualisiere...", + "updateNow": "Jetzt aktualisieren", + "updateSuccess": "LlamaCpp erfolgreich aktualisiert", + "updateError": "Fehler beim Aktualisieren von LlamaCpp" } } diff --git a/web-app/src/locales/en/settings.json b/web-app/src/locales/en/settings.json index cf3d8ec17..8f02d79b4 100644 --- a/web-app/src/locales/en/settings.json +++ b/web-app/src/locales/en/settings.json @@ -6,6 +6,11 @@ "noUpdateAvailable": "You're running the latest version", "devVersion": "Development version detected", "updateError": "Failed to check for updates", + "checkForBackendUpdates": "Check for LlamaCpp Updates", + "checkForBackendUpdatesDesc": "Check if a newer version of the LlamaCpp backend is available.", + "checkingForBackendUpdates": "Checking for LlamaCpp updates...", + "noBackendUpdateAvailable": "You're running the latest LlamaCpp version", + "backendUpdateError": "Failed to check for LlamaCpp updates", "changeLocation": "Change Location", "copied": "Copied", "copyPath": "Copy Path", @@ -249,5 +254,16 @@ "cancel": "Cancel", "changeLocation": "Change Location" } - } + }, + "backendUpdater": { + "newBackendVersion": "New LlamaCpp Version {{version}}", + "backendUpdateAvailable": "LlamaCpp Update Available", + "remindMeLater": "Remind Me Later", + "updating": "Updating...", + "updateNow": "Update Now", + "updateSuccess": "LlamaCpp updated successfully", + "updateError": "Failed to update LlamaCpp" + }, + "backendInstallSuccess": "Backend installed successfully", + "backendInstallError": "Failed to install backend" } diff --git a/web-app/src/locales/id/settings.json b/web-app/src/locales/id/settings.json index 507ebbe3a..5b9af0cb5 100644 --- a/web-app/src/locales/id/settings.json +++ b/web-app/src/locales/id/settings.json @@ -6,6 +6,11 @@ "noUpdateAvailable": "Anda menjalankan versi terbaru", "devVersion": "Versi pengembangan terdeteksi", "updateError": "Gagal memeriksa pembaruan", + "checkForBackendUpdates": "Periksa Pembaruan LlamaCpp", + "checkForBackendUpdatesDesc": "Periksa apakah versi backend LlamaCpp yang lebih baru tersedia.", + "checkingForBackendUpdates": "Memeriksa pembaruan LlamaCpp...", + "noBackendUpdateAvailable": "Anda menjalankan versi LlamaCpp terbaru", + "backendUpdateError": "Gagal memeriksa pembaruan LlamaCpp", "changeLocation": "Ubah Lokasi", "copied": "Tersalin", "copyPath": "Salin Jalur", @@ -244,5 +249,14 @@ "cancel": "Batal", "changeLocation": "Ubah Lokasi" } + }, + "backendUpdater": { + "newBackendVersion": "Versi LlamaCpp Baru {{version}}", + "backendUpdateAvailable": "Pembaruan LlamaCpp Tersedia", + "remindMeLater": "Ingatkan Saya Nanti", + "updating": "Memperbarui...", + "updateNow": "Perbarui Sekarang", + "updateSuccess": "LlamaCpp berhasil diperbarui", + "updateError": "Gagal memperbarui LlamaCpp" } } \ No newline at end of file diff --git a/web-app/src/locales/pl/settings.json b/web-app/src/locales/pl/settings.json index 9c4a485e8..bb28ca524 100644 --- a/web-app/src/locales/pl/settings.json +++ b/web-app/src/locales/pl/settings.json @@ -6,6 +6,11 @@ "noUpdateAvailable": "Używasz najnowszej wersji", "devVersion": "Wykryto wersję deweloperską", "updateError": "Nie udało się sprawdzić dostępności aktualizacji", + "checkForBackendUpdates": "Sprawdź Aktualizacje LlamaCpp", + "checkForBackendUpdatesDesc": "Sprawdza czy dostępna jest nowa wersja backendu LlamaCpp.", + "checkingForBackendUpdates": "Sprawdzanie aktualizacji LlamaCpp...", + "noBackendUpdateAvailable": "Używasz najnowszej wersji LlamaCpp", + "backendUpdateError": "Nie udało się sprawdzić aktualizacji LlamaCpp", "changeLocation": "Zmień Położenie", "copied": "Skopiowano", "copyPath": "Skopiuj Ścieżkę", @@ -249,5 +254,14 @@ "cancel": "Anuluj", "changeLocation": "Zmień Położenie" } + }, + "backendUpdater": { + "newBackendVersion": "Nowa wersja LlamaCpp {{version}}", + "backendUpdateAvailable": "Dostępna aktualizacja LlamaCpp", + "remindMeLater": "Przypomnij mi później", + "updating": "Aktualizowanie...", + "updateNow": "Aktualizuj teraz", + "updateSuccess": "LlamaCpp został pomyślnie zaktualizowany", + "updateError": "Nie udało się zaktualizować LlamaCpp" } } diff --git a/web-app/src/locales/vn/settings.json b/web-app/src/locales/vn/settings.json index 6aecec5b6..659ddc3ab 100644 --- a/web-app/src/locales/vn/settings.json +++ b/web-app/src/locales/vn/settings.json @@ -6,6 +6,11 @@ "noUpdateAvailable": "Bạn đang chạy phiên bản mới nhất", "devVersion": "Đã phát hiện phiên bản phát triển", "updateError": "Không thể kiểm tra cập nhật", + "checkForBackendUpdates": "Kiểm tra Cập nhật LlamaCpp", + "checkForBackendUpdatesDesc": "Kiểm tra xem có phiên bản backend LlamaCpp mới hơn không.", + "checkingForBackendUpdates": "Đang kiểm tra cập nhật LlamaCpp...", + "noBackendUpdateAvailable": "Bạn đang chạy phiên bản LlamaCpp mới nhất", + "backendUpdateError": "Không thể kiểm tra cập nhật LlamaCpp", "changeLocation": "Thay đổi Vị trí", "copied": "Đã sao chép", "copyPath": "Sao chép Đường dẫn", @@ -244,5 +249,14 @@ "cancel": "Hủy", "changeLocation": "Thay đổi vị trí" } + }, + "backendUpdater": { + "newBackendVersion": "Phiên bản LlamaCpp mới {{version}}", + "backendUpdateAvailable": "Có cập nhật LlamaCpp", + "remindMeLater": "Nhắc tôi sau", + "updating": "Đang cập nhật...", + "updateNow": "Cập nhật ngay", + "updateSuccess": "Cập nhật LlamaCpp thành công", + "updateError": "Không thể cập nhật LlamaCpp" } } \ No newline at end of file diff --git a/web-app/src/locales/zh-CN/settings.json b/web-app/src/locales/zh-CN/settings.json index 810c986bd..33d8f374f 100644 --- a/web-app/src/locales/zh-CN/settings.json +++ b/web-app/src/locales/zh-CN/settings.json @@ -6,6 +6,11 @@ "noUpdateAvailable": "您正在运行最新版本", "devVersion": "检测到开发版本", "updateError": "检查更新失败", + "checkForBackendUpdates": "检查 LlamaCpp 更新", + "checkForBackendUpdatesDesc": "检查是否有更新的 LlamaCpp 后端版本。", + "checkingForBackendUpdates": "正在检查 LlamaCpp 更新...", + "noBackendUpdateAvailable": "您正在运行最新的 LlamaCpp 版本", + "backendUpdateError": "检查 LlamaCpp 更新失败", "changeLocation": "更改位置", "copied": "已复制", "copyPath": "复制路径", @@ -244,5 +249,14 @@ "cancel": "取消", "changeLocation": "更改位置" } + }, + "backendUpdater": { + "newBackendVersion": "新的 LlamaCpp 版本 {{version}}", + "backendUpdateAvailable": "LlamaCpp 更新可用", + "remindMeLater": "稍后提醒我", + "updating": "正在更新...", + "updateNow": "立即更新", + "updateSuccess": "LlamaCpp 更新成功", + "updateError": "更新 LlamaCpp 失败" } } \ No newline at end of file diff --git a/web-app/src/locales/zh-TW/settings.json b/web-app/src/locales/zh-TW/settings.json index 418524568..46dc02ad4 100644 --- a/web-app/src/locales/zh-TW/settings.json +++ b/web-app/src/locales/zh-TW/settings.json @@ -6,6 +6,11 @@ "noUpdateAvailable": "您正在運行最新版本", "devVersion": "檢測到開發版本", "updateError": "檢查更新失敗", + "checkForBackendUpdates": "檢查 LlamaCpp 更新", + "checkForBackendUpdatesDesc": "檢查是否有更新的 LlamaCpp 後端版本。", + "checkingForBackendUpdates": "正在檢查 LlamaCpp 更新...", + "noBackendUpdateAvailable": "您正在運行最新的 LlamaCpp 版本", + "backendUpdateError": "檢查 LlamaCpp 更新失敗", "changeLocation": "更改位置", "copied": "已複製", "copyPath": "複製路徑", @@ -244,5 +249,14 @@ "cancel": "取消", "changeLocation": "變更位置" } + }, + "backendUpdater": { + "newBackendVersion": "新的 LlamaCpp 版本 {{version}}", + "backendUpdateAvailable": "LlamaCpp 更新可用", + "remindMeLater": "稍後提醒我", + "updating": "正在更新...", + "updateNow": "立即更新", + "updateSuccess": "LlamaCpp 更新成功", + "updateError": "更新 LlamaCpp 失敗" } -} \ No newline at end of file +} diff --git a/web-app/src/routes/__root.tsx b/web-app/src/routes/__root.tsx index 13ed27813..f97e0c96d 100644 --- a/web-app/src/routes/__root.tsx +++ b/web-app/src/routes/__root.tsx @@ -3,6 +3,7 @@ import { createRootRoute, Outlet, useRouterState } from '@tanstack/react-router' import LeftPanel from '@/containers/LeftPanel' import DialogAppUpdater from '@/containers/dialogs/AppUpdater' +import BackendUpdater from '@/containers/dialogs/BackendUpdater' import { Fragment } from 'react/jsx-runtime' import { AppearanceProvider } from '@/providers/AppearanceProvider' import { ThemeProvider } from '@/providers/ThemeProvider' @@ -113,6 +114,7 @@ const AppLayout = () => { {/* Fake absolute panel top to enable window drag */}
+ {/* Use ResizablePanelGroup only on larger screens */} {!isSmallScreen && isLeftPanelOpen ? ( @@ -164,7 +166,9 @@ const AppLayout = () => {
)} - {PlatformFeatures[PlatformFeature.ANALYTICS] && productAnalyticPrompt && } + {PlatformFeatures[PlatformFeature.ANALYTICS] && productAnalyticPrompt && ( + + )} ) } diff --git a/web-app/src/routes/settings/providers/$providerName.tsx b/web-app/src/routes/settings/providers/$providerName.tsx index 5bcc3de5a..e68ef8fdd 100644 --- a/web-app/src/routes/settings/providers/$providerName.tsx +++ b/web-app/src/routes/settings/providers/$providerName.tsx @@ -27,7 +27,12 @@ import DeleteProvider from '@/containers/dialogs/DeleteProvider' import { useServiceHub } from '@/hooks/useServiceHub' import { localStorageKey } from '@/constants/localStorage' import { Button } from '@/components/ui/button' -import { IconFolderPlus, IconLoader, IconRefresh } from '@tabler/icons-react' +import { + IconFolderPlus, + IconLoader, + IconRefresh, + IconUpload, +} from '@tabler/icons-react' import { toast } from 'sonner' import { useCallback, useEffect, useState } from 'react' import { predefinedProviders } from '@/consts/providers' @@ -35,6 +40,7 @@ import { useModelLoad } from '@/hooks/useModelLoad' import { useLlamacppDevices } from '@/hooks/useLlamacppDevices' import { PlatformFeatures } from '@/lib/platform/const' import { PlatformFeature } from '@/lib/platform/types' +import { useBackendUpdater } from '@/hooks/useBackendUpdater' // as route.threadsDetail export const Route = createFileRoute('/settings/providers/$providerName')({ @@ -75,6 +81,10 @@ function ProviderDetail() { const [activeModels, setActiveModels] = useState([]) const [loadingModels, setLoadingModels] = useState([]) const [refreshingModels, setRefreshingModels] = useState(false) + const [isCheckingBackendUpdate, setIsCheckingBackendUpdate] = useState(false) + const [isInstallingBackend, setIsInstallingBackend] = useState(false) + const { checkForUpdate: checkForBackendUpdate, installBackend } = + useBackendUpdater() const { providerName } = useParams({ from: Route.id }) const { getProviderByName, setProviders, updateProvider } = useModelProvider() const provider = getProviderByName(providerName) @@ -310,6 +320,68 @@ function ProviderDetail() { }) } + const handleCheckForBackendUpdate = useCallback(async () => { + if (provider?.provider !== 'llamacpp') return + + setIsCheckingBackendUpdate(true) + try { + const update = await checkForBackendUpdate(true) + if (!update) { + toast.info(t('settings:noBackendUpdateAvailable')) + } + // If update is available, the BackendUpdater dialog will automatically show + } catch (error) { + console.error('Failed to check for backend updates:', error) + toast.error(t('settings:backendUpdateError')) + } finally { + setIsCheckingBackendUpdate(false) + } + }, [provider, checkForBackendUpdate, t]) + + const handleInstallBackendFromFile = useCallback(async () => { + if (provider?.provider !== 'llamacpp') return + + setIsInstallingBackend(true) + try { + // Open file dialog with filter for .tar.gz files + const selectedFile = await serviceHub.dialog().open({ + multiple: false, + directory: false, + filters: [ + { + name: 'Backend Archives', + extensions: ['tar.gz'], + }, + ], + }) + + if (selectedFile && typeof selectedFile === 'string') { + // Process the file path: replace spaces with dashes and convert to lowercase + const processedFilePath = selectedFile + .replace(/\s+/g, '-') + .toLowerCase() + + // Install the backend using the llamacpp extension + await installBackend(processedFilePath) + + toast.success(t('settings:backendInstallSuccess'), { + description: 'Backend installed successfully', + }) + + // Refresh settings to update backend configuration + await refreshSettings() + } + } catch (error) { + console.error('Failed to install backend from file:', error) + toast.error(t('settings:backendInstallError'), { + description: + error instanceof Error ? error.message : 'Unknown error occurred', + }) + } finally { + setIsInstallingBackend(false) + } + }, [provider, serviceHub, refreshSettings, t, installBackend]) + // Check if model provider settings are enabled for this platform if (!PlatformFeatures[PlatformFeature.MODEL_PROVIDER_SETTINGS]) { return ( @@ -525,6 +597,60 @@ function ProviderDetail() { is the recommended backend. )} + {setting.key === 'version_backend' && + provider?.provider === 'llamacpp' && ( +
+ + +
+ )} } actions={actionComponent}