import { useEffect, useRef, useState, type Dispatch, type SetStateAction } from "react"; import { ChevronDown, ChevronRight, GripVertical, ListOrdered, Loader2, Plus, Trash2, } from "lucide-react"; import { useTranslation } from "react-i18next"; import { ModelIdPicker, ProviderPicker, ProviderPickerIcon, formatContextWindow, formatModelContextWindow, normalizeContextWindowTokens, settingsProviderConfigured, } from "@/components/settings/shared/ModelControls"; import { SettingsGroup, SettingsRow, SettingsSectionTitle, SettingsStatusMessage, StatusPill, } from "@/components/settings/shared/SettingsControls"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { SegmentedControl } from "@/components/ui/segmented-control"; import { cn } from "@/lib/utils"; import type { SettingsPayload } from "@/lib/types"; export interface AgentSettingsDraft { model: string; provider: string; modelPreset: string; maxTokens: number; contextWindowTokens: number; temperature: number; reasoningEffort: string; timezone: string; toolHintMaxLength: number; } const CONTEXT_WINDOW_TOKEN_OPTIONS = [65_536, 200_000, 262_144, 500_000, 1_048_576] as const; function modelPresetValue(payload: SettingsPayload): string { return ( payload.model_call_order?.[0] ?? payload.model_presets.find((preset) => !preset.is_default)?.name ?? "" ); } export const DEFAULT_AGENT_SETTINGS_DRAFT: AgentSettingsDraft = { model: "", provider: "", modelPreset: "", maxTokens: 8192, contextWindowTokens: 200_000, temperature: 0.1, reasoningEffort: "", timezone: "UTC", toolHintMaxLength: 40, }; export function agentDraftFromPayload( payload: SettingsPayload, preferredPresetName?: string, ): AgentSettingsDraft { const activePresetName = preferredPresetName ?? modelPresetValue(payload); const activePreset = payload.model_presets.find( (preset) => !preset.is_default && preset.name === activePresetName, ) ?? null; return { model: activePreset?.model ?? payload.agent.model, provider: activePreset?.provider ?? payload.agent.provider ?? payload.agent.resolved_provider ?? "", modelPreset: activePresetName, maxTokens: activePreset?.max_tokens ?? payload.agent.max_tokens, contextWindowTokens: normalizeContextWindowTokens( activePreset?.context_window_tokens ?? payload.agent.context_window_tokens, ), temperature: activePreset?.temperature ?? payload.agent.temperature, reasoningEffort: activePreset?.reasoning_effort ?? "", timezone: payload.agent.timezone, toolHintMaxLength: payload.agent.tool_hint_max_length, }; } export function ModelPresetDeleteDialog({ preset, deleting, onOpenChange, onConfirm, }: { preset: SettingsPayload["model_presets"][number] | null; deleting: boolean; onOpenChange: (open: boolean) => void; onConfirm: () => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string, values?: Record) => t(key, { defaultValue: fallback, ...(values ?? {}) }); return ( {tx("settings.models.deletePresetTitle", "Delete model preset?")} {tx( "settings.models.deletePresetHelp", "This removes the preset “{{name}}”. Provider credentials are not affected.", { name: preset?.name ?? "" }, )} ); } export function ModelsSettings({ token, form, setForm, editingPresetName, presetNameError, settings, dirty, creating, creatingSaving, callOrder, saving, orderSaving, migrationSaving, showBrandLogos, providerSaving, onChangeCallOrder, onProviderOAuthLogin, onSave, onMigrate, onBeginCreate, onCancelCreate, onClearPresetNameError, onSelectConfiguration, onDeleteConfiguration, }: { token: string; form: AgentSettingsDraft; setForm: Dispatch>; editingPresetName: string; presetNameError: string | null; settings: SettingsPayload; dirty: boolean; creating: boolean; creatingSaving: boolean; callOrder: string[]; saving: boolean; orderSaving: boolean; migrationSaving: boolean; showBrandLogos: boolean; providerSaving: string | null; onChangeCallOrder: (order: string[]) => void; onProviderOAuthLogin: (provider: string) => void; onSave: () => void; onMigrate: () => void; onBeginCreate: () => void; onCancelCreate: () => void; onClearPresetNameError: () => void; onSelectConfiguration: (name: string) => void; onDeleteConfiguration: (preset: SettingsPayload["model_presets"][number]) => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string, values?: Record) => t(key, { defaultValue: fallback, ...(values ?? {}) }); const [editorOpen, setEditorOpen] = useState(false); const presetNameInputRef = useRef(null); const [editorRowKey, setEditorRowKey] = useState(null); const [advancedOpen, setAdvancedOpen] = useState(false); const [draggedCallOrderIndex, setDraggedCallOrderIndex] = useState(null); const [dragOverCallOrderIndex, setDragOverCallOrderIndex] = useState(null); useEffect(() => { if (presetNameError) presetNameInputRef.current?.focus(); }, [presetNameError]); const namedPresets = settings.model_presets.filter((preset) => !preset.is_default); const namedPresetsByName = new Map(namedPresets.map((preset) => [preset.name, preset])); const unorderedPresets = namedPresets.filter((preset) => !callOrder.includes(preset.name)); const callOrderOccurrences = new Map(); const presetRows = [ ...callOrder.map((name, orderIndex) => { const occurrence = callOrderOccurrences.get(name) ?? 0; callOrderOccurrences.set(name, occurrence + 1); return { key: `ordered:${name}:${occurrence}`, name, orderIndex, preset: namedPresetsByName.get(name), }; }), ...unorderedPresets.map((preset) => ({ key: `disabled:${preset.name}`, name: preset.name, orderIndex: -1, preset, })), ]; const selectedPreset = namedPresetsByName.get(editingPresetName) ?? null; const activeEditorRowKey = editorRowKey ?? presetRows.find((row) => row.name === selectedPreset?.name)?.key ?? null; useEffect(() => { setAdvancedOpen(false); }, [editorOpen, selectedPreset?.name]); const configuredProviders = settings.providers.filter((provider) => provider.configured); const selectedProvider = settings.providers.find((provider) => provider.name === form.provider); const selectableProviders = uniqueProviders([ ...configuredProviders, ...(selectedProvider ? [selectedProvider] : []), ]); const showAutoProvider = selectedPreset?.provider === "auto" || form.provider === "auto"; const providerOptions = showAutoProvider ? [{ name: "auto", label: tx("settings.values.auto", "Auto") }, ...selectableProviders] : selectableProviders; const providerValue = providerOptions.some((provider) => provider.name === form.provider) ? form.provider : ""; const selectedProviderNeedsSignIn = selectedProvider?.auth_type === "oauth" && !selectedProvider.configured; const selectedProviderSigningIn = providerSaving === selectedProvider?.name; const selectedProviderConfigured = settingsProviderConfigured( settings, form.provider, selectedPreset?.resolved_provider, ); const modelFieldsMissing = !form.model.trim() || !form.provider.trim() || !form.modelPreset.trim() || form.maxTokens <= 0 || form.temperature < 0 || form.temperature > 2; const selectedPresetReferenced = Boolean( selectedPreset && callOrder.includes(selectedPreset.name), ); const callOrderBusy = orderSaving || saving; const selectPreset = ( preset: SettingsPayload["model_presets"][number], rowKey: string, ) => { const toggleCurrentPreset = !creating && selectedPreset?.name === preset.name && activeEditorRowKey === rowKey; onSelectConfiguration(preset.name); if (toggleCurrentPreset) { setEditorOpen((open) => !open); return; } setForm((prev) => ({ ...prev, modelPreset: preset.name, model: preset.model, provider: preset.provider, maxTokens: preset.max_tokens, contextWindowTokens: normalizeContextWindowTokens(preset.context_window_tokens), temperature: preset.temperature, reasoningEffort: preset.reasoning_effort ?? "", })); setEditorRowKey(rowKey); setEditorOpen(true); }; const moveCallOrderItem = (index: number, offset: -1 | 1) => { if (callOrderBusy) return; const nextIndex = index + offset; if (nextIndex < 0 || nextIndex >= callOrder.length) return; const next = [...callOrder]; [next[index], next[nextIndex]] = [next[nextIndex], next[index]]; onChangeCallOrder(next); }; const removeCallOrderItem = (index: number) => { if (callOrderBusy || callOrder.length <= 1) return; onChangeCallOrder(callOrder.filter((_, itemIndex) => itemIndex !== index)); }; const dropCallOrderItem = (targetIndex: number) => { if ( callOrderBusy || draggedCallOrderIndex === null || draggedCallOrderIndex === targetIndex ) { setDraggedCallOrderIndex(null); setDragOverCallOrderIndex(null); return; } const next = [...callOrder]; const moved = next.splice(draggedCallOrderIndex, 1)[0]; if (!moved) { setDraggedCallOrderIndex(null); setDragOverCallOrderIndex(null); return; } next.splice(targetIndex, 0, moved); setDraggedCallOrderIndex(null); setDragOverCallOrderIndex(null); onChangeCallOrder(next); }; const renderPresetEditor = () => (
{creating ? (
{tx("settings.models.newPreset", "New model preset")}
) : null}
{ onClearPresetNameError(); setForm((prev) => ({ ...prev, modelPreset: event.target.value })); }} className={cn( "h-8 rounded-full text-[13px]", presetNameError && "border-destructive/70 focus-visible:border-destructive focus-visible:ring-destructive/25", )} /> {presetNameError ? ( ) : null}
setForm((prev) => ({ ...prev, provider, model: provider === prev.provider ? prev.model : "", })) } /> {selectedProviderNeedsSignIn ? ( ) : null} setForm((prev) => ({ ...prev, model }))} /> {advancedOpen ? (
setForm((prev) => ({ ...prev, ...value }))} />
) : null}
{creating ? ( ) : selectedPreset ? (
{selectedPresetReferenced ? ( {tx( "settings.models.removeBeforeDelete", "Remove this preset from the call order before deleting it.", )} ) : null}
) : null}
); return (
{tx("settings.models.presets", "Model presets")} {!settings.model_call_order_editable ? (

{tx("settings.models.convertTitle", "Convert the current model setup")}

{tx( "settings.models.convertHelp", "Turn the existing primary and fallback models into presets so their order can be managed here.", )}

) : ( <>
{presetRows.map(({ key, name, orderIndex, preset }) => { const ordered = orderIndex >= 0; const provider = preset ? modelPresetProviderKey(preset, settings) : settings.agent.resolved_provider ?? settings.agent.provider; const presetConfigured = preset ? settingsProviderConfigured( settings, preset.provider, preset.resolved_provider, ) : true; const isDropTarget = ordered && dragOverCallOrderIndex === orderIndex && draggedCallOrderIndex !== orderIndex; const dropAfterTarget = isDropTarget && draggedCallOrderIndex !== null && draggedCallOrderIndex < orderIndex; const isSelected = editorOpen && !creating && activeEditorRowKey === key && selectedPreset?.name === name; const presetRow = (
{ if (!ordered || callOrderBusy) { event.preventDefault(); return; } event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", name); setDraggedCallOrderIndex(orderIndex); setDragOverCallOrderIndex(orderIndex); }} onDragEnd={() => { setDraggedCallOrderIndex(null); setDragOverCallOrderIndex(null); }} onDragEnter={(event) => { if (ordered && draggedCallOrderIndex !== null) { event.preventDefault(); setDragOverCallOrderIndex(orderIndex); } }} onDragOver={(event) => { if (!ordered || draggedCallOrderIndex === null) return; event.preventDefault(); event.dataTransfer.dropEffect = "move"; }} onDrop={(event) => { if (!ordered) return; event.preventDefault(); dropCallOrderItem(orderIndex); }} onKeyDown={(event) => { if (event.currentTarget !== event.target) return; if (ordered && event.key === "ArrowUp") { event.preventDefault(); moveCallOrderItem(orderIndex, -1); } else if (ordered && event.key === "ArrowDown") { event.preventDefault(); moveCallOrderItem(orderIndex, 1); } else if ((event.key === "Enter" || event.key === " ") && preset) { event.preventDefault(); selectPreset(preset, key); } }} className={cn( "group relative flex min-h-[76px] select-none items-center gap-3 px-4 py-3 outline-none transition-[background-color,opacity] duration-150 sm:px-5", ordered && (callOrderBusy ? "cursor-wait" : "cursor-grab active:cursor-grabbing"), "hover:bg-muted/25", isDropTarget && !dropAfterTarget && "before:absolute before:inset-x-4 before:top-0 before:z-10 before:h-0.5 before:rounded-full before:bg-foreground sm:before:inset-x-5", isDropTarget && dropAfterTarget && "after:absolute after:inset-x-4 after:bottom-0 after:z-10 after:h-0.5 after:rounded-full after:bg-foreground sm:after:inset-x-5", ordered && draggedCallOrderIndex === orderIndex && "opacity-35", isSelected && "bg-muted/45 hover:bg-muted/45", "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring", )} > {ordered ? ( ) : ( )}
); return (
{presetRow} {isSelected ? renderPresetEditor() : null}
); })}
{!creating ? ( ) : ( )} {orderSaving ? ( {tx("settings.actions.saving", "Saving...")} ) : null}
{creating && editorOpen ? renderPresetEditor() : null} )}
); } function ModelAdvancedFields({ maxTokens, contextWindowTokens, temperature, reasoningEffort, onChange, }: { maxTokens: number; contextWindowTokens: number; temperature: number; reasoningEffort: string; onChange: ( value: Partial< Pick< AgentSettingsDraft, "maxTokens" | "contextWindowTokens" | "temperature" | "reasoningEffort" > >, ) => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const contextWindowOptions = Array.from( new Set([...CONTEXT_WINDOW_TOKEN_OPTIONS, contextWindowTokens]), ).sort((left, right) => left - right); return (
{tx("settings.rows.contextWindow", "Context window")} ({ value: String(tokens), label: formatModelContextWindow(tokens), }))} onChange={(value) => onChange({ contextWindowTokens: normalizeContextWindowTokens(Number(value)) }) } />
); } function uniqueProviders( providers: SettingsPayload["providers"], ): SettingsPayload["providers"] { const seen = new Set(); return providers.filter((provider) => { if (seen.has(provider.name)) return false; seen.add(provider.name); return true; }); } function modelPresetProviderKey( preset: SettingsPayload["model_presets"][number], settings: SettingsPayload, options: { draftProvider?: string } = {}, ): string { const provider = options.draftProvider ?? preset.provider; if (provider === "auto") { return ( preset.resolved_provider || settings.agent.resolved_provider || settings.agent.provider || preset.provider ); } return provider; }