chore: import upstream snapshot with attribution
@@ -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
|
||||
|
@@ -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,
|
||||
]
|
||||
@@ -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"]
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
},
|
||||
);
|
||||
|
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"]
|
||||
}
|
||||