"use client" import { AlertCircle, Check, CheckCircle2, ChevronRight, CircleDashed, Eye, EyeOff, Loader2, Plus, RefreshCw, Trash2, X, } from "lucide-react" import { useCallback, useEffect, useId, useRef, useState } from "react" import { useChatEngine } from "@/components/chat/chat-engine" import { ProviderCredentialsFields, type SecretField, } from "@/components/provider-credentials-fields" import { ProviderLogo } from "@/components/provider-logo" import { ProviderPicker } from "@/components/settings/provider-picker" import { ProvidersList } from "@/components/settings/providers-list" import { SettingsHeader } from "@/components/settings/settings-header" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { Button } from "@/components/ui/button" import { Command, CommandEmpty, CommandInput, CommandItem, CommandList, } from "@/components/ui/command" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { ScrollArea } from "@/components/ui/scroll-area" 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 { ListedModel } from "@/lib/provider-models" import { hasCredentials } from "@/lib/provider-setup" import { STORAGE_KEYS } from "@/lib/storage" import type { ModelConfig, ProviderConfig, ProviderName, } from "@/lib/types/model-config" import { PROVIDER_INFO, SUGGESTED_MODELS } from "@/lib/types/model-config" import { cn } from "@/lib/utils" import { useUiStore } from "@/stores/ui-store" interface ModelConfigDialogProps { open: boolean modelConfig: UseModelConfigReturn } type ValidationStatus = "idle" | "validating" | "success" | "error" /** One numbered step of a provider's setup; a check mark once done */ function Step({ n, done, title, action, children, }: { n: number done: boolean title: string action?: React.ReactNode children: React.ReactNode }) { return (
{done ? : n}

{title}

{action}
{children}
) } /** * The latest test of each provider. Kept outside the component: this view * mounts anew when settings reopen or another tab comes back, and a test * started before must not overwrite a newer one's results. */ const latestTestRun = new Map() /** Providers as last saved (the model config is saved on every change) */ function savedProviders(): ProviderConfig[] { try { const stored = localStorage.getItem(STORAGE_KEYS.modelConfigs) return stored ? (JSON.parse(stored) as { providers: ProviderConfig[] }).providers : [] } catch { return [] } } export function ModelConfigDialog({ open, modelConfig, }: ModelConfigDialogProps) { const dict = useDictionary() const t = dict.modelConfig const engine = useChatEngine() // Like the chat's model picker, "Use in the chat" waits while a request // runs: the answer keeps the model it was sent with const chatLocked = (engine.isBusy && !engine.error) || engine.isLeaving // The page shown lives in the UI store: a chat error or the model // picker can open settings on a provider or on the provider picker const page = useUiStore((s) => s.modelsPage) const setModelsPage = useUiStore((s) => s.setModelsPage) const selectedProviderId = typeof page === "object" ? page.providerId : null const [showApiKey, setShowApiKey] = useState(false) const [validationStatus, setValidationStatus] = useState("idle") const [validationError, setValidationError] = useState("") // Per provider, the credentials a test was refused with: the key field is // marked until they change, here or in another tab, or a test passes. A // model list says nothing about the key: some providers list models // without one. const [rejectedKeys, setRejectedKeys] = useState>({}) const markRejected = ( providerId: string, askedWith: string, rejected: boolean, ) => setRejectedKeys(({ [providerId]: _, ...rest }) => rejected ? { ...rest, [providerId]: askedWith } : rest, ) const [customModelInput, setCustomModelInput] = useState("") const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) const [deleteConfirmText, setDeleteConfirmText] = useState("") // Models whose test is running (they are all tested at once) const [validatingModelIds, setValidatingModelIds] = useState>( () => new Set(), ) const [duplicateError, setDuplicateError] = useState("") const [editError, setEditError] = useState<{ modelId: string message: string } | null>(null) // Model ID being typed; written to the config only when valid on blur const [modelIdDraft, setModelIdDraft] = useState<{ id: string value: string } | null>(null) // Models fetched from the provider, per provider config, with the // credentials they were fetched with const [fetchedModels, setFetchedModels] = useState< Record >({}) // The providers whose model list is being fetched const [fetchingFor, setFetchingFor] = useState([]) const [fetchModelsError, setFetchModelsError] = useState("") const [modelPickerOpen, setModelPickerOpen] = useState(false) // models.dev data for hints, loaded with the dialog (it is ~180 KB) const [getModelInfo, setGetModelInfo] = useState< typeof import("@/lib/model-catalog").getModelInfo | null >(null) const { config, addProvider, updateProvider, deleteProvider, addModel, updateModel, deleteModel, } = modelConfig // Get selected provider const selectedProvider = config.providers.find( (p) => p.id === selectedProviderId, ) // Another provider shown: what was shown for the one before goes, also // when the page is changed from outside (openSettings) const [shownProviderId, setShownProviderId] = useState(selectedProviderId) if (shownProviderId !== selectedProviderId) { setShownProviderId(selectedProviderId) setValidationStatus("idle") setValidationError("") setShowApiKey(false) setFetchModelsError("") setModelPickerOpen(false) setCustomModelInput("") setDuplicateError("") setEditError(null) setModelIdDraft(null) } // For requests that finish after the user switched provider or edited // a model id const selectedProviderIdRef = useRef(selectedProviderId) selectedProviderIdRef.current = selectedProviderId const configRef = useRef(config) configRef.current = config // Set when this view goes away (settings closed or on another tab): its // config no longer follows edits, the saved one does const closedRef = useRef(false) useEffect(() => { closedRef.current = false return () => { closedRef.current = true } }, []) // The providers as they are now: once this view is gone, as saved const providersNow = () => closedRef.current ? savedProviders() : configRef.current.providers // A model list or test result belongs to the credentials it was asked // with; they can change meanwhile, here or in another tab const credentialsOf = (providerId: string) => { const p = providersNow().find((x) => x.id === providerId) return JSON.stringify([ p?.provider, p?.apiKey, p?.baseUrl, p?.awsAccessKeyId, p?.awsSecretAccessKey, p?.awsRegion, p?.awsSessionToken, p?.vertexApiKey, ]) } // The button that changed the page went with the old page: the new // page's title takes the focus, unless one of its fields took it // (autoFocus). Not on the first page: the dialog places the focus then. const pageRef = useRef(null) // The test result, read out by screen readers const resultId = useId() const useButtonRef = useRef(null) const backButtonRef = useRef(null) // A test passed: its button went with the success panel, whose first // button takes the focus (it is described by the result) unless the // focus went somewhere on the page meanwhile useEffect(() => { if (validationStatus !== "success") return const root = pageRef.current if (!root || root.contains(document.activeElement)) return const use = useButtonRef.current ;(use && !use.disabled ? use : backButtonRef.current)?.focus() }, [validationStatus]) const pageKey = selectedProvider?.id ?? (page === "picker" ? "picker" : "list") const shownPageRef = useRef(pageKey) useEffect(() => { if (shownPageRef.current === pageKey) return shownPageRef.current = pageKey const root = pageRef.current if (!root || root.contains(document.activeElement)) return root.querySelector("[data-page-title]")?.focus() }, [pageKey]) // Discard an unfinished model ID edit when the dialog closes useEffect(() => { if (!open) setModelIdDraft(null) }, [open]) useEffect(() => { if (!open || getModelInfo) return import("@/lib/model-catalog").then((catalog) => setGetModelInfo(() => catalog.getModelInfo), ) }, [open, getModelInfo]) // What to do about an error kind, worded for this page, where the // credentials can be fixed const errorHints = (p: ProviderConfig): Record => ({ ...(dict.errors.llm as Record), invalid_api_key: p.provider === "bedrock" && !p.apiKey ? t.awsKeysRejected : t.keyRejected, model_not_found: t.modelNotFound, }) const handleFetchModels = async () => { if (!selectedProvider) return const providerId = selectedProvider.id const askedWith = credentialsOf(providerId) const hints = errorHints(selectedProvider) setFetchingFor((current) => [...current, providerId]) setFetchModelsError("") try { const response = await fetch( getApiEndpoint("/api/provider-models"), { method: "POST", headers: { "Content-Type": "application/json", "x-access-code": localStorage.getItem(STORAGE_KEYS.accessCode) || "", }, body: JSON.stringify({ provider: selectedProvider.provider, apiKey: selectedProvider.apiKey, baseUrl: selectedProvider.baseUrl, }), }, ) const data = await response.json().catch(() => ({})) if (credentialsOf(providerId) !== askedWith) return // The picker and the error belong to the provider shown const stillShown = selectedProviderIdRef.current === providerId if (Array.isArray(data.models)) { setFetchedModels((current) => ({ ...current, [providerId]: { askedWith, models: data.models }, })) if (!stillShown) return if (data.models.length > 0) setModelPickerOpen(true) else setFetchModelsError(t.noModelsReturned) } else { if (!stillShown) return setFetchModelsError( [hints[data.code], data.error].filter(Boolean).join(" ") || formatMessage(t.requestFailed, { status: response.status, }), ) } } catch { if ( selectedProviderIdRef.current === providerId && credentialsOf(providerId) === askedWith ) { setFetchModelsError(dict.errors.networkError) } } finally { setFetchingFor((current) => current.filter((id) => id !== providerId), ) } } // The provider's own list, if fetched with the credentials it has now, // else the suggested models const fetched = selectedProvider ? fetchedModels[selectedProvider.id] : undefined const suggestedModels: ListedModel[] = selectedProvider ? fetched && fetched.askedWith === credentialsOf(selectedProvider.id) ? fetched.models : (SUGGESTED_MODELS[selectedProvider.provider] || []).map((id) => ({ id, })) : [] // The picker closes when there is nothing left to pick from, so it does // not open by itself once there is again if (modelPickerOpen && suggestedModels.length === 0) { setModelPickerOpen(false) } const keyRejected = !!selectedProvider && rejectedKeys[selectedProvider.id] === credentialsOf(selectedProvider.id) // Tool calls are what drawing needs: false when known to be missing const supportsTools = (model: ListedModel) => selectedProvider ? (model.tools ?? getModelInfo?.(selectedProvider.provider, model.id)?.tools) : undefined // Filter out already-added models from suggestions const existingModelIds = selectedProvider?.models.map((m) => m.modelId) || [] const availableSuggestions = suggestedModels.filter( (model) => !existingModelIds.includes(model.id), ) // A few suggested models to add with one click const quickSuggestions = selectedProvider ? (SUGGESTED_MODELS[selectedProvider.provider] || []) .filter((modelId) => !existingModelIds.includes(modelId)) .slice(0, 4) : [] const openProvider = (providerId: string) => setModelsPage({ providerId }) // Handle adding a new provider const handleAddProvider = (providerType: ProviderName) => { const newProvider = addProvider(providerType) // With the first suggested model picked, a key is all a test needs const firstModel = SUGGESTED_MODELS[providerType]?.[0] if (firstModel) addModel(newProvider.id, firstModel) openProvider(newProvider.id) } // Handle provider field updates const handleProviderUpdate = (updates: Partial) => { if (!selectedProviderId || !selectedProvider) return // Reset validation of the provider and its models when credentials change const credentialFields = [ "apiKey", "baseUrl", "awsAccessKeyId", "awsSecretAccessKey", "awsRegion", "awsSessionToken", "vertexApiKey", ] if (Object.keys(updates).some((f) => credentialFields.includes(f))) { setValidationStatus("idle") // A test of this provider still running no longer counts const ids = new Set(selectedProvider.models.map((m) => m.id)) setValidatingModelIds( (prev) => new Set([...prev].filter((id) => !ids.has(id))), ) setFetchModelsError("") updates = { ...updates, validated: false, models: selectedProvider.models.map((m) => ({ ...m, validated: undefined, validationError: undefined, validationWarning: undefined, responseTime: undefined, })), } } updateProvider(selectedProviderId, updates) } // Empty the secrets of the Bedrock sign-in the user switched away from; // a session token belongs to the access keys const clearSecrets = (fields: SecretField[]) => { if (!selectedProvider) return const cleared = [ ...fields, ...(fields.includes("awsAccessKeyId") ? (["awsSessionToken"] as const) : []), ].filter((field) => selectedProvider[field]) // Nothing filled in: the test results still hold if (cleared.length === 0) return const updates = Object.fromEntries(cleared.map((field) => [field, ""])) // Access keys kept behind an API key were never used (the server // takes the API key first): the test results still hold if (selectedProvider.apiKey && !fields.includes("apiKey")) { updateProvider(selectedProvider.id, updates) } else { handleProviderUpdate(updates) } } // 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(t.modelIdExists) return false } setDuplicateError("") addModel(selectedProviderId, modelId) return true } const addCustomModel = () => { const modelId = customModelInput.trim() if (modelId && handleAddModel(modelId)) setCustomModelInput("") } // 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) setModelsPage("list") setValidationStatus("idle") setDeleteConfirmOpen(false) } // Validate all models const handleValidate = useCallback(async () => { if (!selectedProvider || !selectedProviderId) return if (!hasCredentials(selectedProvider)) return // Need at least one model to validate if (selectedProvider.models.length === 0) { setValidationError(t.addModelFirst) setValidationStatus("error") return } setValidationStatus("validating") setValidationError("") let allValid = true let errorCount = 0 let idChanged = false let rejected = false const askedWith = credentialsOf(selectedProviderId) const run = (latestTestRun.get(selectedProviderId) ?? 0) + 1 latestTestRun.set(selectedProviderId, run) // For EdgeOne, construct baseUrl from current origin const baseUrl = selectedProvider.provider === "edgeone" ? `${window.location.origin}/api/edgeai` : selectedProvider.baseUrl const hints = errorHints(selectedProvider) // Test every model at once; each row updates when its answer arrives setValidatingModelIds( (prev) => new Set([...prev, ...selectedProvider.models.map((m) => m.id)]), ) await Promise.all( selectedProvider.models.map(async (model) => { let update: Partial let code: string | undefined try { const response = await fetch( getApiEndpoint("/api/validate-model"), { method: "POST", headers: { "Content-Type": "application/json", "x-access-code": localStorage.getItem( STORAGE_KEYS.accessCode, ) || "", }, 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, // Temporary AWS credentials, as the chat sends awsSessionToken: selectedProvider.awsSessionToken, // Vertex AI credentials (Express Mode) vertexApiKey: selectedProvider.vertexApiKey, }), }, ) const data = await response.json().catch(() => ({})) code = data.valid ? undefined : data.code update = data.valid ? { validated: true, validationError: undefined, validationWarning: data.warning, responseTime: data.responseTime, } : { validated: false, // The hint for the error's kind, then the // provider's own message validationError: [hints[data.code], data.error] .filter(Boolean) .join(" ") || (response.ok ? t.validationError : formatMessage(t.requestFailed, { status: response.status, })), validationWarning: undefined, } } catch { update = { validated: false, validationError: dict.errors.networkError, validationWarning: undefined, } } // A newer test started: its own results and spinners count, // whatever the credentials are now (they may have come back) if (run !== latestTestRun.get(selectedProviderId)) return // Credentials changed during the test: drop the result. A // change in another tab left the spinner on, so clear it // (model ids are unique, whatever provider is shown). if (credentialsOf(selectedProviderId) !== askedWith) { setValidatingModelIds((prev) => { const next = new Set(prev) next.delete(model.id) return next }) return } // So did this model's id: the result is for the old one const current = providersNow() .find((p) => p.id === selectedProviderId) ?.models.find((m) => m.id === model.id) if (current?.modelId !== model.modelId) { idChanged = true setValidatingModelIds((prev) => { const next = new Set(prev) next.delete(model.id) return next }) return } if (update.validated === false) { allValid = false errorCount++ if (code === "invalid_api_key") rejected = true } updateModel(selectedProviderId, model.id, update) setValidatingModelIds((prev) => { const next = new Set(prev) next.delete(model.id) return next }) }), ) if (run !== latestTestRun.get(selectedProviderId)) return if (credentialsOf(selectedProviderId) !== askedWith) { // The status line is about the provider shown now if (selectedProviderIdRef.current === selectedProviderId) { setValidationStatus("idle") } return } // A model whose id changed was not tested if (allValid && !idChanged) { updateProvider(selectedProviderId, { validated: true }) } markRejected(selectedProviderId, askedWith, rejected) // The status line is about the provider shown now if (selectedProviderIdRef.current !== selectedProviderId) return if (idChanged) { setValidationStatus("idle") } else if (allValid) { setValidationStatus("success") } else { setValidationStatus("error") setValidationError( formatMessage( errorCount === 1 ? t.validationFailedCountOne : t.validationFailedCountOther, { count: errorCount }, ), ) } }, [ selectedProvider, selectedProviderId, updateProvider, updateModel, dict, ]) // Plaintext secret input with show/hide toggle (the user dialog stores // keys client-side, so values are shown directly — unlike the masked // admin panel) const renderProviderSecret = ( field: SecretField | "awsSessionToken", id: string, ) => { if (!selectedProvider) return null const value = (selectedProvider[field] as string | undefined) ?? "" return (
handleProviderUpdate({ [field]: e.target.value }) } placeholder={ field === "awsSecretAccessKey" ? t.enterSecretKey : field === "awsAccessKeyId" ? "AKIA..." : field === "awsSessionToken" ? undefined : t.enterApiKey } aria-invalid={keyRejected || undefined} className={cn( "h-9 pr-10 font-mono text-xs", keyRejected && "border-destructive/60 focus-visible:ring-destructive/30", )} />
) } const providerName = (p: ProviderConfig) => p.name || PROVIDER_INFO[p.provider].label const renderModelRow = (model: ModelConfig) => { if (!selectedProvider) return null const testing = validatingModelIds.has(model.id) const seconds = model.responseTime ? (model.responseTime / 1000).toFixed(1) : undefined return (
{testing ? ( ) : model.validated === true ? ( ) : model.validated === false ? ( ) : ( )} { // Allow free typing - validation happens on blur // Clear edit error when typing if (editError?.modelId === model.id) { setEditError(null) } setModelIdDraft({ id: model.id, value: e.target.value, }) }} onKeyDown={(e) => { if (e.key === "Enter") { e.currentTarget.blur() } }} onBlur={(e) => { const newModelId = e.target.value.trim() // Drop the draft; an invalid ID falls back to the saved one setModelIdDraft(null) // 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(t.modelIdEmpty) return } // Check for duplicate const otherModelIds = selectedProvider?.models .filter((m) => m.id !== model.id) .map((m) => m.modelId) || [] if (otherModelIds.includes(newModelId)) { showError(t.modelIdExists) return } // Clear error on valid blur setEditError(null) if ( selectedProviderId && newModelId !== model.modelId ) { updateModel(selectedProviderId, model.id, { modelId: newModelId, validated: undefined, validationError: undefined, validationWarning: undefined, responseTime: undefined, }) } }} className="h-8 min-w-0 flex-1 border-0 bg-transparent px-1.5 font-mono text-sm shadow-none focus-visible:bg-background focus-visible:ring-1 dark:bg-transparent" /> {!testing && ( {model.validated === true ? seconds ? formatMessage(t.modelWorksTime, { seconds, }) : t.modelWorks : model.validated === false ? t.modelFailed : t.modelUntested} )}
{/* Show validation error inline */} {model.validated === false && model.validationError && (

{model.validationError}

)} {!model.validationWarning && getModelInfo?.(selectedProvider.provider, model.modelId) ?.tools === false && (

{t.mayNotDraw}

)} {model.validated && model.validationWarning && (

{model.validationWarning}

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

{editError.message}

)}
) } const renderProviderPage = (provider: ProviderConfig) => { const models = provider.models const credentialsDone = hasCredentials(provider) const allWork = models.length > 0 && models.every((m) => m.validated === true) const firstWorking = models.find((m) => m.validated === true) const lastSeconds = models.length === 1 && models[0].responseTime ? (models[0].responseTime / 1000).toFixed(1) : undefined const canTest = credentialsDone && models.length > 0 const testing = validationStatus === "validating" const testBlocker = !credentialsDone ? provider.provider === "bedrock" ? t.needAws : provider.provider === "azure" ? t.needAzure : t.needKey : models.length === 0 ? t.needModel : "" const fetchLabel = formatMessage(t.fetchAllModels, { name: providerName(provider), }) const testPassed = validationStatus === "success" && allWork // Until the failed models change, here or in another tab const testFailed = validationStatus === "error" && !!validationError && models.some((m) => m.validated === false) const passedText = models.length === 1 ? formatMessage(t.testPassedOne, { model: models[0].modelId }) : formatMessage(t.testPassedAll, { count: models.length }) const testLabel = models.length === 0 ? t.stepTest : models.length === 1 ? formatMessage(t.testOne, { model: models[0].modelId }) : formatMessage(t.testAll, { count: models.length }) return (

{providerName(provider)}

{/* 1: credentials */} {provider.provider === "edgeone" ? (

{t.edgeoneNoKey}

) : ( handleProviderUpdate({ [field]: value, }) } renderSecret={({ field, id }) => renderProviderSecret(field, id) } /> )}
{/* 2: models */} 0} title={t.stepModels} action={ PROVIDER_INFO[provider.provider].modelList && ( ) } > {fetchModelsError && (

{fetchModelsError}

)} {models.length > 0 && (
{models.map(renderModelRow)}
)} {quickSuggestions.length > 0 && (
{t.suggestedLabel} {quickSuggestions.map((modelId) => ( ))}
)}
{ setCustomModelInput(e.target.value) if (duplicateError) { setDuplicateError("") } }} onKeyDown={(e) => { // Enter that confirms an IME // candidate adds nothing if ( e.nativeEvent.isComposing || e.keyCode === 229 ) { return } if (e.key === "Enter") { addCustomModel() } }} className={cn( "h-8 rounded-lg font-mono text-xs", duplicateError && "border-destructive focus-visible:ring-destructive", )} /> {duplicateError && (

{duplicateError}

)}
{/* Nothing to browse for a provider without suggested models, until its list is fetched. modal: the dialog blocks the wheel outside itself, and the list is rendered outside it */} {suggestedModels.length > 0 && ( {t.noModelsFound} {availableSuggestions.map( (model) => ( { handleAddModel( model.id, ) setModelPickerOpen( false, ) }} className="font-mono text-xs" > {model.id} {supportsTools( model, ) === false && ( { t.noTools } )} ), )} )}
{/* 3: test */} {/* Always on the page, so a screen reader reads out the result when it arrives */}

{testPassed ? passedText : testFailed ? validationError : ""}

{testPassed ? (

{/* A long model id wraps */} {passedText} {lastSeconds && ( {formatMessage( t.answeredIn, { seconds: lastSeconds, }, )} )}

{firstWorking && ( )}
) : ( <>

{t.testHint}

{testFailed && (

{validationError}

)}
{testBlocker && (

{testBlocker}

)} )}
) } return ( <>
{selectedProvider ? ( renderProviderPage(selectedProvider) ) : page === "picker" ? ( p.provider)} onPick={handleAddProvider} onBack={() => setModelsPage("list")} /> ) : ( m.source === "server", )} selectedModelId={modelConfig.selectedModelId} onOpen={openProvider} onAdd={handleAddProvider} onBrowse={() => setModelsPage("picker")} /> )}
{/* Delete Confirmation Dialog */} { setDeleteConfirmOpen(open) if (!open) setDeleteConfirmText("") }} >
{t.deleteProvider} {formatMessage(t.deleteConfirmDesc, { name: selectedProvider ? providerName(selectedProvider) : "this provider", })}
{selectedProvider && selectedProvider.models.length >= 3 && (
setDeleteConfirmText(e.target.value) } placeholder={t.typeProviderName} className="h-9" />
)} {t.cancel} = 3 && deleteConfirmText !== providerName(selectedProvider) } className="bg-destructive text-destructive-foreground hover:bg-destructive/90 disabled:opacity-50" > {t.delete}
) }