"use client" import { AlertCircle, Check, ChevronRight, Clock, Eye, EyeOff, Key, Loader2, Plus, Server, Settings2, Sparkles, Trash2, X, Zap, } from "lucide-react" import { useCallback, useEffect, useRef, useState } from "react" import { ProviderCredentialsFields, type SecretField, } from "@/components/provider-credentials-fields" import { ProviderLogo } from "@/components/provider-logo" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { ScrollArea } from "@/components/ui/scroll-area" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Switch } from "@/components/ui/switch" import { useDictionary } from "@/hooks/use-dictionary" import type { UseModelConfigReturn } from "@/hooks/use-model-config" import { getApiEndpoint } from "@/lib/base-path" import { formatMessage } from "@/lib/i18n/utils" import type { ProviderConfig, ProviderName } from "@/lib/types/model-config" import { PROVIDER_INFO, SUGGESTED_MODELS } from "@/lib/types/model-config" import { cn } from "@/lib/utils" interface ModelConfigDialogProps { open: boolean onOpenChange: (open: boolean) => void modelConfig: UseModelConfigReturn } type ValidationStatus = "idle" | "validating" | "success" | "error" // Configuration section with title and optional action function ConfigSection({ title, icon: Icon, action, children, }: { title: string icon: React.ComponentType<{ className?: string }> action?: React.ReactNode children: React.ReactNode }) { return (
{title}
{action}
{children}
) } // Card wrapper with subtle depth function ConfigCard({ children }: { children: React.ReactNode }) { return (
{children}
) } export function ModelConfigDialog({ open, onOpenChange, modelConfig, }: ModelConfigDialogProps) { const dict = useDictionary() const [selectedProviderId, setSelectedProviderId] = useState( null, ) const [showApiKey, setShowApiKey] = useState(false) const [validationStatus, setValidationStatus] = useState("idle") const [validationError, setValidationError] = useState("") const [customModelInput, setCustomModelInput] = useState("") const scrollRef = useRef(null) const validationResetTimeoutRef = useRef | null>(null) const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) const [deleteConfirmText, setDeleteConfirmText] = useState("") const [validatingModelIndex, setValidatingModelIndex] = useState< number | null >(null) const [duplicateError, setDuplicateError] = useState("") const [editError, setEditError] = useState<{ modelId: string message: string } | null>(null) const [dynamicSuggestedModels, setDynamicSuggestedModels] = useState< Partial> >({}) const [loadedSuggestedProviders, setLoadedSuggestedProviders] = useState< Partial> >({}) const [loadingSuggestedProvider, setLoadingSuggestedProvider] = useState(null) const { config, addProvider, updateProvider, deleteProvider, addModel, updateModel, deleteModel, } = modelConfig // Get selected provider const selectedProvider = config.providers.find( (p) => p.id === selectedProviderId, ) // Cleanup validation reset timeout on unmount useEffect(() => { return () => { if (validationResetTimeoutRef.current) { clearTimeout(validationResetTimeoutRef.current) } } }, []) useEffect(() => { if ( !open || selectedProvider?.provider !== "aihubmix" || loadedSuggestedProviders.aihubmix ) { return } let cancelled = false setLoadingSuggestedProvider("aihubmix") fetch(getApiEndpoint("/api/aihubmix-models")) .then((response) => { if (!response.ok) { throw new Error(`Failed to load models: ${response.status}`) } return response.json() }) .then((data: { models?: unknown }) => { if (cancelled || !Array.isArray(data.models)) { return } const models = data.models.filter( (model): model is string => typeof model === "string", ) if (models.length > 0) { setDynamicSuggestedModels((current) => ({ ...current, aihubmix: models, })) } }) .catch((error) => { console.warn("Failed to load AIHubMix models:", error) }) .finally(() => { if (cancelled) { return } setLoadedSuggestedProviders((current) => ({ ...current, aihubmix: true, })) setLoadingSuggestedProvider(null) }) return () => { cancelled = true } }, [open, selectedProvider?.provider, loadedSuggestedProviders.aihubmix]) // Get suggested models for current provider const suggestedModels = selectedProvider ? dynamicSuggestedModels[selectedProvider.provider] || SUGGESTED_MODELS[selectedProvider.provider] || [] : [] const isLoadingSuggestedModels = selectedProvider?.provider === loadingSuggestedProvider // Filter out already-added models from suggestions const existingModelIds = selectedProvider?.models.map((m) => m.modelId) || [] const availableSuggestions = suggestedModels.filter( (modelId) => !existingModelIds.includes(modelId), ) const emptyStateSuggestions = selectedProvider ? (SUGGESTED_MODELS[selectedProvider.provider] || []) .filter((modelId) => !existingModelIds.includes(modelId)) .slice(0, 4) : [] // Handle adding a new provider const handleAddProvider = (providerType: ProviderName) => { const newProvider = addProvider(providerType) setSelectedProviderId(newProvider.id) setValidationStatus("idle") } // Handle provider field updates const handleProviderUpdate = ( field: keyof ProviderConfig, value: string | boolean, ) => { if (!selectedProviderId) return updateProvider(selectedProviderId, { [field]: value }) // Reset validation when credentials change const credentialFields = [ "apiKey", "baseUrl", "awsAccessKeyId", "awsSecretAccessKey", "awsRegion", "vertexApiKey", ] if (credentialFields.includes(field)) { setValidationStatus("idle") updateProvider(selectedProviderId, { validated: false }) } } // Handle adding a model to current provider // Returns true if model was added successfully, false otherwise const handleAddModel = (modelId: string): boolean => { if (!selectedProviderId || !selectedProvider) return false // Prevent duplicate model IDs if (existingModelIds.includes(modelId)) { setDuplicateError(`Model "${modelId}" already exists`) return false } setDuplicateError("") addModel(selectedProviderId, modelId) return true } // Handle deleting a model const handleDeleteModel = (modelConfigId: string) => { if (!selectedProviderId) return deleteModel(selectedProviderId, modelConfigId) } // Handle deleting the provider const handleDeleteProvider = () => { if (!selectedProviderId) return deleteProvider(selectedProviderId) setSelectedProviderId(null) setValidationStatus("idle") setDeleteConfirmOpen(false) } // Validate all models const handleValidate = useCallback(async () => { if (!selectedProvider || !selectedProviderId) return // Check credentials based on provider type const isBedrock = selectedProvider.provider === "bedrock" const isEdgeOne = selectedProvider.provider === "edgeone" const isOllama = selectedProvider.provider === "ollama" const isVertexAI = selectedProvider.provider === "vertexai" if (isBedrock) { if ( !selectedProvider.awsAccessKeyId || !selectedProvider.awsSecretAccessKey || !selectedProvider.awsRegion ) { return } } else if (isVertexAI) { // Vertex AI requires vertexApiKey for Express Mode if (!selectedProvider.vertexApiKey) { return } } else if (!isEdgeOne && !isOllama && !selectedProvider.apiKey) { return } // Need at least one model to validate if (selectedProvider.models.length === 0) { setValidationError("Add at least one model to validate") setValidationStatus("error") return } setValidationStatus("validating") setValidationError("") let allValid = true let errorCount = 0 // Validate each model for (let i = 0; i < selectedProvider.models.length; i++) { const model = selectedProvider.models[i] setValidatingModelIndex(i) try { // For EdgeOne, construct baseUrl from current origin const baseUrl = isEdgeOne ? `${window.location.origin}/api/edgeai` : selectedProvider.baseUrl const response = await fetch("/api/validate-model", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider: selectedProvider.provider, apiKey: selectedProvider.apiKey, baseUrl, modelId: model.modelId, // AWS Bedrock credentials awsAccessKeyId: selectedProvider.awsAccessKeyId, awsSecretAccessKey: selectedProvider.awsSecretAccessKey, awsRegion: selectedProvider.awsRegion, // Vertex AI credentials (Express Mode) vertexApiKey: selectedProvider.vertexApiKey, }), }) const data = await response.json() if (data.valid) { updateModel(selectedProviderId, model.id, { validated: true, validationError: undefined, }) } else { allValid = false errorCount++ updateModel(selectedProviderId, model.id, { validated: false, validationError: data.error || "Validation failed", }) } } catch { allValid = false errorCount++ updateModel(selectedProviderId, model.id, { validated: false, validationError: "Network error", }) } } setValidatingModelIndex(null) if (allValid) { setValidationStatus("success") updateProvider(selectedProviderId, { validated: true }) // Reset to idle after showing success briefly (with cleanup) if (validationResetTimeoutRef.current) { clearTimeout(validationResetTimeoutRef.current) } validationResetTimeoutRef.current = setTimeout(() => { setValidationStatus("idle") validationResetTimeoutRef.current = null }, 1500) } else { setValidationStatus("error") setValidationError(`${errorCount} model(s) failed validation`) } }, [selectedProvider, selectedProviderId, updateProvider, updateModel]) // Get all available provider types const availableProviders = Object.keys(PROVIDER_INFO) as ProviderName[] // Get display name for provider const getProviderDisplayName = (provider: ProviderConfig) => { return provider.name || PROVIDER_INFO[provider.provider].label } // Inline Test button + error, shared across credential layouts. Disabled // until the relevant credentials are present. const renderTestButton = (canValidate: boolean) => (
{validationStatus === "error" && validationError && (

{validationError}

)}
) // Plaintext secret input with show/hide toggle (the user dialog stores // keys client-side, so values are shown directly — unlike the masked // admin panel). The primary key field carries the inline Test button. const renderProviderSecret = (field: SecretField, id: string) => { if (!selectedProvider) return null const value = (selectedProvider[field] as string | undefined) ?? "" // The "primary" credential sits beside the Test button; for Bedrock // the test lives below the region, so its inputs have no inline test. const isBedrock = selectedProvider.provider === "bedrock" const withInlineTest = !isBedrock && (field === "apiKey" || field === "vertexApiKey") const canValidate = field === "vertexApiKey" ? !!selectedProvider.vertexApiKey : selectedProvider.provider === "ollama" || !!selectedProvider.apiKey const input = (
handleProviderUpdate(field, e.target.value) } placeholder={ field === "awsSecretAccessKey" ? dict.modelConfig.enterSecretKey : field === "awsAccessKeyId" ? "AKIA..." : dict.modelConfig.enterApiKey } className="h-9 pr-10 font-mono text-xs" />
) if (!withInlineTest) return input return (
{input} {renderTestButton(canValidate)}
) } return ( {/* Header */}
{dict.modelConfig?.title || "AI Model Configuration"}
{dict.modelConfig?.description || "Configure multiple AI providers and models for your workspace"}
{/* Provider List (Left Sidebar) */}
{dict.modelConfig.providers}
{config.providers.length === 0 ? (

{dict.modelConfig.addProviderHint}

) : ( config.providers.map((provider) => ( )) )}
{/* Add Provider */}
{/* Provider Details (Right Panel) */}
{selectedProvider ? (
{/* Provider Header */}

{ PROVIDER_INFO[ selectedProvider .provider ].label }

{selectedProvider.models .length === 0 ? dict.modelConfig .noModelsConfigured : formatMessage( dict.modelConfig .modelsConfiguredCount, { count: selectedProvider .models .length, }, )}

{selectedProvider.validated && (
{dict.modelConfig.verified}
)}
{/* Configuration Section */} handleProviderUpdate( field, value, ) } renderSecret={({ field, id }) => renderProviderSecret( field, id, ) } footer={ selectedProvider.provider === "bedrock" ? renderTestButton( !!selectedProvider.awsAccessKeyId && !!selectedProvider.awsSecretAccessKey && !!selectedProvider.awsRegion, ) : selectedProvider.provider === "edgeone" ? renderTestButton( true, ) : undefined } /> {/* Models Section */}
{ setCustomModelInput( e.target.value, ) if ( duplicateError ) { setDuplicateError( "", ) } }} onKeyDown={(e) => { if ( e.key === "Enter" && customModelInput.trim() ) { const success = handleAddModel( customModelInput.trim(), ) if (success) { setCustomModelInput( "", ) } } }} className={cn( "h-8 w-44 rounded-lg font-mono text-xs", duplicateError && "border-destructive focus-visible:ring-destructive", )} /> {duplicateError && (

{duplicateError}

)}
} > {/* Model List */}
{selectedProvider.models.length === 0 ? (

{ dict.modelConfig .noModelsConfigured }

{emptyStateSuggestions.length > 0 && (
{emptyStateSuggestions.map( (modelId) => ( ), )}
)}
) : (
{selectedProvider.models.map( (model, index) => (
{/* Status icon */}
{validatingModelIndex !== null && index === validatingModelIndex ? ( // Currently validating
) : validatingModelIndex !== null && index > validatingModelIndex && model.validated === undefined ? ( // Queued
) : model.validated === true ? ( // Valid
) : model.validated === false ? ( // Invalid
) : ( // Not validated yet
)}
{ // Allow free typing - validation happens on blur // Clear edit error when typing if ( editError?.modelId === model.id ) { setEditError( null, ) } if ( selectedProviderId ) { updateModel( selectedProviderId, model.id, { modelId: e .target .value, validated: undefined, validationError: undefined, }, ) } }} onKeyDown={( e, ) => { if ( e.key === "Enter" ) { e.currentTarget.blur() } }} onBlur={( e, ) => { const newModelId = e.target.value.trim() // Helper to show error with shake const showError = ( message: string, ) => { setEditError( { modelId: model.id, message, }, ) e.target.animate( [ { transform: "translateX(0)", }, { transform: "translateX(-4px)", }, { transform: "translateX(4px)", }, { transform: "translateX(-4px)", }, { transform: "translateX(4px)", }, { transform: "translateX(0)", }, ], { duration: 400, easing: "ease-in-out", }, ) e.target.focus() } // Check for empty model name if ( !newModelId ) { showError( dict .modelConfig .modelIdEmpty, ) return } // Check for duplicate const otherModelIds = selectedProvider?.models .filter( ( m, ) => m.id !== model.id, ) .map( ( m, ) => m.modelId, ) || [] if ( otherModelIds.includes( newModelId, ) ) { showError( dict .modelConfig .modelIdExists, ) return } // Clear error on valid blur setEditError( null, ) }} className="flex-1 min-w-0 font-mono text-sm h-8 border-0 bg-transparent focus-visible:bg-background focus-visible:ring-1" />
{/* Show validation error inline */} {model.validated === false && model.validationError && (

{ model.validationError }

)} {/* Show edit error inline */} {editError?.modelId === model.id && (

{ editError.message }

)}
), )}
)}
) : (

{dict.modelConfig.configureProviders}

{dict.modelConfig.selectProviderHint}

)}
{/* Footer */}

{dict.modelConfig.apiKeyStored}

{/* Delete Confirmation Dialog */} { setDeleteConfirmOpen(open) if (!open) setDeleteConfirmText("") }} >
{dict.modelConfig.deleteProvider} {formatMessage(dict.modelConfig.deleteConfirmDesc, { name: selectedProvider ? selectedProvider.name || PROVIDER_INFO[selectedProvider.provider] .label : "this provider", })}
{selectedProvider && selectedProvider.models.length >= 3 && (
setDeleteConfirmText(e.target.value) } placeholder={ dict.modelConfig.typeProviderName } className="h-9" />
)} {dict.modelConfig.cancel} = 3 && deleteConfirmText !== (selectedProvider.name || PROVIDER_INFO[selectedProvider.provider] .label) } className="bg-destructive text-destructive-foreground hover:bg-destructive/90 disabled:opacity-50" > {dict.modelConfig.delete}
) }