Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/publish-npm-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ on:
- react-email
- openui-cli
- svelte-lang
- solid-lang
- vue-lang

jobs:
Expand Down
40 changes: 17 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,12 @@

</div>


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



---



[Docs](https://openui.com) · [Playground](https://www.openui.com/playground) · [Sample Chat App](./examples/openui-chat) · [Discord](https://discord.com/invite/Pbv5PsqUSv) · [Contributing](./CONTRIBUTING.md) · [Code of Conduct](./CODE_OF_CONDUCT.md) · [Security](./SECURITY.md) · [License](./LICENSE)


---

## What is OpenUI
Expand All @@ -43,7 +37,6 @@ At the center of OpenUI is **OpenUI Lang**: a compact, streaming-first language
- **Streaming renderer** — Parse and render model output progressively in React as tokens arrive.
- **Chat and app surfaces** - Use the same foundation for assistants, copilots, and broader interactive product flows.


## Quick Start

```bash
Expand All @@ -62,8 +55,6 @@ What this gives you:
- **Streaming support** - Update the UI progressively as output arrives.
- **Working app foundation** - Start from a ready-to-run example instead of wiring everything manually.



## How it works

Your components define what the model can generate.
Expand All @@ -87,12 +78,13 @@ Try it yourself in the [Playground](https://www.openui.com/playground) — gener

## Packages

| Package | Description |
| :--- | :--- |
| [`@openuidev/react-lang`](./packages/react-lang) | Core runtime — component definitions, parser, renderer, prompt generation |
| [`@openuidev/react-headless`](./packages/react-headless) | Headless chat state, streaming adapters, message format converters |
| [`@openuidev/react-ui`](./packages/react-ui) | Prebuilt chat layouts and two built-in component libraries |
| [`@openuidev/cli`](./packages/openui-cli) | CLI for scaffolding apps and generating system prompts |
| Package | Description |
| :------------------------------------------------------- | :--------------------------------------------------------------------------- |
| [`@openuidev/react-lang`](./packages/react-lang) | Core runtime — component definitions, parser, renderer, prompt generation |
| [`@openuidev/solid-lang`](./packages/solid-lang) | SolidJS runtime — component definitions, parser, renderer, prompt generation |
| [`@openuidev/react-headless`](./packages/react-headless) | Headless chat state, streaming adapters, message format converters |
| [`@openuidev/react-ui`](./packages/react-ui) | Prebuilt chat layouts and two built-in component libraries |
| [`@openuidev/cli`](./packages/openui-cli) | CLI for scaffolding apps and generating system prompts |

```bash
npm install @openuidev/react-lang @openuidev/react-ui
Expand Down Expand Up @@ -133,14 +125,16 @@ Detailed documentation is available at [openui.com](https://openui.com).
```
openui/
├── packages/
│ ├── react-lang/ # Core runtime (parser, renderer, prompt generation)
│ ├── react-lang/ # React runtime (parser, renderer, prompt generation)
│ ├── solid-lang/ # SolidJS runtime (parser, renderer, prompt generation)
│ ├── react-headless/ # Headless chat state & streaming adapters
│ ├── react-ui/ # Prebuilt chat layouts & component libraries
│ └── openui-cli/ # CLI for scaffolding & prompt generation
├── skills/
│ └── openui/ # Claude Code skill for AI-assisted development
├── examples/
│ └── openui-chat/ # Full working example app (Next.js)
│ ├── openui-chat/ # Full working example app (Next.js)
│ └── solid-chat/ # SolidJS chat example with @openuidev/solid-lang
├── docs/ # Documentation site (openui.com)
└── benchmarks/ # Token efficiency benchmarks
```
Expand All @@ -149,32 +143,32 @@ Good places to start:

- [openui.com](https://openui.com) for the full docs
- [`examples/openui-chat`](./examples/openui-chat) for a working app
- [`examples/solid-chat`](./examples/solid-chat) for SolidJS runtime usage
- [`CONTRIBUTING.md`](./CONTRIBUTING.md) if you want to contribute

## Community

- [Discord](https://discord.com/invite/Pbv5PsqUSv) — Ask questions, share what you're building
- [GitHub Issues](https://github.com/thesysdev/openui/issues) — Report bugs or request features


## Contributing

Contributions are welcome. See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for contribution guidelines and ways to get involved.

## Agent Skill

OpenUI ships an [Agent Skill](https://agentskills.io) so AI coding assistants (Claude Code, Codex, Cursor, Copilot, etc.) can help you scaffold, build, and debug Generative UI apps using OpenUI Lang.

### Install

```bash
# With the skills CLI (works across all agents)
npx skills add thesysdev/openui --skill openui

# Manual — copy into your project
cp -r skills/openui .claude/skills/openui
```

The skill covers component library design, OpenUI Lang syntax, system prompt generation, the Renderer, SDK packages, and debugging malformed LLM output.

## License
Expand Down
3 changes: 3 additions & 0 deletions examples/solid-chat/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
OPENAI_BASE_URL=http://localhost:11434/v1
OPENAI_MODEL=llama3.1:8b
OPENAI_API_KEY=ollama
3 changes: 3 additions & 0 deletions examples/solid-chat/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
dist
.env
59 changes: 59 additions & 0 deletions examples/solid-chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# OpenUI Solid Chat

Example chat app for `@openuidev/solid-lang` using a real AI model (default: Ollama/OpenAI-compatible API), Ark UI Field input, and ECharts-based visualizations.

## Goals

- Show how to define OpenUI components for Solid.
- Show how to render OpenUI Lang responses with `Renderer`.
- Show the action loop from components (for example, `Button`) back into the chat flow.

## Run

From the monorepo root:

```bash
pnpm install
cp examples/solid-chat/.env.example examples/solid-chat/.env
pnpm --filter solid-chat generate:prompt
pnpm --filter solid-chat dev
```

Then open `http://localhost:5174`.

## Model Configuration

- Default (`.env.example`) uses local Ollama:

```bash
OPENAI_BASE_URL=http://localhost:11434/v1
OPENAI_MODEL=llama3.1:8b
OPENAI_API_KEY=ollama
```

- To use OpenAI cloud, change to:

```bash
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_MODEL=gpt-4.1
OPENAI_API_KEY=sk-...
```

## Notes

- The `/api/chat` endpoint is implemented directly in `vite.config.ts` via dev middleware (fast demo setup).
- OpenUI components live in `src/components` and are registered in `src/lib/library.tsx`.
- Chat input uses `@ark-ui/solid` (`Field`).
- The `Chart` component uses `echarts` (bar/line/pie/doughnut).
- Form-like rendering components are also available: `InputField`, `TextAreaField`, `SelectField`, `ToggleField`, and `Divider`.
- Form fields support `$state` binding (for example, `InputField("Email", "name@example.com", $email, "email")`) and will write values back into renderer form state.
- `Button` supports both simple action type strings and structured action objects from `Action(...)` expressions.
- The system prompt is generated into `examples/solid-chat/generated/system-prompt.txt` for easier review and iteration.

## Example Prompts

- `Build a weekly SaaS business dashboard with KPIs, revenue trend, and channel mix charts`
- `Create a support operations dashboard with ticket volume, SLA trend, and follow-up actions`
- `Build a release readiness view with milestones, blockers, and next actions`
- `Create an onboarding status page with progress timeline and action buttons`
- `Create an account settings form with profile inputs, notification toggles, and save actions`
136 changes: 136 additions & 0 deletions examples/solid-chat/generated/system-prompt.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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), null, 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). Write `Stack([children], "row", "l")` NOT `Stack([children], direction: "row", gap: "l")` — colon syntax is NOT supported and silently breaks
7. Optional arguments can be omitted from the end
- 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.
Props typed `ActionExpression` accept an Action([@steps...]) expression. See the Action section for available steps (@ToAssistant, @OpenUrl).
Props marked `$binding<type>` accept a `$variable` reference for two-way binding.

TextContent(text: string, tone?: "normal" | "muted" | "strong" | "success" | "warning" | "danger" | "info") — Displays a block of text.
Button(label: string, action?: string | {type: "open_url", url: string} | {type: "continue_conversation", context?: string}, variant?: "primary" | "secondary" | "ghost") — A clickable button.
Chart(title: string, type: "bar" | "line" | "pie" | "doughnut", labels: string[], values: number[], datasetLabel?: string) — Renders a chart.
Badge(label: string, tone?: "neutral" | "success" | "warning" | "danger" | "info") — Small status pill.
KpiTile(label: string, value: string, delta?: string, trend?: "up" | "down" | "neutral") — Compact KPI tile with value and trend.
MetricList(title: string, items: {label: string, value: string}[]) — Two-column labeled metric list.
Timeline(title: string, items: {title: string, detail: string, status?: "done" | "active" | "next"}[]) — Progress timeline with status dots.
InputField(label: string, placeholder?: string, value?: string, type?: "text" | "email" | "password" | "number" | "url") — Single-line text input with label.
TextAreaField(label: string, placeholder?: string, value?: string, rows?: number) — Multi-line text input with label.
SelectField(label: string, options: string[], selected?: string) — Dropdown-style field with selectable options.
ToggleField(label: string, checked?: boolean) — On/off setting toggle with label.
Divider(label?: string) — Visual divider line with optional label.
Card(title: string, children: (TextContent | Button | Chart | Badge | KpiTile | MetricList | Timeline | InputField | TextAreaField | SelectField | ToggleField | Divider)[], subtitle?: string, variant?: "default" | "glass" | "accent", highlight?: string) — Card container with title and children.
Stack(children: (Card | TextContent | Button | Chart | Badge | KpiTile | MetricList | Timeline | InputField | TextAreaField | SelectField | ToggleField | Divider)[]) — Dashboard layout container. Use as root.

## Built-in Functions

Data functions prefixed with `@` to distinguish from components. These are the ONLY functions available — do NOT invent new ones.
Use @-prefixed built-in functions (@Count, @Sum, @Avg, @Min, @Max, @Round) on Query results — do NOT hardcode computed values.

@Count(array) → number — Returns array length
@First(array) → element — Returns first element of array
@Last(array) → element — Returns last element of array
@Sum(numbers[]) → number — Sum of numeric array
@Avg(numbers[]) → number — Average of numeric array
@Min(numbers[]) → number — Minimum value in array
@Max(numbers[]) → number — Maximum value in array
@Sort(array, field, direction?) → sorted array — Sort array by field. Direction: "asc" (default) or "desc"
@Filter(array, field, operator: "==" | "!=" | ">" | "<" | ">=" | "<=" | "contains", value) → filtered array — Filter array by field value
@Round(number, decimals?) → number — Round to N decimal places (default 0)
@Abs(number) → number — Absolute value
@Floor(number) → number — Round down to nearest integer
@Ceil(number) → number — Round up to nearest integer
@Each(array, varName, template) — Evaluate template for each element. varName is the loop variable — use it ONLY inside the template expression (inline). Do NOT create a separate statement for the template.

Builtins compose — output of one is input to the next:
`@Count(@Filter(data.rows, "field", "==", "val"))` for KPIs/chart values, `@Round(@Avg(data.rows.score), 1)`, `@Each(data.rows, "item", Comp(item.field))` for per-item rendering.
Array pluck: `data.rows.field` extracts a field from every row → use with @Sum, @Avg, charts, tables.

IMPORTANT @Each rule: The loop variable (e.g. "item") is ONLY available inside the @Each template expression. Always inline the template — do NOT extract it to a separate statement.
CORRECT: `Col("Actions", @Each(rows, "t", Button("Edit", Action([@Set($id, t.id)]))))`
WRONG: `myBtn = Button("Edit", Action([@Set($id, t.id)]))` then `Col("Actions", @Each(rows, "t", myBtn))` — t is undefined in myBtn.

## Action — Button Behavior

Action([@steps...]) wires button clicks to operations. Steps are @-prefixed built-in actions. Steps execute in order.
Buttons without an explicit Action prop automatically send their label to the assistant (equivalent to Action([@ToAssistant(label)])).

Available steps:
- @ToAssistant("message") — Send a message to the assistant (for conversational buttons like "Tell me more", "Explain this")
- @OpenUrl("https://...") — Navigate to a URL

Example — simple nav:
```
viewBtn = Button("View", Action([@OpenUrl("https://example.com")]))
```

- Action can be assigned to a variable or inlined: Button("Go", onSubmit) and Button("Go", Action([...])) both work

## 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. $variable declarations — state ready for bindings
3. Query statements — defaults resolve immediately so components render with data
4. Component definitions — fill in with data already available
5. 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

User: Build a weekly product status view

root = Stack([title, summary, panel, action])
title = TextContent("Product Delivery Overview", "strong")
summary = TextContent("Current sprint highlights, blockers, and next actions.", "muted")
state = Badge("On Track", "success")
velocity = Chart("Story Points", "bar", ["W1", "W2", "W3", "W4"], [28, 31, 35, 33], "pts")
burn = Chart("Defect Trend", "line", ["W1", "W2", "W3", "W4"], [14, 11, 9, 7], "count")
metrics = MetricList("Team Metrics", [{"label":"Cycle time", "value":"3.2d"}, {"label":"PR review", "value":"6.8h"}, {"label":"Escaped bugs", "value":"2"}])
steps = Timeline("Release Steps", [{"title":"Scope lock", "detail":"Done", "status":"done"}, {"title":"UAT", "detail":"In progress", "status":"active"}, {"title":"Launch", "detail":"Scheduled Friday", "status":"next"}])
panel = Card("Delivery Health", [state, velocity, burn, metrics, steps], "Sprint 18 status", "glass", "Updated")
action = Button("Generate rollback checklist", "continue-conversation", "primary")


## Important Rules
- 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.)

## Final Verification
Before finishing, walk your output and verify:
1. root = Stack(...) is the FIRST line (for optimal streaming).
2. Every referenced name is defined. Every defined name (other than root) is reachable from root.

- Always use Stack as the root component.
- The FIRST line must be exactly a root assignment: root = Stack(...).
- Do not output any statement before root = Stack(...).
- Use Stack only as root. Never place Stack inside Card children.
- Output openui-lang code only. No markdown fences, no explanations, no think blocks.
- All visible text must be English.
- Start directly with openui-lang code. Do not add preface text.
- Keep syntax valid while streaming: no TODO bullets, no pseudo-code, no planning sections.
- Match the requested UI type; do not force dashboard layouts for every request.
- Use references for readability and better streaming behavior.
- Prefer Card subtitle/highlight only when it improves hierarchy.
- Use Badge, MetricList, Timeline, Chart, and KpiTile when relevant.
- Use InputField, TextAreaField, SelectField, ToggleField, and Divider for form or settings UIs.
- Use state bindings in forms when needed (e.g. InputField("Email", "name@example.com", $email, "email")).
- Button action supports both simple action type strings and structured action objects.
- Use positional arguments only.
- Prefer inline composition in Stack/Card to reduce forward-reference errors.
- Never output reasoning or markdown.
12 changes: 12 additions & 0 deletions examples/solid-chat/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenUI Solid Chat</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
26 changes: 26 additions & 0 deletions examples/solid-chat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "solid-chat",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"generate:prompt": "node scripts/generate-prompt.mjs"
},
"dependencies": {
"@ark-ui/solid": "^5.35.0",
"echarts": "^5.6.0",
"@openuidev/lang-core": "workspace:*",
"lucide-solid": "^0.542.0",
"@openuidev/solid-lang": "workspace:*",
"solid-js": "^1.9.9",
"zod": "^4.3.6"
},
"devDependencies": {
"jiti": "^2.6.1",
"typescript": "^5.9.2",
"vite": "^6.4.1",
"vite-plugin-solid": "^2.11.8"
}
}
Loading