Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion web-app/src/containers/dialogs/AddEditAssistant.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
import { useState, useEffect, useRef } from 'react'
import {

Check warning on line 2 in web-app/src/containers/dialogs/AddEditAssistant.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

1-2 lines are not covered with tests
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {

Check warning on line 11 in web-app/src/containers/dialogs/AddEditAssistant.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

9-11 lines are not covered with tests
IconPlus,
IconTrash,
IconChevronDown,
IconMoodSmile,
} from '@tabler/icons-react'
import EmojiPicker, { EmojiClickData, Theme } from 'emoji-picker-react'

Check warning on line 17 in web-app/src/containers/dialogs/AddEditAssistant.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

17 line is not covered with tests

import { Textarea } from '@/components/ui/textarea'
import { paramsSettings } from '@/lib/predefinedParams'

Check warning on line 20 in web-app/src/containers/dialogs/AddEditAssistant.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

19-20 lines are not covered with tests

import {

Check warning on line 22 in web-app/src/containers/dialogs/AddEditAssistant.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

22 line is not covered with tests
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useTheme } from '@/hooks/useTheme'
import { teamEmoji } from '@/utils/teamEmoji'
import { AvatarEmoji } from '@/containers/AvatarEmoji'
import { useTranslation } from '@/i18n/react-i18next-compat'
import { cn, isDev } from '@/lib/utils'

Check warning on line 32 in web-app/src/containers/dialogs/AddEditAssistant.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

28-32 lines are not covered with tests

interface AddEditAssistantProps {
open: boolean
Expand All @@ -39,48 +39,48 @@
onSave: (assistant: Assistant) => void
}

export default function AddEditAssistant({
open,
onOpenChange,
editingKey,
initialData,
onSave,
}: AddEditAssistantProps) {
const [avatar, setAvatar] = useState<string | undefined>(initialData?.avatar)

Check warning on line 49 in web-app/src/containers/dialogs/AddEditAssistant.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

42-49 lines are not covered with tests

const [name, setName] = useState(initialData?.name || '')
const [description, setDescription] = useState<string | undefined>(
initialData?.description
)
const [instructions, setInstructions] = useState(
initialData?.instructions || ''
)
const { isDark } = useTheme()
const [paramsKeys, setParamsKeys] = useState<string[]>([''])
const [paramsValues, setParamsValues] = useState<unknown[]>([''])
const [paramsTypes, setParamsTypes] = useState<string[]>(['string'])
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const emojiPickerRef = useRef<HTMLDivElement>(null)
const emojiPickerTriggerRef = useRef<HTMLDivElement>(null)
const [nameError, setNameError] = useState<string | null>(null)
const [toolSteps, setToolSteps] = useState(20)

Check warning on line 66 in web-app/src/containers/dialogs/AddEditAssistant.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

51-66 lines are not covered with tests

// Handle click outside emoji picker or trigger
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
emojiPickerRef.current &&
emojiPickerTriggerRef.current &&
!emojiPickerRef.current.contains(event.target as Node) &&
!emojiPickerTriggerRef.current.contains(event.target as Node)
) {
setShowEmojiPicker(false)
}
}

Check warning on line 79 in web-app/src/containers/dialogs/AddEditAssistant.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

69-79 lines are not covered with tests

if (showEmojiPicker) {
document.addEventListener('mousedown', handleClickOutside)
}

Check warning on line 83 in web-app/src/containers/dialogs/AddEditAssistant.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

81-83 lines are not covered with tests

return () => {
document.removeEventListener('mousedown', handleClickOutside)
Expand Down Expand Up @@ -236,7 +236,11 @@

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogContent
onInteractOutside={(e) => {
e.preventDefault()
}}
>
<DialogHeader>
<DialogTitle>
{editingKey
Expand Down
22 changes: 16 additions & 6 deletions web-app/src/containers/dialogs/AddEditMCPServer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -366,7 +371,12 @@ export default function AddEditMCPServer({

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent showCloseButton={false}>
<DialogContent
showCloseButton={false}
onInteractOutside={(e) => {
e.preventDefault()
}}
>
<DialogHeader>
<DialogTitle className="flex items-center justify-between">
<span>
Expand Down
2 changes: 1 addition & 1 deletion web-app/src/containers/dialogs/AddProviderDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,4 @@ export function AddProviderDialog({
</DialogContent>
</Dialog>
)
}
}
6 changes: 5 additions & 1 deletion web-app/src/containers/dialogs/EditJsonMCPserver.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ export default function EditJsonMCPserver({

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogContent
onInteractOutside={(e) => {
e.preventDefault()
}}
>
<DialogHeader>
<DialogTitle>
{serverName
Expand Down
229 changes: 123 additions & 106 deletions web-app/src/containers/dialogs/ImportVisionModelDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,129 +44,142 @@ export const ImportVisionModelDialog = ({
>(null)
const [isValidatingMmproj, setIsValidatingMmproj] = useState(false)

const validateGgufFile = useCallback(async (
filePath: string,
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
const validateGgufFile = useCallback(
async (filePath: string, fileType: 'model' | 'mmproj'): Promise<void> => {
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<string, string> }
).metadata?.['general.architecture']
// 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']
// 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)
// 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<void> => {
await validateGgufFile(filePath, 'model')
}, [validateGgufFile])
const validateModelFile = useCallback(
async (filePath: string): Promise<void> => {
await validateGgufFile(filePath, 'model')
},
[validateGgufFile]
)

const validateMmprojFile = useCallback(async (filePath: string): Promise<void> => {
await validateGgufFile(filePath, 'mmproj')
}, [validateGgufFile])
const validateMmprojFile = useCallback(
async (filePath: string): Promise<void> => {
await validateGgufFile(filePath, 'mmproj')
},
[validateGgufFile]
)

const handleFileSelect = async (type: 'model' | 'mmproj') => {
const selectedFile = await serviceHub.dialog().open({
Expand Down Expand Up @@ -291,7 +304,11 @@ export const ImportVisionModelDialog = ({
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent>
<DialogContent
onInteractOutside={(e) => {
e.preventDefault()
}}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Import Model
Expand Down
Loading