Few Shot Editor
A few-shot example manager — paired input/output cards you add, duplicate, drag-reorder and switch off, with per-example and total token estimates against a budget, and gentle hints for blank outputs and near-duplicate inputs.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/few-shot-editor.jsonPrompt
Build a React + TypeScript + Tailwind "FewShotEditor" component with zod,
lucide-react and dnd-kit (@dnd-kit/core + /sortable + /utilities), reusing an
autosizing textarea for the two editable fields.
Contract
- A zod schema in a sibling contract file is the single source of truth for ONE
demonstration pair: { id: string; input: string; output: string; enabled:
boolean }. `id` must be unique and stable — it is the drag identity, the React
key and the key every token estimate and lint hint is filed under. Never the
array index: a reorder would move identity with the slot instead of with the
example, and the focused textarea would remount mid-word.
- `enabled` is the reason this contract exists. Switching an example off is not
deleting it: the pair stays, stays readable, stays costed, and the prompt
simply ships without it. Every count (the "3 on" line, the token total) is
computed over the enabled subset, because that is what costs money.
- A separate envelope status — "loading" | "empty" | "error" | "ready" — is the
editor's own render state, independent of any one example. "empty" is the data
layer saying nothing is saved yet; "ready" with an empty array is "you just
deleted the last one". Both offer Add; only the first explains itself.
- The contract file also exports THREE pure, React-free functions, so a server
route that assembles the real prompt can price and lint it with the exact code
the author saw: estimateTokens(text), inputSimilarity(a, b), and
lintFewShotExamples(examples, { threshold }).
- Props: status? ("ready"); examples? (controlled) / defaultExamples?
(uncontrolled seed); onChange?(next, change); maxExamples?; tokenBudget?;
countTokens? (defaults to estimateTokens); overheadTokensPerExample? (4);
duplicateThreshold? (0.82); inputLabel? ("Input"); outputLabel? ("Output");
previewChars? (96); defaultExpandedIds?; createExample?; emptyState?;
errorMessage?; onRetry?; label? ("Few-shot examples"); className plus the rest
of the element props, ref forwarded to the <section>.
- `onChange` always receives a BRAND-NEW array plus a discriminated change
descriptor — { kind: "add" | "duplicate" | "remove" | "move" | "edit" |
"toggle", … } — so a consumer can debounce an autosave per kind, drive an undo
stack, or log which affordance people actually use. The input array is never
mutated in place.
- Every numeric prop is clamped (maxExamples >= 1, overhead >= 0, previewChars
>= 16, threshold in [0, 1], a non-positive budget means "no budget"), so a 0
or a NaN can never render an empty or permanently-alarming editor.
Behavior
- COLLAPSED IS THE DEFAULT. A card is one scannable line — the first line of the
input, an arrow, the first line of the output, both capped at previewChars —
and the disclosure opens the two labelled autosizing textareas. Twelve
examples are a list to skim, not a wall of textareas. The panel is UNMOUNTED
when closed, never height-0: a collapsed card whose textareas are still in the
tab order is an invisible keyboard trap. `aria-controls` is set only while
open so it never points at an id that is not in the document.
- Add appends a blank pair, OPENS it and moves the caret into its input; a new
card that arrives closed is a card you have to hunt for. Duplicate inserts the
copy directly after its source with a fresh id, opens it too, and leaves the
original untouched — which is exactly how the near-duplicate hint earns its
keep two edits later.
- Delete is ARM-THEN-CONFIRM for anything you wrote, and immediate for a card
that is still blank — there is nothing there to lose. The primed button keeps
its slot and its focus and only changes its label and its colour, so the
second press lands on the same key the first one did. The primed state expires
by itself after a few seconds, Escape cancels it (and the key is swallowed
ONLY while something is primed, so Escape still closes the dialog the editor
may be sitting in), and any other edit disarms it. The timer is cleared on
unmount and before it is ever re-armed.
- FOCUS FOLLOWS THE STRUCTURAL EDIT: into the new card after an add or a
duplicate, onto the remove button that slid into the freed slot after a
delete, and onto Add when the list is now empty. Without this the focused
button unmounts and focus falls back to <body>, stranding keyboard users at
the top of the page. The intent is stored as a fresh state OBJECT per edit, so
the effect fires once per edit and needs no "clear the queue" write; the card
is then resolved by dataset comparison rather than an attribute selector,
because a consumer id containing a quote would throw inside querySelector.
- Reorder is dnd-kit sortable with FULL keyboard parity: the grip is a real
button (Space to lift, arrows to move, Space to drop) and every card also
carries explicit Move up / Move down buttons. dnd-kit's default announcements
read raw internal ids out loud, so replace them with positional ones ("Picked
up example 2 of 5"), and do NOT announce the drop yourself — dnd-kit already
narrates it through its own live region and the reader would hear it twice.
Pin the DndContext id (useId) or its aria ids drift between server and client.
- ORDER IS PAYLOAD, not decoration: the array order is the order the examples
reach the model, which is why reordering is a first-class action rather than a
sort control.
- The token estimate is per example (input + output + a per-pair serializer
overhead) and a total over the ENABLED subset only. An off example still shows
its own number, struck through — seeing what it would cost is the reason you
kept it. `tokenBudget` is a SOFT ceiling: past it the total turns destructive
and a line says exactly how far over it is. Nothing is ever truncated, nothing
is disabled; the editor states the number and lets the author decide.
- The estimate is always written as an estimate ("~1,204 tokens", never
"1,204"). The default estimator ships no vocabulary: a word-ish run costs one
token per ~4 characters and every other non-space character costs one, which
lands CJK at roughly one token per glyph. Pass `countTokens` to swap in the
real tokenizer once you are willing to ship its vocabulary.
- LINT IS HINTS, NEVER ERRORS. Three per-card hints — empty input, empty output,
near-duplicate input — and one about the set: every example is switched off.
Nothing here blocks anything and nothing is styled as a failure: a half-written
example is a normal intermediate state. Hints stay visible while the card is
COLLAPSED, otherwise the editor would hide the problem it just found.
- Near-duplicate detection is a Dice coefficient over character trigrams of the
normalised input (trimmed, lowercased, whitespace collapsed, capped at a couple
of thousand characters), compared only among ENABLED examples with a non-empty
input — an example that is off cannot be redundant, and a blank card is "not
written yet", not "a copy". Each example reports at most ONE twin (the earliest
it matches), so four near-copies read as three nudges instead of twelve.
- The editor is controlled or uncontrolled: pass `examples` and echo `onChange`,
or pass `defaultExamples` and let it own the array. `defaultExpandedIds` is
read once, on mount — after that the disclosure belongs to the editor.
- Loading renders skeleton rows shaped like real ones under aria-busy plus one
sr-only status line. Error is role="alert", prints the message in full, and
offers Retry only when `onRetry` is passed — a retry button that retries
nothing is worse than none.
Rendering & styling
- Semantic tokens only: bg-card / border / text-muted-foreground for chrome,
bg-primary for the enabled switch track, bg-background for its thumb,
text-destructive with bg-destructive/10 for the primed delete and the
over-budget total, border-dashed for both the empty panel and Add. No
hard-coded colours anywhere, so a theme swap is free and dark mode comes along.
- The per-example switch is a <button role="switch" aria-checked> with a STATIC
accessible name ("Include example 2 in the prompt"): a name that flipped
between "Include" and "Exclude" would read out the opposite of what
aria-checked already said. Blocked controls use aria-disabled (never the
native attribute, which drops them out of the tab order) plus aria-describedby
pointing at the line that says why.
- One sr-only role="status" live region for the whole editor announces the
structural edits the reader cannot see happen (added, removed, moved, on/off).
- Every transition is motion-reduce-guarded, and dnd-kit's inline settle
transition is dropped in JS under prefers-reduced-motion (a CSS-only variant
cannot beat an inline style). Dragging keeps working; only the easing goes.
- Long values never widen the layout: min-w-0 everywhere, `truncate` in the
collapsed line, `wrap-anywhere` in the hints, and the textareas grow between
minRows and maxRows and then scroll internally.
- cn() merges the consumer's className into the root <section>, which also
spreads the remaining element props and forwards its ref. `onKeyDown` is
attached AFTER the spread and calls the consumer's handler itself, so a
passed-in handler cannot silently replace the one that cancels a primed delete.
Customization levers
- Vocabulary: `inputLabel` / `outputLabel` retitle the pair for the task at hand
(User / Assistant, Question / Answer, Source / Translation) — they are
presentation, the data shape never changes.
- Budget policy: `tokenBudget` sets the soft ceiling, `countTokens` swaps the
estimator for a real tokenizer, `overheadTokensPerExample` teaches it what
YOUR serializer costs per pair (role tags, separators). Set the budget to
undefined and the total is plain information.
- Volume: `maxExamples` caps the set (and turns on the `4/8` counter);
`previewChars` decides how much of each side the collapsed line keeps;
`defaultExpandedIds` opens the cards you just imported.
- Strictness: `duplicateThreshold` moves the near-duplicate line — raise it
towards 1 for a set of deliberately similar edge cases, lower it when you are
hunting redundancy. Drop the hint list entirely if your data is machine-made.
- Identity: `createExample` is where you mint ids the way your backend does
(uuid, cuid, a server-issued draft id) and pre-fill a house template.
- Chrome: `label` names the region, `emptyState` replaces the first-run body,
`errorMessage` + `onRetry` own the envelope failure.
- Density: the card is one flex row plus an optional panel — drop the Move
up/down buttons if the grip is enough for your users, or drop the token column
in a surface where cost is somebody else's problem.Concepts
- Enabled is not deleted — the switch keeps the pair, its text and its number while taking it out of the prompt, which is what makes "does this example actually help?" a two-second experiment instead of a paste-buffer ritual. Every aggregate (the active count, the total, the duplicate scan) is computed over the enabled subset, because that is the prompt you are actually paying for.
- Order is payload — the array order is the order the examples reach the model, so reordering is a first-class action (drag by the grip, or Space plus arrow keys, or the explicit Move up/down buttons) rather than a view-only sort. dnd-kit narrates the drop itself; announcing it a second time would say everything twice.
- Collapsed preview vs expanded edit — a card is one scannable line until you open it, and the opened panel is unmounted again on close rather than collapsed to zero height, so a closed card can never hide two textareas that are still reachable by Tab.
- An estimate that admits it is one — every number is prefixed
~, the per-pair serializer overhead is part of it, an off example shows its size struck through, and the budget is a soft ceiling: past it the total turns destructive and states the overshoot. Nothing truncates, nothing is disabled — the editor gives you the figure and the decision stays yours. - Hints, not errors — blank output, blank input, near-duplicate input and "everything is switched off" are derived from the array by a pure function, rendered as muted lines, and block nothing. Near-duplicates are found with a trigram Dice coefficient over normalised inputs, among enabled examples only, one twin per example — a redundancy nudge should never turn into a wall of alarms.
- Arm-then-confirm, with focus that lands somewhere — deleting written work takes a second press on the same button (a blank card goes on the first), the primed state expires by itself and Escape cancels it, and after the delete focus moves to the row that took the freed slot instead of falling back to
<body>.
Tokenizer Preview
A tokenizer playground for input text — alternating-tinted token chunks from a deterministic built-in splitter or your own, token/char/word counts, a live cost estimate at an editable per-Mtok rate, and markers for whitespace and unusually long tokens.
Embedding Map
A 2-D embedding scatter in pure SVG — clusters carried by chart token and mark shape, centroid names that yield instead of overlapping, a query wired to the retriever's own ranking, and four data states.