feat: custom downloaded model name (#6588)
* feat: add field edit model name * fix: update model * chore: updaet UI form with save button, and handle edit capabilities and rename folder will need save button * fix: relocate model * chore: update and refresh list model provider also update test case * chore: state loader * fix: model path * fix: model config update * chore: fix remove depencies provider on edit model dialog * chore: avoid shifted model name or id --------- Co-authored-by: Louis <louis@jan.ai>
This commit is contained in:
parent
453df559b5
commit
b7dae19756
@ -274,6 +274,10 @@ export abstract class AIEngine extends BaseExtension {
|
|||||||
*/
|
*/
|
||||||
abstract delete(modelId: string): Promise<void>
|
abstract delete(modelId: string): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a model
|
||||||
|
*/
|
||||||
|
abstract update(modelId: string, model: Partial<modelInfo>): Promise<void>
|
||||||
/**
|
/**
|
||||||
* Imports a model
|
* Imports a model
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -43,6 +43,12 @@ const mkdir = (...args: any[]) => globalThis.core.api?.mkdir({ args })
|
|||||||
*/
|
*/
|
||||||
const rm = (...args: any[]) => globalThis.core.api?.rm({ args })
|
const rm = (...args: any[]) => globalThis.core.api?.rm({ args })
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a file from the source path to the destination path.
|
||||||
|
* @returns {Promise<any>} A Promise that resolves when the file is moved successfully.
|
||||||
|
*/
|
||||||
|
const mv = (...args: any[]) => globalThis.core.api?.mv({ args })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a file from the local file system.
|
* Deletes a file from the local file system.
|
||||||
* @param {string} path - The path of the file to delete.
|
* @param {string} path - The path of the file to delete.
|
||||||
@ -92,6 +98,7 @@ export const fs = {
|
|||||||
readdirSync,
|
readdirSync,
|
||||||
mkdir,
|
mkdir,
|
||||||
rm,
|
rm,
|
||||||
|
mv,
|
||||||
unlinkSync,
|
unlinkSync,
|
||||||
appendFileSync,
|
appendFileSync,
|
||||||
copyFile,
|
copyFile,
|
||||||
|
|||||||
@ -91,6 +91,7 @@ export enum FileSystemRoute {
|
|||||||
existsSync = 'existsSync',
|
existsSync = 'existsSync',
|
||||||
readdirSync = 'readdirSync',
|
readdirSync = 'readdirSync',
|
||||||
rm = 'rm',
|
rm = 'rm',
|
||||||
|
mv = 'mv',
|
||||||
mkdir = 'mkdir',
|
mkdir = 'mkdir',
|
||||||
readFileSync = 'readFileSync',
|
readFileSync = 'readFileSync',
|
||||||
writeFileSync = 'writeFileSync',
|
writeFileSync = 'writeFileSync',
|
||||||
|
|||||||
@ -386,6 +386,12 @@ export default class JanProviderWeb extends AIEngine {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async update(modelId: string, model: Partial<modelInfo>): Promise<void> {
|
||||||
|
throw new Error(
|
||||||
|
`Update operation not supported for remote Jan API model: ${modelId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async import(modelId: string, _opts: ImportOptions): Promise<void> {
|
async import(modelId: string, _opts: ImportOptions): Promise<void> {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Import operation not supported for remote Jan API model: ${modelId}`
|
`Import operation not supported for remote Jan API model: ${modelId}`
|
||||||
|
|||||||
@ -1155,6 +1155,49 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a model with new information.
|
||||||
|
* @param modelId
|
||||||
|
* @param model
|
||||||
|
*/
|
||||||
|
async update(modelId: string, model: Partial<modelInfo>): Promise<void> {
|
||||||
|
const modelFolderPath = await joinPath([
|
||||||
|
await this.getProviderPath(),
|
||||||
|
'models',
|
||||||
|
modelId,
|
||||||
|
])
|
||||||
|
const modelConfig = await invoke<ModelConfig>('read_yaml', {
|
||||||
|
path: await joinPath([modelFolderPath, 'model.yml']),
|
||||||
|
})
|
||||||
|
const newFolderPath = await joinPath([
|
||||||
|
await this.getProviderPath(),
|
||||||
|
'models',
|
||||||
|
model.id,
|
||||||
|
])
|
||||||
|
// Check if newFolderPath exists
|
||||||
|
if (await fs.existsSync(newFolderPath)) {
|
||||||
|
throw new Error(`Model with ID ${model.id} already exists`)
|
||||||
|
}
|
||||||
|
const newModelConfigPath = await joinPath([newFolderPath, 'model.yml'])
|
||||||
|
await fs.mv(modelFolderPath, newFolderPath).then(() =>
|
||||||
|
// now replace what values have previous model name with format
|
||||||
|
invoke('write_yaml', {
|
||||||
|
data: {
|
||||||
|
...modelConfig,
|
||||||
|
model_path: modelConfig?.model_path?.replace(
|
||||||
|
`${this.providerId}/models/${modelId}`,
|
||||||
|
`${this.providerId}/models/${model.id}`
|
||||||
|
),
|
||||||
|
mmproj_path: modelConfig?.mmproj_path?.replace(
|
||||||
|
`${this.providerId}/models/${modelId}`,
|
||||||
|
`${this.providerId}/models/${model.id}`
|
||||||
|
),
|
||||||
|
},
|
||||||
|
savePath: newModelConfigPath,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2016",
|
"target": "es2018",
|
||||||
"module": "ES6",
|
"module": "ES6",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
|
|||||||
@ -33,6 +33,22 @@ pub fn mkdir<R: Runtime>(app_handle: tauri::AppHandle<R>, args: Vec<String>) ->
|
|||||||
fs::create_dir_all(&path).map_err(|e| e.to_string())
|
fs::create_dir_all(&path).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn mv<R: Runtime>(app_handle: tauri::AppHandle<R>, args: Vec<String>) -> Result<(), String> {
|
||||||
|
if args.len() < 2 || args[0].is_empty() || args[1].is_empty() {
|
||||||
|
return Err("mv error: Invalid argument - source and destination required".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = resolve_path(app_handle.clone(), &args[0]);
|
||||||
|
let destination = resolve_path(app_handle, &args[1]);
|
||||||
|
|
||||||
|
if !source.exists() {
|
||||||
|
return Err("mv error: Source path does not exist".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::rename(&source, &destination).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn join_path<R: Runtime>(
|
pub fn join_path<R: Runtime>(
|
||||||
app_handle: tauri::AppHandle<R>,
|
app_handle: tauri::AppHandle<R>,
|
||||||
|
|||||||
@ -8,7 +8,6 @@ use core::{
|
|||||||
};
|
};
|
||||||
use jan_utils::generate_app_token;
|
use jan_utils::generate_app_token;
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
|
||||||
use tauri::{Emitter, Manager, RunEvent};
|
use tauri::{Emitter, Manager, RunEvent};
|
||||||
use tauri_plugin_llamacpp::cleanup_llama_processes;
|
use tauri_plugin_llamacpp::cleanup_llama_processes;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
@ -54,6 +53,7 @@ pub fn run() {
|
|||||||
core::filesystem::commands::readdir_sync,
|
core::filesystem::commands::readdir_sync,
|
||||||
core::filesystem::commands::read_file_sync,
|
core::filesystem::commands::read_file_sync,
|
||||||
core::filesystem::commands::rm,
|
core::filesystem::commands::rm,
|
||||||
|
core::filesystem::commands::mv,
|
||||||
core::filesystem::commands::file_stat,
|
core::filesystem::commands::file_stat,
|
||||||
core::filesystem::commands::write_file_sync,
|
core::filesystem::commands::write_file_sync,
|
||||||
core::filesystem::commands::write_yaml,
|
core::filesystem::commands::write_yaml,
|
||||||
@ -163,6 +163,8 @@ pub fn run() {
|
|||||||
|
|
||||||
#[cfg(any(windows, target_os = "linux"))]
|
#[cfg(any(windows, target_os = "linux"))]
|
||||||
{
|
{
|
||||||
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
|
|
||||||
app.deep_link().register_all()?;
|
app.deep_link().register_all()?;
|
||||||
}
|
}
|
||||||
setup_mcp(app);
|
setup_mcp(app);
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||||
import {
|
import {
|
||||||
@ -14,12 +16,15 @@ import {
|
|||||||
IconEye,
|
IconEye,
|
||||||
IconTool,
|
IconTool,
|
||||||
IconAlertTriangle,
|
IconAlertTriangle,
|
||||||
|
IconLoader2,
|
||||||
// IconWorld,
|
// IconWorld,
|
||||||
// IconAtom,
|
// IconAtom,
|
||||||
// IconCodeCircle2,
|
// IconCodeCircle2,
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
import { useServiceHub } from '@/hooks/useServiceHub'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
// No need to define our own interface, we'll use the existing Model type
|
// No need to define our own interface, we'll use the existing Model type
|
||||||
type DialogEditModelProps = {
|
type DialogEditModelProps = {
|
||||||
@ -32,8 +37,16 @@ export const DialogEditModel = ({
|
|||||||
modelId,
|
modelId,
|
||||||
}: DialogEditModelProps) => {
|
}: DialogEditModelProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { updateProvider } = useModelProvider()
|
const { updateProvider, setProviders } = useModelProvider()
|
||||||
const [selectedModelId, setSelectedModelId] = useState<string>('')
|
const [selectedModelId, setSelectedModelId] = useState<string>('')
|
||||||
|
const [modelName, setModelName] = useState<string>('')
|
||||||
|
const [originalModelName, setOriginalModelName] = useState<string>('')
|
||||||
|
const [originalCapabilities, setOriginalCapabilities] = useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({})
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const serviceHub = useServiceHub()
|
||||||
const [capabilities, setCapabilities] = useState<Record<string, boolean>>({
|
const [capabilities, setCapabilities] = useState<Record<string, boolean>>({
|
||||||
completion: false,
|
completion: false,
|
||||||
vision: false,
|
vision: false,
|
||||||
@ -45,12 +58,27 @@ export const DialogEditModel = ({
|
|||||||
|
|
||||||
// Initialize with the provided model ID or the first model if available
|
// Initialize with the provided model ID or the first model if available
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (modelId) {
|
// Only set the selected model ID if the dialog is not open to prevent switching during downloads
|
||||||
setSelectedModelId(modelId)
|
if (!isOpen) {
|
||||||
} else if (provider.models && provider.models.length > 0) {
|
if (modelId) {
|
||||||
setSelectedModelId(provider.models[0].id)
|
setSelectedModelId(modelId)
|
||||||
|
} else if (provider.models && provider.models.length > 0) {
|
||||||
|
setSelectedModelId(provider.models[0].id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [provider, modelId])
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [modelId, isOpen]) // Add isOpen dependency to prevent switching when dialog is open
|
||||||
|
|
||||||
|
// Handle dialog opening - set the initial model selection
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && !selectedModelId) {
|
||||||
|
if (modelId) {
|
||||||
|
setSelectedModelId(modelId)
|
||||||
|
} else if (provider.models && provider.models.length > 0) {
|
||||||
|
setSelectedModelId(provider.models[0].id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isOpen, selectedModelId, modelId, provider.models])
|
||||||
|
|
||||||
// Get the currently selected model
|
// Get the currently selected model
|
||||||
const selectedModel = provider.models.find(
|
const selectedModel = provider.models.find(
|
||||||
@ -58,7 +86,7 @@ export const DialogEditModel = ({
|
|||||||
(m: any) => m.id === selectedModelId
|
(m: any) => m.id === selectedModelId
|
||||||
)
|
)
|
||||||
|
|
||||||
// Initialize capabilities from selected model
|
// Initialize capabilities and model name from selected model
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedModel) {
|
if (selectedModel) {
|
||||||
const modelCapabilities = selectedModel.capabilities || []
|
const modelCapabilities = selectedModel.capabilities || []
|
||||||
@ -70,69 +98,106 @@ export const DialogEditModel = ({
|
|||||||
web_search: modelCapabilities.includes('web_search'),
|
web_search: modelCapabilities.includes('web_search'),
|
||||||
reasoning: modelCapabilities.includes('reasoning'),
|
reasoning: modelCapabilities.includes('reasoning'),
|
||||||
})
|
})
|
||||||
|
const modelNameValue = selectedModel.id
|
||||||
|
setModelName(modelNameValue)
|
||||||
|
setOriginalModelName(modelNameValue)
|
||||||
|
|
||||||
|
const originalCaps = {
|
||||||
|
completion: modelCapabilities.includes('completion'),
|
||||||
|
vision: modelCapabilities.includes('vision'),
|
||||||
|
tools: modelCapabilities.includes('tools'),
|
||||||
|
embeddings: modelCapabilities.includes('embeddings'),
|
||||||
|
web_search: modelCapabilities.includes('web_search'),
|
||||||
|
reasoning: modelCapabilities.includes('reasoning'),
|
||||||
|
}
|
||||||
|
setOriginalCapabilities(originalCaps)
|
||||||
}
|
}
|
||||||
}, [selectedModel])
|
}, [selectedModel])
|
||||||
|
|
||||||
// Track if capabilities were updated by user action
|
|
||||||
const [capabilitiesUpdated, setCapabilitiesUpdated] = useState(false)
|
|
||||||
|
|
||||||
// Update model capabilities - only update local state
|
// Update model capabilities - only update local state
|
||||||
const handleCapabilityChange = (capability: string, enabled: boolean) => {
|
const handleCapabilityChange = (capability: string, enabled: boolean) => {
|
||||||
setCapabilities((prev) => ({
|
setCapabilities((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[capability]: enabled,
|
[capability]: enabled,
|
||||||
}))
|
}))
|
||||||
// Mark that capabilities were updated by user action
|
|
||||||
setCapabilitiesUpdated(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use effect to update the provider when capabilities are explicitly changed by user
|
// Handle model name change
|
||||||
useEffect(() => {
|
const handleModelNameChange = (newName: string) => {
|
||||||
// Only run if capabilities were updated by user action and we have a selected model
|
setModelName(newName)
|
||||||
if (!capabilitiesUpdated || !selectedModel) return
|
}
|
||||||
|
|
||||||
// Reset the flag
|
// Check if there are unsaved changes
|
||||||
setCapabilitiesUpdated(false)
|
const hasUnsavedChanges = () => {
|
||||||
|
const nameChanged = modelName !== originalModelName
|
||||||
|
const capabilitiesChanged =
|
||||||
|
JSON.stringify(capabilities) !== JSON.stringify(originalCapabilities)
|
||||||
|
return nameChanged || capabilitiesChanged
|
||||||
|
}
|
||||||
|
|
||||||
// Create updated capabilities array from the state
|
// Handle save changes
|
||||||
const updatedCapabilities = Object.entries(capabilities)
|
const handleSaveChanges = async () => {
|
||||||
.filter(([, isEnabled]) => isEnabled)
|
if (!selectedModel?.id || isLoading) return
|
||||||
.map(([capName]) => capName)
|
|
||||||
|
|
||||||
// Find and update the model in the provider
|
setIsLoading(true)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
try {
|
||||||
const updatedModels = provider.models.map((m: any) => {
|
// Update model name if changed
|
||||||
if (m.id === selectedModelId) {
|
if (modelName !== originalModelName) {
|
||||||
return {
|
await serviceHub
|
||||||
...m,
|
.models()
|
||||||
capabilities: updatedCapabilities,
|
.updateModel(selectedModel.id, { id: modelName })
|
||||||
// Mark that user has manually configured capabilities
|
setOriginalModelName(modelName)
|
||||||
_userConfiguredCapabilities: true,
|
await serviceHub.providers().getProviders().then(setProviders)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return m
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update the provider with the updated models
|
// Update capabilities if changed
|
||||||
updateProvider(provider.provider, {
|
if (
|
||||||
...provider,
|
JSON.stringify(capabilities) !== JSON.stringify(originalCapabilities)
|
||||||
models: updatedModels,
|
) {
|
||||||
})
|
const updatedCapabilities = Object.entries(capabilities)
|
||||||
}, [
|
.filter(([, isEnabled]) => isEnabled)
|
||||||
capabilitiesUpdated,
|
.map(([capName]) => capName)
|
||||||
capabilities,
|
|
||||||
provider,
|
// Find and update the model in the provider
|
||||||
selectedModel,
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
selectedModelId,
|
const updatedModels = provider.models.map((m: any) => {
|
||||||
updateProvider,
|
if (m.id === selectedModelId) {
|
||||||
])
|
return {
|
||||||
|
...m,
|
||||||
|
capabilities: updatedCapabilities,
|
||||||
|
// Mark that user has manually configured capabilities
|
||||||
|
_userConfiguredCapabilities: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update the provider with the updated models
|
||||||
|
updateProvider(provider.provider, {
|
||||||
|
...provider,
|
||||||
|
models: updatedModels,
|
||||||
|
})
|
||||||
|
|
||||||
|
setOriginalCapabilities(capabilities)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success toast and close dialog
|
||||||
|
toast.success('Model updated successfully')
|
||||||
|
setIsOpen(false)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update model:', error)
|
||||||
|
toast.error('Failed to update model. Please try again.')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!selectedModel) {
|
if (!selectedModel) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<div className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out">
|
<div className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out">
|
||||||
<IconPencil size={18} className="text-main-view-fg/50" />
|
<IconPencil size={18} className="text-main-view-fg/50" />
|
||||||
@ -148,6 +213,24 @@ export const DialogEditModel = ({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Model Name Section */}
|
||||||
|
<div className="py-1">
|
||||||
|
<label
|
||||||
|
htmlFor="model-name"
|
||||||
|
className="text-sm font-medium mb-3 block"
|
||||||
|
>
|
||||||
|
Model Name
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="model-name"
|
||||||
|
value={modelName}
|
||||||
|
onChange={(e) => handleModelNameChange(e.target.value)}
|
||||||
|
placeholder="Enter model name"
|
||||||
|
className="w-full"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Warning Banner */}
|
{/* Warning Banner */}
|
||||||
<div className="bg-main-view-fg/5 border border-main-view-fg/10 rounded-md p-3">
|
<div className="bg-main-view-fg/5 border border-main-view-fg/10 rounded-md p-3">
|
||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start space-x-3">
|
||||||
@ -181,6 +264,7 @@ export const DialogEditModel = ({
|
|||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
handleCapabilityChange('tools', checked)
|
handleCapabilityChange('tools', checked)
|
||||||
}
|
}
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -197,6 +281,7 @@ export const DialogEditModel = ({
|
|||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
handleCapabilityChange('vision', checked)
|
handleCapabilityChange('vision', checked)
|
||||||
}
|
}
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -253,6 +338,24 @@ export const DialogEditModel = ({
|
|||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<div className="flex justify-end pt-4">
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveChanges}
|
||||||
|
disabled={!hasUnsavedChanges() || isLoading}
|
||||||
|
className="px-4 py-2"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<IconLoader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Saving...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Save Changes'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -26,6 +26,7 @@ describe('DefaultModelsService', () => {
|
|||||||
const mockEngine = {
|
const mockEngine = {
|
||||||
list: vi.fn(),
|
list: vi.fn(),
|
||||||
updateSettings: vi.fn(),
|
updateSettings: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
import: vi.fn(),
|
import: vi.fn(),
|
||||||
abortImport: vi.fn(),
|
abortImport: vi.fn(),
|
||||||
delete: vi.fn(),
|
delete: vi.fn(),
|
||||||
@ -108,22 +109,40 @@ describe('DefaultModelsService', () => {
|
|||||||
|
|
||||||
describe('updateModel', () => {
|
describe('updateModel', () => {
|
||||||
it('should update model settings', async () => {
|
it('should update model settings', async () => {
|
||||||
|
const modelId = 'model1'
|
||||||
const model = {
|
const model = {
|
||||||
id: 'model1',
|
id: 'model1',
|
||||||
settings: [{ key: 'temperature', value: 0.7 }],
|
settings: [{ key: 'temperature', value: 0.7 }],
|
||||||
}
|
}
|
||||||
|
|
||||||
await modelsService.updateModel(model as any)
|
await modelsService.updateModel(modelId, model as any)
|
||||||
|
|
||||||
expect(mockEngine.updateSettings).toHaveBeenCalledWith(model.settings)
|
expect(mockEngine.updateSettings).toHaveBeenCalledWith(model.settings)
|
||||||
|
expect(mockEngine.update).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle model without settings', async () => {
|
it('should handle model without settings', async () => {
|
||||||
|
const modelId = 'model1'
|
||||||
const model = { id: 'model1' }
|
const model = { id: 'model1' }
|
||||||
|
|
||||||
await modelsService.updateModel(model)
|
await modelsService.updateModel(modelId, model)
|
||||||
|
|
||||||
expect(mockEngine.updateSettings).not.toHaveBeenCalled()
|
expect(mockEngine.updateSettings).not.toHaveBeenCalled()
|
||||||
|
expect(mockEngine.update).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update model when modelId differs from model.id', async () => {
|
||||||
|
const modelId = 'old-model-id'
|
||||||
|
const model = {
|
||||||
|
id: 'new-model-id',
|
||||||
|
settings: [{ key: 'temperature', value: 0.7 }],
|
||||||
|
}
|
||||||
|
mockEngine.update.mockResolvedValue(undefined)
|
||||||
|
|
||||||
|
await modelsService.updateModel(modelId, model as any)
|
||||||
|
|
||||||
|
expect(mockEngine.updateSettings).toHaveBeenCalledWith(model.settings)
|
||||||
|
expect(mockEngine.update).toHaveBeenCalledWith(modelId, model)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -162,11 +162,16 @@ export class DefaultModelsService implements ModelsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateModel(model: Partial<CoreModel>): Promise<void> {
|
async updateModel(modelId: string, model: Partial<CoreModel>): Promise<void> {
|
||||||
if (model.settings)
|
if (model.settings)
|
||||||
this.getEngine()?.updateSettings(
|
this.getEngine()?.updateSettings(
|
||||||
model.settings as SettingComponentProps[]
|
model.settings as SettingComponentProps[]
|
||||||
)
|
)
|
||||||
|
if (modelId !== model.id) {
|
||||||
|
await this.getEngine()
|
||||||
|
?.update(modelId, model)
|
||||||
|
.then(() => console.log('Model updated successfully'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async pullModel(
|
async pullModel(
|
||||||
|
|||||||
@ -99,7 +99,7 @@ export interface ModelsService {
|
|||||||
hfToken?: string
|
hfToken?: string
|
||||||
): Promise<HuggingFaceRepo | null>
|
): Promise<HuggingFaceRepo | null>
|
||||||
convertHfRepoToCatalogModel(repo: HuggingFaceRepo): CatalogModel
|
convertHfRepoToCatalogModel(repo: HuggingFaceRepo): CatalogModel
|
||||||
updateModel(model: Partial<CoreModel>): Promise<void>
|
updateModel(modelId: string, model: Partial<CoreModel>): Promise<void>
|
||||||
pullModel(
|
pullModel(
|
||||||
id: string,
|
id: string,
|
||||||
modelPath: string,
|
modelPath: string,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user