AI

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.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import {
  type Announcements,
  closestCenter,
  DndContext,
  type DragEndEvent,
  KeyboardSensor,
  PointerSensor,
  useSensor,
  useSensors,
} from "@dnd-kit/core"
import {

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/few-shot-editor.json

Prompt

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>.

On This Page