import { useState, useCallback, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { Plus, Trash2, ChevronDown, ChevronRight, Wand2, Settings, FolderInput, Loader2, HelpCircle, Check, ChevronsUpDown, X, } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; import { useReadOmoLocalFile } from "@/lib/query/omo"; import { OMO_BUILTIN_AGENTS, OMO_BUILTIN_CATEGORIES, type OmoAgentDef, type OmoCategoryDef, } from "@/types/omo"; const ADVANCED_PLACEHOLDER = `{ "temperature": 0.5, "top_p": 0.9, "budgetTokens": 20000, "prompt_append": "", "permission": { "edit": "allow", "bash": "ask" } }`; interface OmoFormFieldsProps { modelOptions: Array<{ value: string; label: string }>; modelVariantsMap?: Record; presetMetaMap?: Record< string, { options?: Record; limit?: { context?: number; output?: number }; } >; agents: Record>; onAgentsChange: (agents: Record>) => void; categories: Record>; onCategoriesChange: ( categories: Record>, ) => void; otherFieldsStr: string; onOtherFieldsStrChange: (value: string) => void; } export type CustomModelItem = { key: string; model: string; sourceKey?: string; }; type BuiltinModelDef = Pick< OmoAgentDef | OmoCategoryDef, "key" | "display" | "descKey" | "recommended" | "tooltipKey" >; type ModelOption = { value: string; label: string }; function DeferredKeyInput({ value, onCommit, placeholder, className, }: { value: string; onCommit: (value: string) => void; placeholder?: string; className?: string; }) { const [draft, setDraft] = useState(value); useEffect(() => { setDraft(value); }, [value]); return ( setDraft(e.target.value)} onBlur={() => { if (draft !== value) { onCommit(draft); } }} placeholder={placeholder} className={className} /> ); } const BUILTIN_AGENT_KEYS = new Set(OMO_BUILTIN_AGENTS.map((a) => a.key)); const BUILTIN_CATEGORY_KEYS = new Set(OMO_BUILTIN_CATEGORIES.map((c) => c.key)); const EMPTY_VARIANT_VALUE = "__cc_switch_omo_variant_empty__"; function ModelCombobox({ value, options, recommended, onChange, }: { value: string; options: ModelOption[]; recommended?: string; onChange: (value: string) => void; }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const selectedLabel = options.find((o) => o.value === value)?.label; const selectModelText = t("omo.selectModel", { defaultValue: "Select configured model", }); const placeholderText = recommended ? `${selectModelText} (${t("omo.recommendedHint", { model: recommended, defaultValue: "Recommended: {{model}}" })})` : selectModelText; return ( {t("omo.noEnabledModels", { defaultValue: "No configured models", })} {options.map((option) => ( { onChange(option.value); setOpen(false); }} > {option.label} ))} ); } function getAdvancedStr(config: Record | undefined): string { if (!config) return ""; const adv: Record = {}; for (const [k, v] of Object.entries(config)) { if (k !== "model" && k !== "variant") adv[k] = v; } return Object.keys(adv).length > 0 ? JSON.stringify(adv, null, 2) : ""; } function collectCustomModels( store: Record>, builtinKeys: Set, ): CustomModelItem[] { const customs: CustomModelItem[] = []; for (const [k, v] of Object.entries(store)) { if (!builtinKeys.has(k) && typeof v === "object" && v !== null) { customs.push({ key: k, model: ((v as Record).model as string) || "", sourceKey: k, }); } } return customs; } export function mergeCustomModelsIntoStore( store: Record>, builtinKeys: Set, customs: CustomModelItem[], modelVariantsMap: Record, ): Record> { const updated: Record> = {}; for (const [key, value] of Object.entries(store)) { if (builtinKeys.has(key)) { updated[key] = { ...value }; } } for (const custom of customs) { const targetKey = custom.key.trim(); if (!targetKey) continue; const sourceKey = (custom.sourceKey || targetKey).trim(); const sourceEntry = store[sourceKey] ?? store[targetKey]; const nextEntry = { ...(updated[targetKey] || {}), ...(sourceEntry || {}), }; if (custom.model.trim()) { nextEntry.model = custom.model; const currentVariant = typeof nextEntry.variant === "string" ? nextEntry.variant : ""; if (currentVariant) { const validVariants = modelVariantsMap[custom.model] || []; if (!validVariants.includes(currentVariant)) { delete nextEntry.variant; } } updated[targetKey] = nextEntry; continue; } delete nextEntry.model; delete nextEntry.variant; if (Object.keys(nextEntry).length > 0) { updated[targetKey] = nextEntry; } else { delete updated[targetKey]; } } return updated; } export function OmoFormFields({ modelOptions, modelVariantsMap = {}, presetMetaMap: _presetMetaMap = {}, agents, onAgentsChange, categories, onCategoriesChange, otherFieldsStr, onOtherFieldsStrChange, }: OmoFormFieldsProps) { const { t } = useTranslation(); const [mainAgentsOpen, setMainAgentsOpen] = useState(true); const [subAgentsOpen, setSubAgentsOpen] = useState(true); const [categoriesOpen, setCategoriesOpen] = useState(true); const [otherFieldsOpen, setOtherFieldsOpen] = useState(false); const [expandedAgents, setExpandedAgents] = useState>( {}, ); const [expandedCategories, setExpandedCategories] = useState< Record >({}); const [agentAdvancedDrafts, setAgentAdvancedDrafts] = useState< Record >({}); const [categoryAdvancedDrafts, setCategoryAdvancedDrafts] = useState< Record >({}); const [customAgents, setCustomAgents] = useState(() => collectCustomModels(agents, BUILTIN_AGENT_KEYS), ); const [customCategories, setCustomCategories] = useState( () => collectCustomModels(categories, BUILTIN_CATEGORY_KEYS), ); useEffect(() => { setCustomAgents(collectCustomModels(agents, BUILTIN_AGENT_KEYS)); }, [agents]); useEffect(() => { setCustomCategories(collectCustomModels(categories, BUILTIN_CATEGORY_KEYS)); }, [categories]); const syncCustomAgents = useCallback( (customs: CustomModelItem[]) => { onAgentsChange( mergeCustomModelsIntoStore( agents, BUILTIN_AGENT_KEYS, customs, modelVariantsMap, ), ); }, [agents, onAgentsChange, modelVariantsMap], ); const syncCustomCategories = useCallback( (customs: CustomModelItem[]) => { onCategoriesChange( mergeCustomModelsIntoStore( categories, BUILTIN_CATEGORY_KEYS, customs, modelVariantsMap, ), ); }, [categories, onCategoriesChange, modelVariantsMap], ); const buildEffectiveModelOptions = useCallback( (currentModel: string): ModelOption[] => { if (!currentModel) return modelOptions; if (modelOptions.some((item) => item.value === currentModel)) { return modelOptions; } return [ { value: currentModel, label: t("omo.currentValueNotEnabled", { value: currentModel, defaultValue: "{{value}} (current value, not enabled)", }), }, ...modelOptions, ]; }, [modelOptions, t], ); const resolveRecommendedModel = useCallback( (recommended?: string): string | undefined => { if (!recommended || modelOptions.length === 0) return undefined; const exact = modelOptions.find((item) => item.value === recommended); if (exact) return exact.value; const bySuffix = modelOptions.find((item) => item.value.endsWith(`/${recommended}`), ); return bySuffix?.value; }, [modelOptions], ); const renderModelSelect = ( currentModel: string, onChange: (value: string) => void, recommended?: string, ) => { const options = buildEffectiveModelOptions(currentModel); return ( ); }; const buildEffectiveVariantOptions = useCallback( (currentModel: string, currentVariant: string): string[] => { const variantKeys = modelVariantsMap[currentModel] || []; if (!currentVariant || variantKeys.includes(currentVariant)) { return variantKeys; } return [currentVariant, ...variantKeys]; }, [modelVariantsMap], ); const renderVariantSelect = ( currentModel: string, currentVariant: string, onChange: (value: string) => void, ) => { const hasModel = Boolean(currentModel); const modelVariantKeys = hasModel ? modelVariantsMap[currentModel] || [] : []; const hasVariants = modelVariantKeys.length > 0; const shouldShow = hasModel && (hasVariants || Boolean(currentVariant)); if (!shouldShow) { return null; } const variantOptions = buildEffectiveVariantOptions( currentModel, currentVariant, ); const firstIsUnavailable = Boolean(currentVariant) && !(modelVariantsMap[currentModel] || []).includes(currentVariant); return ( ); }; const handleModelChange = ( key: string, model: string, store: Record>, setter: (v: Record>) => void, ) => { if (model.trim()) { const nextEntry: Record = { ...(store[key] || {}), model, }; const currentVariant = typeof nextEntry.variant === "string" ? nextEntry.variant : ""; if (currentVariant) { const validVariants = modelVariantsMap[model] || []; if (!validVariants.includes(currentVariant)) { delete nextEntry.variant; } } setter({ ...store, [key]: nextEntry }); } else { const existing = store[key]; if (existing) { const adv = { ...existing }; delete adv.model; delete adv.variant; if (Object.keys(adv).length > 0) { setter({ ...store, [key]: adv }); } else { const next = { ...store }; delete next[key]; setter(next); } } } }; const handleVariantChange = ( key: string, variant: string, store: Record>, setter: (v: Record>) => void, ) => { const existing = store[key]; if (variant.trim()) { setter({ ...store, [key]: { ...existing, variant } }); return; } if (!existing) return; const nextEntry = { ...existing }; delete nextEntry.variant; if (Object.keys(nextEntry).length > 0) { setter({ ...store, [key]: nextEntry }); return; } const next = { ...store }; delete next[key]; setter(next); }; const handleAdvancedChange = ( key: string, rawJson: string, store: Record>, setter: (v: Record>) => void, ): boolean => { const currentModel = (store[key]?.model as string) || ""; const currentVariant = (store[key]?.variant as string) || ""; if (!rawJson.trim()) { if (currentModel || currentVariant) { setter({ ...store, [key]: { ...(currentModel ? { model: currentModel } : {}), ...(currentVariant ? { variant: currentVariant } : {}), }, }); } else { const next = { ...store }; delete next[key]; setter(next); } return true; } try { const parsed = JSON.parse(rawJson); if ( typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ) { const parsedAdvanced = { ...(parsed as Record) }; delete parsedAdvanced.model; delete parsedAdvanced.variant; setter({ ...store, [key]: { ...(currentModel ? { model: currentModel } : {}), ...(currentVariant ? { variant: currentVariant } : {}), ...parsedAdvanced, }, }); return true; } return false; } catch { return false; } }; type AdvancedScope = "agent" | "category"; const setAdvancedDraft = ( scope: AdvancedScope, key: string, value: string, ) => { if (scope === "agent") { setAgentAdvancedDrafts((prev) => ({ ...prev, [key]: value })); return; } setCategoryAdvancedDrafts((prev) => ({ ...prev, [key]: value })); }; const removeAdvancedDraft = (scope: AdvancedScope, key: string) => { if (scope === "agent") { setAgentAdvancedDrafts((prev) => { const copied = { ...prev }; delete copied[key]; return copied; }); return; } setCategoryAdvancedDrafts((prev) => { const copied = { ...prev }; delete copied[key]; return copied; }); }; const toggleAdvancedEditor = ( scope: AdvancedScope, key: string, advStr: string, isExpanded: boolean, ) => { const willOpen = !isExpanded; if (scope === "agent") { setExpandedAgents((prev) => ({ ...prev, [key]: willOpen })); if (willOpen && agentAdvancedDrafts[key] === undefined) { setAdvancedDraft(scope, key, advStr); } return; } setExpandedCategories((prev) => ({ ...prev, [key]: willOpen })); if (willOpen && categoryAdvancedDrafts[key] === undefined) { setAdvancedDraft(scope, key, advStr); } }; const renderAdvancedEditor = ({ scope, draftKey, configKey, draftValue, store, setter, showHint, }: { scope: AdvancedScope; draftKey: string; configKey: string; draftValue: string; store: Record>; setter: (value: Record>) => void; showHint?: boolean; }) => (