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}