"use client"; import { PlusIcon, XIcon } from "lucide-react"; import { useMemo, useState } from "react"; import type { ProviderDriverKind, ServerProviderModel } from "@t3tools/contracts"; import type { CustomModelDefinition } from "@t3tools/shared/model"; import { cn } from "../../lib/utils"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; import { Switch } from "../ui/switch"; import { DESCRIPTOR_PRESETS_BY_KIND, type CustomModelDraft, type EditorChoice, type EditorDescriptor, choiceFromPreset, definitionFromDraft, descriptorFromPreset, descriptorsFromCapabilities, draftFromDefinition, emptyEditorChoice, emptyEditorDescriptor, validateDraft, } from "./customModelEditor.logic"; const CUSTOM_ID_VALUE = "__custom__"; const START_FROM_NONE = "__none__"; interface CustomModelEditorProps { readonly instanceId: string; readonly driverKind: ProviderDriverKind | null; readonly entry: CustomModelDefinition; /** Built-in models whose descriptors can be copied as a starting point. */ readonly builtInModels: ReadonlyArray; readonly onSave: (next: CustomModelDefinition) => void; readonly onCancel: () => void; } /** * Inline editor for one custom model: display name plus the option * descriptors the composer should offer for it (Reasoning effort, Fast * mode, ...). Draft state is local; nothing is persisted until Save. */ export function CustomModelEditor({ instanceId, driverKind, entry, builtInModels, onSave, onCancel, }: CustomModelEditorProps) { const [draft, setDraft] = useState(() => draftFromDefinition(entry)); const [error, setError] = useState(null); const presets = useMemo( () => (driverKind ? (DESCRIPTOR_PRESETS_BY_KIND[driverKind] ?? []) : []), [driverKind], ); const startFromCandidates = useMemo( () => builtInModels.filter((model) => (model.capabilities?.optionDescriptors?.length ?? 0) > 0), [builtInModels], ); const domId = (suffix: string) => `provider-instance-${instanceId}-custom-model-${suffix}`; const updateDescriptor = (key: string, patch: Partial) => { setError(null); setDraft((current) => ({ ...current, descriptors: current.descriptors.map((descriptor) => descriptor.key === key ? { ...descriptor, ...patch } : descriptor, ), })); }; const updateChoice = (descriptorKey: string, choiceKey: string, patch: Partial) => { setError(null); setDraft((current) => ({ ...current, descriptors: current.descriptors.map((descriptor) => { if (descriptor.key !== descriptorKey) return descriptor; return { ...descriptor, choices: descriptor.choices.map((choice) => { if (choice.key === choiceKey) return { ...choice, ...patch }; // Only one choice can be the default. return patch.isDefault ? { ...choice, isDefault: false } : choice; }), }; }), })); }; const removeDescriptor = (key: string) => { setError(null); setDraft((current) => ({ ...current, descriptors: current.descriptors.filter((descriptor) => descriptor.key !== key), })); }; const addDescriptor = (descriptor: EditorDescriptor) => { setError(null); setDraft((current) => ({ ...current, descriptors: [...current.descriptors, descriptor] })); }; // Selecting a preset id replaces the descriptor's label/type/choices so the // usual values are one click away; "Custom…" leaves the row blank to type into. const applyPresetId = (descriptor: EditorDescriptor, value: string | null) => { if (value === null) return; if (value === CUSTOM_ID_VALUE) { updateDescriptor(descriptor.key, { id: "" }); return; } const preset = presets.find((candidate) => candidate.id === value); if (!preset) return; updateDescriptor(descriptor.key, { id: preset.id, label: preset.label, type: preset.type, choices: (preset.choices ?? []).map(choiceFromPreset), currentBooleanValue: undefined, description: undefined, }); }; const handleStartFrom = (slug: string | null) => { if (slug === null || slug === START_FROM_NONE) return; const model = startFromCandidates.find((candidate) => candidate.slug === slug); if (!model) return; setError(null); setDraft((current) => ({ ...current, descriptors: descriptorsFromCapabilities(model.capabilities, driverKind), })); }; const handleSave = () => { const problem = validateDraft(draft); if (problem) { setError(problem); return; } onSave(definitionFromDraft(draft)); }; const idSelectValue = (descriptor: EditorDescriptor) => presets.some((preset) => preset.id === descriptor.id) ? descriptor.id : CUSTOM_ID_VALUE; const renderChoice = (descriptor: EditorDescriptor, choice: EditorChoice) => (
updateChoice(descriptor.key, choice.key, { id: event.target.value })} placeholder="value" className="w-28 font-mono" spellCheck={false} aria-label="Choice value" /> updateChoice(descriptor.key, choice.key, { label: event.target.value }) } placeholder="Label" className="min-w-0 flex-1" aria-label="Choice label" />
); const renderDescriptor = (descriptor: EditorDescriptor, index: number) => (
Option {index + 1} {presets.length > 0 ? ( ) : null} {idSelectValue(descriptor) === CUSTOM_ID_VALUE ? ( updateDescriptor(descriptor.key, { id: event.target.value })} placeholder="optionId" className="w-36 font-mono" spellCheck={false} aria-label="Option id" /> ) : null} updateDescriptor(descriptor.key, { label: event.target.value })} placeholder="Label" className="min-w-0 flex-1" aria-label="Option label" />
{descriptor.type === "select" ? (
{descriptor.choices.map((choice) => renderChoice(descriptor, choice))}
) : null}
); return (
{ if (event.key === "Escape") { event.preventDefault(); onCancel(); } }} >
setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={draft.slug} className="sm:w-72" spellCheck={false} />
Options shown in the composer {startFromCandidates.length > 0 ? ( ) : null}
{draft.descriptors.length === 0 ? (

No custom options. The composer uses the provider's default options.

) : null} {draft.descriptors.map(renderDescriptor)}
{presets .filter( (preset) => !draft.descriptors.some((descriptor) => descriptor.id === preset.id), ) .map((preset) => ( ))}
{error ?

{error}

: null}
); }