Sampling Controls
A controlled panel for temperature, top-p, max tokens and the penalties — editable value badges, a worded creative-to-precise scale, per-parameter revert, and a nudge when temperature and top-p are tuned together.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/sampling-controls.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "SamplingControls" component: the panel
where a user tunes the numbers that go into a completion request. Sliders are
hand-rolled (pointer capture + role="slider"), icons from lucide-react, no
other dependency.
Contract
- forwardRef<HTMLDivElement>, extends React.HTMLAttributes<HTMLDivElement>
minus "onChange" and "defaultValue"; the rest spreads onto the root.
- value: SamplingValue — Record<string, number | undefined>. SPARSE: a key is
present only while it differs from that field's default. This is the whole
design; the object is meant to be spread straight into a request body.
- onChange(next: SamplingValue) — every drag frame, key press, committed badge
edit and reset.
- onCommit?(next: SamplingValue) — once per settled interaction: pointer
release, key press, Enter/blur on a badge, a reset. Wire refetches here.
- fields?: SamplingField[] (default SAMPLING_FIELDS, exported)
{ key, label, min, max, step, defaultValue, largeStep?, unit?, hint?,
advanced?, scale? }
Default set: temperature (0..2, step .01, default .7, has a scale), top-p
(0..1, step .01, default 1), max tokens (64..8192, step 32, default 1024,
unit "tok"), then advanced: frequency penalty and presence penalty
(-2..2, step .1, default 0) and top-k (0..100, step 1, default 0).
- scale?: SamplingBand[] — { from, label, hint? }, ascending; `from` is
inclusive and the band runs to the next band's `from` or to the field max.
Temperature ships Precise / Balanced / Creative / Wild.
- conflicts?: SamplingConflict[] (default SAMPLING_CONFLICTS) —
{ keys: [string, string]; message }. Default pair: temperature vs top-p.
- title?: string (default "Sampling"), disabled?: boolean,
defaultAdvancedOpen?: boolean (default false).
Behavior — the sparse value object
- Resolved value of a field = value[key] ?? field.defaultValue. A missing key
means "leave it at the provider default", so an untouched panel adds nothing
to the request body.
- Every write clamps to [min, max] and snaps to the step grid (snap first, then
clamp, so the exact max is reachable even when it is off-grid). Round with
the step's decimal count so 0.1 steps never leave floating-point tails.
- If the snapped result equals the default, DELETE the key instead of writing
it. "Modified", "shown as changed" and "sent" then all mean the same thing,
and dragging a slider home shrinks the payload again.
- Reverting (per-field button, conflict action, Reset all) deletes keys
outright and never routes through the grid, so a consumer whose default does
not sit on the step grid can still get back to clean.
- Guard a step of 0 (Math.abs(step) || 1) — one bad field spec must not turn
every value into NaN.
- An incoming value outside [min, max] is clamped for GEOMETRY only; the badge
and aria-valuenow keep showing what the caller passed. The panel never
rewrites the caller's object on its own.
Behavior — the slider
- One row per field: label + editable value badge, optional hint, track,
optional worded scale, active band line.
- The track fill runs from the DEFAULT tick to the current value, not from the
minimum. A bipolar parameter (-2..2, default 0) then reads as "pushed up" or
"pulled down"; top-p (default 1 = max) reads as "how far you pulled it down".
Draw a small tick at the default position on every track.
- Pointer: pointerdown on the track jumps to that position, sets pointer
capture (so the drag survives leaving the track — no window listeners to
register or clean up) and focuses the thumb; pointermove writes; pointerup /
pointercancel end the gesture and fire onCommit once. Re-check `disabled` on
every move so a consumer that locks the panel from onChange stops receiving
frames mid-gesture. touch-none on the track.
- Keyboard on the thumb: Arrow keys = ±step, PageUp/PageDown = ±largeStep
(default step*10), Home/End = min/max. Each press is its own committed
interaction. preventDefault so the page does not scroll.
- The value badge is a text input, not a static label: focus selects all,
Enter commits and re-selects, Escape reverts to the current value, blur
commits, unparseable or empty text silently reverts. Commit clamps and snaps
like any other write. If a drag started while the badge still held focus,
discard the pending text — the gesture is the newer intent.
- Transitions on left/width are disabled while dragging (the thumb must track
the pointer exactly) and re-enabled for snap-back; motion-reduce turns them
off entirely without changing any value.
Behavior — worded scale, dirt tracking, disclosure
- The active band is the last one whose `from` the value has reached. Render
the bands as a proportional strip under the track (each band's width is its
share of the range), highlight the active one, and print "<label> — <hint>"
under it. The band label also goes into aria-valuetext ("0.92, Creative"),
so a screen-reader user gets the same meaning as the strip.
- A field is dirty when its resolved value differs from its default. A dirty
field shows a revert button next to its badge and tints the badge. Count
dirty fields in the header ("3 changed"); Reset all only acts while there is
something to reset. Mark it aria-disabled with the guard in the handler, never
the native `disabled` attribute — the press itself is what makes the button
unavailable, and a node that goes disabled under the caret is blurred to
<body>.
- The revert button unmounts the instant the field goes clean, so move focus to
that field's thumb BEFORE emitting — never drop a keyboard user on <body>.
- Advanced group: a disclosure button with aria-expanded + aria-controls; the
region stays mounted and is hidden with a display utility (the `hidden`
attribute loses to any Tailwind display class). The button carries its own
"N changed" badge, so a collapsed group can never hide a modified value.
Behavior — the mutually exclusive pair
- A conflict fires when BOTH keys of a pair are dirty — derived from values,
not from interaction, so a preset that arrives already conflicting is caught
too.
- Render it as a gentle notice, not an error: neutral surface, an alert glyph,
the message, a "Reset <second field>" action and "Dismiss". It never blocks
and never rewrites anything by itself.
- Mount the live region (aria-live="polite") EMPTY and keep it mounted; a
region that appears together with its text is often missed. Point the two
thumbs at the notice with aria-describedby while it is up.
- Dismissal is one-shot per episode: remember the dismissed notice id, and drop
that memory as soon as the pair goes clean again, so the nudge is re-armed
for the next time it is earned. Both actions move focus to a thumb before the
notice unmounts.
Rendering & styling
- Semantic tokens only: bg-card / text-card-foreground for the shell, border,
bg-muted for the track and inert badges, bg-primary for the fill,
border-primary for the thumb, bg-primary/10 + border-primary/40 for a dirty
badge, bg-foreground/30 for the default tick, bg-background for band
separators cut into the track, text-muted-foreground for hints, ring/ring-50
for focus. No hex, no rgb(), no oklch().
- Merge className through cn() on the root; keep every decorative node
aria-hidden; the thumb is the only focusable element of a track.
- The shell is role="group" labelled by its own heading, so a screen reader can
jump the whole panel; role="slider" with aria-labelledby (the row label),
aria-valuemin/max/now,
aria-valuetext, aria-orientation, aria-disabled and tabIndex -1 when
disabled. The badge input gets its own aria-label ("Temperature value").
- disabled dims the shell, stops pointer and keyboard writes and takes every
value control out of the tab order, but keeps the numbers readable and leaves
the Advanced disclosure operable — reading a hidden value must not require
unlocking the panel first.
- Ship the reveal keyframes in a React 19 hoisted <style href precedence> so
duplicate instances dedupe, and gate the animation with
motion-reduce:[animation:none].
Customization levers
- fields is the main lever: reorder, drop, retitle, re-range, move any field in
or out of `advanced`, or replace the set wholesale (llama.cpp's min-p /
repeat-penalty / mirostat, an image model's guidance scale and steps). Keys
are request-body field names — rename them to match your API.
- step vs largeStep tunes keyboard feel: a coarse largeStep makes long ranges
(max tokens) usable from the keyboard without touching drag precision.
- scale is optional per field and free-form: add bands to top-p, or drop
temperature's to get a bare slider. Bands are rendered proportionally, so
their widths follow whatever `from` values you pick.
- conflicts is data: add pairs (top-k vs min-p), reword the message, or pass []
to remove the nudge entirely. The action always resets the SECOND key —
order the pair so the one people should leave alone comes last.
- Density: the row gap (gap-5), the hint lines and the band strip are the three
things to cut for a compact sidebar; keeping label + badge + track alone
still leaves a complete control.
- Chrome: `title`, the header icon and the "N changed" pill are independent —
drop them for an embedded panel that already sits under a heading.
- Wiring: onChange for live preview, onCommit for anything expensive (persist,
refetch a cost estimate, restart a stream). Both hand you the same sparse
object; spread it into your request body as-is.Concepts
- Sparse value object — a parameter lives in the object only while it differs from its default, so "untouched" and "explicitly sent the default" stay different things; dragging a slider back home deletes the key and shrinks the request body again.
- Default-anchored fill — the coloured part of the track runs between the default tick and the current value instead of from the minimum, which is the only way a bipolar penalty (-2…2, default 0) and a ceiling-valued top-p (default 1) read correctly on the same widget.
- Worded band scale — temperature's number is meaningless on its own, so the range is cut into named bands (Precise / Balanced / Creative / Wild) drawn proportionally under the track; the active band's label also rides in
aria-valuetext, so the meaning is not sighted-only. - Pairing nudge with a one-shot dismissal — two parameters that fight each other raise a polite, non-blocking notice derived from the values (so a preset that arrives already conflicting is caught too); dismissing it hides it until the pair returns to clean, at which point the nudge is armed again.
- Disclosure that cannot hide a change — the collapsed Advanced group carries its own "N changed" count, so progressive disclosure never turns into a value the user cannot see; the region stays mounted so
aria-controlsalways points at something real. - Change versus commit —
onChangefires on every frame of a drag for live preview,onCommitfires once when the interaction settles, which is where a save, a refetch or a cost re-estimate belongs.
Tone Selector
A radio-group row of rewrite tones where every pill carries a sample phrase, and the custom tone applies only once its free-text instruction is committed.
System Prompt Editor
A system-prompt editing surface: an autosizing monospace field with placeholder highlighting painted behind the caret, a live token estimate against a budget, presets that insert without destroying work, and an unsaved bar that owns Save and Discard.