// Combobox for a "local model or GenAI provider" field (semantic_search.model, // audio_transcription.model). Shows the built-in model enum values alongside the // GenAI providers holding the relevant role. The role and the i18n key prefix // come from ui:options so each field can reuse this with its own wording. import { useState, useMemo } from "react"; import type { WidgetProps } from "@rjsf/utils"; import { useTranslation } from "react-i18next"; import { Check, ChevronsUpDown } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Command, CommandGroup, CommandItem, CommandList, } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import type { ConfigFormContext } from "@/types/configForm"; import { getSizedFieldClassName } from "../utils"; interface ProviderOption { value: string; label: string; } export function GenAIBackedModelWidget(props: WidgetProps) { const { id, value, disabled, readonly, onChange, schema, registry, options } = props; const { t } = useTranslation(["views/settings"]); const [open, setOpen] = useState(false); const formContext = registry?.formContext as ConfigFormContext | undefined; const fieldClassName = getSizedFieldClassName(options, "sm"); const role = (options?.role as string | undefined) ?? "embeddings"; const i18nPrefix = (options?.i18nPrefix as string | undefined) ?? "semanticSearchModel"; // Built-in model options from schema.examples (populated by transformer // collapsing the anyOf enum+string union) const builtInModels: ProviderOption[] = useMemo(() => { const examples = (schema as Record).examples; if (!Array.isArray(examples)) return []; return examples .filter((v): v is string => typeof v === "string") .map((v) => ({ value: v, label: v })); }, [schema]); // GenAI providers that have the role this field is backed by const roleProviders: ProviderOption[] = useMemo(() => { const genai = ( formContext?.fullConfig as Record | undefined )?.genai; if (!genai || typeof genai !== "object" || Array.isArray(genai)) return []; const providers: ProviderOption[] = []; for (const [key, config] of Object.entries( genai as Record, )) { if (!config || typeof config !== "object" || Array.isArray(config)) continue; const roles = (config as Record).roles; if (Array.isArray(roles) && roles.includes(role)) { providers.push({ value: key, label: key }); } } return providers; }, [formContext?.fullConfig, role]); const currentLabel = builtInModels.find((m) => m.value === value)?.label ?? roleProviders.find((p) => p.value === value)?.label ?? (typeof value === "string" && value ? value : undefined); return ( {builtInModels.length > 0 && ( {builtInModels.map((model) => ( { onChange(model.value); setOpen(false); }} > {model.label} ))} )} {roleProviders.length > 0 && ( {roleProviders.map((provider) => ( { onChange(provider.value); setOpen(false); }} > {provider.label} ))} )} ); }