"use client"; import * as React from "react"; import { X, RotateCcw } from "lucide-react"; import { Slider } from "@/components/ui/slider"; import { Input, Label } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import type { GenerationSettingsInput, PolyModel, ReasoningEffort } from "@/lib/client/types"; export type Params = GenerationSettingsInput; const UNSET = "__unset__"; const EFFORTS: ReasoningEffort[] = ["none", "minimal", "low", "medium", "high", "xhigh", "max"]; export const BUILTIN_TOOLS: { id: string; label: string; description: string }[] = [ { id: "calculator", label: "Calculator", description: "Exact arithmetic and unit conversion." }, { id: "clock", label: "Clock", description: "Current date and time in any timezone." }, { id: "random", label: "Random", description: "Random numbers, UUIDs and picks." }, ]; /** Human chips for a stored parameter object — used on preset cards. */ export function parameterChips(p: Record | null | undefined): string[] { if (!p) return []; const out: string[] = []; if (typeof p.temperature === "number") out.push(`temp ${p.temperature}`); if (typeof p.topP === "number") out.push(`top-p ${p.topP}`); if (typeof p.topK === "number") out.push(`top-k ${p.topK}`); if (typeof p.maxTokens === "number") out.push(`${p.maxTokens.toLocaleString()} max`); if (typeof p.reasoningEffort === "string") out.push(`effort ${p.reasoningEffort}`); if (typeof p.thinkingBudget === "number") out.push(`budget ${p.thinkingBudget.toLocaleString()}`); if (typeof p.verbosity === "string") out.push(`verbosity ${p.verbosity}`); if (typeof p.frequencyPenalty === "number") out.push(`freq ${p.frequencyPenalty}`); if (typeof p.presencePenalty === "number") out.push(`pres ${p.presencePenalty}`); if (typeof p.seed === "number") out.push(`seed ${p.seed}`); if (Array.isArray(p.stop) && p.stop.length) out.push(`${p.stop.length} stop`); if (p.webSearch) out.push("web search"); if (Array.isArray(p.tools) && p.tools.length) out.push(`tools: ${(p.tools as string[]).join(", ")}`); if (p.responseFormat && typeof p.responseFormat === "object" && (p.responseFormat as { type?: string }).type !== "text") out.push("JSON output"); return out; } interface Props { value: Params; onChange: (next: Params) => void; /** * When provided, only controls the model supports are shown (its `parameters` sheet + capabilities). * When omitted (prompt presets), a compact generic subset is shown. */ model?: PolyModel | null; compact?: boolean; className?: string; } /** Parameters editor driven by a model's capability sheet. Never shows a control the model does not support. */ export function ParametersForm({ value, onChange, model, compact, className }: Props) { const sheet = model?.parameters; const caps = model?.capabilities; const generic = !model; const set = (k: K, v: Params[K] | undefined) => { const next = { ...value }; if (v === undefined) delete next[k]; else next[k] = v; onChange(next); }; const show = { temperature: generic || Boolean(sheet?.temperature), topP: !compact && Boolean(sheet?.topP), topK: !compact && Boolean(sheet?.topK), maxTokens: generic || Boolean(sheet?.maxTokens), reasoningEffort: generic || Boolean(sheet?.reasoningEffort), thinkingBudget: !compact && Boolean(sheet?.thinkingBudget), verbosity: !compact && Boolean(sheet?.verbosity), frequencyPenalty: !compact && Boolean(sheet?.frequencyPenalty), presencePenalty: !compact && Boolean(sheet?.presencePenalty), seed: !compact && Boolean(sheet?.seed), stop: !compact && Boolean(sheet?.stop), tools: !compact && (generic || Boolean(caps?.tools)), webSearch: !compact && Boolean(caps?.webSearch), structured: !compact && Boolean(caps?.structuredOutput), }; const tempRange = sheet?.temperatureRange ?? { min: 0, max: 2 }; const effortLevels = (sheet?.reasoningEffortLevels?.length ? sheet.reasoningEffortLevels : EFFORTS) as string[]; const budgetRange = sheet?.thinkingBudgetRange ?? { min: 0, max: 200_000 }; const maxOut = model?.limits?.maxOutputTokens; const anySet = Object.keys(value).length > 0; const nothing = !Object.values(show).some(Boolean); return (

Parameters

{anySet ? ( ) : ( Unset = provider default )}
{nothing ?

This model exposes no tunable parameters.

: null} {show.temperature ? ( set("temperature", v)} /> ) : null} {show.topP ? set("topP", v)} /> : null} {show.topK ? set("topK", v)} /> : null} {show.maxTokens ? set("maxTokens", v)} /> : null} {show.reasoningEffort ? ( ({ value: l, label: l }))} onChange={(v) => set("reasoningEffort", v as ReasoningEffort | undefined)} /> ) : null} {show.thinkingBudget ? set("thinkingBudget", v)} /> : null} {show.verbosity ? ({ value: l, label: l }))} onChange={(v) => set("verbosity", v as Params["verbosity"])} /> : null} {show.frequencyPenalty || show.presencePenalty ? (
{show.frequencyPenalty ? set("frequencyPenalty", v)} /> : null} {show.presencePenalty ? set("presencePenalty", v)} /> : null}
) : null} {show.seed ? set("seed", v)} /> : null} {show.stop ? set("stop", v.length ? v : undefined)} /> : null} {show.structured ? ( set("responseFormat", v ? { type: "json" } : undefined)} /> ) : null} {show.webSearch ? set("webSearch", v || undefined)} /> : null} {show.tools ? (
{BUILTIN_TOOLS.map((t) => { const on = value.tools?.includes(t.id) ?? false; return ( ); })}
) : null}
); } function SliderField({ label, hint, value, min, max, step, onChange }: { label: string; hint?: string; value: number | undefined; min: number; max: number; step: number; onChange: (v: number | undefined) => void }) { const id = React.useId(); const isSet = value !== undefined; const shown = value ?? (min + max) / 2; return (
{isSet ? ( <> {value} ) : ( default )}
onChange(Number(v.toFixed(2)))} className={cn(!isSet && "opacity-60")} aria-label={label} />
{min} {hint ? {hint} : null} {max}
); } function NumberField({ label, hint, value, min, max, step, onChange }: { label: string; hint?: string; value: number | undefined; min: number; max: number; step: number; onChange: (v: number | undefined) => void }) { const id = React.useId(); return (
{hint ? {hint} : null}
{ const raw = e.target.value; if (raw === "") return onChange(undefined); const n = Number(raw); if (Number.isNaN(n)) return; onChange(Math.min(max, Math.max(min, step >= 1 ? Math.round(n) : n))); }} className="font-mono" />
); } function SelectField({ label, value, options, onChange }: { label: string; value: string | undefined; options: { value: string; label: string }[]; onChange: (v: string | undefined) => void }) { const id = React.useId(); return (
); } function ToggleField({ label, hint, checked, onChange }: { label: string; hint?: string; checked: boolean; onChange: (v: boolean) => void }) { const id = React.useId(); return (
{hint ?

{hint}

: null}
); } function StopField({ value, onChange }: { value: string[]; onChange: (v: string[]) => void }) { const [draft, setDraft] = React.useState(""); const id = React.useId(); const add = () => { const s = draft.trim(); if (!s || value.includes(s) || value.length >= 4) return; onChange([...value, s.slice(0, 64)]); setDraft(""); }; return (
{value.length}/4
{value.map((s) => ( {JSON.stringify(s).slice(1, -1)} ))} setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === ",") { e.preventDefault(); add(); } }} onBlur={add} placeholder={value.length ? "" : "Type and press Enter"} disabled={value.length >= 4} className="min-w-[8ch] flex-1 bg-transparent font-mono text-[15px] outline-none placeholder:text-fg-subtle sm:text-xs" />
); }