Merge pull request #6360 from menloresearch/feat/llamacpp_backend
feat: enhance llamacpp backend management and installation
This commit is contained in:
commit
7a94e74d6b
1
.gitignore
vendored
1
.gitignore
vendored
@ -54,6 +54,7 @@ docs/.next/
|
|||||||
## cargo
|
## cargo
|
||||||
target
|
target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
src-tauri/resources/
|
||||||
|
|
||||||
## test
|
## test
|
||||||
test-data
|
test-data
|
||||||
|
|||||||
@ -1,9 +1,90 @@
|
|||||||
import { getJanDataFolderPath, fs, joinPath, events } from '@janhq/core'
|
import { getJanDataFolderPath, fs, joinPath, events } from '@janhq/core'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { getProxyConfig } from './util'
|
import { getProxyConfig } from './util'
|
||||||
import { dirname } from '@tauri-apps/api/path'
|
import { dirname, basename } from '@tauri-apps/api/path'
|
||||||
import { getSystemInfo } from '@janhq/tauri-plugin-hardware-api'
|
import { getSystemInfo } from '@janhq/tauri-plugin-hardware-api'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Reads currently installed backends in janDataFolderPath
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export async function getLocalInstalledBackends(): Promise<
|
||||||
|
{ version: string; backend: string }[]
|
||||||
|
> {
|
||||||
|
const local: Array<{ version: string; backend: string }> = []
|
||||||
|
const janDataFolderPath = await getJanDataFolderPath()
|
||||||
|
const backendsDir = await joinPath([
|
||||||
|
janDataFolderPath,
|
||||||
|
'llamacpp',
|
||||||
|
'backends',
|
||||||
|
])
|
||||||
|
if (await fs.existsSync(backendsDir)) {
|
||||||
|
const versionDirs = await fs.readdirSync(backendsDir)
|
||||||
|
|
||||||
|
// If the folder does not exist we are done.
|
||||||
|
if (!versionDirs) {
|
||||||
|
return local
|
||||||
|
}
|
||||||
|
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
|
||||||
|
for (const backendType of backendTypes) {
|
||||||
|
const backendName = await basename(backendType)
|
||||||
|
if (await isBackendInstalled(backendType, versionName)) {
|
||||||
|
local.push({ version: versionName, backend: backendName })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.debug(local)
|
||||||
|
return local
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* currently reads available backends in remote
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async function fetchRemoteSupportedBackends(
|
||||||
|
supportedBackends: string[]
|
||||||
|
): Promise<{ version: string; backend: string }[]> {
|
||||||
|
// Pull the latest releases from the repo
|
||||||
|
const { releases } = await _fetchGithubReleases('menloresearch', 'llama.cpp')
|
||||||
|
releases.sort((a, b) => b.tag_name.localeCompare(a.tag_name))
|
||||||
|
releases.splice(10) // keep only the latest 10 releases
|
||||||
|
|
||||||
|
// Walk the assets and keep only those that match a supported backend
|
||||||
|
const remote: { version: string; backend: string }[] = []
|
||||||
|
|
||||||
|
for (const release of releases) {
|
||||||
|
const version = release.tag_name
|
||||||
|
const prefix = `llama-${version}-bin-`
|
||||||
|
|
||||||
|
for (const asset of release.assets) {
|
||||||
|
const name = asset.name
|
||||||
|
|
||||||
|
if (!name.startsWith(prefix)) continue
|
||||||
|
|
||||||
|
const backend = name.replace(prefix, '').replace('.tar.gz', '')
|
||||||
|
|
||||||
|
if (supportedBackends.includes(backend)) {
|
||||||
|
remote.push({ version, backend })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return remote
|
||||||
|
}
|
||||||
|
|
||||||
// folder structure
|
// folder structure
|
||||||
// <Jan's data folder>/llamacpp/backends/<backend_version>/<backend_type>
|
// <Jan's data folder>/llamacpp/backends/<backend_version>/<backend_type>
|
||||||
|
|
||||||
@ -76,31 +157,29 @@ export async function listSupportedBackends(): Promise<
|
|||||||
} else if (sysType === 'macos-aarch64' || sysType === 'macos-arm64') {
|
} else if (sysType === 'macos-aarch64' || sysType === 'macos-arm64') {
|
||||||
supportedBackends.push('macos-arm64')
|
supportedBackends.push('macos-arm64')
|
||||||
}
|
}
|
||||||
|
// get latest backends from Github
|
||||||
|
const remoteBackendVersions =
|
||||||
|
await fetchRemoteSupportedBackends(supportedBackends)
|
||||||
|
|
||||||
const { releases } = await _fetchGithubReleases('menloresearch', 'llama.cpp')
|
// Get locally installed versions
|
||||||
releases.sort((a, b) => b.tag_name.localeCompare(a.tag_name))
|
const localBackendVersions = await getLocalInstalledBackends()
|
||||||
releases.splice(10) // keep only the latest 10 releases
|
// Use a Map keyed by “${version}|${backend}” for O(1) deduplication.
|
||||||
|
const mergedMap = new Map<string, { version: string; backend: string }>()
|
||||||
let backendVersions = []
|
for (const entry of remoteBackendVersions) {
|
||||||
for (const release of releases) {
|
mergedMap.set(`${entry.version}|${entry.backend}`, entry)
|
||||||
const version = release.tag_name
|
}
|
||||||
const prefix = `llama-${version}-bin-`
|
for (const entry of localBackendVersions) {
|
||||||
|
mergedMap.set(`${entry.version}|${entry.backend}`, entry)
|
||||||
// NOTE: there is checksum.yml. we can also download it to verify the download
|
|
||||||
for (const asset of release.assets) {
|
|
||||||
const name = asset.name
|
|
||||||
if (!name.startsWith(prefix)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const backend = name.replace(prefix, '').replace('.tar.gz', '')
|
|
||||||
if (supportedBackends.includes(backend)) {
|
|
||||||
backendVersions.push({ version, backend })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return backendVersions
|
const merged = Array.from(mergedMap.values())
|
||||||
|
// Sort newest version first; if versions tie, sort by backend name
|
||||||
|
merged.sort((a, b) => {
|
||||||
|
const versionCmp = b.version.localeCompare(a.version)
|
||||||
|
return versionCmp !== 0 ? versionCmp : a.backend.localeCompare(b.backend)
|
||||||
|
})
|
||||||
|
|
||||||
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBackendDir(
|
export async function getBackendDir(
|
||||||
@ -279,9 +358,8 @@ async function _getSupportedFeatures() {
|
|||||||
if (compareVersions(driverVersion, minCuda12DriverVersion) >= 0)
|
if (compareVersions(driverVersion, minCuda12DriverVersion) >= 0)
|
||||||
features.cuda12 = true
|
features.cuda12 = true
|
||||||
}
|
}
|
||||||
// Vulkan support check - only discrete GPUs with 6GB+ VRAM
|
// Vulkan support check
|
||||||
if (gpuInfo.vulkan_info?.api_version && gpuInfo.total_memory >= 6 * 1024) {
|
if (gpuInfo.vulkan_info?.api_version) {
|
||||||
// 6GB (total_memory is in MB)
|
|
||||||
features.vulkan = true
|
features.vulkan = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -299,21 +377,23 @@ async function _fetchGithubReleases(
|
|||||||
const githubUrl = `https://api.github.com/repos/${owner}/${repo}/releases`
|
const githubUrl = `https://api.github.com/repos/${owner}/${repo}/releases`
|
||||||
try {
|
try {
|
||||||
const response = await fetch(githubUrl)
|
const response = await fetch(githubUrl)
|
||||||
if (!response.ok) throw new Error(`GitHub error: ${response.status} ${response.statusText}`)
|
if (!response.ok)
|
||||||
|
throw new Error(`GitHub error: ${response.status} ${response.statusText}`)
|
||||||
const releases = await response.json()
|
const releases = await response.json()
|
||||||
return { releases, source: 'github' }
|
return { releases, source: 'github' }
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
const cdnUrl = 'https://catalog.jan.ai/llama.cpp/releases/releases.json'
|
const cdnUrl = 'https://catalog.jan.ai/llama.cpp/releases/releases.json'
|
||||||
const response = await fetch(cdnUrl)
|
const response = await fetch(cdnUrl)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch releases from both sources. CDN error: ${response.status} ${response.statusText}`)
|
throw new Error(
|
||||||
|
`Failed to fetch releases from both sources. CDN error: ${response.status} ${response.statusText}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const releases = await response.json()
|
const releases = await response.json()
|
||||||
return { releases, source: 'cdn' }
|
return { releases, source: 'cdn' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function _isCudaInstalled(version: string): Promise<boolean> {
|
async function _isCudaInstalled(version: string): Promise<boolean> {
|
||||||
const sysInfo = await getSystemInfo()
|
const sysInfo = await getSystemInfo()
|
||||||
const os_type = sysInfo.os_type
|
const os_type = sysInfo.os_type
|
||||||
|
|||||||
@ -31,12 +31,16 @@ import {
|
|||||||
downloadBackend,
|
downloadBackend,
|
||||||
isBackendInstalled,
|
isBackendInstalled,
|
||||||
getBackendExePath,
|
getBackendExePath,
|
||||||
|
getBackendDir,
|
||||||
} from './backend'
|
} from './backend'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { getProxyConfig } from './util'
|
import { getProxyConfig } from './util'
|
||||||
import { basename } from '@tauri-apps/api/path'
|
import { basename } from '@tauri-apps/api/path'
|
||||||
import { readGgufMetadata } from '@janhq/tauri-plugin-llamacpp-api'
|
import {
|
||||||
import { getSystemUsage } from '@janhq/tauri-plugin-hardware-api'
|
GgufMetadata,
|
||||||
|
readGgufMetadata,
|
||||||
|
} from '@janhq/tauri-plugin-llamacpp-api'
|
||||||
|
import { getSystemUsage, getSystemInfo } from '@janhq/tauri-plugin-hardware-api'
|
||||||
|
|
||||||
type LlamacppConfig = {
|
type LlamacppConfig = {
|
||||||
version_backend: string
|
version_backend: string
|
||||||
@ -321,10 +325,10 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
// Clear the invalid stored preference
|
// Clear the invalid stored preference
|
||||||
this.clearStoredBackendType()
|
this.clearStoredBackendType()
|
||||||
bestAvailableBackendString =
|
bestAvailableBackendString =
|
||||||
this.determineBestBackend(version_backends)
|
await this.determineBestBackend(version_backends)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bestAvailableBackendString = this.determineBestBackend(version_backends)
|
bestAvailableBackendString = await this.determineBestBackend(version_backends)
|
||||||
}
|
}
|
||||||
|
|
||||||
let settings = structuredClone(SETTINGS)
|
let settings = structuredClone(SETTINGS)
|
||||||
@ -486,23 +490,52 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private determineBestBackend(
|
private async determineBestBackend(
|
||||||
version_backends: { version: string; backend: string }[]
|
version_backends: { version: string; backend: string }[]
|
||||||
): string {
|
): Promise<string> {
|
||||||
if (version_backends.length === 0) return ''
|
if (version_backends.length === 0) return ''
|
||||||
|
|
||||||
|
// Check GPU memory availability
|
||||||
|
let hasEnoughGpuMemory = false
|
||||||
|
try {
|
||||||
|
const sysInfo = await getSystemInfo()
|
||||||
|
for (const gpuInfo of sysInfo.gpus) {
|
||||||
|
if (gpuInfo.total_memory >= 6 * 1024) {
|
||||||
|
hasEnoughGpuMemory = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to get system info for GPU memory check:', error)
|
||||||
|
// Default to false if we can't determine GPU memory
|
||||||
|
hasEnoughGpuMemory = false
|
||||||
|
}
|
||||||
|
|
||||||
// Priority list for backend types (more specific/performant ones first)
|
// Priority list for backend types (more specific/performant ones first)
|
||||||
const backendPriorities: string[] = [
|
// Vulkan will be conditionally prioritized based on GPU memory
|
||||||
'cuda-cu12.0',
|
const backendPriorities: string[] = hasEnoughGpuMemory
|
||||||
'cuda-cu11.7',
|
? [
|
||||||
'vulkan',
|
'cuda-cu12.0',
|
||||||
'avx512',
|
'cuda-cu11.7',
|
||||||
'avx2',
|
'vulkan', // Include vulkan if we have enough GPU memory
|
||||||
'avx',
|
'avx512',
|
||||||
'noavx',
|
'avx2',
|
||||||
'arm64',
|
'avx',
|
||||||
'x64',
|
'noavx',
|
||||||
]
|
'arm64',
|
||||||
|
'x64',
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'cuda-cu12.0',
|
||||||
|
'cuda-cu11.7',
|
||||||
|
'avx512',
|
||||||
|
'avx2',
|
||||||
|
'avx',
|
||||||
|
'noavx',
|
||||||
|
'arm64',
|
||||||
|
'x64',
|
||||||
|
'vulkan', // demote to last if we don't have enough memory
|
||||||
|
]
|
||||||
|
|
||||||
// Helper to map backend string to a priority category
|
// Helper to map backend string to a priority category
|
||||||
const getBackendCategory = (backendString: string): string | undefined => {
|
const getBackendCategory = (backendString: string): string | undefined => {
|
||||||
@ -543,10 +576,83 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
return `${foundBestBackend.version}/${foundBestBackend.backend}`
|
return `${foundBestBackend.version}/${foundBestBackend.backend}`
|
||||||
} else {
|
} else {
|
||||||
// Fallback to newest version
|
// Fallback to newest version
|
||||||
|
logger.info(
|
||||||
|
`Fallback to: ${version_backends[0].version}/${version_backends[0].backend}`
|
||||||
|
)
|
||||||
return `${version_backends[0].version}/${version_backends[0].backend}`
|
return `${version_backends[0].version}/${version_backends[0].backend}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateBackend(
|
||||||
|
targetBackendString: string
|
||||||
|
): Promise<{ wasUpdated: boolean; newBackend: string }> {
|
||||||
|
try {
|
||||||
|
if (!targetBackendString)
|
||||||
|
throw new Error(
|
||||||
|
`Invalid backend string: ${targetBackendString} supplied to update function`
|
||||||
|
)
|
||||||
|
|
||||||
|
const [version, backend] = targetBackendString.split('/')
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Updating backend to ${targetBackendString} (backend type: ${backend})`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Download new backend
|
||||||
|
await this.ensureBackendReady(backend, version)
|
||||||
|
|
||||||
|
// Add delay on Windows
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update configuration
|
||||||
|
this.config.version_backend = targetBackendString
|
||||||
|
|
||||||
|
// Store the backend type preference only if it changed
|
||||||
|
const currentStoredBackend = this.getStoredBackendType()
|
||||||
|
if (currentStoredBackend !== backend) {
|
||||||
|
this.setStoredBackendType(backend)
|
||||||
|
logger.info(`Updated stored backend type preference: ${backend}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update settings
|
||||||
|
const settings = await this.getSettings()
|
||||||
|
await this.updateSettings(
|
||||||
|
settings.map((item) => {
|
||||||
|
if (item.key === 'version_backend') {
|
||||||
|
item.controllerProps.value = targetBackendString
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(`Successfully updated to backend: ${targetBackendString}`)
|
||||||
|
|
||||||
|
// Emit for updating frontend
|
||||||
|
if (events && typeof events.emit === 'function') {
|
||||||
|
logger.info(
|
||||||
|
`Emitting settingsChanged event for version_backend with value: ${targetBackendString}`
|
||||||
|
)
|
||||||
|
events.emit('settingsChanged', {
|
||||||
|
key: 'version_backend',
|
||||||
|
value: targetBackendString,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up old versions of the same backend type
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
}
|
||||||
|
await this.removeOldBackend(version, backend)
|
||||||
|
|
||||||
|
return { wasUpdated: true, newBackend: targetBackendString }
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Backend update failed:', error)
|
||||||
|
return { wasUpdated: false, newBackend: this.config.version_backend }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async handleAutoUpdate(
|
private async handleAutoUpdate(
|
||||||
bestAvailableBackendString: string
|
bestAvailableBackendString: string
|
||||||
): Promise<{ wasUpdated: boolean; newBackend: string }> {
|
): Promise<{ wasUpdated: boolean; newBackend: string }> {
|
||||||
@ -571,46 +677,8 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
logger.info(
|
logger.info(
|
||||||
'No valid backend currently selected, using best available backend'
|
'No valid backend currently selected, using best available backend'
|
||||||
)
|
)
|
||||||
try {
|
|
||||||
const [bestVersion, bestBackend] = bestAvailableBackendString.split('/')
|
|
||||||
|
|
||||||
// Download new backend
|
return await this.updateBackend(bestAvailableBackendString)
|
||||||
await this.ensureBackendReady(bestBackend, bestVersion)
|
|
||||||
|
|
||||||
// Add delay on Windows
|
|
||||||
if (IS_WINDOWS) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update configuration
|
|
||||||
this.config.version_backend = bestAvailableBackendString
|
|
||||||
|
|
||||||
// Store the backend type preference only if it changed
|
|
||||||
const currentStoredBackend = this.getStoredBackendType()
|
|
||||||
if (currentStoredBackend !== bestBackend) {
|
|
||||||
this.setStoredBackendType(bestBackend)
|
|
||||||
logger.info(`Stored new backend type preference: ${bestBackend}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update settings
|
|
||||||
const settings = await this.getSettings()
|
|
||||||
await this.updateSettings(
|
|
||||||
settings.map((item) => {
|
|
||||||
if (item.key === 'version_backend') {
|
|
||||||
item.controllerProps.value = bestAvailableBackendString
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`Successfully set initial backend: ${bestAvailableBackendString}`
|
|
||||||
)
|
|
||||||
return { wasUpdated: true, newBackend: bestAvailableBackendString }
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to set initial backend:', error)
|
|
||||||
return { wasUpdated: false, newBackend: this.config.version_backend }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse current backend configuration
|
// Parse current backend configuration
|
||||||
@ -650,65 +718,54 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Perform version update for the same backend type
|
// Perform version update for the same backend type
|
||||||
try {
|
logger.info(
|
||||||
|
`Auto-updating from ${this.config.version_backend} to ${targetBackendString} (preserving backend type)`
|
||||||
|
)
|
||||||
|
|
||||||
|
return await this.updateBackend(targetBackendString)
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseBackendVersion(v: string): number {
|
||||||
|
// Remove any leading non‑digit characters (e.g. the "b")
|
||||||
|
const numeric = v.replace(/^[^\d]*/, '')
|
||||||
|
const n = Number(numeric)
|
||||||
|
return Number.isNaN(n) ? 0 : n
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkBackendForUpdates(): Promise<{
|
||||||
|
updateNeeded: boolean
|
||||||
|
newVersion: string
|
||||||
|
}> {
|
||||||
|
// Parse current backend configuration
|
||||||
|
const [currentVersion, currentBackend] = (
|
||||||
|
this.config.version_backend || ''
|
||||||
|
).split('/')
|
||||||
|
|
||||||
|
if (!currentVersion || !currentBackend) {
|
||||||
|
logger.warn(
|
||||||
|
`Invalid current backend format: ${this.config.version_backend}`
|
||||||
|
)
|
||||||
|
return { updateNeeded: false, newVersion: '0' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the latest version for the currently selected backend type
|
||||||
|
const version_backends = await listSupportedBackends()
|
||||||
|
const targetBackendString = this.findLatestVersionForBackend(
|
||||||
|
version_backends,
|
||||||
|
currentBackend
|
||||||
|
)
|
||||||
|
const [latestVersion] = targetBackendString.split('/')
|
||||||
|
if (
|
||||||
|
this.parseBackendVersion(latestVersion) >
|
||||||
|
this.parseBackendVersion(currentVersion)
|
||||||
|
) {
|
||||||
|
logger.info(`New update available: ${latestVersion}`)
|
||||||
|
return { updateNeeded: true, newVersion: latestVersion }
|
||||||
|
} else {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Auto-updating from ${this.config.version_backend} to ${targetBackendString} (preserving backend type)`
|
`Already at latest version: ${currentVersion} = ${latestVersion}`
|
||||||
)
|
)
|
||||||
|
return { updateNeeded: false, newVersion: '0' }
|
||||||
// Download new version of the same backend type
|
|
||||||
await this.ensureBackendReady(currentBackend, latestVersion)
|
|
||||||
|
|
||||||
// Add delay on Windows
|
|
||||||
if (IS_WINDOWS) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update configuration
|
|
||||||
this.config.version_backend = targetBackendString
|
|
||||||
|
|
||||||
// Update stored backend type preference only if it changed
|
|
||||||
const currentStoredBackend = this.getStoredBackendType()
|
|
||||||
if (currentStoredBackend !== currentBackend) {
|
|
||||||
this.setStoredBackendType(currentBackend)
|
|
||||||
logger.info(`Updated stored backend type preference: ${currentBackend}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update settings
|
|
||||||
const settings = await this.getSettings()
|
|
||||||
await this.updateSettings(
|
|
||||||
settings.map((item) => {
|
|
||||||
if (item.key === 'version_backend') {
|
|
||||||
item.controllerProps.value = targetBackendString
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`Successfully updated to backend: ${targetBackendString} (preserved backend type: ${currentBackend})`
|
|
||||||
)
|
|
||||||
|
|
||||||
// Emit for updating fe
|
|
||||||
if (events && typeof events.emit === 'function') {
|
|
||||||
logger.info(
|
|
||||||
`Emitting settingsChanged event for version_backend with value: ${targetBackendString}`
|
|
||||||
)
|
|
||||||
events.emit('settingsChanged', {
|
|
||||||
key: 'version_backend',
|
|
||||||
value: targetBackendString,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up old versions of the same backend type
|
|
||||||
if (IS_WINDOWS) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
||||||
}
|
|
||||||
await this.removeOldBackend(latestVersion, currentBackend)
|
|
||||||
|
|
||||||
return { wasUpdated: true, newBackend: targetBackendString }
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Auto-update failed:', error)
|
|
||||||
return { wasUpdated: false, newBackend: this.config.version_backend }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1016,6 +1073,52 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
localStorage.setItem('cortex_models_migrated', 'true')
|
localStorage.setItem('cortex_models_migrated', 'true')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Manually installs a supported backend archive
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async installBackend(path: string): Promise<void> {
|
||||||
|
const platformName = IS_WINDOWS ? 'win' : 'linux'
|
||||||
|
const re = /^llama-(b\d+)-bin-(.+?)\.tar\.gz$/
|
||||||
|
const archiveName = await basename(path)
|
||||||
|
logger.info(`Installing backend from path: ${path}`)
|
||||||
|
|
||||||
|
if (!(await fs.existsSync(path)) || !path.endsWith('tar.gz')) {
|
||||||
|
logger.error(`Invalid path or file ${path}`)
|
||||||
|
throw new Error(`Invalid path or file ${path}`)
|
||||||
|
}
|
||||||
|
const match = re.exec(archiveName)
|
||||||
|
if (!match) throw new Error('Failed to parse archive name')
|
||||||
|
const [, version, backend] = match
|
||||||
|
if (!version && !backend) {
|
||||||
|
throw new Error(`Invalid backend archive name: ${archiveName}`)
|
||||||
|
}
|
||||||
|
const backendDir = await getBackendDir(backend, version)
|
||||||
|
try {
|
||||||
|
await invoke('decompress', { path: path, outputDir: backendDir })
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Failed to install: ${String(e)}`)
|
||||||
|
}
|
||||||
|
const binPath =
|
||||||
|
platformName === 'win'
|
||||||
|
? await joinPath([backendDir, 'build', 'bin', 'llama-server.exe'])
|
||||||
|
: await joinPath([backendDir, 'build', 'bin', 'llama-server'])
|
||||||
|
|
||||||
|
if (!fs.existsSync(binPath)) {
|
||||||
|
await fs.rm(backendDir)
|
||||||
|
throw new Error('Not a supported backend archive!')
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.configureBackends()
|
||||||
|
logger.info(`Backend ${backend}/${version} installed and UI refreshed`)
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Backend installed but failed to refresh UI', e)
|
||||||
|
throw new Error(
|
||||||
|
`Backend installed but failed to refresh UI: ${String(e)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override async import(modelId: string, opts: ImportOptions): Promise<void> {
|
override async import(modelId: string, opts: ImportOptions): Promise<void> {
|
||||||
const isValidModelId = (id: string) => {
|
const isValidModelId = (id: string) => {
|
||||||
// only allow alphanumeric, underscore, hyphen, and dot characters in modelId
|
// only allow alphanumeric, underscore, hyphen, and dot characters in modelId
|
||||||
@ -1438,7 +1541,11 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
|
|
||||||
// Boolean flags
|
// Boolean flags
|
||||||
if (!cfg.ctx_shift) args.push('--no-context-shift')
|
if (!cfg.ctx_shift) args.push('--no-context-shift')
|
||||||
if (cfg.flash_attn) args.push('--flash-attn')
|
if (Number(version.replace(/^b/, '')) >= 6325) {
|
||||||
|
if (!cfg.flash_attn) args.push('--flash-attn', 'off') //default: auto = ON when supported
|
||||||
|
} else {
|
||||||
|
if (cfg.flash_attn) args.push('--flash-attn')
|
||||||
|
}
|
||||||
if (cfg.cont_batching) args.push('--cont-batching')
|
if (cfg.cont_batching) args.push('--cont-batching')
|
||||||
args.push('--no-mmap')
|
args.push('--no-mmap')
|
||||||
if (cfg.mlock) args.push('--mlock')
|
if (cfg.mlock) args.push('--mlock')
|
||||||
@ -1901,7 +2008,7 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
const totalMemory = totalVRAM + totalRAM
|
const totalMemory = totalVRAM + totalRAM
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Total VRAM: ${totalVRAM} bytes, Total RAM: ${totalRAM} bytes, Free: ${usableRAM} bytes, Total Memory: ${totalMemory} bytes`
|
`Total VRAM: ${totalVRAM} bytes, Total RAM: ${totalRAM} bytes, Total Memory: ${totalMemory} bytes`
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -2416,9 +2523,7 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
logger.error('Failed to validate GGUF file:', error)
|
logger.error('Failed to validate GGUF file:', error)
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
error: `Failed to read model metadata: ${
|
error: `Failed to read model metadata: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
error instanceof Error ? error.message : 'Unknown error'
|
|
||||||
}`,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@ -2325,6 +2325,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"url",
|
"url",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -5188,7 +5189,6 @@ dependencies = [
|
|||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
"windows-sys 0.60.2",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@ -25,10 +25,6 @@ thiserror = "2.0.12"
|
|||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
reqwest = { version = "0.11", features = ["json", "blocking", "stream"] }
|
reqwest = { version = "0.11", features = ["json", "blocking", "stream"] }
|
||||||
|
|
||||||
# Windows-specific dependencies
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
|
||||||
windows-sys = { version = "0.60.2", features = ["Win32_Storage_FileSystem"] }
|
|
||||||
|
|
||||||
# Unix-specific dependencies
|
# Unix-specific dependencies
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
nix = { version = "=0.30.1", features = ["signal", "process"] }
|
nix = { version = "=0.30.1", features = ["signal", "process"] }
|
||||||
|
|||||||
@ -3,31 +3,7 @@ use std::path::PathBuf;
|
|||||||
use crate::error::{ErrorCode, LlamacppError, ServerResult};
|
use crate::error::{ErrorCode, LlamacppError, ServerResult};
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use std::os::windows::ffi::OsStrExt;
|
use jan_utils::path::get_short_path;
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
use std::ffi::OsStr;
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
use windows_sys::Win32::Storage::FileSystem::GetShortPathNameW;
|
|
||||||
|
|
||||||
/// Get Windows short path to avoid issues with spaces and special characters
|
|
||||||
#[cfg(windows)]
|
|
||||||
pub fn get_short_path<P: AsRef<std::path::Path>>(path: P) -> Option<String> {
|
|
||||||
let wide: Vec<u16> = OsStr::new(path.as_ref())
|
|
||||||
.encode_wide()
|
|
||||||
.chain(Some(0))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut buffer = vec![0u16; 260];
|
|
||||||
let len = unsafe { GetShortPathNameW(wide.as_ptr(), buffer.as_mut_ptr(), buffer.len() as u32) };
|
|
||||||
|
|
||||||
if len > 0 {
|
|
||||||
Some(String::from_utf16_lossy(&buffer[..len as usize]))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate that a binary path exists and is accessible
|
/// Validate that a binary path exists and is accessible
|
||||||
pub fn validate_binary_path(backend_path: &str) -> ServerResult<PathBuf> {
|
pub fn validate_binary_path(backend_path: &str) -> ServerResult<PathBuf> {
|
||||||
@ -259,18 +235,6 @@ mod tests {
|
|||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
#[test]
|
|
||||||
fn test_get_short_path() {
|
|
||||||
// Test with a real path that should exist on Windows
|
|
||||||
use std::env;
|
|
||||||
if let Ok(temp_dir) = env::var("TEMP") {
|
|
||||||
let result = get_short_path(&temp_dir);
|
|
||||||
// Should return some short path or None (both are valid)
|
|
||||||
// We can't assert the exact value as it depends on the system
|
|
||||||
println!("Short path result: {:?}", result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_model_path_multiple_m_flags() {
|
fn test_validate_model_path_multiple_m_flags() {
|
||||||
|
|||||||
@ -165,13 +165,6 @@ pub fn read_yaml(app: tauri::AppHandle, path: &str) -> Result<serde_json::Value,
|
|||||||
pub fn decompress(app: tauri::AppHandle, path: &str, output_dir: &str) -> Result<(), String> {
|
pub fn decompress(app: tauri::AppHandle, path: &str, output_dir: &str) -> Result<(), String> {
|
||||||
let jan_data_folder = crate::core::app::commands::get_jan_data_folder_path(app.clone());
|
let jan_data_folder = crate::core::app::commands::get_jan_data_folder_path(app.clone());
|
||||||
let path_buf = jan_utils::normalize_path(&jan_data_folder.join(path));
|
let path_buf = jan_utils::normalize_path(&jan_data_folder.join(path));
|
||||||
if !path_buf.starts_with(&jan_data_folder) {
|
|
||||||
return Err(format!(
|
|
||||||
"Error: path {} is not under jan_data_folder {}",
|
|
||||||
path_buf.to_string_lossy(),
|
|
||||||
jan_data_folder.to_string_lossy(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let output_dir_buf = jan_utils::normalize_path(&jan_data_folder.join(output_dir));
|
let output_dir_buf = jan_utils::normalize_path(&jan_data_folder.join(output_dir));
|
||||||
if !output_dir_buf.starts_with(&jan_data_folder) {
|
if !output_dir_buf.starts_with(&jan_data_folder) {
|
||||||
@ -191,6 +184,17 @@ pub fn decompress(app: tauri::AppHandle, path: &str, output_dir: &str) -> Result
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
// Use short path on Windows to handle paths with spaces
|
||||||
|
#[cfg(windows)]
|
||||||
|
let file = {
|
||||||
|
if let Some(short_path) = jan_utils::path::get_short_path(&path_buf) {
|
||||||
|
fs::File::open(&short_path).map_err(|e| e.to_string())?
|
||||||
|
} else {
|
||||||
|
fs::File::open(&path_buf).map_err(|e| e.to_string())?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
let file = fs::File::open(&path_buf).map_err(|e| e.to_string())?;
|
let file = fs::File::open(&path_buf).map_err(|e| e.to_string())?;
|
||||||
if path.ends_with(".tar.gz") {
|
if path.ends_with(".tar.gz") {
|
||||||
let tar = flate2::read::GzDecoder::new(file);
|
let tar = flate2::read::GzDecoder::new(file);
|
||||||
|
|||||||
@ -16,6 +16,9 @@ tokio = { version = "1", features = ["process", "fs", "macros", "rt"] }
|
|||||||
tokio-util = "0.7.14"
|
tokio-util = "0.7.14"
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
windows-sys = { version = "0.60.2", features = ["Win32_Storage_FileSystem"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.0"
|
tempfile = "3.0"
|
||||||
|
|
||||||
|
|||||||
@ -1,76 +1,121 @@
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use std::path::Prefix;
|
use std::path::Prefix;
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
|
||||||
/// Normalizes file paths by handling path components, prefixes, and resolving relative paths
|
#[cfg(windows)]
|
||||||
/// Based on: https://github.com/rust-lang/cargo/blob/rust-1.67.0/crates/cargo-util/src/paths.rs#L82-L107
|
use std::os::windows::ffi::OsStrExt;
|
||||||
pub fn normalize_path(path: &Path) -> PathBuf {
|
|
||||||
let mut components = path.components().peekable();
|
#[cfg(windows)]
|
||||||
let mut ret = if let Some(c @ Component::Prefix(_prefix_component)) = components.peek().cloned()
|
use std::ffi::OsStr;
|
||||||
{
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
// Remove only the Verbatim prefix, but keep the drive letter (e.g., C:\)
|
use windows_sys::Win32::Storage::FileSystem::GetShortPathNameW;
|
||||||
match _prefix_component.kind() {
|
|
||||||
Prefix::VerbatimDisk(disk) => {
|
/// Normalizes file paths by handling path components, prefixes, and resolving relative paths
|
||||||
components.next(); // skip this prefix
|
/// Based on: https://github.com/rust-lang/cargo/blob/rust-1.67.0/crates/cargo-util/src/paths.rs#L82-L107
|
||||||
// Re-add the disk prefix (e.g., C:)
|
pub fn normalize_path(path: &Path) -> PathBuf {
|
||||||
let mut pb = PathBuf::new();
|
let mut components = path.components().peekable();
|
||||||
pb.push(format!("{}:", disk as char));
|
let mut ret = if let Some(c @ Component::Prefix(_prefix_component)) = components.peek().cloned()
|
||||||
pb
|
{
|
||||||
}
|
#[cfg(windows)]
|
||||||
Prefix::Verbatim(_) | Prefix::VerbatimUNC(_, _) => {
|
// Remove only the Verbatim prefix, but keep the drive letter (e.g., C:\)
|
||||||
components.next(); // skip this prefix
|
match _prefix_component.kind() {
|
||||||
PathBuf::new()
|
Prefix::VerbatimDisk(disk) => {
|
||||||
}
|
components.next(); // skip this prefix
|
||||||
_ => {
|
// Re-add the disk prefix (e.g., C:)
|
||||||
components.next();
|
let mut pb = PathBuf::new();
|
||||||
PathBuf::from(c.as_os_str())
|
pb.push(format!("{}:", disk as char));
|
||||||
}
|
pb
|
||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
Prefix::Verbatim(_) | Prefix::VerbatimUNC(_, _) => {
|
||||||
{
|
components.next(); // skip this prefix
|
||||||
components.next(); // skip this prefix
|
PathBuf::new()
|
||||||
PathBuf::from(c.as_os_str())
|
}
|
||||||
}
|
_ => {
|
||||||
} else {
|
components.next();
|
||||||
PathBuf::new()
|
PathBuf::from(c.as_os_str())
|
||||||
};
|
}
|
||||||
|
}
|
||||||
for component in components {
|
#[cfg(not(windows))]
|
||||||
match component {
|
{
|
||||||
Component::Prefix(..) => unreachable!(),
|
components.next(); // skip this prefix
|
||||||
Component::RootDir => {
|
PathBuf::from(c.as_os_str())
|
||||||
ret.push(component.as_os_str());
|
}
|
||||||
}
|
} else {
|
||||||
Component::CurDir => {}
|
PathBuf::new()
|
||||||
Component::ParentDir => {
|
};
|
||||||
ret.pop();
|
|
||||||
}
|
for component in components {
|
||||||
Component::Normal(c) => {
|
match component {
|
||||||
ret.push(c);
|
Component::Prefix(..) => unreachable!(),
|
||||||
}
|
Component::RootDir => {
|
||||||
}
|
ret.push(component.as_os_str());
|
||||||
}
|
}
|
||||||
ret
|
Component::CurDir => {}
|
||||||
}
|
Component::ParentDir => {
|
||||||
|
ret.pop();
|
||||||
/// Removes file:/ and file:\ prefixes from file paths
|
}
|
||||||
pub fn normalize_file_path(path: &str) -> String {
|
Component::Normal(c) => {
|
||||||
path.replace("file:/", "").replace("file:\\", "")
|
ret.push(c);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/// Removes prefix from path string with proper formatting
|
}
|
||||||
pub fn remove_prefix(path: &str, prefix: &str) -> String {
|
ret
|
||||||
if !prefix.is_empty() && path.starts_with(prefix) {
|
}
|
||||||
let result = path[prefix.len()..].to_string();
|
|
||||||
if result.is_empty() {
|
/// Removes file:/ and file:\ prefixes from file paths
|
||||||
"/".to_string()
|
pub fn normalize_file_path(path: &str) -> String {
|
||||||
} else if result.starts_with('/') {
|
path.replace("file:/", "").replace("file:\\", "")
|
||||||
result
|
}
|
||||||
} else {
|
|
||||||
format!("/{}", result)
|
/// Removes prefix from path string with proper formatting
|
||||||
}
|
pub fn remove_prefix(path: &str, prefix: &str) -> String {
|
||||||
} else {
|
if !prefix.is_empty() && path.starts_with(prefix) {
|
||||||
path.to_string()
|
let result = path[prefix.len()..].to_string();
|
||||||
}
|
if result.is_empty() {
|
||||||
}
|
"/".to_string()
|
||||||
|
} else if result.starts_with('/') {
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
format!("/{}", result)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Windows short path to avoid issues with spaces and special characters
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn get_short_path<P: AsRef<std::path::Path>>(path: P) -> Option<String> {
|
||||||
|
let wide: Vec<u16> = OsStr::new(path.as_ref())
|
||||||
|
.encode_wide()
|
||||||
|
.chain(Some(0))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut buffer = vec![0u16; 260];
|
||||||
|
let len = unsafe { GetShortPathNameW(wide.as_ptr(), buffer.as_mut_ptr(), buffer.len() as u32) };
|
||||||
|
|
||||||
|
if len > 0 {
|
||||||
|
Some(String::from_utf16_lossy(&buffer[..len as usize]))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[test]
|
||||||
|
fn test_get_short_path() {
|
||||||
|
// Test with a real path that should exist on Windows
|
||||||
|
use std::env;
|
||||||
|
if let Ok(temp_dir) = env::var("TEMP") {
|
||||||
|
let result = get_short_path(&temp_dir);
|
||||||
|
// Should return some short path or None (both are valid)
|
||||||
|
// We can't assert the exact value as it depends on the system
|
||||||
|
println!("Short path result: {:?}", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
104
web-app/src/containers/dialogs/BackendUpdater.tsx
Normal file
104
web-app/src/containers/dialogs/BackendUpdater.tsx
Normal file
@ -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 && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'fixed z-50 min-w-[300px] bottom-3 right-3 bg-main-view text-main-view-fg flex items-center justify-center border border-main-view-fg/10 rounded-lg shadow-md'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="px-0 py-4">
|
||||||
|
<div className="px-4">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<IconDownload
|
||||||
|
size={20}
|
||||||
|
className="shrink-0 text-main-view-fg/60 mt-1"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="text-base font-medium">
|
||||||
|
{t('settings:backendUpdater.newBackendVersion', {
|
||||||
|
version: updateState.updateInfo?.newVersion,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-main-view-fg/70 font-normal mb-2">
|
||||||
|
{t('settings:backendUpdater.backendUpdateAvailable')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-3 px-4">
|
||||||
|
<div className="flex gap-x-4 w-full items-center justify-end">
|
||||||
|
<div className="flex gap-x-5">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="px-0 text-main-view-fg/70 remind-me-later"
|
||||||
|
onClick={() => setRemindMeLater(true)}
|
||||||
|
>
|
||||||
|
{t('settings:backendUpdater.remindMeLater')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleUpdate}
|
||||||
|
disabled={updateState.isUpdating}
|
||||||
|
>
|
||||||
|
{updateState.isUpdating
|
||||||
|
? t('settings:backendUpdater.updating')
|
||||||
|
: t('settings:backendUpdater.updateNow')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BackendUpdater
|
||||||
352
web-app/src/hooks/useBackendUpdater.ts
Normal file
352
web-app/src/hooks/useBackendUpdater.ts
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
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<ExtensionSetting[]>
|
||||||
|
checkBackendForUpdates?(): Promise<BackendUpdateInfo>
|
||||||
|
updateBackend?(
|
||||||
|
targetBackend: string
|
||||||
|
): Promise<{ wasUpdated: boolean; newBackend: string }>
|
||||||
|
installBackend?(filePath: string): Promise<void>
|
||||||
|
configureBackends?(): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackendUpdateState {
|
||||||
|
isUpdateAvailable: boolean
|
||||||
|
updateInfo: BackendUpdateInfo | null
|
||||||
|
isUpdating: boolean
|
||||||
|
remindMeLater: boolean
|
||||||
|
autoUpdateEnabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBackendUpdater = () => {
|
||||||
|
const [updateState, setUpdateState] = useState<BackendUpdateState>({
|
||||||
|
isUpdateAvailable: false,
|
||||||
|
updateInfo: null,
|
||||||
|
isUpdating: false,
|
||||||
|
remindMeLater: false,
|
||||||
|
autoUpdateEnabled: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Listen for backend update state sync events
|
||||||
|
useEffect(() => {
|
||||||
|
const handleUpdateStateSync = (newState: Partial<BackendUpdateState>) => {
|
||||||
|
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<BackendUpdateState>) => {
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Refresh backend list to update UI
|
||||||
|
await extension.configureBackends?.()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error installing backend:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateState,
|
||||||
|
checkForUpdate,
|
||||||
|
updateBackend,
|
||||||
|
setRemindMeLater,
|
||||||
|
installBackend,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,11 @@
|
|||||||
"noUpdateAvailable": "Du verwendest die neueste Version",
|
"noUpdateAvailable": "Du verwendest die neueste Version",
|
||||||
"devVersion": "Entwicklungsversion erkannt",
|
"devVersion": "Entwicklungsversion erkannt",
|
||||||
"updateError": "Fehler beim Suchen nach Updates",
|
"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",
|
"changeLocation": "Ort ändern",
|
||||||
"copied": "Kopiert",
|
"copied": "Kopiert",
|
||||||
"copyPath": "Pfad kopieren",
|
"copyPath": "Pfad kopieren",
|
||||||
@ -244,5 +249,14 @@
|
|||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"changeLocation": "Ort ändern"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,11 @@
|
|||||||
"noUpdateAvailable": "You're running the latest version",
|
"noUpdateAvailable": "You're running the latest version",
|
||||||
"devVersion": "Development version detected",
|
"devVersion": "Development version detected",
|
||||||
"updateError": "Failed to check for updates",
|
"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",
|
"changeLocation": "Change Location",
|
||||||
"copied": "Copied",
|
"copied": "Copied",
|
||||||
"copyPath": "Copy Path",
|
"copyPath": "Copy Path",
|
||||||
@ -249,5 +254,16 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"changeLocation": "Change Location"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,11 @@
|
|||||||
"noUpdateAvailable": "Anda menjalankan versi terbaru",
|
"noUpdateAvailable": "Anda menjalankan versi terbaru",
|
||||||
"devVersion": "Versi pengembangan terdeteksi",
|
"devVersion": "Versi pengembangan terdeteksi",
|
||||||
"updateError": "Gagal memeriksa pembaruan",
|
"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",
|
"changeLocation": "Ubah Lokasi",
|
||||||
"copied": "Tersalin",
|
"copied": "Tersalin",
|
||||||
"copyPath": "Salin Jalur",
|
"copyPath": "Salin Jalur",
|
||||||
@ -244,5 +249,14 @@
|
|||||||
"cancel": "Batal",
|
"cancel": "Batal",
|
||||||
"changeLocation": "Ubah Lokasi"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6,6 +6,11 @@
|
|||||||
"noUpdateAvailable": "Używasz najnowszej wersji",
|
"noUpdateAvailable": "Używasz najnowszej wersji",
|
||||||
"devVersion": "Wykryto wersję deweloperską",
|
"devVersion": "Wykryto wersję deweloperską",
|
||||||
"updateError": "Nie udało się sprawdzić dostępności aktualizacji",
|
"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",
|
"changeLocation": "Zmień Położenie",
|
||||||
"copied": "Skopiowano",
|
"copied": "Skopiowano",
|
||||||
"copyPath": "Skopiuj Ścieżkę",
|
"copyPath": "Skopiuj Ścieżkę",
|
||||||
@ -249,5 +254,14 @@
|
|||||||
"cancel": "Anuluj",
|
"cancel": "Anuluj",
|
||||||
"changeLocation": "Zmień Położenie"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,11 @@
|
|||||||
"noUpdateAvailable": "Bạn đang chạy phiên bản mới nhất",
|
"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",
|
"devVersion": "Đã phát hiện phiên bản phát triển",
|
||||||
"updateError": "Không thể kiểm tra cập nhật",
|
"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í",
|
"changeLocation": "Thay đổi Vị trí",
|
||||||
"copied": "Đã sao chép",
|
"copied": "Đã sao chép",
|
||||||
"copyPath": "Sao chép Đường dẫn",
|
"copyPath": "Sao chép Đường dẫn",
|
||||||
@ -244,5 +249,14 @@
|
|||||||
"cancel": "Hủy",
|
"cancel": "Hủy",
|
||||||
"changeLocation": "Thay đổi vị trí"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6,6 +6,11 @@
|
|||||||
"noUpdateAvailable": "您正在运行最新版本",
|
"noUpdateAvailable": "您正在运行最新版本",
|
||||||
"devVersion": "检测到开发版本",
|
"devVersion": "检测到开发版本",
|
||||||
"updateError": "检查更新失败",
|
"updateError": "检查更新失败",
|
||||||
|
"checkForBackendUpdates": "检查 Llamacpp 更新",
|
||||||
|
"checkForBackendUpdatesDesc": "检查是否有更新的 Llamacpp 后端版本。",
|
||||||
|
"checkingForBackendUpdates": "正在检查 Llamacpp 更新...",
|
||||||
|
"noBackendUpdateAvailable": "您正在运行最新的 Llamacpp 版本",
|
||||||
|
"backendUpdateError": "检查 Llamacpp 更新失败",
|
||||||
"changeLocation": "更改位置",
|
"changeLocation": "更改位置",
|
||||||
"copied": "已复制",
|
"copied": "已复制",
|
||||||
"copyPath": "复制路径",
|
"copyPath": "复制路径",
|
||||||
@ -244,5 +249,14 @@
|
|||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"changeLocation": "更改位置"
|
"changeLocation": "更改位置"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"backendUpdater": {
|
||||||
|
"newBackendVersion": "新的 Llamacpp 版本 {{version}}",
|
||||||
|
"backendUpdateAvailable": "Llamacpp 更新可用",
|
||||||
|
"remindMeLater": "稍后提醒我",
|
||||||
|
"updating": "正在更新...",
|
||||||
|
"updateNow": "立即更新",
|
||||||
|
"updateSuccess": "Llamacpp 更新成功",
|
||||||
|
"updateError": "更新 Llamacpp 失败"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6,6 +6,11 @@
|
|||||||
"noUpdateAvailable": "您正在運行最新版本",
|
"noUpdateAvailable": "您正在運行最新版本",
|
||||||
"devVersion": "檢測到開發版本",
|
"devVersion": "檢測到開發版本",
|
||||||
"updateError": "檢查更新失敗",
|
"updateError": "檢查更新失敗",
|
||||||
|
"checkForBackendUpdates": "檢查 Llamacpp 更新",
|
||||||
|
"checkForBackendUpdatesDesc": "檢查是否有更新的 Llamacpp 後端版本。",
|
||||||
|
"checkingForBackendUpdates": "正在檢查 Llamacpp 更新...",
|
||||||
|
"noBackendUpdateAvailable": "您正在運行最新的 Llamacpp 版本",
|
||||||
|
"backendUpdateError": "檢查 Llamacpp 更新失敗",
|
||||||
"changeLocation": "更改位置",
|
"changeLocation": "更改位置",
|
||||||
"copied": "已複製",
|
"copied": "已複製",
|
||||||
"copyPath": "複製路徑",
|
"copyPath": "複製路徑",
|
||||||
@ -244,5 +249,14 @@
|
|||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"changeLocation": "變更位置"
|
"changeLocation": "變更位置"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"backendUpdater": {
|
||||||
|
"newBackendVersion": "新的 Llamacpp 版本 {{version}}",
|
||||||
|
"backendUpdateAvailable": "Llamacpp 更新可用",
|
||||||
|
"remindMeLater": "稍後提醒我",
|
||||||
|
"updating": "正在更新...",
|
||||||
|
"updateNow": "立即更新",
|
||||||
|
"updateSuccess": "Llamacpp 更新成功",
|
||||||
|
"updateError": "更新 Llamacpp 失敗"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { createRootRoute, Outlet, useRouterState } from '@tanstack/react-router'
|
|||||||
|
|
||||||
import LeftPanel from '@/containers/LeftPanel'
|
import LeftPanel from '@/containers/LeftPanel'
|
||||||
import DialogAppUpdater from '@/containers/dialogs/AppUpdater'
|
import DialogAppUpdater from '@/containers/dialogs/AppUpdater'
|
||||||
|
import BackendUpdater from '@/containers/dialogs/BackendUpdater'
|
||||||
import { Fragment } from 'react/jsx-runtime'
|
import { Fragment } from 'react/jsx-runtime'
|
||||||
import { AppearanceProvider } from '@/providers/AppearanceProvider'
|
import { AppearanceProvider } from '@/providers/AppearanceProvider'
|
||||||
import { ThemeProvider } from '@/providers/ThemeProvider'
|
import { ThemeProvider } from '@/providers/ThemeProvider'
|
||||||
@ -113,6 +114,7 @@ const AppLayout = () => {
|
|||||||
{/* Fake absolute panel top to enable window drag */}
|
{/* Fake absolute panel top to enable window drag */}
|
||||||
<div className="absolute w-full h-10 z-10" data-tauri-drag-region />
|
<div className="absolute w-full h-10 z-10" data-tauri-drag-region />
|
||||||
<DialogAppUpdater />
|
<DialogAppUpdater />
|
||||||
|
<BackendUpdater />
|
||||||
|
|
||||||
{/* Use ResizablePanelGroup only on larger screens */}
|
{/* Use ResizablePanelGroup only on larger screens */}
|
||||||
{!isSmallScreen && isLeftPanelOpen ? (
|
{!isSmallScreen && isLeftPanelOpen ? (
|
||||||
@ -164,7 +166,9 @@ const AppLayout = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
{PlatformFeatures[PlatformFeature.ANALYTICS] && productAnalyticPrompt && <PromptAnalytic />}
|
{PlatformFeatures[PlatformFeature.ANALYTICS] && productAnalyticPrompt && (
|
||||||
|
<PromptAnalytic />
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,12 @@ import DeleteProvider from '@/containers/dialogs/DeleteProvider'
|
|||||||
import { useServiceHub } from '@/hooks/useServiceHub'
|
import { useServiceHub } from '@/hooks/useServiceHub'
|
||||||
import { localStorageKey } from '@/constants/localStorage'
|
import { localStorageKey } from '@/constants/localStorage'
|
||||||
import { Button } from '@/components/ui/button'
|
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 { toast } from 'sonner'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { predefinedProviders } from '@/consts/providers'
|
import { predefinedProviders } from '@/consts/providers'
|
||||||
@ -35,6 +40,7 @@ import { useModelLoad } from '@/hooks/useModelLoad'
|
|||||||
import { useLlamacppDevices } from '@/hooks/useLlamacppDevices'
|
import { useLlamacppDevices } from '@/hooks/useLlamacppDevices'
|
||||||
import { PlatformFeatures } from '@/lib/platform/const'
|
import { PlatformFeatures } from '@/lib/platform/const'
|
||||||
import { PlatformFeature } from '@/lib/platform/types'
|
import { PlatformFeature } from '@/lib/platform/types'
|
||||||
|
import { useBackendUpdater } from '@/hooks/useBackendUpdater'
|
||||||
|
|
||||||
// as route.threadsDetail
|
// as route.threadsDetail
|
||||||
export const Route = createFileRoute('/settings/providers/$providerName')({
|
export const Route = createFileRoute('/settings/providers/$providerName')({
|
||||||
@ -75,6 +81,10 @@ function ProviderDetail() {
|
|||||||
const [activeModels, setActiveModels] = useState<string[]>([])
|
const [activeModels, setActiveModels] = useState<string[]>([])
|
||||||
const [loadingModels, setLoadingModels] = useState<string[]>([])
|
const [loadingModels, setLoadingModels] = useState<string[]>([])
|
||||||
const [refreshingModels, setRefreshingModels] = useState(false)
|
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 { providerName } = useParams({ from: Route.id })
|
||||||
const { getProviderByName, setProviders, updateProvider } = useModelProvider()
|
const { getProviderByName, setProviders, updateProvider } = useModelProvider()
|
||||||
const provider = getProviderByName(providerName)
|
const provider = getProviderByName(providerName)
|
||||||
@ -310,6 +320,73 @@ 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)
|
||||||
|
|
||||||
|
// Extract filename from the selected file path and replace spaces with dashes
|
||||||
|
const fileName = (
|
||||||
|
selectedFile.split(/[/\\]/).pop() || selectedFile
|
||||||
|
).replace(/\s+/g, '-')
|
||||||
|
|
||||||
|
toast.success(t('settings:backendInstallSuccess'), {
|
||||||
|
description: `Llamacpp ${fileName} installed`,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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
|
// Check if model provider settings are enabled for this platform
|
||||||
if (!PlatformFeatures[PlatformFeature.MODEL_PROVIDER_SETTINGS]) {
|
if (!PlatformFeatures[PlatformFeature.MODEL_PROVIDER_SETTINGS]) {
|
||||||
return (
|
return (
|
||||||
@ -525,6 +602,60 @@ function ProviderDetail() {
|
|||||||
<span> is the recommended backend.</span>
|
<span> is the recommended backend.</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{setting.key === 'version_backend' &&
|
||||||
|
provider?.provider === 'llamacpp' && (
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
className="p-0"
|
||||||
|
onClick={handleCheckForBackendUpdate}
|
||||||
|
disabled={isCheckingBackendUpdate}
|
||||||
|
>
|
||||||
|
<div className="cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1">
|
||||||
|
<IconRefresh
|
||||||
|
size={12}
|
||||||
|
className={cn(
|
||||||
|
'text-main-view-fg/50',
|
||||||
|
isCheckingBackendUpdate &&
|
||||||
|
'animate-spin'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{isCheckingBackendUpdate
|
||||||
|
? t(
|
||||||
|
'settings:checkingForBackendUpdates'
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
'settings:checkForBackendUpdates'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
className="p-0"
|
||||||
|
onClick={handleInstallBackendFromFile}
|
||||||
|
disabled={isInstallingBackend}
|
||||||
|
>
|
||||||
|
<div className="cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1">
|
||||||
|
<IconUpload
|
||||||
|
size={12}
|
||||||
|
className={cn(
|
||||||
|
'text-main-view-fg/50',
|
||||||
|
isInstallingBackend && 'animate-pulse'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{isInstallingBackend
|
||||||
|
? 'Installing Backend...'
|
||||||
|
: 'Install Backend from File'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
actions={actionComponent}
|
actions={actionComponent}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user