diff --git a/src/browser/components/Settings/sections/ModelsSection.tsx b/src/browser/components/Settings/sections/ModelsSection.tsx index c2056142c..3c634e772 100644 --- a/src/browser/components/Settings/sections/ModelsSection.tsx +++ b/src/browser/components/Settings/sections/ModelsSection.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback } from "react"; -import { Plus, Trash2 } from "lucide-react"; +import { Plus, Trash2, Pencil, Check, X, Loader2 } from "lucide-react"; import type { ProvidersConfigMap } from "../types"; import { SUPPORTED_PROVIDERS, PROVIDER_DISPLAY_NAMES } from "@/common/constants/providers"; @@ -8,10 +8,18 @@ interface NewModelForm { modelId: string; } +interface EditingState { + provider: string; + originalModelId: string; + newModelId: string; +} + export function ModelsSection() { - const [config, setConfig] = useState({}); + const [config, setConfig] = useState(null); const [newModel, setNewModel] = useState({ provider: "", modelId: "" }); const [saving, setSaving] = useState(false); + const [editing, setEditing] = useState(null); + const [error, setError] = useState(null); // Load config on mount useEffect(() => { @@ -21,26 +29,32 @@ export function ModelsSection() { })(); }, []); - // Get all custom models across providers - const getAllModels = (): Array<{ provider: string; modelId: string }> => { - const models: Array<{ provider: string; modelId: string }> = []; - for (const [provider, providerConfig] of Object.entries(config)) { - if (providerConfig.models) { - for (const modelId of providerConfig.models) { - models.push({ provider, modelId }); - } - } - } - return models; - }; + // Check if a model already exists (for duplicate prevention) + const modelExists = useCallback( + (provider: string, modelId: string, excludeOriginal?: string): boolean => { + if (!config) return false; + const currentModels = config[provider]?.models ?? []; + return currentModels.some((m) => m === modelId && m !== excludeOriginal); + }, + [config] + ); const handleAddModel = useCallback(async () => { - if (!newModel.provider || !newModel.modelId.trim()) return; + if (!config || !newModel.provider || !newModel.modelId.trim()) return; + + const trimmedModelId = newModel.modelId.trim(); + + // Check for duplicates + if (modelExists(newModel.provider, trimmedModelId)) { + setError(`Model "${trimmedModelId}" already exists for this provider`); + return; + } + setError(null); setSaving(true); try { const currentModels = config[newModel.provider]?.models ?? []; - const updatedModels = [...currentModels, newModel.modelId.trim()]; + const updatedModels = [...currentModels, trimmedModelId]; await window.api.providers.setModels(newModel.provider, updatedModels); @@ -54,10 +68,11 @@ export function ModelsSection() { } finally { setSaving(false); } - }, [newModel, config]); + }, [newModel, config, modelExists]); const handleRemoveModel = useCallback( async (provider: string, modelId: string) => { + if (!config) return; setSaving(true); try { const currentModels = config[provider]?.models ?? []; @@ -78,6 +93,78 @@ export function ModelsSection() { [config] ); + const handleStartEdit = useCallback((provider: string, modelId: string) => { + setEditing({ provider, originalModelId: modelId, newModelId: modelId }); + setError(null); + }, []); + + const handleCancelEdit = useCallback(() => { + setEditing(null); + setError(null); + }, []); + + const handleSaveEdit = useCallback(async () => { + if (!config || !editing) return; + + const trimmedModelId = editing.newModelId.trim(); + if (!trimmedModelId) { + setError("Model ID cannot be empty"); + return; + } + + // Only validate duplicates if the model ID actually changed + if (trimmedModelId !== editing.originalModelId) { + if (modelExists(editing.provider, trimmedModelId)) { + setError(`Model "${trimmedModelId}" already exists for this provider`); + return; + } + } + + setError(null); + setSaving(true); + try { + const currentModels = config[editing.provider]?.models ?? []; + const updatedModels = currentModels.map((m) => + m === editing.originalModelId ? trimmedModelId : m + ); + + await window.api.providers.setModels(editing.provider, updatedModels); + + // Refresh config + const cfg = await window.api.providers.getConfig(); + setConfig(cfg); + setEditing(null); + + // Notify other components about the change + window.dispatchEvent(new Event("providers-config-changed")); + } finally { + setSaving(false); + } + }, [editing, config, modelExists]); + + // Show loading state while config is being fetched + if (config === null) { + return ( +
+ + Loading settings... +
+ ); + } + + // Get all custom models across providers + const getAllModels = (): Array<{ provider: string; modelId: string }> => { + const models: Array<{ provider: string; modelId: string }> = []; + for (const [provider, providerConfig] of Object.entries(config)) { + if (providerConfig.models) { + for (const modelId of providerConfig.models) { + models.push({ provider, modelId }); + } + } + } + return models; + }; + const allModels = getAllModels(); return ( @@ -122,6 +209,7 @@ export function ModelsSection() { Add + {error && !editing &&
{error}
} {/* List of custom models */} @@ -130,29 +218,93 @@ export function ModelsSection() {
Custom Models
- {allModels.map(({ provider, modelId }) => ( -
-
- - {PROVIDER_DISPLAY_NAMES[provider as keyof typeof PROVIDER_DISPLAY_NAMES] ?? - provider} - - {modelId} -
- -
- ))} +
+ + {PROVIDER_DISPLAY_NAMES[provider as keyof typeof PROVIDER_DISPLAY_NAMES] ?? + provider} + + {isEditing ? ( +
+ + setEditing((prev) => + prev ? { ...prev, newModelId: e.target.value } : null + ) + } + onKeyDown={(e) => { + if (e.key === "Enter") void handleSaveEdit(); + if (e.key === "Escape") handleCancelEdit(); + }} + className="bg-modal-bg border-border-medium focus:border-accent min-w-0 flex-1 rounded border px-2 py-1 font-mono text-xs focus:outline-none" + autoFocus + /> + {error &&
{error}
} +
+ ) : ( + + {modelId} + + )} +
+
+ {isEditing ? ( + <> + + + + ) : ( + <> + + + + )} +
+ + ); + })} ) : (