chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1,14 @@
node_modules
.next
.git
.gitignore
.env*
!.env.example
.DS_Store
*.pem
.claude
.worktrees
.langgraph_api
agent/.venv
agent/__pycache__
coverage
@@ -0,0 +1,8 @@
AGENT_URL=http://localhost:8123
OPENAI_API_KEY=
# --- CopilotKit Intelligence (optional; set COPILOTKIT_LICENSE_TOKEN to enable Threads — server + UI) ---
# COPILOTKIT_LICENSE_TOKEN=
# INTELLIGENCE_API_URL=http://localhost:4201
# INTELLIGENCE_GATEWAY_WS_URL=ws://localhost:4401
# INTELLIGENCE_API_KEY=
@@ -0,0 +1,54 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
coverage
test-results/
# next.js
.next/
out/
# production
build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# LangGraph API
.langgraph_api
# Git worktrees
.worktrees
# Tools
.claude
.scratch/
@@ -0,0 +1,284 @@
# CopilotKit + LangGraph Todo Demo
## Purpose
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.
**Target audience:** Developers evaluating CopilotKit or starting new projects with AI agents.
## Core Concept
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
This uses CopilotKit's **v2 agent state pattern** where state lives in the agent and syncs to the frontend.
## Architecture
This is a **flat npm project** with a Next.js frontend at the root and a Python agent in `agent/`.
### Repository Structure
```
├── 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
├── scripts/ # Agent setup and run scripts
│ ├── setup-agent.sh / .bat
│ └── run-agent.sh / .bat
├── package.json # Root project config (npm + concurrently)
└── next.config.ts
```
## 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
# 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
// src/components/canvas/index.tsx
const { agent } = useAgent();
return (
<TodoList
todos={agent.state?.todos || []}
onUpdate={(updatedTodos) => 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** (`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="gpt-5.2",
tools=[*todo_tools, ...], # manage_todos, get_todos
middleware=[CopilotKitMiddleware()],
state_schema=AgentState, # Defines state shape
system_prompt="You are a helpful assistant..."
)
```
**Todo Tools** (`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** (`src/components/canvas/index.tsx`):
```typescript
export function Canvas() {
const { agent } = useAgent(); // CopilotKit v2 hook
return (
<div className="h-full p-8 bg-gray-50">
<TodoList
// Read state from agent
todos={agent.state?.todos || []}
// Update state in agent
onUpdate={(updatedTodos) => agent.setState({ todos: updatedTodos })}
// React to agent execution
isAgentRunning={agent.isRunning}
/>
</div>
);
}
```
**Todo List** (`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 (
<div className="flex gap-8">
<TodoColumn title="To Do" todos={pendingTodos} onAddTodo={addTodo} ... />
<TodoColumn title="Done" todos={completedTodos} ... />
</div>
);
}
```
### 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
**Key insight**: State lives in the agent, frontend just reads/writes to it via CopilotKit hooks.
## Tech Stack
- **Frontend**: Next.js 16, React 19, TailwindCSS 4
- **Agent**: LangGraph (Python), OpenAI GPT-5.2
- **CopilotKit**: React hooks for agent integration (v2)
- **Build**: npm with concurrently for parallel dev processes
- **Other**: Recharts for generative UI examples
## Development
```bash
# Install dependencies (also sets up agent via postinstall)
npm install
# Start both frontend and agent
npm run dev
# Start individually
npm run dev:ui # Next.js frontend on port 3000
npm run dev:agent # LangGraph agent on port 8123
# Build
npm run build
```
### Environment Setup
```bash
# Set OpenAI API key
cp .env.example .env
# Edit .env and add your OPENAI_API_KEY
```
## 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
---
## 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
This pattern works great for **agent-driven applications** where the AI needs to manipulate structured application state, not just chat.
@@ -0,0 +1,76 @@
# Stage 1: Build Next.js frontend
FROM node:20-slim AS frontend
WORKDIR /app
COPY package.json ./
RUN npm install --ignore-scripts
COPY src/ ./src/
COPY public/ ./public/
COPY next.config.ts tsconfig.json postcss.config.mjs ./
COPY showcase.json ./showcase.json
# Docker override: use AG-UI HttpAgent instead of LangGraphAgent
# (LangGraphAgent needs Docker-in-Docker which Railway doesn't provide)
# Next.js 16+ rejects both /api/copilotkit/route.ts AND /api/copilotkit/[[...slug]]/route.ts
RUN rm -f ./src/app/api/copilotkit/\[\[...slug\]\]/route.ts
COPY docker-route-override.ts ./src/app/api/copilotkit/route.ts
RUN npm install @ag-ui/client
ENV NODE_OPTIONS="--max-old-space-size=4096"
# Next.js 16+ uses Turbopack by default; use --webpack for serverExternalPackages compat
RUN npx next build --webpack
# Stage 2: Production image with Python + Node
FROM python:3.12.10-slim AS runner
# Install Node.js 20 + uv
RUN apt-get update && \
apt-get install -y --no-install-recommends curl ca-certificates && \
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Install uv by copying from the official image (avoids curl|sh pipe-swallow bug
# where a 5xx on astral.sh silently produces an exit-0 layer with no uv binary).
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
ENV PATH="/root/.local/bin:$PATH"
WORKDIR /app
# Install Python deps — EXCLUDING langgraph-cli and langgraph-api
# (they need Docker-in-Docker which Railway doesn't provide)
# Instead serve via ag-ui-langgraph + copilotkit (same protocol, no Docker needed)
COPY agent/ ./agent/
# Install ag-ui-langgraph FIRST at pinned version (before copilotkit can override it)
RUN uv pip install --system "ag-ui-langgraph[fastapi]==0.0.37" && \
uv pip install --system --no-deps "copilotkit==0.1.93" && \
uv pip install --system "partialjson>=0.0.8,<0.0.9" "toml>=0.10.2,<0.11.0" && \
uv pip install --system \
"langchain==1.0.1" \
"langchain-openai>=1.1.0" \
"langchain-anthropic>=1.3.4" \
"langgraph==1.0.5" \
"langsmith>=0.4.49" \
"openai>=1.68.2,<2.0.0" \
"python-dotenv>=1.0.0,<2.0.0" \
"fastapi>=0.115.5,<1.0.0" \
"uvicorn>=0.29.0,<1.0.0"
# serve.py adapts the original agent for Docker (no langgraph-cli needed)
COPY serve.py ./
# Copy Next.js standalone build
COPY --from=frontend /app/.next/standalone ./
COPY --from=frontend /app/.next/static ./.next/static
COPY --from=frontend /app/public ./public
COPY entrypoint.sh ./
RUN chmod +x entrypoint.sh
EXPOSE 3000
ENV NODE_ENV=production
CMD ["./entrypoint.sh"]
@@ -0,0 +1,21 @@
The MIT License
Copyright (c) Atai Barkai
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,206 @@
# CopilotKit <> LangGraph Starter
This is a starter template for building AI agents using [LangGraph](https://www.langchain.com/langgraph) and [CopilotKit](https://copilotkit.ai). It provides a modern Next.js application with an integrated LangGraph agent to be built on top of.
https://github.com/user-attachments/assets/47761912-d46a-4fb3-b9bd-cb41ddd02e34
## Prerequisites
- Node.js 18+
- Python 3.12+
- [uv](https://docs.astral.sh/uv/) (Python package manager)
- Any of the following package managers:
- npm (default)
- [pnpm](https://pnpm.io/installation)
- [yarn](https://classic.yarnpkg.com/lang/en/docs/install/)
- [bun](https://bun.sh/)
- OpenAI API Key (for the LangGraph agent)
## Getting Started
1. Install dependencies using your preferred package manager:
```bash
# Using npm (default)
npm install
# Using pnpm
pnpm install
# Using yarn
yarn install
# Using bun
bun install
```
This will also install the Python agent dependencies via `uv sync`.
2. Set up your environment variables:
```bash
cp .env.example .env
```
Then edit the `.env` file and add your OpenAI API key:
```bash
OPENAI_API_KEY=your-openai-api-key-here
```
3. Start the development server:
```bash
# Using npm (default)
npm run dev
# Using pnpm
pnpm dev
# Using yarn
yarn dev
# Using bun
bun run dev
```
This will start both the UI and agent servers concurrently.
## Available Scripts
The following scripts can also be run using your preferred package manager:
- `dev` - Starts both UI and agent servers in development mode
- `dev:debug` - Starts development servers with debug logging enabled
- `dev:ui` - Starts only the Next.js UI server
- `dev:agent` - Starts only the LangGraph agent server
- `build` - Builds the Next.js application for production
- `start` - Starts the production server
- `install:agent` - Installs Python dependencies for the agent
## Project Structure
```
├── src/ # Next.js frontend source
│ ├── app/
│ │ ├── page.tsx # Main page
│ │ └── api/copilotkit/ # CopilotKit API route
│ ├── components/
│ │ ├── example-canvas/ # Todo list UI
│ │ ├── example-layout/ # Layout: chat + canvas side-by-side
│ │ └── generative-ui/ # Example generative UI components
│ └── hooks/
├── agent/ # LangGraph Python agent
│ ├── main.py # Agent entry point
│ └── src/
│ ├── todos.py # Todo tools and state schema
│ └── query.py # Example data query tool
├── scripts/ # Agent setup and run scripts
│ ├── setup-agent.sh / .bat
│ └── run-agent.sh / .bat
├── public/ # Static assets
├── next.config.ts
├── tsconfig.json
└── package.json
```
## A2UI — Agent-to-User Interface
This starter includes [A2UI](https://a2ui.org/specification/) support, allowing the agent to generate rich, interactive UI surfaces declaratively. Instead of returning plain text, the agent sends a JSON description of the UI it wants to render, and the frontend turns it into real components.
### How it works
A2UI uses three concepts:
1. **Catalog** — a set of component definitions (schema) paired with React renderers. Registered once in `layout.tsx` via `<CopilotKitProvider a2ui={{ catalog: demonstrationCatalog }}>`.
2. **Surface** — a rendered UI instance. The agent creates a surface, sets its components, and binds data to it.
3. **Operations** — the agent returns `a2ui.render(operations=[...])` from a tool, which the middleware streams to the frontend.
### Two patterns
| Pattern | Description | Agent tool | Frontend |
| ------------------ | ----------------------------------------------------------------------------- | ---------------- | ------------------------------------------- |
| **Fixed schema** | Pre-defined component layout. Only the data changes per invocation. | `search_flights` | Schema in `a2ui/schemas/flight_schema.json` |
| **Dynamic schema** | A secondary LLM generates both components and data based on the conversation. | `generate_a2ui` | Components decided at runtime |
Both patterns use the same catalog on the frontend — the difference is where the component tree comes from.
### Key files
| Purpose | Path |
| ------------------------------------ | -------------------------------------------------- |
| Catalog definitions (Zod schemas) | `src/app/declarative-generative-ui/definitions.ts` |
| Catalog renderers (React components) | `src/app/declarative-generative-ui/renderers.tsx` |
| Catalog registration | `src/app/layout.tsx` |
| Fixed-schema agent tool | `agent/src/a2ui_fixed_schema.py` |
| Dynamic-schema agent tool | `agent/src/a2ui_dynamic_schema.py` |
| Flight schema JSON | `agent/src/a2ui/schemas/flight_schema.json` |
| Showcase config | `showcase.json` |
### Adding a custom component
1. **Define** the component schema in `definitions.ts`:
```typescript
MyWidget: {
description: "A brief description for the agent.",
props: z.object({ title: z.string(), value: z.number() }),
},
```
2. **Render** it in `renderers.tsx`:
```typescript
MyWidget: ({ props }) => (
<div>{props.title}: {props.value}</div>
),
```
Renderers are type-checked against the definitions — TypeScript will error if props don't match.
3. **Use it** from the agent. The component is automatically available to both fixed-schema templates and the dynamic-schema LLM.
### Adding a new fixed-schema tool
1. Create a JSON schema file in `agent/src/a2ui/schemas/` describing the component tree.
2. Create a Python tool that loads the schema with `a2ui.load_schema()` and returns `a2ui.render(operations=[...])` with your data. See `a2ui_fixed_schema.py` for the pattern.
### Showcase mode
`showcase.json` controls which suggestion pills are visually highlighted. Set `"showcase": "a2ui"` to highlight the A2UI demos, or `"showcase": "default"` for no highlights. This is configured automatically when scaffolding via `npx copilotkit create --framework a2ui`.
### Further reading
- [A2UI Specification](https://a2ui.org/specification/)
- [CopilotKit A2UI Documentation](https://docs.copilotkit.ai)
## Documentation
- [LangGraph Documentation](https://langchain-ai.github.io/langgraph/) - Learn more about LangGraph and its features
- [CopilotKit Documentation](https://docs.copilotkit.ai) - Explore CopilotKit's capabilities
## Contributing
Feel free to submit issues and enhancement requests! This starter is designed to be easily extensible.
## License
This project is licensed under the MIT License - see the LICENSE file for details.
## Troubleshooting
### Agent Connection Issues
If you see "I'm having trouble connecting to my tools", make sure:
1. The LangGraph agent is running on port 8123
2. Your OpenAI API key is set correctly
3. Both servers started successfully
### Python Dependencies
If you encounter Python import errors:
```bash
npm run install:agent
```
@@ -0,0 +1,9 @@
venv/
__pycache__/
*.pyc
.env
.vercel
# python
.venv/
.langgraph_api/
@@ -0,0 +1 @@
3.13
@@ -0,0 +1,10 @@
{
"python_version": "3.12",
"dockerfile_lines": [],
"dependencies": ["."],
"package_manager": "uv",
"graphs": {
"sample_agent": "./main.py:graph"
},
"env": "../.env"
}
@@ -0,0 +1,45 @@
"""
This is the main entry point for the agent.
It defines the workflow graph, state, tools, nodes and edges.
"""
from copilotkit import CopilotKitMiddleware, StateStreamingMiddleware, StateItem
from langchain.agents import create_agent
# Data & state tools
from src.query import query_data
from src.todos import AgentState, todo_tools
# A2UI tools
from src.a2ui_dynamic_schema import generate_a2ui
from src.a2ui_fixed_schema import search_flights
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-5.4-mini", model_kwargs={"parallel_tool_calls": False})
agent = create_agent(
model=model,
tools=[query_data, *todo_tools, generate_a2ui, search_flights],
middleware=[
CopilotKitMiddleware(),
StateStreamingMiddleware(
StateItem(state_key="todos", tool="manage_todos", tool_argument="todos")
),
],
state_schema=AgentState,
system_prompt="""
You are a polished, professional demo assistant. Keep responses to 1-2 sentences.
Tool guidance:
- Flights: call search_flights to show flight cards with a pre-built schema.
- Dashboards & rich UI: call generate_a2ui to create dashboard UIs with metrics,
charts, tables, and cards. It handles rendering automatically.
- Charts: call query_data first, then render with the chart component.
- Todos: enable app mode first, then manage todos.
- A2UI actions: when you see a log_a2ui_event result (e.g. "view_details"),
respond with a brief confirmation. The UI already updated on the frontend.
""",
)
graph = agent
@@ -0,0 +1,22 @@
[project]
name = "sample-agent"
version = "0.1.0"
description = "A LangGraph agent"
requires-python = ">=3.12"
dependencies = [
"langchain==1.2.15",
"langgraph==1.1.6",
"langsmith==0.7.33",
"openai==1.109.1",
"fastapi>=0.115.5,<1.0.0",
"uvicorn>=0.29.0,<1.0.0",
"python-dotenv>=1.0.0,<2.0.0",
"langgraph-cli[inmem]==0.4.21",
"langchain-openai==1.1.9",
"copilotkit==0.1.94",
"ag-ui-protocol==0.1.18",
"langgraph-api==0.7.101",
"pip>=26.0.1",
"langchain-anthropic==1.4.1",
]
@@ -0,0 +1,37 @@
[
{
"id": "root",
"component": "Row",
"children": {
"componentId": "flight-card",
"path": "/flights"
},
"gap": 16
},
{
"id": "flight-card",
"component": "FlightCard",
"airline": { "path": "airline" },
"airlineLogo": { "path": "airlineLogo" },
"flightNumber": { "path": "flightNumber" },
"origin": { "path": "origin" },
"destination": { "path": "destination" },
"date": { "path": "date" },
"departureTime": { "path": "departureTime" },
"arrivalTime": { "path": "arrivalTime" },
"duration": { "path": "duration" },
"status": { "path": "status" },
"price": { "path": "price" },
"action": {
"event": {
"name": "book_flight",
"context": {
"flightNumber": { "path": "flightNumber" },
"origin": { "path": "origin" },
"destination": { "path": "destination" },
"price": { "path": "price" }
}
}
}
}
]
@@ -0,0 +1,111 @@
"""
Dynamic A2UI tool: LLM-generated UI from conversation context.
A secondary LLM generates v0.9 A2UI components via a structured tool call.
The generate_a2ui tool wraps the output as a2ui_operations, which the
middleware detects in the TOOL_CALL_RESULT and renders automatically.
"""
from __future__ import annotations
import json
from typing import Any
from langchain.tools import tool, ToolRuntime
from langchain_core.messages import SystemMessage
from langchain_core.tools import tool as lc_tool
from langchain_openai import ChatOpenAI
from copilotkit import a2ui
CUSTOM_CATALOG_ID = "copilotkit://app-dashboard-catalog"
@lc_tool
def render_a2ui(
surfaceId: str,
catalogId: str,
components: list[dict],
data: dict | None = None,
) -> str:
"""Render a dynamic A2UI v0.9 surface.
Args:
surfaceId: Unique surface identifier.
catalogId: The catalog ID (use "copilotkit://app-dashboard-catalog").
components: A2UI v0.9 component array (flat format). The root
component must have id "root".
data: Optional initial data model for the surface (e.g. form values,
list items for data-bound components).
"""
return "rendered"
@tool()
def generate_a2ui(runtime: ToolRuntime[Any]) -> str:
"""Generate dynamic A2UI components based on the conversation.
A secondary LLM designs the UI schema and data. The result is
returned as an a2ui_operations container for the middleware to detect.
"""
import time
t0 = time.time()
print(f"[A2UI-DEBUG] generate_a2ui STARTED at t=0")
messages = runtime.state["messages"][:-1]
print(f"[A2UI-DEBUG] messages count: {len(messages)}")
# Get context entries from copilotkit state (catalog capabilities + component schema)
context_entries = runtime.state.get("copilotkit", {}).get("context", [])
context_text = "\n\n".join(
entry.get("value", "")
for entry in context_entries
if isinstance(entry, dict) and entry.get("value")
)
print(
f"[A2UI-DEBUG] context entries: {len(context_entries)}, context_text_len: {len(context_text)}"
)
prompt = context_text
model = ChatOpenAI(model="gpt-4.1")
model_with_tool = model.bind_tools(
[render_a2ui],
tool_choice="render_a2ui",
)
print(f"[A2UI-DEBUG] calling secondary LLM at t={time.time() - t0:.1f}s")
response = model_with_tool.invoke(
[SystemMessage(content=prompt), *messages],
)
print(f"[A2UI-RESPONSE] {response}")
print(f"[A2UI-DEBUG] secondary LLM responded at t={time.time() - t0:.1f}s")
if not response.tool_calls:
print(f"[A2UI-DEBUG] ERROR: no tool calls in response")
return json.dumps({"error": "LLM did not call render_a2ui"})
tool_call = response.tool_calls[0]
args = tool_call["args"]
surface_id = args.get("surfaceId", "dynamic-surface")
catalog_id = args.get("catalogId", CUSTOM_CATALOG_ID)
components = args.get("components", [])
data = args.get("data", {})
print(
f"[A2UI-DEBUG] components={len(components)} data_keys={list(data.keys()) if data else []} surface={surface_id}"
)
ops = [
a2ui.create_surface(surface_id, catalog_id=catalog_id),
a2ui.update_components(surface_id, components),
]
if data:
ops.append(a2ui.update_data_model(surface_id, data))
result = a2ui.render(operations=ops)
print(
f"[A2UI-DEBUG] generate_a2ui DONE at t={time.time() - t0:.1f}s result_len={len(result)}"
)
return result
@@ -0,0 +1,63 @@
"""
Fixed-schema A2UI tool: flight search results.
Schema is loaded from a JSON file. Only the data changes per invocation.
"""
from __future__ import annotations
from pathlib import Path
from typing import TypedDict
from copilotkit import a2ui
from langchain.tools import tool
CATALOG_ID = "copilotkit://app-dashboard-catalog"
SURFACE_ID = "flight-search-results"
FLIGHT_SCHEMA = a2ui.load_schema(
Path(__file__).parent / "a2ui" / "schemas" / "flight_schema.json"
)
class Flight(TypedDict):
id: str
airline: str
airlineLogo: str
flightNumber: str
origin: str
destination: str
date: str
departureTime: str
arrivalTime: str
duration: str
status: str
statusIcon: str
price: str
@tool
def search_flights(flights: list[Flight]) -> str:
"""Search for flights and display the results as rich cards. Return exactly 2 flights.
Each flight must have: id, airline (e.g. "United Airlines"),
airlineLogo (use Google favicon API: https://www.google.com/s2/favicons?domain={airline_domain}&sz=128
e.g. "https://www.google.com/s2/favicons?domain=united.com&sz=128" for United,
"https://www.google.com/s2/favicons?domain=delta.com&sz=128" for Delta,
"https://www.google.com/s2/favicons?domain=aa.com&sz=128" for American,
"https://www.google.com/s2/favicons?domain=alaskaair.com&sz=128" for Alaska),
flightNumber, origin, destination,
date (short readable format like "Tue, Mar 18" — use near-future dates),
departureTime, arrivalTime,
duration (e.g. "4h 25m"), status (e.g. "On Time" or "Delayed"),
statusIcon (colored dot: use "https://placehold.co/12/22c55e/22c55e.png"
for On Time, "https://placehold.co/12/eab308/eab308.png" for Delayed,
"https://placehold.co/12/ef4444/ef4444.png" for Cancelled),
and price (e.g. "$289").
"""
return a2ui.render(
operations=[
a2ui.create_surface(SURFACE_ID, catalog_id=CATALOG_ID),
a2ui.update_components(SURFACE_ID, FLIGHT_SCHEMA),
a2ui.update_data_model(SURFACE_ID, {"flights": flights}),
],
)
@@ -0,0 +1,41 @@
date,category,subcategory,amount,type,notes
2026-01-05,Revenue,Enterprise Subscriptions,28000,income,3 new enterprise customers (Acme Corp, TechFlow, DataViz Inc)
2026-01-05,Revenue,Pro Tier Upgrades,18000,income,24 users upgraded from free to pro
2026-01-08,Revenue,API Usage Overages,9500,income,High API usage from top 5 customers
2026-01-10,Expenses,Engineering Salaries,42000,expense,7 engineers + 2 contractors
2026-01-10,Expenses,Product Team,18000,expense,PM and 2 designers
2026-01-12,Expenses,AWS Infrastructure,8200,expense,Increased compute for new AI features
2026-01-15,Expenses,Marketing - Paid Ads,12000,expense,Google Ads and LinkedIn campaigns
2026-01-18,Revenue,Consulting Services,14500,income,Custom integration for Acme Corp
2026-01-20,Expenses,Customer Success,15000,expense,3 CSMs + support tools (Intercom)
2026-01-22,Expenses,AI Model Costs,4200,expense,OpenAI API usage for product features
2026-01-25,Revenue,Marketplace Sales,12800,income,Template and plugin sales
2026-01-28,Expenses,Office & Equipment,3500,expense,New laptops and coworking spaces
2026-02-03,Revenue,Enterprise Subscriptions,31000,income,2 new customers + expansion from TechFlow
2026-02-03,Revenue,Pro Tier Upgrades,22500,income,31 upgrades + reduced churn
2026-02-05,Revenue,API Usage Overages,11800,income,DataViz Inc heavy API usage spike
2026-02-07,Expenses,Engineering Salaries,42000,expense,Same headcount as January
2026-02-07,Expenses,Product Team,18000,expense,No changes to product team
2026-02-10,Expenses,AWS Infrastructure,9500,expense,Traffic spike from viral social post
2026-02-12,Expenses,Marketing - Paid Ads,15000,expense,Increased ad spend for Q1 push
2026-02-14,Revenue,Consulting Services,18000,income,2 custom projects (TechFlow + new client)
2026-02-18,Expenses,Customer Success,16500,expense,Hired 1 additional CSM
2026-02-20,Expenses,AI Model Costs,5800,expense,Increased usage from new AI features launch
2026-02-22,Revenue,Marketplace Sales,14200,income,Top template hit featured list
2026-02-25,Expenses,Conference & Travel,4500,expense,Team attended SaaS Conference 2026
2026-02-27,Revenue,Partnership Revenue,11500,income,Referral fees from integration partners
2026-03-02,Revenue,Enterprise Subscriptions,35000,income,Major win: Fortune 500 customer signed
2026-03-02,Revenue,Pro Tier Upgrades,26000,income,42 upgrades - best month yet
2026-03-05,Revenue,API Usage Overages,13200,income,Consistent high usage across top tier
2026-03-08,Expenses,Engineering Salaries,48000,expense,Hired 1 senior engineer for AI team
2026-03-08,Expenses,Product Team,21000,expense,Promoted designer to senior level
2026-03-10,Expenses,AWS Infrastructure,11000,expense,Scaled infrastructure for enterprise client
2026-03-12,Expenses,Marketing - Paid Ads,18000,expense,Doubled down on successful campaigns
2026-03-14,Revenue,Consulting Services,21500,income,Fortune 500 onboarding + 2 other projects
2026-03-16,Expenses,Customer Success,19500,expense,Hired dedicated enterprise CSM
2026-03-18,Expenses,AI Model Costs,7200,expense,Fortune 500 client heavy AI usage
2026-03-20,Revenue,Marketplace Sales,15800,income,3 new templates in top 10
2026-03-22,Expenses,Sales & BD,12000,expense,Hired first sales rep for enterprise
2026-03-24,Revenue,Partnership Revenue,14200,income,New integration partnerships launched
2026-03-26,Expenses,Security & Compliance,6500,expense,SOC 2 audit and security tools
2026-03-28,Revenue,Training & Workshops,10200,income,Conducted 2 customer training sessions
1 date,category,subcategory,amount,type,notes
2 2026-01-05,Revenue,Enterprise Subscriptions,28000,income,3 new enterprise customers (Acme Corp, TechFlow, DataViz Inc)
3 2026-01-05,Revenue,Pro Tier Upgrades,18000,income,24 users upgraded from free to pro
4 2026-01-08,Revenue,API Usage Overages,9500,income,High API usage from top 5 customers
5 2026-01-10,Expenses,Engineering Salaries,42000,expense,7 engineers + 2 contractors
6 2026-01-10,Expenses,Product Team,18000,expense,PM and 2 designers
7 2026-01-12,Expenses,AWS Infrastructure,8200,expense,Increased compute for new AI features
8 2026-01-15,Expenses,Marketing - Paid Ads,12000,expense,Google Ads and LinkedIn campaigns
9 2026-01-18,Revenue,Consulting Services,14500,income,Custom integration for Acme Corp
10 2026-01-20,Expenses,Customer Success,15000,expense,3 CSMs + support tools (Intercom)
11 2026-01-22,Expenses,AI Model Costs,4200,expense,OpenAI API usage for product features
12 2026-01-25,Revenue,Marketplace Sales,12800,income,Template and plugin sales
13 2026-01-28,Expenses,Office & Equipment,3500,expense,New laptops and coworking spaces
14 2026-02-03,Revenue,Enterprise Subscriptions,31000,income,2 new customers + expansion from TechFlow
15 2026-02-03,Revenue,Pro Tier Upgrades,22500,income,31 upgrades + reduced churn
16 2026-02-05,Revenue,API Usage Overages,11800,income,DataViz Inc heavy API usage spike
17 2026-02-07,Expenses,Engineering Salaries,42000,expense,Same headcount as January
18 2026-02-07,Expenses,Product Team,18000,expense,No changes to product team
19 2026-02-10,Expenses,AWS Infrastructure,9500,expense,Traffic spike from viral social post
20 2026-02-12,Expenses,Marketing - Paid Ads,15000,expense,Increased ad spend for Q1 push
21 2026-02-14,Revenue,Consulting Services,18000,income,2 custom projects (TechFlow + new client)
22 2026-02-18,Expenses,Customer Success,16500,expense,Hired 1 additional CSM
23 2026-02-20,Expenses,AI Model Costs,5800,expense,Increased usage from new AI features launch
24 2026-02-22,Revenue,Marketplace Sales,14200,income,Top template hit featured list
25 2026-02-25,Expenses,Conference & Travel,4500,expense,Team attended SaaS Conference 2026
26 2026-02-27,Revenue,Partnership Revenue,11500,income,Referral fees from integration partners
27 2026-03-02,Revenue,Enterprise Subscriptions,35000,income,Major win: Fortune 500 customer signed
28 2026-03-02,Revenue,Pro Tier Upgrades,26000,income,42 upgrades - best month yet
29 2026-03-05,Revenue,API Usage Overages,13200,income,Consistent high usage across top tier
30 2026-03-08,Expenses,Engineering Salaries,48000,expense,Hired 1 senior engineer for AI team
31 2026-03-08,Expenses,Product Team,21000,expense,Promoted designer to senior level
32 2026-03-10,Expenses,AWS Infrastructure,11000,expense,Scaled infrastructure for enterprise client
33 2026-03-12,Expenses,Marketing - Paid Ads,18000,expense,Doubled down on successful campaigns
34 2026-03-14,Revenue,Consulting Services,21500,income,Fortune 500 onboarding + 2 other projects
35 2026-03-16,Expenses,Customer Success,19500,expense,Hired dedicated enterprise CSM
36 2026-03-18,Expenses,AI Model Costs,7200,expense,Fortune 500 client heavy AI usage
37 2026-03-20,Revenue,Marketplace Sales,15800,income,3 new templates in top 10
38 2026-03-22,Expenses,Sales & BD,12000,expense,Hired first sales rep for enterprise
39 2026-03-24,Revenue,Partnership Revenue,14200,income,New integration partnerships launched
40 2026-03-26,Expenses,Security & Compliance,6500,expense,SOC 2 audit and security tools
41 2026-03-28,Revenue,Training & Workshops,10200,income,Conducted 2 customer training sessions
@@ -0,0 +1,22 @@
from langchain.tools import tool
from pathlib import Path
import csv
# Read data at module load time to avoid file I/O issues in
# LangGraph Cloud's sandboxed tool execution environment.
_csv_path = Path(__file__).parent / "db.csv"
with open(_csv_path) as _f:
_cached_data = list(csv.DictReader(_f))
@tool
def query_data(query: str):
"""
Query the database, takes natural language. Always call before showing a chart or graph.
"""
import time
print(
f"[A2UI-DEBUG] query_data called: query='{query[:60]}' at {time.strftime('%H:%M:%S')}"
)
return _cached_data
@@ -0,0 +1,56 @@
from langchain.agents import AgentState as BaseAgentState
from langchain.tools import ToolRuntime, tool
from langchain.messages import ToolMessage
from langgraph.types import Command
from typing import TypedDict, Literal
import uuid
class Todo(TypedDict):
id: str
title: str
description: str
emoji: str
status: Literal["pending", "completed"]
class AgentState(BaseAgentState):
todos: list[Todo]
@tool
def manage_todos(todos: list[Todo], runtime: ToolRuntime) -> Command:
"""
Manage the current todos.
"""
# Ensure all todos have IDs that are unique
for todo in todos:
if "id" not in todo or not todo["id"]:
todo["id"] = str(uuid.uuid4())
# Update the state
return Command(
update={
"todos": todos,
"messages": [
ToolMessage(
content="Successfully updated todos",
tool_call_id=runtime.tool_call_id,
)
],
}
)
@tool
def get_todos(runtime: ToolRuntime):
"""
Get the current todos.
"""
return runtime.state.get("todos", [])
todo_tools = [
manage_todos,
get_todos,
]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,77 @@
# Docker Compose stack for e2e smoke testing with aimock.
# Mirrors the user experience: Next.js frontend + langgraph dev agent + aimock as LLM.
# Usage: docker compose -f docker-compose.test.yml up -d
services:
aimock:
image: ghcr.io/copilotkit/aimock:latest
volumes:
- ./fixtures:/fixtures:ro
command:
["--fixtures", "/fixtures", "--host", "0.0.0.0", "--validate-on-load"]
agent:
build:
context: ./agent
dockerfile: ../docker/Dockerfile.agent
environment:
- OPENAI_API_KEY=test-key-for-aimock
- OPENAI_BASE_URL=http://aimock:4010/v1
depends_on:
aimock:
condition: service_started
healthcheck:
test:
[
"CMD",
"python",
"-c",
"import urllib.request; urllib.request.urlopen('http://localhost:8123/ok')",
]
interval: 5s
timeout: 5s
retries: 30
start_period: 30s
app:
build:
context: .
dockerfile: docker/Dockerfile.app
environment:
- LANGGRAPH_DEPLOYMENT_URL=http://agent:8123
depends_on:
agent:
condition: service_healthy
healthcheck:
test:
[
"CMD",
"node",
"-e",
"const http = require('http'); const req = http.get('http://localhost:3000/', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); }); req.on('error', () => process.exit(1)); req.setTimeout(2000, () => { req.destroy(); process.exit(1); });",
]
interval: 5s
timeout: 10s
retries: 30
start_period: 60s
tests:
image: mcr.microsoft.com/playwright:v1.52.0-noble
working_dir: /tests
volumes:
- ../../../showcase/tests:/tests
- test-results:/tests/test-results
environment:
- STARTER=${STARTER:-langgraph-python}
- STARTER_URL=http://app:3000
depends_on:
app:
condition: service_healthy
command:
[
"bash",
"-c",
"cd /tests && npm install --no-audit --no-fund 2>/dev/null && npx playwright install chromium && npx playwright test starter-smoke --reporter=list",
]
volumes:
test-results:
@@ -0,0 +1,32 @@
/**
* Docker-specific route override.
* In Docker, the agent is served via AG-UI (not LangGraph Platform)
* because langgraph-cli dev requires Docker-in-Docker.
* The original route.ts (using LangGraphAgent) is preserved unchanged.
*/
import {
CopilotRuntime,
InMemoryAgentRunner,
createCopilotEndpoint,
} from "@copilotkit/runtime/v2";
import { HttpAgent } from "@ag-ui/client";
import { handle } from "hono/vercel";
const agentUrl = process.env.AGENT_URL || "http://localhost:8123";
const defaultAgent = new HttpAgent({
url: `${agentUrl}/`,
});
const runtime = new CopilotRuntime({
agents: { default: defaultAgent },
runner: new InMemoryAgentRunner(),
});
const app = createCopilotEndpoint({
runtime,
basePath: "/api/copilotkit",
});
export const GET = handle(app);
export const POST = handle(app);
@@ -0,0 +1,21 @@
# Dockerfile for the Python LangGraph agent.
# Mirrors the user experience: uv sync + langgraph dev (same as npm run dev:agent).
FROM python:3.12-slim
# Install uv by copying from the official image (avoids curl|sh pipe-swallow bug
# where a 5xx on astral.sh silently produces an exit-0 layer with no uv binary).
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
WORKDIR /app
# Copy agent source and install deps the same way users do (uv sync)
COPY pyproject.toml uv.lock langgraph.json ./
COPY src/ ./src/
COPY main.py ./
RUN uv sync
EXPOSE 8123
# Run langgraph dev the same way the npm wrapper does
CMD ["uv", "run", "langgraph", "dev", "--host", "0.0.0.0", "--port", "8123", "--no-browser"]
@@ -0,0 +1,50 @@
# Dockerfile for Next.js App
FROM node:22-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Copy package files
COPY package.json ./
# Install dependencies
RUN npm install --ignore-scripts
# Build the application
FROM base AS builder
WORKDIR /app
# Copy dependencies from deps stage
COPY --from=deps /app/node_modules ./node_modules
# Copy source code
COPY . .
# Build the Next.js app
RUN npx next build
# Production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy built application
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
set -e
echo "[entrypoint] Starting: langgraph-python starter"
if [ -z "$OPENAI_API_KEY" ]; then
echo "[entrypoint] WARNING: OPENAI_API_KEY not set!"
else
echo "[entrypoint] OPENAI_API_KEY: set"
fi
# Start agent via AG-UI protocol (serve.py wraps the original graph)
echo "[entrypoint] Starting agent on port 8123..."
AGENT_PORT=8123 python serve.py 2>&1 &
AGENT_PID=$!
sleep 3
# Start Next.js standalone
echo "[entrypoint] Starting Next.js on port ${PORT:-3000}..."
HOSTNAME=0.0.0.0 PORT=${PORT:-3000} node server.js 2>&1 &
NEXT_PID=$!
echo "[entrypoint] Agent=$AGENT_PID Next=$NEXT_PID"
wait -n $AGENT_PID $NEXT_PID
exit $?
@@ -0,0 +1,180 @@
{
"fixtures": [
{
"match": { "toolCallId": "call_manage_todos_001" },
"response": {
"content": "I've added the tasks to your todo list:\n\n1. **Read the docs** - Read the CopilotKit documentation\n2. **Build a prototype** - Build a small CopilotKit prototype\n3. **Explore agent state** - Explore CopilotKit shared agent state\n\nAll three tasks are currently pending. Would you like to modify any of them or add more tasks?"
}
},
{
"match": { "toolCallId": "call_get_todos_001" },
"response": {
"content": "Here are your current todos:\n\n1. **Review project plan** - Go through the Q2 project plan and provide feedback (pending)\n2. **Update documentation** - Update the API docs with the new endpoints (pending)\n\nYou have 2 tasks, both pending. Would you like to update or add any tasks?"
}
},
{
"match": { "toolCallId": "call_query_data_pie_001" },
"response": {
"toolCalls": [
{
"name": "pieChart",
"arguments": "{\"title\":\"Revenue Distribution by Category\",\"description\":\"Share of total revenue across product categories\",\"data\":[{\"label\":\"Enterprise\",\"value\":2100000},{\"label\":\"Mid-Market\",\"value\":1500000},{\"label\":\"SMB\",\"value\":1200000},{\"label\":\"Self-Serve\",\"value\":800000}]}",
"id": "call_pie_chart_001"
}
]
}
},
{
"match": { "toolCallId": "call_query_data_bar_001" },
"response": {
"toolCalls": [
{
"name": "barChart",
"arguments": "{\"title\":\"Expenses by Category\",\"description\":\"Operating expenses across the business\",\"data\":[{\"label\":\"Salaries\",\"value\":1800000},{\"label\":\"Infrastructure\",\"value\":640000},{\"label\":\"Marketing\",\"value\":420000},{\"label\":\"R&D\",\"value\":520000},{\"label\":\"Operations\",\"value\":310000}]}",
"id": "call_bar_chart_001"
}
]
}
},
{
"match": { "toolCallId": "call_pie_chart_001" },
"response": {
"content": "Here's the revenue distribution as a pie chart. Enterprise leads at $2.1M, followed by Mid-Market ($1.5M), SMB ($1.2M), and Self-Serve ($0.8M). Want me to break any segment down further?"
}
},
{
"match": { "toolCallId": "call_bar_chart_001" },
"response": {
"content": "Here's the expense breakdown as a bar chart. Salaries are the largest line at $1.8M, with Infrastructure ($0.64M), R&D ($0.52M), Marketing ($0.42M), and Operations ($0.31M) following. Want to compare this against revenue?"
}
},
{
"match": { "toolCallId": "call_query_data_dashboard_001" },
"response": {
"content": "Here's your sales dashboard:\n\n- **Total Revenue**: $6.6M (up 20% QoQ)\n- **New Customers**: 1,240\n- **Conversion Rate**: 3.8%\n\nRevenue is trending up steadily across all four quarters. Want me to drill into any of these metrics?"
}
},
{
"match": { "toolCallId": "call_query_data_001" },
"response": {
"content": "Here's a summary of the financial data:\n\n- **Q1 Revenue**: $1.2M\n- **Q2 Revenue**: $1.5M\n- **Q3 Revenue**: $1.8M\n- **Q4 Revenue**: $2.1M\n\nTotal annual revenue: $6.6M, showing steady quarter-over-quarter growth of approximately 20%. Would you like me to visualize this data differently?"
}
},
{
"match": { "toolCallId": "call_schedule_time_001" },
"response": {
"content": "Your 30-minute meeting to learn about CopilotKit is scheduled. I've added it to your calendar — let me know if you'd like to change the time."
}
},
{
"match": { "userMessage": "pie chart of our revenue" },
"response": {
"toolCalls": [
{
"name": "query_data",
"arguments": "{\"query\":\"Show revenue distribution by category as a pie chart\"}",
"id": "call_query_data_pie_001"
}
]
}
},
{
"match": { "userMessage": "bar chart of our expenses" },
"response": {
"toolCalls": [
{
"name": "query_data",
"arguments": "{\"query\":\"Show expenses by category as a bar chart\"}",
"id": "call_query_data_bar_001"
}
]
}
},
{
"match": {
"userMessage": "schedule a 30-minute meeting to learn about CopilotKit"
},
"response": {
"toolCalls": [
{
"name": "scheduleTime",
"arguments": "{\"reasonForScheduling\":\"Learn about CopilotKit\",\"meetingDuration\":30}",
"id": "call_schedule_time_001"
}
]
}
},
{
"match": { "userMessage": "flights from SFO to JFK" },
"response": {
"content": "I found a few options for flights from SFO to JFK next Tuesday:\n\n- **UA 1234** — departs 7:30 AM, arrives 4:05 PM, 1 stop, $312\n- **DL 5678** — departs 10:15 AM, arrives 6:50 PM, nonstop, $389\n- **AA 9012** — departs 1:40 PM, arrives 10:20 PM, nonstop, $415\n\nWould you like me to hold one of these for you?"
}
},
{
"match": {
"userMessage": "sales dashboard with total revenue, new customers, and conversion rate"
},
"response": {
"toolCalls": [
{
"name": "query_data",
"arguments": "{\"query\":\"Fetch financial sales data for a sales dashboard with revenue, customers, and conversion rate\"}",
"id": "call_query_data_dashboard_001"
}
]
}
},
{
"match": {
"userMessage": "Use Excalidraw to create a simple network diagram"
},
"response": {
"content": "Here's a simple network diagram for your topology:\n\n```\n [Router]\n / \\\n [Switch A] [Switch B]\n / \\ / \\\n [PC 1] [PC 2] [PC 3] [PC 4]\n```\n\nThe router connects to two switches, and each switch connects to two computers. In the live demo this renders as an interactive Excalidraw diagram via the Excalidraw MCP app."
}
},
{
"match": { "userMessage": "build a modern calculator" },
"response": {
"content": "Here's the plan for your generative calculator app:\n\n- **Standard keypad** — digits 0-9, decimal point, and the four operators (+, -, ×, ÷) with clear and equals.\n- **Metric shortcut buttons** — labeled chips for **Total Revenue ($6.6M)**, **New Customers (1,240)**, and **Conversion Rate (3.8%)** that insert their value into the display when clicked.\n- **Live display** — shows the running expression and result.\n\nIn the live demo this renders as a fully interactive sandboxed UI via the generateSandboxedUi tool."
}
},
{
"match": {
"userMessage": "Toggle the app theme using the toggleTheme tool"
},
"response": {
"toolCalls": [
{
"name": "toggleTheme",
"arguments": "{}",
"id": "call_toggle_theme_001"
}
]
}
},
{
"match": { "userMessage": "add three todos about learning CopilotKit" },
"response": {
"toolCalls": [
{
"name": "manage_todos",
"arguments": "{\"todos\":[{\"title\":\"Read the docs\",\"description\":\"Read the CopilotKit documentation\",\"emoji\":\"📚\",\"status\":\"pending\"},{\"title\":\"Build a prototype\",\"description\":\"Build a small CopilotKit prototype\",\"emoji\":\"🛠️\",\"status\":\"pending\"},{\"title\":\"Explore agent state\",\"description\":\"Explore CopilotKit shared agent state\",\"emoji\":\"🧠\",\"status\":\"pending\"}]}",
"id": "call_manage_todos_001"
}
]
}
},
{
"match": { "userMessage": "Hello" },
"response": {
"content": "Hello! I'm your todo app assistant. I can help you manage your tasks, query data, and more. Here's what I can do:\n\n- **Manage Todos** - Add, update, or remove tasks\n- **View Todos** - See your current task list\n- **Query Data** - Look up and analyze data\n- **Search Flights** - Find flight information\n\nWhat would you like to do today?"
}
},
{
"match": {},
"response": {
"content": "You're currently running against aimock (a mock LLM server). This response is a catch-all for requests that don't match any test fixture. To use a real LLM: (1) Add your OPENAI_API_KEY to .env, (2) Remove or unset OPENAI_BASE_URL from your environment so requests go to OpenAI instead of aimock, (3) Restart with `npm run dev`."
}
}
]
}
@@ -0,0 +1,25 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
serverExternalPackages: ["@copilotkit/runtime"],
env: {
// The public Threads UI flag is DERIVED from the server-side license token.
// Set COPILOTKIT_LICENSE_TOKEN (only) to enable Threads — do not set this flag
// directly. NOTE: NEXT_PUBLIC_* resolves at BUILD time while the runtime reads
// the token per-request, so the UI gate and runtime agree only when the token is
// present at build time (the standard `next dev` / host-build flow). For a
// standalone/Docker image built without the token and injected at runtime, set
// COPILOTKIT_LICENSE_TOKEN at build time too (or gate the UI at runtime) so the
// baked flag reflects it.
NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED: process.env.COPILOTKIT_LICENSE_TOKEN
? "true"
: "false",
},
typescript: {
// Docker route override uses HttpAgent which has a type mismatch with CopilotRuntime
ignoreBuildErrors: true,
},
};
export default nextConfig;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
{
"name": "copilotkit-langgraph-template",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "concurrently \"npm run dev:ui\" \"npm run dev:agent\" --names ui,agent --prefix-colors blue,green --kill-others",
"dev:debug": "LOG_LEVEL=debug npm run dev",
"dev:ui": "next dev --turbopack",
"dev:agent": "./scripts/run-agent.sh || scripts\\run-agent.bat",
"build": "next build",
"start": "next start",
"install:agent": "./scripts/setup-agent.sh || scripts\\setup-agent.bat",
"postinstall": "npm run install:agent"
},
"dependencies": {
"@copilotkit/a2ui-renderer": "1.62.3",
"@copilotkit/react-core": "1.62.3",
"@copilotkit/runtime": "1.62.3",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-separator": "^1.1.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"hono": "^4.12.10",
"lucide-react": "^0.577.0",
"next": "16.1.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-rnd": "^10.5.2",
"react18-json-view": "^0.2.9",
"recharts": "^3.7.0",
"tailwind-merge": "^3.5.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"concurrently": "^9.1.2",
"tailwindcss": "^4",
"typescript": "^5"
}
}
@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
@@ -0,0 +1,30 @@
<svg viewBox="0 0 224.43 237.166" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M76.4556 75.7547C97.0552 48.81 114.152 22.1656 120.731 0.648813C120.908 0.0633572 121.594 -0.185355 122.103 0.152636C144.979 15.3013 186.646 25.2726 223.5 25.5066C224.134 25.5107 224.571 26.1359 224.342 26.7272C212.088 57.8146 197.122 113.518 196.54 177.128C196.54 178.073 195.21 178.412 194.742 177.59C173.768 140.886 106.586 89.3107 76.7986 77.138C76.2477 76.9114 76.0919 76.2307 76.4556 75.7547Z" fill="url(#paint0_linear)"/>
<path d="M145.956 59.273C113.757 69.4674 84.3336 75.1349 77.3077 76.4226C76.8608 76.5047 76.7673 77.1231 77.1934 77.2977C107.209 89.777 174.059 141.202 194.835 177.757C194.877 177.837 194.981 177.867 195.064 177.83C195.147 177.791 195.189 177.687 195.158 177.597L145.956 59.273Z" fill="url(#paint1_linear)"/>
<path d="M122.197 0.0860308C149.76 15.1211 181.615 21.8738 223.875 25.4319C224.135 25.4546 224.228 25.8103 223.989 25.9339C218.585 28.7116 187.623 44.4667 164.633 52.905C158.47 55.1657 152.275 57.263 146.174 59.1979C146.039 59.2402 145.894 59.1736 145.842 59.0446L121.563 0.655481C121.397 0.262302 121.823 -0.117886 122.197 0.0860308Z" fill="url(#paint2_linear)"/>
<path d="M121.361 0.145972C121.761 -0.0218955 122.214 0.121754 122.45 0.467128L122.536 0.6264L196.496 177.058L196.548 177.233C196.628 177.642 196.415 178.065 196.015 178.233C195.615 178.401 195.162 178.257 194.926 177.912L194.838 177.753L120.881 1.32093L120.826 1.14599C120.745 0.736568 120.961 0.313703 121.361 0.145972Z" fill="#513C9F"/>
<path d="M223.089 25.5869C223.52 25.3424 224.069 25.4925 224.313 25.9238C224.558 26.3552 224.406 26.9035 223.974 27.1483V27.1509H223.969C223.965 27.153 223.96 27.1575 223.953 27.1614C223.939 27.1695 223.916 27.1821 223.888 27.1979C223.832 27.2294 223.749 27.2755 223.64 27.3363C223.419 27.4593 223.091 27.6413 222.661 27.8768C221.8 28.3482 220.529 29.0371 218.885 29.9029C215.597 31.6348 210.813 34.0821 204.83 36.9423C192.865 42.6619 176.091 50.0388 156.86 56.6737C137.624 63.3089 117.782 68.4614 102.755 71.9534C95.2398 73.6998 88.9245 75.0317 84.4882 75.9274C82.2701 76.3752 80.5211 76.7135 79.3262 76.9405C78.7293 77.0538 78.2706 77.1391 77.9606 77.1963C77.8058 77.2249 77.6873 77.2472 77.6081 77.2616C77.5693 77.2687 77.5395 77.2737 77.5194 77.2773C77.5095 77.2791 77.501 77.2816 77.4959 77.2825H77.488L77.3052 77.2982C76.8881 77.2877 76.5205 76.9859 76.4436 76.5593C76.356 76.0711 76.6815 75.6027 77.1695 75.5149H77.1773C77.182 75.514 77.1888 75.5113 77.1982 75.5096C77.2176 75.5061 77.2481 75.501 77.287 75.494C77.3647 75.4798 77.4812 75.4595 77.6342 75.4313C77.9413 75.3746 78.3978 75.2882 78.992 75.1754C80.1806 74.9497 81.9227 74.6138 84.1331 74.1676C88.5551 73.2748 94.8521 71.9459 102.348 70.204C117.342 66.7197 137.119 61.5841 156.276 54.9766C175.426 48.3694 192.134 41.0193 204.055 35.3208C210.015 32.4718 214.777 30.0352 218.047 28.3128C219.682 27.4518 220.944 26.7694 221.796 26.3024C222.222 26.0693 222.546 25.8906 222.763 25.7697C222.871 25.7092 222.954 25.6619 223.008 25.6313C223.035 25.6163 223.055 25.6049 223.068 25.5974C223.075 25.5937 223.08 25.5914 223.084 25.5895L223.086 25.5869H223.089Z" fill="#513C9F"/>
<path d="M2.77027 236.611C2.20838 237.273 1.21525 237.353 0.553517 236.792C-0.107154 236.23 -0.18789 235.239 0.373357 234.577L2.77027 236.611ZM132.306 21.7584C133.136 22.0085 133.607 22.8858 133.358 23.7167L106.569 112.86H169.57L169.886 112.891C170.602 113.037 171.142 113.672 171.142 114.431C171.142 115.191 170.602 115.825 169.886 115.972L169.57 116.003H105.182L2.77027 236.611L1.57181 235.593L0.373357 234.577L103.044 113.664L130.347 22.8133C130.597 21.9819 131.474 21.5086 132.306 21.7584Z" fill="#ABABAB"/>
<path d="M72.9636 210.65L60.8346 212.356C67.1226 228.985 80.0206 236.249 95.4131 236.249C133.141 236.249 121.625 193.585 143.482 193.585C159.342 193.585 152.899 228.165 187.02 228.165C207.848 228.165 209.927 207.183 206.372 198.156C206.351 198.101 206.331 198.051 206.299 198.002L200.718 189.455C200.355 188.887 199.471 189.101 199.409 189.776L198.369 200.135C198.297 200.856 198.317 201.574 198.401 202.293C199.253 209.45 199.804 226.818 187.02 226.818C173.54 226.818 170.297 192.686 143.482 192.686C112.032 192.686 116.075 234.902 96.7643 234.902C84.0221 234.902 74.3043 220.531 72.9636 210.65Z" fill="url(#paint3_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="171.825" y1="13.8344" x2="135.895" y2="112.635" gradientUnits="userSpaceOnUse">
<stop stop-color="#6430AB"/>
<stop offset="1" stop-color="#AA89D8"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="143.981" y1="69.5214" x2="97.7306" y2="158.891" gradientUnits="userSpaceOnUse">
<stop stop-color="#005DBB"/>
<stop offset="1" stop-color="#3D92E8"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="164.633" y1="13.8337" x2="150.706" y2="57.3959" gradientUnits="userSpaceOnUse">
<stop stop-color="#1B70C4"/>
<stop offset="1" stop-color="#54A4F2"/>
</linearGradient>
<linearGradient id="paint3_linear" x1="60.8346" y1="213.57" x2="207.775" y2="213.57" gradientUnits="userSpaceOnUse">
<stop stop-color="#4497EA"/>
<stop offset="0.254755" stop-color="#1463B2"/>
<stop offset="0.498725" stop-color="#0A437D"/>
<stop offset="0.666667" stop-color="#2476C8"/>
<stop offset="0.972542" stop-color="#0C549A"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

@@ -0,0 +1,46 @@
<svg viewBox="0 0 1044.21 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(7.74 31.74)">
<g id="CopilotKit">
<path d="M111.689 53.5173H78.8802C78.647 50.8028 78.0267 48.3403 77.0185 46.1299C76.0487 43.9195 74.6918 42.0192 72.9466 40.4291C71.2403 38.8006 69.1654 37.5591 66.7223 36.7063C64.2791 35.8143 61.5062 35.3683 58.4038 35.3683C52.9745 35.3683 48.379 36.6867 44.6173 39.3235C40.8943 41.9611 38.0633 45.742 36.1243 50.6669C34.224 55.5919 33.2739 61.5063 33.2739 68.4092C33.2739 75.7001 34.2434 81.8084 36.1825 86.7334C38.1602 91.6199 41.0106 95.3042 44.7336 97.7855C48.4566 100.229 52.9358 101.45 58.1711 101.45C61.1573 101.45 63.8331 101.082 66.1988 100.345C68.5644 99.5695 70.6198 98.4647 72.3649 97.0293C74.11 95.5947 75.5258 93.8687 76.6117 91.8523C77.7362 89.7965 78.4923 87.4895 78.8802 84.9297L111.689 85.1629C111.301 90.2041 109.885 95.3426 107.442 100.578C104.999 105.774 101.528 110.583 97.0293 115.004C92.57 119.386 87.0435 122.916 80.4507 125.591C73.8579 128.267 66.1988 129.605 57.4731 129.605C46.5369 129.605 36.7254 127.259 28.0384 122.567C19.3904 117.874 12.5456 110.971 7.50404 101.858C2.50134 92.7443 0 81.5948 0 68.4092C0 55.1467 2.55952 43.9776 7.6786 34.9026C12.7977 25.7893 19.7006 18.9057 28.3875 14.252C37.0744 9.55957 46.7696 7.2133 57.4731 7.2133C64.9965 7.2133 71.9189 8.24101 78.2403 10.2964C84.5614 12.3518 90.1067 15.3572 94.877 19.3129C99.6472 23.2297 103.486 28.0578 106.395 33.7978C109.303 39.5371 111.068 46.1102 111.689 53.5173Z" fill="#010507"/>
<path d="M169.874 129.606C160.179 129.606 151.88 127.686 144.977 123.847C138.074 119.968 132.781 114.578 129.096 107.675C125.412 100.733 123.57 92.6864 123.57 83.5337C123.57 74.3818 125.412 66.3544 129.096 59.4515C132.781 52.5092 138.074 47.1186 144.977 43.2796C151.88 39.4014 160.179 37.4626 169.874 37.4626C179.569 37.4626 187.868 39.4014 194.771 43.2796C201.674 47.1186 206.968 52.5092 210.652 59.4515C214.336 66.3544 216.178 74.3818 216.178 83.5337C216.178 92.6864 214.336 100.733 210.652 107.675C206.968 114.578 201.674 119.968 194.771 123.847C187.868 127.686 179.569 129.606 169.874 129.606ZM170.107 105.872C172.822 105.872 175.168 104.96 177.145 103.138C179.123 101.315 180.655 98.7169 181.741 95.3428C182.827 91.9686 183.37 87.9553 183.37 83.3013C183.37 78.6087 182.827 74.5946 181.741 71.2597C180.655 67.8856 179.123 65.2873 177.145 63.4648C175.168 61.6422 172.822 60.7306 170.107 60.7306C167.237 60.7306 164.775 61.6422 162.719 63.4648C160.664 65.2873 159.093 67.8856 158.007 71.2597C156.922 74.5946 156.378 78.6087 156.378 83.3013C156.378 87.9553 156.922 91.9686 158.007 95.3428C159.093 98.7169 160.664 101.315 162.719 103.138C164.775 104.96 167.237 105.872 170.107 105.872Z" fill="#010507"/>
<path d="M229.208 161.484V38.6256H261.086V54.2155H261.784C262.948 51.1908 264.655 48.4182 266.903 45.8976C269.153 43.3377 271.945 41.3016 275.28 39.7893C278.615 38.2376 282.493 37.4626 286.914 37.4626C292.809 37.4626 298.413 39.0331 303.726 42.174C309.077 45.3157 313.421 50.2603 316.756 57.0078C320.13 63.7561 321.817 72.52 321.817 83.3013C321.817 93.6168 320.208 102.168 316.989 108.955C313.809 115.741 309.543 120.802 304.191 124.137C298.878 127.473 293.041 129.14 286.682 129.14C282.493 129.14 278.751 128.461 275.455 127.104C272.197 125.708 269.405 123.827 267.078 121.462C264.79 119.057 263.025 116.342 261.784 113.317H261.319V161.484H229.208ZM260.621 83.3013C260.621 87.6444 261.183 91.4064 262.308 94.5866C263.471 97.7275 265.101 100.171 267.195 101.916C269.327 103.622 271.868 104.476 274.815 104.476C277.762 104.476 280.263 103.642 282.319 101.974C284.413 100.268 286.003 97.8437 287.089 94.7028C288.214 91.5226 288.776 87.7221 288.776 83.3013C288.776 78.8804 288.214 75.0995 287.089 71.9578C286.003 68.7776 284.413 66.3544 282.319 64.6866C280.263 62.9803 277.762 62.1267 274.815 62.1267C271.868 62.1267 269.327 62.9803 267.195 64.6866C265.101 66.3544 263.471 68.7776 262.308 71.9578C261.183 75.0995 260.621 78.8804 260.621 83.3013Z" fill="#010507"/>
<path d="M335.269 127.977V38.6253H367.38V127.977H335.269ZM351.324 29.318C346.981 29.318 343.258 27.8834 340.156 25.0136C337.054 22.1438 335.502 18.6923 335.502 14.6592C335.502 10.6259 337.054 7.17442 340.156 4.30463C343.258 1.43485 346.981 0 351.324 0C355.707 0 359.429 1.43485 362.493 4.30463C365.596 7.17442 367.147 10.6259 367.147 14.6592C367.147 18.6923 365.596 22.1438 362.493 25.0136C359.429 27.8834 355.707 29.318 351.324 29.318Z" fill="#010507"/>
<path d="M415.851 8.84214V127.977H383.74V8.84214H415.851Z" fill="#010507"/>
<path d="M475.258 129.606C465.563 129.606 457.264 127.686 450.361 123.847C443.458 119.968 438.164 114.578 434.48 107.675C430.796 100.733 428.954 92.6864 428.954 83.5337C428.954 74.3818 430.796 66.3544 434.48 59.4515C438.164 52.5092 443.458 47.1186 450.361 43.2796C457.264 39.4014 465.563 37.4626 475.258 37.4626C484.954 37.4626 493.253 39.4014 500.156 43.2796C507.059 47.1186 512.352 52.5092 516.036 59.4515C519.72 66.3544 521.563 74.3818 521.563 83.5337C521.563 92.6864 519.72 100.733 516.036 107.675C512.352 114.578 507.059 119.968 500.156 123.847C493.253 127.686 484.954 129.606 475.258 129.606ZM475.491 105.872C478.205 105.872 480.552 104.96 482.53 103.138C484.508 101.315 486.039 98.7169 487.125 95.3428C488.211 91.9686 488.754 87.9553 488.754 83.3013C488.754 78.6087 488.211 74.5946 487.125 71.2597C486.039 67.8856 484.508 65.2873 482.53 63.4648C480.552 61.6422 478.205 60.7306 475.491 60.7306C472.621 60.7306 470.158 61.6422 468.103 63.4648C466.048 65.2873 464.477 67.8856 463.391 71.2597C462.305 74.5946 461.763 78.6087 461.763 83.3013C461.763 87.9553 462.305 91.9686 463.391 95.3428C464.477 98.7169 466.048 101.315 468.103 103.138C470.158 104.96 472.621 105.872 475.491 105.872Z" fill="#010507"/>
<path d="M587.877 38.6253V61.8941H529.008V38.6253H587.877ZM540.41 17.2186H572.52V99.2396C572.52 100.481 572.734 101.528 573.16 102.381C573.587 103.196 574.246 103.816 575.138 104.242C576.03 104.63 577.174 104.824 578.57 104.824C579.539 104.824 580.664 104.708 581.944 104.476C583.262 104.242 584.232 104.048 584.852 103.894L589.506 126.464C588.072 126.891 586.016 127.415 583.34 128.035C580.703 128.655 577.561 129.063 573.916 129.256C566.548 129.644 560.363 128.888 555.36 126.987C550.357 125.049 546.595 122.004 544.075 117.854C541.554 113.705 540.332 108.508 540.41 102.265V17.2186Z" fill="#010507"/>
<path d="M602.435 127.977V8.84214H634.778V57.0077H636.406L672.24 8.84214H709.935L669.681 61.8942L710.866 127.977H672.24L645.481 83.3012L634.778 97.2626V127.977H602.435Z" fill="#010507"/>
<path d="M725.597 127.977V38.6253H757.708V127.977H725.597ZM741.653 29.318C737.309 29.318 733.586 27.8834 730.484 25.0136C727.381 22.1438 725.831 18.6923 725.831 14.6592C725.831 10.6259 727.381 7.17442 730.484 4.30463C733.586 1.43485 737.309 0 741.653 0C746.035 0 749.758 1.43485 752.821 4.30463C755.924 7.17442 757.475 10.6259 757.475 14.6592C757.475 18.6923 755.924 22.1438 752.821 25.0136C749.758 27.8834 746.035 29.318 741.653 29.318Z" fill="#010507"/>
<path d="M827.354 38.6253V61.8941H768.484V38.6253H827.354ZM779.886 17.2186H811.994V99.2396C811.994 100.481 812.206 101.528 812.632 102.381C813.066 103.196 813.72 103.816 814.612 104.242C815.504 104.63 816.65 104.824 818.05 104.824C819.015 104.824 820.136 104.708 821.421 104.476C822.739 104.242 823.704 104.048 824.326 103.894L828.983 126.464C827.551 126.891 825.489 127.415 822.812 128.035C820.177 128.655 817.035 129.063 813.393 129.256C806.024 129.644 799.838 128.888 794.836 126.987C789.833 125.049 786.071 122.004 783.55 117.854C781.03 113.705 779.808 108.508 779.886 102.265V17.2186Z" fill="#010507"/>
</g>
</g>
<svg x="859.58" y="7.1" width="176.717" height="186.745" viewBox="0 0 176.717 186.745" fill="none">
<path d="M60.2013 59.6493C76.4214 38.4331 89.8836 17.4532 95.0639 0.510877C95.203 0.0498883 95.7431 -0.145948 96.1441 0.120187C114.156 12.0483 146.965 19.8996 175.984 20.0839C176.484 20.0871 176.827 20.5795 176.647 21.045C166.999 45.5233 155.214 89.384 154.756 139.471C154.756 140.215 153.708 140.482 153.34 139.835C136.825 110.934 83.9258 70.3234 60.4713 60.7386C60.0376 60.5602 59.9148 60.0242 60.2013 59.6493Z" fill="url(#paint0_linear)"/>
<path d="M114.926 46.6717C89.5724 54.6988 66.4044 59.1614 60.8722 60.1754C60.5203 60.24 60.4466 60.7269 60.7822 60.8644C84.4167 70.6906 137.054 111.183 153.414 139.967C153.446 140.03 153.528 140.052 153.594 140.024C153.659 139.993 153.692 139.911 153.667 139.841L114.926 46.6717Z" fill="url(#paint1_linear)"/>
<path d="M96.218 0.0677872C117.921 11.9064 143.004 17.2235 176.279 20.0252C176.484 20.043 176.558 20.3231 176.369 20.4204C172.114 22.6076 147.734 35.0132 129.632 41.6575C124.779 43.4376 119.902 45.089 115.098 46.6126C114.991 46.6459 114.877 46.5934 114.836 46.4919L95.7188 0.516173C95.5878 0.206583 95.9234 -0.0927773 96.218 0.0677872Z" fill="url(#paint2_linear)"/>
<path d="M95.5597 0.114978C95.875 -0.0172013 96.2317 0.0959082 96.417 0.367856L96.4849 0.493267L154.721 139.416L154.762 139.554C154.825 139.876 154.657 140.209 154.343 140.341C154.027 140.473 153.671 140.36 153.485 140.088L153.415 139.963L95.1814 1.04014L95.1383 0.902396C95.075 0.580014 95.2449 0.247049 95.5597 0.114978Z" fill="#513C9F"/>
<path d="M175.661 20.1472C176 19.9546 176.432 20.0729 176.625 20.4124C176.818 20.7522 176.697 21.1839 176.357 21.3767V21.3787H176.353C176.351 21.3803 176.346 21.3839 176.341 21.3869C176.33 21.3933 176.312 21.4033 176.29 21.4157C176.246 21.4405 176.18 21.4768 176.094 21.5247C175.921 21.6215 175.662 21.7648 175.323 21.9503C174.646 22.3215 173.645 22.8639 172.351 23.5456C169.761 24.9093 165.995 26.8363 161.283 29.0884C151.862 33.592 138.655 39.4006 123.512 44.625C108.365 49.8495 92.7416 53.9066 80.9093 56.6563C74.9919 58.0314 70.0192 59.0801 66.5261 59.7854C64.7796 60.138 63.4024 60.4044 62.4615 60.5831C61.9916 60.6723 61.6304 60.7395 61.3863 60.7845C61.2644 60.807 61.1711 60.8246 61.1087 60.8359C61.0782 60.8415 61.0547 60.8454 61.0388 60.8483C61.0311 60.8497 61.0244 60.8517 61.0203 60.8524H61.0142L60.8702 60.8647C60.5418 60.8565 60.2523 60.6188 60.1918 60.2829C60.1228 59.8986 60.3791 59.5297 60.7633 59.4605H60.7695C60.7732 59.4599 60.7786 59.4578 60.786 59.4564C60.8013 59.4537 60.8252 59.4497 60.8559 59.4441C60.9171 59.4329 61.0088 59.417 61.1293 59.3947C61.3711 59.3501 61.7305 59.2821 62.1984 59.1933C63.1343 59.0155 64.506 58.7511 66.2465 58.3997C69.7284 57.6967 74.6866 56.6503 80.5886 55.2788C92.3949 52.5352 107.968 48.4914 123.052 43.2887C138.13 38.0862 151.286 32.2987 160.673 27.8117C165.366 25.5684 169.116 23.6497 171.691 22.2936C172.978 21.6156 173.972 21.0783 174.643 20.7105C174.978 20.527 175.233 20.3863 175.404 20.2911C175.489 20.2435 175.554 20.2062 175.597 20.1822C175.618 20.1704 175.634 20.1614 175.644 20.1554C175.649 20.1525 175.654 20.1507 175.656 20.1493L175.658 20.1472H175.661Z" fill="#513C9F"/>
<path d="M2.18131 186.308C1.73888 186.829 0.956885 186.893 0.435837 186.45C-0.0843773 186.008 -0.147949 185.227 0.293978 184.707L2.18131 186.308ZM104.178 17.1327C104.832 17.3295 105.202 18.0203 105.006 18.6746L83.9124 88.8658H133.52L133.768 88.8905C134.333 89.0058 134.757 89.5053 134.757 90.1035C134.757 90.7016 134.333 91.2011 133.768 91.3164L133.52 91.3411H82.8207L2.18131 186.308L1.23765 185.506L0.293978 184.707L81.1369 89.499L102.636 17.9632C102.832 17.3086 103.523 16.9359 104.178 17.1327Z" fill="#ABABAB"/>
<path d="M57.4516 165.866L47.9012 167.209C52.8524 180.303 63.0083 186.023 75.1284 186.023C104.835 186.023 95.7677 152.43 112.978 152.43C125.466 152.43 120.393 179.658 147.26 179.658C163.66 179.658 165.297 163.136 162.498 156.029C162.481 155.985 162.465 155.946 162.44 155.907L158.046 149.177C157.759 148.73 157.064 148.898 157.015 149.43L156.196 157.587C156.139 158.154 156.155 158.72 156.221 159.286C156.892 164.921 157.326 178.597 147.26 178.597C136.645 178.597 134.092 151.722 112.978 151.722C88.2142 151.722 91.3977 184.962 76.1923 184.962C66.1591 184.962 58.5073 173.647 57.4516 165.866Z" fill="url(#paint3_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="135.295" y1="10.8932" x2="107.004" y2="88.6892" gradientUnits="userSpaceOnUse">
<stop stop-color="#6430AB"/>
<stop offset="1" stop-color="#AA89D8"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="113.371" y1="54.7414" x2="76.9532" y2="125.111" gradientUnits="userSpaceOnUse">
<stop stop-color="#005DBB"/>
<stop offset="1" stop-color="#3D92E8"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="129.632" y1="10.8927" x2="118.666" y2="45.1937" gradientUnits="userSpaceOnUse">
<stop stop-color="#1B70C4"/>
<stop offset="1" stop-color="#54A4F2"/>
</linearGradient>
<linearGradient id="paint3_linear" x1="47.9012" y1="168.165" x2="163.603" y2="168.165" gradientUnits="userSpaceOnUse">
<stop stop-color="#4497EA"/>
<stop offset="0.254755" stop-color="#1463B2"/>
<stop offset="0.498725" stop-color="#0A437D"/>
<stop offset="0.666667" stop-color="#2476C8"/>
<stop offset="0.972542" stop-color="#0C549A"/>
</linearGradient>
</defs>
</svg>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

@@ -0,0 +1,3 @@
@echo off
cd /d "%~dp0..\agent"
npx @langchain/langgraph-cli dev --port 8123 --no-browser
@@ -0,0 +1,3 @@
#!/bin/bash
cd "$(dirname "$0")/../agent" || exit 1
npx @langchain/langgraph-cli dev --port 8123 --no-browser
@@ -0,0 +1,3 @@
@echo off
cd /d "%~dp0..\agent"
uv sync
@@ -0,0 +1,3 @@
#!/bin/bash
cd "$(dirname "$0")/../agent" || exit 1
uv sync
@@ -0,0 +1,59 @@
"""
Thin wrapper to serve the LangGraph agent via AG-UI protocol.
Used in Docker where langgraph-cli dev (which needs Docker) is unavailable.
The original main.py and all agent code remain unmodified.
"""
import os
import sys
# Add the agent directory to the path so imports work
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "agent"))
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from langgraph.checkpoint.memory import MemorySaver
# Import the original graph from the unmodified agent code
from main import graph
# The create_agent() graph may not have a checkpointer (it's normally
# provided by the LangGraph Platform server). Add one for standalone serving.
if not hasattr(graph, "checkpointer") or graph.checkpointer is None:
# Recompile with a checkpointer
graph = graph.copy()
graph.checkpointer = MemorySaver()
# Use copilotkit's LangGraphAGUIAgent to serve via AG-UI
from copilotkit import LangGraphAGUIAgent
from ag_ui_langgraph import add_langgraph_fastapi_endpoint
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health")
async def health():
return {"status": "ok"}
add_langgraph_fastapi_endpoint(
app=app,
agent=LangGraphAGUIAgent(
name="sample_agent",
description="LangGraph Python starter agent",
graph=graph,
),
path="/",
)
if __name__ == "__main__":
port = int(os.getenv("AGENT_PORT", "8123"))
uvicorn.run(app, host="0.0.0.0", port=port)
@@ -0,0 +1,3 @@
{
"showcase": "default"
}
@@ -0,0 +1,60 @@
import {
CopilotRuntime,
CopilotKitIntelligence,
createCopilotEndpoint,
InMemoryAgentRunner,
} from "@copilotkit/runtime/v2";
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
import { handle } from "hono/vercel";
const defaultAgent = new LangGraphAgent({
deploymentUrl:
process.env.AGENT_URL ||
process.env.LANGGRAPH_DEPLOYMENT_URL ||
"http://localhost:8123",
graphId: "sample_agent",
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
});
const runtime = new CopilotRuntime({
agents: { default: defaultAgent },
// --- copilotkit:intelligence (remove this block to opt out) ---
...(process.env.COPILOTKIT_LICENSE_TOKEN
? {
intelligence: new CopilotKitIntelligence({
apiKey: process.env.INTELLIGENCE_API_KEY ?? "",
apiUrl: process.env.INTELLIGENCE_API_URL ?? "http://localhost:4201",
wsUrl:
process.env.INTELLIGENCE_GATEWAY_WS_URL ?? "ws://localhost:4401",
}),
// Demo stub — replace with your real auth-derived user identity before any
// multi-user deployment, or all users share one thread history.
identifyUser: () => ({ id: "demo-user", name: "Demo User" }),
licenseToken: process.env.COPILOTKIT_LICENSE_TOKEN,
}
: { runner: new InMemoryAgentRunner() }),
// --- /copilotkit:intelligence ---
openGenerativeUI: true,
a2ui: {
injectA2UITool: false,
},
mcpApps: {
servers: [
{
type: "http",
url: process.env.MCP_SERVER_URL || "https://mcp.excalidraw.com",
serverId: "example_mcp_app",
},
],
},
});
const app = createCopilotEndpoint({
runtime,
basePath: "/api/copilotkit",
});
export const GET = handle(app);
export const POST = handle(app);
export const PATCH = handle(app);
export const DELETE = handle(app);
@@ -0,0 +1,184 @@
/**
* Demonstration Catalog — Component Definitions
*
* Platform-agnostic definitions: component names, props (Zod), descriptions.
* This is the contract between the app and the AI agent. Agents receive these
* definitions as context so they know what components are available.
*
* Renderers (React, React Native, etc.) import these definitions and provide
* platform-specific implementations, type-checked against the Zod schemas.
*/
import { z } from "zod";
/**
* Dynamic string: accepts either a literal string or a data-model path binding
* like `{ path: "airline" }`. The GenericBinder resolves path bindings to the
* actual value at render time.
*/
const DynString = z.union([z.string(), z.object({ path: z.string() })]);
export const demonstrationCatalogDefinitions = {
Title: {
description: "A heading. Use for section titles and page headers.",
props: z.object({
text: z.string(),
level: z.string().optional(),
}),
},
// Text: removed — the basic catalog's Text uses DynamicStringSchema
// which supports path bindings (e.g. { path: "flights[*].airline" }).
// Overriding it with z.string() breaks fixed-schema data binding.
Row: {
description: "Horizontal layout container.",
props: z.object({
gap: z.number().optional(),
align: z.string().optional(),
justify: z.string().optional(),
// Union with { componentId, path } so GenericBinder treats this as
// STRUCTURAL and resolves template children from the data model.
children: z.union([
z.array(z.string()),
z.object({ componentId: z.string(), path: z.string() }),
]),
}),
},
Column: {
description: "Vertical layout container.",
props: z.object({
gap: z.number().optional(),
align: z.string().optional(),
// Same union as Row — required for template children support.
children: z.union([
z.array(z.string()),
z.object({ componentId: z.string(), path: z.string() }),
]),
}),
},
DashboardCard: {
description:
"A card container with title and optional subtitle. Has a 'child' slot for content (chart, metrics, etc). Use 'child' with a single component ID.",
props: z.object({
title: z.string(),
subtitle: z.string().optional(),
child: z.string().optional(),
}),
},
Metric: {
description:
"A key metric display with label, value, and optional trend indicator. Great for KPIs and stats.",
props: z.object({
label: z.string(),
value: z.string(),
trend: z.enum(["up", "down", "neutral"]).optional(),
trendValue: z.string().optional(),
}),
},
PieChart: {
description:
"A pie/donut chart. Provide data as array of {label, value, color} objects.",
props: z.object({
data: z.array(
z.object({
label: z.string(),
value: z.number(),
color: z.string().optional(),
}),
),
innerRadius: z.number().optional(),
}),
},
BarChart: {
description:
"A bar chart. Provide data as array of {label, value} objects.",
props: z.object({
data: z.array(z.object({ label: z.string(), value: z.number() })),
color: z.string().optional(),
}),
},
Badge: {
description:
"A small status badge/tag. Use for labels, statuses, categories.",
props: z.object({
text: z.string(),
variant: z
.enum(["success", "warning", "error", "info", "neutral"])
.optional(),
}),
},
DataTable: {
description: "A data table with columns and rows.",
props: z.object({
columns: z.array(z.object({ key: z.string(), label: z.string() })),
rows: z.array(z.record(z.any())),
}),
},
Button: {
description:
"An interactive button with an action event. Use 'child' with a Text component ID for the label. 'action' is dispatched on click.",
props: z.object({
child: z
.string()
.describe(
"The ID of the child component (e.g. a Text component for the label).",
),
variant: z.enum(["primary", "secondary", "ghost"]).optional(),
// Union with { event } so GenericBinder resolves this as ACTION → callable () => void.
action: z
.union([
z.object({
event: z.object({
name: z.string(),
context: z.record(z.any()).optional(),
}),
}),
z.null(),
])
.optional(),
}),
},
FlightCard: {
description:
"A rich flight result card. Displays airline, flight number, route, times, duration, status, and price. Use inside a Row for side-by-side layout.",
props: z.object({
airline: DynString,
airlineLogo: DynString,
flightNumber: DynString,
origin: DynString,
destination: DynString,
date: DynString,
departureTime: DynString,
arrivalTime: DynString,
duration: DynString,
status: DynString,
statusColor: DynString.optional(),
price: DynString,
action: z
.union([
z.object({
event: z.object({
name: z.string(),
context: z.record(z.any()).optional(),
}),
}),
z.null(),
])
.optional(),
}),
},
};
/** Type helper for renderers */
export type DemonstrationCatalogDefinitions =
typeof demonstrationCatalogDefinitions;
@@ -0,0 +1,601 @@
/**
* A2UI Catalog — React Renderers
*
* Each renderer maps a component name from definitions.ts to a React
* implementation. Props are type-checked against the Zod schemas.
*
* To add a component: define its schema in definitions.ts, then add a
* renderer here. See README.md "Adding a custom component" for details.
*
* The assembled catalog is registered in layout.tsx via
* <CopilotKit a2ui={{ catalog: demonstrationCatalog }}>.
*/
"use client";
import React, { useState } from "react";
import {
PieChart as RechartsPie,
Pie,
Cell,
ResponsiveContainer,
BarChart as RechartsBar,
Bar,
XAxis,
YAxis,
Tooltip,
CartesianGrid,
} from "recharts";
import { createCatalog } from "@copilotkit/a2ui-renderer";
import type { CatalogRenderers } from "@copilotkit/a2ui-renderer";
import { demonstrationCatalogDefinitions } from "./definitions";
import type { DemonstrationCatalogDefinitions } from "./definitions";
// ─── Theme-aware colors ─────────────────────────────────────────────
const c = {
card: "var(--card)",
cardFg: "var(--card-foreground)",
border: "var(--border)",
muted: "var(--muted-foreground)",
divider: "color-mix(in srgb, var(--border) 50%, var(--card))",
shadow: "0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04)",
btnBg: "color-mix(in srgb, var(--muted) 40%, var(--card))",
btnDoneBg: "color-mix(in srgb, #22c55e 10%, var(--card))",
};
function ActionButton({
label,
doneLabel,
action,
children: child,
}: {
label: string;
doneLabel: string;
action: any;
children?: React.ReactNode;
}) {
const [done, setDone] = useState(false);
return (
<button
disabled={done}
style={{
width: "100%",
padding: "10px 16px",
borderRadius: "10px",
border: done ? "1px solid #bbf7d0" : `1px solid ${c.border}`,
background: done ? c.btnDoneBg : c.btnBg,
color: done ? "#059669" : c.cardFg,
fontSize: "0.85rem",
fontWeight: 500,
cursor: done ? "default" : "pointer",
transition: "all 0.2s ease",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "6px",
}}
onClick={() => {
if (!done) {
action?.();
setDone(true);
}
}}
>
{done && (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="#059669"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
)}
{done ? doneLabel : (child ?? label)}
</button>
);
}
// ─── Renderers (type-checked against schema definitions) ────────────
const demonstrationCatalogRenderers: CatalogRenderers<DemonstrationCatalogDefinitions> =
{
Title: ({ props }) => {
const Tag = (
props.level === "h1" ? "h1" : props.level === "h3" ? "h3" : "h2"
) as keyof JSX.IntrinsicElements;
const sizes: Record<string, string> = {
h1: "1.75rem",
h2: "1.25rem",
h3: "1rem",
};
return (
<Tag
style={{
margin: 0,
fontWeight: 600,
fontSize: sizes[props.level ?? "h2"],
color: c.cardFg,
letterSpacing: "-0.01em",
}}
>
{props.text}
</Tag>
);
},
// Text: removed — use the basic catalog's Text (supports DynamicStringSchema
// for path bindings in fixed-schema templates).
Row: ({ props, children }) => {
const justifyMap: Record<string, string> = {
start: "flex-start",
center: "center",
end: "flex-end",
spaceBetween: "space-between",
};
const items = Array.isArray(props.children) ? props.children : [];
return (
<div
style={{
display: "flex",
flexDirection: "row",
gap: `${props.gap ?? 16}px`,
alignItems: props.align ?? "stretch",
justifyContent:
justifyMap[props.justify ?? "start"] ?? "flex-start",
flexWrap: "wrap",
width: "100%",
}}
>
{items.map((item: any, i: number) => {
if (typeof item === "string")
return (
<div
key={`${item}-${i}`}
style={{ flex: "1 1 0", minWidth: 0 }}
>
{children(item)}
</div>
);
if (item && typeof item === "object" && "id" in item)
return (
<div
key={`${item.id}-${i}`}
style={{ flex: "1 1 0", minWidth: 0 }}
>
{(children as any)(item.id, item.basePath)}
</div>
);
return null;
})}
</div>
);
},
Column: ({ props, children }) => {
const items = Array.isArray(props.children) ? props.children : [];
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: `${props.gap ?? 12}px`,
width: "100%",
}}
>
{items.map((item: any, i: number) => {
if (typeof item === "string")
return (
<React.Fragment key={`${item}-${i}`}>
{children(item)}
</React.Fragment>
);
if (item && typeof item === "object" && "id" in item)
return (
<React.Fragment key={`${item.id}-${i}`}>
{(children as any)(item.id, item.basePath)}
</React.Fragment>
);
return null;
})}
</div>
);
},
DashboardCard: ({ props, children }) => (
<div
style={{
background: c.card,
borderRadius: "12px",
border: `1px solid ${c.border}`,
padding: "20px",
boxShadow: c.shadow,
display: "flex",
flexDirection: "column",
gap: "12px",
}}
>
<div>
<div style={{ fontWeight: 600, fontSize: "0.9rem", color: c.cardFg }}>
{props.title}
</div>
{props.subtitle && (
<div
style={{
fontSize: "0.75rem",
color: c.muted,
marginTop: "2px",
}}
>
{props.subtitle}
</div>
)}
</div>
{props.child && children(props.child)}
</div>
),
Metric: ({ props }) => {
const trendColors: Record<string, string> = {
up: "#059669",
down: "#dc2626",
neutral: c.muted,
};
const trendIcons: Record<string, string> = {
up: "↑",
down: "↓",
neutral: "→",
};
return (
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
<span
style={{
fontSize: "0.75rem",
color: c.muted,
fontWeight: 500,
textTransform: "uppercase",
letterSpacing: "0.05em",
}}
>
{props.label}
</span>
<div style={{ display: "flex", alignItems: "baseline", gap: "8px" }}>
<span
style={{
fontSize: "1.5rem",
fontWeight: 700,
color: c.cardFg,
letterSpacing: "-0.02em",
}}
>
{props.value}
</span>
{props.trend && props.trendValue && (
<span
style={{
fontSize: "0.8rem",
fontWeight: 500,
color: trendColors[props.trend] ?? c.muted,
}}
>
{trendIcons[props.trend]} {props.trendValue}
</span>
)}
</div>
</div>
);
},
PieChart: ({ props }) => {
const COLORS = [
"#3b82f6",
"#8b5cf6",
"#ec4899",
"#f59e0b",
"#10b981",
"#6366f1",
];
const data = props.data ?? [];
return (
<div style={{ width: "100%", height: 200 }}>
<ResponsiveContainer>
<RechartsPie>
<Pie
data={data}
dataKey="value"
nameKey="label"
cx="50%"
cy="50%"
innerRadius={props.innerRadius ?? 40}
outerRadius={80}
paddingAngle={2}
>
{data.map((entry: any, i: number) => (
<Cell
key={i}
fill={entry.color ?? COLORS[i % COLORS.length]}
/>
))}
</Pie>
<Tooltip />
</RechartsPie>
</ResponsiveContainer>
</div>
);
},
BarChart: ({ props }) => {
const data = props.data ?? [];
return (
<div style={{ width: "100%", height: 200 }}>
<ResponsiveContainer>
<RechartsBar data={data}>
<CartesianGrid strokeDasharray="3 3" stroke={c.divider} />
<XAxis dataKey="label" tick={{ fontSize: 11, fill: c.muted }} />
<YAxis tick={{ fontSize: 11, fill: c.muted }} />
<Tooltip />
<Bar
dataKey="value"
fill={props.color ?? "#3b82f6"}
radius={[4, 4, 0, 0]}
/>
</RechartsBar>
</ResponsiveContainer>
</div>
);
},
Badge: ({ props }) => {
const variants: Record<string, { bg: string; color: string }> = {
success: { bg: "#dcfce7", color: "#166534" },
warning: { bg: "#fef3c7", color: "#92400e" },
error: { bg: "#fee2e2", color: "#991b1b" },
info: { bg: "#dbeafe", color: "#1e40af" },
neutral: { bg: "var(--muted)", color: c.cardFg },
};
const v = variants[props.variant ?? "neutral"] ?? variants.neutral;
return (
<span
style={{
display: "inline-block",
padding: "2px 8px",
borderRadius: "9999px",
fontSize: "0.7rem",
fontWeight: 500,
background: v.bg,
color: v.color,
}}
>
{props.text}
</span>
);
},
DataTable: ({ props }) => {
const cols = props.columns ?? [];
const rows = props.rows ?? [];
return (
<div style={{ overflowX: "auto", width: "100%" }}>
<table
style={{
width: "100%",
borderCollapse: "collapse",
fontSize: "0.8rem",
}}
>
<thead>
<tr>
{cols.map((col: any) => (
<th
key={col.key}
style={{
textAlign: "left",
padding: "8px 12px",
borderBottom: `2px solid ${c.border}`,
color: c.muted,
fontWeight: 600,
fontSize: "0.7rem",
textTransform: "uppercase",
letterSpacing: "0.05em",
}}
>
{col.label}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row: any, i: number) => (
<tr key={i} style={{ borderBottom: `1px solid ${c.divider}` }}>
{cols.map((col: any) => (
<td
key={col.key}
style={{ padding: "8px 12px", color: c.cardFg }}
>
{String(row[col.key] ?? "")}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
},
Button: ({ props, children }) => {
return (
<ActionButton label="Click" doneLabel="Done" action={props.action}>
{props.child ? children(props.child) : null}
</ActionButton>
);
},
FlightCard: ({ props: rawProps }) => {
// The binder resolves path bindings to strings at runtime.
const props = rawProps as Record<string, any>;
const statusColors: Record<string, string> = {
"On Time": "#22c55e",
Delayed: "#eab308",
Cancelled: "#ef4444",
};
const dotColor =
props.statusColor ?? statusColors[props.status] ?? "#22c55e";
return (
<div
style={{
border: `1px solid ${c.border}`,
borderRadius: "16px",
padding: "20px",
background: c.card,
color: c.cardFg,
minWidth: 260,
maxWidth: 340,
flex: "1 1 260px",
display: "flex",
flexDirection: "column",
gap: "12px",
boxShadow: c.shadow,
}}
>
{/* Header: airline + price */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<img
src={props.airlineLogo}
alt={props.airline}
style={{
width: 28,
height: 28,
borderRadius: "50%",
objectFit: "contain",
}}
/>
<span style={{ fontWeight: 600, fontSize: "0.95rem" }}>
{props.airline}
</span>
</div>
<span style={{ fontWeight: 700, fontSize: "1.15rem" }}>
{props.price}
</span>
</div>
{/* Meta */}
<div
style={{
display: "flex",
justifyContent: "space-between",
fontSize: "0.8rem",
color: c.muted,
}}
>
<span>{props.flightNumber}</span>
<span>{props.date}</span>
</div>
<hr
style={{
border: "none",
borderTop: `1px solid ${c.divider}`,
margin: 0,
}}
/>
{/* Times */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span style={{ fontWeight: 700, fontSize: "1.1rem" }}>
{props.departureTime}
</span>
<span style={{ fontSize: "0.75rem", color: c.muted }}>
{props.duration}
</span>
<span style={{ fontWeight: 700, fontSize: "1.1rem" }}>
{props.arrivalTime}
</span>
</div>
{/* Route */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
fontSize: "0.95rem",
fontWeight: 600,
}}
>
<span>{props.origin}</span>
<span style={{ color: c.muted }}></span>
<span>{props.destination}</span>
</div>
<div
style={{
marginTop: "auto",
display: "flex",
flexDirection: "column",
gap: "12px",
}}
>
<hr
style={{
border: "none",
borderTop: `1px solid ${c.divider}`,
margin: 0,
}}
/>
{/* Status */}
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
<span
style={{
width: 8,
height: 8,
borderRadius: "50%",
background: dotColor,
display: "inline-block",
}}
/>
<span style={{ fontSize: "0.8rem", color: c.muted }}>
{props.status}
</span>
</div>
<ActionButton
label="Select"
doneLabel="Selected"
action={props.action}
/>
</div>
</div>
);
},
};
// ─── Assembled Catalog ───────────────────────────────────────────────
export const demonstrationCatalog = createCatalog(
demonstrationCatalogDefinitions,
demonstrationCatalogRenderers,
{
catalogId: "copilotkit://app-dashboard-catalog",
},
);
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,125 @@
/* CopilotKit brand fonts */
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300..800&family=Spline+Sans+Mono:wght@400;500;600&display=swap");
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
:root {
--background: #ffffff;
--foreground: #010507;
/* Semantic tokens — CopilotKit brand */
--card: #ffffff;
--card-foreground: #010507;
--primary: #010507;
--primary-foreground: #ffffff;
--secondary: #ededf5;
--secondary-foreground: #010507;
--muted: #ededf5;
--muted-foreground: #57575b;
--accent: #eee6fe;
--accent-foreground: #010507;
--destructive: #fa5f67;
--destructive-foreground: #ffffff;
--border: #dbdbe5;
--input: #dbdbe5;
--ring: #bec2ff;
/* Chart tokens */
--chart-tooltip-bg: #ffffff;
--chart-tooltip-border: #dbdbe5;
--chart-axis: #57575b;
--radius: 0.75rem;
/* Override CopilotKit font */
--cpk-default-font-family: var(--font-body);
/* Fonts */
--font-body: "Plus Jakarta Sans", "DM Sans", "Segoe UI", sans-serif;
--font-code:
"Spline Sans Mono", "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
/* Brand accent colors */
--cpk-mint-400: #85ecce;
--cpk-mint-800: #189370;
--cpk-lilac-400: #bec2ff;
--cpk-orange-400: #ffac4d;
--cpk-yellow-400: #fff388;
--cpk-ambient-gradient: linear-gradient(
90deg,
#bec2ff 0%,
#85ecce 45.673%,
#ffac4d 100%
);
}
:root.dark,
.dark {
--background: #010507;
--foreground: #ffffff;
--card: #191a1e;
--card-foreground: #ffffff;
--primary: #ffffff;
--primary-foreground: #010507;
--secondary: #242529;
--secondary-foreground: #ffffff;
--muted: #242529;
--muted-foreground: #adadb2;
--accent: #303136;
--accent-foreground: #ffffff;
--destructive: #fa5f67;
--destructive-foreground: #ffffff;
--border: #303136;
--input: #303136;
--ring: #bec2ff;
--chart-tooltip-bg: #191a1e;
--chart-tooltip-border: #303136;
--chart-axis: #adadb2;
}
body {
background: var(--background);
color: var(--foreground);
font-family: var(--font-body);
}
body,
html {
height: 100%;
}
/*
* Showcase highlight for suggestion pills.
* Applied via className in use-example-suggestions.tsx when showcase.json
* is set to a non-default mode (e.g. "a2ui", "opengenui"). Uses CopilotKit
* brand colors with a subtle tinted background.
* Add new showcase variants here following the same pattern.
*/
[data-copilotkit] button[data-slot="suggestion-pill"].a2ui-highlight {
border-color: var(--cpk-lilac-400);
background: color-mix(in srgb, var(--cpk-lilac-400) 15%, var(--background));
}
[data-copilotkit] button[data-slot="suggestion-pill"].opengenui-highlight {
border-color: var(--cpk-mint-400);
background: color-mix(in srgb, var(--cpk-mint-400) 15%, var(--background));
}
/*
* Position the CopilotKit inspector FAB BENEATH the Chat/App toggle in the
* top-right, instead of the header row where it would overlap it. The gap below
* the toggle matches the toggle's own 16px gap from the top of the viewport.
* The toggle is 16px from the top and 46px tall (both breakpoints), so its
* bottom is at 62px; a 16px gap puts the FAB circle at 78px. The visible circle
* renders ~16px below the host's `top`, so host top = 78 - 16 = 62px. Right edge
* matches the toggle's 16px inset.
*/
cpk-web-inspector {
top: 62px !important;
bottom: auto !important;
right: 16px !important;
}
@@ -0,0 +1,59 @@
"use client";
import "./globals.css";
import "@copilotkit/react-core/v2/styles.css";
import { CopilotKit } from "@copilotkit/react-core/v2";
import { ThemeProvider } from "@/hooks/use-theme";
// A2UI catalog: definitions + renderers in ./declarative-generative-ui/
import { demonstrationCatalog } from "./declarative-generative-ui/renderers";
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<title>CopilotKit</title>
<link
rel="icon"
type="image/svg+xml"
href="/copilotkit-logo-mark.svg"
/>
{/*
Set the theme class BEFORE first paint to avoid a white→dark flash.
ThemeProvider applies the theme in a useEffect (post-hydration), so
without this the page paints unthemed (light) first, then flips. This
blocking inline script matches ThemeProvider's "system" default so
there's no flash and no class mismatch when the provider re-applies.
*/}
<script
dangerouslySetInnerHTML={{
__html:
"(function(){try{var d=window.matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.add(d?'dark':'light');}catch(e){}})();",
}}
/>
</head>
{/*
suppressHydrationWarning: browser extensions (e.g. Grammarly) inject
attributes like data-gr-ext-installed onto <body> before React hydrates,
which would otherwise surface as a hydration mismatch on first load.
This only relaxes the check for <body>'s own attributes (one level deep);
everything rendered inside <body> is still fully hydration-checked.
*/}
<body className={`antialiased`} suppressHydrationWarning>
<ThemeProvider>
<CopilotKit
runtimeUrl="/api/copilotkit"
inspectorDefaultAnchor={{ horizontal: "right", vertical: "top" }}
a2ui={{ catalog: demonstrationCatalog }}
openGenerativeUI={{}}
useSingleEndpoint={false}
>
{children}
</CopilotKit>
</ThemeProvider>
</body>
</html>
);
}
@@ -0,0 +1,67 @@
.layout {
display: grid;
/*
Reserve the desktop drawer's width (its default `--cpk-drawer-width`, 320px)
as a fixed first column so the layout does NOT shift when the client-only
drawer mounts after hydration. Without this the column is `auto` → 0px until
the drawer boots, so the chat starts full-width and then "slides over" when
the drawer appears. On mobile the drawer is an off-canvas overlay (out of
flow), so the column collapses to `auto` (≈0) and the chat fills the width.
*/
grid-template-columns: var(--cpk-drawer-reserved-width, 320px) minmax(0, 1fr);
/* Bound the row so the drawer's thread list scrolls internally (pinned
header) instead of the panel growing to content height. */
grid-template-rows: minmax(0, 1fr);
/* The drawer sets --cpk-drawer-reserved-width to 0 when collapsed on desktop,
so the reserved column collapses and the chat reclaims the space. */
transition: grid-template-columns 0.2s ease;
min-height: 100vh;
min-height: 100svh;
height: 100dvh;
width: 100%;
overflow: hidden;
/*
Uniform 16px gutter for the collapsed cluster — top AND left — matching the
right-side controls' (toggle + inspector) 16px inset. Same on both
breakpoints. These inherit into <copilotkit-threads-drawer> and pierce its
shadow root.
*/
--cpk-drawer-launcher-top: 16px;
--cpk-drawer-launcher-left: 16px;
}
.mainPanel {
/*
Pin the chat to the SECOND grid track explicitly. The client-only
<CopilotThreadsDrawer> renders NOTHING during SSR/prerender (it is mounted-gated),
so at first paint the grid has a single child — this panel. Without an
explicit placement it would flow into the FIRST (reserved 320px) track,
cramming the chat at x:0, then jump to the second track once the drawer
mounts as the first child — the "starts left, then slides over" shift.
Forcing column 2 keeps the chat in its final position from first paint; the
drawer fills the reserved first track when it mounts.
*/
grid-column: 2;
min-width: 0;
min-height: 100vh;
min-height: 100svh;
height: 100dvh;
overflow: auto;
}
/*
Mobile (≤768px): the drawer is an off-canvas overlay, so don't reserve a
column for it — collapse to a single track and let the chat fill the full
width. This block MUST come after the base rules above: media queries do not
add specificity, so a same-specificity base rule placed later would otherwise
win and leak the desktop two-column layout onto mobile.
*/
@media (max-width: 768px) {
.layout {
grid-template-columns: minmax(0, 1fr);
}
.mainPanel {
grid-column: auto;
}
}
@@ -0,0 +1,49 @@
"use client";
import { ExampleLayout } from "@/components/example-layout";
import { ExampleCanvas } from "@/components/example-canvas";
import { useGenerativeUIExamples, useExampleSuggestions } from "@/hooks";
import {
CopilotChat,
CopilotChatConfigurationProvider,
CopilotThreadsDrawer,
} from "@copilotkit/react-core/v2";
import styles from "./page.module.css";
export default function HomePage() {
useGenerativeUIExamples();
useExampleSuggestions();
return (
/*
One UNCONTROLLED CopilotChatConfigurationProvider (no `threadId` prop) owns
the active thread for the whole surface. The SDK <CopilotThreadsDrawer> drives it
directly — picking a row sets the active thread, "+ New" resets to a fresh
thread (clearing the chat) — with no host thread-state. The chat and the
canvas read the same active thread from the provider (the canvas's
`useAgent()` falls back to it), so they stay on the same per-thread agent
clone the chat's /connect replay populates. A *controlled* provider would
block "+ New" from resetting the chat, so uncontrolled-inside-provider is
required, not optional.
*/
<CopilotChatConfigurationProvider agentId="default">
<div className={styles.layout}>
{/* SDK threads drawer (replaces the hand-rolled fork). License-gated: the locked view's Upgrade CTA opens the Intelligence docs by default. */}
<CopilotThreadsDrawer agentId="default" />
<div className={styles.mainPanel}>
<ExampleLayout
chatContent={
<CopilotChat
attachments={{ enabled: true }}
input={{ disclaimer: () => null, className: "pb-6" }}
/>
}
appContent={<ExampleCanvas />}
/>
</div>
</div>
</CopilotChatConfigurationProvider>
);
}
@@ -0,0 +1,20 @@
"use client";
import { useAgent } from "@copilotkit/react-core/v2";
import { TodoList } from "./todo-list";
export function ExampleCanvas() {
const { agent } = useAgent();
return (
<div className="h-full overflow-y-auto bg-[--background]">
<div className="max-w-4xl mx-auto px-8 py-10 h-full">
<TodoList
todos={agent.state?.todos || []}
onUpdate={(updatedTodos) => agent.setState({ todos: updatedTodos })}
isAgentRunning={agent.isRunning}
/>
</div>
</div>
);
}
@@ -0,0 +1,197 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { Card } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
interface Todo {
id: string;
title: string;
description: string;
emoji: string;
status: "pending" | "completed";
}
interface TodoCardProps {
todo: Todo;
onToggleStatus: (todo: Todo) => void;
onDelete: (todo: Todo) => void;
onUpdateTitle: (todoId: string, title: string) => void;
onUpdateDescription: (todoId: string, description: string) => void;
onUpdateEmoji: (todoId: string, emoji: string) => void;
}
const EMOJI_OPTIONS = ["✅", "🔥", "🎯", "💡", "🚀"];
export function TodoCard({
todo,
onToggleStatus,
onDelete,
onUpdateTitle,
onUpdateDescription,
onUpdateEmoji,
}: TodoCardProps) {
const [editingField, setEditingField] = useState<
"title" | "description" | null
>(null);
const [editValue, setEditValue] = useState("");
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const isCompleted = todo.status === "completed";
const truncatedDescription =
todo.description.length > 120
? todo.description.slice(0, 120) + "..."
: todo.description;
const startEdit = (field: "title" | "description") => {
setEditingField(field);
setEditValue(field === "title" ? todo.title : todo.description);
};
const saveEdit = (field: "title" | "description") => {
if (editValue.trim()) {
if (field === "title") {
onUpdateTitle(todo.id, editValue.trim());
} else {
onUpdateDescription(todo.id, editValue.trim());
}
}
setEditingField(null);
setEditValue("");
};
const cancelEdit = () => {
setEditingField(null);
setEditValue("");
};
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
textareaRef.current.style.height =
textareaRef.current.scrollHeight + "px";
}
}, [editValue]);
return (
<Card
className={cn(
"group relative p-5 transition-all duration-150",
isCompleted && "opacity-60",
)}
>
{/* Delete — top right on hover */}
<Button
variant="ghost"
size="icon"
onClick={() => onDelete(todo)}
className="absolute top-3 right-3 h-7 w-7 opacity-0 group-hover:opacity-100 transition-opacity"
aria-label="Delete todo"
>
<X className="h-3.5 w-3.5" />
</Button>
{/* Emoji avatar */}
<div className="relative inline-block mb-3">
<button
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
className={cn(
"block text-3xl leading-none cursor-pointer rounded-xl p-2 transition-colors",
isCompleted ? "bg-[var(--muted)]" : "bg-[var(--secondary)]",
)}
aria-label="Change emoji"
>
{todo.emoji}
</button>
{showEmojiPicker && (
<div className="absolute top-0 left-full ml-2 z-10 flex gap-1 p-1.5 rounded-full bg-[var(--card)] border border-[var(--border)] shadow-lg">
{EMOJI_OPTIONS.map((emoji) => (
<button
key={emoji}
onClick={() => {
onUpdateEmoji(todo.id, emoji);
setShowEmojiPicker(false);
}}
className="text-lg w-8 h-8 flex items-center justify-center rounded-full cursor-pointer transition-colors hover:bg-[var(--secondary)]"
>
{emoji}
</button>
))}
</div>
)}
</div>
{/* Title */}
<div className="flex items-start gap-3">
<Checkbox
checked={isCompleted}
onCheckedChange={() => onToggleStatus(todo)}
className="mt-[2px]"
/>
<div className="flex-1 min-w-0">
{editingField === "title" ? (
<input
type="text"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={() => saveEdit("title")}
onKeyDown={(e) => {
if (e.key === "Enter") saveEdit("title");
if (e.key === "Escape") cancelEdit();
}}
className="w-full text-base font-semibold focus:outline-none bg-transparent text-[var(--foreground)] border-b-2 border-[var(--primary)] pb-[2px]"
autoFocus
aria-label="Edit todo title"
/>
) : (
<div
onClick={() => startEdit("title")}
className={cn(
"text-base font-semibold cursor-text break-words leading-snug",
isCompleted
? "text-[var(--muted-foreground)] line-through"
: "text-[var(--foreground)]",
)}
>
{todo.title}
</div>
)}
{editingField === "description" ? (
<textarea
ref={textareaRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={() => saveEdit("description")}
onKeyDown={(e) => {
if (e.key === "Escape") cancelEdit();
}}
className="w-full mt-1.5 text-sm leading-relaxed focus:outline-none resize-none bg-transparent text-[var(--muted-foreground)] border-b-2 border-[var(--primary)] pb-[2px]"
rows={1}
autoFocus
aria-label="Edit todo description"
/>
) : (
<p
onClick={() => startEdit("description")}
className={cn(
"mt-1.5 text-sm leading-relaxed cursor-text",
isCompleted
? "text-[var(--muted-foreground)] line-through"
: "text-[var(--muted-foreground)]",
)}
>
{truncatedDescription}
</p>
)}
</div>
</div>
</Card>
);
}
@@ -0,0 +1,88 @@
"use client";
import { TodoCard } from "./todo-card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
interface Todo {
id: string;
title: string;
description: string;
emoji: string;
status: "pending" | "completed";
}
interface TodoColumnProps {
title: string;
todos: Todo[];
emptyMessage: string;
showAddButton?: boolean;
onAddTodo?: () => void;
onToggleStatus: (todo: Todo) => void;
onDelete: (todo: Todo) => void;
onUpdateTitle: (todoId: string, title: string) => void;
onUpdateDescription: (todoId: string, description: string) => void;
onUpdateEmoji: (todoId: string, emoji: string) => void;
isAgentRunning: boolean;
}
export function TodoColumn({
title,
todos,
emptyMessage,
showAddButton = false,
onAddTodo,
onToggleStatus,
onDelete,
onUpdateTitle,
onUpdateDescription,
onUpdateEmoji,
isAgentRunning,
}: TodoColumnProps) {
return (
<section aria-label={`${title} column`} className="flex-1 min-w-0">
{/* Header */}
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-3">
<h2 className="text-lg font-bold tracking-tight text-[var(--foreground)]">
{title}
</h2>
<Badge variant="secondary">{todos.length}</Badge>
</div>
{showAddButton && onAddTodo && (
<Button
variant="ghost"
size="icon"
onClick={onAddTodo}
disabled={isAgentRunning}
aria-label="Add new todo"
>
<Plus className="h-4 w-4" />
</Button>
)}
</div>
{/* Cards */}
<div className="space-y-3">
{todos.length === 0 ? (
<div className="text-center text-sm rounded-[var(--radius)] border-2 border-dashed border-[var(--border)] p-5 min-h-[151px] flex items-center justify-center text-[var(--muted-foreground)]">
{emptyMessage}
</div>
) : (
todos.map((todo) => (
<TodoCard
key={todo.id}
todo={todo}
onToggleStatus={onToggleStatus}
onDelete={onDelete}
onUpdateTitle={onUpdateTitle}
onUpdateDescription={onUpdateDescription}
onUpdateEmoji={onUpdateEmoji}
/>
))
)}
</div>
</section>
);
}
@@ -0,0 +1,115 @@
"use client";
import { TodoColumn } from "./todo-column";
import { Button } from "@/components/ui/button";
interface Todo {
id: string;
title: string;
description: string;
emoji: string;
status: "pending" | "completed";
}
interface TodoListProps {
todos: Todo[];
onUpdate: (todos: Todo[]) => void;
isAgentRunning: boolean;
}
export function TodoList({ todos, onUpdate, isAgentRunning }: TodoListProps) {
const pendingTodos = todos.filter((t) => t.status === "pending");
const completedTodos = todos.filter((t) => t.status === "completed");
const toggleStatus = (todo: Todo) => {
const updated = todos.map((t) =>
t.id === todo.id
? {
...t,
status: (t.status === "completed" ? "pending" : "completed") as
| "pending"
| "completed",
}
: t,
);
onUpdate(updated);
};
const deleteTodo = (todo: Todo) => {
onUpdate(todos.filter((t) => t.id !== todo.id));
};
const updateTitle = (todoId: string, title: string) => {
const updated = todos.map((t) => (t.id === todoId ? { ...t, title } : t));
onUpdate(updated);
};
const updateDescription = (todoId: string, description: string) => {
const updated = todos.map((t) =>
t.id === todoId ? { ...t, description } : t,
);
onUpdate(updated);
};
const updateEmoji = (todoId: string, emoji: string) => {
const updated = todos.map((t) => (t.id === todoId ? { ...t, emoji } : t));
onUpdate(updated);
};
const addTodo = () => {
const newTodo: Todo = {
id: crypto.randomUUID(),
title: "New Todo",
description: "Add a description",
emoji: "🎯",
status: "pending",
};
onUpdate([...todos, newTodo]);
};
if (!todos || todos.length === 0) {
return (
<div className="flex flex-col items-center justify-center h-full gap-4">
<div className="text-5xl"></div>
<p className="text-base font-semibold text-[--foreground]">
No todos yet
</p>
<p className="text-sm text-[--muted-foreground]">
Create your first task to get started
</p>
<Button onClick={addTodo} disabled={isAgentRunning} className="mt-2">
Add a task
</Button>
</div>
);
}
return (
<div className="flex gap-8 h-full">
<TodoColumn
title="To Do"
todos={pendingTodos}
emptyMessage="No pending todos"
showAddButton
onAddTodo={addTodo}
onToggleStatus={toggleStatus}
onDelete={deleteTodo}
onUpdateTitle={updateTitle}
onUpdateDescription={updateDescription}
onUpdateEmoji={updateEmoji}
isAgentRunning={isAgentRunning}
/>
<TodoColumn
title="Done"
todos={completedTodos}
emptyMessage="No completed todos yet"
onToggleStatus={toggleStatus}
onDelete={deleteTodo}
onUpdateTitle={updateTitle}
onUpdateDescription={updateDescription}
onUpdateEmoji={updateEmoji}
isAgentRunning={isAgentRunning}
/>
</div>
);
}
@@ -0,0 +1,83 @@
"use client";
import type { ReactNode } from "react";
import { useState } from "react";
import { ModeToggle } from "./mode-toggle";
import { useFrontendTool } from "@copilotkit/react-core/v2";
interface ExampleLayoutProps {
chatContent: ReactNode;
appContent: ReactNode;
}
export function ExampleLayout({ chatContent, appContent }: ExampleLayoutProps) {
const [mode, setMode] = useState<"chat" | "app">("chat");
useFrontendTool({
name: "enableAppMode",
description:
"Enable app mode, make sure its open when interacting with todos.",
handler: async () => {
setMode("app");
},
});
useFrontendTool({
name: "enableChatMode",
description: "Enable chat mode",
handler: async () => {
setMode("chat");
},
});
return (
<div className="h-full flex flex-row pb-6">
<ModeToggle mode={mode} onModeChange={setMode} />
{/* Chat Content */}
<div
className={`max-h-full flex flex-col dark:bg-stone-950 ${
mode === "app"
? "w-1/2 px-6 max-lg:hidden" // Half/half with the canvas; hidden on mobile in app mode
: "flex-1 max-lg:px-4"
}`}
>
{/* Clear the threads drawer's floating launcher/collapsed cluster, which
is fixed at the top-left corner. Below 1024px (mobile off-canvas) that
is always present → max-lg:pl-24. On desktop it only appears when the
drawer is COLLAPSED — detected via --cpk-drawer-reserved-width, which
the drawer sets to 0px on collapse (else its 320px default): the pl
calc resolves to 1.5rem (pl-6) when expanded and ~6rem when collapsed,
so the logo never sits under the cluster. max-lg:pt-2.5 + pb-0
vertically center the logo with that launcher and the top-right
Chat/App toggle (both pinned at top-2). */}
<div className="shrink-0 pt-[23px] pl-[max(1.5rem,calc(7rem_-_var(--cpk-drawer-reserved-width,320px)))] pb-2 max-lg:pl-24 max-lg:pb-4 flex gap-1.5 items-center align-center">
<span className="font-extrabold text-2xl">CopilotKit</span>
<img
src="/copilotkit-logo-mark.svg"
alt="CopilotKit"
className="h-7"
/>
</div>
<div className="flex-1 min-h-0 overflow-y-auto">{chatContent}</div>
</div>
{/* State Panel */}
<div
className={`h-full overflow-hidden ${
mode === "app"
? "w-1/2 max-lg:w-full border-l border-[var(--border)] max-lg:border-l-0" // Half/half with the chat; full width on mobile
: "w-0 border-l-0"
}`}
>
{/*
Fill the state panel's own width. The previous `lg:w-[66.666vw]` was
viewport-relative, so with a reserved drawer column it overflowed this
container (clipped by overflow-hidden) and pushed centered content
right of the visible box's center.
*/}
<div className="w-full h-full">{appContent}</div>
</div>
</div>
);
}
@@ -0,0 +1,31 @@
interface ModeToggleProps {
mode: "chat" | "app";
onModeChange: (mode: "chat" | "app") => void;
}
export function ModeToggle({ mode, onModeChange }: ModeToggleProps) {
return (
<div className="fixed top-4 right-4 z-50 flex items-center min-h-[46px] rounded-[4px] border border-[var(--border)] bg-[var(--secondary)] p-1.5">
<button
onClick={() => onModeChange("chat")}
className={`px-4 py-1.5 rounded-[2px] text-[13px] leading-[20px] font-medium transition-all cursor-pointer ${
mode === "chat"
? "bg-[var(--card)] text-[var(--card-foreground)] shadow-sm"
: "text-[var(--muted-foreground)]"
}`}
>
Chat
</button>
<button
onClick={() => onModeChange("app")}
className={`px-4 py-1.5 rounded-[2px] text-[13px] leading-[20px] font-medium transition-all cursor-pointer ${
mode === "app"
? "bg-[var(--card)] text-[var(--card-foreground)] shadow-sm"
: "text-[var(--muted-foreground)]"
}`}
>
App
</button>
</div>
);
}
@@ -0,0 +1,157 @@
import { useRef } from "react";
import {
BarChart as RechartsBarChart,
Bar,
XAxis,
YAxis,
Tooltip,
CartesianGrid,
Cell,
ResponsiveContainer,
Rectangle,
} from "recharts";
import { z } from "zod";
import { CHART_COLORS, CHART_CONFIG } from "./config";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card";
import { BarChart3 } from "lucide-react";
export const BarChartProps = z.object({
title: z.string().describe("Chart title"),
description: z.string().describe("Brief description or subtitle"),
data: z.array(
z.object({
label: z.string(),
value: z.number(),
}),
),
});
type BarChartProps = z.infer<typeof BarChartProps>;
/** Tracks seen indices so only NEW bars get the fade-in animation. */
function useSeenIndices() {
const seen = useRef(new Set<number>());
return {
isNew(index: number) {
if (seen.current.has(index)) return false;
seen.current.add(index);
return true;
},
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function AnimatedBar(props: any) {
const { isNew, ...rest } = props;
return (
<g
style={
isNew
? {
animation: "barSlideIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both",
}
: undefined
}
>
<Rectangle {...rest} />
</g>
);
}
export function BarChart({ title, description, data }: BarChartProps) {
const { isNew } = useSeenIndices();
if (!data || !Array.isArray(data) || data.length === 0) {
return (
<Card className="max-w-2xl mx-auto my-4">
<CardHeader>
<div className="flex items-center gap-2">
<BarChart3 className="h-4 w-4 text-[var(--muted-foreground)]" />
<CardTitle>{title}</CardTitle>
</div>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent>
<p className="text-[var(--muted-foreground)] text-center py-8 text-sm">
No data available
</p>
</CardContent>
</Card>
);
}
return (
<Card className="max-w-2xl mx-auto my-4 overflow-hidden">
{/* Scoped keyframe — no globals.css needed */}
<style>{`
@keyframes barSlideIn {
from { transform: translateY(40px); opacity: 0; }
20% { opacity: 1; }
to { transform: translateY(0); opacity: 1; }
}
`}</style>
<CardHeader className="pb-2">
<div className="flex items-center gap-2">
<div className="flex items-center justify-center h-6 w-6 rounded-md bg-[var(--secondary)]">
<BarChart3 className="h-3.5 w-3.5 text-[var(--muted-foreground)]" />
</div>
<CardTitle>{title}</CardTitle>
</div>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent className="pt-2">
<ResponsiveContainer width="100%" height={280}>
<RechartsBarChart
data={data}
margin={{ top: 12, right: 12, bottom: 4, left: -8 }}
>
<CartesianGrid
strokeDasharray="3 3"
stroke="var(--border)"
vertical={false}
/>
<XAxis
dataKey="label"
tick={{ fontSize: 12, fill: "var(--muted-foreground)" }}
stroke="var(--border)"
tickLine={false}
axisLine={false}
/>
<YAxis
tick={{ fontSize: 12, fill: "var(--muted-foreground)" }}
stroke="var(--border)"
tickLine={false}
axisLine={false}
/>
<Tooltip
contentStyle={CHART_CONFIG.tooltipStyle}
cursor={{ fill: "var(--secondary)", opacity: 0.5 }}
/>
<Bar
isAnimationActive={false}
dataKey="value"
radius={[6, 6, 0, 0]}
maxBarSize={48}
shape={(props: Record<string, unknown>) => (
<AnimatedBar {...props} isNew={isNew(props.index as number)} />
)}
>
{data.map((_, index) => (
<Cell
key={index}
fill={CHART_COLORS[index % CHART_COLORS.length]}
/>
))}
</Bar>
</RechartsBarChart>
</ResponsiveContainer>
</CardContent>
</Card>
);
}
@@ -0,0 +1,25 @@
/**
* CopilotKit brand chart palette — Plus Jakarta Sans / brand color system.
*/
export const CHART_COLORS = [
"#BEC2FF", // lilac-400
"#85ECCE", // mint-400
"#FFAC4D", // orange-400
"#FFF388", // yellow-400
"#189370", // mint-800
"#EEE6FE", // primary-100
"#FA5F67", // red-400
] as const;
export const CHART_CONFIG = {
tooltipStyle: {
backgroundColor: "var(--card)",
border: "1px solid var(--border)",
borderRadius: "10px",
padding: "10px 14px",
color: "var(--foreground)",
fontSize: "13px",
fontFamily: "var(--font-body)",
boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
},
};
@@ -0,0 +1,155 @@
import { z } from "zod";
import { CHART_COLORS } from "./config";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card";
export const PieChartProps = z.object({
title: z.string().describe("Chart title"),
description: z.string().describe("Brief description or subtitle"),
data: z.array(
z.object({
label: z.string(),
value: z.number(),
}),
),
});
type PieChartProps = z.infer<typeof PieChartProps>;
/** Custom SVG donut chart built with <circle> + stroke-dasharray. */
function DonutChart({
data,
size = 240,
strokeWidth = 40,
}: {
data: { label: string; value: number }[];
size?: number;
strokeWidth?: number;
}) {
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const center = size / 2;
const total = data.reduce((sum, d) => sum + (Number(d.value) || 0), 0);
// Calculate each slice's arc length and starting position
let accumulated = 0;
const slices = data.map((item, index) => {
const val = Number(item.value) || 0;
const ratio = total > 0 ? val / total : 0;
const arc = ratio * circumference;
const startAt = accumulated;
accumulated += arc;
return {
...item,
arc,
gap: circumference - arc,
// Negative dashoffset shifts the dash forward (clockwise) to the correct position
dashoffset: -startAt,
color: CHART_COLORS[index % CHART_COLORS.length],
};
});
return (
<svg
width="100%"
viewBox={`0 0 ${size} ${size}`}
className="block mx-auto"
style={{ maxWidth: size, transform: "scaleX(-1)" }}
>
{/* Background ring */}
<circle
cx={center}
cy={center}
r={radius}
fill="none"
stroke="var(--secondary)"
strokeWidth={strokeWidth}
/>
{/* Data slices */}
{slices.map((slice, i) => (
<circle
key={i}
cx={center}
cy={center}
r={radius}
fill="none"
stroke={slice.color}
strokeWidth={strokeWidth}
strokeDasharray={`${slice.arc} ${slice.gap}`}
strokeDashoffset={slice.dashoffset}
strokeLinecap="butt"
transform={`rotate(-90 ${center} ${center})`}
/>
))}
</svg>
);
}
export function PieChart({ title, description, data }: PieChartProps) {
if (!data || !Array.isArray(data) || data.length === 0) {
return (
<Card className="max-w-lg mx-auto my-4">
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent>
<p className="text-[var(--muted-foreground)] text-center py-8 text-sm">
No data available
</p>
</CardContent>
</Card>
);
}
const total = data.reduce((sum, d) => sum + (Number(d.value) || 0), 0);
return (
<Card className="max-w-lg mx-auto my-4 overflow-hidden">
<CardHeader className="pb-0">
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent className="pt-4">
<DonutChart data={data} />
{/* Legend */}
<div className="space-y-2 pt-4">
{data.map((item, index) => {
const val = Number(item.value) || 0;
const pct = total > 0 ? ((val / total) * 100).toFixed(0) : 0;
return (
<div
key={index}
className="flex items-center gap-3 text-sm transition-opacity duration-300 ease-out"
style={{ opacity: 1 }}
>
<span
className="inline-block h-3 w-3 rounded-full shrink-0"
style={{
backgroundColor: CHART_COLORS[index % CHART_COLORS.length],
}}
/>
<span className="flex-1 text-[var(--foreground)] truncate">
{item.label}
</span>
<span className="text-[var(--muted-foreground)] tabular-nums">
{val.toLocaleString()}
</span>
<span className="text-[var(--muted-foreground)] text-sm w-10 text-right tabular-nums">
{pct}%
</span>
</div>
);
})}
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,177 @@
import { useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Spinner } from "@/components/ui/spinner";
import { Check, X, Clock, ChevronRight } from "lucide-react";
export interface TimeSlot {
date: string;
time: string;
duration?: string;
}
export interface MeetingTimePickerProps {
status: "inProgress" | "executing" | "complete";
respond?: (response: string) => void;
reasonForScheduling?: string;
meetingDuration?: number;
title?: string;
timeSlots?: TimeSlot[];
}
export function MeetingTimePicker({
status,
respond,
reasonForScheduling,
meetingDuration,
title = "Schedule a Meeting",
timeSlots = [
{ date: "Tomorrow", time: "2:00 PM", duration: "30 min" },
{ date: "Friday", time: "10:00 AM", duration: "30 min" },
{ date: "Next Monday", time: "3:00 PM", duration: "30 min" },
],
}: MeetingTimePickerProps) {
const displayTitle = reasonForScheduling || title;
const slots = meetingDuration
? timeSlots.map((slot) => ({ ...slot, duration: `${meetingDuration} min` }))
: timeSlots;
const [selectedSlot, setSelectedSlot] = useState<TimeSlot | null>(null);
const [declined, setDeclined] = useState(false);
const handleSelectSlot = (slot: TimeSlot) => {
setSelectedSlot(slot);
respond?.(
`Meeting scheduled for ${slot.date} at ${slot.time}${slot.duration ? ` (${slot.duration})` : ""}.`,
);
};
const handleDecline = () => {
setDeclined(true);
respond?.(
"The user declined all proposed meeting times. Please suggest alternative times or ask for their availability.",
);
};
// Confirmed state
if (selectedSlot) {
return (
<Card className="max-w-md w-full mx-auto mb-4 overflow-hidden">
<CardContent className="p-6">
<div className="flex flex-col items-center text-center gap-3">
<div className="flex items-center justify-center h-10 w-10 rounded-full bg-[#189370]">
<Check className="h-5 w-5 text-white" strokeWidth={3} />
</div>
<div>
<h3 className="text-lg font-bold text-[var(--foreground)]">
Meeting Scheduled
</h3>
<p className="text-sm text-[var(--muted-foreground)] mt-1">
{selectedSlot.date} at {selectedSlot.time}
</p>
</div>
{selectedSlot.duration && (
<Badge variant="secondary">
<Clock className="h-3 w-3 mr-1" />
{selectedSlot.duration}
</Badge>
)}
</div>
</CardContent>
</Card>
);
}
// Declined state
if (declined) {
return (
<Card className="max-w-md w-full mx-auto mb-4 overflow-hidden">
<CardContent className="p-6">
<div className="flex flex-col items-center text-center gap-3">
<div className="flex items-center justify-center h-12 w-12 rounded-full bg-[var(--secondary)]">
<X className="h-6 w-6 text-[var(--muted-foreground)]" />
</div>
<div>
<h3 className="text-lg font-bold text-[var(--foreground)]">
No Time Selected
</h3>
<p className="text-sm text-[var(--muted-foreground)] mt-1">
Looking for a better time that works for you
</p>
</div>
</div>
</CardContent>
</Card>
);
}
// Selection state
return (
<Card className="max-w-md w-full mx-auto mb-4 overflow-hidden">
<CardContent className="p-6">
<div className="flex flex-col items-center text-center mb-5">
<div className="flex items-center justify-center h-12 w-12 rounded-full bg-[var(--accent)] mb-3">
<Clock className="h-6 w-6 text-[#BEC2FF]" />
</div>
<h3 className="text-lg font-bold text-[var(--foreground)]">
{displayTitle}
</h3>
<p className="text-sm text-[var(--muted-foreground)] mt-1">
{status === "inProgress"
? "Finding available times..."
: "Pick a time that works for you"}
</p>
</div>
{status === "inProgress" && (
<div className="flex justify-center py-6">
<Spinner size="lg" />
</div>
)}
{status === "executing" && (
<div className="space-y-3">
{slots.map((slot, index) => (
<button
key={index}
onClick={() => handleSelectSlot(slot)}
className="group w-full px-6 py-5 rounded-[var(--radius)]
border border-[var(--border)]
hover:border-[var(--ring)] hover:bg-[var(--accent)]
transition-all duration-150 cursor-pointer
flex items-center gap-4"
>
<div className="flex-1 text-left">
<div className="font-semibold text-base text-[var(--foreground)]">
{slot.date}
</div>
<div className="text-sm text-[var(--muted-foreground)] mt-0.5">
{slot.time}
</div>
</div>
{slot.duration && (
<Badge
variant="secondary"
className="shrink-0 text-sm px-3 py-1"
>
{slot.duration}
</Badge>
)}
<ChevronRight className="h-4 w-4 text-[var(--muted-foreground)] opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
</button>
))}
<Button
variant="ghost"
size="sm"
className="w-full mt-1 text-xs text-[var(--muted-foreground)]"
onClick={handleDecline}
>
None of these work
</Button>
</div>
)}
</CardContent>
</Card>
);
}
@@ -0,0 +1,37 @@
import { useAgent } from "@copilotkit/react-core/v2";
import { useCallback, useState } from "react";
export const HeadlessChat = () => {
const { agent } = useAgent();
const [message, setMessage] = useState("");
const sendMessage = useCallback(
(message: string) => {
agent.addMessage({
role: "user",
id: crypto.randomUUID(),
content: message,
});
agent.runAgent();
setMessage("");
},
[agent],
);
return (
<div>
<h1>Chat</h1>
{agent.messages.map((message) => (
<div key={message.id}>
<p>{JSON.stringify(message.content)}</p>
</div>
))}
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button onClick={() => sendMessage(message)}>Send</button>
</div>
);
};
@@ -0,0 +1,84 @@
"use client";
import { useEffect, useRef } from "react";
import { Wrench, Check, ChevronDown } from "lucide-react";
import { Spinner } from "@/components/ui/spinner";
interface ToolReasoningProps {
name: string;
args?: object | unknown;
status: string;
}
function formatValue(value: unknown): string {
if (Array.isArray(value)) return `[${value.length} items]`;
if (typeof value === "object" && value !== null)
return `{${Object.keys(value).length} keys}`;
if (typeof value === "string") return `"${value}"`;
return String(value);
}
export function ToolReasoning({ name, args, status }: ToolReasoningProps) {
const entries = args ? Object.entries(args) : [];
const detailsRef = useRef<HTMLDetailsElement>(null);
const isRunning = status === "executing" || status === "inProgress";
// Auto-open while executing, auto-close when complete
useEffect(() => {
if (!detailsRef.current) return;
detailsRef.current.open = isRunning;
}, [isRunning]);
const statusIcon = isRunning ? (
<Spinner size="sm" className="h-3 w-3" />
) : (
<Check className="h-3 w-3 text-emerald-500" />
);
return (
<div className="my-1.5">
{entries.length > 0 ? (
<details ref={detailsRef} open className="group">
<summary className="flex items-center gap-2 cursor-pointer list-none text-sm text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors">
{statusIcon}
<Wrench className="h-3 w-3" />
<span
className="font-medium"
style={{ fontFamily: "var(--font-code)" }}
>
{name}
</span>
<ChevronDown className="h-3 w-3 ml-auto transition-transform group-open:rotate-180" />
</summary>
<div className="ml-5 mt-1.5 rounded-md bg-[var(--secondary)] px-3 py-2 space-y-1">
{entries.map(([key, value]) => (
<div
key={key}
className="flex gap-2 min-w-0 text-xs"
style={{ fontFamily: "var(--font-code)" }}
>
<span className="text-[var(--muted-foreground)] shrink-0">
{key}:
</span>
<span className="text-[var(--foreground)] truncate">
{formatValue(value)}
</span>
</div>
))}
</div>
</details>
) : (
<div className="flex items-center gap-2 text-sm text-[var(--muted-foreground)]">
{statusIcon}
<Wrench className="h-3 w-3" />
<span
className="font-medium"
style={{ fontFamily: "var(--font-code)" }}
>
{name}
</span>
</div>
)}
</div>
);
}
@@ -0,0 +1,35 @@
import * as React from "react";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors",
{
variants: {
variant: {
default:
"border-transparent bg-[var(--primary)] text-[var(--primary-foreground)]",
secondary:
"border-transparent bg-[var(--secondary)] text-[var(--secondary-foreground)]",
outline: "border-[var(--border)] text-[var(--foreground)]",
},
},
defaultVariants: {
variant: "secondary",
},
},
);
export interface BadgeProps
extends
React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };
@@ -0,0 +1,52 @@
import * as React from "react";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius)] text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] disabled:pointer-events-none disabled:opacity-50 cursor-pointer",
{
variants: {
variant: {
default:
"bg-[var(--primary)] text-[var(--primary-foreground)] hover:opacity-90",
secondary:
"bg-[var(--secondary)] text-[var(--secondary-foreground)] hover:opacity-80",
outline:
"border border-[var(--border)] bg-[var(--background)] hover:bg-[var(--secondary)]",
ghost:
"hover:bg-[var(--secondary)] hover:text-[var(--secondary-foreground)]",
destructive:
"bg-[var(--destructive)] text-[var(--destructive-foreground)] hover:opacity-90",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-6",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
),
);
Button.displayName = "Button";
export { Button, buttonVariants };
@@ -0,0 +1,85 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)] text-[var(--card-foreground)] shadow-sm",
className,
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-[var(--muted-foreground)]", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
};
@@ -0,0 +1,27 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ComponentRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-5 w-5 shrink-0 rounded-md border border-[var(--border)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-[var(--primary)] data-[state=checked]:text-[var(--primary-foreground)] data-[state=checked]:border-transparent cursor-pointer transition-colors",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-current">
<Check className="h-3.5 w-3.5" strokeWidth={3} />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };
@@ -0,0 +1,19 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-[var(--radius)] border border-[var(--input)] bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-[var(--muted-foreground)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
),
);
Input.displayName = "Input";
export { Input };
@@ -0,0 +1,30 @@
"use client";
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ComponentRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-[var(--border)]",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className,
)}
{...props}
/>
),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };
@@ -0,0 +1,24 @@
import { cn } from "@/lib/utils";
interface SpinnerProps {
className?: string;
size?: "sm" | "md" | "lg";
}
const sizeMap = {
sm: "h-4 w-4 border-2",
md: "h-6 w-6 border-2",
lg: "h-8 w-8 border-3",
};
export function Spinner({ className, size = "md" }: SpinnerProps) {
return (
<span
className={cn(
"inline-block rounded-full border-[var(--muted)] border-t-[var(--primary)] animate-spin",
sizeMap[size],
className,
)}
/>
);
}
@@ -0,0 +1,3 @@
export * from "./use-example-suggestions";
export * from "./use-generative-ui-examples";
export * from "./use-theme";
@@ -0,0 +1,69 @@
/**
* Suggestion pills shown in the chat UI. Each suggestion triggers a specific
* demo feature when clicked.
*
* Ordered from most constrained (fixed UI) to most open (freeform UI).
*
* Showcase mode (showcase.json) controls which pills are visually highlighted.
* Highlight styling: globals.css (.a2ui-highlight, .opengenui-highlight)
* A2UI agent tools: agent/src/a2ui_fixed_schema.py, a2ui_dynamic_schema.py
* A2UI catalog: src/app/declarative-generative-ui/
*/
import { useConfigureSuggestions } from "@copilotkit/react-core/v2";
import showcaseConfig from "../../showcase.json";
const showcase = showcaseConfig.showcase;
export const useExampleSuggestions = () => {
useConfigureSuggestions({
suggestions: [
{
title: "Pie Chart (Controlled Generative UI)",
message:
"Show me a pie chart of our revenue distribution by category. Use the query_data tool to fetch the data first, then render it with the pieChart component.",
},
{
title: "Bar Chart (Controlled Generative UI)",
message:
"Show me a bar chart of our expenses by category. Use the query_data tool to fetch the data first, then render it with the barChart component.",
},
{
title: "Schedule Meeting (Human In The Loop)",
message:
"I'd like to schedule a 30-minute meeting to learn about CopilotKit. Please use the scheduleTime tool to let me pick a time.",
},
{
title: "Search Flights (A2UI Fixed Schema)",
message: "Find flights from SFO to JFK for next Tuesday.",
className: showcase === "a2ui" ? "a2ui-highlight" : undefined,
},
{
title: "Sales Dashboard (A2UI Dynamic)",
message:
"First use the query_data tool to fetch the financial sales data, then using A2UI, show me a sales dashboard with total revenue, new customers, and conversion rate metrics. Include a pie chart of revenue by category and a bar chart of monthly sales.",
className: showcase === "a2ui" ? "a2ui-highlight" : undefined,
},
{
title: "Excalidraw Diagram (MCP App)",
message:
"Use Excalidraw to create a simple network diagram showing a router connected to two switches, each connected to two computers.",
},
{
title: "Calculator App (Open Generative UI)",
message:
"Using the generateSandboxedUi tool, build a modern calculator with standard buttons plus labeled metric shortcut buttons that insert their values into the display when clicked. Use sample company data.",
className: showcase === "opengenui" ? "opengenui-highlight" : undefined,
},
{
title: "Toggle Theme (Frontend Tools)",
message: "Toggle the app theme using the toggleTheme tool.",
},
{
title: "Task Manager (Shared State)",
message:
"Enable app mode and add three todos about learning CopilotKit: one about reading the docs, one about building a prototype, and one about exploring agent state.",
},
],
available: "always",
});
};
@@ -0,0 +1,83 @@
import { z } from "zod";
import { useTheme } from "@/hooks/use-theme";
import {
useComponent,
useFrontendTool,
useHumanInTheLoop,
useDefaultRenderTool,
} from "@copilotkit/react-core/v2";
import {
PieChart,
PieChartProps,
} from "@/components/generative-ui/charts/pie-chart";
import {
BarChart,
BarChartProps,
} from "@/components/generative-ui/charts/bar-chart";
import { MeetingTimePicker } from "@/components/generative-ui/meeting-time-picker";
import { ToolReasoning } from "@/components/tool-rendering";
export const useGenerativeUIExamples = () => {
const { theme, setTheme } = useTheme();
// Human-in-the-Loop (frontend tool requiring user decision)
useHumanInTheLoop({
name: "scheduleTime",
description: "Use human-in-the-loop to schedule a meeting with the user.",
parameters: z.object({
reasonForScheduling: z
.string()
.describe("Reason for scheduling, very brief - 5 words."),
meetingDuration: z
.number()
.describe("Duration of the meeting in minutes"),
}),
render: ({ respond, status, args }) => {
return <MeetingTimePicker status={status} respond={respond} {...args} />;
},
});
// Controlled Generative UI (frontend-defined chart components)
useComponent({
name: "pieChart",
description: "Controlled Generative UI that displays data as a pie chart.",
parameters: PieChartProps,
render: PieChart,
});
useComponent({
name: "barChart",
description: "Controlled Generative UI that displays data as a bar chart.",
parameters: BarChartProps,
render: BarChart,
});
// Default Tool Rendering (backend tool UI)
const ignoredTools = [
"render_a2ui", // Rendered by A2UI streaming, not as a tool card
"generate_a2ui", // Legacy: rendered by A2UI, not as a tool card
"log_a2ui_event", // Internal A2UI event tracker
];
useDefaultRenderTool({
render: ({ name, status, parameters }) => {
if (ignoredTools.includes(name)) return <></>;
return <ToolReasoning name={name} status={status} args={parameters} />;
},
});
// Frontend Tools (direct frontend state manipulation)
useFrontendTool(
{
name: "toggleTheme",
description: "Frontend tool for toggling the theme of the app.",
parameters: z.object({}),
handler: async () => {
const isDark = document.documentElement.classList.contains("dark");
setTheme(isDark ? "light" : "dark");
},
},
[theme, setTheme],
);
};
@@ -0,0 +1,43 @@
"use client";
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light" | "system";
const ThemeContext = createContext<{
theme: Theme;
setTheme: (t: Theme) => void;
}>({
theme: "system",
setTheme: () => {},
});
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("system");
useEffect(() => {
const root = document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const mq = window.matchMedia("(prefers-color-scheme: dark)");
const apply = () => {
root.classList.remove("light", "dark");
root.classList.add(mq.matches ? "dark" : "light");
};
apply();
mq.addEventListener("change", apply);
return () => mq.removeEventListener("change", apply);
}
root.classList.add(theme);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);
@@ -0,0 +1,162 @@
:root {
--n-100: #ffffff;
--n-99: #fcfcfc;
--n-98: #f9f9f9;
--n-95: #f1f1f1;
--n-90: #e2e2e2;
--n-80: #c6c6c6;
--n-70: #ababab;
--n-60: #919191;
--n-50: #777777;
--n-40: #5e5e5e;
--n-35: #525252;
--n-30: #474747;
--n-25: #3b3b3b;
--n-20: #303030;
--n-15: #262626;
--n-10: #1b1b1b;
--n-5: #111111;
--n-0: #000000;
--p-100: #ffffff;
--p-99: #fffbff;
--p-98: #fcf8ff;
--p-95: #f2efff;
--p-90: #e1e0ff;
--p-80: #c0c1ff;
--p-70: #a0a3ff;
--p-60: #8487ea;
--p-50: #6a6dcd;
--p-40: #5154b3;
--p-35: #4447a6;
--p-30: #383b99;
--p-25: #2c2e8d;
--p-20: #202182;
--p-15: #131178;
--p-10: #06006c;
--p-5: #03004d;
--p-0: #000000;
--s-100: #ffffff;
--s-99: #fffbff;
--s-98: #fcf8ff;
--s-95: #f2efff;
--s-90: #e2e0f9;
--s-80: #c6c4dd;
--s-70: #aaa9c1;
--s-60: #8f8fa5;
--s-50: #75758b;
--s-40: #5d5c72;
--s-35: #515165;
--s-30: #454559;
--s-25: #393a4d;
--s-20: #2e2f42;
--s-15: #242437;
--s-10: #191a2c;
--s-5: #0f0f21;
--s-0: #000000;
--t-100: #ffffff;
--t-99: #fffbff;
--t-98: #fff8f9;
--t-95: #ffecf4;
--t-90: #ffd8ec;
--t-80: #e9b9d3;
--t-70: #cc9eb8;
--t-60: #af849d;
--t-50: #946b83;
--t-40: #79536a;
--t-35: #6c475d;
--t-30: #5f3c51;
--t-25: #523146;
--t-20: #46263a;
--t-15: #3a1b2f;
--t-10: #2e1125;
--t-5: #22071a;
--t-0: #000000;
--nv-100: #ffffff;
--nv-99: #fffbff;
--nv-98: #fcf8ff;
--nv-95: #f2effa;
--nv-90: #e4e1ec;
--nv-80: #c8c5d0;
--nv-70: #acaab4;
--nv-60: #918f9a;
--nv-50: #777680;
--nv-40: #5e5d67;
--nv-35: #52515b;
--nv-30: #46464f;
--nv-25: #3b3b43;
--nv-20: #303038;
--nv-15: #25252d;
--nv-10: #1b1b23;
--nv-5: #101018;
--nv-0: #000000;
--e-100: #ffffff;
--e-99: #fffbff;
--e-98: #fff8f7;
--e-95: #ffedea;
--e-90: #ffdad6;
--e-80: #ffb4ab;
--e-70: #ff897d;
--e-60: #ff5449;
--e-50: #de3730;
--e-40: #ba1a1a;
--e-35: #a80710;
--e-30: #93000a;
--e-25: #7e0007;
--e-20: #690005;
--e-15: #540003;
--e-10: #410002;
--e-5: #2d0001;
--e-0: #000000;
--primary: #137fec;
--text-color: #fff;
--background-light: #f6f7f8;
--background-dark: #101922;
--border-color: oklch(
from var(--background-light) l c h / calc(alpha * 0.15)
);
--elevated-background-light: oklch(
from var(--background-light) l c h / calc(alpha * 0.05)
);
--bb-grid-size: 4px;
--bb-grid-size-2: calc(var(--bb-grid-size) * 2);
--bb-grid-size-3: calc(var(--bb-grid-size) * 3);
--bb-grid-size-4: calc(var(--bb-grid-size) * 4);
--bb-grid-size-5: calc(var(--bb-grid-size) * 5);
--bb-grid-size-6: calc(var(--bb-grid-size) * 6);
--bb-grid-size-7: calc(var(--bb-grid-size) * 7);
--bb-grid-size-8: calc(var(--bb-grid-size) * 8);
--bb-grid-size-9: calc(var(--bb-grid-size) * 9);
--bb-grid-size-10: calc(var(--bb-grid-size) * 10);
--bb-grid-size-11: calc(var(--bb-grid-size) * 11);
--bb-grid-size-12: calc(var(--bb-grid-size) * 12);
--bb-grid-size-13: calc(var(--bb-grid-size) * 13);
--bb-grid-size-14: calc(var(--bb-grid-size) * 14);
--bb-grid-size-15: calc(var(--bb-grid-size) * 15);
--bb-grid-size-16: calc(var(--bb-grid-size) * 16);
}
* {
box-sizing: border-box;
}
html,
body {
--font-family: "Google Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-family-flex:
"Google Sans Flex", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-family-mono:
"Google Sans Code", "Helvetica Neue", Helvetica, Arial, sans-serif;
background: var(--background-light);
font-family: var(--font-family);
margin: 0;
padding: 0;
width: 100svw;
height: 100svh;
}
@@ -0,0 +1,7 @@
import { clsx } from "clsx";
import type { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
@@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}