"use client"; /* Declarative config forms - FieldSpec lists render as friendly widgets (toggle/number/ select/model/csv/textarea) instead of raw JSON. Shared by the canvas node inspector (workflows.tsx) and the middleware stack (AgentConfig.tsx). A form merges only its own keys into the config, so unknown/advanced keys are preserved. */ import { Field, Toggle } from "../primitives"; import { useModels } from "@/lib/models"; import { useEffect, useState } from "react"; export type FieldSpec = { key: string; label: string; widget: "toggle" | "number" | "text" | "textarea" | "select" | "model" | "csv" | "multiselect"; help?: string; placeholder?: string; /** Override the "model" widget's empty-value label (default "Project default"). Use when an * empty model doesn't resolve to the project default - e.g. the classifier falls back to the * cheapest model, so showing "Project default" there would misrepresent what actually runs. */ emptyLabel?: string; options?: { value: string; label: string }[]; /** Pull options at render time from a live source (e.g. the project's KB folders or * Q&A kinds) passed via FieldsForm's `dynamic` prop. Keyed by source name. */ dynamicOptions?: "kb_folders" | "qa_kinds"; /** First option for a dynamic select (e.g. "any"/"all"). */ emptyOption?: { value: string; label: string }; min?: number; max?: number; step?: number; /** Convert the stored config value to the widget's display value. */ format?: (v: any) => any; /** Convert the widget's raw value to the stored config value. */ parse?: (raw: any) => any; }; /** Compact multi-select rendered as toggle chips (used for folder selection). Stores an * array; empty array means "all". When there's nothing to choose yet, shows a plain * message instead of an empty input (any already-stored values stay as removable chips). */ export function MultiSelectChips({ value, options, placeholder, onChange }: { value: any; options: string[]; placeholder?: string; onChange: (items: string[]) => void }) { const selected: string[] = Array.isArray(value) ? value.map(String) : (typeof value === "string" && value ? value.split(",").map((s) => s.trim()).filter(Boolean) : []); const toggle = (opt: string) => { const next = selected.includes(opt) ? selected.filter((s) => s !== opt) : [...selected, opt]; onChange(next); }; // No live options: don't render an input - just a hint. (Keep any stored values visible // as removable chips so they aren't silently lost.) const chipBtn = (opt: string, on: boolean) => ( ); if (!options.length) { if (!selected.length) { return
{placeholder || "Nothing to choose from yet - manage these on the Knowledge screen."}
; } return
{selected.map((opt) => chipBtn(opt, true))}
; } return (
{options.map((opt) => { const on = selected.includes(opt); return ( ); })}
); } export function ModelSelect({ value, onChange, emptyLabel = "Project default" }: { value: string; onChange: (v: string) => void; emptyLabel?: string }) { const MODELS = useModels(); return ( ); } function CsvInput({ value, placeholder, onChange }: { value: any; placeholder?: string; onChange: (items: string[]) => void }) { const display = Array.isArray(value) ? value.join(", ") : String(value ?? ""); const [draft, setDraft] = useState(display); useEffect(() => { setDraft(display); }, [display]); const commit = () => { onChange(draft.split(",").map((s) => s.trim()).filter(Boolean)); }; return ( setDraft(e.target.value)} onBlur={commit} onKeyDown={(e) => { if (e.key === "Enter") e.currentTarget.blur(); }} /> ); } export function FieldsForm({ specs, config, onPatch, dynamic }: { specs: FieldSpec[]; config: Record; onPatch: (patch: Record) => void; dynamic?: Record }) { const read = (f: FieldSpec) => { const v = config[f.key]; return f.format ? f.format(v) : v; }; const write = (f: FieldSpec, raw: any) => { onPatch({ [f.key]: f.parse ? f.parse(raw) : raw }); }; const dynOptions = (f: FieldSpec): string[] => (f.dynamicOptions && dynamic?.[f.dynamicOptions]) || []; return (
{specs.map((f) => { const v = read(f); if (f.widget === "toggle") { return (
{f.help &&
{f.help}
}
); } return ( {f.widget === "number" ? ( write(f, e.target.value === "" ? undefined : Number(e.target.value))} /> ) : f.widget === "select" ? ( (() => { // Options come from the static list OR a live source (e.g. Q&A kinds). const opts = f.dynamicOptions ? [...(f.emptyOption ? [f.emptyOption] : []), ...dynOptions(f).map((o) => ({ value: o, label: o }))] : (f.options || []); // Keep a current value that isn't in the live list selectable (don't lose it). const hasV = v == null || v === "" || opts.some((o) => o.value === v); return ( ); })() ) : f.widget === "multiselect" ? ( write(f, items)} /> ) : f.widget === "model" ? ( write(f, val || undefined)} /> ) : f.widget === "csv" ? ( write(f, items)} /> ) : f.widget === "textarea" ? (