chore: import upstream snapshot with attribution
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
.next
|
||||
__pycache__
|
||||
*.pyc
|
||||
.git
|
||||
.env
|
||||
.env.local
|
||||
agent/.venv
|
||||
@@ -0,0 +1,8 @@
|
||||
AGENT_URL=http://localhost:8000
|
||||
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,50 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
.mastra/
|
||||
|
||||
|
||||
# python
|
||||
agent/venv/
|
||||
__pycache__/
|
||||
.venv/
|
||||
@@ -0,0 +1,59 @@
|
||||
# Stage 1: Build Next.js frontend
|
||||
FROM node:20-slim AS frontend
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
RUN npm install --ignore-scripts
|
||||
|
||||
# Fix CVE: Railway scans package.json, so we update it too
|
||||
RUN npm install next@latest && \
|
||||
node -e "const p=require('./package.json'); p.dependencies.next='^16'; require('fs').writeFileSync('package.json',JSON.stringify(p,null,2))"
|
||||
|
||||
COPY src/ ./src/
|
||||
COPY public/ ./public/
|
||||
COPY next.config.ts tsconfig.json postcss.config.mjs ./
|
||||
COPY showcase.json ./showcase.json
|
||||
|
||||
# Patch next.config.ts to ignore TS errors during build (can't modify original source)
|
||||
RUN node -e "const fs=require('fs'); const f='next.config.ts'; let c=fs.readFileSync(f,'utf8'); if(!c.includes('ignoreBuildErrors')){c=c.replace('};',' typescript: { ignoreBuildErrors: true },\n};'); fs.writeFileSync(f,c);}"
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Production image with Python + Node
|
||||
FROM python:3.12-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 dependencies from lockfile
|
||||
COPY agent/ ./agent/
|
||||
RUN cd agent && uv sync --frozen
|
||||
|
||||
# Copy Next.js build output and production node_modules
|
||||
COPY --from=frontend /app/.next ./.next
|
||||
COPY --from=frontend /app/node_modules ./node_modules
|
||||
COPY --from=frontend /app/package.json ./
|
||||
COPY --from=frontend /app/public ./public
|
||||
|
||||
# Copy entrypoint
|
||||
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,117 @@
|
||||
# CopilotKit <> strands Starter
|
||||
|
||||
This is a starter template for building AI agents using [strands](https://strands.com) and [CopilotKit](https://copilotkit.ai). It provides a modern Next.js application with an integrated investment analyst agent that can research stocks, analyze market data, and provide investment insights.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 20+
|
||||
- Python 3.12+
|
||||
- [uv](https://docs.astral.sh/uv/) (Python package manager)
|
||||
- OpenAI API Key (for the strands agent)
|
||||
- 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/)
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
> **Note:** Installing the package dependencies will also install the agent's python dependencies via the `install:agent` script.
|
||||
|
||||
2. Set up your OpenAI API key:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="your-openai-api-key-here"
|
||||
```
|
||||
|
||||
or create a `.env` file.
|
||||
|
||||
```bash
|
||||
echo "OPENAI_API_KEY=your-openai-api-key-here" > .env
|
||||
```
|
||||
|
||||
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:ui` - Starts only the Next.js UI server
|
||||
- `dev:agent` - Starts only the strands agent server
|
||||
- `build` - Builds the Next.js application for production
|
||||
- `start` - Starts the production server
|
||||
- `install:agent` - Installs Python dependencies for the agent
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
The main UI component is in `src/app/page.tsx`. You can:
|
||||
|
||||
- Modify the theme colors and styling
|
||||
- Add new frontend actions
|
||||
- Customize the CopilotKit sidebar appearance
|
||||
|
||||
Otherwise, check out the documentation relevant to your task:
|
||||
|
||||
- [Strands Documentation](https://strandsagents.com/latest/documentation/docs/) - Learn more about Strands and its features
|
||||
- [CopilotKit Documentation](https://docs.copilotkit.ai) - Explore CopilotKit's capabilities
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - Learn about Next.js features and API
|
||||
|
||||
## 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 strands agent is running on port 8000
|
||||
2. Your OpenAI API key is set correctly
|
||||
3. Both servers started successfully
|
||||
|
||||
### Python Dependencies
|
||||
|
||||
If you encounter Python import errors:
|
||||
|
||||
```bash
|
||||
cd agent
|
||||
uv sync
|
||||
```
|
||||
@@ -0,0 +1,353 @@
|
||||
"""Strands AG-UI Integration Example - Proverbs Agent.
|
||||
|
||||
This example demonstrates a Strands agent integrated with AG-UI, featuring:
|
||||
- Shared state management between agent and UI
|
||||
- Backend tool execution (get_weather, update_proverbs)
|
||||
- Frontend tools (set_theme_color)
|
||||
- Generative UI rendering
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
from uuid import uuid4
|
||||
|
||||
from ag_ui_strands import (
|
||||
PredictStateMapping,
|
||||
StrandsAgent,
|
||||
StrandsAgentConfig,
|
||||
ToolBehavior,
|
||||
create_strands_app,
|
||||
)
|
||||
from copilotkit import a2ui
|
||||
from dotenv import load_dotenv
|
||||
from langchain_core.messages import SystemMessage
|
||||
from langchain_core.tools import tool as lc_tool
|
||||
from langchain_openai import ChatOpenAI
|
||||
from pydantic import BaseModel, Field
|
||||
from strands import Agent, tool
|
||||
from strands.models.openai import OpenAIModel
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Env loading (shared demo root pattern used by the other integration demos)
|
||||
# ---------------------------------------------------------------------------
|
||||
_demo_root = Path(__file__).parent.parent
|
||||
for env_path in (_demo_root / ".env", Path(".env")):
|
||||
if env_path.is_file():
|
||||
load_dotenv(env_path)
|
||||
break
|
||||
else:
|
||||
load_dotenv(Path(__file__).resolve().parent.parent / ".env")
|
||||
load_dotenv()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared state schema: todos
|
||||
# ---------------------------------------------------------------------------
|
||||
# Strands "state" is a free-form dict carried on the AG-UI input. We keep
|
||||
# the same todos shape as the reference demo so the frontend renders the
|
||||
# same canvas.
|
||||
|
||||
|
||||
class Todo(BaseModel):
|
||||
id: str = ""
|
||||
title: str
|
||||
description: str
|
||||
emoji: str
|
||||
status: str = "pending" # "pending" | "completed"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tools — same names and contracts as langgraph-python
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@tool
|
||||
def manage_todos(todos: List[Todo]) -> str:
|
||||
"""Manage the current todos.
|
||||
|
||||
IMPORTANT: Always pass the full todo list, not just new items. Each todo
|
||||
should have a title, description, emoji, and status (pending/completed).
|
||||
"""
|
||||
# Strands @tool validates with pydantic but passes ``model_dump()`` output
|
||||
# to the function body — so list elements arrive as plain dicts, not
|
||||
# ``Todo`` instances. Rehydrate before touching attributes.
|
||||
todos = [Todo.model_validate(t) for t in todos]
|
||||
# Ensure every todo has a stable id. The state emission callback
|
||||
# (state_from_args below) re-reads the tool arguments and sends the
|
||||
# final list to the UI, so id injection here is enough.
|
||||
for todo in todos:
|
||||
if not todo.id:
|
||||
todo.id = str(uuid4())
|
||||
return "Successfully updated todos"
|
||||
|
||||
|
||||
@tool
|
||||
def get_todos() -> str:
|
||||
"""Get the current todos.
|
||||
|
||||
Returns a JSON string of the current todos list. The list is injected
|
||||
into the prompt via the state context builder, but this tool is still
|
||||
useful when the model wants to re-confirm state.
|
||||
"""
|
||||
# Strands tools don't get a runtime handle, so we rely on the state
|
||||
# context builder to surface the list. Returning a marker string tells
|
||||
# the model to read state from the prompt it already has.
|
||||
return "See the current todos list already provided in the conversation context."
|
||||
|
||||
|
||||
_CSV_PATH = Path(__file__).parent / "src" / "db.csv"
|
||||
with open(_CSV_PATH) as _f:
|
||||
_CACHED_DATA = list(csv.DictReader(_f))
|
||||
|
||||
|
||||
@tool
|
||||
def query_data(query: str) -> str:
|
||||
"""Query the database with a natural-language query.
|
||||
|
||||
Always call this before rendering a chart so the UI has data to plot.
|
||||
"""
|
||||
return json.dumps(_CACHED_DATA)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# A2UI tools (framework-agnostic — use copilotkit.a2ui helpers directly)
|
||||
# ---------------------------------------------------------------------------
|
||||
CATALOG_ID = "copilotkit://app-dashboard-catalog"
|
||||
FLIGHT_SURFACE_ID = "flight-search-results"
|
||||
FLIGHT_SCHEMA = a2ui.load_schema(
|
||||
Path(__file__).parent / "src" / "a2ui" / "schemas" / "flight_schema.json"
|
||||
)
|
||||
|
||||
|
||||
class Flight(BaseModel):
|
||||
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
|
||||
|
||||
|
||||
class FlightList(BaseModel):
|
||||
flights: List[Flight]
|
||||
|
||||
|
||||
@tool
|
||||
def search_flights(flight_list: FlightList) -> str:
|
||||
"""Search for flights and display the results as rich cards.
|
||||
|
||||
Return exactly 2 flights. Each flight must have: id, airline, airlineLogo
|
||||
(Google favicon API URL for the airline domain), flightNumber, origin,
|
||||
destination, date (e.g. "Tue, Mar 18" — use near-future dates),
|
||||
departureTime, arrivalTime, duration (e.g. "4h 25m"), status (e.g.
|
||||
"On Time" or "Delayed"), statusIcon (colored dot URL:
|
||||
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").
|
||||
"""
|
||||
# Strands @tool passes plain dicts (model_dump output) — ``flight_list``
|
||||
# is a dict, ``flight_list["flights"]`` is a list of dicts. Validate
|
||||
# back to Pydantic to enforce the schema, then dump for a2ui rendering.
|
||||
parsed = FlightList.model_validate(flight_list)
|
||||
flights_payload = [f.model_dump() for f in parsed.flights]
|
||||
return a2ui.render(
|
||||
operations=[
|
||||
a2ui.create_surface(FLIGHT_SURFACE_ID, catalog_id=CATALOG_ID),
|
||||
a2ui.update_components(FLIGHT_SURFACE_ID, FLIGHT_SCHEMA),
|
||||
a2ui.update_data_model(FLIGHT_SURFACE_ID, {"flights": flights_payload}),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@lc_tool
|
||||
def render_a2ui(
|
||||
surfaceId: str,
|
||||
catalogId: str,
|
||||
components: List[Dict[str, Any]],
|
||||
data: Dict[str, Any] | 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(user_intent: str, agent) -> 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 and render.
|
||||
|
||||
Seed the secondary LLM with the catalog + component schema entries
|
||||
that CopilotKit's runtime middleware injects into
|
||||
``RunAgentInput.context``. The ag_ui_strands adapter forwards those
|
||||
entries onto ``agent.state`` under the ``agui_context`` key.
|
||||
"""
|
||||
context_entries = []
|
||||
try:
|
||||
context_entries = agent.state.get("agui_context") or []
|
||||
except Exception:
|
||||
context_entries = []
|
||||
|
||||
context_text = "\n\n".join(
|
||||
e.get("value", "")
|
||||
for e in context_entries
|
||||
if isinstance(e, dict) and e.get("value")
|
||||
)
|
||||
prompt = f"{context_text}\n\n{user_intent}" if context_text else user_intent
|
||||
|
||||
model = ChatOpenAI(model="gpt-4.1")
|
||||
model_with_tool = model.bind_tools(
|
||||
[render_a2ui],
|
||||
tool_choice="render_a2ui",
|
||||
)
|
||||
try:
|
||||
response = model_with_tool.invoke(
|
||||
[SystemMessage(content=prompt)],
|
||||
)
|
||||
except Exception as exc: # pragma: no cover — surface LLM/network failures
|
||||
return json.dumps({"error": f"dynamic-a2ui LLM call failed: {exc}"})
|
||||
|
||||
if not response.tool_calls:
|
||||
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", CATALOG_ID)
|
||||
components = args.get("components", []) or []
|
||||
data = args.get("data") or {}
|
||||
|
||||
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))
|
||||
|
||||
return a2ui.render(operations=ops)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared-state config: inject todos into the prompt, stream state back on
|
||||
# every manage_todos tool call.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def build_todos_prompt(input_data, user_message: str) -> str:
|
||||
"""Inject the current todos state into the prompt."""
|
||||
state_dict = getattr(input_data, "state", None)
|
||||
if isinstance(state_dict, dict) and "todos" in state_dict:
|
||||
todos_json = json.dumps(state_dict.get("todos", []), indent=2)
|
||||
return f"Current todos list:\n{todos_json}\n\nUser request: {user_message}"
|
||||
return user_message
|
||||
|
||||
|
||||
async def todos_state_from_args(context):
|
||||
"""Snapshot state for the UI after a manage_todos call.
|
||||
|
||||
Strands calls this with the tool's parsed arguments. We return the
|
||||
`todos` list so the AG-UI layer can emit a STATE_SNAPSHOT event.
|
||||
"""
|
||||
try:
|
||||
tool_input = context.tool_input
|
||||
if isinstance(tool_input, str):
|
||||
tool_input = json.loads(tool_input)
|
||||
todos = tool_input.get("todos", [])
|
||||
return {"todos": todos}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
shared_state_config = StrandsAgentConfig(
|
||||
state_context_builder=build_todos_prompt,
|
||||
tool_behaviors={
|
||||
"manage_todos": ToolBehavior(
|
||||
state_from_args=todos_state_from_args,
|
||||
predict_state=[
|
||||
PredictStateMapping(
|
||||
state_key="todos",
|
||||
tool="manage_todos",
|
||||
tool_argument="todos",
|
||||
)
|
||||
],
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent wiring
|
||||
# ---------------------------------------------------------------------------
|
||||
api_key = os.getenv("OPENAI_API_KEY", "")
|
||||
model = OpenAIModel(
|
||||
client_args={"api_key": api_key},
|
||||
model_id="gpt-5.4",
|
||||
params={"parallel_tool_calls": False},
|
||||
)
|
||||
|
||||
system_prompt = (
|
||||
"You are a polished, professional demo assistant. Keep responses to 1-2 sentences.\n\n"
|
||||
"Tool guidance:\n"
|
||||
"- Flights: call search_flights to show flight cards with a pre-built schema.\n"
|
||||
"- Dashboards & rich UI: call generate_a2ui to create dashboard UIs with metrics,\n"
|
||||
" charts, tables, and cards. It handles rendering automatically.\n"
|
||||
"- Charts: call query_data first, then render with the chart component.\n"
|
||||
"- Todos: enable app mode first, then manage todos.\n"
|
||||
"- Diagrams (Excalidraw): when MCP Excalidraw tools are exposed (e.g. create_view),\n"
|
||||
" call create_view ONCE with 3-5 elements (shapes + arrows + optional title text).\n"
|
||||
" Include ONE cameraUpdate at the end to frame the diagram. Do NOT call read_me\n"
|
||||
" even if it appears in the toolset — you already know the basic shape API.\n"
|
||||
'- A2UI actions: when you see a log_a2ui_event result (e.g. "view_details"),\n'
|
||||
" respond with a brief confirmation. The UI already updated on the frontend."
|
||||
)
|
||||
|
||||
strands_agent = Agent(
|
||||
model=model,
|
||||
system_prompt=system_prompt,
|
||||
tools=[manage_todos, get_todos, query_data, generate_a2ui, search_flights],
|
||||
)
|
||||
|
||||
agui_agent = StrandsAgent(
|
||||
agent=strands_agent,
|
||||
name="todo_demo_agent",
|
||||
description=(
|
||||
"A polished demo assistant matching the canonical langgraph-python "
|
||||
"todo / charts / a2ui / flights showcase, running on Strands."
|
||||
),
|
||||
config=shared_state_config,
|
||||
)
|
||||
|
||||
agent_path = os.getenv("AGENT_PATH", "/")
|
||||
app = create_strands_app(agui_agent, agent_path)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
port = int(os.getenv("AGENT_PORT", 8000))
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True)
|
||||
@@ -0,0 +1,21 @@
|
||||
[project]
|
||||
name = "aws-strands-server"
|
||||
version = "0.1.0"
|
||||
description = "Strands integration server for AG-UI using OpenAI models"
|
||||
authors = [{ name = "AG-UI Contributors" }]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12,<3.14"
|
||||
dependencies = [
|
||||
"ag-ui-protocol==0.1.18",
|
||||
"fastapi>=0.115.12",
|
||||
"uvicorn>=0.34.3",
|
||||
"strands-agents[OpenAI]==1.18.0",
|
||||
"strands-agents-tools==0.2.16",
|
||||
"ag_ui_strands==0.1.9",
|
||||
"copilotkit==0.1.94",
|
||||
"langchain==1.2.15",
|
||||
"langchain-openai==1.1.9",
|
||||
"openai==1.109.1",
|
||||
"python-dotenv>=1.0.0,<2.0.0",
|
||||
"pydantic>=2.0.0,<3.0.0",
|
||||
]
|
||||
@@ -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,64 @@
|
||||
"""Dynamic A2UI tool: LLM-generated UI from conversation context.
|
||||
|
||||
A secondary LLM (langchain_openai) generates v0.9 A2UI components via a
|
||||
structured tool call. The generate_a2ui tool wraps the output as
|
||||
a2ui_operations, which the middleware/runtime detects and renders
|
||||
automatically. Identical surface to the canonical demo's tool —
|
||||
implementation differs only by the secondary-LLM wiring.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from copilotkit import a2ui
|
||||
from langchain_core.messages import SystemMessage
|
||||
from langchain_openai import ChatOpenAI
|
||||
from pydantic import BaseModel, Field
|
||||
from strands import tool
|
||||
|
||||
CATALOG_ID = "copilotkit://app-dashboard-catalog"
|
||||
|
||||
|
||||
class _A2UIRenderArgs(BaseModel):
|
||||
surfaceId: str = "dynamic-surface"
|
||||
catalogId: str = CATALOG_ID
|
||||
components: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
data: Dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
@tool
|
||||
def generate_a2ui(user_intent: str) -> 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 runtime to detect and render.
|
||||
"""
|
||||
model = ChatOpenAI(model="gpt-4.1")
|
||||
model_with_tool = model.bind_tools(
|
||||
[_A2UIRenderArgs.model_json_schema()],
|
||||
tool_choice={
|
||||
"type": "function",
|
||||
"function": {"name": "_A2UIRenderArgs"},
|
||||
},
|
||||
)
|
||||
try:
|
||||
response = model_with_tool.invoke([SystemMessage(content=user_intent)])
|
||||
except Exception as exc: # surface LLM/network failures
|
||||
return json.dumps({"error": f"dynamic-a2ui LLM call failed: {exc}"})
|
||||
|
||||
if not response.tool_calls:
|
||||
return json.dumps({"error": "LLM did not emit dynamic A2UI arguments"})
|
||||
|
||||
args = response.tool_calls[0]["args"]
|
||||
parsed = _A2UIRenderArgs.model_validate(args)
|
||||
|
||||
ops = [
|
||||
a2ui.create_surface(parsed.surfaceId, catalog_id=parsed.catalogId),
|
||||
a2ui.update_components(parsed.surfaceId, parsed.components),
|
||||
]
|
||||
if parsed.data:
|
||||
ops.append(a2ui.update_data_model(parsed.surfaceId, parsed.data))
|
||||
|
||||
return a2ui.render(operations=ops)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Fixed-schema A2UI tool: flight search results.
|
||||
|
||||
Schema is loaded from a JSON file. Only the data changes per invocation.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from copilotkit import a2ui
|
||||
from pydantic import BaseModel
|
||||
from strands 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(BaseModel):
|
||||
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 (Google favicon API URL like
|
||||
"https://www.google.com/s2/favicons?domain=united.com&sz=128"),
|
||||
flightNumber, origin, destination, date (e.g. "Tue, Mar 18" — use
|
||||
near-future dates), departureTime, arrivalTime, duration (e.g.
|
||||
"4h 25m"), status (e.g. "On Time" or "Delayed"), statusIcon (colored
|
||||
dot URL: 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").
|
||||
"""
|
||||
# Strands @tool passes plain dicts to the function body, so ``flights``
|
||||
# is a list of dicts here. Validate to enforce the schema, then dump
|
||||
# for a2ui rendering.
|
||||
flights_payload = [Flight.model_validate(f).model_dump() for f in flights]
|
||||
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_payload}),
|
||||
],
|
||||
)
|
||||
@@ -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,20 @@
|
||||
import csv
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from strands import tool
|
||||
|
||||
# Read data at module load time to avoid file I/O issues in sandboxed
|
||||
# tool execution environments.
|
||||
_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) -> str:
|
||||
"""Query the database with a natural-language query.
|
||||
|
||||
Always call this before rendering a chart so the UI has data to plot.
|
||||
"""
|
||||
return json.dumps(_cached_data)
|
||||
@@ -0,0 +1,42 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel
|
||||
from strands import tool
|
||||
|
||||
|
||||
class Todo(BaseModel):
|
||||
id: str = ""
|
||||
title: str
|
||||
description: str
|
||||
emoji: str
|
||||
status: str = "pending" # "pending" | "completed"
|
||||
|
||||
|
||||
@tool
|
||||
def manage_todos(todos: list[Todo]) -> str:
|
||||
"""Manage the current todos.
|
||||
|
||||
IMPORTANT: Always pass the full list, not just new items. Each todo
|
||||
should have a title, description, emoji, and status (pending/completed).
|
||||
"""
|
||||
# Strands @tool passes ``model_dump()`` output, so list elements arrive
|
||||
# as plain dicts. Rehydrate before accessing fields.
|
||||
todos = [Todo.model_validate(t) for t in todos]
|
||||
for todo in todos:
|
||||
if not todo.id:
|
||||
todo.id = str(uuid4())
|
||||
return "Successfully updated todos"
|
||||
|
||||
|
||||
@tool
|
||||
def get_todos() -> str:
|
||||
"""Get the current todos.
|
||||
|
||||
The current list is injected into the prompt by the state context
|
||||
builder, so this tool just acknowledges that and tells the model to
|
||||
read it from there.
|
||||
"""
|
||||
return "See the current todos list already provided in the conversation context."
|
||||
|
||||
|
||||
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 + ag_ui_strands (uvicorn) 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:8000/health')",
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 30s
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile.app
|
||||
environment:
|
||||
- AGENT_URL=http://agent:8000
|
||||
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,17 @@
|
||||
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 pyproject.toml uv.lock ./
|
||||
COPY src/ ./src/
|
||||
COPY main.py ./
|
||||
|
||||
RUN uv sync
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -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,28 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "[entrypoint] Starting: strands-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 the Strands agent via AG-UI protocol.
|
||||
# main.py is self-serving (uvicorn on AGENT_PORT); run it from the uv venv.
|
||||
echo "[entrypoint] Starting agent on port 8123..."
|
||||
(cd agent && AGENT_PORT=8123 uv run python main.py 2>&1) &
|
||||
AGENT_PID=$!
|
||||
|
||||
sleep 3
|
||||
|
||||
# Start Next.js. This image is a non-standalone build (.next + node_modules +
|
||||
# package.json), so serve via `next start`, not standalone server.js.
|
||||
echo "[entrypoint] Starting Next.js on port ${PORT:-3000}..."
|
||||
HOSTNAME=0.0.0.0 PORT=${PORT:-3000} ./node_modules/.bin/next start -H 0.0.0.0 -p "${PORT:-3000}" 2>&1 &
|
||||
NEXT_PID=$!
|
||||
|
||||
echo "[entrypoint] Agent=$AGENT_PID Next=$NEXT_PID"
|
||||
wait -n $AGENT_PID $NEXT_PID
|
||||
exit $?
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"fixtures": [
|
||||
{
|
||||
"match": { "toolCallId": "call_manage_todos_001" },
|
||||
"response": {
|
||||
"content": "I've added the tasks to your todo list:\n\n1. **Review project plan** - Go through the Q2 project plan and provide feedback\n2. **Update documentation** - Update the API docs with the new endpoints\n\nBoth 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_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": { "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": { "userMessage": "Task Manager" },
|
||||
"response": {
|
||||
"toolCalls": [
|
||||
{
|
||||
"name": "manage_todos",
|
||||
"arguments": "{\"todos\":[{\"title\":\"Review project plan\",\"description\":\"Go through the Q2 project plan and provide feedback\",\"emoji\":\"📋\",\"status\":\"pending\"},{\"title\":\"Update documentation\",\"description\":\"Update the API docs with the new endpoints\",\"emoji\":\"📝\",\"status\":\"pending\"}]}",
|
||||
"id": "call_manage_todos_001"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": { "userMessage": "Pie Chart" },
|
||||
"response": {
|
||||
"toolCalls": [
|
||||
{
|
||||
"name": "query_data",
|
||||
"arguments": "{\"query\":\"Show quarterly revenue data as a pie chart\"}",
|
||||
"id": "call_query_data_001"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": { "userMessage": "Toggle Theme" },
|
||||
"response": {
|
||||
"content": "I've toggled the theme for you. The application should now be displaying in the alternate color scheme. Let me know if you'd like to switch back or if there's anything else I can help with!"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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 `pnpm 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,51 @@
|
||||
{
|
||||
"name": "strands-starter",
|
||||
"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:agent": "./scripts/run-agent.sh || scripts\\run-agent.bat",
|
||||
"dev:ui": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"install:agent": "./scripts/setup-agent.sh || scripts\\setup-agent.bat",
|
||||
"postinstall": "npm run install:agent",
|
||||
"dev:debug": "LOG_LEVEL=debug npm run dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ag-ui/client": "0.0.57",
|
||||
"@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"
|
||||
},
|
||||
"overrides": {
|
||||
"@ag-ui/client": "0.0.57",
|
||||
"@ag-ui/core": "0.0.57",
|
||||
"@ag-ui/encoder": "0.0.57",
|
||||
"@ag-ui/proto": "0.0.57"
|
||||
}
|
||||
}
|
||||
@@ -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,6 @@
|
||||
@echo off
|
||||
REM Navigate to the agent directory
|
||||
cd /d %~dp0\..\agent
|
||||
|
||||
REM Run the agent using uv
|
||||
uv run python main.py
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Navigate to the agent directory
|
||||
cd "$(dirname "$0")/../agent" || exit 1
|
||||
|
||||
# Run the agent using uv
|
||||
uv run python main.py
|
||||
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
REM Navigate to the agent directory
|
||||
cd /d "%~dp0\..\agent" || exit /b 1
|
||||
|
||||
REM Install dependencies using uv
|
||||
uv sync
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Navigate to the agent directory
|
||||
cd "$(dirname "$0")/../agent" || exit 1
|
||||
|
||||
# Install dependencies using uv
|
||||
uv sync
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"showcase": "default"
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
CopilotRuntime,
|
||||
CopilotKitIntelligence,
|
||||
createCopilotEndpoint,
|
||||
InMemoryAgentRunner,
|
||||
} from "@copilotkit/runtime/v2";
|
||||
import { HttpAgent } from "@ag-ui/client";
|
||||
import { handle } from "hono/vercel";
|
||||
|
||||
// Strands-specific: the agent runs under uvicorn + ag_ui_strands, which
|
||||
// speaks AG-UI directly. We talk to it via HttpAgent (not LangGraphAgent
|
||||
// or LangGraphHttpAgent — those target LangGraph-shaped endpoints).
|
||||
// Everything else — runtime config, v2 endpoint wiring, MCP apps,
|
||||
// openGenerativeUI, a2ui — mirrors the canonical demo.
|
||||
const defaultAgent = new HttpAgent({
|
||||
url: `${process.env.AGENT_URL || process.env.STRANDS_AGENT_URL || "http://localhost:8000"}/`,
|
||||
});
|
||||
|
||||
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"]
|
||||
}
|
||||