diff --git a/.env.example b/.env.example index c669800..dcecf20 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,21 @@ +# Agent settings: place these in apps/agent/.env. +# Use the key for the selected provider; never expose keys to the browser. ANTHROPIC_API_KEY= +OPENAI_API_KEY= -# Claude model — strong models are required for reliable UI generation -# Recommended: claude-fable-5 (default), claude-opus-5-5 -# Fallback: gpt-* names route to OpenAI (requires OPENAI_API_KEY) -LLM_MODEL=claude-fable-5 +# chat-latest follows ChatGPT Instant. gpt-* uses OpenAI; claude-* uses Anthropic. +# Model access/availability must be verified with your provider. +# Blank or unsupported prefixes and missing selected-provider keys fail at startup. +LLM_MODEL=chat-latest + +# Jev selects the renderer and visualization before each new user turn. +TYPESAFE_API_KEY= +JEV_MODEL=jev-latest + +# Frontend settings: place these in apps/app/.env.local. +# LANGGRAPH_DEPLOYMENT_URL defaults to http://localhost:8123. +# LANGGRAPH_DEPLOYMENT_URL=http://localhost:8123 +# MCP_SERVER_URL=http://localhost:3100/mcp # Rate limiting (per IP) — disabled by default RATE_LIMIT_ENABLED=false diff --git a/CLAUDE.md b/CLAUDE.md index 714a420..d41dcb4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,286 +1,44 @@ -# CopilotKit + LangGraph Todo Demo +# Open Generative UI by CopilotKit -## Purpose +This repository builds a general assistant experience around **answers you can interact with**: understand a topic, compare options, or make a useful tool. It is not primarily a todo-list template. Existing todo and form tools are compatibility examples, not the product's organizing concept. -This repository serves as both a **showcase** and **template** for building AI agents with CopilotKit and LangGraph. It demonstrates how CopilotKit can drive interactive UI beyond just chat, using a **collaborative todo list** as the primary example. +## Boundaries and source of truth -**Target audience:** Developers evaluating CopilotKit or starting new projects with AI agents. +- Frontend: `apps/app/`, Next.js and CopilotKit v2. +- Agent: `apps/agent/main.py`, `src/prompt.py`, `src/model.py`, and `skills/`. +- Custom UI: `apps/app/src/components/generative-ui/open-generative-ui/`. +- Validated host callbacks: `apps/app/src/lib/sandbox/`. +- Standalone MCP: `apps/mcp/`; its HTML assembler and legacy bridge are separate from the web app. +- Shared styling: `packages/design-system/`. -## Core Concept +Use the product name in visible copy. Preserve repository URLs, package names, `openGenerativeUI`, `generateSandboxedUi`, activity/event names, and deployment service names unless a task explicitly migrates them. -The todo list demonstrates **agent-driven UI** where: -- The agent can manipulate application state (adding todos, updating status, organizing tasks) -- Users can interact with the same state (editing titles, checking off tasks, deleting todos) -- Both agent and user changes update the same shared state -- The UI reactively updates based on agent state changes +## Agent behavior -This uses CopilotKit's **v2 agent state pattern** where state lives in the agent and syncs to the frontend. +Choose text for direct answers, native components when their schemas fit, and custom UI when a diagram or interaction helps. There is no mandatory acknowledgement/plan/build/narrate sequence. Distinct sections may use multiple tools; do not repeat a successful build merely because it returned "UI generated". -## Architecture +Label sample assumptions and distinguish user inputs, retrieved evidence, and calculated values. `query_data` returns a metadata envelope with bundled sample CSV rows and does not apply its query. There is no built-in live weather or business-data connection. Never imply a mock form authenticates or persists data. -This is a **Turborepo monorepo** with three apps: +Generated controls must work, validate numerical edge cases, support keyboard access and narrow screens, and expose useful errors. Use the user's selected values when sending a deliberate follow-up. Each follow-up is a new turn/output; do not assume a cross-call patch API or persist iframe state through rerenders. -### Repository Structure +## Streaming contract -``` -apps/ -├── app/ # Next.js frontend -│ ├── src/ -│ │ ├── app/ -│ │ │ ├── page.tsx # Main page - wires up all components -│ │ │ └── api/copilotkit/ # CopilotKit API route -│ │ ├── components/ -│ │ │ ├── canvas/ # Todo list UI -│ │ │ │ ├── index.tsx # Canvas container -│ │ │ │ ├── todo-list.tsx # Todo list with columns -│ │ │ │ ├── todo-column.tsx # Column (pending/completed) -│ │ │ │ └── todo-card.tsx # Individual todo card -│ │ │ ├── example-layout/ # Layout: chat + canvas side-by-side -│ │ │ └── generative-ui/ # Example generative UI components -│ │ └── hooks/ -│ │ ├── use-generative-ui-examples.tsx # Example CopilotKit patterns -│ │ └── use-example-suggestions.tsx # Chat suggestions -├── agent/ # LangGraph Python agent -│ ├── main.py # Agent entry point -│ └── src/ -│ ├── todos.py # Todo tools and state schema -│ └── query.py # Example data query tool -└── mcp/ # MCP (Model Context Protocol) integration -``` - -## Key Pattern: Agent State with CopilotKit v2 - -The todo list uses **CopilotKit v2's agent state pattern** where state lives in the agent backend and syncs bidirectionally with the frontend. - -### How It Works - -1. **Agent defines state schema and tools** (Python) - ```python - # apps/agent/src/todos.py - class Todo(TypedDict): - id: str - title: str - description: str - emoji: str - status: Literal["pending", "completed"] - - class AgentState(TypedDict): - todos: list[Todo] - - @tool - def manage_todos(todos: list[Todo], runtime: ToolRuntime) -> Command: - """Manage the current todos.""" - return Command(update={"todos": todos, ...}) - ``` - -2. **Frontend reads from agent state** - ```typescript - // apps/app/src/components/canvas/index.tsx - const { agent } = useAgent(); - - return ( - agent.setState({ todos: updatedTodos })} - isAgentRunning={agent.isRunning} - /> - ); - ``` - -3. **User interactions update agent state** - ```typescript - // User clicks checkbox → frontend calls agent.setState() - const toggleStatus = (todo) => { - const updated = todos.map(t => - t.id === todo.id ? { ...t, status: t.status === "completed" ? "pending" : "completed" } : t - ); - agent.setState({ todos: updated }); - }; - ``` - -4. **Agent can manipulate state via tools** - - The agent calls `manage_todos` tool to update the todo list - - Both user and agent changes update the same `agent.state.todos` - - Frontend automatically re-renders when state changes - -### Why This Pattern? - -- **Single source of truth**: State lives in the agent, not duplicated in frontend -- **Bidirectional sync**: User changes → agent state, Agent changes → UI update -- **Simple**: No need for separate frontend state management -- **Observable**: Agent has full visibility into state changes - -## Implementation Details - -### Agent Backend - -**Agent Definition** (`apps/agent/main.py`): -```python -from langchain.agents import create_agent -from copilotkit import CopilotKitMiddleware -from src.todos import todo_tools, AgentState - -agent = create_agent( - model="claude-fable-5", - tools=[*todo_tools, ...], # manage_todos, get_todos - middleware=[CopilotKitMiddleware()], - state_schema=AgentState, # Defines state shape - system_prompt="You are a helpful assistant..." -) -``` - -**Todo Tools** (`apps/agent/src/todos.py`): -```python -@tool -def manage_todos(todos: list[Todo], runtime: ToolRuntime) -> Command: - """Manage the current todos.""" - # Ensure todos have unique IDs - for todo in todos: - if "id" not in todo or not todo["id"]: - todo["id"] = str(uuid.uuid4()) - - # Update agent state - return Command(update={ - "todos": todos, - "messages": [ToolMessage(...)] - }) - -@tool -def get_todos(runtime: ToolRuntime): - """Get the current todos.""" - return runtime.state.get("todos", []) -``` - -### Frontend - -**Canvas Component** (`apps/app/src/components/canvas/index.tsx`): -```typescript -export function Canvas() { - const { agent } = useAgent(); // CopilotKit v2 hook - - return ( -
- agent.setState({ todos: updatedTodos })} - // React to agent execution - isAgentRunning={agent.isRunning} - /> -
- ); -} -``` - -**Todo List** (`apps/app/src/components/canvas/todo-list.tsx`): -```typescript -export function TodoList({ todos, onUpdate, isAgentRunning }: TodoListProps) { - const toggleStatus = (todo: Todo) => { - const updated = todos.map((t) => - t.id === todo.id - ? { ...t, status: t.status === "completed" ? "pending" : "completed" } - : t - ); - onUpdate(updated); // Calls agent.setState() - }; - - const addTodo = () => { - const newTodo = { id: crypto.randomUUID(), ... }; - onUpdate([...todos, newTodo]); - }; - - return ( -
- - -
- ); -} -``` - -### How State Flows - -1. **User adds/edits todo** → Frontend calls `agent.setState({ todos: [...] })` -2. **Agent state updates** → CopilotKit syncs to backend -3. **Agent observes change** → Can respond via `manage_todos` tool -4. **Agent modifies todos** → Calls `manage_todos` tool -5. **State syncs to frontend** → `agent.state.todos` updates -6. **UI re-renders** → React sees new state and updates display +Emit `initialHeight`, `placeholderMessages`, `css`, `html`, `jsFunctions`, `jsExpressions` in that order. Styles belong in `css`; functions and invocations use their separate channels. Those JavaScript channels run as classic scripts, so dynamic imports with await belong inside async functions, never at top level. -**Key insight**: State lives in the agent, frontend just reads/writes to it via CopilotKit hooks. +Use the existing validated `Websandbox.connection.remote.sendPrompt({ text })` and HTTPS `openLink({ url })` bridge. Do not replace it with unrestricted parent messages. The sandbox has no same-origin storage or fetch access. Preserve preview isolation, final sandbox initialization, and incremental streaming behavior. -## Tech Stack +## Configuration and checks -- **Frontend**: Next.js 16, React 19, TailwindCSS 4 -- **Agent**: LangGraph (Python), Anthropic Claude (Fable 5) -- **CopilotKit**: React hooks for agent integration (v2) -- **Monorepo**: Turborepo with pnpm workspaces -- **Other**: MCP (Model Context Protocol) integration, Recharts for generative UI examples - -## Development - -This is a Turborepo monorepo using pnpm workspaces. +The local default model is `chat-latest` (ChatGPT Instant). `claude-*` requires `ANTHROPIC_API_KEY`; `chat-latest` and `gpt-*` require `OPENAI_API_KEY`. Jev routing requires `TYPESAFE_API_KEY`. Do not invent native model APIs or silently change pinned model IDs. See `render.yaml` for its separate explicit deployment model setting. ```bash -# Install dependencies (all apps) pnpm install - -# Start all apps (app, agent, mcp) -pnpm dev - -# Start individually -pnpm dev:app # Next.js frontend on port 3000 -pnpm dev:agent # LangGraph agent on port 8123 -pnpm dev:mcp # MCP server - -# Build all apps -pnpm build - -# Lint all apps +pnpm test +uv run --directory apps/agent pytest pnpm lint +pnpm build ``` -### Environment Setup - -```bash -# Set Anthropic API key for the agent -echo 'ANTHROPIC_API_KEY=your-key-here' > apps/agent/.env -``` - -## Design Principles - -1. **Simple over complex** - The todo list is intentionally simple and focused -2. **CopilotKit v2 patterns** - Uses modern agent state management -3. **Template-first** - Code is meant to be forked and extended -4. **Showcasing agent-driven UI** - Demonstrates AI manipulating application state beyond chat - -## Future Enhancements - -Possible extensions to demonstrate more CopilotKit capabilities: -- Todo categories/tags/priorities -- Agent-driven task organization (auto-categorize, suggest priorities) -- Due dates and reminders -- Subtasks and dependencies -- Export/import todo lists -- Undo/redo with state history -- Real-time collaboration - ---- - -## Key Takeaways for Developers - -**State Management Pattern**: This app uses CopilotKit v2's agent state pattern where: -- State is defined in the agent backend (Python TypedDict) -- Frontend reads via `agent.state.todos` -- Frontend writes via `agent.setState({ todos: ... })` -- Agent can modify state via tools (`manage_todos`) -- Changes sync bidirectionally automatically - -**When extending this template**: -- Define state schema in the agent (`AgentState`) -- Create tools that manipulate state via `Command(update={...})` -- Use `useAgent()` hook in frontend to read/write state -- Let CopilotKit handle the sync - no manual state management needed +Use focused tests while developing, then relevant workspace checks. Inspect the [launch workflow](docs/deployment.md#verification-and-launch) for browser and live-provider validation. A rendered chat shell does not prove AI generation works. Keep secrets out of source, generated UI, logs, and screenshots. Do not describe a local build as a deployed release. -This pattern works great for **agent-driven applications** where the AI needs to manipulate structured application state, not just chat. +See [Architecture](docs/architecture.md) and [Product and agent behavior](docs/interactive-answers.md). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2c94dd..27fd03b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to Open Intelligent UI +# Contributing to Open Generative UI ⭐ Thank you for your interest in contributing!! @@ -23,18 +23,18 @@ If you find a bug in the source code, you can help us by [submitting an issue](h ## Missing a feature? -So, you've got an awesome feature in mind? Throw it over to us by [creating an issue](https://github.com/CopilotKit/OpenIntelligentUI/issues/new) on our GitHub Repo. +So, you've got an awesome feature in mind? Throw it over to us by [creating an issue](https://github.com/CopilotKit/open-generative-ui/issues/new) on our GitHub Repo. # How do I make a code contribution? ## Step 1: Make a fork -Fork the [OpenIntelligentUI](https://github.com/CopilotKit/OpenIntelligentUI/issues) repository to your GitHub account. This means that you'll have a copy of the repository under _your-GitHub-username/OpenIntelligentUI_. +Fork the [OpenIntelligentUI](https://github.com/CopilotKit/OpenIntelligentUI) repository to your GitHub account. This means that you'll have a copy of the repository under _your-GitHub-username/OpenIntelligentUI_. ## Step 2: Clone the repository to your local machine ```bash -git clone https://github.com//OpenIntelligentUI +git clone https://github.com//OpenGenerativeUI ``` ## Step 3: Prepare the development environment diff --git a/Makefile b/Makefile index 0dc71e9..bc30108 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,8 @@ install: ## Install all dependencies (Node + Python) setup: install ## Full setup: install deps and create .env from template @if [ ! -f apps/agent/.env ]; then \ - echo "ANTHROPIC_API_KEY=your-key-here" > apps/agent/.env; \ - echo "Created apps/agent/.env — add your OpenAI API key"; \ + printf '%s\n' 'OPENAI_API_KEY=' 'TYPESAFE_API_KEY=' 'LLM_MODEL=chat-latest' 'JEV_MODEL=jev-latest' > apps/agent/.env; \ + echo "Created apps/agent/.env — set OPENAI_API_KEY and TYPESAFE_API_KEY"; \ else \ echo "apps/agent/.env already exists, skipping"; \ fi diff --git a/README.md b/README.md index c1040af..be4a3cb 100644 --- a/README.md +++ b/README.md @@ -1,187 +1,205 @@ +
+ # Open Intelligent UI -An open-source showcase for building rich, interactive AI-generated UI with [CopilotKit](https://copilotkit.ai) and [LangChain Deep Agents](https://docs.langchain.com/oss/python/deepagents/overview). Ask the agent to visualize algorithms, create 3D animations, render charts, or generate interactive diagrams — all rendered as live HTML/SVG inside a sandboxed iframe. +### Answers you can interact with. -https://github.com/user-attachments/assets/232cfd7c-9e99-4c6f-814b-5c7e07a87ece +**An open-source chat interface that turns questions into explanations, comparisons, and working tools.** -## What It Does +Built with [CopilotKit](https://github.com/CopilotKit/CopilotKit) and [AG-UI](https://docs.ag-ui.com/introduction). · [Get started](#get-started) · [Overview](#overview) · [Demos](#demo-scenes) · [Architecture](#how-it-works) · [Contributing](CONTRIBUTING.md) -The agent produces **generative UI** — not just text responses, but fully interactive visual components: +[![CI](https://github.com/CopilotKit/OpenIntelligentUI/actions/workflows/ci.yml/badge.svg)](https://github.com/CopilotKit/OpenIntelligentUI/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -- **Algorithm visualizations** — binary search, BFS vs DFS, sorting algorithms -- **3D animations** — interactive WebGL/CSS3D scenes -- **Charts & diagrams** — pie charts, bar charts, network diagrams -- **Interactive widgets** — forms, simulations, math plots +Clone the project, connect your model and visualization router, and adapt the interface and agent to your own workflows. -All visuals are rendered in sandboxed iframes with automatic light/dark theming, progressive reveal animations, and responsive sizing. +[**Building with Open Intelligent UI? Talk to an engineer →**](https://www.copilotkit.ai/talk-to-an-engineer?ref=openintelligentui_readme&utm_source=github&utm_medium=readme&utm_campaign=openintelligentui) -## Quick Start +
-```bash -make setup # Install deps + create .env template -# Edit apps/agent/.env with your real Anthropic API key -make dev # Start all services -``` +--- -> **Strong models required.** Generative UI demands high-capability models that can produce complex, well-structured HTML/SVG in a single pass. The agent runs on Anthropic Claude — `claude-fable-5` by default. Override with `LLM_MODEL` in your `.env`: -> -> | Model | Notes | -> |-------|-------| -> | `claude-fable-5` | Default | -> | `claude-opus-5-5` | Strong alternative | -> -> Setting `LLM_MODEL` to a `gpt-*` name routes to OpenAI instead (requires `OPENAI_API_KEY`). For other providers, swap the chat model in `apps/agent/src/model.py` (see [docs/bring-to-your-app.md](docs/bring-to-your-app.md)). Smaller or weaker models will produce broken layouts, missing interactivity, or incomplete visualizations. - -- **App**: http://localhost:3000 -- **Agent**: http://localhost:8123 - -### Available Commands - -| Command | Description | -|---------|-------------| -| `make setup` | Install all dependencies and create `.env` template | -| `make dev` | Start all services (frontend + agent + mcp) | -| `make dev-app` | Start Next.js frontend only | -| `make dev-agent` | Start LangGraph agent only | -| `make dev-mcp` | Start MCP server only | -| `make build` | Build all apps | -| `make lint` | Lint all apps | -| `make clean` | Clean build artifacts | -| `make help` | Show all available commands | - -You can also use `pnpm` directly (`pnpm dev`, `pnpm dev:app`, `pnpm dev:agent`, etc.). - -## MCP Server (Self-Hosted) - -The repo includes a standalone [Model Context Protocol](https://modelcontextprotocol.io) server that exposes the design system, skill instructions, and an HTML document assembler to any MCP-compatible client — including Claude Desktop, Claude Code, and Cursor. - -### What it provides - -- **`assemble_document` tool** — wraps HTML fragments with the full design system CSS and bridge JS, returning an iframe-ready document -- **Skill resources** — browse and read skill instruction documents (`skills://list`, `skills://{name}`) -- **Prompt templates** — pre-composed prompts for widgets, SVG diagrams, and advanced visualizations - -### Claude Desktop (stdio) - -Add to your Claude Desktop config (`claude_desktop_config.json`): - -```json -{ - "mcpServers": { - "open-intelligent-ui": { - "command": "node", - "args": ["dist/stdio.js"], - "cwd": "/path/to/apps/mcp" - } - } -} -``` +
+ +
+ +https://github.com/user-attachments/assets/eb1666c1-e177-410b-96eb-9eb5c5534d95 + +
+ +
+ +_The 54-second launch film: ask, explore a 3D explanation, compare options, use a calculator, and follow a coastal itinerary. These are rendered launch scenes; the app runs the agent for each request._ + +## Overview + +Ask a question, explore an idea, compare your options, or make a tool for the moment. The agent chooses a direct text answer, a native component, or a custom interactive UI based on what helps you accomplish the task. + +- **Understand** — explore a mechanism, step through an explanation, or change a variable. +- **Compare** — examine criteria, assumptions, and tradeoffs side by side. +- **Make a tool** — use a calculator, planner, or interactive model with working controls. + +The interface opens directly to chat. Type your request or choose a starter suggestion to begin an agent run. Text, native components, and generated interactive answers appear in the conversation. Sample numbers are illustrations, not live business or weather data. + +## Demo scenes + +From the launch film: explanations, comparisons, working tools, and maps in one conversation. + +### Explore in 3D + +Rotate an airplane with labeled pitch, roll, and yaw axes. Change the angles, play a smooth demonstration, or reset the view. + +
+ +
+ +https://github.com/user-attachments/assets/d0f96662-c8c0-4b25-8c0a-c26b032eb463 + +
+ +
+ +_3D plane · 4 seconds._ + +### Turn comparisons into charts + +Compare exact values in a table, then ask a follow-up to visualize the differences. Jev selects A2UI for basic tables and Open Generative UI for charts and more complex visuals. + +
+ +
+ +https://github.com/user-attachments/assets/36c67a73-ddcf-4d64-b76a-ff659c988836 + +
+ +
+ +_Tables and charts · 8 seconds. Plan names and prices are illustrative._ + +### Make a tool for the moment + +Split a bill with controls for the total, tip, and group size. Calculations update as you change the inputs. + +
+ +
-### Claude Code / HTTP clients +https://github.com/user-attachments/assets/a878a71c-2189-4313-848d-49216b52ce5b + +
+ +
+ +_Bill splitter · 5 seconds._ + +### Follow a trip on the map + +Explore a coastal itinerary with numbered pins and destination photos. The app uses live USGS tiles, credits its photos, and lets you pause, replay, or select a stop. Connections illustrate the itinerary; they are not verified driving directions. + +
+ +
+ +https://github.com/user-attachments/assets/2dd65441-e0e7-4813-891f-9bd476e873d6 + +
+ +
+ +_Coastal map · 4 seconds._ + +
+Scene gallery + +| 3D explanations | Tables → charts | +| --- | --- | +| ![3D airplane with pitch, roll, and yaw controls](https://github.com/user-attachments/assets/b74a1551-a096-4c12-84e8-af79949a2df6) | ![A plan comparison becomes a cost-per-seat chart](https://github.com/user-attachments/assets/6a25cc1d-d401-4914-b4f8-6488f3b17005) | +| Explore aircraft rotation with labeled axes and sliders. | Compare exact values, then visualize them with a follow-up. | + +| Interactive tools | Maps | +| --- | --- | +| ![Bill splitter recalculating the share per person](https://github.com/user-attachments/assets/dea3c40b-025c-4d50-8fef-b14c50e3ca62) | ![California coastal itinerary with numbered map pins and photo cards](https://github.com/user-attachments/assets/098d4aad-95a9-4229-aae6-2de38f1eaa69) | +| Change the group size and tip; the calculation updates. | Follow a coastal itinerary with map pins and destination photos. | + +
+ +## Try it + +- **Animate a coastal trip** — a real USGS map with numbered pins, credited destination photos, and a six-second pin-drop sequence. Pause, replay, or select a stop. Connections are illustrative, not verified driving directions. +- **Pitch, Roll & Yaw** — rotate a 3D airplane with labeled axes, angle controls, smooth demonstrations, and reset. +- **Find your kind of weekend** — compare trip ideas and adjust your preferences. +- **Split the bill fairly** — change the total, tip, and group size in a working calculator. + +These starters run the agent; they are not prerecorded responses. Jev chooses the presentation, so outputs can vary. Map tiles and destination photos require network access. Streaming answers preserve your reading position, reveal text as it enters view, and show the copy action on hover or keyboard focus (always available on touch). + + + +## Get started + +Prerequisites: Node.js 22+, pnpm 9+, Python 3.12+, and [uv](https://docs.astral.sh/uv/). ```bash -# Start the HTTP server -cd apps/mcp && pnpm dev +git clone https://github.com/CopilotKit/OpenIntelligentUI.git +cd OpenIntelligentUI +make setup +# Set OPENAI_API_KEY and TYPESAFE_API_KEY in apps/agent/.env +make dev ``` -Add to `.mcp.json`: +Open the [app](http://localhost:3000). The [agent health endpoint](http://localhost:8123/health) confirms the agent service is running. See [Getting started](docs/getting-started.md) for configuration and verification. -```json -{ - "openIntelligentUI": { - "url": "http://localhost:3100/mcp" - } -} -``` +The default model is `chat-latest`, OpenAI’s documented alias for the latest ChatGPT Instant model, requiring `OPENAI_API_KEY`. Jev (`jev-latest`, requiring `TYPESAFE_API_KEY`) selects the renderer and visualization for each user turn. Basic tables use A2UI; charts, diagrams, calculators, and maps use Open Generative UI. Provider failures are surfaced instead of silently substituting another router or model. See [Visualization routing](docs/visualization-routing.md). -See [apps/mcp/README.md](apps/mcp/README.md) for full configuration, Docker deployment, and API reference. +## How it works -## Architecture +The Python Deep Agent uses a task-first system prompt and focused skills. CopilotKit carries the agent stream to the Next.js app. Native components handle supported structured tasks; `generateSandboxedUi` streams custom HTML, CSS, and JavaScript into an isolated iframe. -Turborepo monorepo with three packages: +Custom UI parameters must arrive in this order: +`initialHeight` → `placeholderMessages` → `css` → `html` → `jsFunctions` → `jsExpressions` + +Local controls run inside the sandbox. A user-clicked follow-up can send selected values through the validated host bridge and start another agent turn. Prior outputs remain separate conversation artifacts; there is no cross-call patch API. See [Product and agent behavior](docs/interactive-answers.md) and [Architecture](docs/architecture.md). + +```text +apps/app/ Next.js + CopilotKit frontend +apps/agent/ Python Deep Agent + FastAPI +apps/mcp/ Optional standalone MCP server +packages/design-system/ Shared theme, SVG, and form styles ``` -apps/ -├── app/ Next.js 16 frontend (CopilotKit v2, React 19, Tailwind 4) -├── agent/ Deep Agent (deepagents + CopilotKit middleware, skills-based) -└── mcp/ Standalone MCP server (design system + skills + document assembler) + +## Development + +```bash +make dev-app # Frontend +make dev-agent # Agent +make dev-mcp # Optional MCP server +pnpm test # JavaScript workspace tests +uv run --directory apps/agent pytest +make lint +make build ``` -### Deep Agent + Skills +The [launch workflow](docs/deployment.md#verification-and-launch) includes browser checks and a real provider smoke test. Tests and builds alone do not verify model access or a deployment. -The agent backend uses [LangChain Deep Agents](https://docs.langchain.com/oss/python/deepagents/overview) (`create_deep_agent`) with a skills-based architecture. Instead of injecting all visualization instructions into the system prompt, skills are defined as `SKILL.md` files in `apps/agent/skills/` and loaded on-demand via progressive disclosure: +## Standalone MCP -``` -apps/agent/skills/ -├── advanced-visualization/SKILL.md # UI mockups, dashboards, Chart.js, generative art -├── master-playbook/SKILL.md # Response philosophy, decision trees, narration patterns -└── svg-diagrams/SKILL.md # SVG generation rules, component patterns, diagram types -``` +The optional MCP server exposes skill resources, prompt templates, and `assemble_document`. The assembler returns an HTML document as text; a compatible host must render it and implement its bridge. It is distinct from the web app's streaming Websandbox integration. + +See the [MCP server guide](apps/mcp/README.md) for HTTP, stdio, and Docker configuration. + +## Compatibility and documentation + +Built with CopilotKit. The `openGenerativeUI` runtime API and `generateSandboxedUi` tool power the streaming experience. + +- [Documentation index](docs/README.md) +- [Bring these patterns to your app](docs/bring-to-your-app.md) +- [Source repository](https://github.com/CopilotKit/OpenIntelligentUI) + +## Contributing -Deep agents also provide built-in planning (`write_todos`), filesystem tools, and sub-agent support. - -### How It Works - -Open Intelligent UI is built on CopilotKit's Open Generative UI. Read the [Open Generative UI docs](https://docs.copilotkit.ai/generative-ui/open-generative-ui) to see how it works under the hood. - -1. **User sends a prompt** via the CopilotKit chat UI -2. **Deep agent decides** whether to respond with text, call a tool, or render a visual component — consulting relevant skills as needed -3. **`generateSandboxedUi`** — the canonical tool the CopilotKit runtime exposes when `openGenerativeUI` is enabled — receives the UI as ordered streaming parameters: `initialHeight` → `placeholderMessages` → `css` → `html` → `jsFunctions` → `jsExpressions` -4. **`OpenGenerativeUIMiddleware`** in the runtime translates the streaming tool call into `open-generative-ui` activity events the frontend subscribes to -5. **The demo's activity renderer** (registered via `renderActivityMessages`) shows the html streaming in live — morphing each update into a preview iframe with Idiomorph so nothing flickers — then boots the final websandbox iframe with the shared design-system CSS and CDN importmap injected -6. **Sandbox bridge + autosize** — the generated UI calls back into the host through Zod-validated `sendPrompt`/`openLink` sandbox functions, and a ResizeObserver inside the iframe continuously reports content height for seamless auto-sizing - -### Key CopilotKit Patterns - -| Pattern | Hook / Option | Example | -|---------|---------------|---------| -| [Open Generative UI](https://docs.copilotkit.ai/generative-ui/open-generative-ui) | `openGenerativeUI` + `renderActivityMessages` | Streaming sandboxed widgets via `generateSandboxedUi` | -| Generative UI | `useComponent` | Pie charts, bar charts | -| Frontend tools | `useFrontendTool` | Theme toggle | -| Human-in-the-loop | `useHumanInTheLoop` | Meeting scheduler | -| Default tool render | `useDefaultRenderTool` | Tool execution status | - -## Decision Matrix — Picking the Right Visual - -| User asks about... | Output type | Technology | -|-----------------------------|--------------------------|---------------------| -| How X works (physical) | Illustrative diagram | SVG | -| How X works (abstract) | Interactive explainer | HTML + inline SVG | -| Process / steps | Flowchart | SVG | -| Architecture / containment | Structural diagram | SVG | -| Database schema / ERD | Relationship diagram | Mermaid | -| Trends over time | Line chart | Chart.js | -| Category comparison | Bar chart | Chart.js | -| Part of whole | Doughnut chart | Chart.js | -| KPIs / metrics | Dashboard | HTML metric cards | -| Design a UI | Mockup | HTML | -| Choose between options | Comparison cards | HTML grid | -| Cyclic process | Step-through | HTML stepper | -| Physics / math | Simulation | Canvas + JS | -| Function / equation | Plotter | SVG + JS | -| Data exploration | Sortable table | HTML + JS | -| Creative / decorative | Art / illustration | SVG | -| 3D visualization | 3D scene | Three.js | -| Music / audio | Synthesizer | Tone.js | -| Network / graph | Force layout | D3.js | -| Quick factual answer | Plain text | None | -| Code solution | Code block | None | -| Emotional support | Warm text | None | - -## Tech Stack - -Next.js 16, React 19, Tailwind CSS 4, LangChain Deep Agents, LangGraph, CopilotKit v2, Turborepo, Recharts - -## Star History - - - - - - Star History Chart - - +See [Contributing](CONTRIBUTING.md) for the development workflow and [the documentation index](docs/README.md) for guides to the agent, rendering, and deployment. ## License -MIT +[MIT](LICENSE). diff --git a/apps/agent/main.py b/apps/agent/main.py index 8a5d8fa..2347b2e 100644 --- a/apps/agent/main.py +++ b/apps/agent/main.py @@ -5,7 +5,6 @@ import os import warnings -from pathlib import Path from dotenv import load_dotenv from fastapi import FastAPI @@ -16,9 +15,12 @@ from src.anthropic_compat import ConsecutiveSystemMessagesMiddleware from src.bounded_memory_saver import BoundedMemorySaver from src.model import build_model +from src.visualization_router import JevVisualizationMiddleware +from src.skill_backend import SKILL_SOURCES, build_agent_backend from src.query import query_data from src.todos import AgentState, todo_tools from src.form import generate_form +from src.trip_images import get_trip_stop_images from src.plan import plan_visualization from src.prompt import SYSTEM_PROMPT @@ -26,10 +28,15 @@ agent = create_deep_agent( model=build_model(), - tools=[query_data, plan_visualization, *todo_tools, generate_form], - middleware=[CopilotKitMiddleware(), ConsecutiveSystemMessagesMiddleware()], + tools=[query_data, get_trip_stop_images, plan_visualization, *todo_tools, generate_form], + middleware=[ + CopilotKitMiddleware(), + JevVisualizationMiddleware(), + ConsecutiveSystemMessagesMiddleware(), + ], context_schema=AgentState, - skills=[str(Path(__file__).parent / "skills")], + skills=SKILL_SOURCES, + backend=build_agent_backend, checkpointer=BoundedMemorySaver(max_threads=200), system_prompt=SYSTEM_PROMPT, ) @@ -46,7 +53,7 @@ def health(): app=app, agent=LangGraphAGUIAgent( name="sample_agent", - description="CopilotKit + LangGraph demo agent", + description="Open Generative UI by CopilotKit — answers you can interact with", graph=agent, ), path="/", diff --git a/apps/agent/pyproject.toml b/apps/agent/pyproject.toml index 5ca5eae..b205664 100644 --- a/apps/agent/pyproject.toml +++ b/apps/agent/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ "deepagents>=0.1.0", "langchain-anthropic>=1.4.0", "langchain-openai>=1.3.0", + "httpx>=0.28,<1", ] [dependency-groups] diff --git a/apps/agent/skills/advanced-visualization/SKILL.md b/apps/agent/skills/advanced-visualization/SKILL.md index b1c9e78..8cb30cc 100644 --- a/apps/agent/skills/advanced-visualization/SKILL.md +++ b/apps/agent/skills/advanced-visualization/SKILL.md @@ -1,962 +1,212 @@ --- -name: "Advanced Visualization Techniques" -description: "UI mockups, dashboards, advanced interactivity, generative art, simulations, math visualizations, and design system rules for producing rich generateSandboxedUi output." +name: "advanced-visualization" +description: "Build responsive, accessible calculators, comparisons, charts, and simulations with the sandbox contract." allowed-tools: [] --- -# Agent Visualization Skills — Volume 2: Advanced Techniques +# Interactive answer implementation -Prerequisite: Volume 1 (SVG diagrams, basic interactive widgets, Chart.js, Mermaid). -This volume covers: UI mockups, dashboards, advanced interactivity, generative art, -simulations, math visualizations, and the design system that ties everything together. +Use this guidance after choosing a custom UI because it helps the task. Follow +Jev’s renderer and visualization decision. A2UI handles basic tables; this sandbox +handles charts, diagrams and custom interaction. +Do not turn every answer into a dashboard. Use multiple sections when needed, +including headings, assumptions, units, sources, and instructions inside the UI. ---- - -## Part 0: The Tool Contract — generateSandboxedUi - -Everything in this volume ships through the `generateSandboxedUi` tool. The UI -streams as you generate it, so emit the parameters in this EXACT order: - -1. `initialHeight` — estimated height of the finished UI in px. -2. `placeholderMessages` — 2-4 short, playful progress messages. -3. `css` — ALL styles, up front. The user sees a placeholder until css is complete, - so keep it lean and put every style here for the css-first reveal. -4. `html` — clean body markup, streamed in live. No `` : ""); const withHead = ensureHead(html); diff --git a/apps/app/src/components/generative-ui/open-generative-ui/trip-animator.ts b/apps/app/src/components/generative-ui/open-generative-ui/trip-animator.ts new file mode 100644 index 0000000..e1d5b40 --- /dev/null +++ b/apps/app/src/components/generative-ui/open-generative-ui/trip-animator.ts @@ -0,0 +1,118 @@ +export interface TripFrame { + progress: number; + fromIndex: number; + toIndex: number; + fraction: number; + activeStop: number; + pinIndex: number; + pinProgress: number; +} +export interface TripAnimationOptions { + stopCount: number; + durationMs?: number; + /** Inner marker nodes, never Leaflet positioning wrappers. */ + pinElements?: HTMLElement[]; + onFrame: (frame: TripFrame) => void; + onState?: (state: "playing" | "paused" | "complete") => void; +} + +/** Self-contained: serialized into the sandbox, without host objects or network access. */ +export function createTripAnimator(options: TripAnimationOptions) { + const { stopCount, onFrame, onState } = options; + const duration = options.durationMs ?? 6000; + if (!Number.isInteger(stopCount) || stopCount < 2 || stopCount > 30) + throw new Error("A trip needs 2–30 stops."); + if (!Number.isFinite(duration) || duration < 1000 || duration > 120000) + throw new Error("Trip duration must be between 1 and 120 seconds."); + options.pinElements?.forEach((pin, index) => { + if (!pin.textContent?.trim()) pin.textContent = String(index + 1); + }); + let elapsed = 0; + let lastTime: number | null = null; + let frameId: number | null = null; + let playing = false; + let disposed = false; + const reduceMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false; + const emit = () => { + const progress = Math.min(1, elapsed / duration); + const step = progress * (stopCount - 1); + const fromIndex = Math.min(stopCount - 2, Math.floor(step)); + // Travel for 80% of each leg, then hold at the destination for 20%. + const travel = Math.min(1, (step - fromIndex) / .8); + const fraction = travel * travel * (3 - 2 * travel); + const pinStep = progress * stopCount; + const pinIndex = Math.min(stopCount - 1, Math.floor(pinStep)); + const pinProgress = progress === 1 ? 1 : Math.min(1, (pinStep - pinIndex) / .55); + options.pinElements?.forEach((pin, index) => { + const landing = index < pinIndex ? 1 : index === pinIndex ? pinProgress : 0; + // A short drop with a restrained spring settle; preserve Leaflet's outer transform. + const t = landing - 1; + const ease = 1 + 2.2 * t * t * t + 1.2 * t * t; + pin.style.opacity = landing > 0 ? "1" : "0"; + pin.style.transform = `translateY(${-18 * (1 - ease)}px) scale(${.72 + .28 * ease})`; + }); + onFrame({ progress, fromIndex, toIndex: fromIndex + 1, fraction, + activeStop: pinIndex, pinIndex, pinProgress }); + }; + const pause = () => { + if (disposed) return; + playing = false; + lastTime = null; + if (frameId !== null) cancelAnimationFrame(frameId); + frameId = null; + onState?.(elapsed >= duration ? "complete" : "paused"); + }; + const tick = (time: number) => { + if (!playing || disposed) return; + if (lastTime !== null) elapsed = Math.min(duration, elapsed + Math.max(0, time - lastTime)); + lastTime = time; + emit(); + if (elapsed >= duration) pause(); + else frameId = requestAnimationFrame(tick); + }; + const play = () => { + if (disposed || playing) return; + if (reduceMotion()) { + elapsed = duration; + emit(); + onState?.("complete"); + return; + } + if (elapsed >= duration) return; + playing = true; + lastTime = null; + onState?.("playing"); + frameId = requestAnimationFrame(tick); + }; + const seek = (stop: number) => { + if (disposed) return; + if (!Number.isInteger(stop) || stop < 0 || stop >= stopCount) + throw new Error("Stop index is outside this trip."); + pause(); + elapsed = stop === stopCount - 1 ? duration : duration * (stop + .6) / stopCount; + emit(); + onState?.(elapsed >= duration ? "complete" : "paused"); + }; + const replay = () => { + if (disposed) return; + pause(); + elapsed = 0; + emit(); + play(); + }; + const onVisibility = () => { if (document.hidden) pause(); }; + const dispose = () => { + if (disposed) return; + pause(); + disposed = true; + document.removeEventListener("visibilitychange", onVisibility); + window.removeEventListener("pagehide", dispose); + }; + document.addEventListener("visibilitychange", onVisibility); + window.addEventListener("pagehide", dispose); + if (reduceMotion()) elapsed = duration; + emit(); + return { play, pause, replay, seek, dispose }; +} + +export const TRIP_ANIMATOR_SCRIPT = ``; diff --git a/apps/app/src/components/generative-ui/table-catalog.test.tsx b/apps/app/src/components/generative-ui/table-catalog.test.tsx new file mode 100644 index 0000000..01a4d24 --- /dev/null +++ b/apps/app/src/components/generative-ui/table-catalog.test.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { afterEach, describe, expect, it } from "vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import { + A2UIProvider, + A2UIRenderer, + useA2UIActions, +} from "@copilotkit/a2ui-renderer"; +import { DataTable, tableCatalog } from "./table-catalog"; + +afterEach(cleanup); +const props = { + title: "Quarterly sales", + columns: ["Quarter", "Sales"], + rows: [ + ["Q1", "$100"], + ["Q2", "$200"], + ], + source: "User-provided data", +}; +function Surface() { + const { processMessages } = useA2UIActions(); + useEffect(() => { + processMessages([ + { + createSurface: { + surfaceId: "test", + catalogId: "copilotkit://open-generative-ui-tables", + }, + }, + { + updateComponents: { + surfaceId: "test", + components: [{ id: "root", component: "Table", ...props }], + }, + }, + ]); + }, [processMessages]); + return ; +} +describe("A2UI table catalog", () => { + it("renders real A2UI operations through the catalog", async () => { + render( + + + , + ); + expect( + await screen.findByRole("table", { name: "Quarterly sales" }), + ).toBeTruthy(); + expect(screen.getAllByRole("columnheader")).toHaveLength(2); + expect(screen.getByText("$200")).toBeTruthy(); + expect(screen.getByText("User-provided data")).toBeTruthy(); + }); + it("rejects mismatched rows instead of mislabeling data", () => { + render(); + expect(screen.getByRole("alert").textContent).toContain("invalid rows"); + }); + it("renders untrusted values as text", () => { + const { container } = render( + alert(1)", "100"]] }} + />, + ); + expect(container.querySelector("script")).toBeNull(); + expect(screen.getByText("")).toBeTruthy(); + }); +}); diff --git a/apps/app/src/components/generative-ui/table-catalog.tsx b/apps/app/src/components/generative-ui/table-catalog.tsx new file mode 100644 index 0000000..fa34141 --- /dev/null +++ b/apps/app/src/components/generative-ui/table-catalog.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { createCatalog } from "@copilotkit/a2ui-renderer"; +import { z } from "zod"; + +export const TableProps = z.object({ + title: z.string().max(300), + columns: z.array(z.string().max(200)).min(1).max(12), + rows: z.array(z.array(z.string().max(2000)).max(12)).max(200), + source: z + .string() + .max(1000) + .describe( + "Data provenance: user-provided, calculated, sourced, or explicitly illustrative.", + ), +}); + +export function DataTable({ props }: { props: z.infer }) { + const parsed = TableProps.safeParse(props); + if ( + !parsed.success || + parsed.data.rows.some((row) => row.length !== parsed.data.columns.length) + ) { + return ( +

+ This table has invalid rows or columns. Ask the assistant to regenerate + it. +

+ ); + } + const { title, columns, rows, source } = parsed.data; + return ( +
+
+ + + + + {columns.map((column, index) => ( + + ))} + + + + {rows.map((row, index) => ( + + {row.map((cell, column) => ( + + ))} + + ))} + +
{title}
+ {column} +
{cell}
+ {rows.length === 0 &&

No rows to display.

} +
+

{source}

+
+ ); +} + +export const tableCatalog = createCatalog( + { + Table: { + props: TableProps, + description: + "An accessible basic data table. Every row must match the columns. Use exact values and label data provenance.", + }, + }, + { Table: DataTable }, + { catalogId: "copilotkit://open-generative-ui-tables" }, +); diff --git a/apps/app/src/hooks/use-example-suggestions.tsx b/apps/app/src/hooks/use-example-suggestions.tsx index 7f8f602..57b5859 100644 --- a/apps/app/src/hooks/use-example-suggestions.tsx +++ b/apps/app/src/hooks/use-example-suggestions.tsx @@ -1,12 +1,14 @@ import { useConfigureSuggestions } from "@copilotkit/react-core/v2"; +import { DEMO_EXAMPLES } from "@/components/demo-gallery/demo-data"; export const useExampleSuggestions = () => { useConfigureSuggestions({ - suggestions: [ - { title: "Visualize a car axle", message: "Visualize how a car axle works" }, - { title: "3D Plane Controls", message: "Create a 3D plane in Three.js to explain how pitch, roll, and yaw work with buttons that animate on hover." }, - { title: "Cool 3D sphere", message: "Create a 3D animation of a sphere turning into an icosahedron when the mouse is on it and back to a sphere when it's not on the icosahedron, make it cool." }, - ], - available: "always", // Optional: when to show suggestions + suggestions: DEMO_EXAMPLES.filter(({ id }) => + ["demo-pitch-roll-yaw", "trip", "split", "map"].includes(id), + ).map(({ title, prompt }) => ({ + title, + message: prompt, + })), + available: "always", }); -} +}; diff --git a/apps/app/src/hooks/use-theme.tsx b/apps/app/src/hooks/use-theme.tsx index fac7b20..e02b7d0 100644 --- a/apps/app/src/hooks/use-theme.tsx +++ b/apps/app/src/hooks/use-theme.tsx @@ -4,13 +4,16 @@ import { createContext, useContext, useEffect, useState } from "react"; type Theme = "dark" | "light" | "system"; -const ThemeContext = createContext<{ theme: Theme; setTheme: (t: Theme) => void }>({ +const ThemeContext = createContext<{ + theme: Theme; + setTheme: (t: Theme) => void; +}>({ theme: "system", setTheme: () => {}, }); export function ThemeProvider({ children }: { children: React.ReactNode }) { - const [theme, setTheme] = useState("system"); + const [theme, setTheme] = useState("light"); useEffect(() => { const root = document.documentElement; diff --git a/apps/app/src/lib/sandbox/__tests__/prompt-bridge.test.tsx b/apps/app/src/lib/sandbox/__tests__/prompt-bridge.test.tsx index 9abae04..60a2521 100644 --- a/apps/app/src/lib/sandbox/__tests__/prompt-bridge.test.tsx +++ b/apps/app/src/lib/sandbox/__tests__/prompt-bridge.test.tsx @@ -2,11 +2,26 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, cleanup } from "@testing-library/react"; import "@testing-library/jest-dom/vitest"; import { OpenGenUIPromptBridge } from "@/lib/sandbox/prompt-bridge"; -import { SEND_PROMPT_EVENT } from "@/lib/sandbox/sandbox-functions"; +import { + sendPromptFunction, + SEND_PROMPT_EVENT, +} from "@/lib/sandbox/sandbox-functions"; const addMessage = vi.fn(); const runAgent = vi.fn(); -const fakeAgent = { addMessage }; +interface ErrorSubscriber { + onRunErrorEvent: (params: { event: { message: string } }) => void; +} +const subscribers = new Set(); +const subscribe = vi.fn((subscriber: ErrorSubscriber) => { + subscribers.add(subscriber); + return { + unsubscribe: () => { + subscribers.delete(subscriber); + }, + }; +}); +const fakeAgent = { addMessage, isRunning: false, subscribe }; const fakeCopilotKit = { runAgent }; vi.mock("@copilotkit/react-core/v2", () => ({ @@ -14,63 +29,169 @@ vi.mock("@copilotkit/react-core/v2", () => ({ useCopilotKit: () => ({ copilotkit: fakeCopilotKit }), })); -const dispatchSendPrompt = (text: string) => { - window.dispatchEvent( - new CustomEvent(SEND_PROMPT_EVENT, { detail: { text } }) - ); -}; - -describe("OpenGenUIPromptBridge", () => { - beforeEach(() => { - addMessage.mockClear(); - runAgent.mockClear(); +function pendingRun() { + let finish!: () => void; + const promise = new Promise((resolve) => { + finish = resolve; }); + runAgent.mockReturnValueOnce(promise); + return finish; +} - afterEach(() => { - cleanup(); - }); +beforeEach(() => { + addMessage.mockReset(); + runAgent.mockReset().mockResolvedValue(undefined); + fakeAgent.isRunning = false; + subscribe.mockClear(); + subscribers.clear(); +}); +afterEach(() => { + cleanup(); + expect(subscribers.size).toBe(0); +}); - it("renders nothing", () => { +describe("OpenGenUIPromptBridge", () => { + it("renders nothing and submits trimmed text once", async () => { const { container } = render(); expect(container).toBeEmptyDOMElement(); + await expect( + sendPromptFunction.handler({ text: " draw a chart " }), + ).resolves.toEqual({ ok: true }); + expect(addMessage).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ content: "draw a chart", role: "user" }), + ); + expect(runAgent).toHaveBeenCalledExactlyOnceWith({ agent: fakeAgent }); }); - it("submits the prompt via addMessage and runAgent", () => { + it("waits for the run and propagates failures, then permits retry", async () => { render(); - dispatchSendPrompt("draw a chart"); - - expect(addMessage).toHaveBeenCalledTimes(1); - expect(addMessage).toHaveBeenCalledWith( - expect.objectContaining({ - id: expect.any(String), - content: "draw a chart", - role: "user", - }) + runAgent.mockRejectedValueOnce(new Error("Provider unavailable")); + await expect(sendPromptFunction.handler({ text: "first" })).rejects.toThrow( + "Provider unavailable", ); - expect(runAgent).toHaveBeenCalledTimes(1); - expect(runAgent).toHaveBeenCalledWith({ agent: fakeAgent }); + await expect( + sendPromptFunction.handler({ text: "retry" }), + ).resolves.toEqual({ ok: true }); }); - it("ignores events without a string text", () => { + it("rejects a protocol RUN_ERROR even when runAgent resolves", async () => { render(); - window.dispatchEvent( - new CustomEvent(SEND_PROMPT_EVENT, { detail: { text: 42 } }) + runAgent.mockImplementationOnce(async () => { + for (const subscriber of subscribers) { + subscriber.onRunErrorEvent({ + event: { message: "Agent service unavailable" }, + }); + } + }); + await expect(sendPromptFunction.handler({ text: "first" })).rejects.toThrow( + "Agent service unavailable", ); - window.dispatchEvent(new CustomEvent(SEND_PROMPT_EVENT)); + expect(subscribers.size).toBe(0); + await expect( + sendPromptFunction.handler({ text: "retry" }), + ).resolves.toEqual({ ok: true }); + expect(subscribers.size).toBe(0); + }); + + it("unsubscribes a pending run on unmount before the transport settles", async () => { + const { unmount } = render(); + const finish = pendingRun(); + const first = sendPromptFunction.handler({ text: "first" }); + const rejection = expect(first).rejects.toThrow(/unmounted/i); + expect(subscribers.size).toBe(1); + unmount(); + await rejection; + expect(subscribers.size).toBe(0); + finish(); + await Promise.resolve(); + expect(subscribers.size).toBe(0); + }); + it("rejects when the agent is busy without adding a message", async () => { + render(); + fakeAgent.isRunning = true; + await expect(sendPromptFunction.handler({ text: "next" })).rejects.toThrow( + /busy/i, + ); expect(addMessage).not.toHaveBeenCalled(); - expect(runAgent).not.toHaveBeenCalled(); }); - it("removes the listener on unmount", () => { - const { unmount } = render(); - dispatchSendPrompt("first"); + it("rejects duplicate in-flight requests before isRunning updates", async () => { + render(); + const finish = pendingRun(); + const first = sendPromptFunction.handler({ text: "first" }); + const settled = vi.fn(); + void first.then(settled); + await Promise.resolve(); + expect(settled).not.toHaveBeenCalled(); + await expect( + sendPromptFunction.handler({ text: "second" }), + ).rejects.toThrow(/busy/i); expect(addMessage).toHaveBeenCalledTimes(1); - expect(runAgent).toHaveBeenCalledTimes(1); + finish(); + await expect(first).resolves.toEqual({ ok: true }); + }); - unmount(); - dispatchSendPrompt("second"); + it("claims a request once even with duplicate mounted listeners", async () => { + render( + <> + + + , + ); + await sendPromptFunction.handler({ text: "once" }); expect(addMessage).toHaveBeenCalledTimes(1); expect(runAgent).toHaveBeenCalledTimes(1); }); + + it("rejects missing and unmounted bridges", async () => { + await expect( + sendPromptFunction.handler({ text: "missing" }), + ).rejects.toThrow(/unavailable/i); + const { unmount } = render(); + unmount(); + await expect( + sendPromptFunction.handler({ text: "removed" }), + ).rejects.toThrow(/unavailable/i); + expect(runAgent).not.toHaveBeenCalled(); + }); + + it("rejects pending requests on cleanup without late success", async () => { + const { unmount } = render(); + const finish = pendingRun(); + const first = sendPromptFunction.handler({ text: "first" }); + const rejection = expect(first).rejects.toThrow(/unmounted/i); + unmount(); + await rejection; + finish(); + await Promise.resolve(); + await expect( + sendPromptFunction.handler({ text: "second" }), + ).rejects.toThrow(/unavailable/i); + }); + + it("rejects synchronous submission errors and releases the lock", async () => { + render(); + addMessage.mockImplementationOnce(() => { + throw new Error("Could not add message"); + }); + await expect(sendPromptFunction.handler({ text: "first" })).rejects.toThrow( + "Could not add message", + ); + await expect( + sendPromptFunction.handler({ text: "retry" }), + ).resolves.toEqual({ ok: true }); + }); + + it("ignores malformed legacy events and rejects whitespace-only input", async () => { + render(); + window.dispatchEvent( + new CustomEvent(SEND_PROMPT_EVENT, { detail: { text: 42 } }), + ); + window.dispatchEvent(new CustomEvent(SEND_PROMPT_EVENT)); + await expect( + sendPromptFunction.handler({ text: " \n\t " }), + ).rejects.toThrow(); + expect(addMessage).not.toHaveBeenCalled(); + }); }); diff --git a/apps/app/src/lib/sandbox/__tests__/sandbox-functions.test.ts b/apps/app/src/lib/sandbox/__tests__/sandbox-functions.test.ts index b2f37cd..8df4259 100644 --- a/apps/app/src/lib/sandbox/__tests__/sandbox-functions.test.ts +++ b/apps/app/src/lib/sandbox/__tests__/sandbox-functions.test.ts @@ -5,12 +5,17 @@ import { isAllowedLinkUrl, SANDBOX_FUNCTIONS, SEND_PROMPT_EVENT, + type SendPromptRequest, } from "@/lib/sandbox/sandbox-functions"; describe("sendPromptFunction", () => { let received: string[]; const listener = (e: Event) => { - received.push((e as CustomEvent<{ text: string }>).detail.text); + const request = (e as CustomEvent).detail; + if (request.claim()) { + received.push(request.text); + request.resolve(); + } }; beforeEach(() => { @@ -28,7 +33,7 @@ describe("sendPromptFunction", () => { it("dispatches exactly one CustomEvent with the text for valid args", async () => { await expect( - sendPromptFunction.handler({ text: "draw a chart" }) + sendPromptFunction.handler({ text: "draw a chart" }), ).resolves.toEqual({ ok: true }); expect(received).toEqual(["draw a chart"]); }); @@ -36,6 +41,7 @@ describe("sendPromptFunction", () => { it.each([ ["missing text", {}], ["empty string", { text: "" }], + ["whitespace only", { text: " \n\t " }], ["number", { text: 42 }], ["over 4000 chars", { text: "a".repeat(4001) }], ["extra junk only", { junk: "x" }], @@ -46,7 +52,7 @@ describe("sendPromptFunction", () => { it("accepts text at exactly 4000 chars", async () => { await expect( - sendPromptFunction.handler({ text: "a".repeat(4000) }) + sendPromptFunction.handler({ text: "a".repeat(4000) }), ).resolves.toEqual({ ok: true }); expect(received).toHaveLength(1); }); @@ -70,13 +76,13 @@ describe("openLinkFunction", () => { it("opens an https URL in a new tab with noopener,noreferrer", async () => { await expect( - openLinkFunction.handler({ url: "https://example.com/docs" }) + openLinkFunction.handler({ url: "https://example.com/docs" }), ).resolves.toEqual({ ok: true }); expect(openSpy).toHaveBeenCalledTimes(1); expect(openSpy).toHaveBeenCalledWith( "https://example.com/docs", "_blank", - "noopener,noreferrer" + "noopener,noreferrer", ); }); @@ -95,7 +101,7 @@ describe("openLinkFunction", () => { it("rejects https URLs outside the env allowlist", async () => { vi.stubEnv("NEXT_PUBLIC_OPEN_LINK_ALLOWED_ORIGINS", "https://github.com"); await expect( - openLinkFunction.handler({ url: "https://evil.com/x" }) + openLinkFunction.handler({ url: "https://evil.com/x" }), ).rejects.toThrow(); expect(openSpy).not.toHaveBeenCalled(); }); @@ -103,12 +109,12 @@ describe("openLinkFunction", () => { it("allows https URLs inside the env allowlist", async () => { vi.stubEnv("NEXT_PUBLIC_OPEN_LINK_ALLOWED_ORIGINS", "https://github.com"); await expect( - openLinkFunction.handler({ url: "https://github.com/CopilotKit" }) + openLinkFunction.handler({ url: "https://github.com/CopilotKit" }), ).resolves.toEqual({ ok: true }); expect(openSpy).toHaveBeenCalledWith( "https://github.com/CopilotKit", "_blank", - "noopener,noreferrer" + "noopener,noreferrer", ); }); }); @@ -118,12 +124,12 @@ describe("isAllowedLinkUrl", () => { vi.unstubAllEnvs(); }); - it.each([ - "https://example.com", - "https://example.com/path?query=1#hash", - ])("allows %s without an allowlist", (url) => { - expect(isAllowedLinkUrl(url)).toBe(true); - }); + it.each(["https://example.com", "https://example.com/path?query=1#hash"])( + "allows %s without an allowlist", + (url) => { + expect(isAllowedLinkUrl(url)).toBe(true); + }, + ); it.each([ "javascript:alert(1)", @@ -139,27 +145,27 @@ describe("isAllowedLinkUrl", () => { }); it("allows an https URL whose origin is in the allowlist", () => { - expect(isAllowedLinkUrl("https://github.com/x", ["https://github.com"])).toBe( - true - ); + expect( + isAllowedLinkUrl("https://github.com/x", ["https://github.com"]), + ).toBe(true); }); it("rejects an https URL whose origin is not in the allowlist", () => { expect(isAllowedLinkUrl("https://evil.com", ["https://github.com"])).toBe( - false + false, ); }); it("rejects origin-prefix lookalike hosts", () => { expect( - isAllowedLinkUrl("https://github.com.evil.com/x", ["https://github.com"]) + isAllowedLinkUrl("https://github.com.evil.com/x", ["https://github.com"]), ).toBe(false); }); it("falls back to the env allowlist when no explicit list is given", () => { vi.stubEnv( "NEXT_PUBLIC_OPEN_LINK_ALLOWED_ORIGINS", - "https://github.com, https://copilotkit.ai" + "https://github.com, https://copilotkit.ai", ); expect(isAllowedLinkUrl("https://github.com/x")).toBe(true); expect(isAllowedLinkUrl("https://copilotkit.ai/blog")).toBe(true); diff --git a/apps/app/src/lib/sandbox/prompt-bridge.tsx b/apps/app/src/lib/sandbox/prompt-bridge.tsx index d3939e2..7045e71 100644 --- a/apps/app/src/lib/sandbox/prompt-bridge.tsx +++ b/apps/app/src/lib/sandbox/prompt-bridge.tsx @@ -2,7 +2,11 @@ import { useEffect, useRef } from "react"; import { useAgent, useCopilotKit } from "@copilotkit/react-core/v2"; -import { SEND_PROMPT_EVENT } from "./sandbox-functions"; +import { + SEND_PROMPT_EVENT, + sendPromptParameters, + type SendPromptRequest, +} from "./sandbox-functions"; export function OpenGenUIPromptBridge() { const { agent } = useAgent(); @@ -10,20 +14,113 @@ export function OpenGenUIPromptBridge() { const agentRef = useRef(agent); const copilotkitRef = useRef(copilotkit); - useEffect(() => { agentRef.current = agent; }, [agent]); - useEffect(() => { copilotkitRef.current = copilotkit; }, [copilotkit]); + useEffect(() => { + agentRef.current = agent; + }, [agent]); + useEffect(() => { + copilotkitRef.current = copilotkit; + }, [copilotkit]); useEffect(() => { + let pending: SendPromptRequest | null = null; + let cleanupPendingSubscription: (() => void) | null = null; const handler = (e: Event) => { - const text = (e as CustomEvent<{ text?: unknown }>).detail?.text; - if (typeof text !== "string" || !text) return; + const request = (e as CustomEvent>).detail; + if ( + !request || + typeof request.claim !== "function" || + typeof request.resolve !== "function" || + typeof request.reject !== "function" + ) + return; + // Claim before any asynchronous work so duplicate mounted listeners cannot + // dispatch the same request twice, including requests rejected as busy. + if (!request.claim()) return; + const parsed = sendPromptParameters.safeParse(request); + if (!parsed.success) { + request.reject( + new Error( + "sendPrompt: enter a non-empty question of at most 4000 characters.", + ), + ); + return; + } const a = agentRef.current; const ck = copilotkitRef.current; - a.addMessage({ id: crypto.randomUUID(), content: text, role: "user" }); - ck.runAgent({ agent: a }); + if (pending || a.isRunning) { + request.reject( + new Error( + "sendPrompt: the agent is busy. Wait for the current answer and try again.", + ), + ); + return; + } + const accepted: SendPromptRequest = { + text: parsed.data.text, + claim: request.claim, + resolve: request.resolve, + reject: request.reject, + }; + pending = accepted; + let subscription: { unsubscribe: () => void } | undefined; + const releaseSubscription = () => { + subscription?.unsubscribe(); + subscription = undefined; + }; + cleanupPendingSubscription = releaseSubscription; + void (async () => { + let protocolFailed = false; + try { + // CopilotKit emits protocol RUN_ERROR through the agent subscriber; + // its runAgent promise can still resolve after that terminal event. + subscription = a.subscribe({ + onRunErrorEvent: ({ event }) => { + protocolFailed = true; + accepted.reject( + new Error( + event.message || + "sendPrompt: the agent run failed. Please try again.", + ), + ); + releaseSubscription(); + }, + }); + a.addMessage({ + id: crypto.randomUUID(), + content: accepted.text, + role: "user", + }); + await ck.runAgent({ agent: a }); + if (!protocolFailed) accepted.resolve(); + } catch (error) { + accepted.reject( + error instanceof Error + ? error + : new Error( + "sendPrompt: the agent run failed. Please try again.", + ), + ); + } finally { + releaseSubscription(); + if (pending === accepted) { + pending = null; + cleanupPendingSubscription = null; + } + } + })(); }; window.addEventListener(SEND_PROMPT_EVENT, handler); - return () => window.removeEventListener(SEND_PROMPT_EVENT, handler); + return () => { + window.removeEventListener(SEND_PROMPT_EVENT, handler); + cleanupPendingSubscription?.(); + cleanupPendingSubscription = null; + pending?.reject( + new Error( + "sendPrompt: chat bridge unmounted before the request completed.", + ), + ); + pending = null; + }; }, []); return null; diff --git a/apps/app/src/lib/sandbox/sandbox-functions.ts b/apps/app/src/lib/sandbox/sandbox-functions.ts index 7f1ee15..24a65ba 100644 --- a/apps/app/src/lib/sandbox/sandbox-functions.ts +++ b/apps/app/src/lib/sandbox/sandbox-functions.ts @@ -8,10 +8,19 @@ const formatIssues = (error: z.ZodError) => .map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`) .join("; "); -const sendPromptParameters = z.object({ - text: z.string().min(1).max(4000), +export const sendPromptParameters = z.object({ + text: z.string().trim().min(1).max(4000), }); +// This request stays inside the host window; only the settled result crosses +// the Websandbox bridge. A listener must claim it synchronously during dispatch. +export interface SendPromptRequest { + text: string; + claim: () => boolean; + resolve: () => void; + reject: (reason: unknown) => void; +} + export const sendPromptFunction: SandboxFunction = { name: "sendPrompt", description: @@ -20,12 +29,33 @@ export const sendPromptFunction: SandboxFunction = { handler: async (args) => { const result = sendPromptParameters.safeParse(args); if (!result.success) { - throw new Error(`sendPrompt: invalid arguments — ${formatIssues(result.error)}`); + throw new Error( + `sendPrompt: invalid arguments — ${formatIssues(result.error)}`, + ); } - window.dispatchEvent( - new CustomEvent(SEND_PROMPT_EVENT, { detail: { text: result.data.text } }) - ); - return { ok: true }; + return new Promise<{ ok: true }>((resolve, reject) => { + let claimed = false; + const request: SendPromptRequest = { + text: result.data.text, + claim: () => { + if (claimed) return false; + claimed = true; + return true; + }, + resolve: () => resolve({ ok: true }), + reject, + }; + window.dispatchEvent( + new CustomEvent(SEND_PROMPT_EVENT, { detail: request }), + ); + if (!claimed) { + reject( + new Error( + "sendPrompt: chat bridge unavailable. Please reload the page and try again.", + ), + ); + } + }); }, }; @@ -45,7 +75,7 @@ const envAllowedOrigins = (): string[] | undefined => { export function isAllowedLinkUrl( url: string, - allowedOrigins?: readonly string[] + allowedOrigins?: readonly string[], ): boolean { let parsed: URL; try { @@ -67,7 +97,9 @@ export const openLinkFunction: SandboxFunction = { handler: async (args) => { const result = openLinkParameters.safeParse(args); if (!result.success) { - throw new Error(`openLink: invalid arguments — ${formatIssues(result.error)}`); + throw new Error( + `openLink: invalid arguments — ${formatIssues(result.error)}`, + ); } const { url } = result.data; if (!isAllowedLinkUrl(url)) { diff --git a/apps/app/vitest.config.ts b/apps/app/vitest.config.ts index be75685..7379bb8 100644 --- a/apps/app/vitest.config.ts +++ b/apps/app/vitest.config.ts @@ -4,6 +4,7 @@ import { resolve } from "node:path"; export default defineConfig({ plugins: [react()], + css: { postcss: { plugins: [] } }, resolve: { alias: { "@": resolve(__dirname, "./src"), @@ -11,5 +12,6 @@ export default defineConfig({ }, test: { environment: "jsdom", + server: { deps: { inline: [/@copilotkit\/react-core/] } }, }, }); diff --git a/apps/mcp/README.md b/apps/mcp/README.md index 5ef7be7..c4a6c43 100644 --- a/apps/mcp/README.md +++ b/apps/mcp/README.md @@ -1,10 +1,10 @@ -# OpenIntelligentUI MCP Server +# OpenGenerativeUI MCP Server -A standalone, independently deployable [Model Context Protocol](https://modelcontextprotocol.io) server that exposes OpenIntelligentUI's design system, skills, and document renderer to any MCP-compatible client. +A standalone, independently deployable [Model Context Protocol](https://modelcontextprotocol.io) server that exposes OpenGenerativeUI's design system, skills, and document renderer to any MCP-compatible client. ## Features -- **Design System Tool** — `assemble_document` wraps HTML fragments with OpenIntelligentUI's complete CSS design system and bridge JavaScript +- **Design System Tool** — `assemble_document` wraps HTML fragments with OpenGenerativeUI's complete CSS design system and bridge JavaScript - **Skill Resources** — Browse and read skill instruction documents via `skills://list` and `skills://{name}` - **Prompt Templates** — Pre-composed prompts for common visualization tasks: `create_widget`, `create_svg_diagram`, `create_visualization` - **Standalone** — No dependencies on other packages; can be deployed independently @@ -72,7 +72,7 @@ pnpm build ```json { "mcpServers": { - "open-intelligent-ui": { + "open-generative-ui": { "command": "node", "args": ["dist/stdio.js"], "cwd": "/absolute/path/to/apps/mcp" @@ -86,7 +86,7 @@ For development, you can use `tsx` directly: ```json { "mcpServers": { - "open-intelligent-ui": { + "open-generative-ui": { "command": "npx", "args": ["tsx", "src/stdio.ts"], "cwd": "/absolute/path/to/apps/mcp" @@ -101,7 +101,7 @@ Add to `.mcp.json`: ```json { - "openIntelligentUI": { + "openGenerativeUI": { "url": "http://localhost:3100/mcp" } } @@ -118,7 +118,7 @@ Then start the HTTP server with `pnpm dev`. ### Tool: `assemble_document` -Wraps an HTML fragment with the complete OpenIntelligentUI design system. +Wraps an HTML fragment with the complete OpenGenerativeUI design system. **Input:** ```typescript @@ -284,13 +284,13 @@ MIT ## Support -For issues, feature requests, or questions, visit the [OpenIntelligentUI repository](https://github.com/CopilotKit/OpenIntelligentUI). +For issues, feature requests, or questions, visit the [OpenGenerativeUI repository](https://github.com/CopilotKit/OpenIntelligentUI). ## Contributing This is a standalone deployment package. To contribute improvements: -1. Fork the main [OpenIntelligentUI repository](https://github.com/CopilotKit/OpenIntelligentUI) +1. Fork the main [OpenGenerativeUI repository](https://github.com/CopilotKit/OpenIntelligentUI) 2. Make changes to `apps/mcp/` (or the source files it's forked from) 3. Submit a pull request diff --git a/apps/mcp/package.json b/apps/mcp/package.json index 14d3fce..5dd31a3 100644 --- a/apps/mcp/package.json +++ b/apps/mcp/package.json @@ -1,7 +1,7 @@ { "name": "open-intelligent-ui-mcp", "version": "0.1.0", - "description": "Standalone MCP server for OpenIntelligentUI design system and skills", + "description": "Standalone MCP server for Open Intelligent UI design system and skills", "type": "module", "main": "dist/index.js", "scripts": { diff --git a/apps/mcp/skills/agent-skills-vol2.txt b/apps/mcp/skills/agent-skills-vol2.txt index a92d5e4..52d2d5e 100644 --- a/apps/mcp/skills/agent-skills-vol2.txt +++ b/apps/mcp/skills/agent-skills-vol2.txt @@ -1,747 +1,62 @@ -# Agent Visualization Skills — Volume 2: Advanced Techniques - -Prerequisite: Volume 1 (SVG diagrams, basic interactive widgets, Chart.js, Mermaid). -This volume covers: UI mockups, dashboards, advanced interactivity, generative art, -simulations, math visualizations, and the design system that ties everything together. - ---- - -## Part 1: The Design System - -Every visual you produce should feel native to the host interface — not like -an embedded iframe from somewhere else. These rules apply to ALL output types. - -### CSS Variables (Auto Light/Dark Mode) - -```css -/* Backgrounds */ ---color-background-primary /* white in light, near-black in dark */ ---color-background-secondary /* surface cards */ ---color-background-tertiary /* page background */ ---color-background-info /* blue tint */ ---color-background-danger /* red tint */ ---color-background-success /* green tint */ ---color-background-warning /* amber tint */ - -/* Text */ ---color-text-primary /* main text */ ---color-text-secondary /* muted / labels */ ---color-text-tertiary /* hints / placeholders */ ---color-text-info / -danger / -success / -warning - -/* Borders */ ---color-border-tertiary /* default: 0.15 alpha */ ---color-border-secondary /* hover: 0.3 alpha */ ---color-border-primary /* active: 0.4 alpha */ - -/* Typography */ ---font-sans /* default body font */ ---font-serif /* editorial / blockquote only */ ---font-mono /* code */ - -/* Layout */ ---border-radius-md /* 8px - most elements */ ---border-radius-lg /* 12px - cards */ ---border-radius-xl /* 16px - large containers */ -``` - -**Critical rule**: Never hardcode colors like `#333` or `#fff` in HTML. -They break in the opposite mode. Always use CSS variables. - -### Typography Rules -- h1 = 22px, h2 = 18px, h3 = 16px — all font-weight: 500 -- Body = 16px, weight 400, line-height: 1.7 -- Only two weights: 400 (regular) and 500 (medium). Never 600 or 700. -- Sentence case everywhere. Never Title Case or ALL CAPS. -- No mid-sentence bolding. Use `code style` for entity/class/function names. -- No font-size below 11px anywhere. - -### Component Tokens -- Borders: `0.5px solid var(--color-border-tertiary)` -- Cards: `background: var(--color-background-primary)`, - `border: 0.5px solid var(--color-border-tertiary)`, - `border-radius: var(--border-radius-lg)`, `padding: 1rem 1.25rem` -- No gradients, drop shadows, blur, glow, or neon effects -- No emoji — use CSS shapes or SVG paths for icons -- Background of outer container is always transparent - -### Number Formatting -Always round displayed numbers. JavaScript float math leaks artifacts: -`0.1 + 0.2 = 0.30000000000000004`. Every number on screen must go through -`Math.round()`, `.toFixed(n)`, or `Intl.NumberFormat`. - ---- - -## Part 2: UI Mockups - -For when the user asks you to design or prototype a UI. - -### When to Use -- "Design a settings page for..." -- "Mock up a dashboard" -- "What should this form look like?" -- "Show me a card layout for..." -- Prototyping before building - -### Presentation Rules - -**Contained mockups** (mobile screens, modals, chat threads, single cards): -Wrap in a background surface so they don't float naked: -```html -
- -
-``` - -**Full-width mockups** (dashboards, settings pages, data tables): -No wrapper needed — they naturally fill the viewport. - -### Metric Cards (for dashboards) -```html -
- -
-
Total revenue
-
$142,800
-
- -
-
Active users
-
8,421
-
- -
-``` - -### Contact / Data Record Card -```html -
- -
- -
JD
-
-

Jane Doe

-

Lead Engineer

-
-
- -
- - - - - -
- Emailjane@company.com
-
-
-``` - -### Badges and Status Pills -```html - -Active - - -
- Most popular -
-``` - -### Form Elements -Inputs, selects, textareas, buttons, and range sliders are pre-styled -in the host environment. Write bare tags — they inherit correct styling: -- Text inputs: 36px height, hover/focus states built in -- Range sliders: 4px track + 18px thumb -- Buttons: transparent bg, 0.5px border, hover/active states - -**Never use `
` tags.** Use `onClick` / `onChange` handlers directly. - -### Comparison Cards -For "help me choose between X and Y": -```html -
- -
-

- Option A

-

Description here

-
- - -
-``` - ---- - -## Part 3: Advanced Interactive Widgets - -### Simulations and Physics -For teaching physics, algorithms, or systems behavior with real-time updates. - -**Pattern: Animation Loop with Controls** -```html - - - - -
- - - -
- - -``` - -### Math Visualizations -For plotting functions, showing geometric relationships, or exploring equations. - -**Pattern: Function Plotter with SVG** -```html - - - - - - x - y - - - - -
- - -
- - -``` - -### Sortable / Filterable Data Tables -```html - - - - - - - - - - - - - - - -
NameValueStatus
- - -``` - ---- - -## Part 4: Chart.js — Advanced Patterns - -### Dark Mode Awareness -```javascript -const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; -const textColor = isDark ? '#c2c0b6' : '#3d3d3a'; -const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)'; -const tooltipBg = isDark ? '#2C2C2A' : '#fff'; -``` - -Canvas cannot read CSS variables — always detect dark mode and use -hardcoded hex values. - -### Wrapper Pattern (Critical for Sizing) -```html -
- -
-``` -- Height goes on the wrapper div ONLY, never on canvas. -- Always set `responsive: true, maintainAspectRatio: false`. -- For horizontal bar charts: height = (bars × 40) + 80 pixels. - -### Custom Legend (Always Use This) -Disable Chart.js default legend and build HTML: -```javascript -plugins: { legend: { display: false } } -``` -```html -
- - Series A — 65% - - - Series B — 35% - -
-``` - -### Dashboard Layout -Metric cards on top → chart below → sendPrompt for drill-down: -```html - -
- -
- - -
- -
-``` - -### Chart Type Selection Guide -| Data pattern | Chart type | -|----------------------------|---------------------| -| Trend over time | Line | -| Category comparison | Vertical bar | -| Ranking (few items) | Horizontal bar | -| Part of whole | Doughnut | -| Distribution | Histogram (bar) | -| Correlation (2 variables) | Scatter | -| Multi-variable comparison | Radar | -| Range / uncertainty | Line with fill area | - ---- - -## Part 5: Generative Art and Illustration - -For when the user asks for something creative, decorative, or aesthetic. - -### When to Use -- "Draw me a sunset" / "Create a pattern" -- Decorative headers or visual breaks -- Mood illustrations for creative writing -- Abstract visualizations of data or music - -### Rules (Different from Diagrams) -- Fill the canvas — art should feel rich, not sparse -- Bold colors are encouraged. You can use custom hex freely. -- Layered overlapping shapes create depth -- Organic forms with `` curves, ``, `` -- Texture via repetition (hatching, dots, parallel lines) -- Geometric patterns with `` -- NO gradients, shadows, blur, or glow (still flat aesthetic) - -### Pattern: Geometric Art -```svg - - - - - - - - - - - - - -``` - -### Pattern: Radial Symmetry -```svg - - - - - - - - - - - - -``` - -### Pattern: Landscape with Layered Shapes -For physical scenes, use ALL hardcoded hex (no theme classes): -```svg - - - - - - - - - - - -``` - ---- - -## Part 6: Advanced Patterns - -### Tabbed / Multi-View Interfaces -Since content streams top-down, don't use `display: none` during streaming. -Instead, render all content stacked, then use post-stream JS to create tabs: - -```html -
- - - -
- -
-
-
- - -``` - -### sendPrompt() — Chat-Driven Interactivity -A global function that sends a message as if the user typed it. -Use it when the user's next action benefits from AI thinking: - -```html - - -``` - -**Use for**: drill-downs, follow-up questions, "explain this part". -**Don't use for**: filtering, sorting, toggling — handle those in JS. -Append ` ↗` to button text when it triggers sendPrompt. - -### Responsive Grid Pattern -```css -display: grid; -grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); -gap: 12px; -``` -Use `minmax(0, 1fr)` if children have large min-content that could overflow. - -### CSS Animations (Subtle and Purposeful) -```css -/* Only animate transform and opacity for performance */ -@keyframes fadeSlideIn { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } -} - -/* Always respect user preferences */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - } -} - -/* Flowing particles / convection currents */ -@keyframes flow { to { stroke-dashoffset: -20; } } -.flowing { - stroke-dasharray: 5 5; - animation: flow 1.6s linear infinite; -} - -/* Pulsing for active elements */ -@keyframes pulse { - 0%, 100% { opacity: 0.3; } - 50% { opacity: 0.7; } -} -``` - ---- - -## Part 7: External Libraries (CDN Allowlist) - -Only these CDN origins work (CSP-enforced): -- `cdnjs.cloudflare.com` -- `esm.sh` -- `cdn.jsdelivr.net` -- `unpkg.com` - -### Useful Libraries - -**Chart.js** (data visualization): -```html - -``` - -**Three.js** (3D graphics): -```html - -``` - -**D3.js** (advanced data viz, force layouts, geographic maps): -```html - -``` - -**Mermaid** (ERDs, sequence diagrams, class diagrams): -```html - -``` - -**Tone.js** (audio synthesis): -```html - -``` - ---- - -## Part 8: Quality Checklist - -Before producing any visual, run through this: - -### Functional -- [ ] Does it work without JavaScript during streaming? (Content visible) -- [ ] Do all interactive controls have event handlers? -- [ ] Are all displayed numbers rounded properly? -- [ ] Does the canvas/SVG fit within the container width? - -### Visual -- [ ] Dark mode test: would every element be readable on near-black? -- [ ] No hardcoded text colors in HTML (use CSS variables) -- [ ] No gradients, shadows, blur, or glow -- [ ] Borders are 0.5px (except 2px for featured item accent) -- [ ] Font weights are only 400 or 500 -- [ ] All text is sentence case - -### Content -- [ ] Explanatory text is in the response, not inside the widget -- [ ] No titles or headings embedded in the HTML output -- [ ] Visual is self-explanatory without reading the narration -- [ ] Narration adds value beyond what the visual shows -- [ ] Offered a clear "go deeper" path - -### Accessibility -- [ ] `@media (prefers-reduced-motion: reduce)` for all animations -- [ ] Text contrast is sufficient (dark text on light fills, vice versa) -- [ ] Interactive elements are large enough to click (min 44px touch target) -- [ ] No information conveyed by color alone - ---- - -## Part 9: Decision Matrix — Picking the Right Visual - -| User asks about... | Output type | Technology | -|-----------------------------|--------------------------|---------------------| -| How X works (physical) | Illustrative diagram | SVG | -| How X works (abstract) | Interactive explainer | HTML + inline SVG | -| Process / steps | Flowchart | SVG | -| Architecture / containment | Structural diagram | SVG | -| Database schema / ERD | Relationship diagram | Mermaid | -| Trends over time | Line chart | Chart.js | -| Category comparison | Bar chart | Chart.js | -| Part of whole | Doughnut chart | Chart.js | -| KPIs / metrics | Dashboard | HTML metric cards | -| Design a UI | Mockup | HTML | -| Choose between options | Comparison cards | HTML grid | -| Cyclic process | Step-through | HTML stepper | -| Physics / math | Simulation | Canvas + JS | -| Function / equation | Plotter | SVG + JS | -| Data exploration | Sortable table | HTML + JS | -| Creative / decorative | Art / illustration | SVG | -| 3D visualization | 3D scene | Three.js | -| Music / audio | Synthesizer | Tone.js | -| Network / graph | Force layout | D3.js | -| Quick factual answer | Plain text | None | -| Code solution | Code block | None | -| Emotional support | Warm text | None | +# Open Generative UI — interactive implementation + +Read the master playbook for routing and the standalone assemble_document contract. +This server returns HTML text with theme CSS and legacy postMessage helpers. It +has no injected importmap or Websandbox connection. generateSandboxedUi in the web +app uses a separate streaming contract; do not mix the two environments. + +## Structure + +Supply html with styles and scripts suitable for an isolated iframe. Use theme +variables such as --color-text-primary, --color-text-secondary, +--color-background-secondary, --color-border-tertiary, and --border-radius-md. +Keep useful content visible before scripts load. Include headings, assumptions, +sources, units, and instructions where they help a self-contained answer. + +Use responsive grids, wrapping controls, and viewBox SVG. For charts, bound the +container height and resize with its container. Resolve theme colors for canvas +using getComputedStyle. Provide a textual summary or accessible table alongside +canvas. Respect reduced motion; pause animation and keep one loop per simulation. +Use real Three.js geometry and responsive camera controls for 3D when it is useful. +Dispose resources on teardown and display initialization failures visibly. + +## Numeric tools + +For input[type=number], read valueAsNumber, reject non-finite values and out-of-range +inputs, and check denominators. min/max alone do not validate user typing. Show a +clear error and mark invalid fields; avoid stale output. Preserve internal precision +and format displayed values with Intl.NumberFormat. Include units, assumptions, +reset behavior, and the formula used. Never eval a user-supplied expression. + +## Controls + +Use native buttons with clear labels, explicit input labels, visible keyboard focus, +and role=status for feedback. Forms must prevent unwanted navigation; non-submit +buttons use type=button. Use buttons inside sortable table headers. Sorting and +filtering should use the same state and preserve one another. Insert data using +textContent rather than interpolating values into HTML. Interactive tabs require +proper keyboard/ARIA behavior; simple toggle buttons can use aria-pressed instead. + +## Libraries and network + +Allowed script CDN origins are cdnjs.cloudflare.com, esm.sh, cdn.jsdelivr.net, +and unpkg.com. Load pinned library URLs and handle errors; bare imports require an +explicit importmap, which this renderer does not inject. Module scripts may use +imports; classic scripts cannot use top-level await. Styles are inline under the +current CSP; do not assume arbitrary stylesheet or API URLs work. Do not rely on +cookies, localStorage, or parent DOM in a properly isolated sandbox. + +## Follow-up actions + +Use sendPrompt(text) only when a consuming host explicitly supports the legacy +bridge. It posts a request without delivery acknowledgement. Do not claim success +or substitute this helper for the web app's validated +Websandbox.connection.remote.sendPrompt({ text }). Include selected values only +following a deliberate user click. Local controls must not send chat messages. +The standalone host must validate requests; never send credentials or secrets. + +## Evidence + +Show user data or genuine retrieved evidence. Explicitly label sample metrics, +comparison weights, and simulation assumptions. No fabricated current weather, +market prices, business records, or citations. A renderer supplies no live data. diff --git a/apps/mcp/skills/master-agent-playbook.txt b/apps/mcp/skills/master-agent-playbook.txt index e27ff25..7c76329 100644 --- a/apps/mcp/skills/master-agent-playbook.txt +++ b/apps/mcp/skills/master-agent-playbook.txt @@ -1,432 +1,51 @@ -# Master Agent Playbook: Making AI Responses Extraordinary - -This playbook teaches an AI coding agent how to go beyond plain text and deliver -responses that are visual, interactive, and deeply educational. It covers the -philosophy, decision-making, and technical skills needed. - ---- - -## Part 1: The Core Philosophy - -### Think Like a Teacher, Not a Search Engine - -Bad: "A load path is the route that forces take through a structure to the ground." -Good: [draws an interactive building cross-section with loads flowing downward] - -The principle: **Show, don't just tell.** Before writing any response, ask: -- Would a diagram make this click faster than a paragraph? -- Would an interactive widget let the user explore the concept themselves? -- Would a worked example teach better than a definition? - -### The Response Decision Tree - -``` -User asks a question - │ - ├─ Is it a quick factual answer? → Answer in 1-2 sentences. - │ - ├─ Is it conceptual / "how does X work"? - │ ├─ Is it spatial or visual? → SVG illustrative diagram - │ ├─ Is it a process/flow? → SVG flowchart or HTML stepper - │ ├─ Is it data-driven? → Interactive chart (Chart.js / Recharts) - │ └─ Is it abstract but explorable? → Interactive HTML widget with controls - │ - ├─ Is it "build me X"? → Working code artifact, fully functional - │ - ├─ Is it a comparison? → Side-by-side table or comparative visual - │ - └─ Is it emotional/personal? → Warm text response. No visuals needed. -``` - -### The 3-Layer Response Pattern - -Great responses layer information: - -1. **Hook** (1-2 sentences): Validate the question, set context. -2. **Visual** (diagram/widget): The core explanation, rendered visually. -3. **Narration** (2-4 paragraphs): Walk through the visual, add nuance, - connect to what the user already knows. Offer to go deeper. - -Never dump a visual without narration. Never narrate without visuals -when visuals would help. - ---- - -## Part 2: Skill — Interactive HTML Widgets - -For concepts that benefit from user exploration. More powerful than -static SVGs — users can manipulate parameters and see results. - -### When to Use -- The concept has a variable the user could tweak (temperature, rate, count) -- The system has states the user could toggle (on/off, mode A/B) -- The explanation benefits from stepping through stages -- Data exploration or filtering is involved - -### Template: Interactive Widget with Controls - -```html - - - - - - - - - -
- -
- - -``` - -### Template: Step-Through Explainer - -For cyclic or staged processes (event loops, biological cycles, pipelines). - -```html - - -
- -
- -
- -
- - Step 1 of 4 -
- - -``` - -### CSS Animation Patterns (for live diagrams) - -```css -/* Flowing particles along a path */ -@keyframes flow { - to { stroke-dashoffset: -20; } -} -.flowing { - stroke-dasharray: 5 5; - animation: flow 1.6s linear infinite; -} - -/* Pulsing glow for active elements */ -@keyframes pulse { - 0%, 100% { opacity: 0.3; } - 50% { opacity: 0.7; } -} -.pulsing { animation: pulse 2s ease-in-out infinite; } - -/* Flickering (for flames, sparks) */ -@keyframes flicker { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.8; } -} - -/* Always respect reduced motion preferences */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - } -} -``` - ---- - -## Part 3: Skill — Data Visualization - -### When to Use -- Comparing quantities -- Showing trends over time -- Displaying distributions or proportions -- Making data explorable - -### Approach: Inline SVG Charts (No Dependencies) - -For simple charts, hand-draw in SVG. No library needed. - -```svg - - - - - - - - - - Q1 - $42k - - -``` - -### Approach: Chart.js (For Complex/Interactive Charts) - -When you need tooltips, responsive legends, animations: - -```html - - - -``` - ---- - -## Part 4: Skill — Mermaid Diagrams - -For relationship diagrams (ERDs, class diagrams, sequence diagrams) where -precise layout math isn't worth doing by hand. - -```html -
- -``` - -Use Mermaid for: ERDs, class diagrams, sequence diagrams, Gantt charts. -Use hand-drawn SVG for: everything else (flowcharts, architecture, -illustrative diagrams) — you get much better control. - ---- - -## Part 5: Skill — Explanatory Writing Between Visuals - -### Narration Patterns - -**The Walk-Through**: Point at parts of the visual and explain them. -> "Starting at the top, the roof deck collects distributed loads across -> its surface. These get channeled into the rafters below, which act -> like one-way bridges..." - -**The "Why It Matters"**: Connect the visual to real consequences. -> "This is why lower columns are always larger — they're carrying the -> accumulated weight of every floor above." - -**The "Common Mistake"**: Anticipate misconceptions. -> "One thing that trips people up: removing a single column doesn't just -> lose that member — it breaks the entire load chain." - -**The "Go Deeper" Offer**: End with expansion paths. -> "Want me to show how lateral loads (wind, seismic) take a completely -> different path?" - -### Tone Rules -- Warm and direct. Not academic, not dumbed-down. -- Use "you" and "we" freely. -- Analogies and metaphors are powerful. Use them. -- Short paragraphs (2-4 sentences). No walls of text. -- Bold key terms on first introduction, then don't re-bold. -- Never use bullet points for explanations. Prose only. -- Ask at most one question per response. - ---- - -## Part 6: Skill — Knowing What NOT to Visualize - -Not everything needs a diagram. Skip visuals when: - -- The answer is a single fact or number -- The user is venting or emotional (empathy, not charts) -- The topic is purely textual (writing, editing, drafting) -- A code snippet is the answer (just show the code) -- The user explicitly asked for brief/concise - -### The "Would They Screenshot This?" Test - -If the user would likely screenshot or save the visual to reference -later, it was worth making. If not, just use text. - ---- - -## Part 7: Putting It All Together - -### Example Response Structure (Complex Technical Question) - -``` -[1-2 sentence hook validating the question] - -[Visual: SVG diagram or interactive widget] - -[Walk-through narration: 3-4 paragraphs explaining the visual, - pointing at specific parts, noting key insights] - -[One "go deeper" offer with 2-3 specific directions] -``` - -### Example Response Structure (Simple Question with Visual Aid) - -``` -[Direct answer in 1-2 sentences] - -[Small supporting visual if it adds value] - -[One additional insight or context sentence] -``` - -### Quality Checklist Before Responding - -- [ ] Did I pick the right format? (text vs SVG vs interactive vs chart) -- [ ] Is the visual self-explanatory even without the narration? -- [ ] Does the narration add value beyond what the visual shows? -- [ ] Are colors meaningful, not decorative? -- [ ] Does it work in dark mode? -- [ ] Is the response concise? (Cut anything that doesn't teach) -- [ ] Did I offer a clear next step? - ---- - -## Appendix: Quick Reference - -| Concept Type | Best Format | -|-------------------------|------------------------------| -| How X works (physical) | Illustrative SVG diagram | -| How X works (abstract) | Interactive HTML + SVG | -| Process / workflow | SVG flowchart | -| Architecture | SVG structural diagram | -| Data relationships | Mermaid ERD | -| Trends / comparisons | Chart.js or SVG bar chart | -| Cyclic process | HTML step-through widget | -| System states | Interactive widget + toggles | -| Quick answer | Plain text | -| Code solution | Code block / artifact | -| Emotional support | Warm text only | +# Open Generative UI by CopilotKit — response playbook + +Give people answers they can interact with when interaction helps. Choose text for +simple facts, writing, and code. Use native tools when the connected host exposes a +suitable component. Use a custom UI for an explainer, comparison, or useful tool. + +Understand: show relationships and allow meaningful exploration. +Compare: show criteria, assumptions, and tradeoffs; keep simple comparisons simple. +Make a tool: supply functional inputs, validated computation, outputs, and reset. + +There is no required acknowledgement/plan/build/narrate ceremony. Multiple distinct +sections are welcome. Avoid repeating a successful build or duplicating its content +in narration. Use headings and explanations inside the artifact when helpful. + +## This server's actual contract + +This standalone MCP server exposes assemble_document with title, description, and +html. Supply a self-contained HTML body fragment including any inline style and +script blocks. It returns an HTML document as text; it does not itself display a UI, +fetch data, deploy a site, execute a transaction, or provide an agent conversation. +The consuming host must render it in an isolated iframe and implement bridge handling. + +Theme variables, form styles, and SVG classes are injected. The standalone document +has legacy sendPrompt(text) and openLink(url) helpers posting messages to its parent. +They do not acknowledge delivery: do not claim a question was sent or an action +completed merely because the helper returned. A compatible host must validate +message source, text, URLs, and authorization. Without host support, offer ordinary +instructions rather than follow-up buttons that cannot work. Trigger messages only +on deliberate user clicks, never on load, timers, or routine input changes. + +This differs from the web app's generateSandboxedUi tool. When that tool is actually +available, follow its ordered six-field contract and validated Websandbox bridge, +not this standalone helper API. Do not assume this MCP renderer injects an importmap +or provides Websandbox.connection.remote. + +## Reliable data and controls + +Distinguish actual user/tool data, calculations, and labeled sample assumptions. +Never invent current weather, prices, business metrics, retrieval times, or sources. +A visual component is not a data source. If required facts cannot be retrieved, +ask for data or offer explicitly illustrative inputs. Do not collect credentials or +imply mock forms authenticate or persist records. + +All enabled controls must work. Validate blanks, non-finite numbers, bounds, and +zero divisors; show useful errors rather than NaN/Infinity or stale results. +State units and formulas, format precision, and reset state consistently. +Keep local calculations and filtering local. Use textContent for untrusted values. + +Use semantic headings and labels, keyboard-operable controls, visible focus, +adequate contrast, reduced motion, and responsive layouts. Provide text alternatives +for charts. Do not rely on color alone. Show library-loading errors visibly. diff --git a/apps/mcp/skills/svg-diagram-skill.txt b/apps/mcp/skills/svg-diagram-skill.txt index 2a28385..90f2742 100644 --- a/apps/mcp/skills/svg-diagram-skill.txt +++ b/apps/mcp/skills/svg-diagram-skill.txt @@ -13,7 +13,7 @@ You can generate rich, inline SVG diagrams to visually explain concepts. Use thi ## SVG Setup -Always use this template: +Use this template as a starting point; add an accessible title and description: ```svg @@ -28,10 +28,10 @@ Always use this template: ``` -- **Width is always 680px** via viewBox. Set `width="100%"` so it scales responsively. +- **680px is a useful default coordinate width** via viewBox. Set `width="100%"` so it scales responsively. - **H (height)** = bottom-most element's y + height + 40px padding. Don't guess — compute it. - **Safe content area**: x=40 to x=640, y=40 to y=(H-40). -- **No wrapping divs**, no ``, ``, ``, or DOCTYPE. +- **Body fragments only**: no ``, ``, ``, or DOCTYPE. - **Background is transparent** — the host provides the background. --- @@ -190,7 +190,7 @@ For complex topics, use multiple smaller SVGs instead of one dense one: ## Example: Simple 3-Step Flow ```svg - + @@ -243,3 +243,11 @@ For complex topics, use multiple smaller SVGs instead of one dense one: - **Annotations on the side**: Put explanatory labels in the right margin (x > 560) with leader lines pointing to the relevant element. - **Consistent heights**: All boxes of the same type should be the same height. - **Whitespace is your friend**: Don't fill every pixel. Breathing room makes diagrams readable. + +## Host and accessibility + +Use assemble_document for the standalone MCP HTML contract. If the web app exposes +generateSandboxedUi, follow that tool's separate ordered streaming parameters. +Diagrams are optional when text is clearer. Include an accessible title/description +and keep labels readable on narrow screens. Reflow complex diagrams instead of +shrinking all text. Label illustrative data; never invent current evidence. diff --git a/apps/mcp/src/server.ts b/apps/mcp/src/server.ts index 813bd9c..a2caf8a 100644 --- a/apps/mcp/src/server.ts +++ b/apps/mcp/src/server.ts @@ -1,11 +1,14 @@ -import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { + McpServer, + ResourceTemplate, +} from "@modelcontextprotocol/sdk/server/mcp.js"; import * as z from "zod"; import { listSkills, loadSkill } from "./skills.js"; import { assembleDocument } from "./renderer.js"; export function createMcpServer(): McpServer { const server = new McpServer({ - name: "open-intelligent-ui", + name: "open-generative-ui", version: "0.1.0", }); @@ -13,7 +16,10 @@ export function createMcpServer(): McpServer { server.registerResource( "skills-list", "skills://list", - { description: "JSON array of available skill names", mimeType: "application/json" }, + { + description: "JSON array of available skill names", + mimeType: "application/json", + }, async () => ({ contents: [ { @@ -22,7 +28,7 @@ export function createMcpServer(): McpServer { text: JSON.stringify(listSkills()), }, ], - }) + }), ); server.registerResource( @@ -36,7 +42,10 @@ export function createMcpServer(): McpServer { })), }), }), - { description: "Full text of a skill instruction document", mimeType: "text/plain" }, + { + description: "Full text of a skill instruction document", + mimeType: "text/plain", + }, async (uri, { name }) => ({ contents: [ { @@ -45,7 +54,7 @@ export function createMcpServer(): McpServer { text: loadSkill(name as string), }, ], - }) + }), ); // Prompts: pre-composed skill prompts @@ -59,7 +68,7 @@ export function createMcpServer(): McpServer { content: { type: "text", text: loadSkill("master-agent-playbook") }, }, ], - }) + }), ); server.registerPrompt( @@ -72,7 +81,7 @@ export function createMcpServer(): McpServer { content: { type: "text", text: loadSkill("svg-diagram-skill") }, }, ], - }) + }), ); server.registerPrompt( @@ -85,7 +94,7 @@ export function createMcpServer(): McpServer { content: { type: "text", text: loadSkill("agent-skills-vol2") }, }, ], - }) + }), ); // Tool: assemble complete HTML document with design system @@ -93,17 +102,23 @@ export function createMcpServer(): McpServer { "assemble_document", { description: - "Wraps HTML with OpenIntelligentUI theme CSS, SVG classes, form styles, and bridge JS. " + + "Wraps HTML with Open Intelligent UI theme CSS, SVG classes, form styles, and bridge JS. " + "Returns a complete iframe-ready HTML document.", inputSchema: { title: z.string().describe("Short title for the visualization"), - description: z.string().describe("One-sentence explanation of what this shows"), - html: z.string().describe("Self-contained HTML fragment with inline