TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import { X, RotateCcw } from "lucide-react";4import { Slider } from "@/components/ui/slider";5import { Input, Label } from "@/components/ui/input";6import { Switch } from "@/components/ui/switch";7import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";8import { Button } from "@/components/ui/button";9import { Badge } from "@/components/ui/badge";10import { cn } from "@/lib/utils";11import type { GenerationSettingsInput, PolyModel, ReasoningEffort } from "@/lib/client/types";1213export type Params = GenerationSettingsInput;1415const UNSET = "__unset__";16const EFFORTS: ReasoningEffort[] = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];1718export const BUILTIN_TOOLS: { id: string; label: string; description: string }[] = [19 { id: "calculator", label: "Calculator", description: "Exact arithmetic and unit conversion." },20 { id: "clock", label: "Clock", description: "Current date and time in any timezone." },21 { id: "random", label: "Random", description: "Random numbers, UUIDs and picks." },22];2324/** Human chips for a stored parameter object — used on preset cards. */25export function parameterChips(p: Record<string, unknown> | null | undefined): string[] {26 if (!p) return [];27 const out: string[] = [];28 if (typeof p.temperature === "number") out.push(`temp ${p.temperature}`);29 if (typeof p.topP === "number") out.push(`top-p ${p.topP}`);30 if (typeof p.topK === "number") out.push(`top-k ${p.topK}`);31 if (typeof p.maxTokens === "number") out.push(`${p.maxTokens.toLocaleString()} max`);32 if (typeof p.reasoningEffort === "string") out.push(`effort ${p.reasoningEffort}`);33 if (typeof p.thinkingBudget === "number") out.push(`budget ${p.thinkingBudget.toLocaleString()}`);34 if (typeof p.verbosity === "string") out.push(`verbosity ${p.verbosity}`);35 if (typeof p.frequencyPenalty === "number") out.push(`freq ${p.frequencyPenalty}`);36 if (typeof p.presencePenalty === "number") out.push(`pres ${p.presencePenalty}`);37 if (typeof p.seed === "number") out.push(`seed ${p.seed}`);38 if (Array.isArray(p.stop) && p.stop.length) out.push(`${p.stop.length} stop`);39 if (p.webSearch) out.push("web search");40 if (Array.isArray(p.tools) && p.tools.length) out.push(`tools: ${(p.tools as string[]).join(", ")}`);41 if (p.responseFormat && typeof p.responseFormat === "object" && (p.responseFormat as { type?: string }).type !== "text") out.push("JSON output");42 return out;43}4445interface Props {46 value: Params;47 onChange: (next: Params) => void;48 /**49 * When provided, only controls the model supports are shown (its `parameters` sheet + capabilities).50 * When omitted (prompt presets), a compact generic subset is shown.51 */52 model?: PolyModel | null;53 compact?: boolean;54 className?: string;55}5657/** Parameters editor driven by a model's capability sheet. Never shows a control the model does not support. */58export function ParametersForm({ value, onChange, model, compact, className }: Props) {59 const sheet = model?.parameters;60 const caps = model?.capabilities;61 const generic = !model;6263 const set = <K extends keyof Params>(k: K, v: Params[K] | undefined) => {64 const next = { ...value };65 if (v === undefined) delete next[k];66 else next[k] = v;67 onChange(next);68 };6970 const show = {71 temperature: generic || Boolean(sheet?.temperature),72 topP: !compact && Boolean(sheet?.topP),73 topK: !compact && Boolean(sheet?.topK),74 maxTokens: generic || Boolean(sheet?.maxTokens),75 reasoningEffort: generic || Boolean(sheet?.reasoningEffort),76 thinkingBudget: !compact && Boolean(sheet?.thinkingBudget),77 verbosity: !compact && Boolean(sheet?.verbosity),78 frequencyPenalty: !compact && Boolean(sheet?.frequencyPenalty),79 presencePenalty: !compact && Boolean(sheet?.presencePenalty),80 seed: !compact && Boolean(sheet?.seed),81 stop: !compact && Boolean(sheet?.stop),82 tools: !compact && (generic || Boolean(caps?.tools)),83 webSearch: !compact && Boolean(caps?.webSearch),84 structured: !compact && Boolean(caps?.structuredOutput),85 };8687 const tempRange = sheet?.temperatureRange ?? { min: 0, max: 2 };88 const effortLevels = (sheet?.reasoningEffortLevels?.length ? sheet.reasoningEffortLevels : EFFORTS) as string[];89 const budgetRange = sheet?.thinkingBudgetRange ?? { min: 0, max: 200_000 };90 const maxOut = model?.limits?.maxOutputTokens;91 const anySet = Object.keys(value).length > 0;9293 const nothing = !Object.values(show).some(Boolean);9495 return (96 <div className={cn("space-y-4", className)}>97 <div className="flex items-center justify-between">98 <p className="text-[13px] font-medium text-fg-muted">Parameters</p>99 {anySet ? (100 <Button type="button" variant="ghost" size="xs" onClick={() => onChange({})}>101 <RotateCcw /> Reset to model defaults102 </Button>103 ) : (104 <span className="text-[11px] text-fg-subtle">Unset = provider default</span>105 )}106 </div>107108 {nothing ? <p className="rounded-lg border border-dashed border-border px-3 py-4 text-center text-xs text-fg-subtle">This model exposes no tunable parameters.</p> : null}109110 {show.temperature ? (111 <SliderField label="Temperature" hint="Lower is more deterministic." value={value.temperature} min={tempRange.min} max={tempRange.max} step={0.05} onChange={(v) => set("temperature", v)} />112 ) : null}113 {show.topP ? <SliderField label="Top-p" hint="Nucleus sampling cutoff." value={value.topP} min={0} max={1} step={0.01} onChange={(v) => set("topP", v)} /> : null}114 {show.topK ? <NumberField label="Top-k" value={value.topK} min={1} max={500} step={1} onChange={(v) => set("topK", v)} /> : null}115 {show.maxTokens ? <NumberField label="Max output tokens" hint={maxOut ? `Model limit ${maxOut.toLocaleString()}` : undefined} value={value.maxTokens} min={1} max={maxOut ?? 400_000} step={1} onChange={(v) => set("maxTokens", v)} /> : null}116117 {show.reasoningEffort ? (118 <SelectField label="Reasoning effort" value={value.reasoningEffort} options={effortLevels.map((l) => ({ value: l, label: l }))} onChange={(v) => set("reasoningEffort", v as ReasoningEffort | undefined)} />119 ) : null}120 {show.thinkingBudget ? <NumberField label="Thinking budget (tokens)" hint={`${budgetRange.min.toLocaleString()} – ${budgetRange.max.toLocaleString()}`} value={value.thinkingBudget} min={budgetRange.min} max={budgetRange.max} step={256} onChange={(v) => set("thinkingBudget", v)} /> : null}121 {show.verbosity ? <SelectField label="Verbosity" value={value.verbosity} options={["low", "medium", "high"].map((l) => ({ value: l, label: l }))} onChange={(v) => set("verbosity", v as Params["verbosity"])} /> : null}122123 {show.frequencyPenalty || show.presencePenalty ? (124 <div className="grid gap-4 sm:grid-cols-2">125 {show.frequencyPenalty ? <SliderField label="Frequency penalty" value={value.frequencyPenalty} min={-2} max={2} step={0.1} onChange={(v) => set("frequencyPenalty", v)} /> : null}126 {show.presencePenalty ? <SliderField label="Presence penalty" value={value.presencePenalty} min={-2} max={2} step={0.1} onChange={(v) => set("presencePenalty", v)} /> : null}127 </div>128 ) : null}129130 {show.seed ? <NumberField label="Seed" hint="Best-effort reproducibility." value={value.seed} min={0} max={2_147_483_647} step={1} onChange={(v) => set("seed", v)} /> : null}131 {show.stop ? <StopField value={value.stop ?? []} onChange={(v) => set("stop", v.length ? v : undefined)} /> : null}132133 {show.structured ? (134 <ToggleField label="Strict JSON output" hint="Ask the model to return a JSON object." checked={value.responseFormat?.type === "json"} onChange={(v) => set("responseFormat", v ? { type: "json" } : undefined)} />135 ) : null}136 {show.webSearch ? <ToggleField label="Web search" hint="Let the provider search the live web and cite sources." checked={Boolean(value.webSearch)} onChange={(v) => set("webSearch", v || undefined)} /> : null}137138 {show.tools ? (139 <div className="space-y-2">140 <Label>Built-in tools</Label>141 <div className="grid gap-2 sm:grid-cols-3">142 {BUILTIN_TOOLS.map((t) => {143 const on = value.tools?.includes(t.id) ?? false;144 return (145 <label key={t.id} className={cn("flex cursor-pointer items-start gap-2.5 rounded-lg border p-2.5 transition-colors", on ? "border-accent/50 bg-accent-soft/40" : "border-border hover:border-border-strong")}>146 <Switch147 size="sm"148 checked={on}149 onCheckedChange={(v) => {150 const cur = new Set(value.tools ?? []);151 if (v) cur.add(t.id);152 else cur.delete(t.id);153 set("tools", cur.size ? [...cur] : undefined);154 }}155 className="mt-0.5"156 />157 <span className="min-w-0">158 <span className="block text-[13px] font-medium leading-4">{t.label}</span>159 <span className="block text-[11px] leading-4 text-fg-subtle">{t.description}</span>160 </span>161 </label>162 );163 })}164 </div>165 </div>166 ) : null}167 </div>168 );169}170171function 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 }) {172 const id = React.useId();173 const isSet = value !== undefined;174 const shown = value ?? (min + max) / 2;175 return (176 <div className="space-y-1.5">177 <div className="flex items-center justify-between gap-2">178 <Label htmlFor={id}>{label}</Label>179 <span className="flex items-center gap-1.5">180 {isSet ? (181 <>182 <span className="font-mono text-xs tabular-nums text-fg">{value}</span>183 <button type="button" onClick={() => onChange(undefined)} className="rounded p-0.5 text-fg-subtle hover:text-fg" aria-label={`Unset ${label}`}>184 <X className="size-3" />185 </button>186 </>187 ) : (188 <Badge>default</Badge>189 )}190 </span>191 </div>192 <Slider id={id} min={min} max={max} step={step} value={[shown]} onValueChange={([v]) => onChange(Number(v.toFixed(2)))} className={cn(!isSet && "opacity-60")} aria-label={label} />193 <div className="flex justify-between text-[10.5px] text-fg-subtle">194 <span>{min}</span>195 {hint ? <span>{hint}</span> : null}196 <span>{max}</span>197 </div>198 </div>199 );200}201202function 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 }) {203 const id = React.useId();204 return (205 <div className="space-y-1.5">206 <div className="flex items-center justify-between">207 <Label htmlFor={id}>{label}</Label>208 {hint ? <span className="text-[11px] text-fg-subtle">{hint}</span> : null}209 </div>210 <Input211 id={id}212 type="number"213 inputMode="numeric"214 min={min}215 max={max}216 step={step}217 value={value ?? ""}218 placeholder="default"219 onChange={(e) => {220 const raw = e.target.value;221 if (raw === "") return onChange(undefined);222 const n = Number(raw);223 if (Number.isNaN(n)) return;224 onChange(Math.min(max, Math.max(min, step >= 1 ? Math.round(n) : n)));225 }}226 className="font-mono"227 />228 </div>229 );230}231232function SelectField({ label, value, options, onChange }: { label: string; value: string | undefined; options: { value: string; label: string }[]; onChange: (v: string | undefined) => void }) {233 const id = React.useId();234 return (235 <div className="space-y-1.5">236 <Label htmlFor={id}>{label}</Label>237 <Select value={value ?? UNSET} onValueChange={(v) => onChange(v === UNSET ? undefined : v)}>238 <SelectTrigger id={id}>239 <SelectValue />240 </SelectTrigger>241 <SelectContent>242 <SelectItem value={UNSET}>Provider default</SelectItem>243 {options.map((o) => (244 <SelectItem key={o.value} value={o.value} className="capitalize">245 {o.label}246 </SelectItem>247 ))}248 </SelectContent>249 </Select>250 </div>251 );252}253254function ToggleField({ label, hint, checked, onChange }: { label: string; hint?: string; checked: boolean; onChange: (v: boolean) => void }) {255 const id = React.useId();256 return (257 <div className="flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2.5">258 <div className="min-w-0">259 <label htmlFor={id} className="text-[13px] font-medium">260 {label}261 </label>262 {hint ? <p className="text-[11px] text-fg-subtle">{hint}</p> : null}263 </div>264 <Switch id={id} size="sm" checked={checked} onCheckedChange={onChange} />265 </div>266 );267}268269function StopField({ value, onChange }: { value: string[]; onChange: (v: string[]) => void }) {270 const [draft, setDraft] = React.useState("");271 const id = React.useId();272 const add = () => {273 const s = draft.trim();274 if (!s || value.includes(s) || value.length >= 4) return;275 onChange([...value, s.slice(0, 64)]);276 setDraft("");277 };278 return (279 <div className="space-y-1.5">280 <div className="flex items-center justify-between">281 <Label htmlFor={id}>Stop sequences</Label>282 <span className="text-[11px] text-fg-subtle">{value.length}/4</span>283 </div>284 <div className="flex flex-wrap items-center gap-1.5 rounded-md border border-border bg-bg-elevated px-2 py-1.5 focus-within:border-accent focus-within:ring-2 focus-within:ring-accent/25">285 {value.map((s) => (286 <span key={s} className="inline-flex items-center gap-1 rounded bg-bg-muted px-1.5 py-0.5 font-mono text-xs">287 {JSON.stringify(s).slice(1, -1)}288 <button type="button" onClick={() => onChange(value.filter((x) => x !== s))} className="text-fg-subtle hover:text-fg" aria-label={`Remove stop sequence ${s}`}>289 <X className="size-3" />290 </button>291 </span>292 ))}293 <input294 id={id}295 value={draft}296 onChange={(e) => setDraft(e.target.value)}297 onKeyDown={(e) => {298 if (e.key === "Enter" || e.key === ",") {299 e.preventDefault();300 add();301 }302 }}303 onBlur={add}304 placeholder={value.length ? "" : "Type and press Enter"}305 disabled={value.length >= 4}306 className="min-w-[8ch] flex-1 bg-transparent font-mono text-[15px] outline-none placeholder:text-fg-subtle sm:text-xs"307 />308 </div>309 </div>310 );311}312