Merge pull request #6493 from menloresearch/fix/edit-dialog

chore: prevent click outside for edit dialog
This commit is contained in:
Faisal Amir 2025-09-17 10:22:23 +07:00 committed by GitHub
commit b4fd7c300a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 150 additions and 115 deletions

View File

@ -236,7 +236,11 @@ export default function AddEditAssistant({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent> <DialogContent
onInteractOutside={(e) => {
e.preventDefault()
}}
>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{editingKey {editingKey

View File

@ -303,11 +303,16 @@ export default function AddEditMCPServer({
const serverConfig = config as MCPServerConfig const serverConfig = config as MCPServerConfig
// Validate type field if present // Validate type field if present
if (serverConfig.type && !['stdio', 'http', 'sse'].includes(serverConfig.type)) { if (
setError(t('mcp-servers:editJson.errorInvalidType', { serverConfig.type &&
serverName: trimmedServerName, !['stdio', 'http', 'sse'].includes(serverConfig.type)
type: serverConfig.type ) {
})) setError(
t('mcp-servers:editJson.errorInvalidType', {
serverName: trimmedServerName,
type: serverConfig.type,
})
)
return return
} }
@ -366,7 +371,12 @@ export default function AddEditMCPServer({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent showCloseButton={false}> <DialogContent
showCloseButton={false}
onInteractOutside={(e) => {
e.preventDefault()
}}
>
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center justify-between"> <DialogTitle className="flex items-center justify-between">
<span> <span>

View File

@ -61,7 +61,11 @@ export default function EditJsonMCPserver({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent> <DialogContent
onInteractOutside={(e) => {
e.preventDefault()
}}
>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{serverName {serverName

View File

@ -44,129 +44,142 @@ export const ImportVisionModelDialog = ({
>(null) >(null)
const [isValidatingMmproj, setIsValidatingMmproj] = useState(false) const [isValidatingMmproj, setIsValidatingMmproj] = useState(false)
const validateGgufFile = useCallback(async ( const validateGgufFile = useCallback(
filePath: string, async (filePath: string, fileType: 'model' | 'mmproj'): Promise<void> => {
fileType: 'model' | 'mmproj'
): Promise<void> => {
if (fileType === 'model') {
setIsValidating(true)
setValidationError(null)
} else {
setIsValidatingMmproj(true)
setMmprojValidationError(null)
}
try {
// Handle validation differently for model files vs mmproj files
if (fileType === 'model') { if (fileType === 'model') {
// For model files, use the standard validateGgufFile method setIsValidating(true)
if (typeof serviceHub.models().validateGgufFile === 'function') { setValidationError(null)
const result = await serviceHub.models().validateGgufFile(filePath) } else {
setIsValidatingMmproj(true)
setMmprojValidationError(null)
}
if (result.metadata) { try {
// Check architecture from metadata // Handle validation differently for model files vs mmproj files
const architecture = if (fileType === 'model') {
result.metadata.metadata?.['general.architecture'] // For model files, use the standard validateGgufFile method
if (typeof serviceHub.models().validateGgufFile === 'function') {
const result = await serviceHub.models().validateGgufFile(filePath)
// Extract baseName and use it as model name if available if (result.metadata) {
const baseName = result.metadata.metadata?.['general.basename'] // Check architecture from metadata
const architecture =
result.metadata.metadata?.['general.architecture']
if (baseName) { // Extract baseName and use it as model name if available
setModelName(baseName) 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 (!result.isValid) {
if (architecture === 'clip') { setValidationError(result.error || 'Model validation failed')
const errorMessage = console.error('Model validation failed:', result.error)
'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) }
} 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<string, string> }
).metadata?.['general.architecture']
// Get general.baseName from metadata
const baseName = (metadata as { metadata?: Record<string, string> })
.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)
console.error( console.error(
'CLIP architecture detected in model file:', 'Non-CLIP architecture detected in mmproj file:',
architecture 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(
if (!result.isValid) { 'Failed to validate mmproj file directly:',
setValidationError(result.error || 'Model validation failed') directError
console.error('Model validation failed:', result.error) )
const errorMessage = `Failed to read MMProj metadata: ${
directError instanceof Error
? directError.message
: 'Unknown error'
}`
setMmprojValidationError(errorMessage)
} }
} }
} else { } catch (error) {
// For mmproj files, we need to manually validate since validateGgufFile rejects CLIP models console.error(`Failed to validate ${fileType} file:`, error)
try { const errorMessage = `Failed to read ${fileType} metadata: ${error instanceof Error ? error.message : 'Unknown error'}`
// Import the readGgufMetadata function directly from Tauri
const { invoke } = await import('@tauri-apps/api/core')
const metadata = await invoke('plugin:llamacpp|read_gguf_metadata', { if (fileType === 'model') {
path: filePath, setValidationError(errorMessage)
}) } else {
// Check if architecture matches expected type
const architecture = (
metadata as { metadata?: Record<string, string> }
).metadata?.['general.architecture']
// Get general.baseName from metadata
const baseName = (metadata as { metadata?: Record<string, string> })
.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)
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('Failed to validate mmproj file directly:', directError)
const errorMessage = `Failed to read MMProj metadata: ${
directError instanceof Error ? directError.message : 'Unknown error'
}`
setMmprojValidationError(errorMessage) setMmprojValidationError(errorMessage)
} }
} finally {
if (fileType === 'model') {
setIsValidating(false)
} else {
setIsValidatingMmproj(false)
}
} }
} catch (error) { },
console.error(`Failed to validate ${fileType} file:`, error) [modelName, serviceHub]
const errorMessage = `Failed to read ${fileType} metadata: ${error instanceof Error ? error.message : 'Unknown error'}` )
if (fileType === 'model') { const validateModelFile = useCallback(
setValidationError(errorMessage) async (filePath: string): Promise<void> => {
} else { await validateGgufFile(filePath, 'model')
setMmprojValidationError(errorMessage) },
} [validateGgufFile]
} finally { )
if (fileType === 'model') {
setIsValidating(false)
} else {
setIsValidatingMmproj(false)
}
}
}, [modelName, serviceHub])
const validateModelFile = useCallback(async (filePath: string): Promise<void> => { const validateMmprojFile = useCallback(
await validateGgufFile(filePath, 'model') async (filePath: string): Promise<void> => {
}, [validateGgufFile]) await validateGgufFile(filePath, 'mmproj')
},
const validateMmprojFile = useCallback(async (filePath: string): Promise<void> => { [validateGgufFile]
await validateGgufFile(filePath, 'mmproj') )
}, [validateGgufFile])
const handleFileSelect = async (type: 'model' | 'mmproj') => { const handleFileSelect = async (type: 'model' | 'mmproj') => {
const selectedFile = await serviceHub.dialog().open({ const selectedFile = await serviceHub.dialog().open({
@ -291,7 +304,11 @@ export const ImportVisionModelDialog = ({
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>{trigger}</DialogTrigger> <DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent> <DialogContent
onInteractOutside={(e) => {
e.preventDefault()
}}
>
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
Import Model Import Model