setIsChatModalOpen(true)}
- role="button"
- tabIndex={0}
- onKeyDown={(e) => e.key === "Enter" && setIsChatModalOpen(true)}
- >
-
-
-
-
Try it out live
-
- Live interactive demo of OpenUI Chat in action
-
+ return (
+
setIsChatModalOpen(true)}
+ role="button"
+ tabIndex={0}
+ onKeyDown={(e) => e.key === "Enter" && setIsChatModalOpen(true)}
+ >
+
+
+
+
Try it out live
+
+ Live interactive demo of OpenUI Chat in action
+
+
+
-
+ {isChatModalOpen &&
setIsChatModalOpen(false)} />}
- {isChatModalOpen &&
setIsChatModalOpen(false)} />}
-
);
-}
\ No newline at end of file
+};
diff --git a/docs/content/docs/openui-lang/defining-components.mdx b/docs/content/docs/openui-lang/defining-components.mdx
index c4a70b18d..f0e1075cb 100644
--- a/docs/content/docs/openui-lang/defining-components.mdx
+++ b/docs/content/docs/openui-lang/defining-components.mdx
@@ -84,6 +84,26 @@ const TabItemSchema = z.object({
});
```
+## The `root` field
+
+The `root` option in `createLibrary` specifies which component the LLM must use as the entry point. The generated system prompt instructs the model to always start with `root =
(...)`.
+
+```ts
+const library = createLibrary({
+ root: "Stack", // → prompt tells LLM: "every program must define root = Stack(...)"
+ components: [Stack, Card, TextContent],
+});
+```
+
+This serves two purposes:
+
+1. **Constrains the LLM** — the model always wraps its output in a known top-level component, making output predictable.
+2. **Enables streaming** — because the root statement comes first, the UI shell renders immediately while child components stream in.
+
+The `root` must match the `name` of one of the components in your library. If omitted, the prompt uses "Root" as a placeholder.
+
+For the built-in libraries: `openuiLibrary` uses `Stack` (flexible layout container), while `openuiChatLibrary` uses `Card` (vertical container optimized for chat responses).
+
## Notes on schema metadata
- Positional mapping is driven by Zod object key order.
@@ -103,6 +123,67 @@ const library = createLibrary({
});
```
+### Why group components?
+
+`componentGroups` organize the generated system prompt into named sections (e.g., Layout, Forms, Charts). This helps the LLM locate relevant components quickly instead of scanning a flat list. Without groups, all component signatures appear under a single "Ungrouped" heading.
+
+Groups also let you co-locate related components so the LLM understands which components work together (e.g., `Form` with `FormControl`, `Input`, `Select`).
+
+### Adding group notes
+
+Each group can include a `notes` array — these strings are appended directly after the group's component signatures in the generated prompt. Use notes to give the LLM usage hints and constraints:
+
+```ts
+componentGroups: [
+ {
+ name: "Forms",
+ components: ["Form", "FormControl", "Input", "TextArea", "Select"],
+ notes: [
+ "- Define EACH FormControl as its own reference for progressive streaming.",
+ "- NEVER nest Form inside Form.",
+ "- Form requires explicit buttons: Form(name, buttons, fields).",
+ ],
+ },
+ {
+ name: "Layout",
+ components: ["Stack", "Tabs", "TabItem", "Accordion", "AccordionItem"],
+ notes: [
+ '- For grid-like layouts, use Stack with direction "row" and wrap=true.',
+ ],
+ },
+],
+```
+
+Notes appear in the prompt output like this:
+
+```
+### Forms
+Form(id: string, buttons: Buttons, controls: FormControl[]) — Form container
+FormControl(label: string, field: Input | TextArea | Select) — Single field
+...
+- Define EACH FormControl as its own reference for progressive streaming.
+- NEVER nest Form inside Form.
+- Form requires explicit buttons: Form(name, buttons, fields).
+```
+
+### Prompt options
+
+When generating the system prompt, you can pass `PromptOptions` to customize the output further:
+
+```ts
+import type { PromptOptions } from "@openuidev/react-lang";
+
+const options: PromptOptions = {
+ preamble: "You are an assistant that outputs only OpenUI Lang.",
+ additionalRules: ["Always use Card as the root for chat responses."],
+ examples: [`root = Stack([title])\ntitle = TextContent("Hello", "large-heavy")`],
+};
+
+const prompt = library.prompt(options);
+```
+
+See [System Prompts](/docs/openui-lang/system-prompts) for full details on prompt generation.
+
## Next Steps
diff --git a/docs/content/docs/openui-lang/index.mdx b/docs/content/docs/openui-lang/index.mdx
index 8d678f368..8d90e47d4 100644
--- a/docs/content/docs/openui-lang/index.mdx
+++ b/docs/content/docs/openui-lang/index.mdx
@@ -4,22 +4,19 @@ title: Introduction
import { StreamingComparison } from "@/app/docs/openui-lang/streaming-comparison";
import { TryItOut } from "./components/try-it-out";
-import { LangExample } from "./components/lang-example";
-OpenUI is a framework for building Generative UI with a compact, streaming-first language that is up to **[67% more token-efficient](/docs/openui-lang/benchmarks)** than JSON, resulting in faster AI-generated interfaces.
+OpenUI is a full-stack Generative UI framework — a compact streaming-first language, a React runtime with built-in component libraries, and ready-to-use chat interfaces — that is up to **[67% more token-efficient](/docs/openui-lang/benchmarks)** than JSON.
## What is Generative UI?
-
Most AI applications are limited to returning text (as markdown) or rendering pre-built UI responses. Markdown isn't interactive, and pre-built responses are rigid (they don't adapt to the context of the conversation).
-
Generative UI fundamentally changes this relationship. Instead of merely providing content, the AI composes the interface itself. It dynamically selects, configures, and composes components from a predefined library to create a purpose-built interface tailored to the user's immediate request, be it an interactive chart, a complex form, or a multi-tab dashboard.
## OpenUI Lang
-OpenUI Lang is a compact, line-oriented language designed specifically for Large Language Models (LLMs) to generate user interfaces. It serves as a more efficient, predictable, and stream-friendly alternative to verbose formats like JSON.
+OpenUI Lang is a compact, line-oriented language designed specifically for Large Language Models (LLMs) to generate user interfaces. It serves as a more efficient, predictable, and stream-friendly alternative to verbose formats like JSON. For the complete syntax reference, see the [Language Specification](/docs/openui-lang/specification).
### Why a New Language?
@@ -37,22 +34,14 @@ OpenUI Lang was created to solve these core issues:
## How It Works
-
-
-Here is a breakdown of Generative UI workflow:
-
-1. **User Query:** The process begins when a user interacts with your application. In this example, they ask, "What did I spend on last month?".
-
-2. **Backend Processing:** The user's query is sent to your backend. Backend applies its own business logic (e.g., authenticating the user, fetching spending data from a database) and prepares a request for an LLM provider.
-
-3. **System Prompt to LLM:** Backend sends its system prompt to the request along with OpenUI Lang spec prompt.
+
-4. **LLM Generates OpenUI Lang:** The LLM provider (like OpenAI, Anthropic, etc.) processes the prompt. Instead of returning plain text or JSON, it generates a response in **OpenUI Lang**, a token-efficient syntax designed for this purpose (e.g., `root = Stack([chart])`).
+The key difference from a standard chat app is what happens at the LLM and rendering layers:
-5. **Rendering:** On the client side, the `@openuidev/lang-react` library's `` component receives and parses the OpenUI Lang stream in real-time. As each line arrives, it safely maps the code to the corresponding React components you defined in your library and renders them.
+1. **System prompt includes OpenUI Lang spec** — Your backend appends the generated component library prompt alongside your system prompt, instructing the LLM to respond in OpenUI Lang instead of plain text or JSON.
-The final result is a rich, native UI—like the "Total expenses" card and interactive pie chart—that was dynamically generated by the AI, streamed efficiently, and rendered safely on the client's device.
+2. **LLM generates OpenUI Lang** — Instead of returning markdown, the model outputs a compact, line-oriented syntax (e.g., `root = Stack([chart])`) constrained to your component library.
-## Usage Example
+3. **Streaming render** — On the client, the `` component parses each line as it arrives and maps it to your React components in real-time — structure first, then data fills in progressively.
-
\ No newline at end of file
+The result is a native UI — like the "Total expenses" card and interactive pie chart above — dynamically composed by the AI, streamed efficiently, and rendered safely from your own components.
diff --git a/docs/content/docs/openui-lang/interactivity.mdx b/docs/content/docs/openui-lang/interactivity.mdx
index dd2324cea..795781a48 100644
--- a/docs/content/docs/openui-lang/interactivity.mdx
+++ b/docs/content/docs/openui-lang/interactivity.mdx
@@ -25,13 +25,13 @@ When a user clicks a button or follow-up, the component calls `triggerAction`. T
### `ActionEvent`
-| Field | Type | Description |
-| :--------------------- | :------------------------------ | :--------------------------------------------- |
-| `type` | `string` | Action type (see built-in types below). |
-| `params` | `Record` | Extra parameters from the component. |
-| `humanFriendlyMessage` | `string` | Display label for the action. |
-| `formState` | `Record \| undefined` | Raw field state at time of action. |
-| `formName` | `string \| undefined` | Form that scoped the action, if any. |
+| Field | Type | Description |
+| :--------------------- | :--------------------------------- | :-------------------------------------- |
+| `type` | `string` | Action type (see built-in types below). |
+| `params` | `Record` | Extra parameters from the component. |
+| `humanFriendlyMessage` | `string` | Display label for the action. |
+| `formState` | `Record \| undefined` | Raw field state at time of action. |
+| `formName` | `string \| undefined` | Form that scoped the action, if any. |
### Built-in action types
@@ -91,12 +91,12 @@ Use `onStateUpdate` to persist field state (e.g. to a message in your thread sto
Use these inside `defineComponent` renderers:
-| Hook | Signature | Description |
-| :------------------ | :------------------------------------------------------------------------------------------------- | :--------------------------------- |
-| `useGetFieldValue` | `(formName: string \| undefined, name: string) => any` | Read a field's current value. |
-| `useSetFieldValue` | `(formName: string \| undefined, componentType: string \| undefined, name: string, value: any, shouldTriggerSaveCallback?: boolean) => void` | Write a field value. |
-| `useFormName` | `() => string \| undefined` | Get the enclosing form's name. |
-| `useSetDefaultValue`| `(options: { formName?, componentType, name, existingValue, defaultValue, shouldTriggerSaveCallback? }) => void` | Set a default if no value exists. |
+| Hook | Signature | Description |
+| :------------------- | :------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------- |
+| `useGetFieldValue` | `(formName: string \| undefined, name: string) => any` | Read a field's current value. |
+| `useSetFieldValue` | `(formName: string \| undefined, componentType: string \| undefined, name: string, value: any, shouldTriggerSaveCallback?: boolean) => void` | Write a field value. |
+| `useFormName` | `() => string \| undefined` | Get the enclosing form's name. |
+| `useSetDefaultValue` | `(options: { formName?, componentType, name, existingValue, defaultValue, shouldTriggerSaveCallback? }) => void` | Set a default if no value exists. |
---
diff --git a/docs/content/docs/openui-lang/meta.json b/docs/content/docs/openui-lang/meta.json
index 129b06186..11bc1aede 100644
--- a/docs/content/docs/openui-lang/meta.json
+++ b/docs/content/docs/openui-lang/meta.json
@@ -5,6 +5,7 @@
"index",
"quickstart",
"---Core Concepts---",
+ "overview",
"defining-components",
"system-prompts",
"renderer",
diff --git a/docs/content/docs/openui-lang/overview.mdx b/docs/content/docs/openui-lang/overview.mdx
new file mode 100644
index 000000000..98a32c6fe
--- /dev/null
+++ b/docs/content/docs/openui-lang/overview.mdx
@@ -0,0 +1,98 @@
+---
+title: Overview
+description: Key building blocks of the OpenUI framework and the built-in component libraries.
+---
+
+import { LangExample } from "./components/lang-example";
+
+OpenUI is built around four core building blocks that work together to turn LLM output into rendered UI:
+
+- **Library** — A collection of components defined with Zod schemas and React renderers. The library is the contract between your app and the AI — it defines what components the LLM can use and how they render.
+
+- **Prompt Generator** — Converts your library into a system prompt that instructs the LLM to output valid OpenUI Lang. Includes syntax rules, component signatures, streaming guidelines, and your custom examples/rules.
+
+- **Parser** — Parses OpenUI Lang text (line-by-line, streaming-compatible) into a typed element tree. Validates against your library's JSON Schema and gracefully handles partial/invalid output.
+
+- **Renderer** — The `` React component takes parsed output and maps each element to your library's React components, rendering the UI progressively as the stream arrives.
+
+## Built-in Component Libraries
+
+OpenUI ships with two ready-to-use libraries via `@openuidev/react-ui`. Both include layouts, content blocks, charts, forms, tables, and more.
+
+### General-purpose library (`openuiLibrary`)
+
+Root component is `Stack`. Includes the full component suite with flexible layout primitives. Use this for standalone rendering, playgrounds, and non-chat interfaces.
+
+```ts
+import { openuiLibrary, openuiPromptOptions } from "@openuidev/react-ui/genui-lib";
+import { Renderer } from "@openuidev/react-lang";
+
+// Generate system prompt
+const systemPrompt = openuiLibrary.prompt(openuiPromptOptions);
+
+// Render streamed output
+
+```
+
+### Chat-optimized library (`openuiChatLibrary`)
+
+Root component is `Card` (vertical container, no layout params). Adds chat-specific components like `FollowUpBlock`, `ListBlock`, and `SectionBlock`. Does not include `Stack` — responses are always single-card, vertically stacked.
+
+```ts
+import { openuiChatLibrary, openuiChatPromptOptions } from "@openuidev/react-ui/genui-lib";
+import { FullScreen } from "@openuidev/react-ui";
+
+// Use with a chat layout
+
+```
+
+Both libraries expose a `.prompt()` method to generate the system prompt your LLM needs. See [System Prompts](/docs/openui-lang/system-prompts) for CLI and programmatic generation options.
+
+### Extend a built-in library
+
+```ts
+import { createLibrary, defineComponent } from "@openuidev/react-lang";
+import { openuiLibrary } from "@openuidev/react-ui/genui-lib";
+import { z } from "zod";
+
+const ProductCard = defineComponent({
+ name: "ProductCard",
+ description: "Product tile",
+ props: z.object({
+ name: z.string(),
+ price: z.number(),
+ }),
+ component: ({ props }) => {props.name}: ${props.price}
,
+});
+
+const myLibrary = createLibrary({
+ root: openuiLibrary.root ?? "Stack",
+ componentGroups: openuiLibrary.componentGroups,
+ components: [...Object.values(openuiLibrary.components), ProductCard],
+});
+```
+
+## Usage Example
+
+
+
+## Next Steps
+
+
+
+ Create custom components with Zod schemas and React renderers.
+
+
+ Generate and customize LLM instructions from your library.
+
+
+ Parse and render streamed OpenUI Lang in React.
+
+
+ Build AI chat interfaces with prebuilt layouts.
+
+
diff --git a/docs/content/docs/openui-lang/quickstart.mdx b/docs/content/docs/openui-lang/quickstart.mdx
index 4fd33290b..fc19f1c02 100644
--- a/docs/content/docs/openui-lang/quickstart.mdx
+++ b/docs/content/docs/openui-lang/quickstart.mdx
@@ -1,28 +1,65 @@
---
title: Quick Start
-description: Use the OpenUI library to render OpenUI Lang immediately.
+description: Bootstrap a Generative UI chat app in under a minute.
---
-
-#### Bootstrap a GenUI Chat app
+## Bootstrap a GenUI Chat app
```bash
npx @openuidev/cli@latest create --name genui-chat-app
cd genui-chat-app
```
-#### Add your API key
+## Add your API key
+
+The generated app uses OpenAI by default, but works with any OpenAI-compatible provider (e.g., OpenRouter, Azure OpenAI, Anthropic via proxy).
```bash
echo "OPENAI_API_KEY=sk-your-key-here" > .env
```
-#### Start the dev server
+## Start the dev server
```bash
npm run dev
```
-The generated app wires up a predefined component library in `src/app/page.tsx`.
+## What's included
+
+The CLI generates a Next.js app with everything wired up:
+
+```
+src/
+ app/
+ page.tsx # FullScreen chat layout with the built-in component library
+ api/chat/
+ route.ts # Backend route with OpenAI streaming + example tools
+ library.ts # Re-exports openuiChatLibrary and openuiChatPromptOptions
+ generated/
+ system-prompt.txt # Auto-generated at build time via `openui generate`
+```
+
+- **`page.tsx`** — Renders the `FullScreen` chat layout with `openuiChatLibrary` for Generative UI rendering and `openAIAdapter()` for streaming.
+- **`route.ts`** — A backend API route that sends the system prompt to the LLM and streams the response back.
+- **`library.ts`** — Your component library entrypoint. The `openui generate` CLI reads this file to produce the system prompt.
+
+The `dev` and `build` scripts automatically regenerate the system prompt before starting:
+
+```json
+"generate:prompt": "openui generate src/library.ts --out src/generated/system-prompt.txt",
+"dev": "pnpm generate:prompt && next dev"
+```
+
+## Next Steps
-Follow guide: [Define Your Components](/docs/openui-lang/defining-components) to learn how to create your own component library.
+
+
+ Understand the key building blocks and built-in component libraries.
+
+
+ Create your own custom component library.
+
+
+ Explore chat-specific layouts and configurations.
+
+
diff --git a/docs/content/docs/openui-lang/renderer.mdx b/docs/content/docs/openui-lang/renderer.mdx
index 9e528362e..58adcadba 100644
--- a/docs/content/docs/openui-lang/renderer.mdx
+++ b/docs/content/docs/openui-lang/renderer.mdx
@@ -24,15 +24,15 @@ export function AssistantMessage({
## Props
-| Prop | Type | Description |
-| :--------------- | :-------------------------------------- | :------------------------------------------------------------- |
-| `response` | `string \| null` | Raw OpenUI Lang response text. |
-| `library` | `Library` | Library created by `createLibrary(...)`. |
-| `isStreaming` | `boolean` | Indicates stream is in progress. |
-| `onAction` | `(event: ActionEvent) => void` | Receives structured action events from interactive components. |
-| `onStateUpdate` | `(state: Record) => void` | Called on form field changes with the raw field state map. |
-| `initialState` | `Record` | Hydrates form state on load (e.g. from persisted message). |
-| `onParseResult` | `(result: ParseResult \| null) => void` | Debug/inspect latest parse result. |
+| Prop | Type | Description |
+| :-------------- | :-------------------------------------- | :------------------------------------------------------------- |
+| `response` | `string \| null` | Raw OpenUI Lang response text. |
+| `library` | `Library` | Library created by `createLibrary(...)`. |
+| `isStreaming` | `boolean` | Indicates stream is in progress. |
+| `onAction` | `(event: ActionEvent) => void` | Receives structured action events from interactive components. |
+| `onStateUpdate` | `(state: Record) => void` | Called on form field changes with the raw field state map. |
+| `initialState` | `Record` | Hydrates form state on load (e.g. from persisted message). |
+| `onParseResult` | `(result: ParseResult \| null) => void` | Debug/inspect latest parse result. |
## Streaming behavior
diff --git a/docs/content/docs/openui-lang/standard-library.mdx b/docs/content/docs/openui-lang/standard-library.mdx
index 0c71f9f74..dd754fa5d 100644
--- a/docs/content/docs/openui-lang/standard-library.mdx
+++ b/docs/content/docs/openui-lang/standard-library.mdx
@@ -23,6 +23,14 @@ import { openuiLibrary } from "@openuidev/react-ui";
## Generate prompt
+Use the CLI to generate the system prompt at build time:
+
+```bash
+npx @openuidev/cli generate ./src/library.ts --out src/generated/system-prompt.txt
+```
+
+Or generate programmatically:
+
```ts
import { openuiLibrary, openuiPromptOptions } from "@openuidev/react-ui";
diff --git a/docs/content/docs/openui-lang/system-prompts.mdx b/docs/content/docs/openui-lang/system-prompts.mdx
index 8072cd4bd..fc522a348 100644
--- a/docs/content/docs/openui-lang/system-prompts.mdx
+++ b/docs/content/docs/openui-lang/system-prompts.mdx
@@ -3,9 +3,31 @@ title: System Prompts
description: Generate and customize prompt instructions from your OpenUI library.
---
-`library.prompt(...)` generates the instruction text your model needs to output valid OpenUI Lang.
+`library.prompt(...)` generates the instruction text your model needs to output valid OpenUI Lang. You can generate it programmatically or with the CLI.
-## Generate prompt
+## Generate with the CLI
+
+The fastest way to generate a system prompt — works with any backend language:
+
+```bash
+npx @openuidev/cli generate ./src/library.ts
+```
+
+Write to a file:
+
+```bash
+npx @openuidev/cli generate ./src/library.ts --out system-prompt.txt
+```
+
+Generate JSON Schema instead:
+
+```bash
+npx @openuidev/cli generate ./src/library.ts --json-schema
+```
+
+The CLI auto-detects exported `PromptOptions` (examples, rules) alongside your library. Use `--prompt-options ` to pick a specific export.
+
+## Generate programmatically
```ts
import { openuiLibrary, openuiPromptOptions } from "@openuidev/react-ui";
@@ -46,7 +68,7 @@ The generated prompt includes:
```ts
import OpenAI from "openai";
-import { openuiLibrary, openuiPromptOptions } from "@openuidev/react-ui";
+import { openuiLibrary, openuiPromptOptions } from "@openuidev/react-ui/genui-lib";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
@@ -56,13 +78,12 @@ export async function POST(req: Request) {
const completion = await client.chat.completions.create({
model: "gpt-5.2",
stream: true,
- messages: [
- { role: "system", content: openuiLibrary.prompt(openuiPromptOptions) },
- ...messages,
- ],
+ messages: [{ role: "system", content: openuiLibrary.prompt(openuiPromptOptions) }, ...messages],
});
- return new Response("stream here");
+ return new Response(completion.toReadableStream(), {
+ headers: { "Content-Type": "text/event-stream" },
+ });
}
```
diff --git a/docs/generated/chat-system-prompt.txt b/docs/generated/chat-system-prompt.txt
new file mode 100644
index 000000000..b655c131e
--- /dev/null
+++ b/docs/generated/chat-system-prompt.txt
@@ -0,0 +1,202 @@
+You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang.
+
+## Syntax Rules
+
+1. Each statement is on its own line: `identifier = Expression`
+2. `root` is the entry point — every program must define `root = Card(...)`
+3. Expressions are: strings ("..."), numbers, booleans (true/false), arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)
+4. Use references for readability: define `name = ...` on one line, then use `name` later
+5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.
+6. Arguments are POSITIONAL (order matters, not names)
+7. Optional arguments can be omitted from the end
+8. No operators, no logic, no variables — only declarations
+9. Strings use double quotes with backslash escaping
+
+## Component Signatures
+
+Arguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.
+The `action` prop type accepts: ContinueConversation (sends message to LLM), OpenUrl (navigates to URL), or Custom (app-defined).
+
+### Content
+CardHeader(title?: string, subtitle?: string) — Header with optional title and subtitle
+TextContent(text: string, size?: "small" | "default" | "large" | "small-heavy" | "large-heavy") — Text block. Supports markdown. Optional size: "small" | "default" | "large" | "small-heavy" | "large-heavy".
+MarkDownRenderer(textMarkdown: string, variant?: "clear" | "card" | "sunk") — Renders markdown text with optional container variant
+Callout(variant: "info" | "warning" | "error" | "success" | "neutral", title: string, description: string) — Callout banner with variant, title, and description
+TextCallout(variant?: "neutral" | "info" | "warning" | "success" | "danger", title?: string, description?: string) — Text callout with variant, title, and description
+Image(alt: string, src?: string) — Image with alt text and optional URL
+ImageBlock(src: string, alt?: string) — Image block with loading state
+ImageGallery(images: {src: string, alt?: string, details?: string}[]) — Gallery grid of images with modal preview
+CodeBlock(language: string, codeString: string) — Syntax-highlighted code block
+Separator(orientation?: "horizontal" | "vertical", decorative?: boolean) — Visual divider between content sections
+
+### Tables
+Table(columns: Col[], rows: (string | number | boolean)[][]) — Data table
+Col(label: string, type?: "string" | "number" | "action") — Column definition
+
+### Charts (2D)
+BarChart(labels: string[], series: Series[], variant?: "grouped" | "stacked", xLabel?: string, yLabel?: string) — Vertical bars; use for comparing values across categories with one or more series
+LineChart(labels: string[], series: Series[], variant?: "linear" | "natural" | "step", xLabel?: string, yLabel?: string) — Lines over categories; use for trends and continuous data over time
+AreaChart(labels: string[], series: Series[], variant?: "linear" | "natural" | "step", xLabel?: string, yLabel?: string) — Filled area under lines; use for cumulative totals or volume trends over time
+RadarChart(labels: string[], series: Series[]) — Spider/web chart; use for comparing multiple variables across one or more entities
+HorizontalBarChart(labels: string[], series: Series[], variant?: "grouped" | "stacked", xLabel?: string, yLabel?: string) — Horizontal bars; prefer when category labels are long or for ranked lists
+Series(category: string, values: number[]) — One data series
+
+### Charts (1D)
+PieChart(slices: Slice[], variant?: "pie" | "donut") — Circular slices showing part-to-whole proportions; supports pie and donut variants
+RadialChart(slices: Slice[]) — Radial bars showing proportional distribution across named segments
+SingleStackedBarChart(slices: Slice[]) — Single horizontal stacked bar; use for showing part-to-whole proportions in one row
+Slice(category: string, value: number) — One slice with label and numeric value
+
+### Charts (Scatter)
+ScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string) — X/Y scatter plot; use for correlations, distributions, and clustering
+ScatterSeries(name: string, points: Point[]) — Named dataset
+Point(x: number, y: number, z?: number) — Data point with numeric coordinates
+
+### Forms
+Form(name: string, buttons: Buttons, fields) — Form container with fields and explicit action buttons
+FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup, hint?: string) — Field with label, input component, and optional hint text
+Label(text: string) — Text label
+Input(name: string, placeholder?: string, type?: "text" | "email" | "password" | "number" | "url", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+SelectItem(value: string, label: string) — Option for Select
+DatePicker(name: string, mode: "single" | "range", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+Slider(name: string, variant: "continuous" | "discrete", min: number, max: number, step?: number, defaultValue?: number[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}) — Numeric slider input; supports continuous and discrete (stepped) variants
+CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+CheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean)
+RadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+RadioItem(label: string, description: string, value: string)
+SwitchGroup(name: string, items: SwitchItem[], variant?: "clear" | "card" | "sunk") — Group of switch toggles
+SwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean) — Individual switch toggle
+- Define EACH FormControl as its own reference — do NOT inline all controls in one array.
+- NEVER nest Form inside Form.
+- Form requires explicit buttons. Always pass a Buttons(...) reference as the third Form argument.
+- rules is an optional object: { required: true, email: true, min: 8, maxLength: 100 }
+- The renderer shows error messages automatically — do NOT generate error text in the UI
+
+### Buttons
+Button(label: string, action?: {type: "open_url", url: string} | {type: "continue_conversation", context?: string} | {type: string, params?}, variant?: "primary" | "secondary" | "tertiary", type?: "normal" | "destructive", size?: "extra-small" | "small" | "medium" | "large") — Clickable button
+Buttons(buttons: Button[], direction?: "row" | "column") — Group of Button components. direction: "row" (default) | "column".
+
+### Lists & Follow-ups
+ListBlock(items: ListItem[], variant?: "number" | "image") — A list of items with number or image indicators. Each item can optionally have an action.
+ListItem(title: string, subtitle?: string, image?: {src: string, alt: string}, actionLabel?: string, action?: {type: "open_url", url: string} | {type: "continue_conversation", context?: string} | {type: string, params?}) — Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable.
+FollowUpBlock(items: FollowUpItem[]) — List of clickable follow-up suggestions placed at the end of a response
+FollowUpItem(text: string) — Clickable follow-up suggestion — when clicked, sends text as user message
+- Use ListBlock with ListItem references for numbered, clickable lists.
+- Use FollowUpBlock with FollowUpItem references at the end of a response to suggest next actions.
+- Clicking a ListItem or FollowUpItem sends its text to the LLM as a user message.
+- Example: list = ListBlock([item1, item2]) item1 = ListItem("Option A", "Details about A")
+
+### Sections
+SectionBlock(sections: SectionItem[], isFoldable?: boolean) — Collapsible accordion sections. Auto-opens sections as they stream in. Use SectionItem for each section.
+SectionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps | ListBlock | FollowUpBlock)[]) — Section with a label and collapsible content — used inside SectionBlock
+- SectionBlock renders collapsible accordion sections that auto-open as they stream.
+- Each section needs a unique `value` id, a `trigger` label, and a `content` array.
+- Example: sections = SectionBlock([s1, s2]) s1 = SectionItem("intro", "Introduction", [content1])
+- Set isFoldable=false to render sections as flat headers instead of accordion.
+
+### Layout
+Tabs(items: TabItem[]) — Tabbed container
+TabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[]) — value is unique id, trigger is tab label, content is array of components
+Accordion(items: AccordionItem[]) — Collapsible sections
+AccordionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[]) — value is unique id, trigger is section title
+Steps(items: StepsItem[]) — Step-by-step guide
+StepsItem(title: string, details: string) — title and details text for one step
+Carousel(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[][], variant?: "card" | "sunk") — Horizontal scrollable carousel
+- Use Tabs to present alternative views — each TabItem has a value id, trigger label, and content array.
+- Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]])
+- IMPORTANT: Every slide in a Carousel must have the same structure — same component types in the same order.
+- For image carousels use: [[title, image, description, tags], ...] — every slide must follow this exact pattern.
+- Use real, publicly accessible image URLs (e.g. https://picsum.photos/seed/KEYWORD/800/500). Never hallucinate image URLs.
+
+### Data Display
+TagBlock(tags: string[]) — tags is an array of strings
+Tag(text: string, icon?: string, size?: "sm" | "md" | "lg", variant?: "neutral" | "info" | "success" | "warning" | "danger") — Styled tag/badge with optional icon and variant
+
+### Ungrouped
+Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[]) — Vertical container for all content in a chat response. Children stack top to bottom automatically.
+
+## Hoisting & Streaming (CRITICAL)
+
+openui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed.
+
+During streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in.
+
+**Recommended statement order for optimal streaming:**
+1. `root = Card(...)` — UI shell appears immediately
+2. Component definitions — fill in as they stream
+3. Data values — leaf content last
+
+Always write the root = Card(...) statement first so the UI shell appears immediately, even before child data has streamed in.
+
+## Examples
+
+Example 1 — Table with follow-ups:
+root = Card([title, tbl, followUps])
+title = TextContent("Top Languages", "large-heavy")
+tbl = Table(cols, rows)
+cols = [Col("Language", "string"), Col("Users (M)", "number"), Col("Year", "number")]
+rows = [["Python", 15.7, 1991], ["JavaScript", 14.2, 1995], ["Java", 12.1, 1995]]
+followUps = FollowUpBlock([fu1, fu2])
+fu1 = FollowUpItem("Tell me more about Python")
+fu2 = FollowUpItem("Show me a JavaScript comparison")
+
+Example 2 — Clickable list:
+root = Card([title, list])
+title = TextContent("Choose a topic", "large-heavy")
+list = ListBlock([item1, item2, item3])
+item1 = ListItem("Getting started", "New to the platform? Start here.")
+item2 = ListItem("Advanced features", "Deep dives into powerful capabilities.")
+item3 = ListItem("Troubleshooting", "Common issues and how to fix them.")
+
+Example 3 — Image carousel with consistent slides + follow-ups:
+root = Card([header, carousel, followups])
+header = CardHeader("Featured Destinations", "Discover highlights and best time to visit")
+carousel = Carousel([[t1, img1, d1, tags1], [t2, img2, d2, tags2], [t3, img3, d3, tags3]], "card")
+t1 = TextContent("Paris, France", "large-heavy")
+img1 = ImageBlock("https://picsum.photos/seed/paris/800/500", "Eiffel Tower at night")
+d1 = TextContent("City of light — best Apr–Jun and Sep–Oct.", "default")
+tags1 = TagBlock(["Landmark", "City Break", "Culture"])
+t2 = TextContent("Kyoto, Japan", "large-heavy")
+img2 = ImageBlock("https://picsum.photos/seed/kyoto/800/500", "Bamboo grove in Arashiyama")
+d2 = TextContent("Temples and bamboo groves — best Mar–Apr and Nov.", "default")
+tags2 = TagBlock(["Temples", "Autumn", "Culture"])
+t3 = TextContent("Machu Picchu, Peru", "large-heavy")
+img3 = ImageBlock("https://picsum.photos/seed/machupicchu/800/500", "Inca citadel in the clouds")
+d3 = TextContent("High-altitude Inca citadel — best May–Sep.", "default")
+tags3 = TagBlock(["Andes", "Hike", "UNESCO"])
+followups = FollowUpBlock([fu1, fu2])
+fu1 = FollowUpItem("Show me only beach destinations")
+fu2 = FollowUpItem("Turn this into a comparison table")
+
+Example 4 — Form with validation:
+root = Card([title, form])
+title = TextContent("Contact Us", "large-heavy")
+form = Form("contact", btns, [nameField, emailField, msgField])
+nameField = FormControl("Name", Input("name", "Your name", "text", { required: true, minLength: 2 }))
+emailField = FormControl("Email", Input("email", "you@example.com", "email", { required: true, email: true }))
+msgField = FormControl("Message", TextArea("message", "Tell us more...", 4, { required: true, minLength: 10 }))
+btns = Buttons([Button("Submit", { type: "continue_conversation" }, "primary")])
+
+## Important Rules
+- ALWAYS start with root = Card(...)
+- Write statements in TOP-DOWN order: root → components → data (leverages hoisting for progressive streaming)
+- Each statement on its own line
+- No trailing text or explanations — output ONLY openui-lang code
+- When asked about data, generate realistic/plausible data
+- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.)
+- NEVER define a variable without referencing it from the tree. Every variable must be reachable from root, otherwise it will not render.
+
+- Every response is a single Card(children) — children stack vertically automatically. No layout params are needed on Card.
+- Card is the only layout container. Do NOT use Stack. Use Tabs to switch between sections, Carousel for horizontal scroll.
+- Use FollowUpBlock at the END of a Card to suggest what the user can do or ask next.
+- Use ListBlock when presenting a set of options or steps the user can click to select.
+- Use SectionBlock to group long responses into collapsible sections — good for reports, FAQs, and structured content.
+- Use SectionItem inside SectionBlock: each item needs a unique value id, a trigger (header label), and a content array.
+- Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]])
+- IMPORTANT: Every slide in a Carousel must use the same component structure in the same order — e.g. all slides: [title, image, description, tags].
+- For image carousels, always use real accessible URLs like https://picsum.photos/seed/KEYWORD/800/500. Never hallucinate or invent image URLs.
+- For forms, define one FormControl reference per field so controls can stream progressively.
+- For forms, always provide the second Form argument with Buttons(...) actions: Form(name, buttons, fields).
+- Never nest Form inside Form.
diff --git a/docs/generated/playground-system-prompt.txt b/docs/generated/playground-system-prompt.txt
new file mode 100644
index 000000000..382a5ecd8
--- /dev/null
+++ b/docs/generated/playground-system-prompt.txt
@@ -0,0 +1,157 @@
+You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang.
+
+## Syntax Rules
+
+1. Each statement is on its own line: `identifier = Expression`
+2. `root` is the entry point — every program must define `root = Stack(...)`
+3. Expressions are: strings ("..."), numbers, booleans (true/false), arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)
+4. Use references for readability: define `name = ...` on one line, then use `name` later
+5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.
+6. Arguments are POSITIONAL (order matters, not names)
+7. Optional arguments can be omitted from the end
+8. No operators, no logic, no variables — only declarations
+9. Strings use double quotes with backslash escaping
+
+## Component Signatures
+
+Arguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.
+The `action` prop type accepts: ContinueConversation (sends message to LLM), OpenUrl (navigates to URL), or Custom (app-defined).
+
+### Layout
+Stack([children], direction?: "row" | "column", gap?: "none" | "xs" | "s" | "m" | "l" | "xl" | "2xl", align?: "start" | "center" | "end" | "stretch" | "baseline", justify?: "start" | "center" | "end" | "between" | "around" | "evenly", wrap?: boolean) — Flex container. direction: "row"|"column" (default "column"). gap: "none"|"xs"|"s"|"m"|"l"|"xl"|"2xl" (default "m"). align: "start"|"center"|"end"|"stretch"|"baseline". justify: "start"|"center"|"end"|"between"|"around"|"evenly".
+Tabs(items: TabItem[]) — Tabbed container
+TabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[]) — value is unique id, trigger is tab label, content is array of components
+Accordion(items: AccordionItem[]) — Collapsible sections
+AccordionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[]) — value is unique id, trigger is section title
+Steps(items: StepsItem[]) — Step-by-step guide
+StepsItem(title: string, details: string) — title and details text for one step
+Carousel(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[][], variant?: "card" | "sunk") — Horizontal scrollable carousel
+Separator(orientation?: "horizontal" | "vertical", decorative?: boolean) — Visual divider between content sections
+- For grid-like layouts, use Stack with direction "row" and wrap set to true.
+- Prefer justify "start" (or omit justify) with wrap=true for stable columns instead of uneven gutters.
+- Use nested Stacks when you need explicit rows/sections.
+
+### Content
+Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps | Tabs | Carousel | Stack)[], variant?: "card" | "sunk" | "clear", direction?: "row" | "column", gap?: "none" | "xs" | "s" | "m" | "l" | "xl" | "2xl", align?: "start" | "center" | "end" | "stretch" | "baseline", justify?: "start" | "center" | "end" | "between" | "around" | "evenly", wrap?: boolean) — Styled container. variant: "card" (default, elevated) | "sunk" (recessed) | "clear" (transparent). Always full width. Accepts all Stack flex params (default: direction "column"). Cards flex to share space in row/wrap layouts.
+CardHeader(title?: string, subtitle?: string) — Header with optional title and subtitle
+TextContent(text: string, size?: "small" | "default" | "large" | "small-heavy" | "large-heavy") — Text block. Supports markdown. Optional size: "small" | "default" | "large" | "small-heavy" | "large-heavy".
+MarkDownRenderer(textMarkdown: string, variant?: "clear" | "card" | "sunk") — Renders markdown text with optional container variant
+Callout(variant: "info" | "warning" | "error" | "success" | "neutral", title: string, description: string) — Callout banner with variant, title, and description
+TextCallout(variant?: "neutral" | "info" | "warning" | "success" | "danger", title?: string, description?: string) — Text callout with variant, title, and description
+Image(alt: string, src?: string) — Image with alt text and optional URL
+ImageBlock(src: string, alt?: string) — Image block with loading state
+ImageGallery(images: {src: string, alt?: string, details?: string}[]) — Gallery grid of images with modal preview
+CodeBlock(language: string, codeString: string) — Syntax-highlighted code block
+
+### Tables
+Table(columns: Col[], rows: (string | number | boolean)[][]) — Data table
+Col(label: string, type?: "string" | "number" | "action") — Column definition
+
+### Charts (2D)
+BarChart(labels: string[], series: Series[], variant?: "grouped" | "stacked", xLabel?: string, yLabel?: string) — Vertical bars; use for comparing values across categories with one or more series
+LineChart(labels: string[], series: Series[], variant?: "linear" | "natural" | "step", xLabel?: string, yLabel?: string) — Lines over categories; use for trends and continuous data over time
+AreaChart(labels: string[], series: Series[], variant?: "linear" | "natural" | "step", xLabel?: string, yLabel?: string) — Filled area under lines; use for cumulative totals or volume trends over time
+RadarChart(labels: string[], series: Series[]) — Spider/web chart; use for comparing multiple variables across one or more entities
+HorizontalBarChart(labels: string[], series: Series[], variant?: "grouped" | "stacked", xLabel?: string, yLabel?: string) — Horizontal bars; prefer when category labels are long or for ranked lists
+Series(category: string, values: number[]) — One data series
+
+### Charts (1D)
+PieChart(slices: Slice[], variant?: "pie" | "donut") — Circular slices showing part-to-whole proportions; supports pie and donut variants
+RadialChart(slices: Slice[]) — Radial bars showing proportional distribution across named segments
+SingleStackedBarChart(slices: Slice[]) — Single horizontal stacked bar; use for showing part-to-whole proportions in one row
+Slice(category: string, value: number) — One slice with label and numeric value
+
+### Charts (Scatter)
+ScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string) — X/Y scatter plot; use for correlations, distributions, and clustering
+ScatterSeries(name: string, points: Point[]) — Named dataset
+Point(x: number, y: number, z?: number) — Data point with numeric coordinates
+
+### Forms
+Form(name: string, buttons: Buttons, fields) — Form container with fields and explicit action buttons
+FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup, hint?: string) — Field with label, input component, and optional hint text
+Label(text: string) — Text label
+Input(name: string, placeholder?: string, type?: "text" | "email" | "password" | "number" | "url", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+SelectItem(value: string, label: string) — Option for Select
+DatePicker(name: string, mode: "single" | "range", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+Slider(name: string, variant: "continuous" | "discrete", min: number, max: number, step?: number, defaultValue?: number[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}) — Numeric slider input; supports continuous and discrete (stepped) variants
+CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+CheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean)
+RadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+RadioItem(label: string, description: string, value: string)
+SwitchGroup(name: string, items: SwitchItem[], variant?: "clear" | "card" | "sunk") — Group of switch toggles
+SwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean) — Individual switch toggle
+- For Form fields, define EACH FormControl as its own reference — do NOT inline all controls in one array. This allows progressive field-by-field streaming.
+- NEVER nest Form inside Form — each Form should be a standalone container.
+- Form requires explicit buttons. Always pass a Buttons(...) reference as the third Form argument.
+- rules is an optional array of validation strings: ["required", "email", "min:8", "maxLength:100"]
+- Available rules: required, email, min:N, max:N, minLength:N, maxLength:N, pattern:REGEX, url, numeric
+- The renderer shows error messages automatically — do NOT generate error text in the UI
+
+### Buttons
+Button(label: string, action?: {type: "open_url", url: string} | {type: "continue_conversation", context?: string} | {type: string, params?}, variant?: "primary" | "secondary" | "tertiary", type?: "normal" | "destructive", size?: "extra-small" | "small" | "medium" | "large") — Clickable button
+Buttons(buttons: Button[], direction?: "row" | "column") — Group of Button components. direction: "row" (default) | "column".
+
+### Data Display
+TagBlock(tags: string[]) — tags is an array of strings
+Tag(text: string, icon?: string, size?: "sm" | "md" | "lg", variant?: "neutral" | "info" | "success" | "warning" | "danger") — Styled tag/badge with optional icon and variant
+
+## Hoisting & Streaming (CRITICAL)
+
+openui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed.
+
+During streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in.
+
+**Recommended statement order for optimal streaming:**
+1. `root = Stack(...)` — UI shell appears immediately
+2. Component definitions — fill in as they stream
+3. Data values — leaf content last
+
+Always write the root = Stack(...) statement first so the UI shell appears immediately, even before child data has streamed in.
+
+## Examples
+
+Example 1 — Table:
+root = Stack([title, tbl])
+title = TextContent("Top Languages", "large-heavy")
+tbl = Table(cols, rows)
+cols = [Col("Language", "string"), Col("Users (M)", "number"), Col("Year", "number")]
+rows = [["Python", 15.7, 1991], ["JavaScript", 14.2, 1995], ["Java", 12.1, 1995], ["TypeScript", 8.5, 2012], ["Go", 5.2, 2009]]
+
+Example 2 — Bar chart:
+root = Stack([title, chart])
+title = TextContent("Q4 Revenue", "large-heavy")
+chart = BarChart(labels, [s1, s2], "grouped")
+labels = ["Oct", "Nov", "Dec"]
+s1 = Series("Product A", [120, 150, 180])
+s2 = Series("Product B", [90, 110, 140])
+
+Example 3 — Form with validation:
+root = Stack([title, form])
+title = TextContent("Contact Us", "large-heavy")
+form = Form("contact", btns, [nameField, emailField, countryField, msgField])
+nameField = FormControl("Name", Input("name", "Your name", "text", { required: true, minLength: 2 }))
+emailField = FormControl("Email", Input("email", "you@example.com", "email", { required: true, email: true }))
+countryField = FormControl("Country", Select("country", countryOpts, "Select...", { required: true }))
+msgField = FormControl("Message", TextArea("message", "Tell us more...", 4, { required: true, minLength: 10 }))
+countryOpts = [SelectItem("us", "United States"), SelectItem("uk", "United Kingdom"), SelectItem("de", "Germany")]
+btns = Buttons([Button("Submit", { type: "continue_conversation" }, "primary"), Button("Cancel", { type: "continue_conversation" }, "secondary")])
+
+Example 4 — Tabs with mixed content:
+root = Stack([title, tabs])
+title = TextContent("React vs Vue", "large-heavy")
+tabs = Tabs([tabReact, tabVue])
+tabReact = TabItem("react", "React", reactContent)
+tabVue = TabItem("vue", "Vue", vueContent)
+reactContent = [TextContent("React is a library by Meta for building UIs."), Callout("info", "Note", "React uses JSX syntax.")]
+vueContent = [TextContent("Vue is a progressive framework by Evan You."), Callout("success", "Tip", "Vue has a gentle learning curve.")]
+
+## Important Rules
+- ALWAYS start with root = Stack(...)
+- Write statements in TOP-DOWN order: root → components → data (leverages hoisting for progressive streaming)
+- Each statement on its own line
+- No trailing text or explanations — output ONLY openui-lang code
+- When asked about data, generate realistic/plausible data
+- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.)
+- NEVER define a variable without referencing it from the tree. Every variable must be reachable from root, otherwise it will not render.
diff --git a/docs/imports/OpenUiGeneratesSchema.tsx b/docs/imports/OpenUiGeneratesSchema.tsx
index 2c4a249f1..ab23e8742 100644
--- a/docs/imports/OpenUiGeneratesSchema.tsx
+++ b/docs/imports/OpenUiGeneratesSchema.tsx
@@ -13,10 +13,10 @@ function Group() {
-
{`import { library } from "./library"
-
-const systemPrompt = library.prompt()
+
{`# generate system prompt from library
+npx @openuidev/cli generate ./src/library.ts
+# use in your backend
const completion = await client.chat.completions.create({
model: "gpt-5.2",
stream: true,
diff --git a/docs/lib/chat-library.ts b/docs/lib/chat-library.ts
new file mode 100644
index 000000000..316f65a7d
--- /dev/null
+++ b/docs/lib/chat-library.ts
@@ -0,0 +1,4 @@
+export {
+ openuiChatLibrary as library,
+ openuiChatPromptOptions as promptOptions,
+} from "@openuidev/react-ui/genui-lib";
diff --git a/docs/lib/playground-library.ts b/docs/lib/playground-library.ts
new file mode 100644
index 000000000..5150c55c6
--- /dev/null
+++ b/docs/lib/playground-library.ts
@@ -0,0 +1,4 @@
+import { openuiExamples } from "@openuidev/react-ui/genui-lib";
+
+export { openuiLibrary as library } from "@openuidev/react-ui/genui-lib";
+export const promptOptions = { examples: openuiExamples };
diff --git a/docs/package.json b/docs/package.json
index 7a72c7254..676daa44e 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -3,8 +3,9 @@
"version": "0.0.0",
"private": true,
"scripts": {
+ "generate:prompts": "pnpm --filter @openuidev/cli build && pnpm exec openui generate lib/chat-library.ts --out generated/chat-system-prompt.txt && pnpm exec openui generate lib/playground-library.ts --out generated/playground-system-prompt.txt",
"build": "next build",
- "dev": "next dev",
+ "dev": "pnpm generate:prompts && next dev",
"start": "next start",
"types:check": "fumadocs-mdx && next typegen && tsc --noEmit",
"postinstall": "fumadocs-mdx",
@@ -13,6 +14,7 @@
"format:check": "prettier --check ."
},
"dependencies": {
+ "@openuidev/cli": "workspace:*",
"@openuidev/react-lang": "workspace:^",
"@openuidev/react-headless": "workspace:^",
"@openuidev/react-ui": "workspace:^",
diff --git a/examples/openui-chat/package.json b/examples/openui-chat/package.json
index 28bd6860c..0e46d9c90 100644
--- a/examples/openui-chat/package.json
+++ b/examples/openui-chat/package.json
@@ -3,12 +3,14 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "dev": "next dev",
+ "generate:prompt": "pnpm --filter @openuidev/cli build && pnpm exec openui generate src/library.ts --out src/generated/system-prompt.txt",
+ "dev": "pnpm generate:prompt && next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
+ "@openuidev/cli": "workspace:*",
"@openuidev/react-ui": "workspace:*",
"@openuidev/react-headless": "workspace:*",
"@openuidev/react-lang": "workspace:*",
diff --git a/examples/openui-chat/src/app/api/chat/route.ts b/examples/openui-chat/src/app/api/chat/route.ts
index ec81a0adb..e7cf3b06f 100644
--- a/examples/openui-chat/src/app/api/chat/route.ts
+++ b/examples/openui-chat/src/app/api/chat/route.ts
@@ -1,6 +1,10 @@
+import { readFileSync } from "fs";
import { NextRequest } from "next/server";
import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions.mjs";
+import { join } from "path";
+
+const systemPrompt = readFileSync(join(process.cwd(), "src/generated/system-prompt.txt"), "utf-8");
// ── Tool implementations ──
@@ -193,7 +197,7 @@ function sseToolCallArgs(
// ── Route handler ──
export async function POST(req: NextRequest) {
- const { messages, systemPrompt } = await req.json();
+ const { messages } = await req.json();
const client = new OpenAI({
apiKey: process.env.OPENROUTER_API_KEY,
@@ -215,7 +219,7 @@ export async function POST(req: NextRequest) {
});
const chatMessages: ChatCompletionMessageParam[] = [
- ...(systemPrompt ? [{ role: "system" as const, content: systemPrompt }] : []),
+ { role: "system", content: systemPrompt },
...cleanMessages,
];
diff --git a/examples/openui-chat/src/app/page.tsx b/examples/openui-chat/src/app/page.tsx
index 1814f45d6..b45c90326 100644
--- a/examples/openui-chat/src/app/page.tsx
+++ b/examples/openui-chat/src/app/page.tsx
@@ -3,11 +3,9 @@ import "@openuidev/react-ui/components.css";
import { openAIAdapter, openAIMessageFormat } from "@openuidev/react-headless";
import { FullScreen } from "@openuidev/react-ui";
-import { openuiChatLibrary, openuiChatPromptOptions } from "@openuidev/react-ui/genui-lib";
+import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib";
import { useState } from "react";
-const systemPrompt = openuiChatLibrary.prompt(openuiChatPromptOptions);
-
export default function Page() {
const [mode, setMode] = useState<"light" | "dark">("light");
return (
@@ -46,7 +44,6 @@ export default function Page() {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: openAIMessageFormat.toApi(messages),
- systemPrompt,
}),
signal: abortController.signal,
});
diff --git a/examples/openui-chat/src/generated/system-prompt.txt b/examples/openui-chat/src/generated/system-prompt.txt
new file mode 100644
index 000000000..b655c131e
--- /dev/null
+++ b/examples/openui-chat/src/generated/system-prompt.txt
@@ -0,0 +1,202 @@
+You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang.
+
+## Syntax Rules
+
+1. Each statement is on its own line: `identifier = Expression`
+2. `root` is the entry point — every program must define `root = Card(...)`
+3. Expressions are: strings ("..."), numbers, booleans (true/false), arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)
+4. Use references for readability: define `name = ...` on one line, then use `name` later
+5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.
+6. Arguments are POSITIONAL (order matters, not names)
+7. Optional arguments can be omitted from the end
+8. No operators, no logic, no variables — only declarations
+9. Strings use double quotes with backslash escaping
+
+## Component Signatures
+
+Arguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.
+The `action` prop type accepts: ContinueConversation (sends message to LLM), OpenUrl (navigates to URL), or Custom (app-defined).
+
+### Content
+CardHeader(title?: string, subtitle?: string) — Header with optional title and subtitle
+TextContent(text: string, size?: "small" | "default" | "large" | "small-heavy" | "large-heavy") — Text block. Supports markdown. Optional size: "small" | "default" | "large" | "small-heavy" | "large-heavy".
+MarkDownRenderer(textMarkdown: string, variant?: "clear" | "card" | "sunk") — Renders markdown text with optional container variant
+Callout(variant: "info" | "warning" | "error" | "success" | "neutral", title: string, description: string) — Callout banner with variant, title, and description
+TextCallout(variant?: "neutral" | "info" | "warning" | "success" | "danger", title?: string, description?: string) — Text callout with variant, title, and description
+Image(alt: string, src?: string) — Image with alt text and optional URL
+ImageBlock(src: string, alt?: string) — Image block with loading state
+ImageGallery(images: {src: string, alt?: string, details?: string}[]) — Gallery grid of images with modal preview
+CodeBlock(language: string, codeString: string) — Syntax-highlighted code block
+Separator(orientation?: "horizontal" | "vertical", decorative?: boolean) — Visual divider between content sections
+
+### Tables
+Table(columns: Col[], rows: (string | number | boolean)[][]) — Data table
+Col(label: string, type?: "string" | "number" | "action") — Column definition
+
+### Charts (2D)
+BarChart(labels: string[], series: Series[], variant?: "grouped" | "stacked", xLabel?: string, yLabel?: string) — Vertical bars; use for comparing values across categories with one or more series
+LineChart(labels: string[], series: Series[], variant?: "linear" | "natural" | "step", xLabel?: string, yLabel?: string) — Lines over categories; use for trends and continuous data over time
+AreaChart(labels: string[], series: Series[], variant?: "linear" | "natural" | "step", xLabel?: string, yLabel?: string) — Filled area under lines; use for cumulative totals or volume trends over time
+RadarChart(labels: string[], series: Series[]) — Spider/web chart; use for comparing multiple variables across one or more entities
+HorizontalBarChart(labels: string[], series: Series[], variant?: "grouped" | "stacked", xLabel?: string, yLabel?: string) — Horizontal bars; prefer when category labels are long or for ranked lists
+Series(category: string, values: number[]) — One data series
+
+### Charts (1D)
+PieChart(slices: Slice[], variant?: "pie" | "donut") — Circular slices showing part-to-whole proportions; supports pie and donut variants
+RadialChart(slices: Slice[]) — Radial bars showing proportional distribution across named segments
+SingleStackedBarChart(slices: Slice[]) — Single horizontal stacked bar; use for showing part-to-whole proportions in one row
+Slice(category: string, value: number) — One slice with label and numeric value
+
+### Charts (Scatter)
+ScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string) — X/Y scatter plot; use for correlations, distributions, and clustering
+ScatterSeries(name: string, points: Point[]) — Named dataset
+Point(x: number, y: number, z?: number) — Data point with numeric coordinates
+
+### Forms
+Form(name: string, buttons: Buttons, fields) — Form container with fields and explicit action buttons
+FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup, hint?: string) — Field with label, input component, and optional hint text
+Label(text: string) — Text label
+Input(name: string, placeholder?: string, type?: "text" | "email" | "password" | "number" | "url", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+SelectItem(value: string, label: string) — Option for Select
+DatePicker(name: string, mode: "single" | "range", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+Slider(name: string, variant: "continuous" | "discrete", min: number, max: number, step?: number, defaultValue?: number[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}) — Numeric slider input; supports continuous and discrete (stepped) variants
+CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+CheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean)
+RadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string})
+RadioItem(label: string, description: string, value: string)
+SwitchGroup(name: string, items: SwitchItem[], variant?: "clear" | "card" | "sunk") — Group of switch toggles
+SwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean) — Individual switch toggle
+- Define EACH FormControl as its own reference — do NOT inline all controls in one array.
+- NEVER nest Form inside Form.
+- Form requires explicit buttons. Always pass a Buttons(...) reference as the third Form argument.
+- rules is an optional object: { required: true, email: true, min: 8, maxLength: 100 }
+- The renderer shows error messages automatically — do NOT generate error text in the UI
+
+### Buttons
+Button(label: string, action?: {type: "open_url", url: string} | {type: "continue_conversation", context?: string} | {type: string, params?}, variant?: "primary" | "secondary" | "tertiary", type?: "normal" | "destructive", size?: "extra-small" | "small" | "medium" | "large") — Clickable button
+Buttons(buttons: Button[], direction?: "row" | "column") — Group of Button components. direction: "row" (default) | "column".
+
+### Lists & Follow-ups
+ListBlock(items: ListItem[], variant?: "number" | "image") — A list of items with number or image indicators. Each item can optionally have an action.
+ListItem(title: string, subtitle?: string, image?: {src: string, alt: string}, actionLabel?: string, action?: {type: "open_url", url: string} | {type: "continue_conversation", context?: string} | {type: string, params?}) — Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable.
+FollowUpBlock(items: FollowUpItem[]) — List of clickable follow-up suggestions placed at the end of a response
+FollowUpItem(text: string) — Clickable follow-up suggestion — when clicked, sends text as user message
+- Use ListBlock with ListItem references for numbered, clickable lists.
+- Use FollowUpBlock with FollowUpItem references at the end of a response to suggest next actions.
+- Clicking a ListItem or FollowUpItem sends its text to the LLM as a user message.
+- Example: list = ListBlock([item1, item2]) item1 = ListItem("Option A", "Details about A")
+
+### Sections
+SectionBlock(sections: SectionItem[], isFoldable?: boolean) — Collapsible accordion sections. Auto-opens sections as they stream in. Use SectionItem for each section.
+SectionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps | ListBlock | FollowUpBlock)[]) — Section with a label and collapsible content — used inside SectionBlock
+- SectionBlock renders collapsible accordion sections that auto-open as they stream.
+- Each section needs a unique `value` id, a `trigger` label, and a `content` array.
+- Example: sections = SectionBlock([s1, s2]) s1 = SectionItem("intro", "Introduction", [content1])
+- Set isFoldable=false to render sections as flat headers instead of accordion.
+
+### Layout
+Tabs(items: TabItem[]) — Tabbed container
+TabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[]) — value is unique id, trigger is tab label, content is array of components
+Accordion(items: AccordionItem[]) — Collapsible sections
+AccordionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[]) — value is unique id, trigger is section title
+Steps(items: StepsItem[]) — Step-by-step guide
+StepsItem(title: string, details: string) — title and details text for one step
+Carousel(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[][], variant?: "card" | "sunk") — Horizontal scrollable carousel
+- Use Tabs to present alternative views — each TabItem has a value id, trigger label, and content array.
+- Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]])
+- IMPORTANT: Every slide in a Carousel must have the same structure — same component types in the same order.
+- For image carousels use: [[title, image, description, tags], ...] — every slide must follow this exact pattern.
+- Use real, publicly accessible image URLs (e.g. https://picsum.photos/seed/KEYWORD/800/500). Never hallucinate image URLs.
+
+### Data Display
+TagBlock(tags: string[]) — tags is an array of strings
+Tag(text: string, icon?: string, size?: "sm" | "md" | "lg", variant?: "neutral" | "info" | "success" | "warning" | "danger") — Styled tag/badge with optional icon and variant
+
+### Ungrouped
+Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[]) — Vertical container for all content in a chat response. Children stack top to bottom automatically.
+
+## Hoisting & Streaming (CRITICAL)
+
+openui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed.
+
+During streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in.
+
+**Recommended statement order for optimal streaming:**
+1. `root = Card(...)` — UI shell appears immediately
+2. Component definitions — fill in as they stream
+3. Data values — leaf content last
+
+Always write the root = Card(...) statement first so the UI shell appears immediately, even before child data has streamed in.
+
+## Examples
+
+Example 1 — Table with follow-ups:
+root = Card([title, tbl, followUps])
+title = TextContent("Top Languages", "large-heavy")
+tbl = Table(cols, rows)
+cols = [Col("Language", "string"), Col("Users (M)", "number"), Col("Year", "number")]
+rows = [["Python", 15.7, 1991], ["JavaScript", 14.2, 1995], ["Java", 12.1, 1995]]
+followUps = FollowUpBlock([fu1, fu2])
+fu1 = FollowUpItem("Tell me more about Python")
+fu2 = FollowUpItem("Show me a JavaScript comparison")
+
+Example 2 — Clickable list:
+root = Card([title, list])
+title = TextContent("Choose a topic", "large-heavy")
+list = ListBlock([item1, item2, item3])
+item1 = ListItem("Getting started", "New to the platform? Start here.")
+item2 = ListItem("Advanced features", "Deep dives into powerful capabilities.")
+item3 = ListItem("Troubleshooting", "Common issues and how to fix them.")
+
+Example 3 — Image carousel with consistent slides + follow-ups:
+root = Card([header, carousel, followups])
+header = CardHeader("Featured Destinations", "Discover highlights and best time to visit")
+carousel = Carousel([[t1, img1, d1, tags1], [t2, img2, d2, tags2], [t3, img3, d3, tags3]], "card")
+t1 = TextContent("Paris, France", "large-heavy")
+img1 = ImageBlock("https://picsum.photos/seed/paris/800/500", "Eiffel Tower at night")
+d1 = TextContent("City of light — best Apr–Jun and Sep–Oct.", "default")
+tags1 = TagBlock(["Landmark", "City Break", "Culture"])
+t2 = TextContent("Kyoto, Japan", "large-heavy")
+img2 = ImageBlock("https://picsum.photos/seed/kyoto/800/500", "Bamboo grove in Arashiyama")
+d2 = TextContent("Temples and bamboo groves — best Mar–Apr and Nov.", "default")
+tags2 = TagBlock(["Temples", "Autumn", "Culture"])
+t3 = TextContent("Machu Picchu, Peru", "large-heavy")
+img3 = ImageBlock("https://picsum.photos/seed/machupicchu/800/500", "Inca citadel in the clouds")
+d3 = TextContent("High-altitude Inca citadel — best May–Sep.", "default")
+tags3 = TagBlock(["Andes", "Hike", "UNESCO"])
+followups = FollowUpBlock([fu1, fu2])
+fu1 = FollowUpItem("Show me only beach destinations")
+fu2 = FollowUpItem("Turn this into a comparison table")
+
+Example 4 — Form with validation:
+root = Card([title, form])
+title = TextContent("Contact Us", "large-heavy")
+form = Form("contact", btns, [nameField, emailField, msgField])
+nameField = FormControl("Name", Input("name", "Your name", "text", { required: true, minLength: 2 }))
+emailField = FormControl("Email", Input("email", "you@example.com", "email", { required: true, email: true }))
+msgField = FormControl("Message", TextArea("message", "Tell us more...", 4, { required: true, minLength: 10 }))
+btns = Buttons([Button("Submit", { type: "continue_conversation" }, "primary")])
+
+## Important Rules
+- ALWAYS start with root = Card(...)
+- Write statements in TOP-DOWN order: root → components → data (leverages hoisting for progressive streaming)
+- Each statement on its own line
+- No trailing text or explanations — output ONLY openui-lang code
+- When asked about data, generate realistic/plausible data
+- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.)
+- NEVER define a variable without referencing it from the tree. Every variable must be reachable from root, otherwise it will not render.
+
+- Every response is a single Card(children) — children stack vertically automatically. No layout params are needed on Card.
+- Card is the only layout container. Do NOT use Stack. Use Tabs to switch between sections, Carousel for horizontal scroll.
+- Use FollowUpBlock at the END of a Card to suggest what the user can do or ask next.
+- Use ListBlock when presenting a set of options or steps the user can click to select.
+- Use SectionBlock to group long responses into collapsible sections — good for reports, FAQs, and structured content.
+- Use SectionItem inside SectionBlock: each item needs a unique value id, a trigger (header label), and a content array.
+- Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]])
+- IMPORTANT: Every slide in a Carousel must use the same component structure in the same order — e.g. all slides: [title, image, description, tags].
+- For image carousels, always use real accessible URLs like https://picsum.photos/seed/KEYWORD/800/500. Never hallucinate or invent image URLs.
+- For forms, define one FormControl reference per field so controls can stream progressively.
+- For forms, always provide the second Form argument with Buttons(...) actions: Form(name, buttons, fields).
+- Never nest Form inside Form.
diff --git a/examples/openui-chat/src/library.ts b/examples/openui-chat/src/library.ts
new file mode 100644
index 000000000..c7ceecfc1
--- /dev/null
+++ b/examples/openui-chat/src/library.ts
@@ -0,0 +1 @@
+export { openuiChatLibrary as library, openuiChatPromptOptions as promptOptions } from "@openuidev/react-ui/genui-lib";
diff --git a/packages/openui-cli/README.md b/packages/openui-cli/README.md
index d2da4e5fd..88a5271fb 100644
--- a/packages/openui-cli/README.md
+++ b/packages/openui-cli/README.md
@@ -84,6 +84,7 @@ Options:
- `-o, --out `: Write output to a file instead of stdout
- `--json-schema`: Output JSON Schema instead of the system prompt
- `--export `: Use a specific export name instead of auto-detecting the library export
+- `--prompt-options `: Use a specific `PromptOptions` export name (auto-detected by default)
- `--no-interactive`: Fail instead of prompting for a missing `entry`
What it does:
@@ -93,6 +94,7 @@ What it does:
- supports both TypeScript and JavaScript entry files
- stubs common asset imports such as CSS, SVG, images, and fonts during bundling
- auto-detects the exported library by checking `library`, `default`, and then all exports
+- auto-detects a `PromptOptions` export (with `examples`, `additionalRules`, or `preamble`) and passes it to `library.prompt()`
Examples:
@@ -101,6 +103,7 @@ openui generate ./src/library.ts
openui generate ./src/library.ts --json-schema
openui generate ./src/library.ts --export library
openui generate ./src/library.ts --out ./artifacts/system-prompt.txt
+openui generate ./src/library.ts --prompt-options myPromptOptions
openui generate --no-interactive ./src/library.ts
```
@@ -114,6 +117,16 @@ If `--export` is not provided, it looks for exports in this order:
2. `default`
3. any other export that matches the expected library shape
+### PromptOptions auto-detection
+
+If `--prompt-options` is not provided, the CLI looks for a `PromptOptions` export in this order:
+
+1. `promptOptions`
+2. `options`
+3. any export whose name ends with `PromptOptions` (case-insensitive)
+
+A valid `PromptOptions` object has at least one of: `examples` (string array), `additionalRules` (string array), or `preamble` (string).
+
## Local Development
Build the CLI locally:
diff --git a/packages/openui-cli/package.json b/packages/openui-cli/package.json
index be0b5cc19..5f5f84ebe 100644
--- a/packages/openui-cli/package.json
+++ b/packages/openui-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@openuidev/cli",
- "version": "0.0.2",
+ "version": "0.0.3",
"description": "CLI for OpenUI",
"bin": {
"openui": "dist/index.js"
diff --git a/packages/openui-cli/src/commands/generate-worker.ts b/packages/openui-cli/src/commands/generate-worker.ts
index 6cee6eaf2..1bf20fefd 100644
--- a/packages/openui-cli/src/commands/generate-worker.ts
+++ b/packages/openui-cli/src/commands/generate-worker.ts
@@ -3,7 +3,7 @@
* prompt or JSON schema. Asset imports are stubbed during bundling so React
* component modules can be evaluated without CSS/image/font loaders.
*
- * argv: [entryPath, exportName?, "--json-schema"?]
+ * argv: [entryPath, exportName?, "--json-schema"?, "--prompt-options", name?]
* stdout: the prompt string or JSON schema
*/
@@ -70,16 +70,61 @@ function findLibrary(mod: Record, exportName?: string): Library
return undefined;
}
+interface PromptOptions {
+ preamble?: string;
+ additionalRules?: string[];
+ examples?: string[];
+}
+
+function isPromptOptions(value: unknown): value is PromptOptions {
+ if (typeof value !== "object" || value === null) return false;
+ const obj = value as Record;
+ const hasExamples = Array.isArray(obj["examples"]);
+ const hasRules = Array.isArray(obj["additionalRules"]);
+ const hasPreamble = typeof obj["preamble"] === "string";
+ return hasExamples || hasRules || hasPreamble;
+}
+
+function findPromptOptions(
+ mod: Record,
+ exportName?: string,
+): PromptOptions | undefined {
+ if (exportName) {
+ const val = mod[exportName];
+ return isPromptOptions(val) ? val : undefined;
+ }
+
+ // Check well-known names first
+ for (const name of ["promptOptions", "options"]) {
+ if (isPromptOptions(mod[name])) return mod[name] as PromptOptions;
+ }
+
+ // Check any export ending with "PromptOptions" or "promptOptions"
+ for (const [key, val] of Object.entries(mod)) {
+ if (/[Pp]rompt[Oo]ptions$/.test(key) && isPromptOptions(val)) return val;
+ }
+
+ return undefined;
+}
+
async function main(): Promise {
const args = process.argv.slice(2);
const entryPath = args[0];
if (!entryPath) {
- console.error("Usage: generate-worker [exportName] [--json-schema]");
+ console.error(
+ "Usage: generate-worker [exportName] [--json-schema] [--prompt-options ]",
+ );
process.exit(1);
}
const jsonSchema = args.includes("--json-schema");
- const exportName = args.find((a) => a !== entryPath && a !== "--json-schema");
+ const promptOptionsIdx = args.indexOf("--prompt-options");
+ const promptOptionsName = promptOptionsIdx !== -1 ? args[promptOptionsIdx + 1] : undefined;
+ const reserved = new Set(["--json-schema", "--prompt-options"]);
+ if (promptOptionsName) reserved.add(promptOptionsName);
+ const exportName = args.find(
+ (a, i) => a !== entryPath && !reserved.has(a) && !(i > 0 && args[i - 1] === "--prompt-options"),
+ );
const bundleDir = fs.mkdtempSync(path.join(os.tmpdir(), "openui-generate-"));
const bundlePath = path.join(bundleDir, "entry.cjs");
@@ -125,7 +170,13 @@ async function main(): Promise {
process.exit(1);
}
- const output = jsonSchema ? JSON.stringify(library.toJSONSchema(), null, 2) : library.prompt();
+ let output: string;
+ if (jsonSchema) {
+ output = JSON.stringify(library.toJSONSchema(), null, 2);
+ } else {
+ const promptOptions = findPromptOptions(mod, promptOptionsName);
+ output = library.prompt(promptOptions);
+ }
process.stdout.write(output);
}
diff --git a/packages/openui-cli/src/commands/generate.ts b/packages/openui-cli/src/commands/generate.ts
index e89260441..f6dab897b 100644
--- a/packages/openui-cli/src/commands/generate.ts
+++ b/packages/openui-cli/src/commands/generate.ts
@@ -6,6 +6,7 @@ export interface GenerateOptions {
out?: string;
jsonSchema?: boolean;
export?: string;
+ promptOptions?: string;
}
export async function runGenerate(entry: string, options: GenerateOptions): Promise {
@@ -21,6 +22,7 @@ export async function runGenerate(entry: string, options: GenerateOptions): Prom
const workerArgs = [workerPath, entryPath];
if (options.export) workerArgs.push(options.export);
if (options.jsonSchema) workerArgs.push("--json-schema");
+ if (options.promptOptions) workerArgs.push("--prompt-options", options.promptOptions);
let output: string;
try {
diff --git a/packages/openui-cli/src/index.ts b/packages/openui-cli/src/index.ts
index 36129cb1d..d60f26a53 100644
--- a/packages/openui-cli/src/index.ts
+++ b/packages/openui-cli/src/index.ts
@@ -26,11 +26,21 @@ program
.option("-o, --out ", "Write output to a file instead of stdout")
.option("--json-schema", "Output JSON schema instead of the system prompt")
.option("--export ", "Name of the export to use (auto-detected by default)")
+ .option(
+ "--prompt-options ",
+ "Name of the PromptOptions export to use (auto-detected by default)",
+ )
.option("--no-interactive", "Fail with error if required args are missing")
.action(
async (
entry: string | undefined,
- options: { out?: string; jsonSchema?: boolean; export?: string; interactive: boolean },
+ options: {
+ out?: string;
+ jsonSchema?: boolean;
+ export?: string;
+ promptOptions?: string;
+ interactive: boolean;
+ },
) => {
const args = await resolveArgs(
{
diff --git a/packages/openui-cli/src/templates/openui-chat/.gitignore b/packages/openui-cli/src/templates/openui-chat/.gitignore
index 5ef6a5207..aa8b41dec 100644
--- a/packages/openui-cli/src/templates/openui-chat/.gitignore
+++ b/packages/openui-cli/src/templates/openui-chat/.gitignore
@@ -36,6 +36,9 @@ yarn-error.log*
# vercel
.vercel
+# generated
+/src/generated/
+
# typescript
*.tsbuildinfo
next-env.d.ts
diff --git a/packages/openui-cli/src/templates/openui-chat/package.json b/packages/openui-cli/src/templates/openui-chat/package.json
index d043fd702..87f7d36a9 100644
--- a/packages/openui-cli/src/templates/openui-chat/package.json
+++ b/packages/openui-cli/src/templates/openui-chat/package.json
@@ -3,8 +3,9 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "dev": "next dev",
- "build": "next build",
+ "generate:prompt": "openui generate src/library.ts --out src/generated/system-prompt.txt",
+ "dev": "npm run generate:prompt && next dev",
+ "build": "npm run generate:prompt && next build",
"start": "next start",
"lint": "eslint"
},
@@ -18,6 +19,7 @@
"react-dom": "19.2.3"
},
"devDependencies": {
+ "@openuidev/cli": "latest",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
diff --git a/packages/openui-cli/src/templates/openui-chat/src/app/api/chat/route.ts b/packages/openui-cli/src/templates/openui-chat/src/app/api/chat/route.ts
index 404fd0276..3e6c0c9a3 100644
--- a/packages/openui-cli/src/templates/openui-chat/src/app/api/chat/route.ts
+++ b/packages/openui-cli/src/templates/openui-chat/src/app/api/chat/route.ts
@@ -1,19 +1,21 @@
+import { readFileSync } from "fs";
+import { join } from "path";
import { NextRequest } from "next/server";
import OpenAI from "openai";
+const client = new OpenAI();
+const systemPrompt = readFileSync(join(process.cwd(), "src/generated/system-prompt.txt"), "utf-8");
+
export async function POST(req: NextRequest) {
- const client = new OpenAI();
try {
- const { messages, systemPrompt } = await req.json();
-
- const chatMessages: OpenAI.ChatCompletionMessageParam[] = [
- ...(systemPrompt ? [{ role: "system" as const, content: systemPrompt }] : []),
- ...messages,
- ];
+ const { messages } = await req.json();
const response = await client.chat.completions.create({
- model: "gpt-4o",
- messages: chatMessages,
+ model: "gpt-5.2",
+ messages: [
+ { role: "system", content: systemPrompt },
+ ...messages,
+ ],
stream: true,
});
diff --git a/packages/openui-cli/src/templates/openui-chat/src/app/page.tsx b/packages/openui-cli/src/templates/openui-chat/src/app/page.tsx
index b828b1d20..ddfd81526 100644
--- a/packages/openui-cli/src/templates/openui-chat/src/app/page.tsx
+++ b/packages/openui-cli/src/templates/openui-chat/src/app/page.tsx
@@ -4,9 +4,7 @@ import "@openuidev/react-ui/styles/index.css";
import { openAIMessageFormat, openAIReadableStreamAdapter } from "@openuidev/react-headless";
import { FullScreen } from "@openuidev/react-ui";
-import { openuiLibrary, openuiPromptOptions } from "@openuidev/react-ui/genui-lib";
-
-const systemPrompt = openuiLibrary.prompt(openuiPromptOptions);
+import { openuiLibrary } from "@openuidev/react-ui/genui-lib";
export default function Home() {
return (
@@ -18,7 +16,6 @@ export default function Home() {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: openAIMessageFormat.toApi(messages),
- systemPrompt,
}),
signal: abortController.signal,
});
diff --git a/packages/openui-cli/src/templates/openui-chat/src/library.ts b/packages/openui-cli/src/templates/openui-chat/src/library.ts
new file mode 100644
index 000000000..8a2edfa62
--- /dev/null
+++ b/packages/openui-cli/src/templates/openui-chat/src/library.ts
@@ -0,0 +1 @@
+export { openuiLibrary as library, openuiPromptOptions as promptOptions } from "@openuidev/react-ui/genui-lib";
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4d9ad148e..19085f6f6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -44,6 +44,9 @@ importers:
docs:
dependencies:
+ '@openuidev/cli':
+ specifier: workspace:*
+ version: link:../packages/openui-cli
'@openuidev/react-headless':
specifier: workspace:^
version: link:../packages/react-headless
@@ -129,6 +132,9 @@ importers:
examples/openui-chat:
dependencies:
+ '@openuidev/cli':
+ specifier: workspace:*
+ version: link:../../packages/openui-cli
'@openuidev/react-headless':
specifier: workspace:*
version: link:../../packages/react-headless