orizu 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1461 @@
1
+ # Orizu apps — agent reference
2
+
3
+ You are reading the canonical reference for coding agents producing Orizu apps. Orizu apps are React/TSX components that render inside Orizu's task and preview screens to collect feedback from human reviewers: ratings, comments, comparisons, annotations, rankings, corrections, and agent-transcript review. This document covers everything needed to author a working app — runtime contract, conventions, design principles, the import registry, the per-component reference, recipes, and common pitfalls. Read top-to-bottom on first encounter; jump to specific sections by anchor afterwards.
4
+
5
+ If you are an external coding agent that found this via `orizu.ai/docs/llms.txt`, treat this document as the source of truth for both the runtime contract and the available primitives.
6
+
7
+ ---
8
+
9
+ ## TL;DR
10
+
11
+ You are writing a single TSX file. It must:
12
+
13
+ 1. Use **named imports only**, and only from the paths in the **Imports** section below. There is no `npm install`; any other path fails at runtime with `Module not found`.
14
+ 2. Export a **named** default function whose props are exactly `{ inputData, onComplete, initialValues }`.
15
+ 3. Read the row to label from `inputData`. Pre-populate from `initialValues` if present.
16
+ 4. When the reviewer finishes, call `onComplete(value)`. The value you pass is recorded as the response.
17
+
18
+ A complete, valid app:
19
+
20
+ ```tsx
21
+ import { useState } from 'react';
22
+ import { StarRating } from '@/components/base/input/StarRating';
23
+ import { CommentBox } from '@/components/base/input/CommentBox';
24
+ import { Button } from '@/components/ui/button';
25
+
26
+ export default function Component({ inputData, onComplete, initialValues }) {
27
+ const [rating, setRating] = useState(initialValues?.rating ?? 0);
28
+ const [note, setNote] = useState(initialValues?.note ?? '');
29
+
30
+ return (
31
+ <div className="flex flex-col gap-4 p-4">
32
+ <p className="text-sm">{inputData.text}</p>
33
+ <StarRating value={rating} onChange={setRating} />
34
+ <CommentBox value={note} onChange={setNote} label="Why?" />
35
+ <Button
36
+ onClick={() => onComplete({ rating, note })}
37
+ disabled={rating === 0}
38
+ >
39
+ Submit
40
+ </Button>
41
+ </div>
42
+ );
43
+ }
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Runtime contract
49
+
50
+ Your default export receives three props from the Orizu runtime:
51
+
52
+ | Prop | Type | Meaning |
53
+ |-----------------|---------------------|----------------------------------------------------|
54
+ | `inputData` | object | The current dataset row's payload (matches `input_json_schema`). |
55
+ | `onComplete` | `(payload) => void` | Submit the annotation. Payload must match `output_json_schema`. |
56
+ | `initialValues` | object \| undefined | Previous response if the reviewer resumed; otherwise undefined. |
57
+
58
+ The validator rejects:
59
+
60
+ - **Anonymous default exports** such as `export default () => ...` or `export default memo(...)`. The default export must be a named function or class.
61
+ - **Default exports that destructure `data`** instead of `inputData`, or `onSubmit` instead of `onComplete`. The deprecated names fail validation at the app boundary.
62
+ - **Components that don't accept these props at all.**
63
+
64
+ You may use any other props internally (`useState`, `useReducer`, and so on); just keep the default export's signature exact.
65
+
66
+ ### Output schema
67
+
68
+ `onComplete(payload)` is validated server-side against the pinned app version's `output_json_schema`. The validation surface is intentionally a **subset** of JSON Schema:
69
+
70
+ - `type` (`object`, `string`, `number`, `boolean`, `array`)
71
+ - `required`
72
+ - `properties`
73
+ - `items`
74
+ - `enum`
75
+
76
+ Anything else (`pattern`, `format`, `oneOf`, `anyOf`, `minLength`, …) is ignored or rejected. Keep schemas in this subset and have the component construct the payload literally — don't compute it at submit time from scattered state.
77
+
78
+ Both `input.json` and `output.json` are JSON Schema objects:
79
+
80
+ ```json
81
+ {
82
+ "type": "object",
83
+ "properties": {
84
+ "correctly_identified_issue": { "type": "boolean" },
85
+ "escalated_when_required": { "type": "boolean" },
86
+ "notes": { "type": "string" }
87
+ },
88
+ "required": ["correctly_identified_issue", "escalated_when_required"]
89
+ }
90
+ ```
91
+
92
+ ---
93
+
94
+ ## App vs container ownership
95
+
96
+ The app component renders **one row at a time**. It does not navigate between rows, track queue position, persist drafts, or own session state. Those concerns belong to the platform's labeling container, which hydrates the app with a row's `inputData` (and any `initialValues` from a prior partial submission), then collects the response when the app calls `onComplete`.
97
+
98
+ | Concern | Owned by |
99
+ |----------------------------------------------------------------|-------------------------|
100
+ | Rendering the current row | **App** |
101
+ | Collecting and structuring the response | **App** |
102
+ | Internal UI state for the row (selections, drafts, validation) | **App** |
103
+ | Calling `onComplete(payload)` when ready | **App** |
104
+ | Queue position / "3 of 200" | Container |
105
+ | Submission confirmation / "saved!" toast | Container |
106
+ | Navigation between rows (next / back) | Container |
107
+ | Draft persistence across sessions | Container (via `initialValues`) |
108
+ | Auth, layout chrome, top/side bars | Container |
109
+
110
+ Keep the app's job tight: show the row, collect the response, hand it back. Do not reimplement queue UI, custom saved toasts, or session state. In-row progress hints such as "3 of 5 questions answered" *are* fine — those describe the current row's state, not the container's.
111
+
112
+ ---
113
+
114
+ ## Compile & resolution model
115
+
116
+ - Your TSX is compiled server-side with **esbuild** to a CommonJS bundle. JSX is transformed to the automatic React runtime.
117
+ - Imports are resolved at runtime via a **fixed registry** (see Imports below). There is no `npm install`, no third-party packages, no deep paths beyond what is listed.
118
+ - An import path that isn't in the registry throws `Module not found: Cannot find module '<path>'` at render time. Always copy paths verbatim from the Imports section.
119
+ - React itself is auto-injected — `import React from 'react'` is allowed but not required. Hook imports like `import { useState } from 'react'` are fine.
120
+
121
+ ---
122
+
123
+ ## Use as-is, or inline a fork
124
+
125
+ When you reach for a component, you have exactly two paths. Make this decision deliberately for every primitive you use.
126
+
127
+ 1. **Use as-is.** Import the registered component by name and pass the documented props. This is the default and the right answer for the vast majority of tasks. Wrapping the component in your own layout `div` for spacing, headings, or surrounding logic is still "use as-is" — the component itself is unchanged.
128
+
129
+ 2. **Inline a private fork.** If the registered component's prop set or internal behaviour genuinely doesn't fit your task, fetch its source from the URL in the Component reference (every section has a `**Source:**` link), copy the implementation into your TSX file as a private component (e.g. `MyStarRating`), and adapt it. The runtime resolves imports against a fixed registry — you **cannot import a forked version** from any path the registry doesn't already expose, so any fork must live inline in the same file alongside your default export.
130
+
131
+ Read the source whenever a primitive almost-but-not-quite fits — it shows you the structure, the styling tokens, and the accessibility hooks already worked out, so your fork stays faithful to the design language. Inlining is a real, supported option, not a fallback to apologise for; just don't reach for it before checking whether composition or wrapping covers the case.
132
+
133
+ ---
134
+
135
+ ## Custom components
136
+
137
+ You aren't limited to the registered primitives. The TSX file can also declare any number of helper or composite components inline — standard React, no special framework requirements. Useful when the listed components don't cover your task or when you want a small, named abstraction for repeated structure.
138
+
139
+ ### The default export contract is the only contract
140
+
141
+ Only one component in the file is bound by Orizu's contract: the default export. It must be a **named** function and accept exactly `{ inputData, onComplete, initialValues }`. Helper components defined alongside it have **no contract** — name them whatever you want, give them any prop shape, and call them from your default export like normal React.
142
+
143
+ ```tsx
144
+ function QuestionRow({ id, label, value, onChange }) {
145
+ // any prop shape, any name; not bound by the Orizu contract.
146
+ return (
147
+ <label className="flex items-center justify-between py-2">
148
+ <span className="text-sm">{label}</span>
149
+ <ThumbsRating value={value} onChange={(v) => onChange(id, v)} />
150
+ </label>
151
+ );
152
+ }
153
+
154
+ export default function Component({ inputData, onComplete, initialValues }) {
155
+ // ...uses QuestionRow internally
156
+ }
157
+ ```
158
+
159
+ ### Building from scratch vs. forking a primitive
160
+
161
+ Two paths, both supported:
162
+
163
+ - **From scratch.** Write standard React, style with Tailwind utilities, keep imports inside the registry. No `npm install`, no third-party packages, no global CSS. Hooks like `useState`, `useReducer`, `useEffect` are available; pull them from `'react'`.
164
+ - **Forking a primitive.** Open the relevant component in the **Component reference** below, follow the `**Source:**` link to read the underlying TSX, copy the implementation into your file as a private component (e.g. `MyStarRating`), rename it, and adapt the internals. The runtime can't resolve a forked import path — any fork must live inline in the same file.
165
+
166
+ Either way, the rules below apply.
167
+
168
+ ### What custom components must respect
169
+
170
+ - **Imports come from the registry only.** No deep paths beyond what is listed in the **Imports** section.
171
+ - **Style with Tailwind utilities and design tokens** (`text-foreground`, `text-muted-foreground`, `text-primary`, `text-destructive`). Don't override `app/globals.css` or rely on arbitrary Tailwind colors (`text-red-500`).
172
+ - **Stay accessible**: visible focus, semantic elements, labels above inputs, color isn't the only state signal.
173
+ - **The whole app ships in one TSX file** — no multi-file imports, no separate CSS modules, no asset imports.
174
+
175
+ If a helper grows large enough that you'd reach for a separate file, that's a signal to either (a) compose existing primitives differently, (b) inline-fork a primitive that's closer to what you want, or (c) request a new platform-level primitive rather than smuggling a multi-file pattern into a single file.
176
+
177
+ ---
178
+
179
+ ## Conventions
180
+
181
+ ### Naming
182
+ - Components are PascalCase, named after what they ARE not what they DO. Prefer `StarRating` to `RateWithStars`.
183
+ - Boolean props: `isX` / `hasX`.
184
+ - Event handlers: `onX`.
185
+ - When a callback fires for a specific item, the id comes BEFORE the value: `onChange(id, value)`.
186
+
187
+ ### Controlled inputs
188
+ Every input component is fully controlled. There are no `defaultValue` props. Always pass `value` and `onChange`. `readOnly` flattens visuals AND disables pointer/keyboard interaction.
189
+
190
+ ### Reserved props
191
+ - `id` — required where listed; must be unique per render. Used by analytics and a11y helpers.
192
+ - `readOnly` — boolean. Used by interaction-disable helpers.
193
+
194
+ Do not repurpose these names for unrelated values.
195
+
196
+ ### Styling
197
+ - Components are token-driven via Tailwind + CSS variables. Do not re-style components themselves — wrap them in a layout div if you need spacing or alignment.
198
+ - Use the standard Tailwind utility set inside your component. Custom CSS modules are not supported.
199
+ - Use design tokens (`text-foreground`, `text-muted-foreground`, `text-primary`, `text-destructive`) rather than arbitrary Tailwind colors (`text-red-500`).
200
+
201
+ ### Never invent imports
202
+ Never invent a new import path; never rely on third-party packages. If a registered component doesn't fit, your two real options are spelled out under **Use as-is, or inline a fork** above — wrap, or copy the source inline.
203
+
204
+ ---
205
+
206
+ ## Mental model: components × behaviors
207
+
208
+ Pick a content component, pick a behavior, fill the slot. Examples:
209
+
210
+ - A `ConversationView` turn (content) wrapped in `Reactable` (behavior) whose `renderForm` slot holds a `CommentBox` (input) → per-turn thumbs with a reason field.
211
+ - A `TextContent` (content) wrapped in `Annotatable` (behavior) whose `renderAnnotation` slot holds a `TagPicker` (input) → span-level tagging.
212
+
213
+ The four roles:
214
+
215
+ - **Typography (`Prose`, `Prose.Body`, `Prose.H1`, etc.)** — what *you* write to frame the task: instructions, headings, helper copy. Lives outside the box.
216
+ - **Content (`TextContent`, `CodeBlock`, `ConversationView`, `ContentRenderer`)** — what the model produced or what's under review. Lives inside a box.
217
+ - **Behaviors (`Annotatable`, `Reactable`)** — wrap any content component to make it reviewable. They don't render content; they add affordances and slot in your input surface.
218
+ - **Input (`CommentBox`, `TagPicker`, `StarRating`, etc.)** — the surfaces you slot inside a behavior, or use directly to capture feedback.
219
+
220
+ ---
221
+
222
+ ## Design principles
223
+
224
+ A labeler is an interface annotators stare at for hours. It should feel like an app they want to use, not a form they tolerate. Treat the basics — type, color, spacing, hierarchy — as load-bearing, not decoration.
225
+
226
+ ### Layout & visual hierarchy
227
+
228
+ - The data is the UI. Push the annotator's attention to the trace they're judging — minimize chrome around it.
229
+ - One container per logical region. Don't stack cards-inside-cards. Rely on whitespace and typography to separate sections, not nested borders.
230
+ - Visual hierarchy through size, weight, and position — not boxes, shadows, or gradients.
231
+ - Heavy shadows, tinted backgrounds, and busy gradients add visual noise without adding information. Stay flat or near-flat.
232
+ - Establish one focal point per screen. The annotator should always know where to look first.
233
+
234
+ ### Typography
235
+
236
+ Legibility over personality. Annotators read for hours; the type system is doing the heavy lifting whether you notice it or not.
237
+
238
+ - **Two typefaces, max.** A proportional sans-serif for UI and prose; a monospace for code, JSON, tool calls, IDs. System font stacks (`-apple-system, ui-sans-serif, ...` and `ui-monospace`) are a perfectly good default — they're optimized for OS rendering and ship at zero weight cost. If you want a custom face, Inter, IBM Plex Sans, or Geist are reliable choices.
239
+ - **Keep the type scale small.** ~5 sizes total: 12 / 14 / 16 / 20 / 24 px. 16px is your prose default; 14px for dense UI; 12px reserved for metadata and footnotes.
240
+ - **Weight contrast > size contrast.** Use 500 / 600 weights for headers at the *same* size as body, rather than scaling up to 28–32px. Feels modern, saves vertical space, keeps the page calm.
241
+ - **Line height.** 1.5 for prose, 1.3 for UI labels and dense lists, 1.5–1.6 for code blocks. Tight code is hard to scan.
242
+ - **Line length.** Cap prose at 70–90 characters per line. Full-width text on a 27" display is unreadable.
243
+ - **Color contrast in type.** Body at full text color; secondary metadata at ~60% opacity (or a muted gray); never below ~4.5:1 contrast.
244
+ - Don't mix monospace and proportional inside a single inline run — it reads broken.
245
+
246
+ ### Color
247
+
248
+ A muted, restrained palette beats a vibrant one for any tool people use professionally.
249
+
250
+ - **Neutrals carry the design.** 80–90% of the UI should be neutrals: a near-white background (or near-black in dark mode), one slightly off-tone surface for cards/panels, three or four gray text tones (primary, secondary, tertiary, disabled).
251
+ - **One accent color.** Pick one and use it for selection, focus rings, primary actions, and links. Resist the urge to introduce more.
252
+ - **Semantic colors only for state.** Green = pass / success. Red = fail / destructive. Amber = warn (use sparingly — most things are pass or fail). Don't use these as decoration or as the accent.
253
+ - **No category palettes.** If you need to distinguish many tags, roles, or types, use one color with text labels — not 8 distinct hues. Color is for state, not categorization.
254
+ - **Test in light and dark mode.** A design that only works on one is fragile.
255
+ - **Avoid opacity-as-color.** `text-black/40` for disabled or muted is fine; building a whole palette out of opacities falls apart on tinted backgrounds.
256
+
257
+ ### Buttons & action hierarchy
258
+
259
+ - **One primary button per screen** (ideally per region). Filled, accent color, the single most-likely action — usually "Submit & next" or "Save."
260
+ - **Secondary actions are outlined or ghost.** "Skip," "back," "open notes." Visually quieter than primary; same height, different weight.
261
+ - **Tertiary actions are plain text or icon-only.** "Copy," "expand," "edit." No fill, no border.
262
+ - **Destructive actions are explicitly red**, never primary by default. Require a deliberate path — confirm modal for "delete row," hold-to-delete for inline destructive actions.
263
+ - **Buttons that are usually disabled are a smell.** Either the enabling condition is unclear, or the button shouldn't appear until the action is ready.
264
+ - **Size by importance, not whim.** Comfortable hit area for primary; slightly smaller secondary; inline-sized tertiary. Pick a small set of heights (e.g. 28 / 32 / 40 px) and stick to them.
265
+ - **Show the keyboard shortcut on the button itself** — `Submit ⏎`, `Skip (s)`. Tooltips are too easy to miss.
266
+
267
+ ### Spacing & rhythm
268
+
269
+ - **Use a consistent scale.** 4 / 8 / 12 / 16 / 24 / 32 / 48 px (or equivalent). Don't pad with arbitrary values like 13px or 27px.
270
+ - **Whitespace where it aids parsing**, not as decoration. Tight clusters when things relate; gaps between unrelated regions.
271
+ - **Vertical rhythm.** Equal vertical spacing between siblings keeps the eye moving smoothly. Inconsistent spacing reads sloppy even when nothing else is wrong.
272
+ - **Border radius.** Pick one (4px or 6px reads modern; 8px is slightly softer; 12+ feels playful and is usually too much for a labeler) and use it everywhere.
273
+ - **Borders, not shadows.** A 1px subtle border separates regions cleanly. Shadows imply elevation, which a labeler rarely needs.
274
+
275
+ ### Forms & inputs
276
+
277
+ - **Labels above inputs**, not beside. Easier to scan, works on every screen size, accessible by default.
278
+ - **Tap/click targets ≥ 32px tall.** Smaller feels cramped and hurts on touch.
279
+ - **Inline error states**, near the field. Don't summarize errors at the top of the form.
280
+ - **Placeholder is not a label.** Placeholder disappears when typing — use it for examples ("e.g. CASE-123456"), not for the field's name.
281
+ - **Auto-focus the primary field** on mount. Annotators shouldn't have to click to start.
282
+ - **Show character/token counts** only when there's a real limit; otherwise it's noise.
283
+ - **Inputs match button heights.** A 32px input next to a 40px button looks broken.
284
+
285
+ ### Focus, hover, active states
286
+
287
+ - **Visible focus ring on every interactive element.** Keyboard users need to know what's focused. Don't suppress browser focus rings without replacing them — and replace them with something at least 2px and high-contrast.
288
+ - **Hover states are clear but subtle.** Slight background shift or border darken; not a wholesale color change.
289
+ - **Active states should feel tactile.** A 1–2px translate-y or a slight color darkening when pressed.
290
+ - **Disabled states must be obviously inert** — reduced opacity *and* no hover response.
291
+ - Design all four states (default, hover, focus, active) plus disabled for every interactive element. Missing states feel cheap.
292
+
293
+ ### Iconography
294
+
295
+ - **One icon set.** Lucide, Phosphor, Heroicons — pick one and stay there. Mixing icon styles is jarring even when individuals look fine.
296
+ - **Consistent stroke weight** across the set. Most modern icon libraries get this right by default; don't break it by importing a stray icon from somewhere else.
297
+ - **Icons paired with text labels** for primary actions. Icons-only is fine for tertiary actions where space is tight and meaning is obvious (close, copy, expand, settings).
298
+ - **Same icon, same meaning** throughout the app. Don't reuse a checkmark for both "saved" and "selected."
299
+
300
+ ### Motion & feedback
301
+
302
+ - **Animate state changes briefly** — 100–200ms with ease-out. Cross-fades between turns, slide-in for newly added notes.
303
+ - **Never animate purely for delight.** Each animation should communicate something — what changed, what's loading, what got selected.
304
+ - **Skeletons over spinners** for content loading. Spinners imply "wait"; skeletons imply "this is the shape of what's coming."
305
+ - **Immediate UI feedback** for in-row actions. A toggle should look "on" the instant the annotator clicks it; don't wait on any async work before showing the change.
306
+ - **Submission confirmation lives in the container**, not the app. The app's job ends at `onComplete` — don't render your own "saved!" toast.
307
+ - **Respect `prefers-reduced-motion`.** Cut transitions to instant for users who've opted out.
308
+
309
+ ### Keyboard-first
310
+
311
+ Annotators move fast. Mouse-only flows cap throughput at maybe 1 label per 30 seconds; keyboard-driven flows hit 1 per 5 seconds.
312
+
313
+ - **Number keys** for quick choices (1/2/3 for pass/fail/skip; 1/2 for left/right in side-by-side).
314
+ - **j / k** for next / previous (or arrow keys).
315
+ - **Enter** to submit, **Esc** to clear or back out.
316
+ - Show the shortcut next to each interactive element on first encounter; fade after the annotator demonstrates fluency.
317
+ - Provide a `?` overlay listing every shortcut. Don't hide them in docs.
318
+
319
+ ### Density without clutter
320
+
321
+ - Group related information close. Don't pad everything generously; use whitespace where it helps parse structure, not as decoration.
322
+ - A long trace shouldn't force scrolling past chrome to see content. Sticky or fixed elements should be earned.
323
+ - Truncate aggressively in summaries; expand on demand.
324
+
325
+ ### Responsive without afterthought
326
+
327
+ - Wide screens get side-by-side or multi-column layouts; narrow screens stack. Same component, different breakpoint.
328
+ - Text columns shouldn't span the full width on a 27" display — cap line length around 70–90 characters for prose.
329
+ - Buttons and tap targets stay reachable on smaller laptops and tablets.
330
+
331
+ ### State within the row
332
+
333
+ The container shows queue position and submission status — don't duplicate them in the app. What the app *should* communicate clearly is the state of the current row:
334
+
335
+ - **What's selected.** Active choices look obviously different from inactive ones — filled vs outlined, accent color vs neutral.
336
+ - **What's answered vs unanswered.** For multi-question forms, the difference between a deliberate "false" and "not answered yet" must be visible. A tristate (yes / no / unanswered) is fine; an undifferentiated toggle that defaults to "no" is not.
337
+ - **What's a draft.** If the annotator typed something but hasn't submitted, that text should look distinct from saved or pre-filled content (dashed border, subtle background, or a "draft" tag).
338
+ - **What just changed.** Animate the immediate consequence of an action briefly (100–200ms, ease-out). When a choice is selected, when an option appears, when validation flips. Never longer than 200ms — the annotator's next action shouldn't wait on motion.
339
+ - **What's required to submit.** If `onComplete` will be rejected (missing fields), make that obvious *before* submit — disable the action with an inline reason, or highlight the missing fields. Don't surface a server error after the fact.
340
+
341
+ ### Avoid dead controls
342
+
343
+ - If a Likert slider rarely moves off the middle, you have a Likert problem. Replace with binary toggles.
344
+ - Free-text fields that nobody fills in should be removed or made obviously optional.
345
+ - Buttons that are usually disabled either need a clearer condition or shouldn't be there.
346
+
347
+ ### Accessibility is throughput, not charity
348
+
349
+ - Every interactive element reachable by keyboard, with visible focus.
350
+ - Form controls have associated labels.
351
+ - Color is never the only signal — state is also indicated by icon, text, or position.
352
+ - Sufficient contrast for people labeling at 11pm with tired eyes (≥ 4.5:1 for body text, ≥ 3:1 for UI elements).
353
+
354
+ ### What to leave out
355
+
356
+ - Tooltips that explain things the UI should already make obvious.
357
+ - Confirmation dialogs for reversible actions.
358
+ - Anything that duplicates container chrome — queue position, "saved" toasts, navigation arrows between rows.
359
+ - Decorative illustrations, mascots, or empty-state cartoons. Functional UIs don't need them.
360
+ - Multiple typefaces beyond your sans + mono pair. Don't add a "display" face for headings.
361
+ - Gradient text, glassmorphism, neon glows, animated backgrounds. They date instantly and add nothing.
362
+
363
+ ---
364
+
365
+ ## Common patterns by use case
366
+
367
+ Reach for these as starting points; adapt to your data.
368
+
369
+ ### Agent trace exploration
370
+
371
+ The annotator needs to understand what the agent did, in order, before judging it.
372
+
373
+ - **Vertical timeline of turns/steps**, top-to-bottom, scrollable.
374
+ - Each turn collapsible; default state depends on length (short turns expanded, long turns collapsed with a one-line summary).
375
+ - **Role-coded by subtle left border or label**, not heavy colored backgrounds. User / Assistant / Tool / System are distinct enough with a 2px border + a small label.
376
+ - Tool calls and JSON in **monospace** with syntax-aware coloring; prose in proportional type.
377
+ - **Inline copy buttons** on tool args, IDs, and full turns.
378
+ - **Keyboard navigation:** `j` / `k` to jump between turns; `space` to expand/collapse the focused turn; `e` to expand all, `c` to collapse all.
379
+ - Per-turn metadata (latency, tokens) in a muted footer, not in the main body.
380
+ - If the annotator is judging a specific failure, **anchor the view** to the turn where it happens; don't make them scroll to find it.
381
+
382
+ ### Side-by-side comparison
383
+
384
+ Two outputs from different prompts/models, annotator picks one (or ties).
385
+
386
+ - Two columns of equal width on wide screens; stacked on narrow.
387
+ - **Synced scrolling** — when the annotator scrolls one side, the other follows.
388
+ - **Hide identity until decision** (don't label which is "Model A" or "current prod"; reveal after the choice is locked). Removes anchoring bias.
389
+ - Three keys: `1` left wins, `2` right wins, `3` tie / both bad / neither.
390
+ - Optional **"why?" textarea** below the choice, only for ambiguous cases — don't force it on every label.
391
+ - For long outputs, **pin a one-line summary** of each side at the top; let the body scroll.
392
+ - **Diff highlighting** only when the comparison is small variation (same response, edited). For genuinely different responses, diff highlighting is noise.
393
+
394
+ ### Text annotation (highlights + comments)
395
+
396
+ Annotator marks regions of a passage and attaches notes.
397
+
398
+ - Selected text gets a **subtle highlight** (low-opacity yellow or accent), not a heavy block.
399
+ - **Margin column on the right** with notes anchored to highlights on desktop; stack notes inline on narrow screens.
400
+ - **Hover-to-add** on selection; click-to-edit on existing notes.
401
+ - Distinct visual states for **draft vs. saved** notes (e.g. dashed outline vs. solid).
402
+ - Alternative panel: a **flat list of all notes** for quick triage / bulk-edit.
403
+ - **Keyboard:** `n` adds a note to the current selection; `↑` / `↓` cycles through notes; `enter` opens the focused note for edit.
404
+
405
+ ### Multi-question binary form (most common labeler shape)
406
+
407
+ A trace + several pass/fail questions about it.
408
+
409
+ - Stack of toggles, **one question per row**, each a clear yes/no/unanswered.
410
+ - Numeric keys (`1`–`9`) **focus and toggle** the corresponding question.
411
+ - Per-question **optional notes**, hidden by default; expand on demand (don't show 8 empty textareas).
412
+ - **Per-row progress hint** ("3 of 5 answered" within this row's questions) is fine — this is in-row state, distinct from the container's queue position.
413
+ - Footer **submit** calls `onComplete` with the structured response; shortcut `enter`.
414
+ - `s` triggers an `onComplete` payload that marks the row unlabelable (e.g. `{ "skipped": true }`) — the container handles advancing.
415
+
416
+ ### Free-form feedback (open-coding / error analysis)
417
+
418
+ Used early in the loop, before failure modes are formalized.
419
+
420
+ - **Hide rating widgets entirely.** This phase is qualitative.
421
+ - A single markdown-capable textarea per row.
422
+ - Below the textarea, optional **preset failure-mode tags** the annotator can attach — but never required.
423
+ - Show **previously-used tags** as quick-add chips so categorization emerges naturally.
424
+ - Don't force a category — trust the annotator to write.
425
+
426
+ ### Multi-step agent step labeling
427
+
428
+ Annotator labels which step of a multi-step agent failed.
429
+
430
+ - **Timeline of steps** with state transitions visible (Plan → Search → Code → Finalize).
431
+ - Click a step to drill into a **side panel** with prompt, response, tool I/O.
432
+ - Per-step pass/fail toggle.
433
+ - **Highlight the first failed step** automatically (most upstream failures matter most; downstream errors are often consequences).
434
+ - Optional radio: **"this is where it went wrong"** (single-choice across steps) for cases where multiple steps failed but one is the root cause.
435
+
436
+ ### Ranking
437
+
438
+ Annotator orders N candidates by quality.
439
+
440
+ - Drag-and-drop with keyboard equivalent (focus item, `↑` / `↓` to move).
441
+ - Numeric badges that update live as order changes.
442
+ - For N > 5, prefer **pairwise comparisons** over full ranking — annotators are unreliable at ordering long lists, and pairwise data composes via Bradley-Terry.
443
+
444
+ ### Tool/function call audit
445
+
446
+ Annotator verifies an agent called the right tool with valid args.
447
+
448
+ - **Tool call** rendered as a structured card: name, args (monospace, expandable for big payloads), result.
449
+ - Three quick verdicts: `1` correct tool + correct args, `2` correct tool wrong args, `3` wrong tool.
450
+ - Optional textarea: "what would have been right?"
451
+ - For multi-tool sequences, show as a numbered list with verdicts per call.
452
+
453
+ ---
454
+
455
+ ## Imports
456
+
457
+ The complete set of allowed imports. Paths and exports are auto-generated from the runtime registry — every entry below resolves; nothing else does.
458
+
459
+ <!-- BEGIN ORIZU_AUTO_IMPORT_MAP -->
460
+ ```tsx
461
+ // React hooks
462
+ import { useState, useRef, useEffect } from 'react';
463
+
464
+ // UI primitives (shadcn)
465
+ import { Button } from '@/components/ui/button';
466
+ import { Card } from '@/components/ui/card';
467
+ import { Carousel } from '@/components/ui/carousel';
468
+ import { Checkbox } from '@/components/ui/checkbox';
469
+ import { Form } from '@/components/ui/form';
470
+ import { Input } from '@/components/ui/input';
471
+ import { Label } from '@/components/ui/label';
472
+ import { Progress } from '@/components/ui/progress';
473
+ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
474
+ import { ScrollArea } from '@/components/ui/scroll-area';
475
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
476
+ import { Separator } from '@/components/ui/separator';
477
+ import { Switch } from '@/components/ui/switch';
478
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, TableCaption, TableFooter } from '@/components/ui/table';
479
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
480
+ import { Textarea } from '@/components/ui/textarea';
481
+ import { Toggle } from '@/components/ui/toggle';
482
+ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
483
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
484
+
485
+ // Content
486
+ import { TextContent } from '@/components/base/content/TextContent';
487
+ import { CodeBlock } from '@/components/base/content/CodeBlock';
488
+ import { AssistantMessageBlock, ContextMessageBlock, ConversationMessageBlock, ConversationView, ReasoningMessageBlock, SystemMessageBlock, ToolCallBlock, ToolResultBlock, UserMessageBlock } from '@/components/base/content/ConversationView';
489
+ import { ContentRenderer } from '@/components/base/content/ContentRenderer';
490
+ import { Prose } from '@/components/base/content/Prose';
491
+
492
+ // Behaviors
493
+ import { Annotatable } from '@/components/base/behaviors/Annotatable';
494
+ import { Reactable } from '@/components/base/behaviors/Reactable';
495
+
496
+ // Input
497
+ import { CommentBox } from '@/components/base/input/CommentBox';
498
+ import { CriterionRating } from '@/components/base/input/CriterionRating';
499
+ import { LikertScale } from '@/components/base/input/LikertScale';
500
+ import { NumericRating } from '@/components/base/input/NumericRating';
501
+ import { RatingSelector } from '@/components/base/input/RatingSelector';
502
+ import { StarRating } from '@/components/base/input/StarRating';
503
+ import { TagPicker } from '@/components/base/input/TagPicker';
504
+ import { ThumbsRating } from '@/components/base/input/ThumbsRating';
505
+
506
+ // Base UI
507
+ import { ComparisonPanel } from '@/components/base/ui/ComparisonPanel';
508
+ import { DraggableItem } from '@/components/base/ui/DraggableItem';
509
+
510
+ // Templates
511
+ import { TagSelector } from '@/components/templates/classification/TagSelector';
512
+ import { SideBySideComparison } from '@/components/templates/comparison/SideBySideComparison';
513
+ import { CorrectionTask } from '@/components/templates/correction/CorrectionTask';
514
+ import { CodeComparison } from '@/components/templates/code/CodeComparison';
515
+ import { ContextualQA } from '@/components/templates/qa/ContextualQA';
516
+ import { RankingList } from '@/components/templates/ranking/RankingList';
517
+ import { SingleItemRater } from '@/components/templates/rating/SingleItemRater';
518
+ ```
519
+ <!-- END ORIZU_AUTO_IMPORT_MAP -->
520
+
521
+ Use **named imports** for everything. Some registry entries support default imports for backwards compatibility, but named imports are the documented, portable form.
522
+
523
+ ---
524
+
525
+ ## Component reference
526
+
527
+ Each component below lists its source URL, import path, props, and a minimal usage example. Components flagged with a "**Source:**" link expose the underlying TSX at a stable URL — fetch that URL if you need to inline a private variant.
528
+
529
+ <!-- BEGIN ORIZU_AUTO_COMPONENT_REFERENCE -->
530
+ ### Typography
531
+
532
+ Anything the coding agent writes themselves — instructions, headings, body copy. Lives outside the under-review box.
533
+
534
+ #### Prose {#prose}
535
+
536
+ Typography for anything the coding agent writes themselves — instructions, headings, body copy. Use this outside the under-review box; use TextContent inside it.
537
+
538
+ **Source:** [components/base/content/Prose.tsx](https://orizu.ai/docs/components/Prose/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
539
+
540
+ **Import:** `import { Prose } from "@/components/base/content/Prose"`
541
+
542
+ **Block primitives:**
543
+ - `Prose.Eyebrow` — Small mono uppercase label. Sits above an H1/H2.
544
+ - `Prose.H1 / H2 / H3` — Headings. Tight tracking, sans by default.
545
+ - `Prose.Lead` — Single deck-line under a heading. Larger than Body, lighter weight.
546
+ - `Prose.Body` — Standard paragraph. 14px / 1.6 line-height.
547
+ - `Prose.Small` — Helper / footnote. 12.5px.
548
+ - `Prose.Caption` — Mono caption under a figure or table. 11.5px.
549
+ - `Prose.Code` — Inline mono. Use inside a sentence, not as a block.
550
+ - `Prose.Link` — Documentary blue, underline preserved on hover.
551
+
552
+ **Props:**
553
+ - `children` * — `ReactNode` — <Prose> applies the type ramp to its children. Use the block primitives directly when you don’t want a wrapper.
554
+ - `className` — `string`
555
+
556
+ **Minimal usage:**
557
+
558
+ ```jsx
559
+ <Prose
560
+ children={<span />}
561
+ />
562
+ ```
563
+
564
+ ### Content
565
+
566
+ The under-review primitives. What the model produced — text, code, conversation, media — framed inside a box. Wrap in behaviors to make reviewable.
567
+
568
+ #### TextContent {#textcontent}
569
+
570
+ The in-a-box content-under-review primitive. Plain text inside a frame. For prose the agent writes themselves, reach for Prose instead.
571
+
572
+ **Source:** [components/base/content/TextContent.tsx](https://orizu.ai/docs/components/TextContent/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
573
+
574
+ **Import:** `import { TextContent } from "@/components/base/content/TextContent"`
575
+
576
+ **Props:**
577
+ - `content` * — `string` — The text content to display.
578
+ - `maxHeight` — `string` (default: `"auto"`) — When set, content scrolls within this height.
579
+ - `isMonospace` — `boolean` (default: `false`) — Render in mono. Use for raw payloads, IDs, model output.
580
+ - `preformatted` — `boolean` (default: `false`) — Preserve whitespace (whitespace-pre-wrap).
581
+ - `variant` — `"default" | "mono" | "scrollable"` (default: `"default"`)
582
+ - `className` — `string` — Extra classes.
583
+
584
+ **Minimal usage:**
585
+
586
+ ```jsx
587
+ <TextContent
588
+ content={"..."}
589
+ />
590
+ ```
591
+
592
+ #### CodeBlock {#codeblock}
593
+
594
+ Numbered, scrollable code with optional line highlights and click handlers — the substrate for line-by-line review.
595
+
596
+ **Source:** [components/base/content/CodeBlock.tsx](https://orizu.ai/docs/components/CodeBlock/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
597
+
598
+ **Import:** `import { CodeBlock } from "@/components/base/content/CodeBlock"`
599
+
600
+ **Props:**
601
+ - `code` * — `string | CodeLine[]` — The code to display.
602
+ - `maxHeight` — `string` (default: `"400px"`) — Container height.
603
+ - `showLineNumbers` — `boolean` (default: `true`)
604
+ - `highlightedLines` — `number[]` (default: `[]`) — 1-indexed line numbers to highlight.
605
+ - `highlightClass` — `string` — Class applied to highlighted lines.
606
+ - `onLineClick` — `(n: number) => void` — Fires when a line is clicked.
607
+ - `onLineMouseDown` — `(n: number) => void` — For drag-select line ranges.
608
+ - `onLineMouseMove` — `(n: number) => void`
609
+ - `filename` — `string` — Optional chrome-strip filename.
610
+ - `language` — `string` — Optional chrome-strip language tag.
611
+
612
+ **Minimal usage:**
613
+
614
+ ```jsx
615
+ <CodeBlock
616
+ code={[]}
617
+ />
618
+ ```
619
+
620
+ #### SystemMessageBlock {#systemmessageblock}
621
+
622
+ Standalone system instruction renderer. Use it at the top of custom conversation layouts to show the governing prompt or policy context without taking the full transcript template. Wrap directly with Reactable or Annotatable for review.
623
+
624
+ **Source:** [components/base/content/ConversationBlocks.tsx](https://orizu.ai/docs/components/SystemMessageBlock/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
625
+
626
+ **Import:** `import { SystemMessageBlock } from "@/components/base/content/ConversationView"`
627
+
628
+ **Props:**
629
+ - `message` * — `Message & { role: "system" }` — A single system instruction message.
630
+ - `density` — `"comfortable" | "compact"` (default: `"comfortable"`)
631
+ - `showTimestamp` — `boolean` (default: `true`)
632
+ - `hideAvatar` — `boolean` (default: `false`)
633
+ - `className` — `string`
634
+
635
+ **Minimal usage:**
636
+
637
+ ```jsx
638
+ <SystemMessageBlock
639
+ message={{ id: "1", role: "system", content: "..." }}
640
+ />
641
+ ```
642
+
643
+ #### UserMessageBlock {#usermessageblock}
644
+
645
+ Standalone user turn renderer from the conversation system. Use it when an agent surface needs to place the user message outside the full transcript template.
646
+
647
+ **Source:** [components/base/content/ConversationBlocks.tsx](https://orizu.ai/docs/components/UserMessageBlock/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
648
+
649
+ **Import:** `import { UserMessageBlock } from "@/components/base/content/ConversationView"`
650
+
651
+ **Props:**
652
+ - `message` * — `Message & { role: "user" }` — A single user message.
653
+ - `density` — `"comfortable" | "compact"` (default: `"comfortable"`)
654
+ - `showTimestamp` — `boolean` (default: `true`)
655
+ - `hideAvatar` — `boolean` (default: `false`)
656
+ - `className` — `string`
657
+
658
+ **Minimal usage:**
659
+
660
+ ```jsx
661
+ <UserMessageBlock
662
+ message={{ id: "1", role: "user", content: "..." }}
663
+ />
664
+ ```
665
+
666
+ #### AssistantMessageBlock {#assistantmessageblock}
667
+
668
+ Standalone assistant turn renderer. Compose it with reactions, annotations, generated previews, or custom agent layouts without taking the full ConversationView.
669
+
670
+ **Source:** [components/base/content/ConversationBlocks.tsx](https://orizu.ai/docs/components/AssistantMessageBlock/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
671
+
672
+ **Import:** `import { AssistantMessageBlock } from "@/components/base/content/ConversationView"`
673
+
674
+ **Props:**
675
+ - `message` * — `Message & { role: "assistant" }` — A single assistant message.
676
+ - `density` — `"comfortable" | "compact"` (default: `"comfortable"`)
677
+ - `showTimestamp` — `boolean` (default: `true`)
678
+ - `hideAvatar` — `boolean` (default: `false`)
679
+ - `className` — `string`
680
+
681
+ **Minimal usage:**
682
+
683
+ ```jsx
684
+ <AssistantMessageBlock
685
+ message={{ id: "1", role: "assistant", content: "..." }}
686
+ />
687
+ ```
688
+
689
+ #### ReasoningMessageBlock {#reasoningmessageblock}
690
+
691
+ Standalone reasoning trace renderer. It keeps the existing default-collapsed details shell while letting agent UIs position reasoning independently. Wrap the block directly when reviewers need to react to or annotate the trace.
692
+
693
+ **Source:** [components/base/content/ConversationBlocks.tsx](https://orizu.ai/docs/components/ReasoningMessageBlock/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
694
+
695
+ **Import:** `import { ReasoningMessageBlock } from "@/components/base/content/ConversationView"`
696
+
697
+ **Props:**
698
+ - `message` * — `Message & { role: "reasoning" }` — A single reasoning trace. defaultOpen controls the details shell.
699
+ - `density` — `"comfortable" | "compact"` (default: `"comfortable"`)
700
+ - `showTimestamp` — `boolean` (default: `true`)
701
+ - `hideAvatar` — `boolean` (default: `false`)
702
+ - `className` — `string`
703
+
704
+ **Minimal usage:**
705
+
706
+ ```jsx
707
+ <ReasoningMessageBlock
708
+ message={{ id: "1", role: "reasoning", content: "..." }}
709
+ />
710
+ ```
711
+
712
+ #### ToolCallBlock {#toolcallblock}
713
+
714
+ Standalone tool-call renderer. It renders the tool name, optional badge, and collapsible args payload for trace panels and custom agent timelines. Wrap directly with Reactable or Annotatable before execution review.
715
+
716
+ **Source:** [components/base/content/ConversationBlocks.tsx](https://orizu.ai/docs/components/ToolCallBlock/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
717
+
718
+ **Import:** `import { ToolCallBlock } from "@/components/base/content/ConversationView"`
719
+
720
+ **Props:**
721
+ - `message` * — `Message & { role: "tool_call" }` — A single tool call. args is rendered before content when present.
722
+ - `density` — `"comfortable" | "compact"` (default: `"comfortable"`)
723
+ - `showTimestamp` — `boolean` (default: `true`)
724
+ - `hideAvatar` — `boolean` (default: `false`)
725
+ - `className` — `string`
726
+
727
+ **Minimal usage:**
728
+
729
+ ```jsx
730
+ <ToolCallBlock
731
+ message={{ id: "1", role: "tool_call", content: "..." }}
732
+ />
733
+ ```
734
+
735
+ #### ToolResultBlock {#toolresultblock}
736
+
737
+ Standalone tool-result renderer. It preserves the paired-result visual treatment while allowing callers to group, filter, react to, or annotate results themselves.
738
+
739
+ **Source:** [components/base/content/ConversationBlocks.tsx](https://orizu.ai/docs/components/ToolResultBlock/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
740
+
741
+ **Import:** `import { ToolResultBlock } from "@/components/base/content/ConversationView"`
742
+
743
+ **Props:**
744
+ - `message` * — `Message & { role: "tool_result" }` — A single tool result. result is rendered before content when present.
745
+ - `density` — `"comfortable" | "compact"` (default: `"comfortable"`)
746
+ - `showTimestamp` — `boolean` (default: `true`)
747
+ - `hideAvatar` — `boolean` (default: `false`)
748
+ - `className` — `string`
749
+
750
+ **Minimal usage:**
751
+
752
+ ```jsx
753
+ <ToolResultBlock
754
+ message={{ id: "1", role: "tool_result", content: "..." }}
755
+ />
756
+ ```
757
+
758
+ #### ConversationView {#conversationview}
759
+
760
+ Composed agentic transcript template built from SystemMessageBlock, UserMessageBlock, AssistantMessageBlock, ReasoningMessageBlock, ToolCallBlock, ToolResultBlock, and companion context blocks.
761
+
762
+ **Source:** [components/base/content/ConversationView.tsx](https://orizu.ai/docs/components/ConversationView/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
763
+
764
+ **Import:** `import { ConversationView } from "@/components/base/content/ConversationView"`
765
+
766
+ **Block primitives:**
767
+ - `SystemMessageBlock` — Top-of-transcript system instruction bubble for governing prompt or policy context.
768
+ - `UserMessageBlock` — Right-aligned user message bubble with avatar, label, and optional timestamp.
769
+ - `AssistantMessageBlock` — Assistant message bubble for direct composition with feedback and generated-output surfaces.
770
+ - `ReasoningMessageBlock` — Default-collapsed reasoning shell that can be mounted independently.
771
+ - `ToolCallBlock` — Collapsible tool-call payload with tool name and optional badge.
772
+ - `ToolResultBlock` — Collapsible tool-result payload with the paired result styling.
773
+
774
+ **Props:**
775
+ - `messages` * — `Message[]` — Each turn: { id, role, content, tool?, args?, result?, source?, timestamp?, duration?, avatar?, label?, badge?, defaultOpen? }. timestamp is an optional display string rendered verbatim. Roles: 'user' | 'assistant' | 'system' | 'reasoning' | 'tool_call' | 'tool_result' | 'context'.
776
+ - `density` — `"comfortable" | "compact"` (default: `"comfortable"`) — Compact halves the gap and tightens padding — use for long agentic traces.
777
+ - `showTimestamps` — `boolean` (default: `true`)
778
+ - `maxHeight` — `string` (default: `"300px"`)
779
+ - `scrollToBottom` — `boolean` (default: `true`)
780
+
781
+ **Minimal usage:**
782
+
783
+ ```jsx
784
+ <ConversationView
785
+ messages={[]}
786
+ />
787
+ ```
788
+
789
+ #### ContentRenderer {#contentrenderer}
790
+
791
+ For media (images, videos) and custom content types only. Don't use it for text — TextContent / CodeBlock / ConversationView do that better.
792
+
793
+ **Source:** [components/base/content/ContentRenderer.tsx](https://orizu.ai/docs/components/ContentRenderer/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
794
+
795
+ **Import:** `import { ContentRenderer } from "@/components/base/content/ContentRenderer"`
796
+
797
+ **Props:**
798
+ - `contentType` * — `"image" | "video" | "custom"`
799
+ - `content` * — `string | unknown` — URL for image/video, anything for custom.
800
+ - `maxHeight` — `string` (default: `"400px"`)
801
+ - `customRenderer` — `({ content }) => ReactNode` — Required when contentType="custom".
802
+
803
+ **Minimal usage:**
804
+
805
+ ```jsx
806
+ <ContentRenderer
807
+ contentType={"image"}
808
+ content={"..."}
809
+ />
810
+ ```
811
+
812
+ ### Behaviors
813
+
814
+ Wrap any content component to make it interactive. Behaviors don’t render content themselves — they add affordances and slot in whatever surface you compose.
815
+
816
+ #### Annotatable {#annotatable}
817
+
818
+ Wraps any child. Selection inside produces a span; renderAnnotation({span, save, cancel}) slot populates an anchored popover. The annotation surface is whatever you compose — CommentBox, TagPicker, RatingSelector, etc. Wraps TextContent, a ConversationView turn, or a CodeBlock the same way.
819
+
820
+ **Source:** [components/base/behaviors/Annotatable.tsx](https://orizu.ai/docs/components/Annotatable/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
821
+
822
+ **Import:** `import { Annotatable } from "@/components/base/behaviors/Annotatable"`
823
+
824
+ **Props:**
825
+ - `children` * — `ReactNode` — The component to make annotatable. Selection is captured inside its DOM subtree.
826
+ - `target` — `string` — Identifier for what is annotated (e.g. "msg:42").
827
+ - `annotations` — `AnnotationRecord<TData>[]` (default: `[]`) — Persisted annotations to render as highlights.
828
+ - `renderAnnotation` — `({ span, save, cancel }) => ReactNode` — Slot. Render the annotation surface; call save(payload) to commit, cancel() to discard.
829
+ - `onAnnotationCreate` — `({ target, span, data }) => void`
830
+ - `hideRail` — `boolean` (default: `false`) — Hide the inset guide rail.
831
+
832
+ **Minimal usage:**
833
+
834
+ ```jsx
835
+ <Annotatable
836
+ children={<span />}
837
+ />
838
+ ```
839
+
840
+ #### Reactable {#reactable}
841
+
842
+ Wraps any child. Adds an on-hover affordance rail (thumbs, flag, configurable). Optional renderForm captures context inline when a reaction is active.
843
+
844
+ **Source:** [components/base/behaviors/Reactable.tsx](https://orizu.ai/docs/components/Reactable/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
845
+
846
+ **Import:** `import { Reactable } from "@/components/base/behaviors/Reactable"`
847
+
848
+ **Props:**
849
+ - `children` * — `ReactNode`
850
+ - `target` — `string` — Identifier for what is reacted to.
851
+ - `types` — `("thumbs" | "flag")[]` (default: `["thumbs", "flag"]`) — The affordances on the rail.
852
+ - `value` — `ReactableValue | null` — The active reaction, if any.
853
+ - `renderForm` — `({ type, save, cancel }) => ReactNode` — Inline form opened when a reaction is selected. Slot in CommentBox, TagPicker, etc.
854
+ - `onReact` — `({ target, type, context }) => void`
855
+ - `onClear` — `(target?) => void`
856
+
857
+ **Minimal usage:**
858
+
859
+ ```jsx
860
+ <Reactable
861
+ children={<span />}
862
+ />
863
+ ```
864
+
865
+ ### Input
866
+
867
+ Capture feedback. Rating selectors, comments, criterion ratings, tags. These are the surfaces you slot inside a behavior.
868
+
869
+ #### RatingSelector {#ratingselector}
870
+
871
+ One component, five rating mechanisms. Switch via the ratingType prop. Use this when you want the rating type to be configurable per task.
872
+
873
+ **Source:** [components/base/input/RatingSelector.tsx](https://orizu.ai/docs/components/RatingSelector/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
874
+
875
+ **Import:** `import { RatingSelector } from "@/components/base/input/RatingSelector"`
876
+
877
+ **Props:**
878
+ - `ratingType` * — `"numeric" | "likert" | "thumbs" | "stars" | "slider"`
879
+ - `value` * — `number | string | null`
880
+ - `onChange` * — `(value) => void`
881
+ - `minRating` — `number` (default: `1`)
882
+ - `maxRating` — `number` (default: `5`)
883
+ - `likertLabels` — `string[]` (default: `["Strongly Disagree", … "Strongly Agree"]`)
884
+
885
+ **Minimal usage:**
886
+
887
+ ```jsx
888
+ <RatingSelector
889
+ ratingType={"numeric"}
890
+ value={0}
891
+ onChange={() => {}}
892
+ />
893
+ ```
894
+
895
+ #### StarRating {#starrating}
896
+
897
+ Hover-previewed star rating with configurable scale and active color.
898
+
899
+ **Source:** [components/base/input/StarRating.tsx](https://orizu.ai/docs/components/StarRating/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
900
+
901
+ **Import:** `import { StarRating } from "@/components/base/input/StarRating"`
902
+
903
+ **Props:**
904
+ - `value` * — `number`
905
+ - `maxRating` — `number` (default: `5`)
906
+ - `minRating` — `number` (default: `1`)
907
+ - `readOnly` — `boolean` (default: `false`)
908
+ - `size` — `string | number` (default: `"1.5rem"`)
909
+ - `activeColor` — `string` (default: `"text-primary"`)
910
+ - `inactiveColor` — `string` (default: `"text-border"`)
911
+ - `onChange` — `(rating: number) => void`
912
+
913
+ **Minimal usage:**
914
+
915
+ ```jsx
916
+ <StarRating
917
+ value={0}
918
+ />
919
+ ```
920
+
921
+ #### NumericRating {#numericrating}
922
+
923
+ Row of numbered buttons. The most legible scale at a glance — pick this when accuracy matters more than speed.
924
+
925
+ **Source:** [components/base/input/NumericRating.tsx](https://orizu.ai/docs/components/NumericRating/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
926
+
927
+ **Import:** `import { NumericRating } from "@/components/base/input/NumericRating"`
928
+
929
+ **Props:**
930
+ - `value` * — `number | null`
931
+ - `minRating` — `number` (default: `1`)
932
+ - `maxRating` — `number` (default: `5`)
933
+ - `size` — `"sm" | "md" | "lg"` (default: `"md"`)
934
+ - `readOnly` — `boolean` (default: `false`)
935
+ - `onChange` — `(rating: number) => void`
936
+
937
+ **Minimal usage:**
938
+
939
+ ```jsx
940
+ <NumericRating
941
+ value={0}
942
+ />
943
+ ```
944
+
945
+ #### ThumbsRating {#thumbsrating}
946
+
947
+ Binary feedback. Three visual styles (filled / outline / icon) and an optional minimal mode for compact UIs.
948
+
949
+ **Source:** [components/base/input/ThumbsRating.tsx](https://orizu.ai/docs/components/ThumbsRating/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
950
+
951
+ **Import:** `import { ThumbsRating } from "@/components/base/input/ThumbsRating"`
952
+
953
+ **Props:**
954
+ - `value` * — `"up" | "down" | null`
955
+ - `onChange` — `(value: "up" | "down") => void`
956
+ - `size` — `"sm" | "md" | "lg"` (default: `"md"`)
957
+ - `selectedStyle` — `"filled" | "outline" | "icon"` (default: `"filled"`)
958
+ - `minimal` — `boolean` (default: `false`) — No button background, just colored icons.
959
+ - `showLabels` — `boolean` (default: `false`)
960
+ - `upText` — `string` (default: `"Like"`)
961
+ - `downText` — `string` (default: `"Dislike"`)
962
+ - `readOnly` — `boolean` (default: `false`)
963
+
964
+ **Minimal usage:**
965
+
966
+ ```jsx
967
+ <ThumbsRating
968
+ value={"up"}
969
+ />
970
+ ```
971
+
972
+ #### LikertScale {#likertscale}
973
+
974
+ Ordered radio scale with custom labels. Horizontal by default; verticalize for long labels.
975
+
976
+ **Source:** [components/base/input/LikertScale.tsx](https://orizu.ai/docs/components/LikertScale/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
977
+
978
+ **Import:** `import { LikertScale } from "@/components/base/input/LikertScale"`
979
+
980
+ **Props:**
981
+ - `value` * — `string | null`
982
+ - `labels` * — `string[]`
983
+ - `horizontal` — `boolean` (default: `true`)
984
+ - `readOnly` — `boolean` (default: `false`)
985
+ - `onChange` — `(value: string) => void`
986
+
987
+ **Minimal usage:**
988
+
989
+ ```jsx
990
+ <LikertScale
991
+ value={"..."}
992
+ labels={[]}
993
+ />
994
+ ```
995
+
996
+ #### CriterionRating {#criterionrating}
997
+
998
+ A single bordered criterion (label + optional collapsible description) with a horizontal radio group. Stack several inside a comparison panel.
999
+
1000
+ **Source:** [components/base/input/CriterionRating.tsx](https://orizu.ai/docs/components/CriterionRating/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
1001
+
1002
+ **Import:** `import { CriterionRating } from "@/components/base/input/CriterionRating"`
1003
+
1004
+ **Props:**
1005
+ - `id` * — `string`
1006
+ - `label` * — `string`
1007
+ - `description` — `string` — Shown when expanded; collapses behind a chevron.
1008
+ - `value` * — `string | null`
1009
+ - `options` * — `{ value, label }[]` — Typically [{value:"left"}, {value:"right"}, {value:"tie"}].
1010
+ - `initiallyExpanded` — `boolean` (default: `false`)
1011
+ - `readOnly` — `boolean` (default: `false`)
1012
+ - `onChange` — `(id, value) => void`
1013
+
1014
+ **Minimal usage:**
1015
+
1016
+ ```jsx
1017
+ <CriterionRating
1018
+ id={"..."}
1019
+ label={"..."}
1020
+ value={"..."}
1021
+ options={[]}
1022
+ />
1023
+ ```
1024
+
1025
+ #### CommentBox {#commentbox}
1026
+
1027
+ Multiline text input with label, instructions, validation (min/max length), and optional character count.
1028
+
1029
+ **Source:** [components/base/input/CommentBox.tsx](https://orizu.ai/docs/components/CommentBox/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
1030
+
1031
+ **Import:** `import { CommentBox } from "@/components/base/input/CommentBox"`
1032
+
1033
+ **Props:**
1034
+ - `value` * — `string`
1035
+ - `onChange` — `(value: string) => void`
1036
+ - `label` — `string`
1037
+ - `instructions` — `string` — Helper paragraph under the label.
1038
+ - `placeholder` — `string` (default: `"Enter your comment here..."`)
1039
+ - `rows` — `number` (default: `3`)
1040
+ - `maxLength` — `number` (default: `0`) — 0 disables the cap.
1041
+ - `minLength` — `number` (default: `0`) — Validation triggers on blur.
1042
+ - `showCharCount` — `boolean` (default: `false`)
1043
+ - `error` — `string` — Shown after blur.
1044
+ - `readOnly` — `boolean` (default: `false`)
1045
+
1046
+ **Minimal usage:**
1047
+
1048
+ ```jsx
1049
+ <CommentBox
1050
+ value={"..."}
1051
+ />
1052
+ ```
1053
+
1054
+ #### TagPicker {#tagpicker}
1055
+
1056
+ Pill-shaped tag selector. Single or multi-select, optional category grouping, optional custom-tag input.
1057
+
1058
+ **Source:** [components/base/input/TagPicker.tsx](https://orizu.ai/docs/components/TagPicker/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
1059
+
1060
+ **Import:** `import { TagPicker } from "@/components/base/input/TagPicker"`
1061
+
1062
+ **Props:**
1063
+ - `availableTags` * — `Tag[]` — { id, label, category?, description?, color? }
1064
+ - `selectedTagIds` * — `string[]`
1065
+ - `multiSelect` — `boolean` (default: `true`)
1066
+ - `allowCustomTags` — `boolean` (default: `false`)
1067
+ - `groupByCategory` — `boolean` (default: `false`)
1068
+ - `customTagPlaceholder` — `string` (default: `"Add a custom tag..."`)
1069
+ - `readOnly` — `boolean` (default: `false`)
1070
+ - `onTagsChange` — `(ids: string[]) => void`
1071
+ - `onCustomTagAdd` — `(label: string) => void`
1072
+
1073
+ **Minimal usage:**
1074
+
1075
+ ```jsx
1076
+ <TagPicker
1077
+ availableTags={[]}
1078
+ selectedTagIds={[]}
1079
+ />
1080
+ ```
1081
+
1082
+ ### UI
1083
+
1084
+ Layout pieces used by templates.
1085
+
1086
+ #### ComparisonPanel {#comparisonpanel}
1087
+
1088
+ Card with a labeled header bar — the left/right wells of any side-by-side comparison.
1089
+
1090
+ **Source:** [components/base/ui/ComparisonPanel.tsx](https://orizu.ai/docs/components/ComparisonPanel/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
1091
+
1092
+ **Import:** `import { ComparisonPanel } from "@/components/base/ui/ComparisonPanel"`
1093
+
1094
+ **Props:**
1095
+ - `label` * — `string`
1096
+ - `children` * — `ReactNode`
1097
+ - `description` — `string`
1098
+ - `isSelected` — `boolean` (default: `false`) — Applies highlightClass.
1099
+ - `highlightClass` — `string` — Override selection class.
1100
+ - `badge` — `ReactNode` — Top-right pill (e.g. comment count).
1101
+ - `height` — `string` (default: `"auto"`)
1102
+ - `onClick` — `() => void`
1103
+
1104
+ **Minimal usage:**
1105
+
1106
+ ```jsx
1107
+ <ComparisonPanel
1108
+ label={"..."}
1109
+ children={<span />}
1110
+ />
1111
+ ```
1112
+
1113
+ #### DraggableItem {#draggableitem}
1114
+
1115
+ A row that supports both native drag-and-drop and explicit up/down buttons. The atom of a RankingList.
1116
+
1117
+ **Source:** [components/base/ui/DraggableItem.tsx](https://orizu.ai/docs/components/DraggableItem/source) — fetch this URL to read the implementation, or to copy it inline as a private fork if the registered props don't fit your task.
1118
+
1119
+ **Import:** `import { DraggableItem } from "@/components/base/ui/DraggableItem"`
1120
+
1121
+ **Props:**
1122
+ - `id` * — `string`
1123
+ - `children` * — `ReactNode`
1124
+ - `rank` — `number` (default: `0`)
1125
+ - `maxRank` — `number` (default: `0`)
1126
+ - `isDragging` — `boolean` (default: `false`)
1127
+ - `isDraggedOver` — `boolean` (default: `false`)
1128
+ - `showControls` — `boolean` (default: `true`)
1129
+ - `disableControls` — `boolean` (default: `false`)
1130
+ - `onDragStart` — `(id) => void`
1131
+ - `onDragOver` — `(id) => void`
1132
+ - `onDrop` — `(id) => void`
1133
+ - `onDragEnd` — `() => void`
1134
+ - `onMoveUp` — `(id) => void`
1135
+ - `onMoveDown` — `(id) => void`
1136
+
1137
+ **Minimal usage:**
1138
+
1139
+ ```jsx
1140
+ <DraggableItem
1141
+ id={"..."}
1142
+ children={<span />}
1143
+ />
1144
+ ```
1145
+
1146
+ ### Recipes
1147
+
1148
+ Worked component + behavior compositions. Read these before building — the mental model is pick a component, pick a behavior, fill the slot.
1149
+
1150
+ #### Composition recipes {#composition-recipes}
1151
+
1152
+ Worked behavior × component patterns. The mental model: pick an individual content block, pick a behavior, fill the slot.
1153
+
1154
+ **Recipe 1 — Flag a tool call before it executes**
1155
+ Why: Reactable wraps the tool_call turn; the rail offers ‘flag’; the form captures why before letting the agent run.
1156
+
1157
+ ```jsx
1158
+ const [flagReason, setFlagReason] = useState("")
1159
+
1160
+ <Reactable
1161
+ types={["flag"]}
1162
+ renderForm={({ save, cancel }) => (
1163
+ <div className="space-y-2">
1164
+ <CommentBox
1165
+ label="Why is this call wrong?"
1166
+ value={flagReason}
1167
+ onChange={setFlagReason}
1168
+ />
1169
+ <div className="flex gap-2">
1170
+ <Button onClick={() => save(flagReason)} disabled={!flagReason.trim()}>
1171
+ Save
1172
+ </Button>
1173
+ <Button variant="outline" onClick={cancel}>
1174
+ Cancel
1175
+ </Button>
1176
+ </div>
1177
+ </div>
1178
+ )}
1179
+ onReact={recordReaction}
1180
+ >
1181
+ <ToolCallBlock
1182
+ message={{ id: "1", role: "tool_call", tool: "send_email", content: "..." }}
1183
+ />
1184
+ </Reactable>
1185
+ ```
1186
+
1187
+ **Recipe 2 — Annotate a system instruction**
1188
+ Why: Annotatable wraps the standalone system block at the top of a custom conversation layout, so reviewers can mark the exact policy phrase that made the run too constrained.
1189
+
1190
+ ```jsx
1191
+ const [annotationText, setAnnotationText] = useState("")
1192
+
1193
+ <Annotatable
1194
+ annotations={existing}
1195
+ renderAnnotation={({ span, save, cancel }) => (
1196
+ <div className="space-y-2">
1197
+ <CommentBox
1198
+ label={`On "${span.text}"`}
1199
+ value={annotationText}
1200
+ onChange={setAnnotationText}
1201
+ />
1202
+ <div className="flex gap-2">
1203
+ <Button
1204
+ onClick={() => save({ comment: annotationText })}
1205
+ disabled={!annotationText.trim()}
1206
+ >
1207
+ Save
1208
+ </Button>
1209
+ <Button variant="outline" onClick={cancel}>
1210
+ Cancel
1211
+ </Button>
1212
+ </div>
1213
+ </div>
1214
+ )}
1215
+ onAnnotationCreate={appendAnnotation}
1216
+ >
1217
+ <SystemMessageBlock
1218
+ message={{ id: "1", role: "system", content: "Use only verified context." }}
1219
+ />
1220
+ </Annotatable>
1221
+ ```
1222
+
1223
+ **Recipe 3 — Per-turn thumbs with optional note**
1224
+ Why: Reactable on each assistant turn. Thumbs are the rail; renderForm only opens for thumbs-down to capture a reason.
1225
+
1226
+ ```jsx
1227
+ const [note, setNote] = useState("")
1228
+
1229
+ <Reactable
1230
+ types={["thumbs"]}
1231
+ renderForm={({ type, save, cancel }) =>
1232
+ type === "thumbs_down" ? (
1233
+ <div className="space-y-2">
1234
+ <CommentBox
1235
+ label="What was wrong?"
1236
+ value={note}
1237
+ onChange={setNote}
1238
+ />
1239
+ <div className="flex gap-2">
1240
+ <Button onClick={() => save(note)} disabled={!note.trim()}>
1241
+ Save
1242
+ </Button>
1243
+ <Button variant="outline" onClick={cancel}>
1244
+ Cancel
1245
+ </Button>
1246
+ </div>
1247
+ </div>
1248
+ ) : null
1249
+ }
1250
+ onReact={saveReaction}
1251
+ >
1252
+ <AssistantMessageBlock
1253
+ message={{ id: "1", role: "assistant", content: "..." }}
1254
+ />
1255
+ </Reactable>
1256
+ ```
1257
+
1258
+ **Recipe 4 — Annotate spans inside a reasoning trace**
1259
+ Why: Annotatable wraps the reasoning turn; selection inside captures a span; the slot is a CommentBox so reviewers can call out a wrong inference at the exact phrase.
1260
+
1261
+ ```jsx
1262
+ const [annotationText, setAnnotationText] = useState("")
1263
+
1264
+ <Annotatable
1265
+ annotations={existing}
1266
+ renderAnnotation={({ span, save, cancel }) => (
1267
+ <div className="space-y-2">
1268
+ <CommentBox
1269
+ label={`On "${span.text}"`}
1270
+ value={annotationText}
1271
+ onChange={setAnnotationText}
1272
+ />
1273
+ <div className="flex gap-2">
1274
+ <Button
1275
+ onClick={() => save({ comment: annotationText })}
1276
+ disabled={!annotationText.trim()}
1277
+ >
1278
+ Save
1279
+ </Button>
1280
+ <Button variant="outline" onClick={cancel}>
1281
+ Cancel
1282
+ </Button>
1283
+ </div>
1284
+ </div>
1285
+ )}
1286
+ onAnnotationCreate={appendAnnotation}
1287
+ >
1288
+ <ReasoningMessageBlock
1289
+ message={{ id: "1", role: "reasoning", content: "...", defaultOpen: true }}
1290
+ />
1291
+ </Annotatable>
1292
+ ```
1293
+
1294
+ **Recipe 5 — React to a tool result**
1295
+ Why: Reactable wraps the standalone tool result, so reviewers can thumbs-down or flag bad evidence without reacting to the whole conversation.
1296
+
1297
+ ```jsx
1298
+ const [note, setNote] = useState("")
1299
+
1300
+ <Reactable
1301
+ types={["thumbs", "flag"]}
1302
+ renderForm={({ save, cancel }) => (
1303
+ <div className="space-y-2">
1304
+ <CommentBox
1305
+ label="What is wrong with this result?"
1306
+ value={note}
1307
+ onChange={setNote}
1308
+ />
1309
+ <div className="flex gap-2">
1310
+ <Button onClick={() => save(note)} disabled={!note.trim()}>
1311
+ Save
1312
+ </Button>
1313
+ <Button variant="outline" onClick={cancel}>
1314
+ Cancel
1315
+ </Button>
1316
+ </div>
1317
+ </div>
1318
+ )}
1319
+ onReact={recordReaction}
1320
+ >
1321
+ <ToolResultBlock
1322
+ message={{ id: "1", role: "tool_result", tool: "search_docs", content: "..." }}
1323
+ />
1324
+ </Reactable>
1325
+ ```
1326
+ <!-- END ORIZU_AUTO_COMPONENT_REFERENCE -->
1327
+
1328
+ ---
1329
+
1330
+ ## Core type shapes
1331
+
1332
+ These are the shapes most agents need when composing the primitives. The runtime does not require you to declare these interfaces, but matching them avoids bad callback names and invalid reaction values.
1333
+
1334
+ ```ts
1335
+ type MessageRole =
1336
+ | 'user'
1337
+ | 'assistant'
1338
+ | 'system'
1339
+ | 'reasoning'
1340
+ | 'tool_call'
1341
+ | 'tool_result'
1342
+ | 'context';
1343
+
1344
+ interface Message {
1345
+ id: string;
1346
+ role: MessageRole;
1347
+ content: string;
1348
+ tool?: string;
1349
+ args?: unknown;
1350
+ result?: unknown;
1351
+ source?: string;
1352
+ timestamp?: string;
1353
+ duration?: string;
1354
+ avatar?: React.ReactNode;
1355
+ label?: string;
1356
+ badge?: React.ReactNode;
1357
+ defaultOpen?: boolean;
1358
+ }
1359
+
1360
+ type ReactionType = 'thumbs_up' | 'thumbs_down' | 'flag';
1361
+
1362
+ interface AnnotationSpan {
1363
+ startOffset: number;
1364
+ endOffset: number;
1365
+ text: string;
1366
+ }
1367
+
1368
+ interface AnnotationRecord<TData = unknown> {
1369
+ id: string;
1370
+ span: AnnotationSpan;
1371
+ data: TData;
1372
+ createdAt: Date | string;
1373
+ updatedAt?: Date | string;
1374
+ hoverContent?: React.ReactNode;
1375
+ }
1376
+
1377
+ interface Tag {
1378
+ id: string;
1379
+ label: string;
1380
+ category?: string;
1381
+ description?: string;
1382
+ color?: string;
1383
+ }
1384
+ ```
1385
+
1386
+ ---
1387
+
1388
+ ## Common pitfalls (do NOT do these)
1389
+
1390
+ - ❌ `import StarRating from '@/components/base/input/StarRating'` — default imports are not consistently available. Use `{ StarRating }`.
1391
+ - ❌ `import { Heart } from 'lucide-react'` — third-party packages aren't in the registry. If you need an icon, use one already imported by a component you're rendering, or use Unicode (★, ✓, ↗).
1392
+ - ❌ `import { Foo } from '@/components/base/content/Foo'` for any `Foo` not in the Imports section — runtime throws `Module not found`.
1393
+ - ❌ Using `data` or `onSubmit` as the root component prop names — must be `inputData` and `onComplete`.
1394
+ - ❌ Anonymous default exports (`export default () => …`, `export default memo(…)`). Use a named function.
1395
+ - ❌ Calling `onComplete` more than once per row, or never calling it.
1396
+ - ❌ Mutating `inputData` or `initialValues`. Treat them as immutable.
1397
+ - ❌ Adding global CSS or modifying `app/globals.css`. Style with Tailwind utilities inside your component only.
1398
+ - ❌ `<TextContent>{children}</TextContent>` — TextContent takes a `content` string prop, not children.
1399
+ - ❌ Re-styling primitives' colors with arbitrary Tailwind values (`text-red-500`). Use the design tokens (`text-foreground`, `text-muted-foreground`, `text-primary`, `text-destructive`).
1400
+ - ❌ Schema features beyond the supported subset (`pattern`, `format`, `oneOf`, `anyOf`, `minLength`). They will be ignored or rejected.
1401
+ - ❌ Multi-point Likert scales for binary judgments — annotators on Likert collapse to the middle. Use multiple binary fields.
1402
+
1403
+ ---
1404
+
1405
+ ## Validating before publish
1406
+
1407
+ Customers can validate a generated file before publishing it via the CLI app publish endpoints, including `POST /api/cli/apps/create-from-file` and `POST /api/cli/apps/[id]/update-from-file`. These endpoints run the same validator and esbuild pipeline used at render time. If validation fails, errors describe exactly which rule was violated. Use this in your agent's CI step if you have one.
1408
+
1409
+ ### Offline smoke test
1410
+
1411
+ Before `orizu apps create`, run the smoke test that ships with this skill:
1412
+
1413
+ ```bash
1414
+ node /path/to/orizu-cli-skill/scripts/test-app.mjs \
1415
+ ./labeler/App.tsx \
1416
+ ./labeler/input.json \
1417
+ ./labeler/output.json \
1418
+ ./labeler/sample-payload.json # optional
1419
+ ```
1420
+
1421
+ What it checks:
1422
+ - File parses, has a single named default export.
1423
+ - Default export's signature destructures `inputData`, `onComplete`, `initialValues` (and not the deprecated names).
1424
+ - `input.json` and `output.json` use only the supported validation surface (`type`, `required`, `properties`, `items`, `enum`).
1425
+ - If a `sample-payload.json` is provided, it validates against `output.json`.
1426
+
1427
+ What it doesn't check:
1428
+ - Server-side persistence and project authorization. Those still happen during `orizu apps create`.
1429
+
1430
+ ### Local Playwright preview
1431
+
1432
+ Before publishing, render the app with the local CLI preview:
1433
+
1434
+ ```bash
1435
+ orizu apps preview \
1436
+ --file ./labeler/App.tsx \
1437
+ --input-schema ./labeler/input.json \
1438
+ --output-schema ./labeler/output.json \
1439
+ --sample-row ./labeler/sample-row.json \
1440
+ --screenshot ./labeler/preview.png
1441
+ ```
1442
+
1443
+ The command validates the same app contract and allowed import registry as upload, validates the sample row against `input.json`, serves a temporary static preview page, passes `inputData`, `initialValues`, and `onComplete`, then uses Playwright to render it. Add `--headed` for visible Chromium review, and `--keep-open` when you want to inspect the local page manually. In the Orizu web checkout, preview uses the live component tree and global Tailwind CSS; in the mirrored/published CLI package, it uses the bundled preview runtime snapshot so the workflow remains available without the site source tree.
1444
+
1445
+ For coding agents: do not treat a passing contract check as enough. After generating or editing an app, run `orizu apps preview` with a representative row, inspect the screenshot, and compare the rendered workflow to the user's likely intent: are the right fields visible, is the primary judgment obvious, do controls fit, and would a human reviewer know what to do? If the screenshot looks wrong, revise the app and preview again before publishing.
1446
+
1447
+ ---
1448
+
1449
+ ## Pre-publish checklist
1450
+
1451
+ Before `orizu apps create`:
1452
+
1453
+ - [ ] Default export is a named function/class
1454
+ - [ ] Props are exactly `inputData`, `onComplete`, `initialValues`
1455
+ - [ ] Schemas use only the supported validation subset
1456
+ - [ ] Keyboard shortcuts cover the primary actions
1457
+ - [ ] Layout reflows on narrow screens
1458
+ - [ ] App calls `onComplete` with the full response payload; no custom save state in the app
1459
+ - [ ] `?` shortcut overlay or visible cheat sheet
1460
+ - [ ] Smoke test passes (`node scripts/test-app.mjs ...`)
1461
+ - [ ] Local preview renders and screenshot is inspected against the user's intended workflow (`orizu apps preview ... --screenshot preview.png`)