chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1,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",
},
);
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,125 @@
/* CopilotKit brand fonts */
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300..800&family=Spline+Sans+Mono:wght@400;500;600&display=swap");
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
:root {
--background: #ffffff;
--foreground: #010507;
/* Semantic tokens — CopilotKit brand */
--card: #ffffff;
--card-foreground: #010507;
--primary: #010507;
--primary-foreground: #ffffff;
--secondary: #ededf5;
--secondary-foreground: #010507;
--muted: #ededf5;
--muted-foreground: #57575b;
--accent: #eee6fe;
--accent-foreground: #010507;
--destructive: #fa5f67;
--destructive-foreground: #ffffff;
--border: #dbdbe5;
--input: #dbdbe5;
--ring: #bec2ff;
/* Chart tokens */
--chart-tooltip-bg: #ffffff;
--chart-tooltip-border: #dbdbe5;
--chart-axis: #57575b;
--radius: 0.75rem;
/* Override CopilotKit font */
--cpk-default-font-family: var(--font-body);
/* Fonts */
--font-body: "Plus Jakarta Sans", "DM Sans", "Segoe UI", sans-serif;
--font-code:
"Spline Sans Mono", "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
/* Brand accent colors */
--cpk-mint-400: #85ecce;
--cpk-mint-800: #189370;
--cpk-lilac-400: #bec2ff;
--cpk-orange-400: #ffac4d;
--cpk-yellow-400: #fff388;
--cpk-ambient-gradient: linear-gradient(
90deg,
#bec2ff 0%,
#85ecce 45.673%,
#ffac4d 100%
);
}
:root.dark,
.dark {
--background: #010507;
--foreground: #ffffff;
--card: #191a1e;
--card-foreground: #ffffff;
--primary: #ffffff;
--primary-foreground: #010507;
--secondary: #242529;
--secondary-foreground: #ffffff;
--muted: #242529;
--muted-foreground: #adadb2;
--accent: #303136;
--accent-foreground: #ffffff;
--destructive: #fa5f67;
--destructive-foreground: #ffffff;
--border: #303136;
--input: #303136;
--ring: #bec2ff;
--chart-tooltip-bg: #191a1e;
--chart-tooltip-border: #303136;
--chart-axis: #adadb2;
}
body {
background: var(--background);
color: var(--foreground);
font-family: var(--font-body);
}
body,
html {
height: 100%;
}
/*
* Showcase highlight for suggestion pills.
* Applied via className in use-example-suggestions.tsx when showcase.json
* is set to a non-default mode (e.g. "a2ui", "opengenui"). Uses CopilotKit
* brand colors with a subtle tinted background.
* Add new showcase variants here following the same pattern.
*/
[data-copilotkit] button[data-slot="suggestion-pill"].a2ui-highlight {
border-color: var(--cpk-lilac-400);
background: color-mix(in srgb, var(--cpk-lilac-400) 15%, var(--background));
}
[data-copilotkit] button[data-slot="suggestion-pill"].opengenui-highlight {
border-color: var(--cpk-mint-400);
background: color-mix(in srgb, var(--cpk-mint-400) 15%, var(--background));
}
/*
* Position the CopilotKit inspector FAB BENEATH the Chat/App toggle in the
* top-right, instead of the header row where it would overlap it. The gap below
* the toggle matches the toggle's own 16px gap from the top of the viewport.
* The toggle is 16px from the top and 46px tall (both breakpoints), so its
* bottom is at 62px; a 16px gap puts the FAB circle at 78px. The visible circle
* renders ~16px below the host's `top`, so host top = 78 - 16 = 62px. Right edge
* matches the toggle's 16px inset.
*/
cpk-web-inspector {
top: 62px !important;
bottom: auto !important;
right: 16px !important;
}
@@ -0,0 +1,59 @@
"use client";
import "./globals.css";
import "@copilotkit/react-core/v2/styles.css";
import { CopilotKit } from "@copilotkit/react-core/v2";
import { ThemeProvider } from "@/hooks/use-theme";
// A2UI catalog: definitions + renderers in ./declarative-generative-ui/
import { demonstrationCatalog } from "./declarative-generative-ui/renderers";
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<title>CopilotKit</title>
<link
rel="icon"
type="image/svg+xml"
href="/copilotkit-logo-mark.svg"
/>
{/*
Set the theme class BEFORE first paint to avoid a white→dark flash.
ThemeProvider applies the theme in a useEffect (post-hydration), so
without this the page paints unthemed (light) first, then flips. This
blocking inline script matches ThemeProvider's "system" default so
there's no flash and no class mismatch when the provider re-applies.
*/}
<script
dangerouslySetInnerHTML={{
__html:
"(function(){try{var d=window.matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.add(d?'dark':'light');}catch(e){}})();",
}}
/>
</head>
{/*
suppressHydrationWarning: browser extensions (e.g. Grammarly) inject
attributes like data-gr-ext-installed onto <body> before React hydrates,
which would otherwise surface as a hydration mismatch on first load.
This only relaxes the check for <body>'s own attributes (one level deep);
everything rendered inside <body> is still fully hydration-checked.
*/}
<body className={`antialiased`} suppressHydrationWarning>
<ThemeProvider>
<CopilotKit
runtimeUrl="/api/copilotkit"
inspectorDefaultAnchor={{ horizontal: "right", vertical: "top" }}
a2ui={{ catalog: demonstrationCatalog }}
openGenerativeUI={{}}
useSingleEndpoint={false}
>
{children}
</CopilotKit>
</ThemeProvider>
</body>
</html>
);
}
@@ -0,0 +1,67 @@
.layout {
display: grid;
/*
Reserve the desktop drawer's width (its default `--cpk-drawer-width`, 320px)
as a fixed first column so the layout does NOT shift when the client-only
drawer mounts after hydration. Without this the column is `auto` → 0px until
the drawer boots, so the chat starts full-width and then "slides over" when
the drawer appears. On mobile the drawer is an off-canvas overlay (out of
flow), so the column collapses to `auto` (≈0) and the chat fills the width.
*/
grid-template-columns: var(--cpk-drawer-reserved-width, 320px) minmax(0, 1fr);
/* Bound the row so the drawer's thread list scrolls internally (pinned
header) instead of the panel growing to content height. */
grid-template-rows: minmax(0, 1fr);
/* The drawer sets --cpk-drawer-reserved-width to 0 when collapsed on desktop,
so the reserved column collapses and the chat reclaims the space. */
transition: grid-template-columns 0.2s ease;
min-height: 100vh;
min-height: 100svh;
height: 100dvh;
width: 100%;
overflow: hidden;
/*
Uniform 16px gutter for the collapsed cluster — top AND left — matching the
right-side controls' (toggle + inspector) 16px inset. Same on both
breakpoints. These inherit into <copilotkit-threads-drawer> and pierce its
shadow root.
*/
--cpk-drawer-launcher-top: 16px;
--cpk-drawer-launcher-left: 16px;
}
.mainPanel {
/*
Pin the chat to the SECOND grid track explicitly. The client-only
<CopilotThreadsDrawer> renders NOTHING during SSR/prerender (it is mounted-gated),
so at first paint the grid has a single child — this panel. Without an
explicit placement it would flow into the FIRST (reserved 320px) track,
cramming the chat at x:0, then jump to the second track once the drawer
mounts as the first child — the "starts left, then slides over" shift.
Forcing column 2 keeps the chat in its final position from first paint; the
drawer fills the reserved first track when it mounts.
*/
grid-column: 2;
min-width: 0;
min-height: 100vh;
min-height: 100svh;
height: 100dvh;
overflow: auto;
}
/*
Mobile (≤768px): the drawer is an off-canvas overlay, so don't reserve a
column for it — collapse to a single track and let the chat fill the full
width. This block MUST come after the base rules above: media queries do not
add specificity, so a same-specificity base rule placed later would otherwise
win and leak the desktop two-column layout onto mobile.
*/
@media (max-width: 768px) {
.layout {
grid-template-columns: minmax(0, 1fr);
}
.mainPanel {
grid-column: auto;
}
}
@@ -0,0 +1,49 @@
"use client";
import { ExampleLayout } from "@/components/example-layout";
import { ExampleCanvas } from "@/components/example-canvas";
import { useGenerativeUIExamples, useExampleSuggestions } from "@/hooks";
import {
CopilotChat,
CopilotChatConfigurationProvider,
CopilotThreadsDrawer,
} from "@copilotkit/react-core/v2";
import styles from "./page.module.css";
export default function HomePage() {
useGenerativeUIExamples();
useExampleSuggestions();
return (
/*
One UNCONTROLLED CopilotChatConfigurationProvider (no `threadId` prop) owns
the active thread for the whole surface. The SDK <CopilotThreadsDrawer> drives it
directly — picking a row sets the active thread, "+ New" resets to a fresh
thread (clearing the chat) — with no host thread-state. The chat and the
canvas read the same active thread from the provider (the canvas's
`useAgent()` falls back to it), so they stay on the same per-thread agent
clone the chat's /connect replay populates. A *controlled* provider would
block "+ New" from resetting the chat, so uncontrolled-inside-provider is
required, not optional.
*/
<CopilotChatConfigurationProvider agentId="default">
<div className={styles.layout}>
{/* SDK threads drawer (replaces the hand-rolled fork). License-gated: the locked view's Upgrade CTA opens the Intelligence docs by default. */}
<CopilotThreadsDrawer agentId="default" />
<div className={styles.mainPanel}>
<ExampleLayout
chatContent={
<CopilotChat
attachments={{ enabled: true }}
input={{ disclaimer: () => null, className: "pb-6" }}
/>
}
appContent={<ExampleCanvas />}
/>
</div>
</div>
</CopilotChatConfigurationProvider>
);
}