Xzero UI
Pro library

Prompt studioPro

One card for editing, testing and shipping a prompt template: variables as chips with sample values, a test run against the chosen model, and versions you can compare and promote to production.

$ pnpm dlx shadcn@4.23.0 add @xzero-ui-pro/prompt-studio
Live · keyboard ready

Pass template, variables and versions to load your own prompt, run to call your model, and onPromote to ship a version; with no props it edits a sample support prompt.

When to use

When not to use

Installation

Register both Xzero UI namespaces in your project's components.json:

{
  "registries": {
    "@xzero-ui": "https://mcp.xzero.co.uk/r/{name}.json",
    "@xzero-ui-pro": "https://mcp.xzero.co.uk/r/pro/{name}.json"
  }
}
npx shadcn@4.23.0 add @xzero-ui-pro/prompt-studio

Install xzero-foundation once, and import @/components/xzero/foundation.css in your app root. No Xzero token is used by this snapshot. For sample portraits, photos, fonts, or logos, download https://mcp.xzero.co.uk/install-assets.mjs and run node xzero-install-assets.mjs <receiving-project>.

Usage

import { PromptStudio } from "@/components/xzero/blocks/prompt-studio/prompt-studio";

export function SupportPrompt({ prompt, cases }) {
  return (
    <PromptStudio
      name={prompt.name}
      versions={prompt.versions}
      productionId={prompt.productionId}
      variables={["customer_name", "order_id", "policy"]}
      testCases={cases}
      onRun={async function* (request, signal) {
        const response = await fetch("/api/prompt-run", { method: "POST", body: JSON.stringify(request), signal });
        const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader();
        for (;;) {
          const { value, done } = await reader.read();
          if (done) return;
          yield value;
        }
      }}
      onPromote={versionId => setProduction(prompt.id, versionId)}
    />
  );
}

// A version is plain text with {{variables}}.
const version = {
  id: "v4", label: "v4", createdAt: "2026-10-16T15:20:00Z", message: "Escalate when the policy falls short",
  author: { name: "Hannah Walsh", avatar: "/media/people/hannah-walsh.jpg" },
  system: "You are a support agent for Fernwood. Follow this policy exactly: {{policy}}",
  user: "Reply to the latest message from {{customer_name}} about order {{order_id}}.",
};

API reference

PromptStudio

One card for editing, testing and shipping a prompt template. The header holds the prompt's name, a version chip (a menu of versions with compare, promote to production and undo) and a model chip (model and effort). System and User tabs show the template with {{variable}} placeholders as atomic chips, each variable in its own series color. A row of colored fields fills the variables from saved test cases, and Run streams the reply into a quiet panel at the foot with latency, tokens and cost. Compare turns the editor into an inline word diff.

PropTypeDefaultDescription
namestring"Support reply"Name of the prompt, shown in the header.
versionsPromptVersion[]sampleVersionsSaved versions, newest first. Each has id, label ("v4"), message, author ({ name, avatar? }), createdAt (ISO date) and the system and user text.
versionIdstring–The version open in the editor; the chip shows its label. Defaults to the newest version.
productionIdstring | null–The version production serves; it carries the Production marker. Defaults to the open version.
template{ system: string; user: string }–The draft the editor opens with. Defaults to the open version's text.
variablesstring[]–Variable order for colors and fields: the first four take --series-1 to --series-4, then the colors repeat. Variables found in the text but not listed are added after. Defaults to the order they appear in the open version.
testCasesPromptTestCase[]sampleCasesSaved test cases as { id, label, values }, where values maps a variable name to its value. The first case is loaded on mount. Keep labels short: the cases share one segmented control.
modelsPromptModel[]studioModelsModels for the model menu: id, name, short (chip label), mark ("claude" | "openai" | "gemini"), input and output price per million tokens, sample (marks illustrative prices) and defaultEffort.
defaultModelstring"claude-sonnet-5-5"Id of the model selected on first render.
defaultEffort"low" | "medium" | "high" | "xhigh" | "max"–Effort selected on first render. Defaults to the model's own default (high for Claude Sonnet 5.5). Choosing another model resets effort to that model's default.
nowstring | number | Date–Reference moment for the relative dates in the version menu. Defaults to the current time, or to Oct 20, 2026 for the sample versions so the demo never ages.
onRun(request: PromptRunRequest, signal: AbortSignal) => Promise<string | AsyncIterable<string>> | AsyncIterable<string>–Runs the prompt. The request carries the raw template, the rendered template, the values, model, effort, test case id and version id. Return the whole reply or an async iterable of text deltas; throw to show the error state with Retry. Stop aborts the signal. Without it, runs are simulated and labelled as such.
onPromote(versionId: string, previousId: string | null) => void–Called when a version is promoted to production, and again with the ids swapped when the promotion is undone.
onTemplateChange(template: { system: string; user: string }) => void–Called with the draft after every edit, for autosave.
classNamestring–Class on the root element.

PromptEditor

The chip editor on its own, exported from prompt-editor.tsx. A plain text contenteditable where the string is the model: every edit is applied to the string and the DOM is rebuilt from it, so chips cannot be split and copy and paste keep the braces.

PropTypeDefaultDescription
value (required)string–The template text, with placeholders written as {{name}}.
onChange (required)(value: string) => void–Called with the new text after every edit. Placeholders are normalized to {{name}} without inner spaces.
variables (required)string[]–Names offered after typing {{.
toneOf (required)(name: string) => number–Color slot (1 to 4) for a variable, read as --series-1 to --series-4.
label (required)string–Accessible name of the text box.
missingstring[]–Variables without a value; their chips draw a dashed outline.
activeVariablestring | null–A variable to light up, for example while its value field has focus.
flash{ name: string; at: number } | null–Flashes every chip of a variable once. Pass a new at to flash again, for example on each keystroke in its field.
onChipClick(name: string) => void–Called when a chip is clicked.
onSubmit() => void–Called on Mod+Enter.
placeholderstring–Shown while the text is empty.

Keyboard interactions

KeysAction
Mod+EnterRuns the prompt for the selected test case from anywhere in the card, including the editors and the variable fields.
{{In the editor, opens the list of known variables. Arrow up and down move, Enter or Tab inserts the variable as a chip, Escape closes the list. Typing a new name then }} creates a variable and a field for it.
Backspace, DeleteRemove a chip whole when the caret is beside it. Option or Ctrl with Backspace removes a word.
Mod+Z, Shift+Mod+ZUndo and redo inside the editor. Typing within a short pause is one step.
Arrow left / Arrow rightOn the System and User tabs, the test case control and the effort control, move the selection.
Arrow up / Arrow downIn the version menu and the model menu, move between rows and select them.
EscapeCloses an open menu. In compare mode, returns to editing.
TabMoves through the version chip, model chip, tabs, editor, each variable field, the test cases, Run, and Copy or Retry.

Accessibility

Motion

Responsive behavior

Performance

Notes for AI

Guidance for AI tools

Blocks are complete, self-contained screens with sample data. Replace the sample data and connect the callbacks described above. Follow the declared prop types and do not invent props. Keep keyboard access, reduced motion support, and both light and dark themes intact when adapting it.

Full library index: https://ui.xzero.co.uk/llms.txt

Browse all components