Tony openui-dashboard
Generate dashboard UIs using the OpenUI spec (openui.com) with a React + DuckDB backend. Use this skill when building dashboards, data visualization UIs, chat interfaces, or any LLM-driven UI generation. Includes pre-built chat session support for TheGardener.
git clone https://github.com/jaydeland/Tony
T=$(mktemp -d) && git clone --depth=1 https://github.com/jaydeland/Tony "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/openui-dashboard" ~/.claude/skills/jaydeland-tony-openui-dashboard && rm -rf "$T"
.claude/skills/openui-dashboard/skill.mdOpenUI Dashboard Generation Skill
This skill generates dashboard UIs by combining the OpenUI spec (openui.com / github.com/thesysdev/openui) with a React frontend and DuckDB backend. The workflow is:
DuckDB (local) → Orchestrator Agent → OpenUI Lang output → React Renderer → Dashboard
The orchestrator agent (or any LLM) is given a component library and outputs OpenUI Lang — a code-like streaming syntax — instead of raw JSX or HTML. The React runtime parses and renders it progressively.
Step 1 — Understand OpenUI Core Concepts
What OpenUI Is
OpenUI is a rendering specification for LLM-generated interfaces. It has three layers:
| Layer | What it does |
|---|---|
| OpenUI Lang | A compact code-like syntax LLMs output (not JSON — ~67% fewer tokens) |
| Parser + renderer — converts streaming OpenUI Lang into React components |
| Prebuilt chat UI + component library (charts, tables, forms) |
OpenUI Lang Syntax (Basics)
root = Stack([title, StatCard(label="Agents", value="3")]) title = TextContent("Dashboard")
- No JSON — syntax resembles Python/TypeScript function calls
- Positional args — key order from Zod schema determines arg order
- Streaming-first — root renders immediately; children stream in
- Root constraint — output always starts with the library's declared root component
Defining Components
import { defineComponent, createLibrary } from "@openuidev/react-lang"; import { z } from "zod"; // 1. Define each component with Zod props schema const StatCard = defineComponent({ name: "StatCard", // used in OpenUI Lang output description: "Displays a metric label and value.", // seen by the LLM props: z.object({ label: z.string(), value: z.string(), }), component: ({ props }) => ( <div className="stat-card"> <strong>{props.label}</strong> <div className="value">{props.value}</div> </div> ), }); // 2. For nested children, use .ref const Item = defineComponent({ name: "Item", props: z.object({ label: z.string() }), component: ({ props }) => <div>{props.label}</div>, }); const List = defineComponent({ name: "List", props: z.object({ items: z.array(Item.ref) }), // .ref enables nesting component: ({ props, renderNode }) => ( <div>{renderNode(props.items)}</div> // renderNode handles children ), }); // 3. Compose into a library export const dashboardLibrary = createLibrary({ root: "Stack", // entry point — output always wrapped in Stack components: [StatCard, Item, List], componentGroups: [ { name: "Stats", components: ["StatCard"] }, { name: "Layout", components: ["Stack", "List"] }, ], });
Generating the System Prompt
import { dashboardLibrary, openuiPromptOptions } from "./dashboard-library"; // Generate the instruction text the LLM needs const systemPrompt = dashboardLibrary.prompt({ preamble: "You are a dashboard generator. Output only OpenUI Lang.", additionalRules: ["Always wrap the root in Stack.", "Use StatCard for metrics."], examples: [ `root = Stack([StatCard(label="Active Agents", value="4")])`, ], });
Streaming Renderer (React)
import { OpenUIStreamRenderer } from "@openuidev/react-lang"; function DashboardPage() { return ( <OpenUIStreamRenderer library={dashboardLibrary} onError={(err) => console.error("Parse error:", err)} > {/* children receive the streamed output as it's parsed */} </OpenUIStreamRenderer> ); }
Step 2 — DuckDB Integration Pattern
DuckDB runs locally. The orchestrator agent queries it, then passes results to the LLM for visualization.
Python: Query DuckDB + Format for OpenUI
import duckdb def query_dashboard_data(sql: str) -> dict: """Execute DuckDB query, return results as dict for OpenUI generation.""" conn = duckdb.connect("openseed.db") result = conn.execute(sql).fetchdf() conn.close() return { "columns": result.columns.tolist(), "rows": result.to_dict(orient="records"), "row_count": len(result), } # Example: get agent run stats stats = query_dashboard_data(""" SELECT agent_name, COUNT(*) as runs, SUM(cost_usd) as total_cost, MAX(started_at) as last_run FROM runs GROUP BY agent_name """) # stats → {"columns": ["agent_name", "runs", ...], "rows": [...], "row_count": N}
What to Pass to the LLM
After querying DuckDB, give the LLM:
- Component library definition (the
+defineComponent
code above)createLibrary - DuckDB results (as JSON — columns, rows)
- Task prompt e.g., "Generate a dashboard showing agent activity from this data"
Step 3 — Dashboard Component Library Template
Here is a ready-to-use OpenUI component library for OpenSeed-style dashboards. It covers the 7 DB tables from the OpenSeed schema.
// dashboard-library.ts import { defineComponent, createLibrary } from "@openuidev/react-lang"; import { z } from "zod"; // ── Layout Components ──────────────────────────────────────────────────────── const Stack = defineComponent({ name: "Stack", description: "Vertical stack layout — root container for dashboard sections.", props: z.object({ children: z.array(z.any()).optional(), }), component: ({ props, renderNode }) => ( <div className="flex flex-col gap-4 p-4"> {props.children ? renderNode(props.children) : null} </div> ), }); // ── Stat / Metric Components ──────────────────────────────────────────────── const StatCard = defineComponent({ name: "StatCard", description: "Single metric tile — label + large value + optional delta.", props: z.object({ label: z.string(), value: z.string(), delta: z.string().optional(), delta_positive: z.boolean().optional(), }), component: ({ props }) => ( <div className="bg-white rounded shadow p-4 border"> <div className="text-gray-500 text-sm">{props.label}</div> <div className="text-2xl font-bold mt-1">{props.value}</div> {props.delta && ( <div className={`text-xs mt-1 ${props.delta_positive ? "text-green-600" : "text-red-600"}`}> {props.delta} </div> )} </div> ), }); const StatGrid = defineComponent({ name: "StatGrid", description: "Responsive grid of StatCard components.", props: z.object({ items: z.array(StatCard.ref), }), component: ({ props, renderNode }) => ( <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> {renderNode(props.items)} </div> ), }); // ── Table Component ────────────────────────────────────────────────────────── const DataTable = defineComponent({ name: "DataTable", description: "Sortable table with column headers and row data.", props: z.object({ columns: z.array(z.string()), rows: z.array(z.record(z.string(), z.any())), }), component: ({ props }) => ( <div className="overflow-x-auto bg-white rounded shadow"> <table className="min-w-full text-sm"> <thead className="bg-gray-50 border-b"> <tr> {props.columns.map((col) => ( <th key={col} className="px-4 py-2 text-left font-medium text-gray-600"> {col} </th> ))} </tr> </thead> <tbody> {props.rows.map((row, i) => ( <tr key={i} className="border-b hover:bg-gray-50"> {props.columns.map((col) => ( <td key={col} className="px-4 py-2">{String(row[col] ?? "—")}</td> ))} </tr> ))} </tbody> </table> </div> ), }); // ── Chart Component (Bar) ─────────────────────────────────────────────────── const BarChart = defineComponent({ name: "BarChart", description: "Horizontal bar chart for categorical data.", props: z.object({ title: z.string(), data: z.array(z.object({ label: z.string(), value: z.number() })), }), component: ({ props }) => { const max = Math.max(...props.data.map((d) => d.value)); return ( <div className="bg-white rounded shadow p-4"> <div className="font-medium mb-3">{props.title}</div> <div className="flex flex-col gap-2"> {props.data.map((item) => ( <div key={item.label} className="flex items-center gap-2"> <div className="w-24 text-sm truncate">{item.label}</div> <div className="flex-1 bg-gray-100 rounded h-5 overflow-hidden"> <div className="bg-blue-500 h-full rounded" style={{ width: `${(item.value / max) * 100}%` }} /> </div> <div className="text-sm w-12 text-right">{item.value}</div> </div> ))} </div> </div> ); }, }); // ── Status Badge ───────────────────────────────────────────────────────────── const StatusBadge = defineComponent({ name: "StatusBadge", description: "Colored badge for status labels (active, pending, error, done).", props: z.object({ label: z.string(), status: z.enum(["active", "pending", "error", "done", "running"]), }), component: ({ props }) => { const colors = { active: "bg-green-100 text-green-800", pending: "bg-yellow-100 text-yellow-800", error: "bg-red-100 text-red-800", done: "bg-gray-100 text-gray-800", running: "bg-blue-100 text-blue-800", }; return ( <span className={`px-2 py-1 rounded text-xs font-medium ${colors[props.status]}`}> {props.label} </span> ); }, }); // ── Timeline / Activity Feed ──────────────────────────────────────────────── const ActivityFeed = defineComponent({ name: "ActivityFeed", description: "Vertical timeline of events with timestamps.", props: z.object({ items: z.array(z.object({ time: z.string(), agent: z.string(), action: z.string(), status: z.enum(["active", "pending", "error", "done", "running"]).optional(), })), }), component: ({ props }) => ( <div className="bg-white rounded shadow divide-y"> {props.items.map((item, i) => ( <div key={i} className="p-3 flex items-start gap-3"> <div className="mt-1">{item.status && <StatusBadge label={item.status} status={item.status} />}</div> <div className="flex-1 min-w-0"> <div className="text-sm font-medium">{item.agent}</div> <div className="text-sm text-gray-600">{item.action}</div> </div> <div className="text-xs text-gray-400 whitespace-nowrap">{item.time}</div> </div> ))} </div> ), }); // ── Gate Card ──────────────────────────────────────────────────────────────── const GateCard = defineComponent({ name: "GateCard", description: "Gate approval request with action description and context.", props: z.object({ id: z.string(), action: z.string(), description: z.string(), created_at: z.string(), context: z.record(z.string(), z.any()).optional(), }), component: ({ props }) => ( <div className="bg-yellow-50 border border-yellow-200 rounded p-4"> <div className="flex items-center justify-between mb-2"> <span className="font-bold text-yellow-800">{props.action}</span> <StatusBadge label="PENDING" status="pending" /> </div> <p className="text-sm text-gray-700 mb-2">{props.description}</p> <div className="text-xs text-gray-400">Created: {props.created_at}</div> </div> ), }); // ── Library Export ─────────────────────────────────────────────────────────── export const dashboardLibrary = createLibrary({ root: "Stack", components: [ Stack, StatCard, StatGrid, DataTable, BarChart, StatusBadge, ActivityFeed, GateCard, ], componentGroups: [ { name: "Stats", components: ["StatCard", "StatGrid"], notes: ["Use StatCard for single metrics.", "Use StatGrid for multiple related metrics."], }, { name: "Data Display", components: ["DataTable", "BarChart", "ActivityFeed"], notes: ["DataTable for tabular data.", "BarChart for categorical comparisons."], }, { name: "Status", components: ["StatusBadge"], notes: ["Use StatusBadge inside other components, not as root."], }, { name: "Gates", components: ["GateCard"], notes: ["GateCard for pending approval items."], }, ], });
Step 4 — Orchestrator Agent Integration
The orchestrator agent (the Gardener in OpenSeed) generates the OpenUI Lang output. Here is how to wire it up:
Step 3.5 — Chat Session Implementation (NEW)
The dashboard skill now includes full chat session support for TheGardener. This implementation provides:
Chat Server (web/server.py
)
web/server.py# FastAPI chat server with WebSocket streaming from fastapi import FastAPI, WebSocket from web.server import ChatServer, ChatDatabase # Routes: # POST /api/chat/session - Create new chat session # GET /api/chat/sessions - List all sessions # GET /api/chat/{id}/history - Get conversation history # POST /api/chat/{id}/message - Send message, stream SSE response # WS /api/chat/{id}/ws - WebSocket real-time streaming
Chat UI (web/ChatApp.tsx
)
web/ChatApp.tsxReact chat interface with:
- Session sidebar (create/select sessions)
- Message feed with user/assistant styling
- SSE streaming for real-time responses
- WebSocket support for bidirectional communication
Component Library (web/dashboard-library.ts
)
web/dashboard-library.tsPre-built chat components:
- Role-based message stylingChatMessage
- Scrollable message containerChatFeed
- Text input with send buttonChatInput
- Session list itemSessionCard
- Status indicatorStatusBadge
Running the Chat Server
# Terminal 1 - Start chat server cd web pip install -r requirements.txt python server.py # Terminal 2 - Start React dev server cd web npm install npm run dev # Open http://localhost:5173
TheGardener Integration
Chat messages flow through TheGardener's classification system:
- User sends message via chat UI
- Server stores message in
tablechat_message - TheGardener classifies content keywords
- Delegates to appropriate skill agent (gsd-2, obsidian, dashboard, etc.)
- Response streams back via SSE/WebSocket
- Stored in conversation history
Step 4 — Orchestrator Agent Integration (Original)
The orchestrator agent (the Gardener in OpenSeed) generates the OpenUI Lang output. Here is how to wire it up:
System Prompt Fragment
Give the orchestrator this system prompt addition:
You are an OpenUI dashboard generator. When asked to generate a dashboard, you MUST output **only** valid OpenUI Lang syntax. **Rules:** 1. Output starts with `root = ` followed by the root component name 2. Use positional arguments for props (order matches Zod schema key order) 3. String values use double quotes 4. Arrays use square brackets `[]` 5. Children use square bracket notation: `Stack([StatCard(...), DataTable(...)])` **Available components** (from the dashboard library): - Stack — vertical layout container - StatCard — single metric (label, value, optional delta) - StatGrid — grid of StatCard - DataTable — sortable table (columns array, rows array) - BarChart — horizontal bar chart (title, data array) - ActivityFeed — timeline of events - StatusBadge — colored status badge - GateCard — gate approval request **Example output:**
root = Stack([ StatGrid([ StatCard(label="Active Agents", value="3"), StatCard(label="Runs Today", value="12"), StatCard(label="Daily Cost", value="$2.47"), StatCard(label="Pending Gates", value="1"), ]), ActivityFeed([ { time="10:32", agent="coder-1", action="Committed hello.py", status="done" }, { time="10:28", agent="monitor-1", action="Generated dashboard", status="done" }, ]), ])
Python: Orchestrator → OpenUI Lang → React
# openseed/outputs/dashboard.py (sketch) import httpx DASHBOARD_API = "http://localhost:8420" def generate_dashboard_openui(data: dict, model: str = "claude-sonnet-4") -> str: """ Ask the LLM (via OpenAI-compatible API) to generate OpenUI Lang from DuckDB query results + component library. Returns the OpenUI Lang string. """ system_prompt = _load_dashboard_system_prompt() user_prompt = f"Generate a dashboard for this data:\n{_pformat(data)}\n\nOutput only OpenUI Lang." response = httpx.post("https://api.anthropic.com/v1/messages", json={ "model": model, "max_tokens": 4096, "system": system_prompt, "messages": [{"role": "user", "content": user_prompt}], }) return response.json()["content"][0]["text"] def serve_dashboard(openui_lang: str, library_id: str = "dashboard"): """POST OpenUI Lang to the dashboard render endpoint.""" httpx.post(f"{DASHBOARD_API}/api/render", json={ "lang": openui_lang, "library": library_id, }) def dashboard_cycle(db_path: str = "openseed.db"): """Full cycle: query DuckDB → generate OpenUI Lang → push to dashboard.""" from openseed.db import Database db = Database(db_path) # Query key stats stats = db.query(""" SELECT agent_name, COUNT(*) as runs, SUM(cost_usd) as cost FROM runs GROUP BY agent_name """) signals = db.query("SELECT * FROM signals ORDER BY created_at DESC LIMIT 20") gates = db.query("SELECT * FROM gates WHERE status = 'pending'") db.close() # Format data for LLM data = {"stats": stats, "signals": signals, "gates": gates} openui_lang = generate_dashboard_openui(data) serve_dashboard(openui_lang) return openui_lang
React Dashboard Renderer (Frontend)
// DashboardApp.tsx import { useState } from "react"; import { dashboardLibrary } from "./dashboard-library"; async function streamDashboard(openuiLang: string) { const res = await fetch("/api/render", { method: "POST", body: JSON.stringify({ lang: openuiLang }), }); // The response is a streaming text/event-stream of OpenUI Lang tokens // Use @openuidev/react-lang's streaming parser to render progressively return res.body; } export default function DashboardApp() { const [rendered, setRendered] = useState<React.ReactNode>(null); function handleOpenUIStream(stream: ReadableStream) { const reader = stream.getReader(); const decoder = new TextDecoder(); const parser = new OpenUIParser(dashboardLibrary); function pump() { reader.read().then(({ done, value }) => { if (done) return; const text = decoder.decode(value); parser.feed(text); // parse incremental chunk setRendered(parser.render()); // re-render on each parsed statement pump(); }); } pump(); } return ( <div className="min-h-screen bg-gray-50"> {rendered || <div className="p-4 text-gray-500">Loading dashboard...</div>} </div> ); }
Step 5 — Prompt Patterns for Dashboard Generation
Pattern 1: Stats Overview
Generate a stats overview dashboard showing: - Total runs today - Active agents count - Total cost today - Pending signals count Use StatGrid with StatCard for each metric.
Pattern 2: Agent Activity Feed
Show a timeline of recent agent activity: - Filter to last 24 hours - Include agent name, action description, timestamp, status Use ActivityFeed with StatusBadge for each entry.
Pattern 3: Cost Breakdown
Visualize daily cost by agent as a bar chart. Show top 10 agents by cumulative cost. Use BarChart with agent names as labels.
Pattern 4: Gate Queue
Display all pending gates in a stacked layout. Each gate should show: action type, description, created time. Use GateCard for each pending gate.
Pattern 5: Signals Table
Show recent signals in a sortable table with columns: source, type, status, created_at. Use DataTable with the signals data.
Step 6 — DuckDB → Dashboard End-to-End Flow
┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │ DuckDB │────▶│ Orchestrator │────▶│ OpenUI Lang │ │ openseed.db│ │ (Gardener) │ │ (streaming text)│ └─────────────┘ └──────────────┘ └────────┬─────────┘ │ stream ▼ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │ Dashboard │◀────│ React │◀────│ Parser + Render │ │ (browser) │ │ Renderer │ │ @openuidev/lang │ └─────────────┘ └──────────────┘ └──────────────────┘
- DuckDB holds all OpenSeed state (signals, runs, costs, gates)
- Orchestrator queries DuckDB → builds prompt with data + library → calls LLM
- LLM outputs OpenUI Lang — streaming, incremental text
- React renderer parses and renders progressively — no full round-trip needed
Quick Reference
| What | How |
|---|---|
| Define component | |
| Enable children | + |
| Nest in prompt | Use — the schema includes the child component name for LLM discovery |
| Root component | Set in — LLM always wraps output |
| Generate prompt | |
| Group components | array — helps LLM find related components |
| Style components | Use Tailwind classes in the render function |
| Key ordering | Props key order in Zod schema = positional arg order in OpenUI Lang |
| Union children | — for polymorphic children |
| Streaming | OpenUI Lang streams token-by-token; root renders immediately |
See Also
- OpenUI Lang Docs: https://openui.com/docs/openui-lang/system-prompts
- Defining Components: https://openui.com/docs/openui-lang/defining-components
- GitHub: https://github.com/thesysdev/openui
- React SDK:
,@openuidev/react-lang@openuidev/react-ui - OpenUI Blog: https://www.thesys.dev/blogs/openui (Why OpenUI uses code syntax instead of JSON)