diff --git a/examples/analytics-demo/.gitignore b/examples/analytics-demo/.gitignore
new file mode 100644
index 000000000..5ef6a5207
--- /dev/null
+++ b/examples/analytics-demo/.gitignore
@@ -0,0 +1,41 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/examples/analytics-demo/README.md b/examples/analytics-demo/README.md
new file mode 100644
index 000000000..e8a266d33
--- /dev/null
+++ b/examples/analytics-demo/README.md
@@ -0,0 +1,202 @@
+# Analytics Demo
+
+A conversational analytics app that streams OpenUI Lang charts, tables, and metric cards using OpenAI and server-side tool execution.
+
+It uses `@openuidev/react-ui`'s FullScreen layout and the built-in `openuiChatLibrary` — no custom component definitions needed — to render charts, tables, and metric cards from live tool data.
+
+The example includes:
+
+- A **Next.js frontend** with a FullScreen chat layout and built-in conversation starters
+- A **Next.js API route** that calls OpenAI with streaming and automatic tool execution via `runTools()`
+- **Four mock analytics tools** with built-in sample data, so nothing external is required
+
+## Architecture
+
+```
+Browser (FullScreen) -- POST /api/chat --> Next.js route --> OpenAI
+ <-- SSE stream -- (OpenUI Lang + tool calls)
+```
+
+The client sends a conversation to `/api/chat`. The API route loads a generated `system-prompt.txt`, uses OpenAI's `runTools()` to handle multi-round tool calling automatically, and streams the response as SSE. On the client side, `openAIAdapter()` parses the stream, and `openuiChatLibrary` maps each node to a chart, table, or metric card that renders progressively as tokens arrive.
+
+## Project layout
+
+```
+examples/analytics-demo/
+|- src/app/ # Next.js app (layout, page, API route)
+|- src/data/ # Built-in sample data
+|- src/tools/ # Analytics tool definitions and implementations
+|- src/generated/ # Generated system prompt
+|- src/library.ts # Re-exports openuiChatLibrary and promptOptions
+```
+
+## Getting Started
+
+1. Install dependencies:
+
+```bash
+cd examples/analytics-demo
+pnpm install
+```
+
+2. Set your OpenAI API key:
+
+```bash
+export OPENAI_API_KEY=your-key-here
+```
+
+3. Start the dev server:
+
+```bash
+pnpm dev
+```
+
+This automatically generates the system prompt from the library definition before starting Next.js.
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+## What to expect
+
+Try one of the built-in conversation starters:
+
+- "Revenue trends" — a line or area chart of monthly revenue, expenses, and profit
+- "Q1 vs Q2 sales" — a grouped bar chart comparing sales by product category across quarters
+- "Key metrics" — metric cards for MRR, ARR, churn rate, NPS, CAC, LTV, and more
+- "Customer segments" — a pie or bar chart breaking down customers by tier or region
+
+All responses are streamed progressively. The assistant picks the right chart type for the data and can combine multiple visualizations (a summary metric, a chart, and a detail table) in a single response card.
+
+## Key files
+
+### `src/app/page.tsx` — FullScreen chat setup
+
+The page wires up the FullScreen layout with the built-in `openuiChatLibrary`:
+
+```tsx
+import { openAIAdapter, openAIMessageFormat } from "@openuidev/react-headless";
+import { FullScreen } from "@openuidev/react-ui";
+import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib";
+
+export default function Page() {
+ return (
+
+ {
+ return fetch("/api/chat", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ messages: openAIMessageFormat.toApi(messages),
+ }),
+ signal: abortController.signal,
+ });
+ }}
+ streamProtocol={openAIAdapter()}
+ componentLibrary={openuiChatLibrary}
+ agentName="Analytics Demo"
+ theme={{ mode: "light" }}
+ conversationStarters={{
+ variant: "short",
+ options: [
+ {
+ displayText: "Revenue trends",
+ prompt: "Show me monthly revenue trends for the past year.",
+ },
+ {
+ displayText: "Q1 vs Q2 sales",
+ prompt: "Compare Q1 vs Q2 sales by product category.",
+ },
+ { displayText: "Key metrics", prompt: "What are our key business metrics right now?" },
+ {
+ displayText: "Customer segments",
+ prompt: "Break down our customer base by segment and show spending patterns.",
+ },
+ ],
+ }}
+ />
+
+ );
+}
+```
+
+Because this example uses the standard `openuiChatLibrary`, there is no custom component library to define. The same library is used by the CLI to generate the system prompt.
+
+### `src/library.ts` — library re-export
+
+The library file re-exports the built-in library and prompt options so the CLI can discover them:
+
+```ts
+export {
+ openuiChatLibrary as library,
+ openuiChatPromptOptions as promptOptions,
+} from "@openuidev/react-ui/genui-lib";
+```
+
+The `generate:prompt` script points the CLI at this file. The exported names `library` and `promptOptions` are the convention the CLI uses for auto-detection.
+
+### `src/app/api/chat/route.ts` — OpenAI streaming with tool execution
+
+The API route uses the OpenAI SDK's `runTools()` to handle multi-round tool calling automatically, streaming content and tool call events as SSE:
+
+```ts
+const runner = (client.chat.completions as any).runTools({
+ model,
+ messages: chatMessages,
+ tools,
+ stream: true,
+});
+
+runner.on("functionToolCall", (fc) => {
+ enqueue(sseToolCallStart(encoder, { id, function: { name: fc.name } }, callIdx));
+});
+
+runner.on("functionToolCallResult", (result) => {
+ enqueue(sseToolCallArgs(encoder, { id: tc.id, function: { arguments: tc.arguments } }, result, resultIdx));
+});
+
+runner.on("chunk", (chunk) => {
+ const delta = chunk.choices?.[0]?.delta;
+ if (delta?.content) enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
+});
+```
+
+`runTools()` handles the full tool-calling loop internally — calling tools, feeding results back to the model, and repeating until a final text response is produced.
+
+### `src/tools/analytics-tools.ts` — tool definitions
+
+Each tool is defined with a JSON Schema description (for the LLM) and a plain async function (executed server-side):
+
+```ts
+{
+ type: "function",
+ function: {
+ name: "query_revenue",
+ description: "Query revenue, expenses, and profit data. Can return monthly or quarterly breakdowns.",
+ parameters: {
+ type: "object",
+ properties: {
+ period: { type: "string", description: "Time period: 'monthly' or 'quarterly'." },
+ },
+ },
+ function: queryRevenue,
+ },
+}
+```
+
+## Tools
+
+The API route defines four server-side tools with built-in sample data:
+
+| Tool | Description |
+| ----------------- | -------------------------------------------------------------------------------------- |
+| `query_revenue` | Monthly or quarterly revenue, expenses, and profit. Includes YoY growth. |
+| `query_sales` | Sales grouped by product category (per quarter), region, or individual product. |
+| `query_metrics` | Key business metrics: MRR, ARR, churn, NPS, conversion rate, CAC, LTV, customer count. |
+| `query_customers` | Customer segmentation by tier (Enterprise, Mid-Market, SMB, Individual) or region. |
+
+No external API keys or data sources are needed. In a production app, replace the mock implementations with real database queries or API calls.
+
+## Learn More
+
+- [OpenUI Documentation](https://openui.com/docs) — learn about OpenUI features and API.
+- [OpenUI GitHub repository](https://github.com/thesysdev/openui) — your feedback and contributions are welcome!
diff --git a/examples/analytics-demo/eslint.config.mjs b/examples/analytics-demo/eslint.config.mjs
new file mode 100644
index 000000000..05e726d1b
--- /dev/null
+++ b/examples/analytics-demo/eslint.config.mjs
@@ -0,0 +1,18 @@
+import { defineConfig, globalIgnores } from "eslint/config";
+import nextVitals from "eslint-config-next/core-web-vitals";
+import nextTs from "eslint-config-next/typescript";
+
+const eslintConfig = defineConfig([
+ ...nextVitals,
+ ...nextTs,
+ // Override default ignores of eslint-config-next.
+ globalIgnores([
+ // Default ignores of eslint-config-next:
+ ".next/**",
+ "out/**",
+ "build/**",
+ "next-env.d.ts",
+ ]),
+]);
+
+export default eslintConfig;
diff --git a/examples/analytics-demo/next.config.ts b/examples/analytics-demo/next.config.ts
new file mode 100644
index 000000000..69512fdcd
--- /dev/null
+++ b/examples/analytics-demo/next.config.ts
@@ -0,0 +1,8 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ turbopack: {},
+ transpilePackages: ["@openuidev/react-ui", "@openuidev/react-headless", "@openuidev/react-lang"],
+};
+
+export default nextConfig;
diff --git a/examples/analytics-demo/package.json b/examples/analytics-demo/package.json
new file mode 100644
index 000000000..c8313b880
--- /dev/null
+++ b/examples/analytics-demo/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "analytics-demo",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "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": {
+ "openai": "^6.22.0",
+ "@openuidev/cli": "workspace:*",
+ "@openuidev/react-headless": "workspace:*",
+ "@openuidev/react-lang": "workspace:*",
+ "@openuidev/react-ui": "workspace:*",
+ "lucide-react": "^0.575.0",
+ "next": "16.1.6",
+ "react": "19.2.3",
+ "react-dom": "19.2.3"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "^4",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "eslint": "^9",
+ "eslint-config-next": "16.1.6",
+ "tailwindcss": "^4",
+ "typescript": "^5"
+ }
+}
diff --git a/examples/analytics-demo/postcss.config.mjs b/examples/analytics-demo/postcss.config.mjs
new file mode 100644
index 000000000..61e36849c
--- /dev/null
+++ b/examples/analytics-demo/postcss.config.mjs
@@ -0,0 +1,7 @@
+const config = {
+ plugins: {
+ "@tailwindcss/postcss": {},
+ },
+};
+
+export default config;
diff --git a/examples/analytics-demo/src/app/api/chat/route.ts b/examples/analytics-demo/src/app/api/chat/route.ts
new file mode 100644
index 000000000..dba5694f5
--- /dev/null
+++ b/examples/analytics-demo/src/app/api/chat/route.ts
@@ -0,0 +1,177 @@
+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";
+
+import { tools } from "@/tools/analytics-tools";
+
+const systemPrompt = readFileSync(join(process.cwd(), "src/generated/system-prompt.txt"), "utf-8");
+
+const analyticsSystemPrompt = `${systemPrompt}
+
+You are an analytics assistant. When users ask analytics questions, use the available tools to fetch data, then present the results using charts, tables, and metric cards in openui-lang.
+
+Guidelines for analytics responses:
+- Use LineChart or AreaChart for time-series data (revenue over months, trends).
+- Use BarChart for comparisons across categories (products, regions, quarters).
+- Use PieChart for proportional breakdowns (market share, segment distribution).
+- Use Table for detailed data with multiple columns.
+- Use TextContent with "large-heavy" size for key metric values.
+- Combine multiple visualizations in a single Card when the data supports it (e.g., a summary metric at the top, chart below, table at the bottom).
+- Use Callout for notable insights or highlights from the data.
+- Always include a title for each visualization.
+`;
+
+// ── SSE helpers ──
+
+function sseToolCallStart(
+ encoder: TextEncoder,
+ tc: { id: string; function: { name: string } },
+ index: number,
+) {
+ return encoder.encode(
+ `data: ${JSON.stringify({
+ id: `chatcmpl-tc-${tc.id}`,
+ object: "chat.completion.chunk",
+ choices: [{
+ index: 0,
+ delta: {
+ tool_calls: [{ index, id: tc.id, type: "function", function: { name: tc.function.name, arguments: "" } }],
+ },
+ finish_reason: null,
+ }],
+ })}\n\n`,
+ );
+}
+
+function sseToolCallArgs(
+ encoder: TextEncoder,
+ tc: { id: string; function: { arguments: string } },
+ result: string,
+ index: number,
+) {
+ let enrichedArgs: string;
+ try {
+ enrichedArgs = JSON.stringify({ _request: JSON.parse(tc.function.arguments), _response: JSON.parse(result) });
+ } catch {
+ enrichedArgs = tc.function.arguments;
+ }
+ return encoder.encode(
+ `data: ${JSON.stringify({
+ id: `chatcmpl-tc-${tc.id}-args`,
+ object: "chat.completion.chunk",
+ choices: [{
+ index: 0,
+ delta: { tool_calls: [{ index, function: { arguments: enrichedArgs } }] },
+ finish_reason: null,
+ }],
+ })}\n\n`,
+ );
+}
+
+// ── Route handler ──
+
+export async function POST(req: NextRequest) {
+ const { messages } = await req.json();
+
+ const client = new OpenAI({
+ apiKey: process.env.OPENAI_API_KEY,
+ });
+ const model = process.env.OPENAI_MODEL ?? "gpt-4.1";
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const cleanMessages = (messages as any[])
+ .filter((m) => m.role !== "tool")
+ .map((m) => {
+ if (m.role === "assistant" && m.tool_calls?.length) {
+ const { tool_calls: _tc, ...rest } = m; // eslint-disable-line @typescript-eslint/no-unused-vars
+ return rest;
+ }
+ return m;
+ });
+
+ const chatMessages: ChatCompletionMessageParam[] = [
+ { role: "system", content: analyticsSystemPrompt },
+ ...cleanMessages,
+ ];
+
+ const encoder = new TextEncoder();
+ let controllerClosed = false;
+
+ const readable = new ReadableStream({
+ start(controller) {
+ const enqueue = (data: Uint8Array) => {
+ if (controllerClosed) return;
+ try { controller.enqueue(data); } catch { /* already closed */ }
+ };
+ const close = () => {
+ if (controllerClosed) return;
+ controllerClosed = true;
+ try { controller.close(); } catch { /* already closed */ }
+ };
+
+ const pendingCalls: Array<{ id: string; name: string; arguments: string }> = [];
+ let callIdx = 0;
+ let resultIdx = 0;
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const runner = (client.chat.completions as any).runTools({
+ model,
+ messages: chatMessages,
+ tools,
+ stream: true,
+ });
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ runner.on("functionToolCall", (fc: any) => {
+ const id = `tc-${callIdx}`;
+ pendingCalls.push({ id, name: fc.name, arguments: fc.arguments });
+ enqueue(sseToolCallStart(encoder, { id, function: { name: fc.name } }, callIdx));
+ callIdx++;
+ });
+
+ runner.on("functionToolCallResult", (result: string) => {
+ const tc = pendingCalls[resultIdx];
+ if (tc) {
+ enqueue(sseToolCallArgs(encoder, { id: tc.id, function: { arguments: tc.arguments } }, result, resultIdx));
+ }
+ resultIdx++;
+ });
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ runner.on("chunk", (chunk: any) => {
+ const choice = chunk.choices?.[0];
+ const delta = choice?.delta;
+ if (!delta) return;
+ if (delta.content) {
+ enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
+ }
+ if (choice?.finish_reason === "stop") {
+ enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
+ }
+ });
+
+ runner.on("end", () => {
+ enqueue(encoder.encode("data: [DONE]\n\n"));
+ close();
+ });
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ runner.on("error", (err: any) => {
+ const msg = err instanceof Error ? err.message : "Stream error";
+ console.error("Chat route error:", msg);
+ enqueue(encoder.encode(`data: ${JSON.stringify({ error: msg })}\n\n`));
+ close();
+ });
+ },
+ });
+
+ return new Response(readable, {
+ headers: {
+ "Content-Type": "text/event-stream",
+ "Cache-Control": "no-cache, no-transform",
+ Connection: "keep-alive",
+ },
+ });
+}
diff --git a/examples/analytics-demo/src/app/globals.css b/examples/analytics-demo/src/app/globals.css
new file mode 100644
index 000000000..f1d8c73cd
--- /dev/null
+++ b/examples/analytics-demo/src/app/globals.css
@@ -0,0 +1 @@
+@import "tailwindcss";
diff --git a/examples/analytics-demo/src/app/layout.tsx b/examples/analytics-demo/src/app/layout.tsx
new file mode 100644
index 000000000..e188db401
--- /dev/null
+++ b/examples/analytics-demo/src/app/layout.tsx
@@ -0,0 +1,19 @@
+import type { Metadata } from "next";
+import "./globals.css";
+
+export const metadata: Metadata = {
+ title: "Analytics Demo",
+ description: "Conversational Analytics with OpenUI",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/examples/analytics-demo/src/app/page.tsx b/examples/analytics-demo/src/app/page.tsx
new file mode 100644
index 000000000..e0075803d
--- /dev/null
+++ b/examples/analytics-demo/src/app/page.tsx
@@ -0,0 +1,50 @@
+"use client";
+import "@openuidev/react-ui/components.css";
+
+import { openAIAdapter, openAIMessageFormat } from "@openuidev/react-headless";
+import { FullScreen } from "@openuidev/react-ui";
+import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib";
+
+export default function Page() {
+ return (
+
+ {
+ return fetch("/api/chat", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ messages: openAIMessageFormat.toApi(messages),
+ }),
+ signal: abortController.signal,
+ });
+ }}
+ streamProtocol={openAIAdapter()}
+ componentLibrary={openuiChatLibrary}
+ agentName="Analytics Demo"
+ theme={{ mode: "light" }}
+ conversationStarters={{
+ variant: "short",
+ options: [
+ {
+ displayText: "Revenue trends",
+ prompt: "Show me monthly revenue trends for the past year.",
+ },
+ {
+ displayText: "Q1 vs Q2 sales",
+ prompt: "Compare Q1 vs Q2 sales by product category.",
+ },
+ {
+ displayText: "Key metrics",
+ prompt: "What are our key business metrics right now?",
+ },
+ {
+ displayText: "Customer segments",
+ prompt: "Break down our customer base by segment and show spending patterns.",
+ },
+ ],
+ }}
+ />
+
+ );
+}
diff --git a/examples/analytics-demo/src/data/sample-data.ts b/examples/analytics-demo/src/data/sample-data.ts
new file mode 100644
index 000000000..769666a11
--- /dev/null
+++ b/examples/analytics-demo/src/data/sample-data.ts
@@ -0,0 +1,75 @@
+export const monthlyRevenue = [
+ { month: "Jan", revenue: 124000, expenses: 89000, profit: 35000 },
+ { month: "Feb", revenue: 131000, expenses: 92000, profit: 39000 },
+ { month: "Mar", revenue: 148000, expenses: 95000, profit: 53000 },
+ { month: "Apr", revenue: 142000, expenses: 91000, profit: 51000 },
+ { month: "May", revenue: 156000, expenses: 98000, profit: 58000 },
+ { month: "Jun", revenue: 169000, expenses: 102000, profit: 67000 },
+ { month: "Jul", revenue: 172000, expenses: 105000, profit: 67000 },
+ { month: "Aug", revenue: 165000, expenses: 101000, profit: 64000 },
+ { month: "Sep", revenue: 178000, expenses: 108000, profit: 70000 },
+ { month: "Oct", revenue: 189000, expenses: 112000, profit: 77000 },
+ { month: "Nov", revenue: 201000, expenses: 118000, profit: 83000 },
+ { month: "Dec", revenue: 215000, expenses: 125000, profit: 90000 },
+];
+
+export const quarterlyRevenue = [
+ { quarter: "Q1", revenue: 403000, expenses: 276000, profit: 127000 },
+ { quarter: "Q2", revenue: 467000, expenses: 291000, profit: 176000 },
+ { quarter: "Q3", revenue: 515000, expenses: 314000, profit: 201000 },
+ { quarter: "Q4", revenue: 605000, expenses: 355000, profit: 250000 },
+];
+
+export const salesByCategory = {
+ Q1: { Electronics: 245000, Clothing: 132000, "Home & Garden": 98000, Sports: 67000, Books: 41000 },
+ Q2: { Electronics: 278000, Clothing: 145000, "Home & Garden": 112000, Sports: 89000, Books: 38000 },
+ Q3: { Electronics: 312000, Clothing: 158000, "Home & Garden": 105000, Sports: 95000, Books: 45000 },
+ Q4: { Electronics: 356000, Clothing: 189000, "Home & Garden": 125000, Sports: 72000, Books: 52000 },
+};
+
+export const customerSegments = [
+ { segment: "Enterprise", count: 142, avgSpend: 28500, retention: 94, growth: 12 },
+ { segment: "Mid-Market", count: 387, avgSpend: 8200, retention: 87, growth: 18 },
+ { segment: "Small Business", count: 1243, avgSpend: 2100, retention: 78, growth: 25 },
+ { segment: "Individual", count: 4521, avgSpend: 340, retention: 65, growth: 31 },
+];
+
+export const topProducts = [
+ { name: "Pro Analytics Suite", revenue: 892000, units: 1240, growth: 23 },
+ { name: "Cloud Dashboard", revenue: 654000, units: 3420, growth: 45 },
+ { name: "Data Connector Pro", revenue: 521000, units: 890, growth: 12 },
+ { name: "Report Builder", revenue: 445000, units: 2100, growth: 34 },
+ { name: "API Gateway", revenue: 389000, units: 670, growth: 8 },
+ { name: "Mobile Analytics", revenue: 312000, units: 4500, growth: 67 },
+ { name: "Alert Manager", revenue: 278000, units: 1890, growth: 19 },
+ { name: "Custom Widgets", revenue: 234000, units: 3200, growth: 28 },
+];
+
+export const regionData = [
+ { region: "North America", revenue: 1250000, customers: 2840, marketShare: 38 },
+ { region: "Europe", revenue: 890000, customers: 1920, marketShare: 27 },
+ { region: "Asia Pacific", revenue: 650000, customers: 1450, marketShare: 20 },
+ { region: "Latin America", revenue: 310000, customers: 680, marketShare: 9 },
+ { region: "Middle East & Africa", revenue: 200000, customers: 403, marketShare: 6 },
+];
+
+export const businessMetrics = {
+ metrics: {
+ mrr: 165800,
+ arr: 1990000,
+ customerCount: 6293,
+ churnRate: 4.2,
+ conversionRate: 3.8,
+ avgDealSize: 4850,
+ nps: 72,
+ cac: 320,
+ ltv: 12400,
+ ltvCacRatio: 38.75,
+ },
+ trends: {
+ mrr: { change: 8.2, direction: "up" },
+ churnRate: { change: -0.5, direction: "down" },
+ conversionRate: { change: 0.3, direction: "up" },
+ nps: { change: 4, direction: "up" },
+ },
+};
diff --git a/examples/analytics-demo/src/generated/system-prompt.txt b/examples/analytics-demo/src/generated/system-prompt.txt
new file mode 100644
index 000000000..9a444471c
--- /dev/null
+++ b/examples/analytics-demo/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[], label?: string, 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/analytics-demo/src/library.ts b/examples/analytics-demo/src/library.ts
new file mode 100644
index 000000000..c7ceecfc1
--- /dev/null
+++ b/examples/analytics-demo/src/library.ts
@@ -0,0 +1 @@
+export { openuiChatLibrary as library, openuiChatPromptOptions as promptOptions } from "@openuidev/react-ui/genui-lib";
diff --git a/examples/analytics-demo/src/tools/analytics-tools.ts b/examples/analytics-demo/src/tools/analytics-tools.ts
new file mode 100644
index 000000000..2d595d55c
--- /dev/null
+++ b/examples/analytics-demo/src/tools/analytics-tools.ts
@@ -0,0 +1,151 @@
+import {
+ businessMetrics,
+ customerSegments,
+ monthlyRevenue,
+ quarterlyRevenue,
+ regionData,
+ salesByCategory,
+ topProducts,
+} from "@/data/sample-data";
+
+// ── Tool implementations ──
+
+function queryRevenue({ period }: { period?: string }): Promise {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ if (period && (period.toLowerCase().includes("q") || period.toLowerCase().includes("quarter"))) {
+ resolve(JSON.stringify({
+ period: "quarterly",
+ data: quarterlyRevenue,
+ totalRevenue: 1990000,
+ totalProfit: 754000,
+ yoyGrowth: 18.5,
+ }));
+ } else {
+ resolve(JSON.stringify({
+ period: "monthly",
+ data: monthlyRevenue,
+ totalRevenue: monthlyRevenue.reduce((s, m) => s + m.revenue, 0),
+ totalProfit: monthlyRevenue.reduce((s, m) => s + m.profit, 0),
+ yoyGrowth: 18.5,
+ }));
+ }
+ }, 600);
+ });
+}
+
+function querySales({ groupBy }: { groupBy?: string }): Promise {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ if (groupBy?.toLowerCase() === "region") {
+ resolve(JSON.stringify({ groupBy: "region", data: regionData }));
+ } else if (groupBy?.toLowerCase() === "product") {
+ resolve(JSON.stringify({ groupBy: "product", data: topProducts }));
+ } else {
+ resolve(JSON.stringify({ groupBy: "category_by_quarter", data: salesByCategory }));
+ }
+ }, 500);
+ });
+}
+
+function queryMetrics(): Promise {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(JSON.stringify(businessMetrics));
+ }, 400);
+ });
+}
+
+function queryCustomers({ segmentBy }: { segmentBy?: string }): Promise {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ if (segmentBy?.toLowerCase() === "region") {
+ resolve(JSON.stringify({ segmentBy: "region", data: regionData }));
+ } else {
+ resolve(JSON.stringify({
+ segmentBy: "tier",
+ data: customerSegments,
+ totalCustomers: customerSegments.reduce((s, c) => s + c.count, 0),
+ avgRetention: 81,
+ }));
+ }
+ }, 500);
+ });
+}
+
+// ── Tool definitions ──
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const tools: any[] = [
+ {
+ type: "function",
+ function: {
+ name: "query_revenue",
+ description: "Query revenue, expenses, and profit data. Can return monthly or quarterly breakdowns.",
+ parameters: {
+ type: "object",
+ properties: {
+ period: {
+ type: "string",
+ description: "Time period granularity: 'monthly' or 'quarterly'. Defaults to monthly.",
+ },
+ },
+ required: [],
+ },
+ function: queryRevenue,
+ parse: JSON.parse,
+ },
+ },
+ {
+ type: "function",
+ function: {
+ name: "query_sales",
+ description: "Query sales data. Can be grouped by 'category' (per quarter), 'region', or 'product'.",
+ parameters: {
+ type: "object",
+ properties: {
+ groupBy: {
+ type: "string",
+ description: "How to group results: 'category', 'region', or 'product'. Defaults to category by quarter.",
+ },
+ },
+ required: [],
+ },
+ function: querySales,
+ parse: JSON.parse,
+ },
+ },
+ {
+ type: "function",
+ function: {
+ name: "query_metrics",
+ description: "Get current key business metrics including MRR, ARR, churn rate, conversion rate, NPS, CAC, LTV, and customer count.",
+ parameters: {
+ type: "object",
+ properties: {},
+ required: [],
+ },
+ function: queryMetrics,
+ parse: JSON.parse,
+ },
+ },
+ {
+ type: "function",
+ function: {
+ name: "query_customers",
+ description: "Query customer data segmented by 'tier' (Enterprise, Mid-Market, Small Business, Individual) or by 'region'.",
+ parameters: {
+ type: "object",
+ properties: {
+ segmentBy: {
+ type: "string",
+ description: "Segment customers by 'tier' or 'region'. Defaults to tier.",
+ },
+ },
+ required: [],
+ },
+ function: queryCustomers,
+ parse: JSON.parse,
+ },
+ },
+];
diff --git a/examples/analytics-demo/tsconfig.json b/examples/analytics-demo/tsconfig.json
new file mode 100644
index 000000000..cf9c65d3e
--- /dev/null
+++ b/examples/analytics-demo/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts",
+ "**/*.mts"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 60c96deeb..98acaef86 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -64,7 +64,7 @@ importers:
version: 16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6)
fumadocs-mdx:
specifier: 14.2.8
- version: 14.2.8(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react@19.2.4)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(sass@1.89.2))
+ version: 14.2.8(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react@19.2.4)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.89.2)(terser@5.43.0)(tsx@4.20.3)(yaml@2.8.0))
fumadocs-ui:
specifier: 16.6.5
version: 16.6.5(@takumi-rs/image-response@0.68.17)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.2.1)
@@ -130,6 +130,61 @@ importers:
specifier: ^4.1.18
version: 4.2.1
+ examples/analytics-demo:
+ dependencies:
+ '@openuidev/cli':
+ specifier: workspace:*
+ version: link:../../packages/openui-cli
+ '@openuidev/react-headless':
+ specifier: workspace:*
+ version: link:../../packages/react-headless
+ '@openuidev/react-lang':
+ specifier: workspace:*
+ version: link:../../packages/react-lang
+ '@openuidev/react-ui':
+ specifier: workspace:*
+ version: link:../../packages/react-ui
+ lucide-react:
+ specifier: ^0.575.0
+ version: 0.575.0(react@19.2.3)
+ next:
+ specifier: 16.1.6
+ version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.89.2)
+ openai:
+ specifier: ^4.104.0
+ version: 4.104.0(ws@8.18.2)(zod@3.25.67)
+ react:
+ specifier: 19.2.3
+ version: 19.2.3
+ react-dom:
+ specifier: 19.2.3
+ version: 19.2.3(react@19.2.3)
+ devDependencies:
+ '@tailwindcss/postcss':
+ specifier: ^4
+ version: 4.2.1
+ '@types/node':
+ specifier: ^20
+ version: 20.19.35
+ '@types/react':
+ specifier: ^19
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: ^19
+ version: 19.2.3(@types/react@19.2.14)
+ eslint:
+ specifier: ^9
+ version: 9.29.0(jiti@2.6.1)
+ eslint-config-next:
+ specifier: 16.1.6
+ version: 16.1.6(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3)
+ tailwindcss:
+ specifier: ^4
+ version: 4.2.1
+ typescript:
+ specifier: ^5
+ version: 5.9.3
+
examples/openui-chat:
dependencies:
'@openuidev/cli':
@@ -15868,7 +15923,7 @@ snapshots:
eslint: 10.0.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@10.0.2(jiti@2.6.1))
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.2(jiti@2.6.1))
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@10.0.2(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@10.0.2(jiti@2.6.1))
eslint-plugin-react-hooks: 7.0.1(eslint@10.0.2(jiti@2.6.1))
@@ -15910,7 +15965,7 @@ snapshots:
dependencies:
debug: 3.2.7
is-core-module: 2.16.1
- resolve: 1.22.10
+ resolve: 1.22.11
transitivePeerDependencies:
- supports-color
@@ -15925,7 +15980,7 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.2(jiti@2.6.1))
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
@@ -15944,7 +15999,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1)):
+ eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.2(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -15955,7 +16010,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)):
+ eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -15966,7 +16021,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.2(jiti@2.6.1)):
+ eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -15977,7 +16032,7 @@ snapshots:
doctrine: 2.1.0
eslint: 10.0.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1))
+ eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.2(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -16006,7 +16061,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.29.0(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1))
+ eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -16635,7 +16690,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- fumadocs-mdx@14.2.8(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react@19.2.4)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(sass@1.89.2)):
+ fumadocs-mdx@14.2.8(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react@19.2.4)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.89.2)(terser@5.43.0)(tsx@4.20.3)(yaml@2.8.0)):
dependencies:
'@mdx-js/mdx': 3.1.1
'@standard-schema/spec': 1.1.0
@@ -18583,7 +18638,7 @@ snapshots:
'@next/env': 16.1.6
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.0
- caniuse-lite: 1.0.30001778
+ caniuse-lite: 1.0.30001723
postcss: 8.4.31
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -18610,7 +18665,7 @@ snapshots:
'@next/env': 16.1.6
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.0
- caniuse-lite: 1.0.30001778
+ caniuse-lite: 1.0.30001723
postcss: 8.4.31
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
@@ -18771,6 +18826,21 @@ snapshots:
is-docker: 2.2.1
is-wsl: 2.2.0
+ openai@4.104.0(ws@8.18.2)(zod@3.25.67):
+ dependencies:
+ '@types/node': 18.19.130
+ '@types/node-fetch': 2.6.11
+ abort-controller: 3.0.0
+ agentkeepalive: 4.6.0
+ form-data-encoder: 1.7.2
+ formdata-node: 4.4.1
+ node-fetch: 2.7.0
+ optionalDependencies:
+ ws: 8.18.2
+ zod: 3.25.67
+ transitivePeerDependencies:
+ - encoding
+
openai@4.104.0(ws@8.18.2)(zod@4.3.6):
dependencies:
'@types/node': 18.19.130