diff --git a/web-app/src/containers/dialogs/AddEditAssistant.tsx b/web-app/src/containers/dialogs/AddEditAssistant.tsx index 4c218aec0d..6a74f8bcd9 100644 --- a/web-app/src/containers/dialogs/AddEditAssistant.tsx +++ b/web-app/src/containers/dialogs/AddEditAssistant.tsx @@ -236,7 +236,11 @@ export default function AddEditAssistant({ return ( - + { + e.preventDefault() + }} + > {editingKey diff --git a/web-app/src/containers/dialogs/AddEditMCPServer.tsx b/web-app/src/containers/dialogs/AddEditMCPServer.tsx index cf28130097..29782c6914 100644 --- a/web-app/src/containers/dialogs/AddEditMCPServer.tsx +++ b/web-app/src/containers/dialogs/AddEditMCPServer.tsx @@ -303,11 +303,16 @@ export default function AddEditMCPServer({ const serverConfig = config as MCPServerConfig // Validate type field if present - if (serverConfig.type && !['stdio', 'http', 'sse'].includes(serverConfig.type)) { - setError(t('mcp-servers:editJson.errorInvalidType', { - serverName: trimmedServerName, - type: serverConfig.type - })) + if ( + serverConfig.type && + !['stdio', 'http', 'sse'].includes(serverConfig.type) + ) { + setError( + t('mcp-servers:editJson.errorInvalidType', { + serverName: trimmedServerName, + type: serverConfig.type, + }) + ) return } @@ -366,7 +371,12 @@ export default function AddEditMCPServer({ return ( - + { + e.preventDefault() + }} + > diff --git a/web-app/src/containers/dialogs/AddProviderDialog.tsx b/web-app/src/containers/dialogs/AddProviderDialog.tsx index 6b12b87f57..67b6205a42 100644 --- a/web-app/src/containers/dialogs/AddProviderDialog.tsx +++ b/web-app/src/containers/dialogs/AddProviderDialog.tsx @@ -102,4 +102,4 @@ export function AddProviderDialog({ ) -} \ No newline at end of file +} diff --git a/web-app/src/containers/dialogs/EditJsonMCPserver.tsx b/web-app/src/containers/dialogs/EditJsonMCPserver.tsx index 6cf0224eba..63ebacb6d8 100644 --- a/web-app/src/containers/dialogs/EditJsonMCPserver.tsx +++ b/web-app/src/containers/dialogs/EditJsonMCPserver.tsx @@ -61,7 +61,11 @@ export default function EditJsonMCPserver({ return ( - + { + e.preventDefault() + }} + > {serverName diff --git a/web-app/src/containers/dialogs/ImportVisionModelDialog.tsx b/web-app/src/containers/dialogs/ImportVisionModelDialog.tsx index 73a2caabfd..5984895062 100644 --- a/web-app/src/containers/dialogs/ImportVisionModelDialog.tsx +++ b/web-app/src/containers/dialogs/ImportVisionModelDialog.tsx @@ -44,129 +44,142 @@ export const ImportVisionModelDialog = ({ >(null) const [isValidatingMmproj, setIsValidatingMmproj] = useState(false) - const validateGgufFile = useCallback(async ( - filePath: string, - fileType: 'model' | 'mmproj' - ): Promise => { - if (fileType === 'model') { - setIsValidating(true) - setValidationError(null) - } else { - setIsValidatingMmproj(true) - setMmprojValidationError(null) - } - - try { - // Handle validation differently for model files vs mmproj files + const validateGgufFile = useCallback( + async (filePath: string, fileType: 'model' | 'mmproj'): Promise => { if (fileType === 'model') { - // For model files, use the standard validateGgufFile method - if (typeof serviceHub.models().validateGgufFile === 'function') { - const result = await serviceHub.models().validateGgufFile(filePath) - - if (result.metadata) { - // Check architecture from metadata - const architecture = - result.metadata.metadata?.['general.architecture'] - - // Extract baseName and use it as model name if available - const baseName = result.metadata.metadata?.['general.basename'] + setIsValidating(true) + setValidationError(null) + } else { + setIsValidatingMmproj(true) + setMmprojValidationError(null) + } - if (baseName) { - setModelName(baseName) + try { + // Handle validation differently for model files vs mmproj files + if (fileType === 'model') { + // For model files, use the standard validateGgufFile method + if (typeof serviceHub.models().validateGgufFile === 'function') { + const result = await serviceHub.models().validateGgufFile(filePath) + + if (result.metadata) { + // Check architecture from metadata + const architecture = + result.metadata.metadata?.['general.architecture'] + + // Extract baseName and use it as model name if available + const baseName = result.metadata.metadata?.['general.basename'] + + if (baseName) { + setModelName(baseName) + } + + // Model files should NOT be clip + if (architecture === 'clip') { + const errorMessage = + 'This model has CLIP architecture and cannot be imported as a text generation model. CLIP models are designed for vision tasks and require different handling.' + setValidationError(errorMessage) + console.error( + 'CLIP architecture detected in model file:', + architecture + ) + } } - // Model files should NOT be clip - if (architecture === 'clip') { - const errorMessage = - 'This model has CLIP architecture and cannot be imported as a text generation model. CLIP models are designed for vision tasks and require different handling.' - setValidationError(errorMessage) - console.error( - 'CLIP architecture detected in model file:', - architecture - ) + if (!result.isValid) { + setValidationError(result.error || 'Model validation failed') + console.error('Model validation failed:', result.error) } } + } else { + // For mmproj files, we need to manually validate since validateGgufFile rejects CLIP models + try { + // Import the readGgufMetadata function directly from Tauri + const { invoke } = await import('@tauri-apps/api/core') + + const metadata = await invoke( + 'plugin:llamacpp|read_gguf_metadata', + { + path: filePath, + } + ) - if (!result.isValid) { - setValidationError(result.error || 'Model validation failed') - console.error('Model validation failed:', result.error) - } - } - } else { - // For mmproj files, we need to manually validate since validateGgufFile rejects CLIP models - try { - // Import the readGgufMetadata function directly from Tauri - const { invoke } = await import('@tauri-apps/api/core') - - const metadata = await invoke('plugin:llamacpp|read_gguf_metadata', { - path: filePath, - }) - - - // Check if architecture matches expected type - const architecture = ( - metadata as { metadata?: Record } - ).metadata?.['general.architecture'] + // Check if architecture matches expected type + const architecture = ( + metadata as { metadata?: Record } + ).metadata?.['general.architecture'] - // Get general.baseName from metadata - const baseName = (metadata as { metadata?: Record }) - .metadata?.['general.basename'] + // Get general.baseName from metadata + const baseName = (metadata as { metadata?: Record }) + .metadata?.['general.basename'] - // MMProj files MUST be clip - if (architecture !== 'clip') { - const errorMessage = `This MMProj file has "${architecture}" architecture but should have "clip" architecture. MMProj files must be CLIP models for vision processing.` - setMmprojValidationError(errorMessage) + // MMProj files MUST be clip + if (architecture !== 'clip') { + const errorMessage = `This MMProj file has "${architecture}" architecture but should have "clip" architecture. MMProj files must be CLIP models for vision processing.` + setMmprojValidationError(errorMessage) + console.error( + 'Non-CLIP architecture detected in mmproj file:', + architecture + ) + } else if ( + baseName && + modelName && + !modelName.toLowerCase().includes(baseName.toLowerCase()) && + !baseName.toLowerCase().includes(modelName.toLowerCase()) + ) { + // Validate that baseName and model name are compatible (one should contain the other) + const errorMessage = `MMProj file baseName "${baseName}" does not match model name "${modelName}". The MMProj file should be compatible with the selected model.` + setMmprojValidationError(errorMessage) + console.error('BaseName mismatch in mmproj file:', { + baseName, + modelName, + }) + } + } catch (directError) { console.error( - 'Non-CLIP architecture detected in mmproj file:', - architecture + 'Failed to validate mmproj file directly:', + directError ) - } else if ( - baseName && - modelName && - !modelName.toLowerCase().includes(baseName.toLowerCase()) && - !baseName.toLowerCase().includes(modelName.toLowerCase()) - ) { - // Validate that baseName and model name are compatible (one should contain the other) - const errorMessage = `MMProj file baseName "${baseName}" does not match model name "${modelName}". The MMProj file should be compatible with the selected model.` + const errorMessage = `Failed to read MMProj metadata: ${ + directError instanceof Error + ? directError.message + : 'Unknown error' + }` setMmprojValidationError(errorMessage) - console.error('BaseName mismatch in mmproj file:', { - baseName, - modelName, - }) } - } catch (directError) { - console.error('Failed to validate mmproj file directly:', directError) - const errorMessage = `Failed to read MMProj metadata: ${ - directError instanceof Error ? directError.message : 'Unknown error' - }` - setMmprojValidationError(errorMessage) } - } - } catch (error) { - console.error(`Failed to validate ${fileType} file:`, error) - const errorMessage = `Failed to read ${fileType} metadata: ${error instanceof Error ? error.message : 'Unknown error'}` + } catch (error) { + console.error(`Failed to validate ${fileType} file:`, error) + const errorMessage = `Failed to read ${fileType} metadata: ${error instanceof Error ? error.message : 'Unknown error'}` - if (fileType === 'model') { - setValidationError(errorMessage) - } else { - setMmprojValidationError(errorMessage) - } - } finally { - if (fileType === 'model') { - setIsValidating(false) - } else { - setIsValidatingMmproj(false) + if (fileType === 'model') { + setValidationError(errorMessage) + } else { + setMmprojValidationError(errorMessage) + } + } finally { + if (fileType === 'model') { + setIsValidating(false) + } else { + setIsValidatingMmproj(false) + } } - } - }, [modelName, serviceHub]) + }, + [modelName, serviceHub] + ) - const validateModelFile = useCallback(async (filePath: string): Promise => { - await validateGgufFile(filePath, 'model') - }, [validateGgufFile]) + const validateModelFile = useCallback( + async (filePath: string): Promise => { + await validateGgufFile(filePath, 'model') + }, + [validateGgufFile] + ) - const validateMmprojFile = useCallback(async (filePath: string): Promise => { - await validateGgufFile(filePath, 'mmproj') - }, [validateGgufFile]) + const validateMmprojFile = useCallback( + async (filePath: string): Promise => { + await validateGgufFile(filePath, 'mmproj') + }, + [validateGgufFile] + ) const handleFileSelect = async (type: 'model' | 'mmproj') => { const selectedFile = await serviceHub.dialog().open({ @@ -291,7 +304,11 @@ export const ImportVisionModelDialog = ({ return ( {trigger} - + { + e.preventDefault() + }} + > Import Model