diff --git a/web-app/src/containers/dialogs/EditModel.tsx b/web-app/src/containers/dialogs/EditModel.tsx new file mode 100644 index 000000000..5fd9b3f85 --- /dev/null +++ b/web-app/src/containers/dialogs/EditModel.tsx @@ -0,0 +1,253 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog' +import { Switch } from '@/components/ui/switch' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip' +import { useModelProvider } from '@/hooks/useModelProvider' +import { + IconPencil, + IconEye, + IconTool, + // IconWorld, + // IconAtom, + IconCodeCircle2, +} from '@tabler/icons-react' +import { useState, useEffect } from 'react' +import { useTranslation } from '@/i18n/react-i18next-compat' + +// No need to define our own interface, we'll use the existing Model type +type DialogEditModelProps = { + provider: ModelProvider + modelId?: string // Optional model ID to edit +} + +export const DialogEditModel = ({ + provider, + modelId, +}: DialogEditModelProps) => { + const { t } = useTranslation() + const { updateProvider } = useModelProvider() + const [selectedModelId, setSelectedModelId] = useState('') + const [capabilities, setCapabilities] = useState>({ + completion: false, + vision: false, + tools: false, + reasoning: false, + embeddings: false, + web_search: false, + }) + + // Initialize with the provided model ID or the first model if available + useEffect(() => { + if (modelId) { + setSelectedModelId(modelId) + } else if (provider.models && provider.models.length > 0) { + setSelectedModelId(provider.models[0].id) + } + }, [provider, modelId]) + + // Get the currently selected model + const selectedModel = provider.models.find( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (m: any) => m.id === selectedModelId + ) + + // Initialize capabilities from selected model + useEffect(() => { + if (selectedModel) { + const modelCapabilities = selectedModel.capabilities || [] + setCapabilities({ + 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'), + }) + } + }, [selectedModel]) + + // Track if capabilities were updated by user action + const [capabilitiesUpdated, setCapabilitiesUpdated] = useState(false) + + // Update model capabilities - only update local state + const handleCapabilityChange = (capability: string, enabled: boolean) => { + setCapabilities((prev) => ({ + ...prev, + [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 + useEffect(() => { + // Only run if capabilities were updated by user action and we have a selected model + if (!capabilitiesUpdated || !selectedModel) return + + // Reset the flag + setCapabilitiesUpdated(false) + + // Create updated capabilities array from the state + const updatedCapabilities = Object.entries(capabilities) + .filter(([, isEnabled]) => isEnabled) + .map(([capName]) => capName) + + // Find and update the model in the provider + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const updatedModels = provider.models.map((m: any) => { + if (m.id === selectedModelId) { + return { + ...m, + capabilities: updatedCapabilities, + } + } + return m + }) + + // Update the provider with the updated models + updateProvider(provider.provider, { + ...provider, + models: updatedModels, + }) + }, [ + capabilitiesUpdated, + capabilities, + provider, + selectedModel, + selectedModelId, + updateProvider, + ]) + + if (!selectedModel) { + return null + } + + return ( + + +
+ +
+
+ + + + {t('providers:editModel.title', { modelId: selectedModel.id })} + + + {t('providers:editModel.description')} + + + +
+

+ {t('providers:editModel.capabilities')} +

+
+
+
+ + + {t('providers:editModel.tools')} + +
+ + handleCapabilityChange('tools', checked) + } + /> +
+ +
+
+ + + {t('providers:editModel.vision')} + +
+ + + + handleCapabilityChange('vision', checked) + } + /> + + + {t('providers:editModel.notAvailable')} + + +
+ +
+
+ + + {t('providers:editModel.embeddings')} + +
+ + + + handleCapabilityChange('embeddings', checked) + } + /> + + + {t('providers:editModel.notAvailable')} + + +
+ + {/*
+
+ + Web Search +
+ + handleCapabilityChange('web_search', checked) + } + /> +
*/} + + {/*
+
+ + {t('reasoning')} +
+ + handleCapabilityChange('reasoning', checked) + } + /> +
*/} +
+
+
+
+ ) +} diff --git a/web-app/src/routes/settings/providers/$providerName.tsx b/web-app/src/routes/settings/providers/$providerName.tsx index 180b42c03..9d456cc40 100644 --- a/web-app/src/routes/settings/providers/$providerName.tsx +++ b/web-app/src/routes/settings/providers/$providerName.tsx @@ -22,6 +22,7 @@ import { useTranslation } from '@/i18n/react-i18next-compat' import Capabilities from '@/containers/Capabilities' import { DynamicControllerSetting } from '@/containers/dynamicControllerSetting' import { RenderMarkdown } from '@/containers/RenderMarkdown' +import { DialogEditModel } from '@/containers/dialogs/EditModel' import { DialogAddModel } from '@/containers/dialogs/AddModel' import { ModelSetting } from '@/containers/ModelSetting' import { DialogDeleteModel } from '@/containers/dialogs/DeleteModel' @@ -583,6 +584,10 @@ function ProviderDetail() { } actions={
+ {model.settings && (