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 { Plus, Trash2, ChevronDown, ChevronRight, Wand2, Settings, FolderInput, Loader2, } 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; agents: Record>; onAgentsChange: (agents: Record>) => void; categories: Record>; onCategoriesChange: ( categories: Record>, ) => void; otherFieldsStr: string; onOtherFieldsStrChange: (value: string) => void; } type CustomModelItem = { key: string; model: string }; type BuiltinModelDef = Pick< OmoAgentDef | OmoCategoryDef, "key" | "display" | "descZh" | "descEn" | "recommended" >; type ModelOption = { value: string; label: string }; 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_MODEL_VALUE = "__cc_switch_omo_model_empty__"; const UNAVAILABLE_MODEL_VALUE = "__cc_switch_omo_model_unavailable__"; const EMPTY_VARIANT_VALUE = "__cc_switch_omo_variant_empty__"; const UNAVAILABLE_VARIANT_VALUE = "__cc_switch_omo_variant_unavailable__"; 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) || "", }); } } return customs; } function mergeCustomModelsIntoStore( store: Record>, builtinKeys: Set, customs: CustomModelItem[], ): Record> { const updated = { ...store }; for (const key of Object.keys(updated)) { if (!builtinKeys.has(key)) delete updated[key]; } for (const custom of customs) { if (custom.key.trim()) { updated[custom.key] = { ...updated[custom.key], model: custom.model }; } } return updated; } export function OmoFormFields({ modelOptions, modelVariantsMap = {}, agents, onAgentsChange, categories, onCategoriesChange, otherFieldsStr, onOtherFieldsStrChange, }: OmoFormFieldsProps) { const { t, i18n } = useTranslation(); const isZh = i18n.language?.startsWith("zh"); 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), ); }, [agents, onAgentsChange], ); const syncCustomCategories = useCallback( (customs: CustomModelItem[]) => { onCategoriesChange( mergeCustomModelsIntoStore(categories, BUILTIN_CATEGORY_KEYS, customs), ); }, [categories, onCategoriesChange], ); 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, placeholder?: 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 variantOptions = buildEffectiveVariantOptions( currentModel, currentVariant, ); const hasModel = Boolean(currentModel); 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; }) => (