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
+50
View File
@@ -0,0 +1,50 @@
# Angular Demo Server
Minimal Hono server for the Angular CopilotKit demo, matching the React demo behavior.
## Setup
1. Add your OpenAI API key to `.env`:
```
OPENAI_API_KEY=sk-...
```
2. Install dependencies (from repository root):
```bash
pnpm install
```
3. Start the server:
```bash
pnpm --filter @copilotkit/angular-demo-server dev
```
The server will be available at http://localhost:3001/api/copilotkit
## Testing
To verify the server is running:
```bash
curl http://localhost:3001/api/copilotkit/info
```
You should see JSON with agents and version information.
## Using with Angular Storybook
1. Start the demo server (Terminal A):
```bash
pnpm --filter @copilotkit/angular-demo-server dev
```
2. Start Angular Storybook (Terminal B):
```bash
pnpm --filter storybook-angular dev
```
3. Open http://localhost:6007 and navigate to "Live/CopilotChat"
@@ -0,0 +1,24 @@
{
"name": "@copilotkit/angular-demo-server",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "tsx --env-file-if-exists ../demo/.env --env-file-if-exists .env src/index.ts",
"start": "tsx --env-file-if-exists ../demo/.env --env-file-if-exists .env src/index.ts"
},
"dependencies": {
"@ag-ui/langgraph": "^0.0.11",
"@ai-sdk/openai": "3.0.36",
"@copilotkit/demo-agents": "workspace:^",
"@copilotkit/runtime": "workspace:^",
"@hono/node-server": "^1.13.6",
"hono": "^4.11.4"
},
"devDependencies": {
"nodemon": "^3.1.7",
"tsx": "^4.19.2",
"typescript": "^5.6.3"
}
}
@@ -0,0 +1,144 @@
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { cors } from "hono/cors";
import {
BuiltInAgent,
CopilotRuntime,
createCopilotEndpoint,
InMemoryAgentRunner,
} from "@copilotkit/runtime/v2";
import type { BuiltInAgentClassicConfig } from "@copilotkit/runtime/v2";
import { createOpenAI } from "@ai-sdk/openai";
import { SlowToolCallStreamingAgent } from "@copilotkit/demo-agents";
const openRouterApiKey = process.env.OPENROUTER_API_KEY?.trim();
const openAIApiKey = process.env.OPENAI_API_KEY?.trim();
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
const DEFAULT_OPENROUTER_MODEL = "anthropic/claude-sonnet-4.6";
const DEFAULT_OPENROUTER_MAX_OUTPUT_TOKENS = 16_384;
function determineOpenRouterModelId(): string {
const configuredModel = process.env.OPENROUTER_MODEL?.trim();
if (!configuredModel) {
return DEFAULT_OPENROUTER_MODEL;
}
if (configuredModel.includes("/")) {
return configuredModel;
}
return `openai/${configuredModel}`;
}
function determineMaxOutputTokens(): number | undefined {
if (!openRouterApiKey) {
return undefined;
}
const configuredLimit = Number(
process.env.OPENROUTER_MAX_OUTPUT_TOKENS?.trim(),
);
if (Number.isSafeInteger(configuredLimit) && configuredLimit > 0) {
return configuredLimit;
}
return DEFAULT_OPENROUTER_MAX_OUTPUT_TOKENS;
}
function determineModel(): BuiltInAgentClassicConfig["model"] {
if (openRouterApiKey) {
const openrouter = createOpenAI({
apiKey: openRouterApiKey,
baseURL: process.env.OPENROUTER_BASE_URL?.trim() || OPENROUTER_BASE_URL,
});
return openrouter(determineOpenRouterModelId());
}
if (openAIApiKey) {
return "openai/gpt-5.2";
}
if (process.env.ANTHROPIC_API_KEY?.trim()) {
return "anthropic/claude-3-7-sonnet-20250219";
}
if (process.env.GOOGLE_API_KEY?.trim()) {
return "google/gemini-2.5-pro";
}
return "openai/gpt-5.2";
}
const builtInAgent = new BuiltInAgent({
model: determineModel(),
maxOutputTokens: determineMaxOutputTokens(),
prompt:
"You are a helpful AI assistant. Use reasoning to answer the user's question. If you don't know the answer, say you don't know.",
providerOptions: {
...(openAIApiKey
? { openai: { reasoningEffort: "high", reasoningSummary: "detailed" } }
: {}),
...(!openAIApiKey &&
!openRouterApiKey &&
!!process.env.ANTHROPIC_API_KEY?.trim() && {
anthropic: { thinking: { type: "enabled", budgetTokens: 5000 } },
}),
},
});
const agents = {
default: builtInAgent,
"slow-tools": new SlowToolCallStreamingAgent(),
};
const runtime = new CopilotRuntime({
agents,
runner: new InMemoryAgentRunner(),
a2ui: {},
openGenerativeUI: true,
});
// Create a main app with CORS enabled
const app = new Hono();
// Enable CORS for local dev (Angular demo at http://localhost:4200)
app.use(
"*",
cors({
origin: "http://localhost:4200",
allowMethods: ["GET", "POST", "OPTIONS", "PUT", "DELETE"],
allowHeaders: [
"Content-Type",
"Authorization",
"X-Requested-With",
"x-copilotcloud-public-api-key",
],
exposeHeaders: ["Content-Type"],
credentials: true,
maxAge: 86400,
}),
);
// Create the CopilotKit endpoint
const copilotApp = createCopilotEndpoint({
runtime,
basePath: "/api/copilotkit",
});
// Mount the CopilotKit app
app.route("/", copilotApp);
const port = Number(process.env.PORT || 3001);
const server = serve({ fetch: app.fetch, port });
server.on("error", (error: NodeJS.ErrnoException) => {
if (error.code === "EADDRINUSE") {
console.error(
`Port ${port} is already in use. Stop the existing process or set PORT to another value.`,
);
process.exit(1);
}
throw error;
});
console.log(
`CopilotKit runtime listening at http://localhost:${port}/api/copilotkit`,
);
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "dist"
},
"include": ["src"]
}
+63
View File
@@ -0,0 +1,63 @@
{
"$schema": "https://json.schemastore.org/angular-cli",
"version": 1,
"projects": {
"angular-demo": {
"projectType": "application",
"root": ".",
"sourceRoot": "src",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/angular-demo",
"index": "src/index.html",
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets"],
"styles": [
"../../../../packages/angular/src/styles/generated.css",
"src/styles.css"
],
"optimization": false
}
},
"serve": {
"builder": "@angular/build:dev-server",
"options": {
"buildTarget": "angular-demo:build"
}
}
}
}
},
"cli": {
"analytics": "980356bd-241e-4f9c-91f9-6c7714a97a43"
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}
+62
View File
@@ -0,0 +1,62 @@
{
"name": "@copilotkit/angular-demo",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "ng serve",
"build": "ng build",
"dev": "pnpm -s run serve:clean",
"serve:clean": "rimraf .angular || true && ng serve",
"clean": "rimraf dist .angular"
},
"dependencies": {
"@ag-ui/client": "0.0.53",
"@ag-ui/core": "0.0.53",
"@ag-ui/encoder": "0.0.53",
"@ag-ui/proto": "0.0.53",
"@a2ui/web_core": "0.9.0",
"@angular/animations": "^21.2.15",
"@angular/cdk": "^21.2.13",
"@angular/common": "^21.2.15",
"@angular/compiler": "^21.2.15",
"@angular/core": "^21.2.15",
"@angular/forms": "^21.2.15",
"@angular/platform-browser": "^21.2.15",
"@angular/platform-browser-dynamic": "^21.2.15",
"@angular/router": "^21.2.15",
"@copilotkit/a2ui-renderer": "workspace:*",
"@copilotkit/core": "workspace:*",
"@copilotkit/shared": "workspace:*",
"@copilotkit/web-inspector": "workspace:*",
"@copilotkit/angular": "workspace:*",
"@jetbrains/websandbox": "^1.1.3",
"@tanstack/pacer": "^0.20.1",
"clsx": "^2.1.1",
"compare-versions": "^6.1.1",
"fast-json-patch": "^3.1.1",
"highlight.js": "^11.11.1",
"katex": "^0.16.22",
"lit": "^3.3.1",
"lucide": "^0.525.0",
"lucide-angular": "^0.540.0",
"marked": "^16.2.0",
"partial-json": "^0.1.7",
"phoenix": "^1.8.4",
"rxjs": "^7.8.1",
"tailwind-merge": "^2.6.0",
"tslib": "^2.8.1",
"untruncate-json": "^0.0.1",
"uuid": "^11.1.0",
"zod": "^3.25.75",
"zod-to-json-schema": "^3.24.6"
},
"devDependencies": {
"@angular/build": "^21.2.13",
"@angular/cli": "^21.2.13",
"@angular/compiler-cli": "^21.2.15",
"nodemon": "^3.1.7",
"rimraf": "^6.0.1",
"typescript": "5.9.3"
}
}
@@ -0,0 +1,60 @@
import { Component, inject } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import {
ActivatedRoute,
NavigationEnd,
Router,
RouterOutlet,
} from "@angular/router";
import { filter, map, startWith } from "rxjs";
import { DemoWebInspectorComponent } from "./components/demo-web-inspector.component";
@Component({
selector: "app-root",
standalone: true,
imports: [RouterOutlet, DemoWebInspectorComponent],
template: `
<div class="demo-shell">
<router-outlet />
@if (showInspector()) {
<angular-demo-web-inspector />
}
</div>
`,
styles: `
.demo-shell {
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
overflow: hidden;
display: block;
}
`,
})
export class AppComponent {
private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);
/**
* The web inspector is shown on every route except those that opt out via
* `data: { inspector: false }` (currently the headless route).
*/
protected readonly showInspector = toSignal(
this.router.events.pipe(
filter((event) => event instanceof NavigationEnd),
map(() => this.inspectorEnabled()),
startWith(this.inspectorEnabled()),
),
{ initialValue: true },
);
private inspectorEnabled(): boolean {
let route = this.route;
while (route.firstChild) {
route = route.firstChild;
}
return route.snapshot.data["inspector"] !== false;
}
}
@@ -0,0 +1,44 @@
import type { ApplicationConfig } from "@angular/core";
import { importProvidersFrom } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { provideRouter } from "@angular/router";
import {
provideCopilotKit,
provideCopilotChatLabels,
} from "@copilotkit/angular";
import { WildcardToolRenderComponent } from "./components/wildcard-tool-render.component";
import { a2uiDemoSandboxFunctions } from "./routes/a2ui/a2ui-demo-sandbox-functions";
import { routes } from "./app.routes";
import { z } from "zod";
export const appConfig: ApplicationConfig = {
providers: [
importProvidersFrom(BrowserModule),
provideRouter(routes),
provideCopilotKit({
runtimeUrl: "http://localhost:3001/api/copilotkit",
licenseKey: "ck_pub_00000000000000000000000000000000",
renderToolCalls: [
{
name: "*",
args: z.record(z.string(), z.unknown()),
component: WildcardToolRenderComponent,
},
],
suggestionsConfig: [
{
instructions:
"Suggest follow-up tasks based on the current page content",
available: "always",
},
],
humanInTheLoop: [],
openGenerativeUI: { sandboxFunctions: a2uiDemoSandboxFunctions },
}),
provideCopilotChatLabels({
chatInputPlaceholder: "Ask me anything...",
chatDisclaimerText:
"CopilotKit Angular Demo - AI responses may need verification.",
}),
],
};
@@ -0,0 +1,57 @@
import type { Routes } from "@angular/router";
export const routes: Routes = [
{
// Default landing: redirect the root to the A2UI demo.
path: "",
pathMatch: "full",
redirectTo: "a2ui-demo",
},
{
path: "a2ui-demo",
title: "A2UI Demo",
loadComponent: () =>
import("./routes/a2ui/a2ui-demo.component").then(
(m) => m.A2UIDemoComponent,
),
},
{
path: "headless",
title: "Headless Chat",
loadComponent: () =>
import("./routes/headless/headless-chat.component").then(
(m) => m.HeadlessChatComponent,
),
// The web inspector is hidden on the headless route.
data: { inspector: false },
},
{
path: "custom-input",
title: "Custom Input",
loadComponent: () =>
import("./routes/custom-input/custom-input-chat.component").then(
(m) => m.CustomInputChatComponent,
),
},
{
path: "ukg-port",
title: "UKG Port",
loadComponent: () =>
import("./routes/ukg-port/co-pilot-port.component").then(
(m) => m.CoPilotPortComponent,
),
},
{
path: "default",
title: "Default Chat",
loadComponent: () =>
import("./routes/default/default-chat.component").then(
(m) => m.DefaultChatComponent,
),
},
{
// Unknown paths fall back to the default landing (A2UI demo).
path: "**",
redirectTo: "a2ui-demo",
},
];
@@ -0,0 +1,37 @@
import { afterNextRender, Component, DestroyRef, inject } from "@angular/core";
import { CopilotKit } from "@copilotkit/angular";
import { WEB_INSPECTOR_TAG } from "@copilotkit/web-inspector";
import type { WebInspectorElement } from "@copilotkit/web-inspector";
@Component({
selector: "angular-demo-web-inspector",
standalone: true,
template: "",
})
export class DemoWebInspectorComponent {
readonly #copilotKit = inject(CopilotKit);
readonly #destroyRef = inject(DestroyRef);
constructor() {
afterNextRender(() => {
const existing =
document.querySelector<WebInspectorElement>(WEB_INSPECTOR_TAG);
const inspector =
existing ??
(document.createElement(WEB_INSPECTOR_TAG) as WebInspectorElement);
inspector.core = this.#copilotKit.core;
inspector.setAttribute("auto-attach-core", "false");
if (!existing) {
document.body.appendChild(inspector);
}
this.#destroyRef.onDestroy(() => {
if (inspector.isConnected) {
inspector.remove();
}
});
});
}
}
@@ -0,0 +1,280 @@
import {
ChangeDetectionStrategy,
Component,
computed,
input,
linkedSignal,
} from "@angular/core";
import { CommonModule } from "@angular/common";
import {
Check,
ChevronDown,
LoaderCircle,
LucideAngularModule,
Wrench,
} from "lucide-angular";
import type { AngularToolCall, ToolRenderer } from "@copilotkit/angular";
type WildcardToolArgs = Record<string, unknown>;
type ToolEntry = {
key: string;
value: string;
};
@Component({
selector: "wildcard-tool-render",
standalone: true,
imports: [CommonModule, LucideAngularModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="copilot-tool-reasoning" data-testid="wildcard-tool-render">
<button
type="button"
class="copilot-tool-summary"
[class.copilot-tool-summary--static]="!hasDetails()"
[attr.aria-expanded]="hasDetails() ? open() : null"
(click)="toggle()"
>
@if (isRunning()) {
<lucide-angular
[img]="LoaderCircleIcon"
[size]="14"
class="copilot-tool-icon copilot-tool-icon--spin"
/>
} @else {
<lucide-angular
[img]="CheckIcon"
[size]="14"
class="copilot-tool-icon copilot-tool-icon--complete"
/>
}
<lucide-angular [img]="WrenchIcon" [size]="14" class="copilot-tool-icon" />
<span class="copilot-tool-name">{{ toolName() }}</span>
<span class="copilot-tool-status">{{ statusLabel() }}</span>
@if (hasDetails()) {
<lucide-angular
[img]="ChevronDownIcon"
[size]="14"
class="copilot-tool-chevron"
[class.copilot-tool-chevron--open]="open()"
/>
}
</button>
@if (hasDetails()) {
<div
class="copilot-tool-details-wrap"
[style.grid-template-rows]="open() ? '1fr' : '0fr'"
>
<div class="copilot-tool-details-clip">
<div class="copilot-tool-details">
@for (entry of entries(); track entry.key) {
<div class="copilot-tool-entry">
<span class="copilot-tool-entry-key">{{ entry.key }}:</span>
<span class="copilot-tool-entry-value">{{ entry.value }}</span>
</div>
}
@if (resultSummary(); as result) {
<div class="copilot-tool-entry">
<span class="copilot-tool-entry-key">result:</span>
<span class="copilot-tool-entry-value">{{ result }}</span>
</div>
}
</div>
</div>
</div>
}
</div>
`,
styles: [
`
.copilot-tool-reasoning {
margin: 6px 0;
color: var(--muted-foreground, #737373);
}
.copilot-tool-summary {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
border: 0;
background: transparent;
color: inherit;
cursor: pointer;
padding: 4px 0;
font: inherit;
font-size: 14px;
text-align: left;
transition: color 0.15s ease;
}
.copilot-tool-summary:hover {
color: var(--foreground, #171717);
}
.copilot-tool-summary--static {
cursor: default;
}
.copilot-tool-summary--static:hover {
color: inherit;
}
.copilot-tool-icon {
width: 14px;
height: 14px;
flex: 0 0 14px;
}
.copilot-tool-icon--spin {
animation: copilot-tool-spin 1s linear infinite;
}
.copilot-tool-icon--complete {
color: #10b981;
}
.copilot-tool-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--foreground, #171717);
font-family:
var(--font-code), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
font-weight: 500;
}
.copilot-tool-status {
flex: 0 0 auto;
font-size: 12px;
color: var(--muted-foreground, #737373);
}
.copilot-tool-chevron {
margin-left: auto;
width: 14px;
height: 14px;
flex: 0 0 14px;
transition: transform 0.2s ease;
}
.copilot-tool-chevron--open {
transform: rotate(180deg);
}
.copilot-tool-details-wrap {
display: grid;
transition: grid-template-rows 0.2s ease;
}
.copilot-tool-details-clip {
overflow: hidden;
}
.copilot-tool-details {
margin: 6px 0 0 22px;
padding: 8px 12px;
display: grid;
gap: 4px;
border-radius: 6px;
background: var(--secondary, #f5f5f5);
}
.copilot-tool-entry {
min-width: 0;
display: flex;
gap: 8px;
font-family:
var(--font-code), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
font-size: 12px;
line-height: 1.4;
}
.copilot-tool-entry-key {
flex: 0 0 auto;
color: var(--muted-foreground, #737373);
}
.copilot-tool-entry-value {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--foreground, #171717);
}
@keyframes copilot-tool-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
`,
],
})
export class WildcardToolRenderComponent implements ToolRenderer<WildcardToolArgs> {
readonly toolCall = input.required<AngularToolCall<WildcardToolArgs>>();
protected readonly LoaderCircleIcon = LoaderCircle;
protected readonly CheckIcon = Check;
protected readonly ChevronDownIcon = ChevronDown;
protected readonly WrenchIcon = Wrench;
protected readonly isRunning = computed(
() => this.toolCall().status !== "complete",
);
protected readonly open = linkedSignal(() => this.isRunning());
protected readonly toolName = computed(() => this.toolCall().name ?? "tool");
protected readonly entries = computed<ToolEntry[]>(() =>
Object.entries(this.toolCall().args ?? {}).map(([key, value]) => ({
key,
value: this.formatValue(value),
})),
);
protected readonly resultSummary = computed(() => {
const toolCall = this.toolCall();
if (toolCall.status !== "complete") return undefined;
if (!toolCall.result) return undefined;
return this.formatValue(toolCall.result);
});
protected readonly hasDetails = computed(
() => this.entries().length > 0 || this.resultSummary() !== undefined,
);
protected readonly statusLabel = computed(() =>
this.isRunning() ? "Running" : "Complete",
);
private 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}"`;
if (value === undefined) return "undefined";
return String(value);
}
protected toggle(): void {
if (!this.hasDetails()) return;
this.open.update((value) => !value);
}
}
@@ -0,0 +1,39 @@
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
import { CopilotChatInput, injectChatState } from "@copilotkit/angular";
import type { ToolsMenuItem } from "@copilotkit/angular";
@Component({
selector: "a2ui-demo-input",
standalone: true,
imports: [CopilotChatInput],
template: `
<copilot-chat-input [inputClass]="inputClass() ?? ''" [toolsMenu]="toolsMenu" />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class A2UIDemoInputComponent {
readonly inputClass = input<string | undefined>();
private readonly chatState = injectChatState();
readonly toolsMenu: (ToolsMenuItem | "-")[] = [
{
label: "Say hi to CopilotKit",
action: () => {
this.chatState.changeInput(
"Hello Copilot! 👋 Could you help me with something?",
);
},
},
"-",
{
label: "Open CopilotKit Docs",
action: () => {
window.open(
"https://docs.copilotkit.ai",
"_blank",
"noopener,noreferrer",
);
},
},
];
}
@@ -0,0 +1,36 @@
import type { SandboxFunction } from "@copilotkit/angular";
import { z } from "zod";
export type Theme = "light" | "dark";
type ThemeHandler = (mode: Theme) => void;
let currentThemeHandler: ThemeHandler | undefined;
const setThemeParameters = z.object({
mode: z.enum(["light", "dark"]).describe("The theme mode to set"),
});
export function bindA2UIDemoThemeHandler(handler: ThemeHandler): () => void {
currentThemeHandler = handler;
return () => {
if (currentThemeHandler === handler) {
currentThemeHandler = undefined;
}
};
}
export const a2uiDemoSandboxFunctions: SandboxFunction[] = [
{
name: "setTheme",
description:
"Switch the host application theme between light and dark mode. " +
"Call this when the user asks to change the theme or when generating UI with a theme toggle.",
parameters: setThemeParameters,
handler: async (args) => {
const { mode } = setThemeParameters.parse(args);
currentThemeHandler?.(mode);
return `Theme set to ${mode}`;
},
},
];
@@ -0,0 +1,275 @@
import {
ChangeDetectionStrategy,
Component,
DestroyRef,
computed,
inject,
signal,
} from "@angular/core";
import {
CopilotChat,
connectAgentContext,
provideCopilotChatLabels,
registerFrontendTool,
} from "@copilotkit/angular";
import type { AttachmentsConfig } from "@copilotkit/angular";
import { A2UIDemoInputComponent } from "./a2ui-demo-input.component";
import { z } from "zod";
import { bindA2UIDemoThemeHandler } from "./a2ui-demo-sandbox-functions";
import type { Theme } from "./a2ui-demo-sandbox-functions";
type ThreadId = "thread---a" | "thread---b" | "thread---c";
const themeColors = {
light: {
bg: "oklch(1 0 0)",
text: "oklch(0.145 0 0)",
border: "oklch(0.922 0 0)",
muted: "oklch(0.97 0 0)",
},
dark: {
bg: "oklch(0.145 0 0)",
text: "oklch(0.985 0 0)",
border: "oklch(0.269 0 0)",
muted: "oklch(0.269 0 0)",
},
} satisfies Record<Theme, Record<"bg" | "text" | "border" | "muted", string>>;
const threadOptions: Array<{ id: ThreadId | undefined; label: string }> = [
{ id: undefined, label: "Stateless" },
{ id: "thread---a", label: "Thread A" },
{ id: "thread---b", label: "Thread B" },
{ id: "thread---c", label: "Thread C" },
];
@Component({
selector: "a2ui-demo",
standalone: true,
imports: [CopilotChat],
template: `
<div
class="a2ui-demo-root"
[class.dark]="theme() === 'dark'"
[style.background-color]="colors().bg"
[style.color]="colors().text"
>
<div class="a2ui-demo-shell" data-testid="a2ui-demo-shell">
<div class="a2ui-demo-toolbar" data-testid="a2ui-demo-toolbar">
<button
type="button"
class="a2ui-demo-theme-toggle"
(click)="toggleTheme()"
[attr.aria-label]="
'Switch to ' + (theme() === 'light' ? 'dark' : 'light') + ' mode'
"
[style.border]="'1px solid ' + colors().border"
[style.background-color]="colors().muted"
[style.color]="colors().text"
>
@if (theme() === "light") {
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
} @else {
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</svg>
}
</button>
<div class="a2ui-demo-thread-tabs">
@for (option of threadOptions; track option.label) {
<button
type="button"
class="a2ui-demo-thread-tab"
(click)="selectThread(option.id)"
[attr.aria-pressed]="option.id === selectedThreadId()"
[style.border]="threadBorder(option.id)"
[style.background-color]="threadBackground(option.id)"
[style.color]="threadColor(option.id)"
>
{{ option.label }}
</button>
}
</div>
</div>
<div class="a2ui-demo-chat">
@if (selectedThreadId(); as threadId) {
<copilot-chat
[class]="theme() === 'dark' ? 'dark' : undefined"
[threadId]="threadId"
[inputComponent]="inputComponent"
[attachments]="attachments"
/>
} @else {
<copilot-chat
[class]="theme() === 'dark' ? 'dark' : undefined"
[inputComponent]="inputComponent"
[attachments]="attachments"
/>
}
</div>
</div>
</div>
`,
styles: [
`
.a2ui-demo-root {
height: 100vh;
margin: 0;
padding: 0;
overflow: hidden;
font-family: Arial, Helvetica, sans-serif;
line-height: 1.5;
transition:
background-color 0.3s,
color 0.3s;
}
.a2ui-demo-shell {
display: flex;
flex-direction: column;
height: 100%;
padding: 16px;
gap: 16px;
}
.a2ui-demo-toolbar {
display: flex;
gap: 10px;
align-items: center;
}
.a2ui-demo-theme-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
cursor: pointer;
transition: all 0.15s ease-in-out;
}
.a2ui-demo-thread-tabs {
flex: 1;
display: flex;
gap: 10px;
justify-content: center;
}
.a2ui-demo-thread-tab {
padding: 6px 14px;
border-radius: 20px;
font-weight: 600;
font-size: 0.85rem;
line-height: 1.5;
cursor: pointer;
transition: all 0.15s ease-in-out;
}
.a2ui-demo-chat {
flex: 1;
min-height: 0;
}
`,
],
providers: [
provideCopilotChatLabels({
chatInputPlaceholder: "Type a message...",
chatDisclaimerText:
"AI can make mistakes. Please verify important information.",
}),
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class A2UIDemoComponent {
private readonly destroyRef = inject(DestroyRef);
readonly inputComponent = A2UIDemoInputComponent;
readonly threadOptions = threadOptions;
readonly attachments: AttachmentsConfig = {
enabled: true,
accept: "image/*,audio/*,video/*,.pdf,.txt,.md,application/pdf,text/*",
};
readonly theme = signal<Theme>("light");
readonly selectedThreadId = signal<ThreadId | undefined>(undefined);
readonly colors = computed(() => themeColors[this.theme()]);
readonly agentContext = computed(() => ({
description: "The current Thread ID is:",
value: this.selectedThreadId() ?? "stateless",
}));
constructor() {
connectAgentContext(this.agentContext);
this.destroyRef.onDestroy(
bindA2UIDemoThemeHandler((mode) => this.theme.set(mode)),
);
registerFrontendTool<{ name: string }>({
name: "sayHello",
description: "Use this tool to greet the user by name.",
parameters: z.object({
name: z.string(),
}),
handler: async ({ name }) => {
window.alert(`Hello ${name}`);
return `Hello ${name}`;
},
});
}
toggleTheme(): void {
this.theme.update((theme) => (theme === "light" ? "dark" : "light"));
}
selectThread(threadId: ThreadId | undefined): void {
this.selectedThreadId.set(threadId);
}
threadBorder(threadId: ThreadId | undefined): string {
return threadId === this.selectedThreadId()
? `2px solid ${this.colors().text}`
: `1px solid ${this.colors().border}`;
}
threadBackground(threadId: ThreadId | undefined): string {
return threadId === this.selectedThreadId()
? this.colors().text
: this.colors().bg;
}
threadColor(threadId: ThreadId | undefined): string {
return threadId === this.selectedThreadId()
? this.colors().bg
: this.colors().text;
}
}
@@ -0,0 +1,168 @@
import {
ChangeDetectionStrategy,
Component,
inject,
input,
} from "@angular/core";
import { FormsModule } from "@angular/forms";
import { injectChatState } from "@copilotkit/angular";
@Component({
selector: "nextgen-custom-input",
standalone: true,
imports: [FormsModule],
template: `
<form
class="ck-input-wrapper"
(ngSubmit)="submit()"
[class.ck-disabled]="inProgress()"
novalidate
autocomplete="off"
>
<button
type="button"
class="ck-icon"
[disabled]="inProgress()"
title="Add attachment"
>
</button>
<label for="ck-message" class="sr-only">Message</label>
<input
id="ck-message"
name="message"
class="ck-input"
type="text"
[(ngModel)]="value"
(ngModelChange)="chatState.changeInput($event)"
[disabled]="inProgress()"
placeholder="Ask anything…"
autocapitalize="sentences"
autocomplete="off"
spellcheck="true"
maxlength="4000"
(keydown)="onKeyDown($event)"
(compositionstart)="composing = true"
(compositionend)="composing = false"
/>
<button
type="submit"
class="ck-send"
[disabled]="inProgress() || !canSend"
[attr.aria-label]="'Send message'"
>
</button>
</form>
`,
styles: [
`
.ck-input-wrapper {
display: flex;
align-items: center;
gap: 8px;
background: #fff;
border-radius: 16px;
padding: 10px 12px;
margin: 8px;
border: 1px solid #eee;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
}
.ck-input-wrapper.ck-disabled {
opacity: 0.7;
pointer-events: none;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.ck-icon {
width: 36px;
height: 36px;
border-radius: 50%;
border: 1px solid #ddd;
background: #fff;
display: grid;
place-items: center;
cursor: pointer;
}
.ck-input {
flex: 1;
border: none;
outline: none;
background: transparent;
font-size: 14px;
color: #333;
padding: 8px 4px;
}
.ck-input::placeholder {
color: #a0a0a0;
}
.ck-send {
width: 36px;
height: 36px;
border-radius: 50%;
border: none;
display: grid;
place-items: center;
background: #000;
color: #fff;
cursor: pointer;
font-size: 16px;
}
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomChatInputComponent {
readonly inProgress = input(false);
readonly inputClass = input<string>();
value = "";
composing = false;
readonly chatState = injectChatState();
get valueTrimmed(): string {
return this.value.trim();
}
get canSend(): boolean {
return this.valueTrimmed.length > 0;
}
async submit(): Promise<void> {
if (this.inProgress()) return;
if (!this.canSend) return;
this.chatState.submitInput(this.valueTrimmed);
this.value = "";
}
onKeyDown(e: KeyboardEvent): void {
if (this.composing) return;
if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
this.submit();
}
}
}
@@ -0,0 +1,30 @@
import { ChangeDetectionStrategy, Component } from "@angular/core";
import {
CopilotChatView,
CopilotChat,
provideCopilotChatLabels,
} from "@copilotkit/angular";
import { CustomChatInputComponent } from "./custom-chat-input.component";
@Component({
selector: "nextgen-custom-input-chat",
standalone: true,
imports: [CopilotChat],
template: `
<div style="display: block; height: 100vh">
<copilot-chat [inputComponent]="customInput" />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
// Optional: tweak labels/placeholders shown by CopilotKit
provideCopilotChatLabels({
chatInputPlaceholder: "Ask anything...",
chatDisclaimerText: "AI can make mistakes. Verify important info.",
}),
],
})
export class CustomInputChatComponent {
customInput = CustomChatInputComponent;
}
@@ -0,0 +1,13 @@
import { Component } from "@angular/core";
import { CopilotChat } from "@copilotkit/angular";
@Component({
selector: "default-chat",
standalone: true,
imports: [CopilotChat],
template: `
<copilot-chat [threadId]="'xyz'" />
`,
})
export class DefaultChatComponent {}
@@ -0,0 +1,235 @@
import type { OnDestroy, OnInit } from "@angular/core";
import {
Component,
ChangeDetectionStrategy,
computed,
inject,
input,
signal,
} from "@angular/core";
import { TitleCasePipe } from "@angular/common";
import { FormsModule } from "@angular/forms";
import type {
HumanInTheLoopToolCall,
HumanInTheLoopToolRenderer,
} from "@copilotkit/angular";
import {
connectAgentContext,
CopilotKit,
injectAgentStore,
registerHumanInTheLoop,
} from "@copilotkit/angular";
import { RenderToolCalls } from "@copilotkit/angular";
import { WEB_INSPECTOR_TAG } from "@copilotkit/web-inspector";
import type { WebInspectorElement } from "@copilotkit/web-inspector";
import { z } from "zod";
@Component({
selector: "require-approval",
standalone: true,
imports: [FormsModule],
template: `
<div>Require approval</div>
<button (click)="respond({ approved: true })">Approve</button>
<button (click)="respond({ approved: false })">Deny</button>
`,
})
export class RequireApprovalComponent implements HumanInTheLoopToolRenderer {
toolCall =
input.required<
HumanInTheLoopToolCall<{ action: string; reason: string }>
>();
respond(result: { approved: boolean }) {
this.toolCall().respond(result);
}
}
@Component({
selector: "headless-chat",
standalone: true,
imports: [FormsModule, RenderToolCalls, TitleCasePipe],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div
class="headless-container"
style="display: flex; flex-direction: column; height: 100vh; width: 100vw"
>
<div
class="messages"
style="
flex: 1;
overflow: auto;
padding: 16px;
background: #f9fafb;
color: #111827;
"
>
@for (m of messages(); track m) {
<div style="margin-bottom: 16px">
<div style="font-weight: 600; color: #374151">
{{ m.role | titlecase }}
</div>
<div style="white-space: pre-wrap">{{ m.content }}</div>
@if (m.role === "assistant") {
<copilot-render-tool-calls
[message]="m"
[messages]="messages()"
[isLoading]="isRunning()"
/>
}
</div>
}
@if (isRunning()) {
<div style="opacity: 0.9; color: #6b7280">Thinking…</div>
}
</div>
<form
(ngSubmit)="send()"
style="
display: flex;
gap: 8px;
padding: 12px;
background: #ffffff;
border-top: 1px solid #e5e7eb;
"
>
<input
name="message"
[(ngModel)]="inputValue"
[disabled]="isRunning()"
placeholder="Type a message…"
style="
flex: 1;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid #d1d5db;
background: #ffffff;
color: #111827;
outline: none;
"
/>
<button
type="submit"
[disabled]="!inputValue.trim() || isRunning()"
style="
padding: 10px 14px;
border-radius: 8px;
border: 1px solid #1d4ed8;
background: #2563eb;
color: #ffffff;
cursor: pointer;
"
>
Send
</button>
<button
type="button"
(click)="clearThreads()"
style="
padding: 10px 14px;
border-radius: 8px;
border: 1px solid #d1d5db;
background: #ffffff;
color: #374151;
cursor: pointer;
"
>
Clear threads
</button>
</form>
</div>
`,
})
export class HeadlessChatComponent implements OnInit, OnDestroy {
readonly agentStore = injectAgentStore("openai");
readonly agent = computed(() => this.agentStore()?.agent);
readonly isRunning = computed(() => !!this.agentStore()?.isRunning());
readonly messages = computed(() => this.agentStore()?.messages());
readonly copilotkit = inject(CopilotKit);
inputValue = "";
private inspectorElement: WebInspectorElement | null = null;
constructor() {
registerHumanInTheLoop({
name: "requireApproval",
description: "Requires human approval before proceeding",
parameters: z.object({
action: z.string(),
reason: z.string(),
}),
component: RequireApprovalComponent,
});
connectAgentContext(
signal({
value: "voice-mode",
description: "active",
}),
);
}
ngOnInit(): void {
if (typeof document === "undefined") return;
const existing =
document.querySelector<WebInspectorElement>(WEB_INSPECTOR_TAG);
const inspector =
existing ??
(document.createElement(WEB_INSPECTOR_TAG) as WebInspectorElement);
inspector.core = this.copilotkit.core;
inspector.setAttribute("auto-attach-core", "false");
if (!existing) {
document.body.appendChild(inspector);
}
this.inspectorElement = inspector;
}
ngOnDestroy(): void {
if (this.inspectorElement && this.inspectorElement.isConnected) {
this.inspectorElement.remove();
}
this.inspectorElement = null;
}
async clearThreads() {
const runtimeUrl = this.copilotkit.core?.runtimeUrl;
if (!runtimeUrl) return;
const url = runtimeUrl.replace(/\/$/, "");
try {
const res = await fetch(`${url}/threads/clear`, {
method: "POST",
credentials: "include",
});
if (!res.ok) {
console.error(
`Failed to clear threads: HTTP ${res.status} ${res.statusText}`,
);
}
} catch (err) {
console.error("Failed to clear threads", err);
}
}
async send() {
const content = this.inputValue.trim();
const agent = this.agent();
const isRunning = this.isRunning();
if (!agent || !content || isRunning) return;
agent.addMessage({ id: crypto.randomUUID(), role: "user", content });
this.inputValue = "";
try {
await this.copilotkit.core.runAgent({ agent });
} catch (e) {
console.error("Agent run error", e);
}
}
}
@@ -0,0 +1,24 @@
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { CopilotChatView, provideCopilotChatLabels } from "@copilotkit/angular";
import { CustomChatInputComponent } from "../custom-input/custom-chat-input.component";
@Component({
selector: "ukg-co-pilot-port",
standalone: true,
imports: [CopilotChatView],
template: `
<div style="display: block; height: 100vh">
<copilot-chat-view [inputComponent]="customInput" />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
provideCopilotChatLabels({
chatInputPlaceholder: "Ask anything... (UKG port)",
chatDisclaimerText: "AI may be inaccurate (UKG PORT).",
}),
],
})
export class CoPilotPortComponent {
customInput = CustomChatInputComponent;
}
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
placeholder
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>CopilotKit Angular Demo</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
import { provideZonelessChangeDetection } from "@angular/core";
import { bootstrapApplication } from "@angular/platform-browser";
import { AppComponent } from "./app/app.component";
import { appConfig } from "./app/app.config";
bootstrapApplication(AppComponent, {
...appConfig,
providers: [provideZonelessChangeDetection(), ...appConfig.providers],
}).catch((err) => console.error(err));
+9
View File
@@ -0,0 +1,9 @@
html,
body {
margin: 0;
height: 100%;
}
body {
font-family: Arial, Helvetica, sans-serif;
}
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}
+37
View File
@@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": false,
"esModuleInterop": true,
"module": "ES2020",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noPropertyAccessFromIndexSignature": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"baseUrl": ".",
"paths": {
"@copilotkit/angular": ["../../../../packages/angular/src/index.ts"],
"@copilotkit/angular/*": ["../../../../packages/angular/src/*"],
"@copilotkit/core": ["node_modules/@copilotkit/core"],
"@copilotkit/shared": ["node_modules/@copilotkit/shared"],
"@copilotkit/web-inspector": ["node_modules/@copilotkit/web-inspector"],
"@copilotkit/a2ui-renderer": ["node_modules/@copilotkit/a2ui-renderer"],
"@copilotkit/a2ui-renderer/web-components": [
"node_modules/@copilotkit/a2ui-renderer/web-components"
],
"@copilotkit/a2ui-renderer/web-components/define": [
"node_modules/@copilotkit/a2ui-renderer/web-components/define"
]
}
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
},
"exclude": ["dist", "node_modules"]
}
@@ -0,0 +1,22 @@
import type { StorybookConfig } from "@storybook/angular";
const config: StorybookConfig = {
framework: {
name: "@storybook/angular",
options: {},
},
stories: ["../stories/**/*.stories.@(ts|tsx|mdx)"],
addons: ["@storybook/addon-themes"],
webpackFinal: async (cfg) => {
// Suppress size warnings for development
cfg.performance = {
...cfg.performance,
maxAssetSize: 5000000, // 5MB
maxEntrypointSize: 5000000, // 5MB
};
return cfg;
},
};
export default config;
@@ -0,0 +1,97 @@
/* Storybook dark mode custom background */
html.dark {
background-color: #212121;
}
html.dark body {
background-color: #212121;
}
/* Ensure the story canvas also uses the dark background */
html.dark .docs-story,
html.dark .sb-show-main,
html.dark .sb-main-padded {
background-color: #212121;
}
/* Custom disclaimer demo styles (global to ensure they apply in stories) */
.custom-disclaimer {
background: linear-gradient(
90deg,
#ff6b6b 0%,
#4ecdc4 50%,
#45b7d1 100%
) !important;
color: white;
font-weight: 600;
font-size: 14px;
padding: 16px 24px;
margin: 12px auto;
border-radius: 12px;
text-align: center;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
animation: disclaimer-pulse 3s ease-in-out infinite;
position: relative;
overflow: hidden;
}
.custom-disclaimer::before {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(
45deg,
transparent,
rgba(255, 255, 255, 0.1),
transparent
);
transform: rotate(45deg);
animation: disclaimer-shimmer 3s infinite;
}
@keyframes disclaimer-pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.02);
}
}
@keyframes disclaimer-shimmer {
0% {
transform: translateX(-100%) translateY(-100%) rotate(45deg);
}
100% {
transform: translateX(100%) translateY(100%) rotate(45deg);
}
}
.custom-disclaimer-text {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
gap: 8px;
}
.custom-disclaimer-icon {
position: relative;
z-index: 1;
font-size: 20px;
animation: disclaimer-bounce 2s infinite;
}
@keyframes disclaimer-bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
@@ -0,0 +1,55 @@
import type { Preview } from "@storybook/angular";
import { withThemeByClassName } from "@storybook/addon-themes";
const preview: Preview = {
parameters: {
// Disable the backgrounds addon to avoid conflicts with dark mode
backgrounds: { disable: true },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
docs: {
toc: true,
// Canvas (bottom) code panel behavior
canvas: { sourceState: "shown" }, // Show source code by default
// Enable the separate Code panel in Docs tab
codePanel: true,
// Configure source display
source: {
type: "dynamic", // Update snippet as args/Controls change
// Ensure the code pane reflects the actual story template
transform: (src: string, ctx: any) => {
try {
// Prefer the currently rendered story function
const storyResult = (ctx?.storyFn || ctx?.originalStoryFn)?.(
ctx?.args || {},
);
if (
storyResult &&
typeof storyResult === "object" &&
"template" in storyResult
) {
return (storyResult as any).template as string;
}
} catch {}
// Fallback to Storybooks generated source
return src;
},
},
},
},
decorators: [
withThemeByClassName({
themes: {
light: "", // default = no extra class
dark: "dark", // adds class="dark" to <html> in the preview iframe
},
defaultTheme: "light",
}),
],
};
export default preview;
@@ -0,0 +1,18 @@
/* Import the pre-built Tailwind CSS */
@import "../../../../../packages/angular/dist/styles.css";
/* Storybook dark mode custom background */
html.dark {
background-color: #212121;
}
html.dark body {
background-color: #212121;
}
/* Ensure the story canvas also uses the dark background */
html.dark .docs-story,
html.dark .sb-show-main,
html.dark .sb-main-padded {
background-color: #212121;
}
+101
View File
@@ -0,0 +1,101 @@
# @copilotkit-storybook/angular
## 0.0.6-next.1
### Patch Changes
- Updated dependencies [1ceb963]
- @copilotkit/core@1.55.0-next.7
## 0.0.6-next.0
### Patch Changes
- @copilotkitnext/core@1.54.1-next.0
## 0.0.5
### Patch Changes
- Updated dependencies [eb1e0bf]
- Updated dependencies [3780c6a]
- Updated dependencies [c80498e]
- Updated dependencies [f1571ef]
- @copilotkitnext/core@1.54.0
## 0.0.5-next.1
### Patch Changes
- @copilotkitnext/core@1.54.0-next.3
## 0.0.5-next.0
### Patch Changes
- @copilotkit/core@1.53.1-next.0
## 0.0.4
### Patch Changes
- Updated dependencies [1510f64]
- @copilotkit/core@1.53.0
## 0.0.4-next.1
### Patch Changes
- @copilotkit/core@1.53.0-next.5
## 0.0.4-next.0
### Patch Changes
- @copilotkit/core@1.52.2-next.0
## 0.0.3
### Patch Changes
- @copilotkit/core@1.52.1
## 0.0.3-next.0
### Patch Changes
- @copilotkit/core@1.52.1-next.0
## 0.0.2
### Patch Changes
- Updated dependencies [d77f347]
- Updated dependencies [ef0f539]
- @copilotkit/core@1.52.0
## 0.0.2-next.1
### Patch Changes
- @copilotkit/core@1.52.0-next.5
## 0.0.2-next.0
### Patch Changes
- Updated dependencies [ef0f539]
- @copilotkit/core@1.51.5-next.0
## 0.0.1
### Patch Changes
- Updated dependencies [c998f30]
- @copilotkit/core@1.51.4
## 0.0.1-next.0
### Patch Changes
- @copilotkit/core@1.51.4-next.0
@@ -0,0 +1,76 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"storybook-angular": {
"projectType": "application",
"root": "",
"sourceRoot": "stories",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser-esbuild",
"options": {
"outputPath": "dist",
"index": "stories/index.html",
"main": "stories/main.ts",
"polyfills": [],
"tsConfig": "tsconfig.json",
"assets": [],
"styles": [
"../../../../packages/angular/dist/styles.css",
".storybook/preview.css"
],
"scripts": []
},
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false
},
"development": {
"optimization": false,
"sourceMap": true
}
},
"defaultConfiguration": "development"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "storybook-angular:build:production"
},
"development": {
"buildTarget": "storybook-angular:build:development"
}
},
"defaultConfiguration": "development"
},
"storybook": {
"builder": "@storybook/angular:start-storybook",
"options": {
"browserTarget": "storybook-angular:build:development",
"configDir": ".storybook",
"port": 6007,
"compodoc": false
}
},
"build-storybook": {
"builder": "@storybook/angular:build-storybook",
"options": {
"browserTarget": "storybook-angular:build:production",
"configDir": ".storybook",
"outputDir": "storybook-static",
"compodoc": false
}
}
}
}
},
"cli": {
"analytics": false
}
}
@@ -0,0 +1,44 @@
import { Component, Input } from "@angular/core";
@Component({
selector: "custom-disclaimer",
standalone: true,
template: `
<div
[class]="inputClass"
style="
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
margin: 10px;
font-family: &quot;Segoe UI&quot;, Tahoma, Geneva, Verdana, sans-serif;
"
>
<h3 style="margin: 0 0 10px 0; font-size: 20px">
✨ Custom Disclaimer Component ✨
</h3>
<p style="margin: 0; font-size: 14px; opacity: 0.9">
{{
text || "This is a custom disclaimer demonstrating component overrides!"
}}
</p>
<div
style="
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid rgba(255, 255, 255, 0.3);
font-size: 12px;
opacity: 0.7;
"
>
🎨 Styled with custom gradients and animations
</div>
</div>
`,
})
export class CustomDisclaimerComponent {
@Input() text?: string;
@Input() inputClass?: string;
}
@@ -0,0 +1,68 @@
import { Component, Input } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormsModule } from "@angular/forms";
import type { ChatState } from "@copilotkit/angular";
@Component({
selector: "custom-input",
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div
[class]="inputClass"
style="
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
padding: 20px;
border-radius: 15px;
margin: 10px;
"
>
<input
type="text"
[(ngModel)]="inputValue"
placeholder="💬 Ask me anything..."
style="
width: 100%;
padding: 15px;
border: 2px solid white;
border-radius: 10px;
font-size: 16px;
background: rgba(255, 255, 255, 0.9);
color: #333;
outline: none;
"
(keyup.enter)="handleSend()"
/>
<button
style="
margin-top: 10px;
padding: 10px 20px;
background: white;
color: #f5576c;
border: none;
border-radius: 5px;
font-weight: bold;
cursor: pointer;
"
(click)="handleSend()"
>
Send Message ✨
</button>
</div>
`,
})
export class CustomInputComponent {
@Input() inputClass?: string;
inputValue = "";
constructor(private chat: ChatState) {}
handleSend() {
const value = this.inputValue.trim();
if (value) {
this.chat.submitInput(value);
this.inputValue = "";
}
}
}
@@ -0,0 +1,62 @@
import { Component, Input, Output, EventEmitter } from "@angular/core";
import { CommonModule } from "@angular/common";
@Component({
selector: "custom-scroll-button",
standalone: true,
imports: [CommonModule],
template: `
<button
type="button"
(click)="handleClick()"
[class]="inputClass"
[class.hover]="isHovered"
(mouseenter)="isHovered = true"
(mouseleave)="isHovered = false"
style="
position: fixed;
bottom: 100px;
right: 20px;
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: 3px solid white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
cursor: pointer;
pointer-events: auto;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s;
z-index: 1000;
"
[style.transform]="isHovered ? 'scale(1.1)' : 'scale(1)'"
>
<span style="color: white; font-size: 24px; pointer-events: none">⬇️</span>
</button>
`,
styles: [
`
button.hover {
transform: scale(1.1);
}
`,
],
})
export class CustomScrollButtonComponent {
@Input() onClick?: () => void;
@Input() inputClass?: string;
@Output() clicked = new EventEmitter<void>();
isHovered = false;
handleClick() {
// Emit the clicked event for the slot system to handle
this.clicked.emit();
// Also call onClick if provided for backward compatibility
if (this.onClick) {
this.onClick();
}
}
}
@@ -0,0 +1,27 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { CommonModule } from "@angular/common";
@Component({
selector: "custom-send-button",
standalone: true,
imports: [CommonModule],
template: `
<button
[disabled]="disabled"
(click)="handleClick()"
class="cpk:rounded-full cpk:w-10 cpk:h-10 cpk:bg-blue-500 cpk:text-white cpk:hover:bg-blue-600 cpk:transition-colors cpk:mr-2 cpk:disabled:opacity-50 cpk:disabled:cursor-not-allowed"
>
✈️
</button>
`,
})
export class CustomSendButtonComponent {
@Input() disabled = false;
@Output() clicked = new EventEmitter<void>();
handleClick(): void {
if (!this.disabled) {
this.clicked.emit();
}
}
}
@@ -0,0 +1,46 @@
{
"name": "@copilotkit-storybook/angular",
"version": "0.0.6-next.1",
"private": true,
"scripts": {
"dev": "pnpm --filter @copilotkit/angular run build:css && sleep 1 && ng run storybook-angular:storybook",
"build": "ng run storybook-angular:build-storybook",
"storybook:dev": "pnpm --filter @copilotkit/angular run build:css && sleep 1 && ng run storybook-angular:storybook",
"storybook:build": "ng run storybook-angular:build-storybook"
},
"dependencies": {
"@ag-ui/client": "0.0.51",
"@angular/animations": "^21.2.15",
"@angular/common": "^21.2.15",
"@angular/compiler": "^21.2.15",
"@angular/core": "^21.2.15",
"@angular/forms": "^21.2.15",
"@angular/platform-browser": "^21.2.15",
"@angular/platform-browser-dynamic": "^21.2.15",
"@copilotkit/a2ui-renderer": "workspace:*",
"@copilotkit/core": "workspace:^",
"@copilotkit/shared": "workspace:*",
"rxjs": "^7.8.1",
"tslib": "^2.8.1",
"zod": "^3.25.75"
},
"devDependencies": {
"@angular-devkit/build-angular": "^21.2.13",
"@angular/cli": "^21.2.13",
"@angular/compiler-cli": "^21.2.15",
"@copilotkit/typescript-config": "workspace:*",
"@copilotkit/angular": "workspace:*",
"@storybook/addon-themes": "10.3.6",
"@storybook/angular": "10.3.6",
"@tailwindcss/postcss": "^4.1.12",
"@types/node": "^22",
"autoprefixer": "^10.4.21",
"css-loader": "^7.1.2",
"postcss": "^8.4.31",
"postcss-loader": "^8.1.1",
"storybook": "10.3.6",
"style-loader": "^4.0.0",
"tailwindcss": "^4.1.11",
"typescript": "5.9.3"
}
}
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};
@@ -0,0 +1,733 @@
import type { Meta, StoryObj } from "@storybook/angular";
import { moduleMetadata } from "@storybook/angular";
import { fn } from "storybook/test";
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import {
CopilotChatAssistantMessage,
provideCopilotChatLabels,
} from "@copilotkit/angular";
import type { AssistantMessage } from "@ag-ui/client";
// Simple default message
const simpleMessage: AssistantMessage = {
id: "simple-message",
content: "Hello! How can I help you today?",
role: "assistant",
};
// Comprehensive markdown test content
const markdownTestMessage: AssistantMessage = {
id: "test-message",
content: `# Markdown Test Message
This message tests various markdown features including **bold**, *italic*, and \`inline code\`.
## Code Blocks with Copy Buttons
Here are some code examples to test the copy functionality:
### JavaScript/TypeScript
\`\`\`javascript
function greet(name) {
console.log(\`Hello, \${name}!\`);
return \`Welcome, \${name}\`;
}
// Usage
greet("World");
\`\`\`
### Python
\`\`\`python
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Generate sequence
for i in range(10):
print(fibonacci(i))
\`\`\`
### SQL
\`\`\`sql
SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2023-01-01'
GROUP BY u.id, u.name
ORDER BY order_count DESC;
\`\`\`
### JSON
\`\`\`json
{
"name": "CopilotKit",
"version": "2.0.0",
"dependencies": {
"react": "^18.0.0",
"react-markdown": "^10.1.0"
}
}
\`\`\`
### Shell/Bash
\`\`\`bash
#!/bin/bash
echo "Building project..."
npm install
npm run build
echo "Build complete!"
\`\`\`
## Inline Code Testing
Here's some \`inline code\` that should not have a copy button. You can also have \`npm install\` or \`const variable = "value"\` inline.
## Links and Images
- [External link](https://example.com)
- [Internal link](#section)
- ![Alt text](https://picsum.photos/150/100)
## Blockquotes
> This is a blockquote
>
> It can span multiple lines
>
> > And can be nested
## Tables
| Feature | Supported | Notes |
|---------|-----------|-------|
| Headers | ✅ | All levels |
| Lists | ✅ | Nested support |
| Code | ✅ | Syntax highlighting |
| Links | ✅ | External & internal |
| Copy Button | ✅ | On code blocks only |
## Horizontal Rule
---
## Miscellaneous
- [ ] Unchecked task
- [x] Checked task
- Emoji support: 🚀 ✨ 💡 🎉
### Math (if supported)
Inline math: $E = mc^2$
Block math:
$$
\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}
$$
---
*End of markdown test content*`,
role: "assistant",
};
// Message with code blocks and inline literals
const codeBlocksTestMessage: AssistantMessage = {
id: "msg-code-blocks-test",
content:
"# Code Blocks and Inline Literals Test\n\n" +
"This message demonstrates code syntax highlighting with various languages and inline code usage. " +
"When you want to reference a variable like `userName` or a function like `getData()`, you can use inline code blocks.\n\n" +
"## JavaScript Example\n" +
"Here's how you might handle user authentication in JavaScript, so that you can verify credentials:\n\n" +
"```javascript\n" +
"const authenticateUser = async (email, password) => {\n" +
" try {\n" +
" const response = await fetch('/api/auth', {\n" +
" method: 'POST',\n" +
" headers: { 'Content-Type': 'application/json' },\n" +
" body: JSON.stringify({ email, password })\n" +
" });\n" +
" \n" +
" if (!response.ok) {\n" +
" throw new Error('Authentication failed');\n" +
" }\n" +
" \n" +
" return await response.json();\n" +
" } catch (error) {\n" +
" console.error('Error:', error);\n" +
" return null;\n" +
" }\n" +
"};\n" +
"```\n\n" +
"## Python Data Processing\n" +
"Python is great for data manipulation, so here's an example with pandas:\n\n" +
"```python\n" +
"import pandas as pd\n" +
"import numpy as np\n\n" +
"def process_user_data(csv_file):\n" +
" # Read the data\n" +
" df = pd.read_csv(csv_file)\n" +
" \n" +
" # Clean the data\n" +
" df['age'] = pd.to_numeric(df['age'], errors='coerce')\n" +
" df = df.dropna(subset=['age'])\n" +
" \n" +
" # Calculate statistics\n" +
" stats = {\n" +
" 'mean_age': df['age'].mean(),\n" +
" 'median_age': df['age'].median(),\n" +
" 'total_users': len(df)\n" +
" }\n" +
" \n" +
" return stats\n\n" +
"# Usage\n" +
"result = process_user_data('users.csv')\n" +
"print(f\"Average age: {result['mean_age']:.1f}\")\n" +
"```\n\n" +
"## SQL Database Query\n" +
"When working with databases, you might use SQL queries like `SELECT * FROM users` or more complex ones:\n\n" +
"```sql\n" +
"SELECT \n" +
" u.id,\n" +
" u.username,\n" +
" u.email,\n" +
" COUNT(p.id) as post_count,\n" +
" MAX(p.created_at) as last_post_date\n" +
"FROM users u\n" +
"LEFT JOIN posts p ON u.id = p.user_id\n" +
"WHERE u.active = true\n" +
" AND u.created_at >= '2023-01-01'\n" +
"GROUP BY u.id, u.username, u.email\n" +
"HAVING COUNT(p.id) > 0\n" +
"ORDER BY post_count DESC, last_post_date DESC\n" +
"LIMIT 50;\n" +
"```\n\n" +
"## Shell/Bash Commands\n" +
"For deployment scripts, you might use bash commands. The `chmod` command changes permissions, so you can make files executable:\n\n" +
"```bash\n" +
"#!/bin/bash\n\n" +
"# Deploy script\n" +
'APP_NAME="my-app"\n' +
"VERSION=$(git describe --tags --abbrev=0)\n\n" +
'echo "Deploying $APP_NAME version $VERSION"\n\n' +
"# Build the application\n" +
"npm install\n" +
"npm run build\n\n" +
"# Create deployment package\n" +
'tar -czf "${APP_NAME}-${VERSION}.tar.gz" dist/\n\n' +
"# Upload to server\n" +
'scp "${APP_NAME}-${VERSION}.tar.gz" user@server:/opt/deployments/\n\n' +
"# Extract and restart\n" +
"ssh user@server << EOF\n" +
" cd /opt/deployments\n" +
" tar -xzf ${APP_NAME}-${VERSION}.tar.gz\n" +
" sudo systemctl restart ${APP_NAME}\n" +
' echo "Deployment complete"\n' +
"EOF\n" +
"```\n\n" +
"## TypeScript Interface\n" +
"TypeScript helps with type safety, so you can define interfaces like:\n\n" +
"```typescript\n" +
"interface UserProfile {\n" +
" id: string;\n" +
" username: string;\n" +
" email: string;\n" +
" preferences: {\n" +
" theme: 'light' | 'dark';\n" +
" language: string;\n" +
" notifications: boolean;\n" +
" };\n" +
" lastLoginAt: Date | null;\n" +
"}\n\n" +
"class UserService {\n" +
" private users: Map<string, UserProfile> = new Map();\n\n" +
" async createUser(data: Omit<UserProfile, 'id' | 'lastLoginAt'>): Promise<UserProfile> {\n" +
" const user: UserProfile = {\n" +
" ...data,\n" +
" id: crypto.randomUUID(),\n" +
" lastLoginAt: null,\n" +
" };\n" +
" \n" +
" this.users.set(user.id, user);\n" +
" return user;\n" +
" }\n" +
" \n" +
" updateLastLogin(userId: string): void {\n" +
" const user = this.users.get(userId);\n" +
" if (user) {\n" +
" user.lastLoginAt = new Date();\n" +
" }\n" +
" }\n" +
"}\n" +
"```\n\n" +
"## CSS Styling\n" +
"For styling components, you can use CSS classes like `.container` or `#header`:\n\n" +
"```css\n" +
".user-profile-card {\n" +
" background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n" +
" border-radius: 12px;\n" +
" padding: 24px;\n" +
" box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);\n" +
" transition: transform 0.3s ease;\n" +
"}\n\n" +
".user-profile-card:hover {\n" +
" transform: translateY(-4px);\n" +
"}\n\n" +
".user-avatar {\n" +
" width: 80px;\n" +
" height: 80px;\n" +
" border-radius: 50%;\n" +
" border: 3px solid rgba(255, 255, 255, 0.2);\n" +
" object-fit: cover;\n" +
"}\n\n" +
"@media (max-width: 768px) {\n" +
" .user-profile-card {\n" +
" padding: 16px;\n" +
" margin: 8px;\n" +
" }\n" +
"}\n" +
"```\n\n" +
"All these examples show how inline code like `const`, `function`, and `class` can be mixed with code blocks to create comprehensive documentation.",
role: "assistant",
};
const meta: Meta<CopilotChatAssistantMessage> = {
title: "UI/CopilotChatAssistantMessage",
component: CopilotChatAssistantMessage,
parameters: {
docs: {
source: {
language: "html",
type: "dynamic",
},
},
},
decorators: [
moduleMetadata({
imports: [CommonModule, CopilotChatAssistantMessage],
providers: [provideCopilotChatLabels({})],
}),
],
render: (args) => ({
props: {
...args,
},
template: `
<div style="display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 16px;">
<div style="width: 100%; max-width: 640px;">
<copilot-chat-assistant-message
[message]="message"
[toolbarVisible]="toolbarVisible"
(thumbsUp)="thumbsUp($event)"
(thumbsDown)="thumbsDown($event)"
(readAloud)="readAloud($event)"
(regenerate)="regenerate($event)">
</copilot-chat-assistant-message>
</div>
</div>
`,
}),
args: {
message: simpleMessage,
toolbarVisible: true,
thumbsUp: fn(),
thumbsDown: fn(),
readAloud: fn(),
regenerate: fn(),
},
argTypes: {
message: {
description: "The assistant message to display",
control: { type: "object" },
},
toolbarVisible: {
description: "Whether to show the toolbar",
control: { type: "boolean" },
},
},
};
export default meta;
type Story = StoryObj<CopilotChatAssistantMessage>;
export const Default: Story = {
args: {
message: simpleMessage,
toolbarVisible: true,
},
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatAssistantMessage } from '@copilotkit/angular';
import { AssistantMessage } from '@ag-ui/client';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatAssistantMessage],
template: \`
<copilot-chat-assistant-message
[message]="message"
[toolbarVisible]="true"
(thumbsUp)="onThumbsUp($event)"
(thumbsDown)="onThumbsDown($event)"
(readAloud)="onReadAloud($event)"
(regenerate)="onRegenerate($event)">
</copilot-chat-assistant-message>
\`
})
export class ChatComponent {
message: AssistantMessage = {
id: 'simple-message',
content: 'Hello! How can I help you today?',
role: 'assistant',
};
onThumbsUp(event: any): void {
console.log('Thumbs up clicked!');
}
onThumbsDown(event: any): void {
console.log('Thumbs down clicked!');
}
onReadAloud(event: any): void {
console.log('Read aloud clicked!');
}
onRegenerate(event: any): void {
console.log('Regenerate clicked!');
}
}`,
language: "typescript",
},
},
},
};
export const TestAllMarkdownFeatures: Story = {
args: {
message: markdownTestMessage,
toolbarVisible: true,
},
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatAssistantMessage } from '@copilotkit/angular';
import { AssistantMessage } from '@ag-ui/client';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatAssistantMessage],
template: \`
<copilot-chat-assistant-message
[message]="message"
[toolbarVisible]="true">
</copilot-chat-assistant-message>
\`
})
export class ChatComponent {
message: AssistantMessage = {
id: 'test-message',
content: \`# Markdown Test Message
This message tests various markdown features including **bold**, *italic*, and \\\`inline code\\\`.
## Code Blocks
\\\`\\\`\\\`javascript
function greet(name) {
console.log(\\\`Hello, \\${name}!\\\`);
return \\\`Welcome, \\${name}\\\`;
}
\\\`\\\`\\\`
## Links and Tables
- [External link](https://example.com)
| Feature | Supported | Notes |
|---------|-----------|-------|
| Headers | ✅ | All levels |
| Lists | ✅ | Nested support |
| Code | ✅ | Syntax highlighting |\`,
role: 'assistant',
};
}`,
language: "typescript",
},
},
},
};
export const WithToolbarButtons: Story = {
args: {
message: simpleMessage,
toolbarVisible: true,
},
render: (args) => ({
props: {
...args,
onThumbsUp: (event: any) => {
alert("Thumbs up clicked!");
},
onThumbsDown: (event: any) => {
alert("Thumbs down clicked!");
},
onReadAloud: (event: any) => {
alert("Read aloud clicked!");
},
onRegenerate: (event: any) => {
alert("Regenerate clicked!");
},
},
template: `
<div style="display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 16px;">
<div style="width: 100%; max-width: 640px;">
<copilot-chat-assistant-message
[message]="message"
[toolbarVisible]="toolbarVisible"
(thumbsUp)="onThumbsUp($event)"
(thumbsDown)="onThumbsDown($event)"
(readAloud)="onReadAloud($event)"
(regenerate)="onRegenerate($event)">
</copilot-chat-assistant-message>
</div>
</div>
`,
}),
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatAssistantMessage } from '@copilotkit/angular';
import { AssistantMessage } from '@ag-ui/client';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatAssistantMessage],
template: \`
<copilot-chat-assistant-message
[message]="message"
[toolbarVisible]="true"
(thumbsUp)="onThumbsUp($event)"
(thumbsDown)="onThumbsDown($event)"
(readAloud)="onReadAloud($event)"
(regenerate)="onRegenerate($event)">
</copilot-chat-assistant-message>
\`
})
export class ChatComponent {
message: AssistantMessage = {
id: 'simple-message',
content: 'Hello! How can I help you today?',
role: 'assistant',
};
onThumbsUp(event: any): void {
alert('Thumbs up clicked!');
}
onThumbsDown(event: any): void {
alert('Thumbs down clicked!');
}
onReadAloud(event: any): void {
alert('Read aloud clicked!');
}
onRegenerate(event: any): void {
alert('Regenerate clicked!');
}
}`,
language: "typescript",
},
},
},
};
export const WithAdditionalToolbarItems: Story = {
render: (args) => ({
props: {
message: simpleMessage,
onThumbsUp: (event: any) => console.log("Thumbs up clicked!"),
onThumbsDown: (event: any) => console.log("Thumbs down clicked!"),
onReadAloud: (event: any) => console.log("Read aloud clicked!"),
onRegenerate: (event: any) => console.log("Regenerate clicked!"),
onCustom1: () => alert("Custom button 1 clicked!"),
onCustom2: () => alert("Custom button 2 clicked!"),
},
template: `
<div style="display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 16px;">
<div style="width: 100%; max-width: 640px;">
<copilot-chat-assistant-message
[message]="message"
[toolbarVisible]="true"
(thumbsUp)="onThumbsUp($event)"
(thumbsDown)="onThumbsDown($event)"
(readAloud)="onReadAloud($event)"
(regenerate)="onRegenerate($event)"
[additionalToolbarItems]="additionalItems">
<ng-template #additionalItems>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:bg-gray-100 cpk:hover:bg-gray-200 cpk:flex cpk:items-center cpk:justify-center"
(click)="onCustom1()"
title="Custom Action 1">
📌
</button>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:bg-gray-100 cpk:hover:bg-gray-200 cpk:flex cpk:items-center cpk:justify-center"
(click)="onCustom2()"
title="Custom Action 2">
❤️
</button>
</ng-template>
</copilot-chat-assistant-message>
</div>
</div>
`,
}),
parameters: {
docs: {
source: {
type: "code",
code: `import { Component, ViewChild, TemplateRef } from '@angular/core';
import { CopilotChatAssistantMessage } from '@copilotkit/angular';
import { AssistantMessage } from '@ag-ui/client';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatAssistantMessage],
template: \`
<ng-template #additionalItems>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:bg-gray-100 cpk:hover:bg-gray-200 cpk:flex cpk:items-center cpk:justify-center"
(click)="onCustom1()"
title="Custom Action 1">
📌
</button>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:bg-gray-100 cpk:hover:bg-gray-200 cpk:flex cpk:items-center cpk:justify-center"
(click)="onCustom2()"
title="Custom Action 2">
❤️
</button>
</ng-template>
<copilot-chat-assistant-message
[message]="message"
[toolbarVisible]="true"
[additionalToolbarItems]="additionalItems"
(thumbsUp)="onThumbsUp($event)"
(thumbsDown)="onThumbsDown($event)"
(readAloud)="onReadAloud($event)"
(regenerate)="onRegenerate($event)">
</copilot-chat-assistant-message>
\`
})
export class ChatComponent {
@ViewChild('additionalItems') additionalItems!: TemplateRef<any>;
message: AssistantMessage = {
id: 'simple-message',
content: 'Hello! How can I help you today?',
role: 'assistant',
};
onThumbsUp(event: any): void {
console.log('Thumbs up clicked!');
}
onThumbsDown(event: any): void {
console.log('Thumbs down clicked!');
}
onReadAloud(event: any): void {
console.log('Read aloud clicked!');
}
onRegenerate(event: any): void {
console.log('Regenerate clicked!');
}
onCustom1(): void {
alert('Custom button 1 clicked!');
}
onCustom2(): void {
alert('Custom button 2 clicked!');
}
}`,
language: "typescript",
},
},
},
};
export const CodeBlocksWithLanguages: Story = {
args: {
message: codeBlocksTestMessage,
toolbarVisible: true,
},
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatAssistantMessage } from '@copilotkit/angular';
import { AssistantMessage } from '@ag-ui/client';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatAssistantMessage],
template: \`
<copilot-chat-assistant-message
[message]="message"
[toolbarVisible]="true">
</copilot-chat-assistant-message>
\`
})
export class ChatComponent {
message: AssistantMessage = {
id: 'msg-code-blocks-test',
content: \`# Code Blocks Test
## JavaScript Example
\\\`\\\`\\\`javascript
const authenticateUser = async (email, password) => {
try {
const response = await fetch('/api/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
return await response.json();
} catch (error) {
console.error('Error:', error);
return null;
}
};
\\\`\\\`\\\`
## Python Example
\\\`\\\`\\\`python
import pandas as pd
def process_user_data(csv_file):
df = pd.read_csv(csv_file)
df['age'] = pd.to_numeric(df['age'], errors='coerce')
return df
\\\`\\\`\\\`\`,
role: 'assistant',
};
}`,
language: "typescript",
},
},
},
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,585 @@
import type { Meta, StoryObj } from "@storybook/angular";
import { moduleMetadata } from "@storybook/angular";
import { CommonModule } from "@angular/common";
import {
CopilotChatUserMessage,
provideCopilotChatLabels,
} from "@copilotkit/angular";
import type { UserMessage } from "@ag-ui/client";
// Simple default message
const simpleMessage: UserMessage = {
id: "simple-user-message",
content: "Hello! Can you help me build an Angular component?",
role: "user",
};
// Longer user message
const longMessage: UserMessage = {
id: "long-user-message",
content: `I need help with creating a complex Angular component that handles user authentication. Here are my requirements:
1. The component should have login and signup forms
2. It needs to integrate with Firebase Auth
3. Should handle form validation
4. Must be responsive and work on mobile
5. Include forgot password functionality
6. Support social login (Google, GitHub)
Can you help me implement this step by step? I'm particularly struggling with the form validation and state management parts.`,
role: "user",
};
// Code-related user message
const codeMessage: UserMessage = {
id: "code-user-message",
content: `I'm getting this error in my Angular app:
TypeError: Cannot read property 'map' of undefined
The error happens in this component:
@Component({
selector: 'app-user-list',
template: \`
<div *ngFor="let user of users">
{{ user.name }}
</div>
\`
})
export class UserListComponent {
@Input() users: User[];
}
How can I fix this?`,
role: "user",
};
// Short question
const shortMessage: UserMessage = {
id: "short-user-message",
content: "What's the difference between signals and observables in Angular?",
role: "user",
};
const meta: Meta<CopilotChatUserMessage> = {
title: "UI/CopilotChatUserMessage",
component: CopilotChatUserMessage,
decorators: [
moduleMetadata({
imports: [CommonModule, CopilotChatUserMessage],
providers: [provideCopilotChatLabels({})],
}),
],
render: (args) => ({
props: {
...args,
},
template: `
<div style="display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 16px;">
<div style="width: 100%; max-width: 640px;">
<copilot-chat-user-message
[message]="message"
[branchIndex]="branchIndex"
[numberOfBranches]="numberOfBranches"
[inputClass]="inputClass"
[additionalToolbarItems]="additionalToolbarItems"
(editMessage)="editMessage && editMessage($event)"
(switchToBranch)="switchToBranch && switchToBranch($event)"
></copilot-chat-user-message>
</div>
</div>
`,
}),
args: {
message: simpleMessage,
editMessage: () => console.log("Edit clicked!"),
},
};
export default meta;
type Story = StoryObj<CopilotChatUserMessage>;
export const Default: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatUserMessage, UserMessage } from '@copilotkit/angular';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatUserMessage],
template: \`
<copilot-chat-user-message
[message]="message"
(editMessage)="onEditMessage($event)">
</copilot-chat-user-message>
\`
})
export class ChatComponent {
message: UserMessage = {
id: 'user-1',
content: 'Hello! Can you help me build an Angular component?',
role: 'user',
timestamp: new Date(),
};
onEditMessage(event: any): void {
console.log('Edit message:', event);
}
}`,
language: "typescript",
},
},
},
};
export const LongMessage: Story = {
args: {
message: longMessage,
},
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatUserMessage, UserMessage } from '@copilotkit/angular';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatUserMessage],
template: \`
<copilot-chat-user-message
[message]="message"
(editMessage)="onEditMessage($event)">
</copilot-chat-user-message>
\`
})
export class ChatComponent {
message: UserMessage = {
id: 'long-user-message',
content: \`I need help with creating a complex Angular component that handles user authentication. Here are my requirements:
1. The component should have login and signup forms
2. It needs to integrate with Firebase Auth
3. Should handle form validation
4. Must be responsive and work on mobile
5. Include forgot password functionality
6. Support social login (Google, GitHub)
Can you help me implement this step by step? I'm particularly struggling with the form validation and state management parts.\`,
role: 'user',
timestamp: new Date(),
};
onEditMessage(event: any): void {
console.log('Edit message:', event);
}
}`,
language: "typescript",
},
},
},
};
export const WithEditButton: Story = {
args: {
message: simpleMessage,
editMessage: () => alert("Edit message clicked!"),
},
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatUserMessage, UserMessage } from '@copilotkit/angular';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatUserMessage],
template: \`
<copilot-chat-user-message
[message]="message"
(editMessage)="onEditMessage($event)">
</copilot-chat-user-message>
\`
})
export class ChatComponent {
message: UserMessage = {
id: 'simple-user-message',
content: 'Hello! Can you help me build an Angular component?',
role: 'user',
timestamp: new Date(),
};
onEditMessage(event: any): void {
alert('Edit message clicked!');
console.log('Edit message:', event);
}
}`,
language: "typescript",
},
},
},
};
export const WithoutEditButton: Story = {
args: {
message: simpleMessage,
editMessage: undefined,
},
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatUserMessage, UserMessage } from '@copilotkit/angular';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatUserMessage],
template: \`
<copilot-chat-user-message
[message]="message">
</copilot-chat-user-message>
\`
})
export class ChatComponent {
message: UserMessage = {
id: 'simple-user-message',
content: 'Hello! Can you help me build an Angular component?',
role: 'user',
timestamp: new Date(),
};
// No edit handler - edit button won't appear
}`,
language: "typescript",
},
},
},
};
export const CodeRelatedMessage: Story = {
args: {
message: codeMessage,
editMessage: () => alert("Edit code message clicked!"),
},
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatUserMessage, UserMessage } from '@copilotkit/angular';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatUserMessage],
template: \`
<copilot-chat-user-message
[message]="message"
(editMessage)="onEditMessage($event)">
</copilot-chat-user-message>
\`
})
export class ChatComponent {
message: UserMessage = {
id: 'code-user-message',
content: \`I'm getting this error in my Angular app:
TypeError: Cannot read property 'map' of undefined
The error happens in this component:
@Component({
selector: 'app-user-list',
template: \\\`
<div *ngFor="let user of users">
{{ user.name }}
</div>
\\\`
})
export class UserListComponent {
@Input() users: User[];
}
How can I fix this?\`,
role: 'user',
timestamp: new Date(),
};
onEditMessage(event: any): void {
alert('Edit code message clicked!');
}
}`,
language: "typescript",
},
},
},
};
export const ShortQuestion: Story = {
args: {
message: shortMessage,
editMessage: () => console.log("Edit short message clicked!"),
},
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatUserMessage, UserMessage } from '@copilotkit/angular';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatUserMessage],
template: \`
<copilot-chat-user-message
[message]="message"
(editMessage)="onEditMessage($event)">
</copilot-chat-user-message>
\`
})
export class ChatComponent {
message: UserMessage = {
id: 'short-user-message',
content: "What's the difference between signals and observables in Angular?",
role: 'user',
timestamp: new Date(),
};
onEditMessage(event: any): void {
console.log('Edit short message clicked!');
}
}`,
language: "typescript",
},
},
},
};
export const WithAdditionalToolbarItems: Story = {
render: () => ({
props: {
message: simpleMessage,
editMessage: () => console.log("Edit clicked!"),
},
template: `
<ng-template #additionalItems>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:bg-gray-100 cpk:hover:bg-gray-200 cpk:flex cpk:items-center cpk:justify-center"
(click)="alert('Custom button 1 clicked!')"
title="Custom Action 1">
📎
</button>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:bg-gray-100 cpk:hover:bg-gray-200 cpk:flex cpk:items-center cpk:justify-center"
(click)="alert('Custom button 2 clicked!')"
title="Custom Action 2">
🔄
</button>
</ng-template>
<div style="display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 16px;">
<div style="width: 100%; max-width: 640px;">
<copilot-chat-user-message
[message]="message"
[additionalToolbarItems]="additionalItems"
(editMessage)="editMessage($event)"
></copilot-chat-user-message>
</div>
</div>
`,
}),
parameters: {
docs: {
source: {
type: "code",
code: `import { Component, ViewChild, TemplateRef } from '@angular/core';
import { CopilotChatUserMessage, UserMessage } from '@copilotkit/angular';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatUserMessage],
template: \`
<ng-template #additionalItems>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:bg-gray-100 cpk:hover:bg-gray-200 cpk:flex cpk:items-center cpk:justify-center"
(click)="onCustomAction1()"
title="Custom Action 1">
📎
</button>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:bg-gray-100 cpk:hover:bg-gray-200 cpk:flex cpk:items-center cpk:justify-center"
(click)="onCustomAction2()"
title="Custom Action 2">
🔄
</button>
</ng-template>
<copilot-chat-user-message
[message]="message"
[additionalToolbarItems]="additionalItems"
(editMessage)="onEditMessage($event)">
</copilot-chat-user-message>
\`
})
export class ChatComponent {
@ViewChild('additionalItems') additionalItems!: TemplateRef<any>;
message: UserMessage = {
id: 'simple-user-message',
content: 'Hello! Can you help me build an Angular component?',
role: 'user',
timestamp: new Date(),
};
onEditMessage(event: any): void {
console.log('Edit clicked!');
}
onCustomAction1(): void {
alert('Custom button 1 clicked!');
}
onCustomAction2(): void {
alert('Custom button 2 clicked!');
}
}`,
language: "typescript",
},
},
},
};
export const CustomAppearance: Story = {
args: {
message: simpleMessage,
editMessage: () => console.log("Edit clicked!"),
inputClass:
"cpk:bg-blue-50 cpk:border cpk:border-blue-200 cpk:rounded-lg cpk:p-4",
},
render: () => ({
props: {
message: simpleMessage,
editMessage: () => console.log("Edit clicked!"),
},
template: `
<ng-template #messageRenderer let-content="content">
<div class="cpk:prose cpk:dark:prose-invert cpk:bg-muted cpk:relative cpk:max-w-[80%] cpk:rounded-[18px] cpk:px-4 cpk:py-1.5 cpk:data-[multiline]:py-3 cpk:inline-block cpk:whitespace-pre-wrap cpk:text-blue-900 cpk:font-medium">
{{ content }}
</div>
</ng-template>
<ng-template #toolbar>
<div class="cpk:w-full cpk:bg-transparent cpk:flex cpk:items-center cpk:justify-end cpk:-mr-[5px] cpk:mt-[8px] cpk:invisible cpk:group-hover:visible">
<div class="cpk:flex cpk:items-center cpk:gap-1 cpk:justify-end">
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:text-blue-600 cpk:hover:bg-blue-100 cpk:flex cpk:items-center cpk:justify-center"
(click)="handleCopy()">
📋
</button>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:text-blue-600 cpk:hover:bg-blue-100 cpk:flex cpk:items-center cpk:justify-center"
(click)="editMessage({message: message})">
✏️
</button>
</div>
</div>
</ng-template>
<div style="display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 16px;">
<div style="width: 100%; max-width: 640px;">
<copilot-chat-user-message
[message]="message"
inputClass="cpk:bg-blue-50 cpk:border-blue-200 cpk:rounded-lg cpk:p-4"
(editMessage)="editMessage($event)">
<ng-template #messageRenderer let-content="content">
<div class="cpk:prose cpk:dark:prose-invert cpk:bg-muted cpk:relative cpk:max-w-[80%] cpk:rounded-[18px] cpk:px-4 cpk:py-1.5 cpk:data-[multiline]:py-3 cpk:inline-block cpk:whitespace-pre-wrap cpk:text-blue-900 cpk:font-medium">
{{ content }}
</div>
</ng-template>
</copilot-chat-user-message>
</div>
</div>
`,
}),
};
export const CustomComponents: Story = {
args: {
message: simpleMessage,
editMessage: () => console.log("Edit clicked!"),
inputClass:
"cpk:bg-gradient-to-r cpk:from-purple-100 cpk:to-pink-100 cpk:rounded-xl cpk:p-4 cpk:shadow-sm",
},
render: () => ({
props: {
message: simpleMessage,
editMessage: () => console.log("Edit clicked!"),
},
template: `
<ng-template #messageRenderer let-content="content">
<div class="cpk:font-mono cpk:text-purple-800 cpk:bg-white/50 cpk:rounded-lg cpk:px-3 cpk:py-2 cpk:inline-block">
💬 {{ content }}
</div>
</ng-template>
<div style="display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 16px;">
<div style="width: 100%; max-width: 640px;">
<copilot-chat-user-message
[message]="message"
inputClass="cpk:bg-gradient-to-r cpk:from-purple-100 cpk:to-pink-100 cpk:rounded-xl cpk:p-4 cpk:shadow-sm"
(editMessage)="editMessage($event)">
<ng-template #messageRenderer let-content="content">
<div class="cpk:font-mono cpk:text-purple-800 cpk:bg-white/50 cpk:rounded-lg cpk:px-3 cpk:py-2 cpk:inline-block">
💬 {{ content }}
</div>
</ng-template>
</copilot-chat-user-message>
</div>
</div>
`,
}),
};
export const WithBranchNavigation: Story = {
args: {
message: {
id: "branch-message",
content:
"This message has multiple branches. You can navigate between them using the branch controls.",
role: "user",
},
editMessage: () => console.log("Edit clicked!"),
branchIndex: 2,
numberOfBranches: 3,
switchToBranch: ({ branchIndex }) =>
console.log(`Switching to branch ${branchIndex + 1}`),
},
};
export const WithManyBranches: Story = {
args: {
message: {
id: "many-branches-message",
content:
"This is branch 5 of 10. Use the navigation arrows to explore different variations of this message.",
role: "user",
},
editMessage: () => console.log("Edit clicked!"),
branchIndex: 4,
numberOfBranches: 10,
switchToBranch: ({ branchIndex }) =>
alert(`Would switch to branch ${branchIndex + 1} of 10`),
},
};
@@ -0,0 +1,216 @@
import type { Meta, StoryObj } from "@storybook/angular";
import { moduleMetadata } from "@storybook/angular";
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
provideCopilotChatLabels,
provideCopilotKit,
} from "@copilotkit/angular";
import type { Message } from "@ag-ui/client";
const meta: Meta<CopilotChatView> = {
title: "UI/CopilotChatView/Custom Actions",
component: CopilotChatView,
decorators: [
moduleMetadata({
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: "Type a message...",
chatDisclaimerText:
"AI can make mistakes. Please verify important information.",
}),
],
}),
],
parameters: {
layout: "fullscreen",
},
};
export default meta;
type Story = StoryObj<CopilotChatView>;
export const ThumbsUpDown: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
import { Message } from '@ag-ui/client';
// Custom disclaimer component
&#64;Component({
selector: 'custom-disclaimer',
standalone: true,
template: \`
<div
[class]="inputClass"
style="
text-align: center;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-size: 14px;
margin: 8px 16px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
">
🎨 This chat interface is fully customizable!
</div>
\`
})
class CustomDisclaimerComponent {
&#64;Input() text?: string;
&#64;Input() inputClass?: string;
}
&#64;Component({
selector: 'app-chat-actions',
standalone: true,
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
CustomDisclaimerComponent
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: "Type a message...",
chatDisclaimerText:
"AI can make mistakes. Please verify important information.",
})
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[disclaimerComponent]="customDisclaimerComponent"
(assistantMessageThumbsUp)="onThumbsUp($event)"
(assistantMessageThumbsDown)="onThumbsDown($event)">
</copilot-chat-view>
</div>
\`
})
export class ChatActionsComponent {
messages: Message[] = [
{
id: 'user-1',
content: 'Hello! Can you help me with TypeScript?',
role: 'user'
},
{
id: 'assistant-1',
content: 'Of course! TypeScript is a superset of JavaScript that adds static typing. What would you like to know?',
role: 'assistant'
}
];
customDisclaimerComponent = CustomDisclaimerComponent;
onThumbsUp(event: any) {
console.log('Thumbs up!', event);
alert('You liked this message!');
}
onThumbsDown(event: any) {
console.log('Thumbs down!', event);
alert('You disliked this message!');
}
}`,
language: "typescript",
},
},
},
render: () => {
// Custom disclaimer component
@Component({
selector: "custom-disclaimer",
standalone: true,
template: `
<div
[class]="inputClass"
style="
text-align: center;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-size: 14px;
margin: 8px 16px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
"
>
🎨 This chat interface is fully customizable!
</div>
`,
})
class CustomDisclaimerComponent {
// Accept slot-provided inputs to avoid NG0303
@Input() text?: string;
@Input() inputClass?: string;
}
const messages: Message[] = [
{
id: "user-1",
content: "Hello! Can you help me with TypeScript?",
role: "user" as const,
},
{
id: "assistant-1",
content:
"Of course! TypeScript is a superset of JavaScript that adds static typing. What would you like to know?",
role: "assistant" as const,
},
];
const onThumbsUp = (event: any) => {
console.log("Thumbs up!", event);
alert("You liked this message!");
};
const onThumbsDown = (event: any) => {
console.log("Thumbs down!", event);
alert("You disliked this message!");
};
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[disclaimerComponent]="customDisclaimerComponent"
(assistantMessageThumbsUp)="onThumbsUp($event)"
(assistantMessageThumbsDown)="onThumbsDown($event)">
</copilot-chat-view>
</div>
`,
props: {
messages,
customDisclaimerComponent: CustomDisclaimerComponent,
onThumbsUp,
onThumbsDown,
},
};
},
};
@@ -0,0 +1,180 @@
import type { Meta, StoryObj } from "@storybook/angular";
import { moduleMetadata } from "@storybook/angular";
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
provideCopilotChatLabels,
provideCopilotKit,
} from "@copilotkit/angular";
import type { Message } from "@ag-ui/client";
const meta: Meta<CopilotChatView> = {
title: "UI/CopilotChatView/Customized with CSS",
component: CopilotChatView,
decorators: [
moduleMetadata({
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: "Type a message...",
chatDisclaimerText:
"AI can make mistakes. Please verify important information.",
}),
],
}),
],
parameters: {
layout: "fullscreen",
},
};
export default meta;
type Story = StoryObj<CopilotChatView>;
// Story with custom disclaimer text
export const CustomDisclaimerText: Story = {
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "Hello!",
role: "user" as const,
},
{
id: "assistant-1",
content: "Hi there! How can I help you today?",
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[disclaimerText]="'This is a custom disclaimer message for your chat interface.'">
</copilot-chat-view>
</div>
`,
props: {
messages,
},
};
},
};
export const AnimatedDisclaimer: Story = {
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "Hello! Can you help me with styling?",
role: "user" as const,
},
{
id: "assistant-1",
content: `Absolutely! I can help you with CSS styling, design patterns, and UI/UX best practices. What specific styling challenge are you working on?`,
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<style>
.custom-disclaimer {
background: linear-gradient(90deg, #FF6B6B 0%, #4ECDC4 50%, #45B7D1 100%);
color: white;
font-weight: 600;
font-size: 14px;
padding: 16px 24px;
margin: 12px 20px;
border-radius: 12px;
text-align: center;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
animation: pulse 3s ease-in-out infinite;
position: relative;
overflow: hidden;
}
.custom-disclaimer::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(
45deg,
transparent,
rgba(255, 255, 255, 0.1),
transparent
);
transform: rotate(45deg);
animation: shimmer 3s infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.02);
}
}
@keyframes shimmer {
0% {
transform: translateX(-100%) translateY(-100%) rotate(45deg);
}
100% {
transform: translateX(100%) translateY(100%) rotate(45deg);
}
}
.custom-disclaimer-text {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.custom-disclaimer-icon {
font-size: 20px;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
</style>
<copilot-chat-view
[messages]="messages"
[disclaimerClass]="'custom-disclaimer'"
[disclaimerText]="'✨ Styled with custom CSS classes - AI responses may need verification ✨'">
</copilot-chat-view>
</div>
`,
props: {
messages,
},
};
},
};
@@ -0,0 +1,834 @@
import type { Meta, StoryObj } from "@storybook/angular";
import { moduleMetadata } from "@storybook/angular";
import { CommonModule } from "@angular/common";
import { Component, Injectable, signal } from "@angular/core";
import { FormsModule } from "@angular/forms";
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
ChatState,
provideCopilotChatLabels,
provideCopilotKit,
} from "@copilotkit/angular";
import type { Message } from "@ag-ui/client";
import { CustomDisclaimerComponent } from "../components/custom-disclaimer.component";
import { CustomInputComponent } from "../components/custom-input.component";
import { CustomScrollButtonComponent } from "../components/custom-scroll-button.component";
@Injectable()
class StoryChatState extends ChatState {
readonly inputValue = signal<string>("");
submitInput(value: string): void {
const trimmed = value.trim();
if (!trimmed) return;
console.log("[Storybook] submitInput", trimmed);
this.inputValue.set("");
}
changeInput(value: string): void {
this.inputValue.set(value);
}
}
const meta: Meta<CopilotChatView> = {
title: "UI/CopilotChatView/Customized with Components",
component: CopilotChatView,
decorators: [
moduleMetadata({
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: "Type a message...",
chatDisclaimerText:
"AI can make mistakes. Please verify important information.",
}),
{ provide: ChatState, useClass: StoryChatState },
],
}),
],
parameters: {
layout: "fullscreen",
},
};
export default meta;
type Story = StoryObj<CopilotChatView>;
export const CustomDisclaimer: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
import { Message } from '@ag-ui/client';
// Custom disclaimer component
&#64;Component({
selector: 'custom-disclaimer',
standalone: true,
template: \`
<div [class]="inputClass" style="
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
margin: 10px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
">
<h3 style="margin: 0 0 10px 0; font-size: 20px;">
✨ Custom Disclaimer Component ✨
</h3>
<p style="margin: 0; font-size: 14px; opacity: 0.9;">
{{ '{' }}{{ '{' }} text || 'This is a custom disclaimer demonstrating component overrides!' {{ '}' }}{{ '}' }}
</p>
<div style="
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid rgba(255, 255, 255, 0.3);
font-size: 12px;
opacity: 0.7;
">
🎨 Styled with custom gradients and animations
</div>
</div>
\`
})
class CustomDisclaimerComponent {
&#64;Input() text?: string;
&#64;Input() inputClass?: string;
}
&#64;Component({
selector: 'app-custom-disclaimer',
standalone: true,
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
CustomDisclaimerComponent
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: "Type a message...",
chatDisclaimerText:
"AI can make mistakes. Please verify important information.",
})
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[disclaimerComponent]="customDisclaimerComponent">
</copilot-chat-view>
</div>
\`
})
export class CustomDisclaimerExampleComponent {
messages: Message[] = [
{
id: 'user-1',
content: 'Hello! Can you help me with TypeScript?',
role: 'user'
},
{
id: 'assistant-1',
content: 'Of course! TypeScript is a superset of JavaScript that adds static typing. What would you like to know?',
role: 'assistant'
}
];
customDisclaimerComponent = CustomDisclaimerComponent;
}`,
language: "typescript",
},
},
},
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "Hello! Can you help me with TypeScript?",
role: "user" as const,
},
{
id: "assistant-1",
content:
"Of course! TypeScript is a superset of JavaScript that adds static typing. What would you like to know?",
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[disclaimerComponent]="customDisclaimerComponent">
</copilot-chat-view>
</div>
`,
props: {
messages,
customDisclaimerComponent: CustomDisclaimerComponent,
},
};
},
};
export const CustomInput: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
ChatState,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
import { Message } from '@ag-ui/client';
// Custom input component
&#64;Component({
selector: 'custom-input',
standalone: true,
imports: [CommonModule, FormsModule],
template: \`
<div [class]="inputClass" style="
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
padding: 20px;
border-radius: 15px;
margin: 10px;
">
<input
type="text"
[(ngModel)]="inputValue"
placeholder="💬 Ask me anything..."
style="
width: 100%;
padding: 15px;
border: 2px solid white;
border-radius: 10px;
font-size: 16px;
background: rgba(255, 255, 255, 0.9);
color: #333;
outline: none;
"
(keyup.enter)="handleSend()"
/>
<button
style="
margin-top: 10px;
padding: 10px 20px;
background: white;
color: #f5576c;
border: none;
border-radius: 5px;
font-weight: bold;
cursor: pointer;
"
(click)="handleSend()">
Send Message ✨
</button>
</div>
\`
})
class CustomInputComponent {
&#64;Input() inputClass?: string;
inputValue = '';
constructor(private chat: ChatState) {}
handleSend() {
const value = this.inputValue.trim();
if (value) {
this.chat.submitInput(value);
this.inputValue = '';
}
}
}
&#64;Component({
selector: 'app-custom-input',
standalone: true,
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
CustomInputComponent
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: 'Type a message...',
chatDisclaimerText: 'AI can make mistakes. Please verify important information.'
})
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[inputComponent]="customInputComponent">
</copilot-chat-view>
</div>
\`
})
export class CustomInputExampleComponent {
messages: Message[] = [
{
id: 'user-1',
content: 'Check out this custom input!',
role: 'user'
},
{
id: 'assistant-1',
content: 'That's a beautiful custom input component! The gradient and styling look great.',
role: 'assistant'
}
];
customInputComponent = CustomInputComponent;
}`,
language: "typescript",
},
},
},
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "Check out this custom input!",
role: "user" as const,
},
{
id: "assistant-1",
content:
"That's a beautiful custom input component! The gradient and styling look great.",
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[inputComponent]="customInputComponent">
</copilot-chat-view>
</div>
`,
props: {
messages,
customInputComponent: CustomInputComponent,
},
};
},
};
export const CustomScrollButton: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
import { Message } from '@ag-ui/client';
// Custom scroll button component
&#64;Component({
selector: 'custom-scroll-button',
standalone: true,
imports: [CommonModule],
template: \`
<button
type="button"
(click)="handleClick()"
[class]="inputClass"
[class.hover]="isHovered"
(mouseenter)="isHovered = true"
(mouseleave)="isHovered = false"
style="
position: fixed;
bottom: 100px;
right: 20px;
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: 3px solid white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
cursor: pointer;
pointer-events: auto;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s;
z-index: 1000;
"
[style.transform]="isHovered ? 'scale(1.1)' : 'scale(1)'">
<span style="color: white; font-size: 24px; pointer-events: none;">⬇️</span>
</button>
\`,
styles: [\`
button.hover {
transform: scale(1.1);
}
\`]
})
class CustomScrollButtonComponent {
&#64;Input() onClick?: () => void;
&#64;Input() inputClass?: string;
&#64;Output() clicked = new EventEmitter<void>();
isHovered = false;
handleClick() {
this.clicked.emit();
if (this.onClick) {
this.onClick();
}
}
}
&#64;Component({
selector: 'app-custom-scroll',
standalone: true,
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
CustomScrollButtonComponent
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: 'Type a message...',
chatDisclaimerText: 'AI can make mistakes. Please verify important information.'
})
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[autoScroll]="false"
[scrollToBottomButtonComponent]="scrollToBottomButtonComponent">
</copilot-chat-view>
</div>
\`
})
export class CustomScrollButtonExampleComponent {
messages: Message[] = [];
scrollToBottomButtonComponent = CustomScrollButtonComponent;
constructor() {
// Generate many messages to show scroll behavior
for (let i = 0; i < 20; i++) {
this.messages.push({
id: \`msg-\${i}\`,
content: \`Message \${i}: This is a test message to demonstrate the custom scroll button.\`,
role: i % 2 === 0 ? 'user' : 'assistant'
} as Message);
}
}
}`,
language: "typescript",
},
},
},
render: () => {
// Generate many messages to show scroll behavior
const messages: Message[] = [];
for (let i = 0; i < 20; i++) {
messages.push({
id: `msg-${i}`,
content: `Message ${i}: This is a test message to demonstrate the custom scroll button.`,
role: i % 2 === 0 ? "user" : "assistant",
} as Message);
}
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[autoScroll]="false"
[scrollToBottomButtonComponent]="scrollToBottomButtonComponent">
</copilot-chat-view>
</div>
`,
props: {
messages,
scrollToBottomButtonComponent: CustomScrollButtonComponent,
},
};
},
};
export const NoFeatherEffect: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
import { Message } from '@ag-ui/client';
&#64;Component({
selector: 'app-no-feather',
standalone: true,
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: 'Type a message...',
chatDisclaimerText: 'AI can make mistakes. Please verify important information.'
})
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[featherComponent]="null">
</copilot-chat-view>
</div>
\`
})
export class NoFeatherEffectComponent {
messages: Message[] = [
{
id: 'user-1',
content: 'Hello!',
role: 'user'
},
{
id: 'assistant-1',
content: 'Hi there! How can I help you today?',
role: 'assistant'
}
];
}`,
language: "typescript",
},
},
},
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "Hello!",
role: "user" as const,
},
{
id: "assistant-1",
content: "Hi there! How can I help you today?",
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[featherComponent]="null">
</copilot-chat-view>
</div>
`,
props: {
messages,
},
};
},
};
export const CustomInputServiceBased: Story = {
name: "Custom Input via Service (Recommended)",
parameters: {
docs: {
description: {
story: `
Demonstrates the recommended approach for custom inputs using service injection.
This pattern uses \`ChatState.submitInput()\` to submit messages,
which is the idiomatic Angular approach for cross-component communication.
**Key differences from React:**
- Angular uses dependency injection with services
- React uses callback props (e.g., \`onSubmitMessage\`)
- Both achieve the same result with framework-appropriate patterns
`,
},
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
ChatState,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
import { Message } from '@ag-ui/client';
// Minimal custom input component with service injection
&#64;Component({
selector: 'service-based-input',
standalone: true,
imports: [CommonModule, FormsModule],
template: \`
<div style="
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
padding: 20px;
border-radius: 15px;
margin: 10px;
">
<h4 style="color: white; margin: 0 0 10px 0;">
Service-Based Custom Input
</h4>
<div style="display: flex; gap: 10px;">
<input
type="text"
[(ngModel)]="value"
placeholder="Type your message..."
style="
flex: 1;
padding: 12px;
border: 2px solid white;
border-radius: 8px;
font-size: 14px;
background: rgba(255, 255, 255, 0.95);
color: #333;
outline: none;
"
(keyup.enter)="submit()"
/>
<button
style="
padding: 12px 24px;
background: white;
color: #059669;
border: none;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
"
(click)="submit()">
Submit
</button>
</div>
<p style="color: rgba(255, 255, 255, 0.9); font-size: 12px; margin: 8px 0 0 0;">
This component uses ChatState.submitInput()
</p>
</div>
\`
})
class ServiceBasedInputComponent {
value = '';
constructor(private chat: ChatState) {}
submit() {
const trimmedValue = this.value.trim();
if (!trimmedValue) return;
// Use the service to submit the message
this.chat.submitInput(trimmedValue);
this.value = '';
}
}
&#64;Component({
selector: 'app-service-based-example',
standalone: true,
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
ServiceBasedInputComponent
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: 'Type a message...',
chatDisclaimerText: 'AI can make mistakes. Please verify important information.'
})
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[inputComponent]="customInputComponent">
</copilot-chat-view>
</div>
\`
})
export class ServiceBasedExampleComponent {
messages: Message[] = [
{
id: 'user-1',
content: 'How does the service-based approach work?',
role: 'user'
},
{
id: 'assistant-1',
content: 'The service-based approach uses Angular\\'s dependency injection to access ChatState, which provides the submitInput() method for sending messages. This is the idiomatic Angular pattern!',
role: 'assistant'
}
];
customInputComponent = ServiceBasedInputComponent;
}`,
language: "typescript",
},
},
},
render: () => {
// Define the service-based input component inline for the story
@Component({
selector: "story-service-input",
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div
style="
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
padding: 20px;
border-radius: 15px;
margin: 10px;
"
>
<h4 style="color: white; margin: 0 0 10px 0">Service-Based Custom Input</h4>
<div style="display: flex; gap: 10px">
<input
type="text"
[(ngModel)]="value"
placeholder="Type your message..."
style="
flex: 1;
padding: 12px;
border: 2px solid white;
border-radius: 8px;
font-size: 14px;
background: rgba(255, 255, 255, 0.95);
color: #333;
outline: none;
"
(keyup.enter)="submit()"
/>
<button
style="
padding: 12px 24px;
background: white;
color: #059669;
border: none;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
"
(click)="submit()"
>
Submit
</button>
</div>
<p
style="color: rgba(255, 255, 255, 0.9); font-size: 12px; margin: 8px 0 0 0"
>
This component uses ChatState.submitInput()
</p>
</div>
`,
})
class StoryServiceInputComponent {
value = "";
constructor(private chat: ChatState) {}
submit() {
const trimmedValue = this.value.trim();
if (!trimmedValue) return;
this.chat.submitInput(trimmedValue);
this.value = "";
}
}
const messages: Message[] = [
{
id: "user-1",
content: "How does the service-based approach work?",
role: "user" as const,
},
{
id: "assistant-1",
content:
"The service-based approach uses Angular's dependency injection to access ChatState, which provides the submitInput() method for sending messages. This is the idiomatic Angular pattern!",
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[inputComponent]="customInputComponent">
</copilot-chat-view>
</div>
`,
props: {
messages,
customInputComponent: StoryServiceInputComponent,
},
};
},
};
@@ -0,0 +1,452 @@
import type { Meta, StoryObj } from "@storybook/angular";
import { moduleMetadata } from "@storybook/angular";
import { CommonModule } from "@angular/common";
import { Component, Injectable, Input, signal } from "@angular/core";
import { FormsModule } from "@angular/forms";
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
ChatState,
provideCopilotChatLabels,
provideCopilotKit,
} from "@copilotkit/angular";
import type { Message } from "@ag-ui/client";
@Injectable()
class StoryChatState extends ChatState {
readonly inputValue = signal<string>("");
submitInput(value: string): void {
const trimmed = value.trim();
if (!trimmed) return;
console.log("[Storybook] submitInput", trimmed);
this.inputValue.set("");
}
changeInput(value: string): void {
this.inputValue.set(value);
}
}
// Custom input components defined after imports
const meta: Meta<CopilotChatView> = {
title: "UI/CopilotChatView/Customized with Templates",
component: CopilotChatView,
decorators: [
moduleMetadata({
imports: [
CommonModule,
FormsModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: "Type a message...",
chatDisclaimerText:
"AI can make mistakes. Please verify important information.",
}),
{ provide: ChatState, useClass: StoryChatState },
],
}),
],
parameters: {
layout: "fullscreen",
},
};
export default meta;
type Story = StoryObj<CopilotChatView>;
export const CustomDisclaimerTemplate: Story = {
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "How do I use templates for customization?",
role: "user" as const,
},
{
id: "assistant-1",
content:
"Templates provide a powerful way to customize components! You can use ng-template with template references to inject custom HTML directly into the component slots.",
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<!-- Custom Disclaimer Template -->
<ng-template #customDisclaimer>
<div style="
text-align: center;
padding: 16px;
background: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 100%);
color: white;
font-size: 14px;
font-weight: 600;
margin: 12px 20px;
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
position: relative;
overflow: hidden;
">
<div style="
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent);
transform: rotate(45deg);
animation: shimmer 3s infinite;
"></div>
<span style="position: relative; z-index: 1;">
⚡ Template-based customization - AI assistance at your fingertips!
</span>
</div>
</ng-template>
<copilot-chat-view
[messages]="messages"
[disclaimerTemplate]="customDisclaimer">
</copilot-chat-view>
</div>
`,
props: {
messages,
},
};
},
};
// Custom input component for template story
@Component({
selector: "template-custom-input",
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div
style="
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 24px;
margin: 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
"
>
<div style="display: flex; gap: 12px; max-width: 1200px; margin: 0 auto">
<input
type="text"
[(ngModel)]="inputValue"
placeholder="✨ Type your message here..."
style="
flex: 1;
padding: 16px 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
font-size: 16px;
background: rgba(255, 255, 255, 0.95);
color: #333;
outline: none;
transition: all 0.3s ease;
"
(keyup.enter)="sendMessage()"
/>
<button
style="
padding: 16px 32px;
background: white;
color: #667eea;
border: none;
border-radius: 12px;
font-weight: bold;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
"
(click)="sendMessage()"
>
Send
</button>
</div>
<div
style="
text-align: center;
margin-top: 8px;
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
"
>
Press Enter to send • Powered by Templates
</div>
</div>
`,
})
class TemplateCustomInputComponent {
inputValue = "";
constructor(private chat: ChatState) {}
sendMessage() {
const value = this.inputValue.trim();
if (value) {
this.chat.submitInput(value);
this.inputValue = "";
}
}
}
export const CustomInputTemplate: Story = {
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "This input is created with a component!",
role: "user" as const,
},
{
id: "assistant-1",
content:
"Yes! Components with service injection provide complete control over the input area, including custom styling and behavior.",
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[inputComponent]="customInputComponent">
</copilot-chat-view>
</div>
`,
props: {
messages,
customInputComponent: TemplateCustomInputComponent,
},
};
},
};
export const CustomScrollButtonTemplate: Story = {
render: () => {
// Generate many messages to show scroll behavior
const messages: Message[] = [];
for (let i = 0; i < 25; i++) {
messages.push({
id: `msg-${i}`,
content:
i % 2 === 0
? `User message ${i}: Template-based scroll button demonstration!`
: `Assistant response ${i}: Templates provide maximum flexibility for UI customization, allowing you to create exactly the experience you want.`,
role: i % 2 === 0 ? "user" : "assistant",
} as Message);
}
// Simple click handler without DOM manipulation
const handleScroll = (onClick: () => void) => {
onClick();
};
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<!-- Custom Scroll Button Template -->
<ng-template #customScrollButton let-onClick="onClick">
<button
(click)="handleScroll(onClick)"
(mouseenter)="isHovered = true"
(mouseleave)="isHovered = false"
style="
position: fixed;
bottom: 100px;
right: 30px;
width: 64px;
height: 64px;
border-radius: 50%;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
border: 3px solid white;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1000;
"
[style.transform]="isHovered ? 'scale(1.15) rotate(360deg)' : 'scale(1) rotate(0deg)'">
<div style="
display: flex;
flex-direction: column;
align-items: center;
color: white;
">
<span style="font-size: 24px;">↓</span>
<span style="font-size: 10px; margin-top: -4px;">SCROLL</span>
</div>
</button>
</ng-template>
<copilot-chat-view
[messages]="messages"
[autoScroll]="false"
[scrollToBottomButtonTemplate]="customScrollButton">
</copilot-chat-view>
</div>
`,
props: {
messages,
handleScroll,
isHovered: false,
},
};
},
};
// Custom input component for combined story
@Component({
selector: "combined-custom-input",
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div
style="
background: linear-gradient(90deg, #00d2ff 0%, #3a47d5 100%);
padding: 20px;
"
>
<input
type="text"
[(ngModel)]="inputValue"
placeholder="Template-powered input..."
style="
width: calc(100% - 100px);
padding: 12px;
border: 2px solid white;
border-radius: 8px;
font-size: 16px;
outline: none;
"
(keyup.enter)="sendMessage()"
/>
<button
style="
width: 80px;
padding: 12px;
margin-left: 10px;
background: white;
color: #3a47d5;
border: none;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
"
(click)="sendMessage()"
>
Go
</button>
</div>
`,
})
class CombinedCustomInputComponent {
inputValue = "";
constructor(private chat: ChatState) {}
sendMessage() {
const value = this.inputValue.trim();
if (value) {
this.chat.submitInput(value);
this.inputValue = "";
}
}
}
export const AllTemplatesCombined: Story = {
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "Show me all templates working together!",
role: "user" as const,
},
{
id: "assistant-1",
content:
"Here you can see custom disclaimer, input, and scroll button templates all working in harmony!",
role: "assistant" as const,
},
{
id: "user-2",
content: "This is amazing flexibility!",
role: "user" as const,
},
{
id: "assistant-2",
content:
"Templates give you complete control over every aspect of the chat interface while maintaining the core functionality.",
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<!-- Combined Templates -->
<ng-template #disclaimer>
<div style="
text-align: center;
padding: 12px;
background: linear-gradient(90deg, #00d2ff 0%, #3a47d5 100%);
color: white;
font-size: 13px;
margin: 8px 16px;
border-radius: 8px;
">
🚀 All custom templates active!
</div>
</ng-template>
<ng-template #scrollBtn let-onClick="onClick">
<button
(click)="onClick()"
style="
position: fixed;
bottom: 90px;
right: 20px;
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(90deg, #00d2ff 0%, #3a47d5 100%);
border: 2px solid white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
cursor: pointer;
color: white;
font-size: 20px;
">
</button>
</ng-template>
<copilot-chat-view
[messages]="messages"
[autoScroll]="false"
[disclaimerTemplate]="disclaimer"
[inputComponent]="customInputComponent"
[scrollToBottomButtonTemplate]="scrollBtn">
</copilot-chat-view>
</div>
`,
props: {
messages,
customInputComponent: CombinedCustomInputComponent,
},
};
},
};
@@ -0,0 +1,353 @@
import type { Meta, StoryObj } from "@storybook/angular";
import { moduleMetadata } from "@storybook/angular";
import { CommonModule } from "@angular/common";
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
provideCopilotChatLabels,
provideCopilotKit,
} from "@copilotkit/angular";
import type { Message } from "@ag-ui/client";
const meta: Meta<CopilotChatView> = {
title: "UI/CopilotChatView/Basic Examples",
component: CopilotChatView,
decorators: [
moduleMetadata({
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: "Type a message...",
chatDisclaimerText:
"AI can make mistakes. Please verify important information.",
}),
],
}),
],
parameters: {
layout: "fullscreen",
},
};
export default meta;
type Story = StoryObj<CopilotChatView>;
// Default story
export const Default: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
import { Message } from '@ag-ui/client';
@Component({
selector: 'app-chat',
standalone: true,
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: 'Type a message...',
chatDisclaimerText: 'AI can make mistakes. Please verify important information.'
}),
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
(assistantMessageThumbsUp)="onThumbsUp($event)"
(assistantMessageThumbsDown)="onThumbsDown($event)">
</copilot-chat-view>
</div>
\`
})
export class ChatComponent {
messages: Message[] = [
{
id: 'user-1',
content: 'Hello! How can I integrate CopilotKit with my Angular app?',
role: 'user'
},
{
id: 'assistant-1',
content: \`To integrate CopilotKit with your Angular app, follow these steps:
1. Install the package:
\\\`\\\`\\\`bash
npm install @copilotkit/angular
\\\`\\\`\\\`
2. Import and configure in your component:
\\\`\\\`\\\`typescript
import { provideCopilotKit } from '@copilotkit/angular';
@Component({
providers: [provideCopilotKit({})]
})
\\\`\\\`\\\`
3. Use the chat components in your template!\`,
role: 'assistant'
},
{
id: 'user-2',
content: 'That looks great! Can I customize the appearance?',
role: 'user'
},
{
id: 'assistant-2',
content: 'Yes! CopilotKit is highly customizable. You can customize the appearance using Tailwind CSS classes or by providing your own custom components through the slot system.',
role: 'assistant'
}
];
onThumbsUp(event: any) {
alert('Thumbs up! You liked this message.');
console.log('Thumbs up event:', event);
}
onThumbsDown(event: any) {
alert('Thumbs down! You disliked this message.');
console.log('Thumbs down event:', event);
}
}`,
language: "typescript",
},
},
},
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "Hello! How can I integrate CopilotKit with my Angular app?",
role: "user" as const,
},
{
id: "assistant-1",
content: `To integrate CopilotKit with your Angular app, follow these steps:
1. Install the package:
\`\`\`bash
npm install @copilotkit/angular
\`\`\`
2. Import and configure in your component:
\`\`\`typescript
import { provideCopilotKit } from '@copilotkit/angular';
@Component({
providers: [provideCopilotKit({})]
})
\`\`\`
3. Use the chat components in your template!`,
role: "assistant" as const,
},
{
id: "user-2",
content: "That looks great! Can I customize the appearance?",
role: "user" as const,
},
{
id: "assistant-2",
content:
"Yes! CopilotKit is highly customizable. You can customize the appearance using Tailwind CSS classes or by providing your own custom components through the slot system.",
role: "assistant" as const,
},
];
const onThumbsUp = (event: any) => {
alert("Thumbs up! You liked this message.");
console.log("Thumbs up event:", event);
};
const onThumbsDown = (event: any) => {
alert("Thumbs down! You disliked this message.");
console.log("Thumbs down event:", event);
};
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
(assistantMessageThumbsUp)="onThumbsUp($event)"
(assistantMessageThumbsDown)="onThumbsDown($event)">
</copilot-chat-view>
</div>
`,
props: {
messages,
onThumbsUp,
onThumbsDown,
},
};
},
};
// Story with manual scroll
export const ManualScroll: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
CopilotChatView,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
import { Message } from '@ag-ui/client';
@Component({
selector: 'app-chat-scroll',
standalone: true,
imports: [CommonModule, CopilotChatView],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: 'Type a message...',
chatDisclaimerText: 'AI can make mistakes. Please verify important information.'
})
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[autoScroll]="false">
</copilot-chat-view>
</div>
\`
})
export class ChatScrollComponent {
messages: Message[] = [];
constructor() {
// Generate many messages to show scroll behavior
for (let i = 0; i < 20; i++) {
if (i % 2 === 0) {
this.messages.push({
id: \`user-\${i}\`,
content: \`User message \${i}: This is a test message to demonstrate scrolling behavior.\`,
role: 'user'
});
} else {
this.messages.push({
id: \`assistant-\${i}\`,
content: \`Assistant response \${i}: This is a longer response to demonstrate how the chat interface handles various message lengths and scrolling behavior when there are many messages in the conversation.\`,
role: 'assistant'
});
}
}
}
}`,
language: "typescript",
},
},
},
render: () => {
// Generate many messages to show scroll behavior
const messages: Message[] = [];
for (let i = 0; i < 20; i++) {
if (i % 2 === 0) {
messages.push({
id: `user-${i}`,
content: `User message ${i}: This is a test message to demonstrate scrolling behavior.`,
role: "user" as const,
});
} else {
messages.push({
id: `assistant-${i}`,
content: `Assistant response ${i}: This is a longer response to demonstrate how the chat interface handles various message lengths and scrolling behavior when there are many messages in the conversation.`,
role: "assistant" as const,
});
}
}
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[autoScroll]="false">
</copilot-chat-view>
</div>
`,
props: {
messages,
},
};
},
};
// Story with empty state
export const EmptyState: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import {
CopilotChatView,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
@Component({
selector: 'app-chat-empty',
standalone: true,
imports: [CopilotChatView],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: 'Type a message...',
chatDisclaimerText: 'AI can make mistakes. Please verify important information.'
})
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view [messages]="[]">
</copilot-chat-view>
</div>
\`
})
export class EmptyChatComponent {}`,
language: "typescript",
},
},
},
render: () => {
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="[]">
</copilot-chat-view>
</div>
`,
props: {},
};
},
};
@@ -0,0 +1,120 @@
import { Meta } from "@storybook/blocks";
<Meta title="Welcome" />
# CopilotKit Angular Components
Welcome to the **CopilotKit Angular Storybook**! This is the dedicated documentation and showcase for all Angular components in the CopilotKit ecosystem.
## What is CopilotKit?
CopilotKit is a comprehensive toolkit for building AI-powered copilot experiences in your applications. This Angular package provides native Angular components that integrate seamlessly with your Angular applications.
## Available Components
### Chat Components
- **CopilotChatInput** - A feature-rich chat input component with voice recording, tools menu, and file attachments
- **CopilotChatTextarea** - An auto-resizing textarea component for chat interfaces
- **CopilotChatView** - Complete chat interface with message history
- **CopilotChatUserMessage** - User message display component
- **CopilotAssistantMessage** - Assistant message display component
### Context Management
- **CopilotAgentContextProvider** - Provides agent context to child components
- **CopilotChatConfigurationProvider** - Configures chat components globally
## Getting Started
### Installation
```bash
npm install @copilotkit/angular
# or
pnpm add @copilotkit/angular
```
### Basic Usage
```typescript
import { Component } from "@angular/core";
import { CopilotChatInput } from "@copilotkit/angular";
import { provideCopilotChatConfiguration } from "@copilotkit/angular";
@Component({
selector: "app-chat",
standalone: true,
imports: [CopilotChatInput],
providers: [
provideCopilotChatConfiguration({
labels: {
chatInputPlaceholder: "Type a message...",
},
}),
],
template: `
<copilot-chat-input
(submitMessage)="onSubmitMessage($event)"
></copilot-chat-input>
`,
})
export class ChatComponent {
onSubmitMessage(message: string): void {
console.log("Message:", message);
}
}
```
## Features
### Angular 19+ Support
Built with the latest Angular features including:
- Standalone components
- Signals for reactive state management
- OnPush change detection
- TypeScript strict mode
### Full TypeScript Support
All components are fully typed with comprehensive TypeScript definitions.
### Customizable
- Custom CSS classes for styling
- Configurable labels and placeholders
- Event emitters for all interactions
- Flexible slot system for content projection
## Development
This Storybook runs alongside the [React Storybook](http://localhost:6006) on port 6007.
### Available Scripts
```bash
# Run Angular Storybook in development mode
pnpm --filter storybook-angular dev
# Build Angular Storybook for production
pnpm --filter storybook-angular build
```
## Links
- [CopilotKit Documentation](https://docs.copilotkit.ai)
- [GitHub Repository](https://github.com/CopilotKit/CopilotKit)
- [React Components Storybook](http://localhost:6006)
## Support
For issues, feature requests, or questions:
- [GitHub Issues](https://github.com/CopilotKit/CopilotKit/issues)
- [Discord Community](https://discord.gg/copilotkit)
---
_Built with Angular 19 and Storybook 8_
@@ -0,0 +1,35 @@
{
"extends": "@copilotkit/typescript-config/base.json",
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022", "dom"],
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"useDefineForClassFields": false,
"baseUrl": ".",
"paths": {
"@copilotkit/angular": ["../../../../packages/angular/src/index.ts"],
"@copilotkit/angular/*": ["../../../../packages/angular/src/*"],
"@copilotkit/a2ui-renderer": ["node_modules/@copilotkit/a2ui-renderer"],
"@copilotkit/a2ui-renderer/web-components": [
"node_modules/@copilotkit/a2ui-renderer/web-components"
],
"@copilotkit/a2ui-renderer/web-components/define": [
"node_modules/@copilotkit/a2ui-renderer/web-components/define"
],
"@copilotkit/core": ["node_modules/@copilotkit/core"],
"@copilotkit/shared": ["node_modules/@copilotkit/shared"]
},
"types": ["node"]
},
"include": ["**/*.ts", "**/*.tsx", ".storybook/**/*"],
"exclude": ["node_modules", "dist"]
}
+17
View File
@@ -0,0 +1,17 @@
# @copilotkit/docs
## 0.1.8
## 0.1.7
## 0.1.6
## 0.1.5
## 0.1.4
## 0.1.3
## 0.1.2
## 0.1.1
+102
View File
@@ -0,0 +1,102 @@
{
"$schema": "https://mintlify.com/schema.json",
"name": "CopilotKit vnext_experimental",
"theme": "mint",
"logo": {
"dark": "/logo/dark.svg",
"light": "/logo/light.svg"
},
"favicon": "/favicon.ico",
"colors": {
"primary": "#0D9373",
"light": "#07C983",
"dark": "#0D9373"
},
"topbarLinks": [
{
"name": "Support",
"url": "mailto:hi@copilotkit.ai"
}
],
"topbarCtaButton": {
"name": "Dashboard",
"url": "https://dashboard.copilotkit.ai"
},
"navigation": {
"anchors": [
{
"name": "Community",
"icon": "slack",
"url": "https://discord.gg/6dffbvGU3D"
},
{
"name": "Blog",
"icon": "newspaper",
"url": "https://copilotkit.ai/blog"
}
],
"groups": [
{
"group": "Get Started",
"pages": ["introduction"]
},
{
"group": "API Reference",
"pages": [
{
"group": "Core",
"pages": [
"reference/copilotkit-core",
"reference/proxied-copilot-runtime-agent",
"reference/frontend-tool"
]
},
{
"group": "React",
"pages": [
{
"group": "Providers",
"pages": [
"reference/copilotkit-provider",
"reference/copilot-chat-configuration-provider"
]
},
{
"group": "Components",
"pages": [
"reference/copilot-chat",
"reference/copilot-sidebar",
"reference/copilot-popup",
"reference/copilot-chat-message-view",
"reference/copilot-chat-input",
"reference/copilot-chat-assistant-message",
"reference/copilot-chat-user-message",
"reference/copilot-chat-scroll-view",
"reference/copilot-chat-suggestion-view",
"reference/copilot-chat-welcome-screen",
"reference/slot-system"
]
},
{
"group": "Hooks",
"pages": [
"reference/use-copilotkit",
"reference/use-agent",
"reference/use-agent-context",
"reference/use-frontend-tool",
"reference/use-human-in-the-loop",
"reference/use-render-tool-call"
]
}
]
}
]
}
]
},
"footerSocials": {
"x": "https://x.com/copilotkit",
"github": "https://github.com/copilotkit/copilotkit",
"linkedin": "https://www.linkedin.com/company/copilotkit"
}
}
+92
View File
@@ -0,0 +1,92 @@
---
title: CopilotKit VNext Documentation
description: "CopilotKit VNext Documentation"
---
## API Reference
### Core
<Card title="CopilotKitCore" icon="code" href="/reference/copilotkit-core">
Core CopilotKit class
</Card>
<Card title="CopilotRuntime" icon="server" href="/reference/copilot-runtime">
Host agents and expose runtime endpoints
</Card>
<Card
title="ProxiedCopilotRuntimeAgent"
icon="server"
href="/reference/proxied-copilot-runtime-agent"
>
Proxied runtime agent for CopilotKit
</Card>
<Card title="FrontendTool" icon="wrench" href="/reference/frontend-tool">
Frontend tool utilities
</Card>
### React
#### Providers
<Card
title="CopilotKitProvider"
icon="layer-group"
href="/reference/copilotkit-provider"
>
Main provider for CopilotKit React integration
</Card>
<Card
title="CopilotChatConfigurationProvider"
icon="cog"
href="/reference/copilot-chat-configuration-provider"
>
Configuration provider for CopilotChat
</Card>
#### Components
<Card title="CopilotChat" icon="comments" href="/reference/copilot-chat">
Chat component for CopilotKit
</Card>
#### Hooks
<Card title="useCopilotKit" icon="link" href="/reference/use-copilotkit">
Main hook for CopilotKit functionality
</Card>
<Card title="useAgent" icon="robot" href="/reference/use-agent">
Hook for agent interactions
</Card>
<Card
title="useAgentContext"
icon="square-check"
href="/reference/use-agent-context"
>
Hook for agent context management
</Card>
<Card title="useFrontendTool" icon="hammer" href="/reference/use-frontend-tool">
Hook for frontend tools
</Card>
<Card
title="useHumanInTheLoop"
icon="user-check"
href="/reference/use-human-in-the-loop"
>
Hook for human-in-the-loop functionality
</Card>
<Card
title="useRenderToolCall"
icon="paintbrush"
href="/reference/use-render-tool-call"
>
Hook for rendering tool calls
</Card>
+16
View File
@@ -0,0 +1,16 @@
{
"name": "@copilotkit/docs",
"version": "0.1.8",
"private": true,
"scripts": {
"dev": "mintlify dev --port 4000",
"lint": "mintlify broken-links",
"check-types": "echo 'No TypeScript compilation needed for Mintlify docs'"
},
"dependencies": {
"sharp": "^0.34.2"
},
"devDependencies": {
"mintlify": "^4.2.127"
}
}
@@ -0,0 +1,344 @@
---
title: CopilotChatAssistantMessage
description: "AI response rendering component with toolbar actions"
---
`CopilotChatAssistantMessage` is the default component used by [CopilotChatMessageView](/reference/copilot-chat-message-view) to render AI assistant responses. It handles markdown rendering, feedback collection, and tool call display.
## What is CopilotChatAssistantMessage?
The CopilotChatAssistantMessage component:
- Renders AI responses with full markdown support
- Provides a toolbar with copy, feedback, and action buttons
- Handles thumbs up/down feedback collection
- Supports text-to-speech (read aloud) functionality
- Displays tool call executions with visual feedback
- Built on the [slot system](/reference/slot-system) for deep customization
## Component Architecture
CopilotChatAssistantMessage provides slots for customizing each part of the message:
```mermaid
graph LR
AM[CopilotChatAssistantMessage] --> markdownRenderer
AM --> toolbar
AM --> copyButton
AM --> thumbsUpButton
AM --> thumbsDownButton
AM --> readAloudButton
AM --> regenerateButton
AM --> toolCallsView
```
### Slot Descriptions
| Slot | Description |
| ------------------ | ------------------------------------------------- |
| `markdownRenderer` | Renders the message content with markdown support |
| `toolbar` | Container for action buttons |
| `copyButton` | Button to copy message content |
| `thumbsUpButton` | Positive feedback button |
| `thumbsDownButton` | Negative feedback button |
| `readAloudButton` | Text-to-speech button |
| `regenerateButton` | Button to regenerate the response |
| `toolCallsView` | Renders tool call executions |
## Basic Usage
Customize assistant messages through the `messageView.assistantMessage` slot on [CopilotChat](/reference/copilot-chat):
```tsx
<CopilotChat
messageView={{
assistantMessage: {
className: "bg-slate-50 rounded-xl p-4",
onThumbsUp: (message) => trackFeedback(message.id, "positive"),
onThumbsDown: (message) => trackFeedback(message.id, "negative"),
},
}}
/>
```
## Feedback Callbacks
CopilotChatAssistantMessage provides callbacks for user feedback:
```tsx
<CopilotChat
messageView={{
assistantMessage: {
onThumbsUp: (message) => {
analytics.track("positive_feedback", {
messageId: message.id,
content: message.content,
});
},
onThumbsDown: (message) => {
analytics.track("negative_feedback", {
messageId: message.id,
content: message.content,
});
},
onRegenerate: (message) => {
console.log("Regenerating:", message.id);
},
onReadAloud: (message) => {
speechSynthesis.speak(new SpeechSynthesisUtterance(message.content));
},
},
}}
/>
```
### Callback Props
| Callback | Signature | Description |
| -------------- | ------------------------------------- | -------------------------------------- |
| `onThumbsUp` | `(message: AssistantMessage) => void` | Called when user clicks thumbs up |
| `onThumbsDown` | `(message: AssistantMessage) => void` | Called when user clicks thumbs down |
| `onRegenerate` | `(message: AssistantMessage) => void` | Called when user requests regeneration |
| `onReadAloud` | `(message: AssistantMessage) => void` | Called when user clicks read aloud |
## Slot Customization
CopilotChatAssistantMessage uses the [slot system](/reference/slot-system). Each slot accepts four types of values:
1. **Tailwind class string** - Add or override CSS classes
2. **Props object** - Pass additional props to the default component
3. **Custom component** - Replace the component entirely
4. **Nested sub-slots** - Drill down to customize child components
### Toolbar Customization
Control visibility and styling of the toolbar:
```tsx
<CopilotChat
messageView={{
assistantMessage: {
toolbar: "bg-gray-50 rounded-lg p-2",
},
}}
/>
```
To hide the entire toolbar:
```tsx
<CopilotChat
messageView={{
assistantMessage: {
toolbarVisible: false,
},
}}
/>
```
### Individual Button Customization
Customize specific toolbar buttons:
```tsx
<CopilotChat
messageView={{
assistantMessage: {
copyButton: "text-blue-500 hover:text-blue-700",
thumbsUpButton: "text-green-500 hover:text-green-700",
thumbsDownButton: "text-red-500 hover:text-red-700",
readAloudButton: "text-purple-500 hover:text-purple-700",
regenerateButton: "text-orange-500 hover:text-orange-700",
},
}}
/>
```
### Hiding Specific Buttons
Hide buttons by returning null:
```tsx
<CopilotChat
messageView={{
assistantMessage: {
readAloudButton: () => null,
regenerateButton: () => null,
},
}}
/>
```
Note: `thumbsUpButton` and `thumbsDownButton` only show when their respective callbacks (`onThumbsUp`, `onThumbsDown`) are provided.
### Markdown Renderer Customization
Customize how message content is rendered:
```tsx
<CopilotChat
messageView={{
assistantMessage: {
markdownRenderer: {
className: "prose prose-lg dark:prose-invert",
},
},
}}
/>
```
### Tool Calls View Customization
Customize how tool executions are displayed:
```tsx
<CopilotChat
messageView={{
assistantMessage: {
toolCallsView: "bg-gray-100 rounded-lg p-3",
},
}}
/>
```
## Replacing the Component
To completely replace the assistant message component:
```tsx
import { CopilotChatAssistantMessage } from "@copilotkit/react-core";
function CustomAssistantMessage({ message, isRunning, ...props }) {
return (
<div className="flex gap-3 items-start">
<Avatar src="/ai-avatar.png" />
<div className="flex-1">
<CopilotChatAssistantMessage
message={message}
isRunning={isRunning}
className="bg-white shadow-sm rounded-xl p-4"
{...props}
/>
</div>
</div>
);
}
<CopilotChat
messageView={{
assistantMessage: CustomAssistantMessage,
}}
/>;
```
### Using the Render Function
For full layout control, use the children render function:
```tsx
function CustomAssistantMessage(props) {
return (
<CopilotChatAssistantMessage {...props}>
{({ markdownRenderer, toolbar, toolCallsView, message }) => (
<div className="flex gap-4 p-4 bg-gradient-to-r from-blue-50 to-purple-50 rounded-xl">
<img src="/ai-avatar.png" className="w-8 h-8 rounded-full" />
<div className="flex-1">
<div className="text-sm text-gray-500 mb-1">AI Assistant</div>
{markdownRenderer}
{toolCallsView}
<div className="mt-2 border-t pt-2">{toolbar}</div>
</div>
</div>
)}
</CopilotChatAssistantMessage>
);
}
<CopilotChat
messageView={{
assistantMessage: CustomAssistantMessage,
}}
/>;
```
The render function receives:
| Property | Type | Description |
| ------------------ | ------------------ | ----------------------------- |
| `markdownRenderer` | `ReactElement` | The rendered markdown content |
| `toolbar` | `ReactElement` | The action buttons toolbar |
| `toolCallsView` | `ReactElement` | Tool call display |
| `copyButton` | `ReactElement` | Copy button |
| `thumbsUpButton` | `ReactElement` | Thumbs up button |
| `thumbsDownButton` | `ReactElement` | Thumbs down button |
| `readAloudButton` | `ReactElement` | Read aloud button |
| `regenerateButton` | `ReactElement` | Regenerate button |
| `message` | `AssistantMessage` | The message data |
| `isRunning` | `boolean` | Whether AI is generating |
| `toolbarVisible` | `boolean` | Whether toolbar should show |
## Examples
### Analytics Integration
```tsx
<CopilotChat
messageView={{
assistantMessage: {
onThumbsUp: (message) => {
analytics.track("ai_feedback", {
type: "positive",
messageId: message.id,
messageLength: message.content?.length,
timestamp: new Date().toISOString(),
});
},
onThumbsDown: (message) => {
analytics.track("ai_feedback", {
type: "negative",
messageId: message.id,
messageLength: message.content?.length,
timestamp: new Date().toISOString(),
});
},
},
}}
/>
```
### Minimal Toolbar
Show only the copy button:
```tsx
<CopilotChat
messageView={{
assistantMessage: {
thumbsUpButton: () => null,
thumbsDownButton: () => null,
readAloudButton: () => null,
regenerateButton: () => null,
},
}}
/>
```
### Custom Styling
```tsx
<CopilotChat
messageView={{
assistantMessage: {
className: "bg-white shadow-md rounded-2xl p-6 border border-gray-100",
markdownRenderer: "prose prose-blue max-w-none",
toolbar: "mt-4 pt-4 border-t border-gray-100",
},
}}
/>
```
## Related
- [CopilotChat](/reference/copilot-chat) - Parent component
- [CopilotChatMessageView](/reference/copilot-chat-message-view) - Message list component that uses assistant messages
- [Slot System](/reference/slot-system) - Deep dive into slot customization
@@ -0,0 +1,184 @@
---
title: CopilotChatConfigurationProvider
description: "CopilotChatConfigurationProvider API Reference"
---
`CopilotChatConfigurationProvider` is a React context provider that manages configuration for chat UI components,
including labels, agent ID, and thread ID. It enables customization of all text labels in the chat interface and
provides context for chat components to access configuration values.
## What is CopilotChatConfigurationProvider?
The CopilotChatConfigurationProvider:
- Manages UI labels for all chat components (placeholders, button labels, etc.), allowing for localization or
customization of the chat interface.
- Lets you to access the current `agentId` and `threadId`
- Supports nested configuration with proper inheritance
## Basic Usage
Wrap your chat components with the provider to customize configuration:
```tsx
import { CopilotChatConfigurationProvider } from "@copilotkit/react-core";
function App() {
return (
<CopilotChatConfigurationProvider
threadId="main-thread"
agentId="assistant"
labels={{
chatInputPlaceholder: "Ask me anything...",
assistantMessageToolbarCopyMessageLabel: "Copy response",
}}
>
{/* Your chat components */}
</CopilotChatConfigurationProvider>
);
}
```
## Props
### threadId
`string` **(required)**
A unique identifier for the conversation thread. This is used to maintain conversation history and context.
```tsx
<CopilotChatConfigurationProvider threadId="conversation-123">
{children}
</CopilotChatConfigurationProvider>
```
### agentId
`string` **(optional)**
The ID of the agent to use for this chat context. If not provided, defaults to `"default"`.
```tsx
<CopilotChatConfigurationProvider
threadId="thread-1"
agentId="customer-support"
>
{children}
</CopilotChatConfigurationProvider>
```
### labels
`Partial<CopilotChatLabels>` **(optional)**
Custom labels for chat UI elements. Any labels not provided will use the default values.
```tsx
<CopilotChatConfigurationProvider
threadId="thread-1"
labels={{
chatInputPlaceholder: "Type your message here...",
chatDisclaimerText: "AI responses may contain errors.",
assistantMessageToolbarRegenerateLabel: "Try again",
}}
>
{children}
</CopilotChatConfigurationProvider>
```
### children
`ReactNode` **(required)**
The React components that will have access to the chat configuration context.
## Using with CopilotChat
The `CopilotChat` component automatically creates its own `CopilotChatConfigurationProvider` internally. When using
`CopilotChat`, you can pass configuration directly as props:
```tsx
<CopilotChat
threadId="main-thread"
agentId="assistant"
labels={{
chatInputPlaceholder: "How can I help you today?",
}}
/>
```
### Priority System
When `CopilotChat` is used within an existing `CopilotChatConfigurationProvider`, values are merged with the following
priority (highest to lowest):
1. Props passed directly to `CopilotChat`
2. Values from the outer `CopilotChatConfigurationProvider`
3. Default values
Example:
```tsx
<CopilotChatConfigurationProvider agentId="outer-agent">
<CopilotChat
threadId="inner-thread"
// agentId inherits from outer: "outer-agent"
/>
</CopilotChatConfigurationProvider>
```
## Accessing Configuration
Use the `useCopilotChatConfiguration` hook to access configuration values in your components:
```tsx
import { useCopilotChatConfiguration } from "@copilotkit/react-core";
function MyComponent() {
const config = useCopilotChatConfiguration();
if (!config) {
// No provider found
return null;
}
return (
<div>
<p>Agent: {config.agentId}</p>
<p>Thread: {config.threadId}</p>
<p>Labels: {JSON.stringify(config.labels)}</p>
</div>
);
}
```
Note: The hook returns `null` if no provider is found in the component tree.
## Localization Example
The provider is ideal for implementing localization:
```tsx
import { useTranslation } from "react-i18next";
function LocalizedChat() {
const { t } = useTranslation();
return (
<CopilotChatConfigurationProvider
threadId="chat-1"
labels={{
chatInputPlaceholder: t("chat.input.placeholder"),
assistantMessageToolbarCopyMessageLabel: t("chat.assistant.copy"),
assistantMessageToolbarThumbsUpLabel: t("chat.assistant.thumbsUp"),
assistantMessageToolbarThumbsDownLabel: t("chat.assistant.thumbsDown"),
userMessageToolbarEditMessageLabel: t("chat.user.edit"),
chatDisclaimerText: t("chat.disclaimer"),
}}
>
<CopilotChat />
</CopilotChatConfigurationProvider>
);
}
```
@@ -0,0 +1,302 @@
---
title: CopilotChatInput
description: "Text input component with voice transcription and tools menu"
---
`CopilotChatInput` is the default input component used by [CopilotChat](/reference/copilot-chat). It provides a full-featured text input with support for voice transcription, slash commands, and customizable toolbar buttons.
## What is CopilotChatInput?
The CopilotChatInput component:
- Provides a rich text input with auto-expanding height
- Supports voice transcription via microphone button
- Includes a slash command menu for quick actions
- Shows a tools/add menu button for file uploads and custom actions
- Handles send and stop functionality during streaming
- Built on the [slot system](/reference/slot-system) for deep customization
## Component Architecture
CopilotChatInput provides slots for customizing each part of the input:
```mermaid
graph LR
Input[CopilotChatInput] --> textArea
Input --> sendButton
Input --> startTranscribeButton
Input --> cancelTranscribeButton
Input --> finishTranscribeButton
Input --> addMenuButton
Input --> audioRecorder
Input --> disclaimer
```
### Slot Descriptions
| Slot | Description |
| ------------------------ | ------------------------------------------- |
| `textArea` | The main textarea input element |
| `sendButton` | Send message or stop generation button |
| `startTranscribeButton` | Button to start voice recording |
| `cancelTranscribeButton` | Button to cancel voice recording |
| `finishTranscribeButton` | Button to finish voice recording |
| `addMenuButton` | Plus button for file uploads and tools menu |
| `audioRecorder` | Voice recording visualization component |
| `disclaimer` | Disclaimer text shown below the input |
## Basic Usage
Customize the input through the `input` prop on [CopilotChat](/reference/copilot-chat):
```tsx
<CopilotChat
input={{
className: "border-2 border-blue-200 rounded-xl",
sendButton: "bg-blue-500 hover:bg-blue-600",
}}
/>
```
## Input Modes
CopilotChatInput operates in three modes:
| Mode | Description |
| ------------ | ---------------------------------------- |
| `input` | Normal text input mode (default) |
| `transcribe` | Voice recording mode |
| `processing` | Shows loading spinner while transcribing |
The component automatically switches between modes based on user interaction with the transcription buttons.
## Slot Customization
CopilotChatInput uses the [slot system](/reference/slot-system). Each slot accepts four types of values:
1. **Tailwind class string** - Add or override CSS classes
2. **Props object** - Pass additional props to the default component
3. **Custom component** - Replace the component entirely
4. **Nested sub-slots** - Drill down to customize child components
### TextArea Customization
The `textArea` slot controls the main text input:
```tsx
<CopilotChat
input={{
textArea: {
className: "text-lg font-medium",
placeholder: "What's on your mind?",
},
}}
/>
```
### Send Button Customization
The `sendButton` slot controls the send/stop button:
```tsx
<CopilotChat
input={{
sendButton: "bg-green-500 hover:bg-green-600 text-white",
}}
/>
```
Or with a custom component:
```tsx
function CustomSendButton({ onClick, disabled, children }) {
return (
<button
onClick={onClick}
disabled={disabled}
className="px-4 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-lg"
>
{children}
</button>
);
}
<CopilotChat
input={{
sendButton: CustomSendButton,
}}
/>;
```
### Transcription Buttons
Customize the voice transcription buttons:
```tsx
<CopilotChat
input={{
startTranscribeButton: "text-blue-500",
cancelTranscribeButton: "text-red-500",
finishTranscribeButton: "text-green-500",
}}
/>
```
### Add Menu Button
The add menu button shows a dropdown with file upload and custom tool options:
```tsx
<CopilotChat
input={{
addMenuButton: "text-gray-500 hover:text-gray-700",
}}
/>
```
### Disclaimer Customization
The `disclaimer` slot shows helper text below the input:
```tsx
<CopilotChat
input={{
disclaimer: "text-xs text-gray-400 italic",
}}
/>
```
To hide the disclaimer entirely:
```tsx
<CopilotChat
input={{
disclaimer: () => null,
}}
/>
```
## Replacing the Component
To completely replace the input with your own component:
```tsx
import { CopilotChatInput } from "@copilotkit/react-core";
function CustomInput({ onSubmitMessage, isRunning, onStop, ...props }) {
return (
<div className="custom-input-wrapper">
<CopilotChatInput
onSubmitMessage={onSubmitMessage}
isRunning={isRunning}
onStop={onStop}
textArea="bg-gray-50"
sendButton="bg-blue-600"
{...props}
/>
</div>
);
}
<CopilotChat input={CustomInput} />;
```
### Using the Render Function
For full control, use the children render function pattern:
```tsx
function CustomInput(props) {
return (
<CopilotChatInput {...props}>
{({ textArea, sendButton, addMenuButton }) => (
<div className="flex items-center gap-2 p-4 bg-gray-100 rounded-xl">
{addMenuButton}
<div className="flex-1">{textArea}</div>
{sendButton}
</div>
)}
</CopilotChatInput>
);
}
<CopilotChat input={CustomInput} />;
```
The render function receives:
| Property | Type | Description |
| ------------------------ | ----------------------------------------- | ----------------------------- |
| `textArea` | `ReactElement` | The bound textarea component |
| `sendButton` | `ReactElement` | The bound send/stop button |
| `startTranscribeButton` | `ReactElement` | Start recording button |
| `cancelTranscribeButton` | `ReactElement` | Cancel recording button |
| `finishTranscribeButton` | `ReactElement` | Finish recording button |
| `addMenuButton` | `ReactElement` | The tools menu button |
| `audioRecorder` | `ReactElement` | Voice recording visualization |
| `disclaimer` | `ReactElement` | The disclaimer text |
| `mode` | `'input' \| 'transcribe' \| 'processing'` | Current input mode |
| `isRunning` | `boolean` | Whether AI is generating |
## Keyboard Shortcuts
| Shortcut | Action |
| ------------- | ------------------------------------ |
| `Enter` | Send message (or stop if generating) |
| `Shift+Enter` | Insert newline |
| `/` | Open slash command menu |
| `Escape` | Close slash command menu |
| `↑/↓` | Navigate slash menu items |
## Examples
### Custom Styled Input
```tsx
<CopilotChat
input={{
className: "shadow-lg",
textArea: "text-base placeholder:text-gray-400",
sendButton: "bg-indigo-600 hover:bg-indigo-700",
addMenuButton: "text-indigo-600",
}}
/>
```
### Hiding the Disclaimer
```tsx
<CopilotChat
input={{
disclaimer: () => null,
}}
/>
```
### Custom Disclaimer Text
Use labels to customize the disclaimer text:
```tsx
<CopilotChat
labels={{
chatDisclaimerText:
"AI responses may contain errors. Always verify important information.",
}}
/>
```
### Hide Transcription Button
```tsx
<CopilotChat
input={{
startTranscribeButton: () => null,
}}
/>
```
## Related
- [CopilotChat](/reference/copilot-chat) - Parent component that uses CopilotChatInput
- [Slot System](/reference/slot-system) - Deep dive into slot customization
@@ -0,0 +1,286 @@
---
title: CopilotChatMessageView
description: "Message list container component"
---
`CopilotChatMessageView` is the default component used by [CopilotChat](/reference/copilot-chat) to render the message list. It handles rendering user messages, assistant messages, activity messages, and custom message renderers with optimized memoization for performance.
## What is CopilotChatMessageView?
The CopilotChatMessageView component:
- Renders the conversation message list
- Handles user, assistant, and activity message types
- Supports custom message renderers for extensibility
- Optimizes re-renders with memoization
- Shows a typing cursor during streaming responses
- Built on the [slot system](/reference/slot-system) for deep customization
## Component Architecture
CopilotChatMessageView provides slots for customizing message rendering:
```mermaid
graph LR
MV[CopilotChatMessageView] --> assistantMessage
MV --> userMessage
MV --> cursor
```
### Slot Descriptions
| Slot | Description |
| ------------------ | ----------------------------------------------- |
| `assistantMessage` | Component for rendering assistant (AI) messages |
| `userMessage` | Component for rendering user messages |
| `cursor` | Typing indicator shown during streaming |
## Basic Usage
Customize the message view through the `messageView` prop on [CopilotChat](/reference/copilot-chat):
```tsx
<CopilotChat
messageView={{
className: "space-y-4 p-4",
assistantMessage: "bg-blue-50 rounded-lg",
userMessage: "bg-gray-100 rounded-lg",
}}
/>
```
## Slot Customization
CopilotChatMessageView uses the [slot system](/reference/slot-system). Each slot accepts four types of values:
1. **Tailwind class string** - Add or override CSS classes
2. **Props object** - Pass additional props to the default component
3. **Custom component** - Replace the component entirely
4. **Nested sub-slots** - Drill down to customize child components
### Assistant Message Customization
The `assistantMessage` slot controls how AI responses are rendered:
```tsx
<CopilotChat
messageView={{
assistantMessage: {
className: "bg-slate-50 border border-slate-200 rounded-xl p-4",
onThumbsUp: (message) => sendFeedback(message.id, "positive"),
onThumbsDown: (message) => sendFeedback(message.id, "negative"),
onRegenerate: (message) => regenerateResponse(message.id),
},
}}
/>
```
#### Assistant Message Sub-Slots
| Sub-Slot | Description |
| ------------------ | ------------------------------------------------- |
| `markdownRenderer` | Renders the message content with markdown support |
| `toolbar` | Container for action buttons |
| `copyButton` | Button to copy message content |
| `thumbsUpButton` | Positive feedback button |
| `thumbsDownButton` | Negative feedback button |
| `readAloudButton` | Text-to-speech button |
| `regenerateButton` | Button to regenerate the response |
| `toolCallsView` | Renders tool call executions |
For full details, see [CopilotChatAssistantMessage](/reference/copilot-chat-assistant-message).
### User Message Customization
The `userMessage` slot controls how user messages are rendered:
```tsx
<CopilotChat
messageView={{
userMessage: {
className: "bg-primary text-primary-foreground rounded-2xl px-4 py-2",
onEditMessage: ({ message }) => editMessage(message),
},
}}
/>
```
#### User Message Sub-Slots
| Sub-Slot | Description |
| ------------------ | ------------------------------------ |
| `messageRenderer` | Renders the message text content |
| `toolbar` | Container for action buttons |
| `copyButton` | Button to copy message content |
| `editButton` | Button to edit the message |
| `branchNavigation` | Navigation for conversation branches |
For full details, see [CopilotChatUserMessage](/reference/copilot-chat-user-message).
### Cursor Customization
The cursor appears while the AI is generating a response:
```tsx
<CopilotChat
messageView={{
cursor: "bg-blue-500 w-3 h-3",
}}
/>
```
Or with a custom component:
```tsx
function CustomCursor() {
return (
<div className="flex gap-1">
<span className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" />
<span className="w-2 h-2 bg-blue-500 rounded-full animate-bounce delay-100" />
<span className="w-2 h-2 bg-blue-500 rounded-full animate-bounce delay-200" />
</div>
);
}
<CopilotChat
messageView={{
cursor: CustomCursor,
}}
/>;
```
## Replacing the Message View
To completely replace the message view with your own component, pass a custom component to the `messageView` prop:
```tsx
import { CopilotChatMessageView } from "@copilotkit/react-core";
function CustomMessageView({ messages, isRunning, ...props }) {
return (
<div className="custom-message-list">
<div className="message-count text-sm text-muted-foreground mb-4">
{messages.length} messages
</div>
{/* Use the default implementation with customizations */}
<CopilotChatMessageView
messages={messages}
isRunning={isRunning}
className="space-y-6"
assistantMessage="bg-white shadow-sm rounded-xl p-4"
userMessage="bg-blue-500 text-white rounded-2xl"
{...props}
/>
{isRunning && (
<div className="text-center text-muted-foreground mt-4">
AI is thinking...
</div>
)}
</div>
);
}
<CopilotChat messageView={CustomMessageView} />;
```
### Using the Render Function
For even more control, use the children render function pattern:
```tsx
function CustomMessageView({ messages, isRunning }) {
return (
<CopilotChatMessageView messages={messages} isRunning={isRunning}>
{({ messageElements, messages, isRunning }) => (
<div className="custom-layout">
<header className="sticky top-0 bg-background p-2 border-b">
{messages.length} messages
</header>
<div className="messages p-4">{messageElements}</div>
{isRunning && (
<footer className="sticky bottom-0 bg-background p-2">
Generating response...
</footer>
)}
</div>
)}
</CopilotChatMessageView>
);
}
<CopilotChat messageView={CustomMessageView} />;
```
The render function receives:
| Property | Type | Description |
| ----------------- | ---------------- | ------------------------------- |
| `messageElements` | `ReactElement[]` | Pre-rendered message components |
| `messages` | `Message[]` | Raw message data |
| `isRunning` | `boolean` | Whether AI is generating |
## Examples
### Styling All Messages
```tsx
<CopilotChat
messageView={{
className: "space-y-6 p-4",
assistantMessage: "bg-white shadow-sm rounded-xl p-4",
userMessage: "bg-blue-500 text-white rounded-2xl px-4 py-2",
}}
/>
```
### Adding Feedback Handlers
```tsx
<CopilotChat
messageView={{
assistantMessage: {
onThumbsUp: (message) => {
analytics.track("positive_feedback", { messageId: message.id });
},
onThumbsDown: (message) => {
analytics.track("negative_feedback", { messageId: message.id });
},
},
}}
/>
```
### Custom Assistant Message Component
Replace the assistant message component entirely:
```tsx
function CustomAssistantMessage({ message, isRunning }) {
return (
<div className="flex gap-3">
<Avatar src="/bot-avatar.png" />
<div className="flex-1">
<Markdown>{message.content}</Markdown>
{isRunning && <TypingIndicator />}
</div>
</div>
);
}
<CopilotChat
messageView={{
assistantMessage: CustomAssistantMessage,
}}
/>;
```
## Related
- [CopilotChat](/reference/copilot-chat) - Parent component that uses CopilotChatMessageView
- [CopilotChatAssistantMessage](/reference/copilot-chat-assistant-message) - Assistant message customization
- [CopilotChatUserMessage](/reference/copilot-chat-user-message) - User message customization
- [Slot System](/reference/slot-system) - Deep dive into slot customization
@@ -0,0 +1,205 @@
---
title: CopilotChatScrollView
description: "Scrollable message container with auto-scroll behavior"
---
`CopilotChatScrollView` is the default scroll container used by [CopilotChat](/reference/copilot-chat). It handles auto-scrolling during message streaming, provides a scroll-to-bottom button, and displays a gradient fade (feather) overlay.
## What is CopilotChatScrollView?
The CopilotChatScrollView component:
- Provides a scrollable container for the message list
- Auto-scrolls to bottom during AI response streaming
- Shows a scroll-to-bottom button when scrolled up
- Includes a gradient "feather" overlay for visual polish
- Built on the [slot system](/reference/slot-system) for deep customization
## Component Architecture
CopilotChatScrollView provides slots for customizing its visual elements:
```mermaid
graph LR
SV[CopilotChatScrollView] --> scrollToBottomButton
SV --> feather
```
### Slot Descriptions
| Slot | Description |
| ---------------------- | ------------------------------------------------------ |
| `scrollToBottomButton` | Button that appears when scrolled up from bottom |
| `feather` | Gradient fade overlay at the bottom of the scroll area |
## Basic Usage
Customize the scroll view through the `scrollView` prop on [CopilotChat](/reference/copilot-chat):
```tsx
<CopilotChat
scrollView={{
scrollToBottomButton: "bg-blue-500 hover:bg-blue-600",
}}
/>
```
## Auto-Scroll Behavior
By default, CopilotChatScrollView automatically scrolls to the bottom when:
- New messages are added to the conversation
- The AI is streaming a response
- The user is already near the bottom of the scroll area
To disable auto-scroll:
```tsx
<CopilotChat autoScroll={false} />
```
When auto-scroll is disabled, users must manually scroll to see new messages. The scroll-to-bottom button will appear when new content is available below the viewport.
## Slot Customization
CopilotChatScrollView uses the [slot system](/reference/slot-system). Each slot accepts four types of values:
1. **Tailwind class string** - Add or override CSS classes
2. **Props object** - Pass additional props to the default component
3. **Custom component** - Replace the component entirely
4. **Nested sub-slots** - Drill down to customize child components
### Scroll-to-Bottom Button Customization
Style the scroll-to-bottom button:
```tsx
<CopilotChat
scrollView={{
scrollToBottomButton: "bg-blue-500 shadow-xl rounded-full",
}}
/>
```
Or with a custom component:
```tsx
function CustomScrollButton({ onClick }) {
return (
<button
onClick={onClick}
className="px-4 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-lg shadow-lg"
>
Jump to latest
</button>
);
}
<CopilotChat
scrollView={{
scrollToBottomButton: CustomScrollButton,
}}
/>;
```
### Feather (Gradient Overlay) Customization
The feather provides a gradient fade at the bottom of the scroll area:
```tsx
<CopilotChat
scrollView={{
feather: "from-blue-50 via-blue-50 to-transparent",
}}
/>
```
To remove the feather entirely:
```tsx
<CopilotChat
scrollView={{
feather: () => null,
}}
/>
```
## Replacing the Scroll View
To completely replace the scroll view with your own component:
```tsx
import { CopilotChatView } from "@copilotkit/react-core";
function CustomScrollView({ children, autoScroll, ...props }) {
return (
<CopilotChatView.ScrollView
autoScroll={autoScroll}
scrollToBottomButton="bg-indigo-600"
feather="from-gray-50 via-gray-50 to-transparent"
{...props}
>
{children}
</CopilotChatView.ScrollView>
);
}
<CopilotChat scrollView={CustomScrollView} />;
```
## Examples
### Custom Scroll Button
```tsx
<CopilotChat
scrollView={{
scrollToBottomButton: {
className:
"bg-gradient-to-r from-pink-500 to-orange-500 text-white shadow-xl",
},
}}
/>
```
### Dark Mode Feather
```tsx
<CopilotChat
scrollView={{
feather: "from-gray-900 via-gray-900 to-transparent",
}}
/>
```
### Removing Visual Elements
Remove both the scroll button and feather for a minimal look:
```tsx
<CopilotChat
scrollView={{
scrollToBottomButton: () => null,
feather: () => null,
}}
/>
```
### Custom Themed Scroll View
```tsx
<CopilotChat
className="bg-slate-50"
scrollView={{
className: "bg-slate-50",
scrollToBottomButton: "bg-slate-700 hover:bg-slate-800 text-white",
feather: "from-slate-50 via-slate-50 to-transparent",
}}
/>
```
## Related
- [CopilotChat](/reference/copilot-chat) - Parent component that uses CopilotChatScrollView
- [CopilotChatMessageView](/reference/copilot-chat-message-view) - Message list rendered inside the scroll view
- [Slot System](/reference/slot-system) - Deep dive into slot customization
@@ -0,0 +1,244 @@
---
title: CopilotChatSuggestionView
description: "Clickable suggestion chips component"
---
`CopilotChatSuggestionView` is the default component used by [CopilotChat](/reference/copilot-chat) to render clickable suggestion chips. These chips provide quick actions that users can click to send predefined messages.
## What is CopilotChatSuggestionView?
The CopilotChatSuggestionView component:
- Renders a list of clickable suggestion chips
- Supports loading states for individual suggestions
- Handles click events to trigger message sending
- Displays suggestions in a flexible wrapped layout
- Built on the [slot system](/reference/slot-system) for deep customization
## Component Architecture
CopilotChatSuggestionView provides slots for customizing the container and individual chips:
```mermaid
graph LR
SV[CopilotChatSuggestionView] --> container
SV --> suggestion
```
### Slot Descriptions
| Slot | Description |
| ------------ | --------------------------------------------------- |
| `container` | The outer container that holds all suggestion chips |
| `suggestion` | Individual suggestion chip (pill button) |
## Basic Usage
Customize suggestions through the `suggestionView` prop on [CopilotChat](/reference/copilot-chat):
```tsx
<CopilotChat
suggestionView={{
container: "gap-4",
suggestion: "bg-blue-100 hover:bg-blue-200 text-blue-800",
}}
/>
```
## How Suggestions Work
Suggestions are managed through the `useSuggestions` hook, which:
1. Receives suggestions from the AI agent
2. Displays them as clickable chips
3. Triggers message sending when clicked
4. Shows loading states during processing
You don't need to manage suggestions manually - CopilotChat handles this automatically.
## Slot Customization
CopilotChatSuggestionView uses the [slot system](/reference/slot-system). Each slot accepts four types of values:
1. **Tailwind class string** - Add or override CSS classes
2. **Props object** - Pass additional props to the default component
3. **Custom component** - Replace the component entirely
4. **Nested sub-slots** - Drill down to customize child components
### Container Customization
Style the suggestions container:
```tsx
<CopilotChat
suggestionView={{
container: "flex-wrap gap-2 justify-center",
}}
/>
```
### Suggestion Chip Customization
Style individual suggestion chips:
```tsx
<CopilotChat
suggestionView={{
suggestion:
"bg-gradient-to-r from-blue-500 to-purple-500 text-white px-4 py-2 rounded-full",
}}
/>
```
Or with a props object:
```tsx
<CopilotChat
suggestionView={{
suggestion: {
className: "bg-indigo-100 text-indigo-800 font-medium",
},
}}
/>
```
## Replacing the Component
To completely replace the suggestion view with your own component:
```tsx
import { CopilotChatSuggestionView } from "@copilotkit/react-core";
function CustomSuggestionView({ suggestions, onSelectSuggestion, ...props }) {
return (
<div className="custom-suggestions-wrapper">
<CopilotChatSuggestionView
suggestions={suggestions}
onSelectSuggestion={onSelectSuggestion}
container="gap-3"
suggestion="bg-white shadow-md hover:shadow-lg rounded-xl px-4 py-2"
{...props}
/>
</div>
);
}
<CopilotChat suggestionView={CustomSuggestionView} />;
```
### Using the Render Function
For full layout control, use the children render function:
```tsx
function CustomSuggestionView(props) {
return (
<CopilotChatSuggestionView {...props}>
{({ suggestions, onSelectSuggestion }) => (
<div className="grid grid-cols-2 gap-2 p-4">
{suggestions.map((suggestion, index) => (
<button
key={index}
onClick={() => onSelectSuggestion(suggestion, index)}
className="p-3 bg-white border rounded-lg hover:bg-gray-50 text-left"
>
{suggestion.title}
</button>
))}
</div>
)}
</CopilotChatSuggestionView>
);
}
<CopilotChat suggestionView={CustomSuggestionView} />;
```
The render function receives:
| Property | Type | Description |
| -------------------- | ----------------------------- | ---------------------------------------- |
| `container` | `ReactElement` | The bound container element |
| `suggestion` | `ReactElement` | A sample bound suggestion chip |
| `suggestions` | `Suggestion[]` | Array of suggestion objects |
| `onSelectSuggestion` | `(suggestion, index) => void` | Callback when suggestion is clicked |
| `loadingIndexes` | `ReadonlyArray<number>` | Indexes of suggestions currently loading |
## Loading States
Individual suggestions can show loading states when clicked:
```tsx
// Suggestions automatically show loading states when:
// 1. The suggestion is clicked and processing
// 2. The suggestion has isLoading: true in its data
```
The loading state is indicated by a spinner or visual feedback on the chip.
## Examples
### Pill-Style Suggestions
```tsx
<CopilotChat
suggestionView={{
container: "flex flex-wrap gap-2",
suggestion:
"rounded-full bg-gray-100 hover:bg-gray-200 px-4 py-1.5 text-sm",
}}
/>
```
### Card-Style Suggestions
```tsx
<CopilotChat
suggestionView={{
container: "grid grid-cols-2 gap-3",
suggestion:
"bg-white shadow-sm border rounded-xl p-3 hover:shadow-md text-left",
}}
/>
```
### Colorful Gradient Chips
```tsx
<CopilotChat
suggestionView={{
suggestion:
"bg-gradient-to-r from-pink-500 to-orange-500 text-white hover:from-pink-600 hover:to-orange-600",
}}
/>
```
### Minimal Style
```tsx
<CopilotChat
suggestionView={{
container: "gap-1",
suggestion:
"text-blue-600 hover:text-blue-800 underline underline-offset-2 bg-transparent",
}}
/>
```
### Centered Suggestions
```tsx
<CopilotChat
suggestionView={{
container: "flex justify-center flex-wrap gap-2",
suggestion:
"bg-blue-50 text-blue-700 hover:bg-blue-100 rounded-lg px-3 py-1.5",
}}
/>
```
## Related
- [CopilotChat](/reference/copilot-chat) - Parent component that uses CopilotChatSuggestionView
- [CopilotChatWelcomeScreen](/reference/copilot-chat-welcome-screen) - Welcome screen that displays suggestions
- [Slot System](/reference/slot-system) - Deep dive into slot customization
@@ -0,0 +1,376 @@
---
title: CopilotChatUserMessage
description: "User message rendering component with edit and branch navigation"
---
`CopilotChatUserMessage` is the default component used by [CopilotChatMessageView](/reference/copilot-chat-message-view) to render user messages. It handles message display, editing functionality, and branch navigation for conversation history.
## What is CopilotChatUserMessage?
The CopilotChatUserMessage component:
- Renders user messages in a styled bubble
- Provides a toolbar with copy and edit buttons
- Supports message editing functionality
- Handles branch navigation for conversation forks
- Built on the [slot system](/reference/slot-system) for deep customization
## Component Architecture
CopilotChatUserMessage provides slots for customizing each part of the message:
```mermaid
graph LR
UM[CopilotChatUserMessage] --> messageRenderer
UM --> toolbar
UM --> copyButton
UM --> editButton
UM --> branchNavigation
```
### Slot Descriptions
| Slot | Description |
| ------------------ | ----------------------------------------------- |
| `messageRenderer` | Renders the message text content |
| `toolbar` | Container for action buttons (appears on hover) |
| `copyButton` | Button to copy message content |
| `editButton` | Button to edit the message |
| `branchNavigation` | Navigation controls for conversation branches |
## Basic Usage
Customize user messages through the `messageView.userMessage` slot on [CopilotChat](/reference/copilot-chat):
```tsx
<CopilotChat
messageView={{
userMessage: {
className: "bg-blue-500 text-white rounded-2xl",
onEditMessage: ({ message }) => handleEdit(message),
},
}}
/>
```
## Callbacks
CopilotChatUserMessage provides callbacks for user interactions:
```tsx
<CopilotChat
messageView={{
userMessage: {
onEditMessage: ({ message }) => {
// Open edit modal or inline editor
setEditingMessage(message);
},
onSwitchToBranch: ({ message, branchIndex, numberOfBranches }) => {
// Switch to a different conversation branch
switchToBranch(message.id, branchIndex);
},
},
}}
/>
```
### Callback Props
| Callback | Signature | Description |
| ------------------ | ------------------------------------------------------ | ------------------------------------------- |
| `onEditMessage` | `({ message }) => void` | Called when user clicks the edit button |
| `onSwitchToBranch` | `({ message, branchIndex, numberOfBranches }) => void` | Called when user navigates between branches |
## Slot Customization
CopilotChatUserMessage uses the [slot system](/reference/slot-system). Each slot accepts four types of values:
1. **Tailwind class string** - Add or override CSS classes
2. **Props object** - Pass additional props to the default component
3. **Custom component** - Replace the component entirely
4. **Nested sub-slots** - Drill down to customize child components
### Message Renderer Customization
Style the message bubble:
```tsx
<CopilotChat
messageView={{
userMessage: {
messageRenderer:
"bg-gradient-to-r from-blue-500 to-purple-500 text-white",
},
}}
/>
```
### Toolbar Customization
The toolbar appears on hover and contains action buttons:
```tsx
<CopilotChat
messageView={{
userMessage: {
toolbar: "bg-gray-50 rounded-lg p-1",
},
}}
/>
```
### Individual Button Customization
Customize specific toolbar buttons:
```tsx
<CopilotChat
messageView={{
userMessage: {
copyButton: "text-gray-500 hover:text-gray-700",
editButton: "text-blue-500 hover:text-blue-700",
},
}}
/>
```
### Hiding Buttons
Hide buttons by returning null:
```tsx
<CopilotChat
messageView={{
userMessage: {
editButton: () => null,
branchNavigation: () => null,
},
}}
/>
```
Note: The `editButton` only shows when `onEditMessage` callback is provided.
### Branch Navigation Customization
Customize the branch navigation controls:
```tsx
<CopilotChat
messageView={{
userMessage: {
branchNavigation: "bg-gray-100 rounded-lg px-2",
},
}}
/>
```
Branch navigation only appears when there are multiple branches (conversation forks) available.
## Replacing the Component
To completely replace the user message component:
```tsx
import { CopilotChatUserMessage } from "@copilotkit/react-core";
function CustomUserMessage({ message, ...props }) {
return (
<div className="flex gap-3 items-start justify-end">
<div className="flex-1">
<CopilotChatUserMessage
message={message}
className="bg-blue-600 text-white"
{...props}
/>
</div>
<Avatar src="/user-avatar.png" />
</div>
);
}
<CopilotChat
messageView={{
userMessage: CustomUserMessage,
}}
/>;
```
### Using the Render Function
For full layout control, use the children render function:
```tsx
function CustomUserMessage(props) {
return (
<CopilotChatUserMessage {...props}>
{({ messageRenderer, toolbar, message }) => (
<div className="flex flex-col items-end gap-1">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-400">You</span>
<span className="text-xs text-gray-400">
{new Date(message.createdAt).toLocaleTimeString()}
</span>
</div>
{messageRenderer}
{toolbar}
</div>
)}
</CopilotChatUserMessage>
);
}
<CopilotChat
messageView={{
userMessage: CustomUserMessage,
}}
/>;
```
The render function receives:
| Property | Type | Description |
| ------------------ | -------------- | ---------------------------- |
| `messageRenderer` | `ReactElement` | The rendered message content |
| `toolbar` | `ReactElement` | The action buttons toolbar |
| `copyButton` | `ReactElement` | Copy button |
| `editButton` | `ReactElement` | Edit button |
| `branchNavigation` | `ReactElement` | Branch navigation controls |
| `message` | `UserMessage` | The message data |
| `branchIndex` | `number` | Current branch index |
| `numberOfBranches` | `number` | Total number of branches |
## Branch Navigation
When users edit messages and regenerate responses, CopilotKit creates conversation branches. The branch navigation allows users to switch between these alternative conversation paths:
```tsx
<CopilotChat
messageView={{
userMessage: {
onSwitchToBranch: ({ message, branchIndex, numberOfBranches }) => {
console.log(
`Switching to branch ${branchIndex + 1} of ${numberOfBranches}`,
);
// Your branch switching logic
},
},
}}
/>
```
The branch navigation shows:
- Previous/Next arrows to navigate between branches
- Current branch indicator (e.g., "2/3")
## Examples
### Chat Bubble Style
```tsx
<CopilotChat
messageView={{
userMessage: {
className: "items-end",
messageRenderer:
"bg-blue-600 text-white rounded-2xl px-4 py-2 max-w-[75%]",
toolbar: "opacity-0 group-hover:opacity-100 transition-opacity",
},
}}
/>
```
### With Edit Functionality
```tsx
function ChatWithEdit() {
const [editingMessage, setEditingMessage] = useState(null);
return (
<>
<CopilotChat
messageView={{
userMessage: {
onEditMessage: ({ message }) => setEditingMessage(message),
editButton: "text-blue-500 hover:text-blue-700",
},
}}
/>
{editingMessage && (
<EditMessageModal
message={editingMessage}
onClose={() => setEditingMessage(null)}
/>
)}
</>
);
}
```
### Minimal Style
Hide all toolbar elements for a clean look:
```tsx
<CopilotChat
messageView={{
userMessage: {
toolbar: () => null,
},
}}
/>
```
### Custom Message with Avatar
```tsx
function UserMessageWithAvatar(props) {
return (
<CopilotChatUserMessage {...props}>
{({ messageRenderer, toolbar }) => (
<div className="flex items-start gap-3 justify-end">
<div className="flex flex-col items-end">
{messageRenderer}
{toolbar}
</div>
<img
src="/user-avatar.png"
alt="You"
className="w-8 h-8 rounded-full"
/>
</div>
)}
</CopilotChatUserMessage>
);
}
<CopilotChat
messageView={{
userMessage: UserMessageWithAvatar,
}}
/>;
```
### Styled for Dark Mode
```tsx
<CopilotChat
messageView={{
userMessage: {
messageRenderer: "bg-blue-600 text-white dark:bg-blue-500",
toolbar: "text-gray-400 dark:text-gray-500",
copyButton: "hover:text-white dark:hover:text-gray-300",
editButton: "hover:text-white dark:hover:text-gray-300",
},
}}
/>
```
## Related
- [CopilotChat](/reference/copilot-chat) - Parent component
- [CopilotChatMessageView](/reference/copilot-chat-message-view) - Message list component that uses user messages
- [CopilotChatAssistantMessage](/reference/copilot-chat-assistant-message) - Counterpart for AI messages
- [Slot System](/reference/slot-system) - Deep dive into slot customization
@@ -0,0 +1,292 @@
---
title: CopilotChatWelcomeScreen
description: "Initial empty state and welcome message component"
---
`CopilotChatWelcomeScreen` is the default component displayed by [CopilotChat](/reference/copilot-chat) when there are no messages. It provides a welcoming introduction with an input field and optional suggestion chips.
## What is CopilotChatWelcomeScreen?
The CopilotChatWelcomeScreen component:
- Displays when the conversation is empty (no messages)
- Shows a customizable welcome message
- Includes the chat input for starting conversations
- Displays suggestion chips for quick actions
- Built on the [slot system](/reference/slot-system) for deep customization
## Component Architecture
CopilotChatWelcomeScreen provides a slot for the welcome message and receives input and suggestions as props:
```mermaid
graph LR
WS[CopilotChatWelcomeScreen] --> welcomeMessage
WS --> input[input prop]
WS --> suggestionView[suggestionView prop]
```
### Slot Descriptions
| Slot/Prop | Description |
| ---------------- | --------------------------------------------------- |
| `welcomeMessage` | The greeting text or component displayed at the top |
| `input` | The chat input component (passed as a prop) |
| `suggestionView` | Suggestion chips component (passed as a prop) |
## Basic Usage
Customize the welcome screen through the `welcomeScreen` prop on [CopilotChat](/reference/copilot-chat):
```tsx
<CopilotChat
welcomeScreen={{
className: "bg-gradient-to-b from-blue-50 to-white",
welcomeMessage: "text-2xl font-bold text-blue-900",
}}
/>
```
## Customizing the Welcome Message
The simplest way to customize the welcome message is through labels:
```tsx
<CopilotChat
labels={{
welcomeMessage: "Hello! I'm your AI assistant. How can I help you today?",
}}
/>
```
Or style the welcome message component:
```tsx
<CopilotChat
welcomeScreen={{
welcomeMessage: {
className:
"text-3xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent",
},
}}
/>
```
## Disabling the Welcome Screen
To skip the welcome screen and show an empty chat directly:
```tsx
<CopilotChat welcomeScreen={false} />
```
## Slot Customization
CopilotChatWelcomeScreen uses the [slot system](/reference/slot-system). Each slot accepts four types of values:
1. **Tailwind class string** - Add or override CSS classes
2. **Props object** - Pass additional props to the default component
3. **Custom component** - Replace the component entirely
4. **Nested sub-slots** - Drill down to customize child components
### Welcome Message Customization
Style the welcome message:
```tsx
<CopilotChat
welcomeScreen={{
welcomeMessage: "text-4xl font-extrabold tracking-tight",
}}
/>
```
Or with a custom component:
```tsx
function CustomWelcomeMessage() {
return (
<div className="text-center">
<img src="/logo.svg" alt="Logo" className="w-16 h-16 mx-auto mb-4" />
<h1 className="text-2xl font-bold">Welcome to AI Assistant</h1>
<p className="text-gray-500 mt-2">Ask me anything about your project</p>
</div>
);
}
<CopilotChat
welcomeScreen={{
welcomeMessage: CustomWelcomeMessage,
}}
/>;
```
## Replacing the Welcome Screen
To completely replace the welcome screen with your own component:
```tsx
import { CopilotChatView } from "@copilotkit/react-core";
function CustomWelcomeScreen({ input, suggestionView }) {
return (
<div className="flex flex-col items-center justify-center h-full bg-gradient-to-b from-indigo-50 to-white p-8">
<img src="/mascot.svg" alt="AI Mascot" className="w-32 h-32 mb-6" />
<h1 className="text-3xl font-bold text-gray-900 mb-2">Hi there!</h1>
<p className="text-gray-600 text-center max-w-md mb-8">
I'm your AI assistant. I can help you with coding, research, writing,
and much more. What would you like to explore?
</p>
<div className="w-full max-w-2xl">{input}</div>
<div className="mt-6">{suggestionView}</div>
</div>
);
}
<CopilotChat welcomeScreen={CustomWelcomeScreen} />;
```
### Using the Render Function
For full layout control while keeping the default components:
```tsx
function CustomWelcomeScreen(props) {
return (
<CopilotChatView.WelcomeScreen {...props}>
{({ welcomeMessage, input, suggestionView }) => (
<div className="flex flex-col lg:flex-row h-full">
<div className="lg:w-1/2 bg-indigo-600 text-white p-12 flex items-center justify-center">
<div className="max-w-md">
<h1 className="text-4xl font-bold mb-4">AI Assistant</h1>
<p className="text-indigo-200">
Your intelligent companion for coding, writing, and
problem-solving.
</p>
</div>
</div>
<div className="lg:w-1/2 p-12 flex flex-col items-center justify-center">
<div className="w-full max-w-md">
{welcomeMessage}
<div className="mt-8">{input}</div>
<div className="mt-6">{suggestionView}</div>
</div>
</div>
</div>
)}
</CopilotChatView.WelcomeScreen>
);
}
<CopilotChat welcomeScreen={CustomWelcomeScreen} />;
```
The render function receives:
| Property | Type | Description |
| ---------------- | -------------- | ------------------------------- |
| `welcomeMessage` | `ReactElement` | The rendered welcome message |
| `input` | `ReactElement` | The chat input component |
| `suggestionView` | `ReactElement` | The suggestions chips component |
## Examples
### Branded Welcome Screen
```tsx
<CopilotChat
labels={{
welcomeMessage: "Welcome to Acme AI Assistant",
}}
welcomeScreen={{
className: "bg-brand-50",
welcomeMessage: "text-brand-900 text-3xl font-display",
}}
/>
```
### Minimal Welcome
```tsx
<CopilotChat
labels={{
welcomeMessage: "How can I help?",
}}
welcomeScreen={{
welcomeMessage: "text-lg text-gray-500 font-normal",
}}
/>
```
### Welcome with Custom Layout
```tsx
function CenteredWelcome({ input, suggestionView }) {
return (
<div className="flex flex-col items-center justify-center h-full p-8">
<div className="animate-pulse mb-8">
<div className="w-20 h-20 bg-gradient-to-br from-blue-400 to-purple-500 rounded-full" />
</div>
<h1 className="text-2xl font-semibold text-gray-900 mb-1">
Ready to help
</h1>
<p className="text-gray-500 mb-8">Start a conversation below</p>
<div className="w-full max-w-xl">{input}</div>
<div className="mt-4 flex flex-wrap justify-center gap-2">
{suggestionView}
</div>
</div>
);
}
<CopilotChat welcomeScreen={CenteredWelcome} />;
```
### Welcome Screen with Feature List
```tsx
function FeatureWelcome({ input, suggestionView }) {
return (
<div className="flex flex-col items-center justify-center h-full p-8">
<h1 className="text-3xl font-bold mb-8">AI Assistant</h1>
<div className="grid grid-cols-3 gap-4 mb-8 max-w-2xl">
<div className="text-center p-4">
<div className="text-2xl mb-2">💻</div>
<div className="font-medium">Code Help</div>
</div>
<div className="text-center p-4">
<div className="text-2xl mb-2">📝</div>
<div className="font-medium">Writing</div>
</div>
<div className="text-center p-4">
<div className="text-2xl mb-2">🔍</div>
<div className="font-medium">Research</div>
</div>
</div>
<div className="w-full max-w-xl">{input}</div>
<div className="mt-4">{suggestionView}</div>
</div>
);
}
<CopilotChat welcomeScreen={FeatureWelcome} />;
```
## Related
- [CopilotChat](/reference/copilot-chat) - Parent component that uses CopilotChatWelcomeScreen
- [CopilotChatInput](/reference/copilot-chat-input) - Input component displayed in welcome screen
- [CopilotChatSuggestionView](/reference/copilot-chat-suggestion-view) - Suggestions displayed in welcome screen
- [Slot System](/reference/slot-system) - Deep dive into slot customization
+444
View File
@@ -0,0 +1,444 @@
---
title: CopilotChat
description: "CopilotChat Component API Reference"
---
`CopilotChat` is a React component that provides a complete chat interface for interacting with AI agents. It handles
message display, user input, tool execution rendering, and agent communication automatically.
## What is CopilotChat?
The CopilotChat component:
- Provides a complete chat UI out of the box
- Manages conversation threads and message history
- Automatically connects to agents and handles message routing
- Renders tool executions with visual feedback
- Supports deep customization through the [slot system](/reference/slot-system)
- Handles auto-scrolling and responsive layouts
- Supports voice transcription for hands-free input
## Component Architecture
CopilotChat is built on a composable component hierarchy. Understanding this structure helps you customize exactly what you need.
```mermaid
graph LR
CC[CopilotChat] --> messageView
CC --> scrollView
CC --> input
CC --> suggestionView
CC --> welcomeScreen
```
### Slot Descriptions
| Slot | Description | Reference |
| ---------------- | -------------------------------------------------------------- | -------------------------------------------------------------------- |
| `messageView` | Container for the message list (user and assistant messages) | [CopilotChatMessageView](/reference/copilot-chat-message-view) |
| `scrollView` | Scrollable container with auto-scroll behavior | [CopilotChatScrollView](/reference/copilot-chat-scroll-view) |
| `input` | Text input with toolbar, transcription support, and disclaimer | [CopilotChatInput](/reference/copilot-chat-input) |
| `suggestionView` | Clickable suggestion chips | [CopilotChatSuggestionView](/reference/copilot-chat-suggestion-view) |
| `welcomeScreen` | Initial screen before any messages | [CopilotChatWelcomeScreen](/reference/copilot-chat-welcome-screen) |
See [Slot Customization](#slot-customization) for details on how to customize these slots.
## Basic Usage
```tsx
import { CopilotChat, CopilotKitProvider } from "@copilotkit/react-core";
function App() {
return (
<CopilotKitProvider runtimeUrl="/api/copilotkit">
<CopilotChat />
</CopilotKitProvider>
);
}
```
## Props
### agentId
`string` **(optional)**
The ID of the agent to connect to. Defaults to `"default"`.
```tsx
<CopilotChat agentId="assistant" />
```
### threadId
`string` **(optional)**
The conversation thread ID. If not provided, a new thread ID is automatically generated.
When you provide a `threadId`, CopilotChat automatically loads the conversation history for that thread, including any messages that are currently streaming. This enables seamless continuation of conversations across page reloads or component remounts.
```tsx
<CopilotChat threadId="thread-123" />
```
### labels
`Partial<CopilotChatLabels>` **(optional)**
Customize the text labels used throughout the chat interface.
```tsx
<CopilotChat
labels={{
chatInputPlaceholder: "Ask me anything...",
chatDisclaimerText: "AI assistant - verify important info",
welcomeMessage: "Hello! How can I help you today?",
}}
/>
```
### autoScroll
`boolean` **(optional, default: true)**
Automatically scroll to the bottom when new messages appear.
```tsx
<CopilotChat autoScroll={false} />
```
### className
`string` **(optional)**
CSS class name for the root container.
```tsx
<CopilotChat className="h-screen bg-white" />
```
### isModalDefaultOpen
`boolean` **(optional)**
When using CopilotChat in modal mode, controls whether the modal is open by default.
```tsx
<CopilotChat isModalDefaultOpen={true} />
```
### chatView
`SlotValue<typeof CopilotChatView>` **(optional)**
Customize the main chat view component. See [Slot Customization](#slot-customization) for details.
## Slot Customization
CopilotChat uses a powerful [slot system](/reference/slot-system) that allows you to customize any part of the UI. Each slot accepts four types of values:
1. **Tailwind class string** - Add or override CSS classes
2. **Props object** - Pass additional props to the default component
3. **Custom component** - Replace the component entirely
4. **Nested sub-slots** - Drill down to customize child components
### Customizing Appearance with Tailwind Classes
The simplest way to customize appearance is with Tailwind class strings:
```tsx
<CopilotChat
className="bg-gradient-to-b from-white to-gray-50 rounded-xl shadow-2xl"
messageView="space-y-4"
input="border-2 border-gray-200"
/>
```
### Customizing with Props
Pass a props object to modify component behavior while keeping the default implementation:
```tsx
<CopilotChat
messageView={{ className: "custom-message-view" }}
input={{ placeholder: "Ask a question..." }}
scrollView="custom-scroll-view"
/>
```
### Nested Slot Customization
You can drill down into nested components through props objects:
```tsx
<CopilotChat
messageView={{
assistantMessage: {
onThumbsUp: () => console.log("thumbsUp"),
onThumbsDown: () => console.log("thumbsDown"),
},
}}
/>
```
### Custom Components
For full control, replace components entirely:
```tsx
import { CopilotChatView } from "@copilotkit/react-core";
function CustomChatView(props) {
return (
<div className="custom-chat-layout">
<CopilotChatView
{...props}
messageView="custom-message-from-wrapper"
input="custom-input-from-wrapper"
/>
</div>
);
}
<CopilotChat chatView={CustomChatView} />;
```
## Message View Customization
The `messageView` slot controls how messages are rendered:
```tsx
<CopilotChat
messageView={{
// Style the message container
className: "space-y-4 p-4",
// Customize assistant messages
assistantMessage: {
className: "bg-blue-50 rounded-lg",
onThumbsUp: (message) => trackFeedback(message.id, "positive"),
onThumbsDown: (message) => trackFeedback(message.id, "negative"),
},
// Customize user messages
userMessage: "bg-gray-100 rounded-lg",
// Customize the typing cursor
cursor: "bg-blue-500",
}}
/>
```
### Assistant Message Customization
Customize how assistant messages appear and behave:
```tsx
<CopilotChat
messageView={{
assistantMessage: {
className: "bg-slate-50 border border-slate-200 rounded-xl p-4",
onThumbsUp: (message) => sendFeedback(message.id, "positive"),
onThumbsDown: (message) => sendFeedback(message.id, "negative"),
onRegenerate: (message) => regenerateResponse(message.id),
},
}}
/>
```
For full details on assistant message slots and customization options, see [CopilotChatAssistantMessage](/reference/copilot-chat-assistant-message).
### User Message Customization
Customize how user messages appear:
```tsx
<CopilotChat
messageView={{
userMessage: "bg-blue-500 text-white rounded-2xl px-4 py-2",
}}
/>
```
You can also pass a props object or a custom component:
```tsx
<CopilotChat
messageView={{
userMessage: {
className: "bg-primary text-primary-foreground",
"data-testid": "user-message",
},
}}
/>
```
## Input Customization
The `input` slot controls the text input and its toolbar:
```tsx
<CopilotChat
input={{
className: "border-2 border-primary rounded-xl",
placeholder: "Type your message...",
// Customize individual buttons
sendButton: "bg-blue-500 hover:bg-blue-600",
}}
/>
```
For full details on input slots and customization options, see [CopilotChatInput](/reference/copilot-chat-input).
## Suggestions
CopilotChat automatically manages suggestion chips through the `useSuggestions` hook. Suggestions are generated by the agent and displayed as clickable chips that users can select to quickly send messages.
You can customize suggestions through the `suggestionView` slot, which has two sub-slots:
```tsx
<CopilotChat
suggestionView={{
// Customize the container that holds all chips
container: "gap-4",
// Customize individual suggestion chips
suggestion: "bg-blue-100 hover:bg-blue-200 rounded-full",
}}
/>
```
## Voice Transcription
CopilotChat supports voice input through transcription. To enable it, configure your CopilotKitProvider with transcription settings:
```tsx
<CopilotKitProvider
runtimeUrl="/api/copilotkit"
transcribeAudioUrl="/api/transcribe"
>
<CopilotChat />
</CopilotKitProvider>
```
Once enabled, a microphone button appears in the input toolbar. Users can record audio which is transcribed and inserted into the message input.
## Welcome Screen Customization
The welcome screen is displayed before any messages are sent. You can customize it in several ways:
### Custom Welcome Message
The simplest customization is changing the welcome message text:
```tsx
<CopilotChat
labels={{
welcomeMessage: "Hello! I'm your AI assistant. How can I help you today?",
}}
/>
```
### Custom Welcome Screen Component
For full control, replace the entire welcome screen:
```tsx
function CustomWelcomeScreen() {
return (
<div className="flex flex-col items-center justify-center h-full p-8">
<img src="/logo.svg" alt="Logo" className="w-16 h-16 mb-4" />
<h2 className="text-2xl font-bold mb-2">Welcome to AI Assistant</h2>
<p className="text-muted-foreground text-center">
Ask me anything about your data, documents, or tasks.
</p>
</div>
);
}
<CopilotChat welcomeScreen={CustomWelcomeScreen} />;
```
## Auto-scrolling Behavior
CopilotChat automatically scrolls to the bottom when:
- New messages are added
- The user is already near the bottom
- `autoScroll` prop is true (default)
The scroll-to-bottom button appears when the user scrolls up and new content is available.
```tsx
// Disable auto-scroll
<CopilotChat autoScroll={false} />
// Customize the scroll button
<CopilotChat scrollView={{ scrollToBottomButton: "bg-blue-500 rounded-full shadow-lg" }} />
```
## Complete Example
Here's a fully customized CopilotChat implementation:
```tsx
import { CopilotChat, CopilotKitProvider } from "@copilotkit/react-core";
function App() {
const handleFeedback = (messageId: string, type: "positive" | "negative") => {
analytics.track("message_feedback", { messageId, type });
};
return (
<CopilotKitProvider runtimeUrl="/api/copilotkit">
<div className="h-screen">
<CopilotChat
agentId="my-assistant"
className="h-full"
labels={{
chatInputPlaceholder: "Ask me anything...",
chatDisclaimerText: "AI responses may not be accurate.",
welcomeMessage: "Hello! I'm here to help.",
}}
messageView={{
assistantMessage: {
onThumbsUp: (msg) => handleFeedback(msg.id, "positive"),
onThumbsDown: (msg) => handleFeedback(msg.id, "negative"),
},
}}
input={{
className: "border-2 border-gray-200 rounded-xl",
disclaimer: "text-xs text-gray-400",
}}
/>
</div>
</CopilotKitProvider>
);
}
```
## Related
### Slot Components
- [CopilotChatMessageView](/reference/copilot-chat-message-view) - Message list customization
- [CopilotChatScrollView](/reference/copilot-chat-scroll-view) - Scroll container customization
- [CopilotChatInput](/reference/copilot-chat-input) - Input component customization
- [CopilotChatSuggestionView](/reference/copilot-chat-suggestion-view) - Suggestion chips customization
- [CopilotChatWelcomeScreen](/reference/copilot-chat-welcome-screen) - Welcome screen customization
- [CopilotChatAssistantMessage](/reference/copilot-chat-assistant-message) - Assistant message customization
### Other Components
- [CopilotSidebar](/reference/copilot-sidebar) - Slide-in sidebar chat interface
- [CopilotPopup](/reference/copilot-popup) - Floating popup chat dialog
### Guides & Concepts
- [Slot System](/reference/slot-system) - Deep dive into slot customization
### Providers & Hooks
- [CopilotKitProvider](/reference/copilotkit-provider) - Provider configuration
- [useAgent](/reference/use-agent) - Hook for programmatic agent control
- [useFrontendTool](/reference/use-frontend-tool) - Adding custom tools to the chat
@@ -0,0 +1,216 @@
---
title: CopilotPopup
description: "Floating popup chat dialog"
---
`CopilotPopup` is a React component that provides a floating popup chat interface. It wraps [CopilotChat](/reference/copilot-chat) with additional popup-specific behavior including a toggle button, fade/scale animation, and optional click-outside-to-close functionality.
## What is CopilotPopup?
The CopilotPopup component:
- Provides a floating dialog with smooth fade/scale animation
- Includes a toggle button for open/close
- Supports click-outside-to-close behavior
- Responsive design (fullscreen on mobile, fixed size on desktop)
- Built on [CopilotChat](/reference/copilot-chat) - inherits all its features and customization options
## Component Architecture
CopilotPopup extends [CopilotChat](/reference/copilot-chat#component-architecture) with additional slots:
```mermaid
graph LR
CP[CopilotPopup] --> header
CP --> toggleButton
```
### Slot Descriptions
| Slot | Description |
| -------------- | --------------------------------------- |
| `header` | Header bar with title and close button |
| `toggleButton` | Floating button to open/close the popup |
CopilotPopup also inherits all slots from CopilotChat: `messageView`, `scrollView`, `input`, `suggestionView`, and `welcomeScreen`.
See [Slot Customization](#slot-customization) for details on how to customize these slots.
## Basic Usage
```tsx
import { CopilotPopup, CopilotKitProvider } from "@copilotkit/react-core";
function App() {
return (
<CopilotKitProvider runtimeUrl="/api/copilotkit">
<CopilotPopup />
</CopilotKitProvider>
);
}
```
## Props
### Popup-Specific Props
These props are unique to CopilotPopup:
| Prop | Type | Default | Description |
| --------------------- | ------------------ | ------- | --------------------------------------------------- |
| `defaultOpen` | `boolean` | `true` | Whether the popup is open initially |
| `width` | `number \| string` | `420` | Popup width in pixels or CSS unit |
| `height` | `number \| string` | `560` | Popup height in pixels or CSS unit |
| `clickOutsideToClose` | `boolean` | `false` | Close the popup when clicking outside |
| `header` | `SlotValue` | - | Custom header component with title and close button |
| `toggleButton` | `SlotValue` | - | Custom toggle button to open/close the popup |
### Shared Props
CopilotPopup inherits all props from [CopilotChat](/reference/copilot-chat), including:
- `agentId` - The agent to connect to
- `threadId` - The conversation thread ID
- `labels` - Customize text labels
- `autoScroll` - Auto-scroll behavior
- `className` - CSS class for the root container
See [CopilotChat Props](/reference/copilot-chat#props) for the complete list.
## Header Customization
The popup includes a header with a title and close button. Customize it through the `header` prop:
```tsx
<CopilotPopup
header={{
titleContent: "My Assistant",
closeButton: "hidden",
}}
/>
```
### Header Sub-Slots
| Sub-Slot | Description |
| -------------- | -------------------------------- |
| `titleContent` | The title text or component |
| `closeButton` | The close button (can be hidden) |
### Custom Header Component
Replace the entire header with your own component:
```tsx
function CustomHeader() {
return (
<div className="flex items-center justify-between p-4 border-b">
<div className="flex items-center gap-2">
<BotIcon className="w-5 h-5" />
<span className="font-semibold">My AI Assistant</span>
</div>
<button onClick={() => /* close popup */}>
<XIcon className="w-5 h-5" />
</button>
</div>
);
}
<CopilotPopup header={CustomHeader} />
```
## Toggle Button Customization
The popup includes a floating toggle button to open and close it. Customize it through the `toggleButton` prop:
```tsx
<CopilotPopup
toggleButton={{
className: "bg-purple-500 hover:bg-purple-600",
openIcon: "text-white",
closeIcon: "text-white",
}}
/>
```
### Toggle Button Sub-Slots
| Sub-Slot | Description |
| ----------- | ----------------------------------------------- |
| `openIcon` | Icon shown when popup is closed (click to open) |
| `closeIcon` | Icon shown when popup is open (click to close) |
### Custom Toggle Button Component
Replace the toggle button entirely:
```tsx
function CustomToggleButton() {
return (
<button className="fixed bottom-4 right-4 p-3 bg-blue-500 rounded-full">
<ChatIcon className="w-6 h-6 text-white" />
</button>
);
}
<CopilotPopup toggleButton={CustomToggleButton} />;
```
## Slot Customization
CopilotPopup uses the same [slot system](/reference/slot-system) as CopilotChat. Each slot accepts four types of values:
1. **Tailwind class string** - Add or override CSS classes
2. **Props object** - Pass additional props to the default component
3. **Custom component** - Replace the component entirely
4. **Nested sub-slots** - Drill down to customize child components
### Custom Dimensions
```tsx
<CopilotPopup width={500} height={700} />
// Or with CSS units
<CopilotPopup width="30vw" height="80vh" />
```
### Click Outside to Close
```tsx
<CopilotPopup clickOutsideToClose={true} />
```
### Custom Header Styling
```tsx
<CopilotPopup
header={{
className: "bg-gradient-to-r from-blue-500 to-purple-500 text-white",
titleContent: "AI Assistant",
}}
/>
```
### Nested Slot Customization
```tsx
<CopilotPopup
messageView={{
assistantMessage: {
onThumbsUp: (msg) => console.log("Liked:", msg.id),
onThumbsDown: (msg) => console.log("Disliked:", msg.id),
},
}}
input={{
className: "border-2 border-gray-200",
sendButton: "bg-blue-500 hover:bg-blue-600",
}}
/>
```
## Related
- [CopilotChat](/reference/copilot-chat) - Base chat component with full customization options
- [CopilotSidebar](/reference/copilot-sidebar) - Slide-in sidebar chat interface
- [Slot System](/reference/slot-system) - Deep dive into slot customization
- [CopilotKitProvider](/reference/copilotkit-provider) - Provider configuration
@@ -0,0 +1,341 @@
---
title: CopilotRuntime
description: "Server-side runtime for hosting CopilotKit agents"
---
The `CopilotRuntime` package gives you everything you need to host CopilotKit agents on your own infrastructure. It owns the agent registry, wires an `AgentRunner`, exposes HTTP endpoints (Express or Hono), and optionally coordinates middleware and transcription services.
## Basic Setup
```ts
import { CopilotRuntime, InMemoryAgentRunner } from "@copilotkit/runtime";
import { BasicAgent } from "@copilotkit/runtime/v2";
const runtime = new CopilotRuntime({
agents: {
default: new BasicAgent({
model: "openai/gpt-4o",
prompt: "You are a helpful assistant.",
}),
},
runner: new InMemoryAgentRunner(),
});
```
Once you create a runtime instance you can mount one of the provided HTTP endpoints (Express or Hono) described below.
## `CopilotRuntime` Constructor
```ts
new CopilotRuntime(options: CopilotRuntimeOptions)
```
### `CopilotRuntimeOptions`
| Option | Type | Description |
| ------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agents` | `Promise<Record<string, AbstractAgent>> \| Record<string, AbstractAgent>` | Required. A map from agent identifier to an `AbstractAgent`. You can return the map synchronously or asynchronously. |
| `runner` | `AgentRunner` | Optional. Defaults to `new InMemoryAgentRunner()`. Controls how agent runs are scheduled and streamed. |
| `transcriptionService` | `TranscriptionService` | Optional. Enables `/transcribe` and reports `audioFileTranscriptionEnabled: true` from `/info`. |
| `beforeRequestMiddleware` | `BeforeRequestMiddleware` | Optional. Runs before every request. Can mutate the incoming `Request` or return `void`. |
| `afterRequestMiddleware` | `AfterRequestMiddleware` | Optional. Runs after every handler resolves. Receives the response along with parsed messages, thread ID, and run ID extracted from the SSE stream. Use this for logging, metrics, telemetry, etc. |
### Passing Agents
- Provide a plain object mapping agent ids to instances. The helper `BasicAgent` covers common OpenAI/Anthropic/Gemini setups, but you can supply any `AbstractAgent`.
- Because `agents` may be an async function, you can lazily load credentials or fetch configuration before returning the record.
- Each request clones the selected agent instance so per-request state (messages, headers, tool registration) does not leak between threads.
- `Authorization` and `x-*` headers on the incoming request are forwarded to the cloned agent automatically. Override or strip them inside `beforeRequestMiddleware` if needed.
```ts
const runtime = new CopilotRuntime({
agents: async () => ({
default: buildDefaultAgent(),
support: buildSupportAgent(),
}),
});
```
### Choosing an `AgentRunner`
`InMemoryAgentRunner` is the default and streams events directly from your Node.js process. Swap it with a custom `AgentRunner` if you need to enqueue work or forward to another service:
```ts
class QueueBackedRunner implements AgentRunner {
/* ... */
}
const runtime = new CopilotRuntime({
agents,
runner: new QueueBackedRunner(),
});
```
### Middleware Hooks
```ts
const runtime = new CopilotRuntime({
agents,
beforeRequestMiddleware: async ({ request, path }) => {
const auth = request.headers.get("authorization");
if (!auth) {
throw new Response("Unauthorized", { status: 401 });
}
},
afterRequestMiddleware: async ({
response,
path,
messages,
threadId,
runId,
}) => {
console.info("Handled", path, response.status);
console.info("Thread:", threadId, "Run:", runId);
console.info("Messages:", messages);
},
});
```
Throwing a `Response` from `beforeRequestMiddleware` short-circuits the request. `afterRequestMiddleware` runs in the background and should swallow its own errors.
#### `AfterRequestMiddlewareParameters`
| Parameter | Type | Description |
| ---------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `runtime` | `CopilotRuntime` | The runtime instance. |
| `response` | `Response` | A clone of the response sent to the client. The body is still readable. |
| `path` | `string` | The matched route path (e.g. `/agent/default/run`). |
| `messages` | `Message[]` | Reconstructed messages from the SSE stream. Empty for non-SSE responses. Each message has `id`, `role`, and optionally `content`, `toolCalls`, or `toolCallId`. |
| `threadId` | `string \| undefined` | Thread ID from the `RUN_STARTED` event. |
| `runId` | `string \| undefined` | Run ID from the `RUN_STARTED` event. |
This makes `afterRequestMiddleware` suitable for telemetry, audit logging, and post-run analytics without needing to manually parse the streamed response.
### Audio Transcription
Enable audio transcription by providing a `transcriptionService`. The `@copilotkit/voice` package includes providers:
```ts
import { CopilotRuntime } from "@copilotkit/runtime";
import { TranscriptionServiceOpenAI } from "@copilotkit/voice";
import OpenAI from "openai";
const runtime = new CopilotRuntime({
agents: { default: yourAgent },
transcriptionService: new TranscriptionServiceOpenAI({
openai: new OpenAI({ apiKey: process.env.OPENAI_API_KEY }),
}),
});
```
This enables the `/transcribe` endpoint and shows a microphone button in the chat UI. The `/info` endpoint will report `audioFileTranscriptionEnabled: true`.
For custom providers, extend `TranscriptionService` from runtime:
```ts
import {
TranscriptionService,
TranscribeFileOptions,
} from "@copilotkit/runtime";
class MyTranscriptionService extends TranscriptionService {
async transcribeFile(options: TranscribeFileOptions): Promise<string> {
// options.audioFile, options.mimeType, options.size
return "transcribed text";
}
}
```
## HTTP Endpoints
Import the helper that matches your server framework and the transport style you want to expose.
### Hono (REST-style)
```ts
import { createCopilotEndpoint } from "@copilotkit/runtime";
const copilot = createCopilotEndpoint({ runtime, basePath: "/api/copilotkit" });
const app = new Hono();
app.route("/", copilot);
```
This mounts five routes under the base path:
| Method | Path | Purpose |
| ------ | -------------------------------- | -------------------------------------------------------------- |
| `POST` | `/agent/:agentId/run` | Streams agent events (SSE). |
| `POST` | `/agent/:agentId/connect` | Creates a live WebSocket/stream connection. |
| `POST` | `/agent/:agentId/stop/:threadId` | Cancels an in-flight thread. |
| `GET` | `/info` | Returns runtime metadata and agent list. |
| `POST` | `/transcribe` | Proxies audio transcription (requires `transcriptionService`). |
### Hono (Single-route)
```ts
import { createCopilotEndpointSingleRoute } from "@copilotkit/runtime";
const copilot = createCopilotEndpointSingleRoute({
runtime,
basePath: "/api/copilotkit",
});
const app = new Hono();
app.route("/", copilot);
```
All interactions happen through a single `POST /api/copilotkit` endpoint. The client sends a JSON envelope:
```json
{
"method": "agent/run",
"params": { "agentId": "default" },
"body": { "messages": [...], "threadId": "thread-123" }
}
```
Allowed `method` values are:
- `agent/run`
- `agent/connect`
- `agent/stop`
- `info`
- `transcribe`
Pair this server endpoint with the React provider flag `<CopilotKitProvider useSingleEndpoint />`.
### Next.js (App Router) with Hono
When you're inside a Next.js `app/` route, export the Hono handlers directly:
```ts
// app/api/copilotkit/[[...slug]]/route.ts
import { CopilotRuntime, createCopilotEndpoint } from "@copilotkit/runtime";
import { handle } from "hono/vercel";
const runtime = new CopilotRuntime({ agents, runner });
const app = createCopilotEndpoint({ runtime, basePath: "/api/copilotkit" });
export const GET = handle(app);
export const POST = handle(app);
```
Swap `createCopilotEndpoint` for `createCopilotEndpointSingleRoute` if you configure the client with `useSingleEndpoint`.
### Express (REST-style)
```ts
import express from "express";
import { createCopilotEndpointExpress } from "@copilotkit/runtime/express";
const app = express();
app.use(
"/api/copilotkit",
createCopilotEndpointExpress({ runtime, basePath: "/" }),
);
```
The router mirrors the Hono REST routes and automatically enables permissive CORS (allowing all origins, headers, and methods). Adjust headers upstream if you need tighter controls.
### Express (Single-route)
```ts
import express from "express";
import { createCopilotEndpointSingleRouteExpress } from "@copilotkit/runtime/express";
const app = express();
app.use(
"/api/copilotkit",
createCopilotEndpointSingleRouteExpress({ runtime, basePath: "/" }),
);
```
Single-route Express works identically to the Hono version: send the JSON envelope described earlier, and set `useSingleEndpoint` on the client.
### NestJS (Express backend)
NestJS uses Express by default. Mount the Express router in `main.ts`:
```ts
// main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { NestExpressApplication } from "@nestjs/platform-express";
import { CopilotRuntime } from "@copilotkit/runtime";
import { createCopilotEndpointExpress } from "@copilotkit/runtime/express";
import { BasicAgent } from "@copilotkit/runtime/v2";
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
const runtime = new CopilotRuntime({
agents: {
default: new BasicAgent({
model: "openai/gpt-4o",
prompt: "You are helpful.",
}),
},
});
// Mount under /api/copilotkit (REST-style routes)
app.use(
"/api/copilotkit",
createCopilotEndpointExpress({ runtime, basePath: "/" }),
);
await app.listen(3000);
}
bootstrap();
```
Available routes (no global prefix):
- `POST /api/copilotkit/agent/:agentId/run`
- `POST /api/copilotkit/agent/:agentId/connect`
- `POST /api/copilotkit/agent/:agentId/stop/:threadId`
- `GET /api/copilotkit/info`
- `POST /api/copilotkit/transcribe`
If you prefer the single-route transport, mount the single-route helper instead and set `useSingleEndpoint` on the client:
```ts
import { createCopilotEndpointSingleRouteExpress } from "@copilotkit/runtime/express";
app.use(
"/api/copilotkit",
createCopilotEndpointSingleRouteExpress({ runtime, basePath: "/" }),
);
```
Notes:
- If you call `app.setGlobalPrefix('api')`, the effective paths become `/api/copilotkit/...` (or `/api/copilotkit` for single-route).
- Endpoints stream ServerSent Events; ensure your proxy honors `text/event-stream` and keepalive.
- The router enables permissive CORS; tighten via Nests `enableCors` if needed.
## Runtime Metadata
Calling `GET /info` (or the `info` single-route method) returns:
```json
{
"version": "0.0.20",
"agents": {
"default": {
"name": "default",
"className": "BasicAgent",
"description": "You are a helpful assistant."
}
},
"audioFileTranscriptionEnabled": false
}
```
Use this to verify deployments and surface diagnostics in your UI.
## Next Steps
- Configure the React client with `runtimeUrl` (and `useSingleEndpoint` if you chose the single endpoint helper).
- Register frontend tools with `CopilotKitProvider` so agents can trigger UI actions.
- Extend the runtime runner or middleware hooks to integrate logging, rate limiting, or background queues.
@@ -0,0 +1,208 @@
---
title: CopilotSidebar
description: "Sidebar chat interface with slide-in animation"
---
`CopilotSidebar` is a React component that provides a slide-in sidebar chat interface. It wraps [CopilotChat](/reference/copilot-chat) with additional sidebar-specific behavior including a toggle button, slide-in animation, and automatic body margin adjustment.
## What is CopilotSidebar?
The CopilotSidebar component:
- Provides a fixed right-side panel with smooth slide-in animation
- Includes a toggle button for open/close
- Automatically adjusts body margin when open to prevent content overlap
- Responsive design (full width on mobile, custom width on desktop)
- Built on [CopilotChat](/reference/copilot-chat) - inherits all its features and customization options
## Component Architecture
CopilotSidebar extends [CopilotChat](/reference/copilot-chat#component-architecture) with additional slots:
```mermaid
graph LR
CS[CopilotSidebar] --> header
CS --> toggleButton
```
### Slot Descriptions
| Slot | Description |
| -------------- | ----------------------------------------- |
| `header` | Header bar with title and close button |
| `toggleButton` | Floating button to open/close the sidebar |
CopilotSidebar also inherits all slots from CopilotChat: `messageView`, `scrollView`, `input`, `suggestionView`, and `welcomeScreen`.
See [Slot Customization](#slot-customization) for details on how to customize these slots.
## Basic Usage
```tsx
import { CopilotSidebar, CopilotKitProvider } from "@copilotkit/react-core";
function App() {
return (
<CopilotKitProvider runtimeUrl="/api/copilotkit">
<CopilotSidebar />
</CopilotKitProvider>
);
}
```
## Props
### Sidebar-Specific Props
These props are unique to CopilotSidebar:
| Prop | Type | Default | Description |
| -------------- | ------------------ | ------- | --------------------------------------------------- |
| `defaultOpen` | `boolean` | `true` | Whether the sidebar is open initially |
| `width` | `number \| string` | `480` | Sidebar width in pixels or CSS unit |
| `header` | `SlotValue` | - | Custom header component with title and close button |
| `toggleButton` | `SlotValue` | - | Custom toggle button to open/close the sidebar |
### Shared Props
CopilotSidebar inherits all props from [CopilotChat](/reference/copilot-chat), including:
- `agentId` - The agent to connect to
- `threadId` - The conversation thread ID
- `labels` - Customize text labels
- `autoScroll` - Auto-scroll behavior
- `className` - CSS class for the root container
See [CopilotChat Props](/reference/copilot-chat#props) for the complete list.
## Header Customization
The sidebar includes a header with a title and close button. Customize it through the `header` prop:
```tsx
<CopilotSidebar
header={{
titleContent: "My Assistant",
closeButton: "hidden",
}}
/>
```
### Header Sub-Slots
| Sub-Slot | Description |
| -------------- | -------------------------------- |
| `titleContent` | The title text or component |
| `closeButton` | The close button (can be hidden) |
### Custom Header Component
Replace the entire header with your own component:
```tsx
function CustomHeader() {
return (
<div className="flex items-center justify-between p-4 border-b">
<div className="flex items-center gap-2">
<BotIcon className="w-5 h-5" />
<span className="font-semibold">My AI Assistant</span>
</div>
<button onClick={() => /* close sidebar */}>
<XIcon className="w-5 h-5" />
</button>
</div>
);
}
<CopilotSidebar header={CustomHeader} />
```
## Toggle Button Customization
The sidebar includes a floating toggle button to open and close it. Customize it through the `toggleButton` prop:
```tsx
<CopilotSidebar
toggleButton={{
className: "bg-purple-500 hover:bg-purple-600",
openIcon: "text-white",
closeIcon: "text-white",
}}
/>
```
### Toggle Button Sub-Slots
| Sub-Slot | Description |
| ----------- | ------------------------------------------------- |
| `openIcon` | Icon shown when sidebar is closed (click to open) |
| `closeIcon` | Icon shown when sidebar is open (click to close) |
### Custom Toggle Button Component
Replace the toggle button entirely:
```tsx
function CustomToggleButton() {
return (
<button className="fixed bottom-4 right-4 p-3 bg-blue-500 rounded-full">
<ChatIcon className="w-6 h-6 text-white" />
</button>
);
}
<CopilotSidebar toggleButton={CustomToggleButton} />;
```
## Slot Customization
CopilotSidebar uses the same [slot system](/reference/slot-system) as CopilotChat. Each slot accepts four types of values:
1. **Tailwind class string** - Add or override CSS classes
2. **Props object** - Pass additional props to the default component
3. **Custom component** - Replace the component entirely
4. **Nested sub-slots** - Drill down to customize child components
### Custom Width
```tsx
<CopilotSidebar width={600} />
// Or with CSS units
<CopilotSidebar width="50vw" />
```
### Custom Header Styling
```tsx
<CopilotSidebar
header={{
className: "bg-gradient-to-r from-blue-500 to-purple-500 text-white",
titleContent: "AI Assistant",
}}
/>
```
### Nested Slot Customization
```tsx
<CopilotSidebar
messageView={{
assistantMessage: {
onThumbsUp: (msg) => console.log("Liked:", msg.id),
onThumbsDown: (msg) => console.log("Disliked:", msg.id),
},
}}
input={{
className: "border-2 border-gray-200",
sendButton: "bg-blue-500 hover:bg-blue-600",
}}
/>
```
## Related
- [CopilotChat](/reference/copilot-chat) - Base chat component with full customization options
- [CopilotPopup](/reference/copilot-popup) - Floating popup chat dialog
- [Slot System](/reference/slot-system) - Deep dive into slot customization
- [CopilotKitProvider](/reference/copilotkit-provider) - Provider configuration
@@ -0,0 +1,550 @@
---
title: CopilotKitCore
description: "CopilotKitCore API Reference"
---
`CopilotKitCore` is the client-side cross-framework orchestration layer that coordinates communication between your
application's UI, agents abd the remote Copilot runtime.
## Architecture Overview
```mermaid
graph TB
subgraph Framework["Framework Layer"]
React["**CopilotKit React**<br/>Components & Hooks"]
Angular["**CopilotKit Angular**<br/>Directives & Services"]
Vue["**CopilotKit ...**<br/>Future frameworks"]
end
subgraph CoreLayer["Core Layer"]
Core["**CopilotKitCore**<br/>Cross-framework Orchestration<br/>State Management & Agent Coordination"]
end
subgraph RuntimeLayer["Runtime Layer"]
Runtime["**CopilotRuntime**<br/>AI Agent Execution<br/>Backend Services"]
end
React --> Core
Angular --> Core
Vue -.-> Core
Core <--> Runtime
```
## What does CopilotKitCore do?
CopilotKitCore solves several complex challenges in building AI-powered applications:
1. **Agent Orchestration**: Handles the entire lifecycle of running AI agents, from starting the agent and executing
tools, to providing tool results, handling follow-ups and handling streaming updates to the UI.
2. **Runtime Connection**: Manages connecting to and synchronizing with the remote `CopilotRuntime`.
3. **State Synchronization**: Maintains consistent state across your application, ensuring all components have access to
the latest agents, tools, and context
4. **Framework Independence**: Works with any frontend framework, providing a consistent API whether you're using React,
Angular, Vue, or vanilla JavaScript
At its heart, CopilotKitCore maintains the authoritative state for these critical elements:
- **Agents**: Both local and remote AI agents that power your application's intelligence
- **Context**: Additional information that agents can use to make more informed decisions
- **Frontend Tools**: Functions that agents can call to interact with your UI
- **Properties and Headers**: Application-specific data forwarded to agents as additional properties or HTTP headers
- **Runtime Metadata**: Connection status, versioning, and configuration details
## Creating a CopilotKitCore Instance
<Info>
In most applications, you dont need to create a `CopilotKitCore` instance
yourself—CopilotKit initializes and manages it behind the scenes.
</Info>
Instantiate `CopilotKitCore` to initialize the client that manages runtime connections, agents, and tools.
```typescript
new CopilotKitCore(config?: CopilotKitCoreConfig)
```
#### Parameters
```typescript
interface CopilotKitCoreConfig {
runtimeUrl?: string; // The endpoint where your CopilotRuntime server is hosted
headers?: Record<string, string>; // Custom HTTP headers to include with every request
properties?: Record<string, unknown>; // Application-specific data forwarded to agents as context
agents__unsafe_dev_only?: Record<string, AbstractAgent>; // Local agents for development only - production requires CopilotRuntime
tools?: FrontendTool<any>[]; // Frontend functions that agents can invoke
}
```
Configuration details:
- `runtimeUrl`: When provided, CopilotKitCore will automatically connect and discover available remote agents.
- `headers`: Headers to include with every request.
- `properties`: Properties forwarded to agents as `forwardedProps`.
- `tools`: Frontend tools that agents can invoke.
- `agents__unsafe_dev_only`: **Development only** - This property is intended solely for rapid prototyping during
development. Production deployments require the security, reliability, and performance guarantees that only the
CopilotRuntime can provide. The key becomes the agent's identifier.
## Core Properties
Once initialized, CopilotKitCore exposes several properties that provide insight into its current state:
### Runtime Configuration
- `runtimeUrl`: `string | undefined` The current runtime endpoint. Changing this property triggers a connection attempt
to the new runtime. Setting it to `undefined` gracefully disconnects from the current runtime.
- `runtimeVersion`: `string | undefined` The version of the connected runtime, helpful for compatibility checks and
debugging.
- `runtimeConnectionStatus`: `CopilotKitCoreRuntimeConnectionStatus` Tracks the current connection state, allowing you
to respond to connection changes in your UI.
### State Management
- `headers`: `Readonly<Record<string, string>>` The current request headers sent with every request.
- `properties`: `Readonly<Record<string, unknown>>` The application properties being forwarded to agents.
- `agents`: `Readonly<Record<string, AbstractAgent>>` A combined view of all available agents, merging local development
agents with those discovered from the runtime.
- `tools`: `Readonly<FrontendTool<any>[]>` The list of registered frontend tools available to agents.
- `context`: `Readonly<Record<string, Context>>` Contextual information that agents can use to make more informed
decisions.
## Understanding Runtime Connections
When you provide a `runtimeUrl`, CopilotKitCore initiates a connection to your CopilotRuntime server. Here's what
happens behind the scenes:
1. CopilotKitCore sends a GET request to `{runtimeUrl}/info`
2. The runtime responds with available agents and metadata
3. For each remote agent, `CopilotKitCore` creates a `ProxiedCopilotRuntimeAgent` instance
4. These remote agents are merged with your local agents
5. Subscribers are notified of the connection status and agent availability
### Runtime Connection States
The connection to your runtime can be in one of four states:
- `Disconnected`: No runtime URL is configured, or the connection was intentionally closed
- `Connecting`: Actively attempting to establish a connection with the runtime
- `Connected`: Successfully connected and remote agents are available
- `Error`: The connection attempt failed, but CopilotKitCore will continue with local agents only
## Event Subscription System
CopilotKitCore implements a robust event system that allows you to react to state changes and agent activities:
### subscribe()
Registers a subscriber to receive events. Returns a subscription object with an `unsubscribe()` method, keeping
unsubscription localized to the subscription object.
```typescript
subscribe(subscriber: CopilotKitCoreSubscriber): { unsubscribe(): void }
```
## Subscribing to Events
The event subscription system allows you to build reactive UIs that respond to CopilotKitCore's state changes. Each
event provides both the CopilotKitCore instance and relevant data:
### onRuntimeConnectionStatusChanged()
Notified whenever the connection to the runtime changes state. Use this to show connection indicators in your UI.
```typescript
onRuntimeConnectionStatusChanged?: (event: {
copilotkit: CopilotKitCore;
status: CopilotKitCoreRuntimeConnectionStatus;
}) => void | Promise<void>
```
### onToolExecutionStart()
Fired when an agent begins executing a tool. Useful for showing loading states or activity indicators.
```typescript
onToolExecutionStart?: (event: {
copilotkit: CopilotKitCore;
toolCallId: string;
agentId: string;
toolName: string;
args: unknown;
}) => void | Promise<void>
```
### onToolExecutionEnd()
Fired when tool execution completes, whether successfully or with an error. Use this to update your UI based on the
results.
```typescript
onToolExecutionEnd?: (event: {
copilotkit: CopilotKitCore;
toolCallId: string;
agentId: string;
toolName: string;
result: string;
error?: string;
}) => void | Promise<void>
```
### onAgentsChanged()
Notified when agents are added, removed, or updated. This includes both local changes and runtime discovery.
```typescript
onAgentsChanged?: (event: {
copilotkit: CopilotKitCore;
agents: Readonly<Record<string, AbstractAgent>>;
}) => void | Promise<void>
```
### onContextChanged()
Fired when context items are added or removed. Use this to keep UI elements in sync with available context.
```typescript
onContextChanged?: (event: {
copilotkit: CopilotKitCore;
context: Readonly<Record<string, Context>>;
}) => void | Promise<void>
```
### onPropertiesChanged()
Notified when application properties are updated. Useful for debugging or showing current configuration.
```typescript
onPropertiesChanged?: (event: {
copilotkit: CopilotKitCore;
properties: Readonly<Record<string, unknown>>;
}) => void | Promise<void>
```
### onHeadersChanged()
Fired when HTTP headers are modified. Important for tracking authentication state changes.
```typescript
onHeadersChanged?: (event: {
copilotkit: CopilotKitCore;
headers: Readonly<Record<string, string>>;
}) => void | Promise<void>
```
### onError()
The central error handler for all CopilotKitCore operations. Every error includes a code and contextual information to
help with debugging.
```typescript
onError?: (event: {
copilotkit: CopilotKitCore;
error: Error;
code: CopilotKitCoreErrorCode;
context: Record<string, any>;
}) => void | Promise<void>
```
## Error Handling
CopilotKitCore provides detailed error information through typed error codes, making it easier to handle different
failure scenarios appropriately:
### Understanding Error Codes
Each error is categorized with a specific code that indicates what went wrong:
- `RUNTIME_INFO_FETCH_FAILED`: The attempt to fetch runtime information failed. This might indicate network issues or an
incorrect runtime URL.
- `AGENT_CONNECT_FAILED`: Failed to establish a connection with an agent. Check that the agent is properly configured.
- `AGENT_RUN_FAILED`: The agent execution failed. This could be due to invalid messages or agent-side errors.
- `AGENT_RUN_FAILED_EVENT`: The agent reported a failure through its event stream.
- `AGENT_RUN_ERROR_EVENT`: The agent emitted an error event during execution.
- `TOOL_ARGUMENT_PARSE_FAILED`: The arguments provided to a tool couldn't be parsed. This usually indicates a mismatch
between expected and actual parameters.
- `TOOL_HANDLER_FAILED`: A tool's handler function threw an error during execution.
## Running Agents
CopilotKitCore provides two primary methods for executing agents: `runAgent` for immediate execution with message
handling, and `connectAgent` for establishing live connections to existing agent sessions.
### runAgent()
Executes an agent and intelligently handles the complete request-response cycle, including automatic tool execution and
follow-up runs.
The execution flow:
1. Sends initial messages (if provided) to the agent
2. Agent processes the request and may request tool calls
3. CopilotKitCore automatically executes any requested frontend tools
4. Tool results are added back to the message history
5. If tools were executed and have `followUp` enabled (default), the agent is automatically re-run with the tool results
6. This cycle continues until the agent completes without requesting more tools
Key behaviors:
- **Automatic Tool Execution**: When an agent requests a tool call, CopilotKitCore finds the matching tool, executes it,
and adds the result to the conversation
- **Smart Follow-ups**: After executing tools, it automatically re-runs the agent so it can process the tool results and
continue its work
- **Error Recovery**: Tool execution errors are captured and returned to the agent, allowing it to handle failures
gracefully
- **Wildcard Tools**: Supports a special "\*" tool that can handle any undefined tool request
```typescript
runAgent(params: CopilotKitCoreRunAgentParams): Promise<RunAgentResult>
```
#### Parameters
```typescript
interface CopilotKitCoreRunAgentParams {
agent: AbstractAgent; // The agent to execute
}
```
### connectAgent()
Establishes a live connection with an agent, restoring any existing conversation history and subscribing to real-time
events. This method is particularly useful when:
- Reconnecting to an agent that's already running (displays new events as they occur)
- Restoring conversation history after a page refresh
- Setting up an agent that will receive updates from background processes
The connection process:
1. Provides the agent with current properties and available tools
2. Sets up error event subscribers for proper error handling
3. Restores any existing message history
4. Returns immediately without triggering a new agent run
```typescript
connectAgent(params: CopilotKitCoreConnectAgentParams): Promise<RunAgentResult>
```
#### Parameters
```typescript
interface CopilotKitCoreConnectAgentParams {
agent: AbstractAgent; // The agent to connect
agentId?: string; // Override the agent's default identifier
}
```
### getAgent()
Retrieves an agent by its identifier. Returns `undefined` if the agent doesn't exist or if the runtime is still
connecting.
```typescript
getAgent(id: string): AbstractAgent | undefined
```
## Managing Context
Context provides agents with additional information about the current state of your application:
### addContext()
Adds contextual information that agents can reference. Returns a unique identifier for later removal.
```typescript
addContext(context: Context): string
```
#### Parameters
```typescript
interface Context {
description: string; // A human-readable description of what this context represents
value: any; // The actual context data
}
```
### removeContext()
Removes previously added context using its identifier.
```typescript
removeContext(id: string): void
```
## Managing Tools
Tools are functions that agents can call to interact with your application:
### addTool()
Registers a new frontend tool. If a tool with the same name and agent scope already exists, the registration is skipped
to prevent duplicates.
```typescript
addTool<T>(tool: FrontendTool<T>): void
```
### removeTool()
Removes a tool from the registry. You can remove tools globally or for specific agents:
- When `agentId` is provided, removes the tool only for that agent
- When `agentId` is omitted, removes only global tools with the matching name
```typescript
removeTool(id: string, agentId?: string): void
```
### getTool()
Retrieves a tool by name, with intelligent fallback behavior:
- If an agent-specific tool isn't found, it falls back to global tools
```typescript
getTool(params: CopilotKitCoreGetToolParams): FrontendTool<any> | undefined
```
#### Parameters
```typescript
interface CopilotKitCoreGetToolParams {
toolName: string; // The tool's name
agentId?: string; // Look for agent-specific tools first
}
```
### setTools()
Replaces all tools at once. Useful for completely reconfiguring available tools.
```typescript
setTools(tools: FrontendTool<any>[]): void
```
## Tool Execution Lifecycle
Understanding how CopilotKitCore handles tool execution is crucial for building responsive AI applications. Here's the
complete lifecycle:
### 1. Tool Request
When an agent needs to interact with your application, it returns a tool call request with:
- Tool name
- Structured arguments (JSON)
- Unique call ID for tracking
### 2. Tool Resolution
CopilotKitCore uses a two-tier resolution strategy:
- First checks for agent-specific tools (scoped to that particular agent)
- Falls back to global tools available to all agents
- Special wildcard tool (`*`) can handle any unmatched tool requests
### 3. Execution & Error Handling
The tool handler is invoked with parsed arguments:
- Successful results are stringified and added to the conversation
- Errors are caught and returned to the agent for graceful handling
- Subscribers are notified of start and completion events
### 4. Automatic Follow-up
By default, after tool execution completes:
- The tool result is inserted into the message history
- The agent is automatically re-run to process the results
- This continues until no more tools are requested
- You can disable follow-up by setting `followUp: false` on specific tools
## Working with Configuration
These methods allow you to dynamically update CopilotKitCore's configuration after initialization:
### setRuntimeUrl()
Changes the runtime endpoint and manages the connection lifecycle. This is useful when switching between different
environments or disconnecting from the runtime entirely.
```typescript
setRuntimeUrl(runtimeUrl: string | undefined): void
```
### setHeaders()
Replaces all HTTP headers. Subscribers are notified so they can react to authentication changes or other header updates.
```typescript
setHeaders(headers: Record<string, string>): void
```
### setProperties()
Updates the properties forwarded to agents. Use this when your application state changes in ways that might affect agent
behavior.
```typescript
setProperties(properties: Record<string, unknown>): void
```
## Managing Local Agents
<Warning>
In production applications, agents must be managed through the CopilotRuntime,
which provides proper isolation, monitoring, and scalability. The local agent
methods below are marked `__unsafe_dev_only` as they're intended solely for
rapid prototyping during development. Production deployments require the
security and performance guarantees that only the CopilotRuntime can provide.
</Warning>
Agents are the AI-powered components that process requests and generate responses. CopilotKitCore provides several
methods to manage them locally during development:
### setAgents\_\_unsafe_dev_only()
Replaces all local development agents while preserving remote agents from the runtime.
```typescript
setAgents__unsafe_dev_only(agents: Record<string, AbstractAgent>): void
```
### addAgent\_\_unsafe_dev_only()
Adds a single agent for development testing.
```typescript
addAgent__unsafe_dev_only(params: CopilotKitCoreAddAgentParams): void
```
#### Parameters
```typescript
interface CopilotKitCoreAddAgentParams {
id: string; // A unique identifier for the agent
agent: AbstractAgent; // The agent instance to add
}
```
### removeAgent\_\_unsafe_dev_only()
Removes a local development agent by its identifier. Remote agents from the runtime cannot be removed this way.
```typescript
removeAgent__unsafe_dev_only(id: string): void
```
@@ -0,0 +1,298 @@
---
title: CopilotKitProvider
description: "CopilotKitProvider API Reference"
---
`CopilotKitProvider` is the React context provider that initializes and manages `CopilotKitCore` for your React
application. It provides all child components with access to agents, tools, and copilot functionality through React's
context API.
## What is CopilotKitProvider?
The CopilotKitProvider is the root component that:
- Creates and manages a `CopilotKitCore` instance
- Provides React-specific features like hooks and render components
- Manages tool rendering and human-in-the-loop interactions
- Handles state synchronization between your React app and AI agents
## Basic Usage
Typically you would wrap your application with `CopilotKitProvider` at the root level:
```tsx
import { CopilotKitProvider } from "@copilotkit/react-core";
function App() {
return (
<CopilotKitProvider runtimeUrl="http://localhost:3000/api/copilotkit">
{/* Your app components */}
</CopilotKitProvider>
);
}
```
## Props
### runtimeUrl
`string` **(optional)**
The URL of your CopilotRuntime server. The provider will automatically connect to the runtime and discover available
agents.
```tsx
<CopilotKitProvider runtimeUrl="https://api.example.com/copilot">
{children}
</CopilotKitProvider>
```
### headers
`Record<string, string>` **(optional)**
Custom HTTP headers to include with every request to the runtime. Useful for authentication and custom metadata.
```tsx
<CopilotKitProvider
runtimeUrl="https://api.example.com"
headers={{
Authorization: "Bearer your-token",
"X-Custom-Header": "value",
}}
>
{children}
</CopilotKitProvider>
```
### properties
`Record<string, unknown>` **(optional)**
Application-specific data that gets forwarded to agents as additional context. Agents receive these as `forwardedProps`.
```tsx
<CopilotKitProvider
properties={{
userId: "user-123",
theme: "dark",
locale: "en-US",
featureFlags: {
betaFeatures: true,
},
}}
>
{children}
</CopilotKitProvider>
```
### agents\_\_unsafe_dev_only
`Record<string, AbstractAgent>` **(optional, development only)**
<Warning>
This property is intended solely for rapid prototyping during development.
Production deployments require the security, reliability, and performance
guarantees that only the CopilotRuntime can provide.
</Warning>
Local agents for development testing. The key becomes the agent's identifier.
```tsx
import { HttpAgent } from "@ag-ui/client";
const devAgent = new HttpAgent({
url: "http://localhost:8000",
});
<CopilotKitProvider
agents__unsafe_dev_only={{
devAgent,
}}
>
{children}
</CopilotKitProvider>;
```
### useSingleEndpoint
`boolean` **(optional, default: `false`)**
When set to `true`, the provider connects to runtimes that expose the **single-route** transport (a single POST endpoint that multiplexes all runtime actions). Leave this `false` for the default REST-style transport.
Pair this flag with the matching server endpoint helper:
```tsx
<CopilotKitProvider runtimeUrl="/api/copilotkit" useSingleEndpoint>
{children}
</CopilotKitProvider>
```
On the server, mount one of the single-route runtimes (`createCopilotEndpointSingleRoute` for Hono or `createCopilotEndpointSingleRouteExpress` for Express).
### renderToolCalls
`ReactToolCallRenderer[]` **(optional)**
A static list of components to render when specific tools are called. Enables visual feedback for tool execution.
```tsx
const renderToolCalls = [
{
name: "searchProducts",
args: z.object({
query: z.string(),
}),
render: ({ args }) => <div>Searching for: {args.query}</div>,
},
];
<CopilotKitProvider renderToolCalls={renderToolCalls}>
{children}
</CopilotKitProvider>;
```
<Note>
The `renderToolCalls` array must be stable across renders. Define it outside
your component or use `useMemo`. For dynamic tool rendering, use the
`useRenderToolCall` hook instead.
</Note>
### frontendTools
`ReactFrontendTool[]` **(optional)**
A static list of frontend tools that agents can invoke. These are React-specific wrappers around the base `FrontendTool`
type with additional rendering capabilities.
```tsx
const tools = [
{
name: "showNotification",
description: "Display a notification to the user",
parameters: z.object({
message: z.string(),
type: z.enum(["info", "success", "warning", "error"]),
}),
handler: async ({ message, type }) => {
toast[type](message);
return "Notification displayed";
},
},
];
<CopilotKitProvider frontendTools={tools}>{children}</CopilotKitProvider>;
```
<Note>
The `frontendTools` array must be stable across renders. For dynamically
adding/removing tools, use the `useFrontendTool` hook.
</Note>
### humanInTheLoop
`ReactHumanInTheLoop[]` **(optional)**
Tools that require human interaction or approval before execution. These tools pause agent execution until the user
responds.
```tsx
const humanInTheLoop = [
{
name: "confirmAction",
description: "Request user confirmation for an action",
parameters: z.object({
action: z.string(),
details: z.string(),
}),
render: ({ args, resolve }) => (
<ConfirmDialog
action={args.action}
details={args.details}
onConfirm={() => resolve({ confirmed: true })}
onCancel={() => resolve({ confirmed: false })}
/>
),
},
];
<CopilotKitProvider humanInTheLoop={humanInTheLoop}>
{children}
</CopilotKitProvider>;
```
### a2ui
`{ theme?: Theme; catalog?: any; loadingComponent?: React.ComponentType; includeSchema?: boolean }` **(optional)**
Configuration for the A2UI (Agent-to-UI) renderer. The built-in renderer activates automatically when the runtime reports that `a2ui` is configured in `CopilotRuntime`. This prop is only needed to override defaults.
| Option | Type | Description |
| ------------------ | --------------------- | ------------------------------------------------------------------------------------------------------------ |
| `theme` | `Theme` | Override the default A2UI viewer theme. |
| `catalog` | `any` | Custom component catalog. Defaults to `basicCatalog`. |
| `loadingComponent` | `React.ComponentType` | Custom loading component shown while a surface is generating. |
| `includeSchema` | `boolean` | When `true` (default), full component schemas are sent as agent context so the agent knows what's available. |
```tsx
<CopilotKitProvider
runtimeUrl="/api/copilotkit"
a2ui={{
theme: myCustomTheme,
catalog: myCustomCatalog,
loadingComponent: MySpinner,
includeSchema: true,
}}
>
{children}
</CopilotKitProvider>
```
### children
`ReactNode` **(required)**
The React components that will have access to the CopilotKit context.
## Context Value
The provider makes a `CopilotKitContextValue` available to child components through React context:
```typescript
interface CopilotKitContextValue {
copilotkit: CopilotKitCore;
renderToolCalls: ReactToolCallRenderer<any>[];
currentRenderToolCalls: ReactToolCallRenderer<unknown>[];
setCurrentRenderToolCalls: React.Dispatch<
React.SetStateAction<ReactToolCallRenderer<unknown>[]>
>;
}
```
Access this context using the `useCopilotKit` hook:
```tsx
import { useCopilotKit } from "@copilotkit/react-core";
function MyComponent() {
const { copilotkit } = useCopilotKit();
// Access CopilotKitCore instance
const agent = copilotkit.getAgent("assistant");
}
```
## Considerations
### Server-Side Rendering (SSR)
The provider is compatible with SSR but won't fetch runtime information during server-side rendering. The runtime
connection is established only on the client side to prevent blocking SSR.
### Dynamic Updates
You can dynamically update the following props:
- `runtimeUrl`: Changing this will disconnect from the current runtime and connect to the new one
- `headers`: Updates are applied to all future requests
- `properties`: Changes are immediately available to agents
@@ -0,0 +1,159 @@
---
title: FrontendTool
description: "FrontendTool API Reference"
---
`FrontendTool` is a cross-platform type that defines tools (functions) that AI agents can invoke in your frontend
application. These tools enable agents to interact the user, retrieve data, perform actions, and integrate with your
application's functionality.
## What is a FrontendTool?
A FrontendTool represents a capability you expose to AI agents, allowing them to:
- Interact with the user
- Fetch or manipulate application data
- Trigger application workflows
Frontend tools are framework-agnostic and work consistently across all frameworks through `CopilotKitCore`.
## Type Definition
```typescript
type FrontendToolHandlerContext = {
toolCall: ToolCall;
agent: AbstractAgent;
};
type FrontendTool<T extends Record<string, unknown> = Record<string, unknown>> =
{
name: string;
description?: string;
parameters?: z.ZodType<T>;
handler?: (
args: T,
context: FrontendToolHandlerContext,
) => Promise<unknown>;
followUp?: boolean;
agentId?: string;
};
```
## Properties
### name
`string` **(required)**
A unique identifier for the tool. This is the name agents will use to request this tool's execution. Avoid spaces and
special characters.
```typescript
{
name: "searchProducts";
}
```
A special wildcard tool is available with the name `*`. This tool will handle any unmatched tool requests.
### description
`string` **(optional)**
A human-readable description that helps agents understand when and how to use this tool. This description is sent to the
LLM to guide its decision-making.
```typescript
{
name: "searchProducts",
description: "Search for products in the catalog by name, category, or price range"
}
```
### parameters
`z.ZodType<T>` **(optional)**
A Zod schema that defines and validates the tool's input parameters. This ensures type safety and provides automatic
validation of agent-provided arguments.
```typescript
import { z } from "zod";
{
name: "updateUserProfile",
parameters: z.object({
firstName: z.string().optional(),
lastName: z.string().optional(),
email: z.string().email().optional(),
preferences: z.object({
theme: z.enum(["light", "dark"]).optional(),
notifications: z.boolean().optional()
}).optional()
})
}
```
### handler
`(args: T, context: FrontendToolHandlerContext) => Promise<unknown>` **(optional)**
The async function that executes when the agent invokes this tool. It receives the validated arguments and a context
object containing the `toolCall` with metadata about the invocation.
```typescript
{
name: "addToCart",
parameters: z.object({
productId: z.string(),
quantity: z.number().min(1)
}),
handler: async ({productId, quantity}) => {
// Add product to cart
const result = await cartService.addItem(productId, quantity);
// Return a string or serializable object
return {
success: true,
cartTotal: result.total,
itemCount: result.itemCount
};
}
}
```
### followUp
`boolean` **(optional, default: true)**
Controls whether the agent should be automatically re-run after this tool completes. When `true`, the tool's result is
added to the conversation and the agent continues processing. Disable follow-up for final actions that complete a
workflow.
```typescript
{
name: "saveDocument",
handler: async (args) => {
await documentService.save(args);
return "Document saved successfully";
},
followUp: false // Don't re-run agent after saving
}
```
### agentId
`string` **(optional)**
Restricts this tool to a specific agent. When set, only the specified agent can invoke this tool.
```typescript
{
name: "adminAction",
agentId: "admin-assistant",
handler: async (args) => {
// Only the admin-assistant agent can call this
return await performAdminAction(args);
}
}
```
@@ -0,0 +1,81 @@
---
title: ProxiedCopilotRuntimeAgent
description: "ProxiedCopilotRuntimeAgent API Reference"
---
`ProxiedCopilotRuntimeAgent` is a specialized HTTP agent that acts as a proxy between your client application and remote
agents hosted on the `CopilotRuntime` server. It extends the base `HttpAgent` class to provide seamless communication
with runtime-hosted agents.
## Architecture Overview
```mermaid
graph LR
subgraph Client["Client Application"]
Core[CopilotKitCore]
PRA1[ProxiedCopilotRuntimeAgent<br/>customer-support]
PRA2[ProxiedCopilotRuntimeAgent<br/>code-assistant]
PRA3[ProxiedCopilotRuntimeAgent<br/>data-analyst]
Core --> PRA1
Core --> PRA2
Core --> PRA3
end
subgraph Server["CopilotRuntime Server"]
Runtime[Runtime API]
Agent1[Agent: customer-support]
Agent2[Agent: code-assistant]
Agent3[Agent: data-analyst]
Runtime --> Agent1
Runtime --> Agent2
Runtime --> Agent3
end
PRA1 -.-> Runtime
PRA2 -.-> Runtime
PRA3 -.-> Runtime
```
## What is ProxiedCopilotRuntimeAgent?
When `CopilotKitCore` connects to a `CopilotRuntime` server, it discovers available remote agents. For each remote
agent, it creates a `ProxiedCopilotRuntimeAgent` instance that handles all communication with that specific agent
through the runtime's API endpoints.
Key characteristics:
- **Remote Agent Communication**: Handles communication with the remote agent through the runtime's API endpoints
- **Header and Property Forwarding**: Inherits and forwards authentication headers and properties from `CopilotKitCore`
- **Connection and History Management**: Handles loading history and reconnecting to existing live agent sessions
## How does it work?
`CopilotKitCore` automatically creates `ProxiedCopilotRuntimeAgent` instances during runtime discovery:
1. `CopilotKitCore` fetches `/info` from the runtime
2. The runtime responds with available agents
3. For each agent, `CopilotKitCore` creates a `ProxiedCopilotRuntimeAgent`
4. These agents are merged with any local agents
5. The agents become available through `copilotKit.getAgent()`
## Creating a ProxiedCopilotRuntimeAgent
<Note>
In typical usage, you don't create `ProxiedCopilotRuntimeAgent` instances
directly. `CopilotKitCore` automatically creates them when discovering agents
from the runtime.
</Note>
```typescript
import { ProxiedCopilotRuntimeAgent } from "@copilotkit/core";
const agent = new ProxiedCopilotRuntimeAgent({
runtimeUrl: "https://your-runtime.example.com",
agentId: "my-agent",
headers: {
Authorization: "Bearer your-token",
},
});
```
+394
View File
@@ -0,0 +1,394 @@
---
title: Slot System
description: "Deep customization system for CopilotKit components"
---
The Slot System is CopilotKit's approach to component customization. It allows you to customize any part of the UI - from simple styling changes to complete component replacement - all through a consistent, composable API.
## What is the Slot System?
The Slot System:
- Provides four levels of customization depth
- Maintains type safety throughout the customization process
- Supports nested slots for drilling into child components
- Uses automatic memoization for optimal performance
- Works consistently across all CopilotKit components
## Four Customization Levels
Every slot accepts one of four value types, from simplest to most flexible:
### 1. Tailwind Class String
Pass a string of Tailwind classes to add or override styles:
```tsx
<CopilotChat
input="border-2 border-blue-500 rounded-xl"
messageView="space-y-4 p-4"
/>
```
Classes are merged with the component's existing classes using `tailwind-merge`, so conflicting classes are resolved intelligently.
### 2. Props Object
Pass an object of props to customize behavior while keeping the default component:
```tsx
<CopilotChat
input={{
className: "custom-input",
autoFocus: false,
}}
messageView={{
className: "custom-messages",
assistantMessage: {
onThumbsUp: (msg) => trackFeedback(msg.id, "positive"),
},
}}
/>
```
Props are merged with defaults, and you can include nested slots to drill down to child components.
### 3. Custom Component
Replace the component entirely with your own implementation:
```tsx
function CustomInput({ onSubmitMessage, isRunning, ...props }) {
return (
<div className="my-custom-wrapper">
<CopilotChatInput
onSubmitMessage={onSubmitMessage}
isRunning={isRunning}
{...props}
/>
</div>
);
}
<CopilotChat input={CustomInput} />;
```
Custom components receive all the props that would have been passed to the default component.
### 4. Render Function (Children)
For full layout control, use the children render function pattern:
```tsx
function CustomInput(props) {
return (
<CopilotChatInput {...props}>
{({ textArea, sendButton, addMenuButton }) => (
<div className="flex gap-2">
{addMenuButton}
<div className="flex-1">{textArea}</div>
{sendButton}
</div>
)}
</CopilotChatInput>
);
}
<CopilotChat input={CustomInput} />;
```
The render function receives pre-built slot elements that you can arrange however you like.
## Nested Slot Customization
Slots can be nested to customize deeply nested components:
```tsx
<CopilotChat
// Top-level slot
messageView={{
// First level nesting
assistantMessage: {
// Second level nesting
toolbar: "bg-gray-50 rounded-lg",
copyButton: "text-blue-500",
thumbsUpButton: () => null, // Hide the button
},
userMessage: "bg-blue-100 rounded-xl",
}}
input={{
textArea: "text-lg",
sendButton: "bg-green-500",
}}
/>
```
## Hiding Components
To hide a slot entirely, return `null` from a component function:
```tsx
<CopilotChat
input={{
disclaimer: () => null, // Hide disclaimer
startTranscribeButton: () => null, // Hide voice button
}}
messageView={{
assistantMessage: {
regenerateButton: () => null, // Hide regenerate
},
}}
/>
```
## Complete Slot Hierarchy
Here's the full hierarchy of all customizable slots in CopilotChat:
```
CopilotChat
├── chatView
│ ├── messageView
│ │ ├── assistantMessage
│ │ │ ├── markdownRenderer
│ │ │ ├── toolbar
│ │ │ ├── copyButton
│ │ │ ├── thumbsUpButton
│ │ │ ├── thumbsDownButton
│ │ │ ├── readAloudButton
│ │ │ ├── regenerateButton
│ │ │ └── toolCallsView
│ │ ├── userMessage (see CopilotChatUserMessage)
│ │ │ ├── messageRenderer
│ │ │ ├── toolbar
│ │ │ ├── copyButton
│ │ │ ├── editButton
│ │ │ └── branchNavigation
│ │ └── cursor
│ ├── scrollView
│ │ ├── scrollToBottomButton
│ │ └── feather
│ ├── input
│ │ ├── textArea
│ │ ├── sendButton
│ │ ├── startTranscribeButton
│ │ ├── cancelTranscribeButton
│ │ ├── finishTranscribeButton
│ │ ├── addMenuButton
│ │ ├── audioRecorder
│ │ └── disclaimer
│ ├── suggestionView
│ │ ├── container
│ │ └── suggestion
│ └── welcomeScreen
│ └── welcomeMessage
```
## How It Works
Under the hood, the slot system uses three key concepts:
### SlotValue Type
Every slot accepts one of three value types:
```typescript
type SlotValue<C extends React.ComponentType<any>> =
| C // Custom component
| string // Tailwind class string
| Partial<React.ComponentProps<C>>; // Props object
```
### renderSlot Function
The `renderSlot` function resolves a slot value into a React element:
```typescript
// Internal implementation (simplified)
function renderSlot(slot, DefaultComponent, props) {
if (typeof slot === "string") {
// Merge className with existing
return <DefaultComponent {...props} className={twMerge(props.className, slot)} />;
}
if (isReactComponent(slot)) {
// Use custom component
return <slot {...props} />;
}
if (isPropsObject(slot)) {
// Merge props
return <DefaultComponent {...props} {...slot} />;
}
// Use default
return <DefaultComponent {...props} />;
}
```
### WithSlots Type
Components use the `WithSlots` type to define their slot interface:
```typescript
type MyComponentProps = WithSlots<
{
button: typeof MyButton;
input: typeof MyInput;
},
{
value: string;
onChange: (value: string) => void;
}
>;
```
## Best Practices
### 1. Start Simple, Escalate as Needed
Begin with Tailwind classes, then move to props objects, and only use custom components when necessary:
```tsx
// Start here
<CopilotChat input="border-blue-500" />
// Then this
<CopilotChat input={{ className: "border-blue-500", autoFocus: false }} />
// Only if needed
<CopilotChat input={CustomInputComponent} />
```
### 2. Use Props Objects for Nested Customization
When customizing nested slots, use props objects to drill down:
```tsx
<CopilotChat
messageView={{
assistantMessage: {
className: "bg-blue-50",
toolbar: "border-t mt-2",
copyButton: "text-blue-600",
},
}}
/>
```
### 3. Preserve Default Behavior
When creating custom components, spread the remaining props to preserve default functionality:
```tsx
function CustomButton({ onClick, disabled, className, ...props }) {
return (
<button
onClick={onClick}
disabled={disabled}
className={twMerge("my-custom-classes", className)}
{...props} // Preserve other props
/>
);
}
```
### 4. Use Render Functions for Complex Layouts
When you need to completely rearrange elements, use the render function pattern:
```tsx
function CustomLayout(props) {
return (
<CopilotChatInput {...props}>
{({ textArea, sendButton, addMenuButton }) => (
<div className="grid grid-cols-[auto_1fr_auto] gap-2">
{addMenuButton}
{textArea}
{sendButton}
</div>
)}
</CopilotChatInput>
);
}
```
## Examples
### Themed Chat Interface
```tsx
<CopilotChat
className="bg-gray-900 text-white"
messageView={{
className: "p-4",
assistantMessage: {
className: "bg-gray-800 rounded-xl p-4",
toolbar: "border-gray-700",
},
userMessage: "bg-blue-600 text-white rounded-2xl px-4 py-2",
}}
input={{
className: "bg-gray-800 border-gray-700",
sendButton: "bg-blue-600 hover:bg-blue-700",
}}
scrollView={{
feather: "from-gray-900 via-gray-900 to-transparent",
}}
/>
```
### Minimal Interface
```tsx
<CopilotChat
welcomeScreen={false}
input={{
disclaimer: () => null,
startTranscribeButton: () => null,
addMenuButton: () => null,
}}
scrollView={{
scrollToBottomButton: () => null,
feather: () => null,
}}
messageView={{
assistantMessage: {
toolbar: () => null,
},
}}
/>
```
### Feedback-Focused Interface
```tsx
<CopilotChat
messageView={{
assistantMessage: {
onThumbsUp: (msg) => {
analytics.track("positive_feedback", { messageId: msg.id });
toast.success("Thanks for your feedback!");
},
onThumbsDown: (msg) => {
analytics.track("negative_feedback", { messageId: msg.id });
showFeedbackModal(msg);
},
toolbar: "bg-yellow-50 border border-yellow-200 rounded-lg p-2",
thumbsUpButton: "text-green-600 hover:text-green-800",
thumbsDownButton: "text-red-600 hover:text-red-800",
},
}}
/>
```
## Related
- [CopilotChat](/reference/copilot-chat) - Main chat component
- [CopilotChatInput](/reference/copilot-chat-input) - Input component slots
- [CopilotChatAssistantMessage](/reference/copilot-chat-assistant-message) - Assistant message slots
- [CopilotChatUserMessage](/reference/copilot-chat-user-message) - User message slots
- [CopilotChatScrollView](/reference/copilot-chat-scroll-view) - Scroll container slots
- [CopilotChatSuggestionView](/reference/copilot-chat-suggestion-view) - Suggestion chips slots
- [CopilotChatWelcomeScreen](/reference/copilot-chat-welcome-screen) - Welcome screen slots
- [CopilotChatMessageView](/reference/copilot-chat-message-view) - Message list slots
@@ -0,0 +1,391 @@
---
title: useAgentContext
description: "useAgentContext Hook API Reference"
---
`useAgentContext` is a React hook that provides contextual information to AI agents during their execution. It allows
you to dynamically add relevant data that agents can use to make more informed decisions and provide better responses.
## What is useAgentContext?
The useAgentContext hook:
- Provides contextual information to agents
- Automatically manages context lifecycle (add on mount, remove on unmount)
- Updates context when values change
- Helps agents understand application state and user data
## Basic Usage
```tsx
import { useAgentContext } from "@copilotkit/react-core";
function UserPreferences() {
const userSettings = {
theme: "dark",
language: "en",
timezone: "UTC-5",
};
useAgentContext({
description: "User preferences and settings",
value: userSettings,
});
return <div>User preferences loaded</div>;
}
```
## Parameters
The hook accepts a single `Context` object with the following properties:
### description
`string` **(required)**
A clear description of what this context represents. This helps agents understand how to use the provided information.
```tsx
useAgentContext({
description: "Current shopping cart contents",
value: cartItems,
});
```
### value
`any` **(required)**
The actual data to provide as context. Can be any serializable value including objects, arrays, strings, or numbers.
```tsx
useAgentContext({
description: "Current form validation state",
value: {
hasErrors: false,
touchedFields: ["email", "name"],
dirtyFields: ["email"],
isSubmitting: false,
},
});
```
## Examples
### User Preferences Context
```tsx
import { useAgentContext } from "@copilotkit/react-core";
import { useUserPreferences } from "./hooks/useUserPreferences";
function UserPreferencesContext() {
const { preferences, isLoading } = useUserPreferences();
useAgentContext({
description: "User display preferences and settings",
value: {
theme: preferences?.theme || "light",
language: preferences?.language || "en",
timezone: preferences?.timezone || "UTC",
displayDensity: preferences?.displayDensity || "comfortable",
isLoading,
},
});
return null; // Context-only component
}
```
### Form State Context
```tsx
import { useAgentContext } from "@copilotkit/react-core";
import { useState } from "react";
function ContactForm() {
const [formData, setFormData] = useState({
name: "",
email: "",
subject: "",
message: "",
});
// Provide form state to agent for assistance
useAgentContext({
description: "Contact form current state",
value: {
formData,
hasUnsavedChanges: Object.values(formData).some((v) => v !== ""),
isValid: formData.email.includes("@") && formData.name.length > 0,
},
});
return (
<form>
<input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Name"
/>
{/* Rest of form fields */}
</form>
);
}
```
### Application State Context
```tsx
import { useAgentContext } from "@copilotkit/react-core";
import { useLocation } from "react-router-dom";
function AppStateContext() {
const location = useLocation();
const currentTime = new Date().toISOString();
useAgentContext({
description: "Current application state and navigation",
value: {
currentPath: location.pathname,
queryParams: Object.fromEntries(new URLSearchParams(location.search)),
timestamp: currentTime,
},
});
return null;
}
```
### Dynamic Data Context
```tsx
import { useAgentContext } from "@copilotkit/react-core";
import { useEffect, useState } from "react";
function DynamicDataContext() {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const response = await fetch("/api/context-data");
setData(await response.json());
};
fetchData();
}, []);
// Context updates automatically when data changes
useAgentContext({
description: "Dynamic application data",
value: data || { loading: true },
});
return null;
}
```
### Multiple Contexts
```tsx
import { useAgentContext } from "@copilotkit/react-core";
function MultipleContexts() {
const userContext = { id: "123", name: "John" };
const appContext = { version: "1.0.0", features: ["chat", "search"] };
// Use multiple hooks for different contexts
useAgentContext({
description: "User information",
value: userContext,
});
useAgentContext({
description: "Application configuration",
value: appContext,
});
return <div>Multiple contexts provided</div>;
}
```
## Context Lifecycle
### Automatic Management
Context is automatically managed throughout the component lifecycle:
```tsx
function ManagedContext() {
const [count, setCount] = useState(0);
useAgentContext({
description: "Counter state",
value: { count, lastUpdated: Date.now() },
});
// Context is:
// 1. Added when component mounts
// 2. Updated when count changes
// 3. Removed when component unmounts
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
```
### Updates on Change
Context automatically updates when values change:
```tsx
function ReactiveContext() {
const [filters, setFilters] = useState({
category: "all",
priceRange: [0, 100],
});
// Context updates whenever filters change
useAgentContext({
description: "Active search filters",
value: filters,
});
return (
<div>
<select
value={filters.category}
onChange={(e) => setFilters({ ...filters, category: e.target.value })}
>
<option value="all">All</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
</div>
);
}
```
## Best Practices
### Descriptive Context Names
Provide clear, descriptive names for your context:
```tsx
// ✅ Good - Clear and specific
useAgentContext({
description: "E-commerce shopping cart with items and totals",
value: cartData,
});
// ❌ Avoid - Too vague
useAgentContext({
description: "Data",
value: cartData,
});
```
### Structured Data
Organize context data in a structured format:
```tsx
// ✅ Good - Well-structured data
useAgentContext({
description: "Order processing state",
value: {
orderId: "ORD-123",
status: "processing",
items: [{ id: "1", name: "Product", quantity: 2, price: 29.99 }],
customer: {
id: "CUST-456",
email: "user@example.com",
},
timestamps: {
created: "2024-01-01T10:00:00Z",
updated: "2024-01-01T10:30:00Z",
},
},
});
// ❌ Avoid - Unstructured data
useAgentContext({
description: "Order info",
value: "Order ORD-123 for user@example.com with 2 items",
});
```
### Performance Optimization
Memoize complex computed values:
```tsx
import { useMemo } from "react";
function OptimizedContext({ items }) {
const contextValue = useMemo(
() => ({
itemCount: items.length,
totalValue: items.reduce((sum, item) => sum + item.price, 0),
categories: [...new Set(items.map((item) => item.category))],
}),
[items],
);
useAgentContext({
description: "Computed inventory statistics",
value: contextValue,
});
return null;
}
```
## Integration with Agents
Context provided through this hook is available to agents during execution:
```tsx
import {
useAgentContext,
useAgent,
useCopilotKit,
} from "@copilotkit/react-core";
function IntegratedExample() {
const { agent } = useAgent();
const { copilotkit } = useCopilotKit();
const [productSearch, setProductSearch] = useState("");
// Provide search context
useAgentContext({
description: "Current product search parameters",
value: {
searchQuery: productSearch,
resultsPerPage: 20,
sortBy: "relevance",
},
});
const handleSearch = async () => {
// Agent has access to the context when running
agent.addMessage({
id: crypto.randomUUID(),
role: "user",
content: `Help me refine my search for: ${productSearch}`,
});
await copilotkit.runAgent({ agent });
};
return (
<div>
<input
value={productSearch}
onChange={(e) => setProductSearch(e.target.value)}
placeholder="Search products..."
/>
<button onClick={handleSearch}>Get AI Help</button>
</div>
);
}
```
+491
View File
@@ -0,0 +1,491 @@
---
title: useAgent
description: "useAgent Hook API Reference"
---
`useAgent` is a React hook that provides access to [AG-UI](https://ag-ui.com) agents and subscribes to their state
changes. It enables components to interact with agents, access their messages, and respond to updates in real-time.
The hook always returns an agent instance. While the runtime is syncing, it returns a provisional runtime agent; once
the runtime has synced, if the agent id does not exist the hook throws an error.
## What is useAgent?
The useAgent hook:
- Retrieves an agent by ID from the CopilotKit context
- Subscribes to agent state changes (messages, state, run status)
- Automatically triggers component re-renders when the agent updates
- Handles cleanup of subscriptions when the component unmounts
## Basic Usage
```tsx
import { useAgent } from "@copilotkit/react-core";
function ChatComponent() {
const { agent } = useAgent({ agentId: "assistant" });
return (
<div>
<h2>Agent: {agent.id}</h2>
<div>Messages: {agent.messages.length}</div>
<div>Running: {agent.isRunning ? "Yes" : "No"}</div>
</div>
);
}
```
## Parameters
### agentId
`string` **(optional)**
The ID of the agent to retrieve. If not provided, defaults to `"default"`.
```tsx
const { agent } = useAgent({ agentId: "customer-support" });
```
### updates
`UseAgentUpdate[]` **(optional)**
An array of update types to subscribe to. This allows you to optimize re-renders by only subscribing to specific
changes.
```tsx
import { useAgent, UseAgentUpdate } from "@copilotkit/react-core";
const { agent } = useAgent({
agentId: "assistant",
updates: [UseAgentUpdate.OnMessagesChanged],
});
```
Available update types:
- `UseAgentUpdate.OnMessagesChanged` - Updates when messages are added or modified
- `UseAgentUpdate.OnStateChanged` - Updates when agent state changes
- `UseAgentUpdate.OnRunStatusChanged` - Updates when agent starts or stops running
If `updates` is not provided, the hook subscribes to all update types by default.
## Return Value
The hook returns an object with a single property:
### agent
`AbstractAgent`
The agent instance. During runtime synchronization, the hook returns a provisional runtime agent so you can bind UI
immediately. After the runtime has synced (Connected or Error), if the agent id does not exist the hook throws an
error.
## Accessing Agent Messages
The agent's message history is available through the `messages` property. You can read messages and add new ones to
create interactive conversations.
### Reading Messages
```tsx
function SimpleChat() {
const { agent } = useAgent();
return (
<div>
{agent.messages.map((message) => (
<div key={message.id}>
<strong>{message.role}:</strong> {message.content}
</div>
))}
</div>
);
}
```
### Sending Messages
To send a message, add it to the agent and then run the agent:
```tsx
import { useAgent } from "@copilotkit/react-core";
import { useCopilotKit } from "@copilotkit/react-core";
function MessageSender() {
const { agent } = useAgent({ agentId: "assistant" });
const { copilotkit } = useCopilotKit();
const sendMessage = async (content: string) => {
// Add message to agent
agent.addMessage({
id: crypto.randomUUID(),
role: "user",
content,
});
// Run the agent to get a response
await copilotkit.runAgent({
agent,
agentId: "assistant",
});
};
return (
<div>
<button onClick={() => sendMessage("Hello!")} disabled={agent.isRunning}>
Send Hello
</button>
</div>
);
}
```
## Accessing and Updating Shared State
Shared state enables real-time collaboration between users and agents. Both can read and modify the state, creating a
synchronized workspace for interactive features.
### Understanding Shared State
The agent's state is a shared data structure that:
- Can be read by both your application and the agent
- Can be modified by both parties
- Automatically triggers re-renders when changed
- Persists throughout the conversation
State updates cause re-renders when:
- You call `agent.setState()` from your application
- The agent modifies the state during execution
- Any state change occurs, regardless of source
### Reading State
```tsx
function StateDisplay() {
const { agent } = useAgent({
updates: [UseAgentUpdate.OnStateChanged], // Subscribe to state changes
});
return (
<div>
<h3>Current State</h3>
<pre>{JSON.stringify(agent.state, null, 2)}</pre>
</div>
);
}
```
### Updating State
```tsx
function StateController() {
const { agent } = useAgent({
updates: [UseAgentUpdate.OnStateChanged],
});
const updateState = (key: string, value: any) => {
// Update the shared state
agent.setState({
...agent.state,
[key]: value,
});
// This will trigger re-renders for all components
// subscribed to OnStateChanged
};
return (
<div>
<button
onClick={() => updateState("counter", (agent.state.counter || 0) + 1)}
>
Increment Counter: {agent.state.counter || 0}
</button>
</div>
);
}
```
### Collaborative Features
Shared state enables collaborative features where users and agents work together:
```tsx
function CollaborativeTodo() {
const { agent } = useAgent({
updates: [UseAgentUpdate.OnStateChanged],
});
const { copilotkit } = useCopilotKit();
const todos = agent.state.todos || [];
const addTodo = (text: string) => {
agent.setState({
...agent.state,
todos: [...todos, { id: crypto.randomUUID(), text, done: false }],
});
};
const toggleTodo = (id: string) => {
agent.setState({
...agent.state,
todos: todos.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo,
),
});
};
const askAgentToOrganize = async () => {
// Add a message asking the agent to organize todos
agent.addMessage({
id: crypto.randomUUID(),
role: "user",
content: "Please organize my todos by priority",
});
// The agent can read and modify the todos in agent.state
await copilotkit.runAgent({ agent });
// After the agent runs, the state will be updated
// and the component will re-render automatically
};
return (
<div>
<h3>Shared Todo List</h3>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
<span className={todo.done ? "done" : ""}>{todo.text}</span>
</li>
))}
</ul>
<button onClick={() => addTodo(prompt("New todo:") || "")}>
Add Todo
</button>
<button onClick={askAgentToOrganize}>Ask Agent to Organize</button>
</div>
);
}
```
## Examples
### Optimized Updates
Subscribe only to message changes to avoid unnecessary re-renders:
```tsx
import { useAgent, UseAgentUpdate } from "@copilotkit/react-core";
function MessageList() {
const { agent } = useAgent({
updates: [UseAgentUpdate.OnMessagesChanged],
});
return (
<ul>
{agent.messages.map((msg) => (
<li key={msg.id}>{msg.content}</li>
))}
</ul>
);
}
```
### Run Status Indicator
Show a loading indicator when the agent is processing:
```tsx
import { useAgent, UseAgentUpdate } from "@copilotkit/react-core";
function RunStatus() {
const { agent } = useAgent({
agentId: "assistant",
updates: [UseAgentUpdate.OnRunStatusChanged],
});
return (
<div className={`status ${agent.isRunning ? "running" : "idle"}`}>
{agent.isRunning ? "🔄 Processing..." : "✅ Ready"}
</div>
);
}
```
### Multiple Agents
Access different agents in different components:
```tsx
function DualAgentView() {
const { agent: primaryAgent } = useAgent({
agentId: "primary-assistant",
});
const { agent: supportAgent } = useAgent({
agentId: "support-assistant",
});
return (
<div className="dual-view">
<div className="primary">
<h3>Primary Assistant</h3>
<div>Messages: {primaryAgent.messages.length}</div>
</div>
<div className="support">
<h3>Support Assistant</h3>
<div>Messages: {supportAgent.messages.length}</div>
</div>
</div>
);
}
```
### No Updates Subscription
For static access without subscribing to updates:
```tsx
const { agent } = useAgent({
agentId: "assistant",
updates: [], // No subscriptions
});
// Component won't re-render on agent changes
```
### Custom Message Rendering with Optimized Updates
```tsx
import { useAgent, UseAgentUpdate } from "@copilotkit/react-core";
import { Message } from "@ag-ui/core";
function MessageRenderer() {
const { agent } = useAgent({
updates: [UseAgentUpdate.OnMessagesChanged], // Only re-render on message changes
});
if (agent.messages.length === 0) {
return <div>No messages yet</div>;
}
const renderMessage = (message: Message) => {
switch (message.role) {
case "user":
return (
<div className="user-message">
<span className="avatar">👤</span>
<span className="content">{message.content}</span>
</div>
);
case "assistant":
return (
<div className="assistant-message">
<span className="avatar">🤖</span>
<span className="content">{message.content}</span>
</div>
);
default:
return null;
}
};
return (
<div className="message-list">
{agent.messages.map((msg) => (
<div key={msg.id}>{renderMessage(msg)}</div>
))}
</div>
);
}
```
## Performance Considerations
### Selective Updates
By default, `useAgent` subscribes to all update types, which can cause frequent re-renders. Use the `updates` parameter
to subscribe only to the changes you need:
```tsx
// ❌ Subscribes to all updates (may cause unnecessary re-renders)
const { agent } = useAgent({ agentId: "assistant" });
// ✅ Only subscribes to message changes
const { agent } = useAgent({
agentId: "assistant",
updates: [UseAgentUpdate.OnMessagesChanged],
});
// ✅ Only subscribes to run status changes
const { agent } = useAgent({
agentId: "assistant",
updates: [UseAgentUpdate.OnRunStatusChanged],
});
```
### Component Splitting
Split components by update type to optimize rendering:
```tsx
// Message display component - only updates on message changes
function Messages() {
const { agent } = useAgent({
updates: [UseAgentUpdate.OnMessagesChanged],
});
// Render messages...
}
// Status indicator - only updates on run status changes
function StatusIndicator() {
const { agent } = useAgent({
updates: [UseAgentUpdate.OnRunStatusChanged],
});
// Render status...
}
// Parent component doesn't need to subscribe
function ChatView() {
return (
<>
<StatusIndicator />
<Messages />
</>
);
}
```
## Error Handling
After the runtime has synced (Connected or Error), `useAgent` throws if the requested agent id does not exist. During
runtime syncing, a provisional agent is returned. To avoid runtime errors:
- Ensure the agent id you request is registered (via `agents__unsafe_dev_only` or exposed by your runtime).
- Optionally wrap the component that calls `useAgent` in an error boundary to render a fallback UI if the agent is
missing due to misconfiguration in development.
Example configuration with a local agent for development:
```tsx
<CopilotKitProvider agents__unsafe_dev_only={{ myAgent: new MyAgent() }}>
<App />
</CopilotKitProvider>
```
@@ -0,0 +1,150 @@
---
title: useCopilotKit
description: "useCopilotKit Hook API Reference"
---
`useCopilotKit` is a React hook that provides access to the CopilotKit context, including the core instance, tool
rendering capabilities, and runtime connection status. It's the primary way to interact with CopilotKit's core
functionality from React components.
## What is useCopilotKit?
The useCopilotKit hook:
- Provides access to the `CopilotKitCore` instance for agent and tool management
- Exposes render tool calls for visual tool execution feedback
- Subscribes to runtime connection status changes
- Enables programmatic control over agents, tools, and context
## Basic Usage
```tsx
import { useCopilotKit } from "@copilotkit/react-core";
function MyComponent() {
const { copilotkit } = useCopilotKit();
// Access runtime status
console.log("Runtime status:", copilotkit.runtimeConnectionStatus);
// Get an agent
const agent = copilotkit.getAgent("assistant");
return (
<div>Connected: {copilotkit.runtimeConnectionStatus === "Connected"}</div>
);
}
```
## Return Value
The hook returns a `CopilotKitContextValue` object with the following properties:
### copilotkit
`CopilotKitCore`
The core CopilotKit instance that manages agents, tools, context, and runtime connections. This is the main interface
for interacting with CopilotKit programmatically.
### renderToolCalls
`ReactToolCallRenderer<any>[]`
An array of tool call render configurations defined at the provider level. These are used to render visual feedback when
tools are executed.
### currentRenderToolCalls
`ReactToolCallRenderer<unknown>[]`
The current list of render tool calls, including both static configurations and dynamically registered ones.
### setCurrentRenderToolCalls
`React.Dispatch<React.SetStateAction<ReactToolCallRenderer<unknown>[]>>`
A setter function to update the current render tool calls. Useful for dynamically adding or removing tool renderers.
## Examples
### Running an Agent
```tsx
import { useCopilotKit } from "@copilotkit/react-core";
import { useAgent } from "@copilotkit/react-core";
function AgentRunner() {
const { copilotkit } = useCopilotKit();
const { agent } = useAgent({ agentId: "assistant" });
const [message, setMessage] = useState("");
const handleSubmit = async () => {
if (!message) return;
// Add user message
agent.addMessage({
id: crypto.randomUUID(),
role: "user",
content: message,
});
// Run the agent
await copilotkit.runAgent({ agent, agentId: "assistant" });
setMessage("");
};
return (
<div>
<input
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Type a message..."
/>
<button onClick={handleSubmit}>Send</button>
</div>
);
}
```
### Monitoring Runtime Connection
```tsx
import { useCopilotKit } from "@copilotkit/react-core";
function ConnectionStatus() {
const { copilotkit } = useCopilotKit();
const getStatusColor = () => {
switch (copilotkit.runtimeConnectionStatus) {
case "Connected":
return "green";
case "Connecting":
return "yellow";
case "Error":
return "red";
default:
return "gray";
}
};
return (
<div className="connection-status">
<div className={`indicator ${getStatusColor()}`} />
<span>Runtime: {copilotkit.runtimeConnectionStatus}</span>
{copilotkit.runtimeVersion && <span>v{copilotkit.runtimeVersion}</span>}
</div>
);
}
```
## Automatic Re-renders
The hook automatically subscribes to runtime connection status changes and triggers re-renders when:
- The runtime connects or disconnects
- The connection status changes
- Connection errors occur
This ensures your components always display the current connection state without manual subscriptions.
@@ -0,0 +1,281 @@
---
title: useFrontendTool
description: "useFrontendTool Hook API Reference"
---
`useFrontendTool` is a React hook that dynamically registers tools (functions) that AI agents can invoke in your
application. It enables components to expose interactive capabilities to agents, with optional visual rendering of tool
execution.
## What is useFrontendTool?
The useFrontendTool hook:
- Registers tools dynamically when components mount
- Automatically cleans up tools when components unmount
- Optionally registers visual renderers for tool execution feedback in the chat interface
- Supports agent-specific tool registration
- Handles tool lifecycle management automatically
## Basic Usage
```tsx
import { useFrontendTool } from "@copilotkit/react-core";
import { z } from "zod";
function SearchComponent() {
useFrontendTool({
name: "searchProducts",
description: "Search for products in the catalog",
parameters: z.object({
query: z.string(),
category: z.string().optional(),
}),
handler: async ({ query, category }) => {
const results = await searchAPI(query, category);
return results;
},
});
return <div>Rest of your component...</div>;
}
```
## Parameters
The hook accepts:
- A required `ReactFrontendTool` object describing the tool
- An optional dependency array to control when the tool is re-registered
### name
`string` **(required)**
A unique identifier for the tool. This is the name agents will use to request this tool's execution.
```tsx
useFrontendTool({
name: "calculateTotal",
// ...
});
```
### description
`string` **(optional)**
A description that helps agents understand when and how to use this tool. This is sent to the LLM to guide its
decision-making.
```tsx
useFrontendTool({
name: "fetchUserData",
description:
"Retrieve detailed user profile information including preferences and history",
// ...
});
```
### parameters
`z.ZodType<T>` **(optional)**
A Zod schema defining the tool's input parameters. Provides type safety and automatic validation.
```tsx
import { z } from "zod";
useFrontendTool({
name: "updateSettings",
parameters: z.object({
theme: z.enum(["light", "dark", "auto"]),
language: z.string(),
notifications: z.boolean(),
}),
handler: async ({ theme, language, notifications }) => {
// settings is fully typed based on the schema
await updateUserSettings({ theme, language, notifications });
return "Settings updated successfully";
},
});
```
### handler
`(args: T, toolCall: ToolCall) => Promise<unknown>` **(optional)**
The async function executed when the agent invokes this tool. Receives validated arguments and metadata about the
invocation.
```tsx
useFrontendTool({
name: "addToCart",
parameters: z.object({
productId: z.string(),
quantity: z.number().min(1),
}),
handler: async ({ productId, quantity }, toolCall) => {
console.log(`Tool called by agent at ${toolCall.id}`);
const result = await cartService.addItem(productId, quantity);
return {
success: true,
cartTotal: result.total,
};
},
});
```
### render
`(props: { name: string; args: T; result?: unknown; status: ToolCallStatus }) => React.ReactNode` **(optional)**
A React component that renders visual feedback when the tool is executed. This appears in the chat interface to show
tool execution progress and results.
```tsx
import { ToolCallStatus } from "@copilotkit/core";
useFrontendTool({
name: "generateChart",
parameters: z.object({
data: z.array(z.number()),
type: z.enum(["bar", "line", "pie"]),
}),
handler: async ({ data, type }) => {
return generateChartData(data, type);
},
render: ({ name, args, result, status }) => (
<div className="tool-execution">
<h3>Generating {args.type} chart...</h3>
{status === ToolCallStatus.InProgress && <Spinner />}
{status === ToolCallStatus.Complete && result && (
<ChartDisplay data={result} />
)}
</div>
),
});
```
### followUp
`boolean` **(optional, default: true)**
Controls whether the agent should automatically continue after this tool completes. Set to `false` for final actions.
```tsx
useFrontendTool({
name: "submitForm",
handler: async (formData) => {
await submitToServer(formData);
return "Form submitted successfully";
},
followUp: false, // Don't continue after submission
});
```
### agentId
`string` **(optional)**
Restricts this tool to a specific agent. Only the specified agent can invoke this tool.
```tsx
useFrontendTool({
name: "adminAction",
agentId: "admin-assistant",
handler: async (args) => {
return await performAdminAction(args);
},
});
```
### deps (second argument)
`ReadonlyArray<unknown>` **(optional)**
Additional dependencies that should trigger re-registration of the tool. By default, the hook only depends on the tool
name and CopilotKit instance to avoid re-register loops from object identity changes. Pass a dependency array as the
second argument when the tool's configuration is derived from changing props or state:
```tsx
function PriceTool({ currency }: { currency: string }) {
useFrontendTool(
{
name: "convertPrice",
handler: async (args) => convertPrice(args.amount, currency),
},
[currency],
);
return null;
}
```
## Lifecycle Management
### Automatic Registration
Tools are automatically registered when the component mounts:
```tsx
function DynamicTool() {
// Tool is registered when this component mounts
useFrontendTool({
name: "dynamicAction",
handler: async () => "Action performed",
});
return <div>Tool available</div>;
}
// Usage
function App() {
const [showTool, setShowTool] = useState(false);
return (
<div>
<button onClick={() => setShowTool(!showTool)}>Toggle Tool</button>
{showTool && <DynamicTool />} {/* Tool only available when mounted */}
</div>
);
}
```
### Cleanup on Unmount
Tools are automatically removed when components unmount, but their renderers persist to maintain chat history:
```tsx
function TemporaryTool() {
useFrontendTool({
name: "tempAction",
handler: async () => "Temporary action",
render: ({ status }) => <div>Temp tool: {status}</div>,
});
// When this component unmounts:
// - The tool handler is removed (agent can't call it anymore)
// - The renderer persists (previous executions still visible in chat)
return null;
}
```
### Tool Override Warning
If a tool with the same name already exists, it will be overridden with a console warning:
```tsx
// First registration
useFrontendTool({
name: "search",
handler: async () => "Search v1",
});
// Second registration (overrides first)
useFrontendTool({
name: "search", // Same name - will override with warning
handler: async () => "Search v2",
});
```
@@ -0,0 +1,223 @@
---
title: useHumanInTheLoop
description: "useHumanInTheLoop Hook API Reference"
---
`useHumanInTheLoop` is a React hook that creates interactive tools requiring human approval or input before the agent
can proceed. It enables you to build approval workflows, confirmation dialogs, and interactive decision points where
human oversight is needed.
## What is useHumanInTheLoop?
The useHumanInTheLoop hook:
- Creates tools that pause execution until human input is received
- Manages status transitions (InProgress → Executing → Complete)
- Provides a `respond` callback for user interactions
- Automatically handles promise resolution for agent continuation
- Renders interactive UI components in the chat interface
## Basic Usage
```tsx
import { useHumanInTheLoop } from "@copilotkit/react-core";
import { ToolCallStatus } from "@copilotkit/core";
import { z } from "zod";
function ApprovalComponent() {
useHumanInTheLoop({
name: "requireApproval",
description: "Requires human approval before proceeding",
parameters: z.object({
action: z.string(),
reason: z.string(),
}),
render: ({ status, args, respond }) => {
if (status === ToolCallStatus.Executing && respond) {
return (
<div className="approval-request">
<p>Approve action: {args.action}</p>
<p>Reason: {args.reason}</p>
<button onClick={() => respond({ approved: true })}>Approve</button>
<button onClick={() => respond({ approved: false })}>Deny</button>
</div>
);
}
return <div>Status: {status}</div>;
},
});
return <div>Approval system active</div>;
}
```
## Parameters
The hook accepts:
- A required `ReactHumanInTheLoop` object describing the tool
- An optional `options` object for controlling dependency behavior
### name
`string` **(required)**
A unique identifier for the human-in-the-loop tool.
```tsx
useHumanInTheLoop({
name: "confirmDeletion",
// ...
});
```
### description
`string` **(optional)**
Describes the tool's purpose to help agents understand when human approval is needed.
```tsx
useHumanInTheLoop({
name: "sensitiveAction",
description: "Requires human confirmation for sensitive operations",
// ...
});
```
### parameters
`z.ZodType<T>` **(optional)**
A Zod schema defining the parameters the agent will provide when requesting human input.
```tsx
import { z } from "zod";
useHumanInTheLoop({
name: "reviewChanges",
parameters: z.object({
changes: z.array(z.string()),
impact: z.enum(["low", "medium", "high"]),
estimatedTime: z.number(),
}),
// ...
});
```
### render
`React.ComponentType` **(required)**
A React component that renders the interactive UI. It receives different props based on the current status:
#### Status: InProgress
Initial state when the tool is called but not yet executing:
```tsx
{
name: string;
description: string;
args: Partial<T>; // Partial arguments as they're being streamed
status: ToolCallStatus.InProgress;
result: undefined;
respond: undefined;
}
```
#### Status: Executing
Active state where user interaction is required:
```tsx
{
name: string;
description: string;
args: T; // Complete arguments
status: ToolCallStatus.Executing;
result: undefined;
respond: (result: unknown) => Promise<void>; // Callback to provide response
}
```
#### Status: Complete
Final state after user has responded:
```tsx
{
name: string;
description: string;
args: T; // Complete arguments
status: ToolCallStatus.Complete;
result: string; // The response provided via respond()
respond: undefined;
}
```
### followUp
`boolean` **(optional, default: true)**
Controls whether the agent continues after receiving the human response. Provided for completeness, but typically you
would want the agent to continue (default behavior).
```tsx
useHumanInTheLoop({
name: "finalConfirmation",
followUp: false, // Don't continue after confirmation
// ...
});
```
### agentId
`string` **(optional)**
Restricts this tool to a specific agent.
```tsx
useHumanInTheLoop({
name: "adminApproval",
agentId: "admin-assistant",
// ...
});
```
## Options
### deps (second argument)
`ReadonlyArray<unknown>` **(optional)**
Additional dependencies that should trigger re-registration of the human-in-the-loop tool. By default, the hook only
depends on stable keys like the tool name and CopilotKit instance to avoid re-register loops from object identity
changes. Pass a dependency array as the second argument when the tool configuration is derived from changing props or
state:
```tsx
function VersionedApproval({ version }: { version: number }) {
useHumanInTheLoop(
{
name: "versionedApproval",
description: `Approve deployment v${version}`,
// ...
},
[version],
);
return null;
}
```
## Differences from useFrontendTool
While `useFrontendTool` executes immediately and returns results, `useHumanInTheLoop`:
- Pauses execution until human input is received
- Provides a `respond` callback during the Executing state
- Manages status transitions automatically
- Is designed specifically for approval workflows and interactive decisions
Choose `useHumanInTheLoop` when you need human oversight, and `useFrontendTool` for automated tool execution.
@@ -0,0 +1,277 @@
---
title: useRenderToolCall
description: "useRenderToolCall Hook API Reference"
---
<Warning>
You would use this hook if you use the headless functionality of CopilotKit,
rendering your own chat UI instead of the default `CopilotChat`. If you are
using `CopilotChat`, you don't need to use this hook.
</Warning>
`useRenderToolCall` is a React hook that provides a function to render visual representations of tool calls in the chat
interface. It manages the rendering of tool execution states (InProgress, Executing, Complete) based on configured
render functions.
## What is useRenderToolCall?
The useRenderToolCall hook:
- Returns a render function for tool calls
- Automatically determines the appropriate status (InProgress, Executing, or Complete)
- Manages tool execution state transitions
- Supports agent-specific and wildcard renderers
- Integrates with CopilotKit's tool rendering system
## Basic Usage
```tsx
import { useRenderToolCall } from "@copilotkit/react-core";
import { ToolCall } from "@ag-ui/core";
function ToolCallDisplay({ toolCall, toolMessage }) {
const renderToolCall = useRenderToolCall();
return (
<div className="tool-call-container">
{renderToolCall({ toolCall, toolMessage })}
</div>
);
}
```
## Return Value
The hook returns a function with the following signature:
```tsx
(props: UseRenderToolCallProps) => React.ReactElement | null;
```
### UseRenderToolCallProps
```tsx
interface UseRenderToolCallProps {
toolCall: ToolCall; // The tool call to render
toolMessage?: ToolMessage; // Optional result message
}
```
## How It Works
### Status Determination
The render function automatically determines the tool's status:
1. **Complete**: When a `toolMessage` is provided
2. **Executing**: When the tool is currently running (tracked internally)
3. **InProgress**: Default state when neither complete nor executing
### Renderer Selection
The function selects renderers based on priority:
1. **Exact match** with matching agentId
2. **Exact match** without agentId (global)
3. **Exact match** (any agentId)
4. **Wildcard** renderer (`*`)
5. **No render** (returns null)
## Examples
### Basic Tool Call Rendering
```tsx
import { useRenderToolCall } from "@copilotkit/react-core";
import { AssistantMessage } from "@ag-ui/core";
function ChatMessage({ message }: { message: AssistantMessage }) {
const renderToolCall = useRenderToolCall();
if (!message.toolCalls) {
return <div>{message.content}</div>;
}
return (
<div>
{message.content}
{message.toolCalls.map((toolCall) => (
<div key={toolCall.id} className="tool-call">
{renderToolCall({ toolCall })}
</div>
))}
</div>
);
}
```
### With Tool Results
```tsx
import { useRenderToolCall } from "@copilotkit/react-core";
import { Message, ToolMessage } from "@ag-ui/core";
function ChatWithResults({
message,
allMessages,
}: {
message: AssistantMessage;
allMessages: Message[];
}) {
const renderToolCall = useRenderToolCall();
return (
<>
{message.toolCalls?.map((toolCall) => {
// Find the corresponding result message
const toolMessage = allMessages.find(
(m): m is ToolMessage =>
m.role === "tool" && m.toolCallId === toolCall.id,
);
return (
<div key={toolCall.id}>
{renderToolCall({
toolCall,
toolMessage, // Pass result if available
})}
</div>
);
})}
</>
);
}
```
## Integration with Tool Renderers
The hook works with tool renderers defined at various levels:
### Provider-Level Renderers
Renderers defined in `CopilotKitProvider`:
```tsx
import {
CopilotKitProvider,
defineToolCallRenderer,
} from "@copilotkit/react-core";
const searchRenderer = defineToolCallRenderer({
name: "search",
render: ({ args, status }) => <SearchDisplay {...args} status={status} />,
});
function App() {
return (
<CopilotKitProvider renderToolCalls={[searchRenderer]}>
{/* Components using useRenderToolCall will use this renderer */}
</CopilotKitProvider>
);
}
```
### Dynamic Tool Renderers
Renderers registered via `useFrontendTool`:
```tsx
function DynamicTool() {
useFrontendTool({
name: "dynamicAction",
handler: async (args) => {
/* ... */
},
render: ({ args, status }) => <div>Dynamic tool: {status}</div>,
});
// This renderer is automatically available to useRenderToolCall
return null;
}
```
### Wildcard Renderer
A fallback renderer for unmatched tools:
```tsx
const wildcardRenderer = defineToolCallRenderer({
name: "*",
render: ({ name, args, status }) => (
<div className="unknown-tool">
<span>Unknown tool: {name}</span>
<span>Status: {status}</span>
</div>
),
});
```
## Status Lifecycle
The hook manages three status states automatically:
### InProgress
Initial state when tool is called but not executing:
```tsx
// Renderer receives:
{
name: string;
args: Partial<T>; // May be incomplete during streaming
status: ToolCallStatus.InProgress;
result: undefined;
}
```
### Executing
Active execution state:
```tsx
// Renderer receives:
{
name: string;
args: T; // Complete arguments
status: ToolCallStatus.Executing;
result: undefined;
}
```
### Complete
Final state with results:
```tsx
// Renderer receives:
{
name: string;
args: T; // Complete arguments
status: ToolCallStatus.Complete;
result: string; // Tool execution result
}
```
## Agent-Specific Rendering
The hook supports agent-specific renderers:
```tsx
import { useCopilotChatConfiguration } from "@copilotkit/react-core";
function AgentAwareRendering() {
const renderToolCall = useRenderToolCall();
const config = useCopilotChatConfiguration();
// The hook automatically selects renderers based on the current agent
// Priority: agent-specific > global > wildcard
return (
<div>
<h3>Agent: {config?.agentId || "default"}</h3>
{/* Renders will use agent-appropriate renderers */}
{toolCalls.map((tc) => renderToolCall({ toolCall: tc }))}
</div>
);
}
```
@@ -0,0 +1,54 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
.next/
/out/
# turbo
.turbo
# Build outputs
dist/
# 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
# lock files
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lockb
@@ -0,0 +1,135 @@
# langgraph-js-starter
## 0.1.7 — 2026-04-21
### Fixed
- Hardened `emit_unknown_tools_notice` and `intercept_frontend_tools`
against OpenAI tool_call invariant violations and prior-stash
clobbering.
- `useCopilotAction` deps on `setThemeColor` and `getWeather`.
- `next lint` removal in Next 16 — replaced with direct ESLint invocation.
- `route.ts` LANGSMITH warn now gates on NODE_ENV like DEPLOYMENT_URL.
- `route.ts` log prefixes distinguish runtime-construction from dispatch
failures.
- `parseInterruptPayload` single-return-value, caller-owned log.
### Changed
- LICENSE copyright attribution: `2025-2026 CopilotKit` (was: individual).
- README: fixed broken `pnpm --filter` example in troubleshooting;
replaced inline `echo > .env` with `cp .env.example .env` + edit;
corrected project-structure diagram comment to reference
`pnpm-workspace.yaml`.
- `apps/web/tsconfig.json`: dropped dead `.next/dev/types/**` include.
- `apps/web/.env.example`: clarified that LANGSMITH\_\* vars forward to the
agent.
- Root `.gitignore`: ignore `dist/`.
- `turbo.json`: add `start` task.
- `apps/web/project.json`: `cache: false` on `build` target for Nx parity
with package.json.
- `apps/agent/tsconfig.json`: set `noEmit: true` so direct `tsc`
invocation matches script-driven builds.
## 0.1.6 — 2026-04-21
### Changed
- Added `turbo.json` at the starter root so `turbo run dev/build/lint`
works when the starter is extracted standalone (root scripts delegate
to turbo; missing config previously broke extraction).
- Added `pnpm-workspace.yaml` so pnpm v9+ reliably links
`workspace:*` deps on standalone extraction; the `workspaces` array in
the root `package.json` is not honored by pnpm on its own.
- Bumped the agent `tsconfig.json` `target` from `es2016` to `ES2022`
(with explicit `lib: ["ES2022"]`, no DOM) to match the declared
Node 20+ runtime baseline.
- Added `build` and `lint` scripts to `apps/agent/package.json`
(`tsc -p tsconfig.json --noEmit` — identical to what `project.json`
declares) so `turbo run build/lint` and nx targets agree.
- Filled in the empty agent `description` and `author` fields.
- Added `LANGSMITH_TRACING=true` (commented) to `apps/web/.env.example`
to match the agent's `.env.example`.
- Added `"baseUrl": "."` to `apps/web/tsconfig.json` so the `@/*` path
alias resolves reliably across IDEs and tooling.
- Removed Python-specific entries (`venv/`, `__pycache__/`, `*.pyc`)
from `apps/agent/.gitignore` — leftovers from a forked Python
LangGraph starter; this is a TypeScript project.
- Added a copyright year to `LICENSE` (MIT convention requires one).
### Versioning
- Root, `apps/agent`, and `apps/web` all bumped from `0.1.5` to `0.1.6`
in lockstep per the starter's shared-version convention.
## 0.1.5 — 2026-04-17
### Renamed
- Renamed directory from `interrupts-langraph` to `interrupts-langgraph`
(spelling fix). The rename itself is code-neutral; hardening described
below.
### Changed
- Tightened types in the agent graph (no `as any`, structural message
narrowing) and added an explicit `bindTools` capability guard.
- `shouldContinue` now evaluates ALL tool calls on an AIMessage and
routes to `tool_node` whenever ANY call targets a registered backend
tool. Previously a mixed batch (frontend action + backend tool) could
silently drop the backend call. Each unknown tool-call name emits its
own `console.warn`; routing then keeps the batch on `tool_node` when
any known backend tool is present (ToolNode will emit an error
ToolMessage for unknown tool names; the graph then loops back to
`chat_node` with that error in context) and falls through to `END`
otherwise.
- Validated the interrupt payload on the web side via
`parseInterruptPayload`; malformed payloads now render a cancellation
fallback instead of crashing the renderer. Arrays are explicitly
rejected (previously passed the `typeof === "object"` check and were
coerced to the `Record` lookup path).
- Validated the resumed interrupt value on the agent side via a zod
schema (`ApprovalResumeSchema`); an out-of-band Client resuming with
the wrong shape now fails loudly at the tool boundary instead of
silently branching to "cancelled".
- `deleteProverb` on the web side now uses a functional `setState`
updater so concurrent state writes don't race.
- React key for the proverb list is `${index}-${proverb}` (index + content
composite). Chosen because plain `proverb` collides on duplicates and
plain `index` destabilizes rows during agent-driven inserts. Migrating
the underlying state to `{id, text}` objects is the proper long-term
fix and is deferred.
- Removed the unused `starterAgent` alias; extracted the model name
to a `MODEL` constant for single-point swaps.
- Wrapped `handleRequest` with a structured error response in the web
runtime route (`apps/web/src/app/api/copilotkit/route.ts`), so
unhandled exceptions surface as a structured 500 JSON response rather
than the raw Next.js error page.
- Corrected port references in the README (8125 everywhere).
- Replaced the placeholder Next metadata with a real title/description.
### Dependencies
- `@types/node` ^20 → ^22.19.11
- `typescript` ^5 → ^5.9.3
- `zod` ^3.24.4 → ^3.25.76
- Agent `@langchain/langgraph``1.1.5` (previously pinned via a root
`overrides` entry at `1.0.2`; the override has been removed and the
version is now declared directly on `apps/agent/package.json`).
- Agent `@langchain/core``^1.1.26`.
- Dropped the dead `@langchain/core` override from the starter root; it
had no effect inside the monorepo pnpm workspace and would cap the
agent's `^1.1.26` requirement if the starter were extracted.
- Removed root `overrides` entry for `@langchain/langgraph` (was `1.0.2`);
the agent owns its own version now.
### Versioning
- Sub-app versions synced to the root: `apps/agent` `0.0.1``0.1.5` and
`apps/web` `0.1.0``0.1.5`. Convention: sub-app versions track the
root starter version so changelog entries and `package.json` reads
stay consistent across the workspace.
## 0.1.1 0.1.4
Internal only (dependency sync / tooling bumps).
+21
View File
@@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2025-2026 CopilotKit
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.
+107
View File
@@ -0,0 +1,107 @@
# CopilotKit <> LangGraph Starter
This is a starter template for building AI agents using [LangGraph](https://www.langchain.com/langgraph) and [CopilotKit](https://copilotkit.ai). It provides a modern Next.js application with an integrated LangGraph agent to be built on top of.
This project is organized as a monorepo using [pnpm workspaces](https://pnpm.io/workspaces).
## Project Structure
```
.
├── apps/
│ ├── web/ # Next.js frontend application
│ └── agent/ # LangGraph agent
└── package.json # pnpm workspaces via pnpm-workspace.yaml
```
## Prerequisites
- Node.js 20+
- [pnpm](https://pnpm.io/installation) 9.15.0 or later
- OpenAI API Key (for the LangGraph agent)
## Getting Started
1. Install all dependencies (this installs everything for both apps):
```bash
pnpm install
```
2. Set up your OpenAI API key by copying the example env file and editing it:
```bash
cp apps/agent/.env.example apps/agent/.env
```
Then open `apps/agent/.env` in your editor and fill in `OPENAI_API_KEY` with your key.
For production, also copy `apps/web/.env.example` to `apps/web/.env` and set `LANGGRAPH_DEPLOYMENT_URL` to your deployed agent URL.
3. Start the development servers:
```bash
pnpm dev
```
This will start both the Next.js app (on port 3000) and the LangGraph agent (on port 8125) via Turbo (installed as a dev dependency).
## Available Scripts
All scripts use Turbo to run tasks across the workspace:
- `pnpm dev` - Starts both the web app and agent servers in development mode
- `pnpm build` - Builds all apps for production
- `pnpm lint` - Runs linting across all apps
### Running Scripts for Individual Apps
You can also run scripts for individual apps using pnpm's filter flag:
```bash
# Run dev for just the web app
pnpm --filter web-langgraph-interrupt dev
# Run dev for just the agent
pnpm --filter agent-langgraph-interrupt dev
# Or navigate to the app directory
cd apps/web
pnpm dev
```
## Customization
The main UI component is in `apps/web/src/app/page.tsx`. You can:
- Modify the theme colors and styling
- Add new frontend actions
- Utilize shared-state
- Customize your user-interface for interacting with LangGraph
The LangGraph agent code is in `apps/agent/src/`.
## 📚 Documentation
- [CopilotKit Documentation](https://docs.copilotkit.ai) - Explore CopilotKit's capabilities
- [LangGraph Documentation](https://langchain-ai.github.io/langgraph/) - Learn more about LangGraph and its features
- [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 the chat returns an "Internal error while dispatching CopilotKit request" 500:**
Check the Next.js server logs for `[copilotkit/route] runtime construction failed:` or `[copilotkit/route] handleRequest dispatch failed:`. Common causes:
- `LANGGRAPH_DEPLOYMENT_URL` unset in production (required — check `apps/web/.env`)
- LangGraph server not running at the configured URL (check `pnpm --filter agent-langgraph-interrupt dev` started cleanly)
- `OPENAI_API_KEY` missing from `apps/agent/.env`
@@ -0,0 +1,7 @@
# OpenAI API key (required)
OPENAI_API_KEY=
# LangSmith tracing (optional)
# LANGSMITH_API_KEY=
# LANGSMITH_TRACING=true
# LANGSMITH_PROJECT=interrupts-langgraph
@@ -0,0 +1,6 @@
.env
.vercel
# LangGraph API
.langgraph_api
node_modules/
@@ -0,0 +1,9 @@
{
"node_version": "20",
"dockerfile_lines": [],
"dependencies": ["."],
"graphs": {
"default": "./src/agent.ts:graph"
},
"env": ".env"
}
@@ -0,0 +1,28 @@
{
"name": "agent-langgraph-interrupt",
"version": "0.1.7",
"private": true,
"description": "LangGraph agent for the CopilotKit interrupts starter",
"keywords": [],
"license": "MIT",
"author": "CopilotKit",
"scripts": {
"dev": "npx @langchain/langgraph-cli dev --port 8125 --no-browser",
"build": "tsc -p tsconfig.json --noEmit",
"lint": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@copilotkit/sdk-js": "workspace:*",
"@langchain/core": "^1.1.26",
"@langchain/langgraph": "1.1.5",
"@langchain/openai": "^1.2.8",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/node": "^22.19.11",
"typescript": "^5.9.3"
},
"engines": {
"node": ">=20.0.0"
}
}
@@ -0,0 +1,29 @@
{
"name": "agent-langgraph-interrupt",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/agent/src",
"projectType": "application",
"targets": {
"dev": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm --filter agent-langgraph-interrupt dev"
}
},
"build": {
"executor": "nx:run-commands",
"options": {
"command": "tsc -p tsconfig.json --noEmit",
"cwd": "examples/v2/interrupts-langgraph/apps/agent"
}
},
"lint": {
"executor": "nx:run-commands",
"options": {
"command": "tsc -p tsconfig.json --noEmit",
"cwd": "examples/v2/interrupts-langgraph/apps/agent"
}
}
},
"tags": []
}
@@ -0,0 +1,924 @@
/**
* This is the main entry point for the agent.
* It defines the workflow graph, state, tools, nodes and edges.
*/
import { randomUUID } from "node:crypto";
import { z } from "zod";
import type { RunnableConfig } from "@langchain/core/runnables";
import { tool } from "@langchain/core/tools";
import type { ToolRunnableConfig } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import type { BaseMessage, ToolCall } from "@langchain/core/messages";
import {
AIMessage,
isAIMessage,
SystemMessage,
ToolMessage,
} from "@langchain/core/messages";
import {
Annotation,
Command,
END,
getCurrentTaskInput,
interrupt,
MemorySaver,
START,
StateGraph,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
// Include CopilotKitStateAnnotation so the frontend can attach actions and
// so messages flow through the same channel the SDK expects.
//
// `interceptedToolCalls` + `originalAIMessageId` mirror
// `@copilotkit/sdk-js/langgraph`'s `copilotkitMiddleware.afterModel`
// intercept pattern (see node_modules/@copilotkit/sdk-js/src/langgraph/middleware.ts):
// on a mixed batch (backend tool call + frontend-action call in the same
// AIMessage), we strip the frontend calls out of the AIMessage before
// ToolNode runs (otherwise ToolNode errors on "Tool not found" for the
// frontend action names), stash them here, then restore them onto the
// original AIMessage before the graph ends so the frontend runtime still
// dispatches them. Raw-StateGraph starters like this one don't use
// createAgent+middleware, so we reproduce the pattern inline.
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
proverbs: Annotation<string[]>,
interceptedToolCalls: Annotation<ToolCall[] | undefined>,
originalAIMessageId: Annotation<string | undefined>,
});
export type AgentState = typeof AgentStateAnnotation.State;
// The renderer in apps/web/src/app/page.tsx validates the *emitted* interrupt
// payload against its own `parseInterruptPayload` shape. We validate the
// *resumed* payload here so an out-of-band Client that resumes with the wrong
// shape fails loudly at the tool boundary instead of silently branching to
// "cancelled".
const ApprovalResumeSchema = z.object({
approved: z.boolean(),
});
const getWeather = tool(
(args) => {
return `The weather for ${args.location} is 70 degrees, clear skies, 45% humidity, 5 mph wind, and feels like 72 degrees.`;
},
{
name: "getWeather",
description: "Get the weather for a given location.",
schema: z.object({
location: z.string().describe("The location to get weather for"),
}),
},
);
// HITL tool: triggers an interrupt that the frontend resolves with
// `{ approved: boolean }`. Validated with zod so a malformed resume value
// surfaces as a deterministic tool message rather than throwing through
// ToolNode.
//
// On approval, returns a `Command` that BOTH emits a ToolMessage (so the
// model sees the tool result) AND applies a state update that removes the
// matching proverb from `state.proverbs`. Without the state update, the
// UI (which reads `state.proverbs` via CopilotKit) would still show the
// "deleted" proverb, making the HITL demo a sham.
const deleteProverb = tool(
async (args, config: ToolRunnableConfig) => {
// `config.toolCall.id` is the canonical id accessor when a tool is
// invoked by ToolNode. ToolNode calls `tool.invoke({...call, type:
// "tool_call"}, config)` (see
// node_modules/@langchain/langgraph/dist/prebuilt/tool_node.js runTool),
// and @langchain/core's StructuredTool.invoke then copies the call
// onto `enrichedConfig.toolCall` (see
// node_modules/@langchain/core/dist/tools/index.js lines 84-91) before
// forwarding to the tool function. This is typed on `ToolRunnableConfig`
// — typing `config` explicitly above is what gives us the safe accessor.
//
// We need the id here because returning a `Command` bypasses
// `_formatToolOutput`'s automatic tool_call_id wiring. If the id is
// somehow missing we throw loudly rather than silently emitting
// `tool_call_id: ""`, which OpenAI rejects on the next turn with
// "tool_call_id does not match any preceding tool_calls".
const toolCallId = config.toolCall?.id;
if (typeof toolCallId !== "string" || toolCallId.length === 0) {
throw new Error(
"deleteProverb: missing tool_call_id on ToolRunnableConfig.toolCall — " +
"tool was invoked outside a ToolNode context. Refusing to emit a " +
"ToolMessage with an empty tool_call_id (OpenAI rejects those).",
);
}
const rawApproval = interrupt({
action: "delete_proverb",
proverb: args.proverb,
message: `Are you sure you want to delete the proverb: "${args.proverb}"?`,
});
let approval: z.infer<typeof ApprovalResumeSchema>;
try {
approval = ApprovalResumeSchema.parse(rawApproval);
} catch (err) {
// Only swallow ZodError — any other throw (programming errors, runtime
// failures, etc.) must propagate so we don't mask real bugs behind a
// generic tool message.
if (!(err instanceof z.ZodError)) {
throw err;
}
// eslint-disable-next-line no-console
console.error("[deleteProverb] resume payload rejected:", err.issues);
// Don't let ZodError propagate through ToolNode. Return a
// deterministic tool message so the graph can loop back to chat_node
// with a readable result in context.
return new ToolMessage({
status: "error",
name: "deleteProverb",
tool_call_id: toolCallId,
content:
"Confirmation failed due to an unexpected resume payload shape; deletion was NOT performed.",
});
}
if (approval.approved) {
// Read the current graph state via LangGraph's task-local accessor so
// we can filter `proverbs` deterministically. We match by content
// (the schema accepts the proverb text). If multiple proverbs tie
// exactly, only the first matching entry is removed — consistent
// with "delete the proverb the user named".
//
// AgentStateAnnotation's `proverbs` channel has no reducer, so
// Annotation<string[]> defaults to last-write-wins: emitting a
// filtered array replaces the channel wholesale (which is exactly
// what chat_node reads on the next turn for the system prompt).
const currentState = getCurrentTaskInput<AgentState>();
const current = Array.isArray(currentState?.proverbs)
? currentState.proverbs
: [];
const idx = current.indexOf(args.proverb);
// Approved-but-not-present: do not lie to the model. Return an
// error ToolMessage so the model sees "nothing matched" and can
// respond truthfully instead of confirming a deletion that never
// happened.
if (idx === -1) {
return new Command({
update: {
messages: [
new ToolMessage({
status: "error",
name: "deleteProverb",
tool_call_id: toolCallId,
content: `No proverb matching "${args.proverb}" was found; nothing was deleted.`,
}),
],
},
});
}
const filtered = [...current.slice(0, idx), ...current.slice(idx + 1)];
return new Command({
update: {
messages: [
new ToolMessage({
status: "success",
name: "deleteProverb",
tool_call_id: toolCallId,
content: `Proverb "${args.proverb}" has been deleted.`,
}),
],
proverbs: filtered,
},
});
}
// Mirror the approved branch: return a Command wrapping a ToolMessage
// so the model sees a well-formed tool result with the correct
// tool_call_id (OpenAI rejects tool messages with mismatched ids).
// Cancellation is not success — the tool did not complete its stated
// intent — so the ToolMessage status is "error". The content string is
// truthful as a user-cancelled message.
return new Command({
update: {
messages: [
new ToolMessage({
status: "error",
name: "deleteProverb",
tool_call_id: toolCallId,
content: `Deletion of proverb "${args.proverb}" was cancelled by the user.`,
}),
],
},
});
},
{
name: "deleteProverb",
description:
"Delete a proverb from the list. This will ask the user for confirmation before deleting.",
schema: z.object({
proverb: z.string().describe("The proverb to delete"),
}),
},
);
const tools = [getWeather, deleteProverb];
// gpt-4o-mini: reliable tool-calling, low cost. Swap model here if you
// need different tradeoffs.
const MODEL = "gpt-4o-mini";
async function chat_node(state: AgentState, config: RunnableConfig) {
const model = new ChatOpenAI({ model: MODEL });
// Bind tools to the model, including CopilotKit frontend actions.
//
// bindTools is optional on BaseChatModel's type; guard explicitly instead
// of using the non-null `!` escape hatch so a model instance that doesn't
// support tool binding fails loudly.
if (typeof model.bindTools !== "function") {
throw new Error(
`ChatOpenAI instance for model "${MODEL}" does not expose bindTools; cannot bind tools for this model.`,
);
}
const modelWithTools = model.bindTools([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
...tools,
]);
const systemMessage = new SystemMessage({
content: `You are a helpful assistant. The current proverbs are ${JSON.stringify(state.proverbs ?? [])}. If a user asks to delete a proverb, call deleteProverb to trigger a human-in-the-loop interrupt for confirmation.`,
});
const response = await modelWithTools.invoke(
[systemMessage, ...(state.messages ?? [])],
config,
);
return {
messages: [response],
};
}
// intercept_frontend_tools: strips frontend-action tool_calls out of the
// last AIMessage before ToolNode runs and stashes them in state.
//
// ToolNode only knows about backend `tools` (getWeather, deleteProverb); it
// looks up each `tool_call.name` in its own registry and throws
// "Tool not found" for any frontend-action name (see
// node_modules/@langchain/langgraph/dist/prebuilt/tool_node.js, runTool).
// On a mixed batch that would leave backend results AND a model-visible
// error ToolMessage for the frontend action, and the frontend would never
// see the frontend-action call at all.
//
// The intercept+restore pattern mirrors CopilotKit's own
// `copilotkitMiddleware.afterModel` + `afterAgent` (see
// node_modules/@copilotkit/sdk-js/src/langgraph/middleware.ts). The
// `restore_frontend_tools` node below reattaches the stashed calls to the
// original AIMessage (matched by id) before the graph ends so the
// CopilotKit runtime still dispatches them to the frontend.
//
// Pure-frontend-only batches skip this node entirely — shouldContinue
// routes them straight to END, and the AIMessage with their tool_calls
// reaches the frontend as-is.
// Rebuild an AIMessage with a different tool_calls set while preserving
// every other field (additional_kwargs, response_metadata, usage_metadata,
// name, invalid_tool_calls, id, content). Required for LangSmith tracing
// + token accounting — naively constructing `new AIMessage({ content,
// tool_calls, id })` drops everything else.
//
// Note: `tool_call_chunks` only exists on AIMessageChunk, and the AIMessage
// constructor does not accept it — preserving it here was a no-op. Omitted.
function rebuildAIMessageWithToolCalls(
source: AIMessage,
toolCalls: ToolCall[],
): AIMessage {
return new AIMessage({
content: source.content,
id: source.id,
name: source.name,
additional_kwargs: source.additional_kwargs,
response_metadata: source.response_metadata,
usage_metadata: source.usage_metadata,
invalid_tool_calls: source.invalid_tool_calls,
tool_calls: toolCalls,
});
}
function intercept_frontend_tools(state: AgentState) {
const frontendActionNames = new Set(
(state.copilotkit?.actions ?? []).map((a: { name: string }) => a.name),
);
if (frontendActionNames.size === 0) {
return {};
}
// Widen to our directly-imported `BaseMessage` type so `isAIMessage`
// (1.1.27) can narrow a `state.messages[i]` (structurally identical
// 1.1.40) without the pnpm nominal-mismatch error. Runtime values are
// unaffected — see the matching annotation on `lastMessage` in
// `shouldContinue`.
let messages = (state.messages ?? []) as unknown as BaseMessage[];
// The per-turn intercept slot is a single pair (interceptedToolCalls +
// originalAIMessageId) with no reducer on the annotation — it cannot
// queue. If the graph re-enters this node for a second mixed batch in
// the same thread before `restore_frontend_tools` has flushed the prior
// stash, we must flush the previous stash onto its matching AIMessage
// inline here. Otherwise last-write-wins would silently drop the
// earlier frontend-action calls and the frontend would never see them.
//
// Flush strategy on re-entry:
// (a) First pass: walk `messages` and reattach the prior stash onto
// the AIMessage whose id matches `priorOriginalId` (pre-strip).
// (b) If (a) finds no match AND this pass would otherwise overwrite
// the slot with a new stash (the mixed-batch return below), we
// make a second flush attempt against the newly-rewritten
// `messages` array (post-strip) before committing the new stash.
// (c) If both attempts fail, we emit a loud warn naming the lost
// AIMessage id + tool-call ids and STILL write the new stash —
// merging two different AIMessage ids into one slot would corrupt
// `originalAIMessageId`. The warn is the escape valve.
//
// The `frontendToolCalls.length === 0` return branch applies a
// flush-or-clear rule: if the pre-strip flush matched (prior_flushed),
// the updated messages are emitted and the slot is cleared; if it
// didn't match but a prior stash was present, the slot is cleared
// with a warn (retaining it risks double-flushing onto the original
// AIMessage on a subsequent non-stripping turn since the AIMessage
// may still be in history).
const priorIntercepted = state.interceptedToolCalls;
const priorOriginalId = state.originalAIMessageId;
const priorSlotPresent =
!!priorIntercepted &&
priorIntercepted.length > 0 &&
typeof priorOriginalId === "string" &&
priorOriginalId.length > 0;
let prior_flushed = false;
if (priorSlotPresent) {
messages = messages.map((msg) => {
if (isAIMessage(msg) && msg.id === priorOriginalId) {
prior_flushed = true;
const existing = msg.tool_calls ?? [];
return rebuildAIMessageWithToolCalls(msg, [
...existing,
...priorIntercepted!,
]);
}
return msg;
});
if (!prior_flushed) {
// eslint-disable-next-line no-console
console.warn(
`[intercept_frontend_tools] prior intercept slot held id=${priorOriginalId} but no matching AIMessage was found to flush onto (pre-strip); downstream branches will flush-or-clear.`,
);
}
}
const lastMessage: BaseMessage | undefined = messages[messages.length - 1];
if (lastMessage === undefined || !isAIMessage(lastMessage)) {
return {};
}
const toolCalls = lastMessage.tool_calls ?? [];
const backendToolCalls: ToolCall[] = [];
const frontendToolCalls: ToolCall[] = [];
for (const call of toolCalls) {
if (frontendActionNames.has(call.name)) {
frontendToolCalls.push(call);
} else {
backendToolCalls.push(call);
}
}
// `AIMessage.id` is typed `string | undefined` in @langchain/core. If the
// upstream provider (or a test fixture) produced an AIMessage without an
// id AND this batch needs stripping, `restore_frontend_tools` would never
// find a matching id on the later pass — frontend-action calls would be
// silently dropped and the user would see nothing. Synthesize a stable
// id in place so the strip/stash/restore chain can match. LangChain
// AIMessage is mutable; the synthesized id survives `rebuildAIMessageWithToolCalls`
// (which copies `id` from `source.id`) and lives on the same object
// reference in `messages`.
if (
frontendToolCalls.length > 0 &&
(typeof lastMessage.id !== "string" || lastMessage.id.length === 0)
) {
const synthesizedId = `synthesized-${randomUUID()}`;
// eslint-disable-next-line no-console
console.warn(
`[intercept_frontend_tools] lastMessage.id is missing on an AIMessage with ${frontendToolCalls.length} frontend-action call(s); synthesizing id=${synthesizedId} so restore_frontend_tools can match. Upstream provider should supply stable AIMessage ids.`,
);
(lastMessage as AIMessage).id = synthesizedId;
}
if (frontendToolCalls.length === 0) {
// No frontend calls in the batch — nothing to strip.
//
// Three prior-slot cases to distinguish:
// (a) prior_flushed === true: we reattached the stashed calls onto
// a matching AIMessage above; emit the updated messages and
// clear the slot.
// (b) priorSlotPresent && !prior_flushed: no matching AIMessage was
// found THIS pass. Over a sequence like
// [mixed-stash] → [backend-only+unknown] → [pure-backend],
// retaining the stash across multiple non-stripping turns would
// eventually let `restore_frontend_tools` re-apply it to an
// unrelated AIMessage (or let a future intercept pass
// double-append onto the original AIMessage still in history).
// Flush-or-clear discipline: if we had a stash and this path
// isn't stripping, clear it. Emit a warn so operators can
// debug the dropped frontend-action dispatch.
// (c) No prior slot: no-op.
if (prior_flushed) {
return {
messages,
interceptedToolCalls: undefined,
originalAIMessageId: undefined,
} as unknown as Partial<AgentState>;
}
if (priorSlotPresent) {
const lostIds = priorIntercepted!
.map((c) => c.id ?? "<no-id>")
.join(", ");
// eslint-disable-next-line no-console
console.warn(
`[intercept_frontend_tools] prior intercept slot held id=${priorOriginalId} but no matching AIMessage was found and this path isn't stripping; clearing stash to prevent later re-application onto an unrelated AIMessage. Lost tool-call ids: [${lostIds}]`,
);
return {
interceptedToolCalls: undefined,
originalAIMessageId: undefined,
} as unknown as Partial<AgentState>;
}
return {};
}
// Rebuild the AIMessage preserving id (so restore_frontend_tools can
// find it later) AND all other metadata (additional_kwargs,
// response_metadata, usage_metadata, etc.) with only the backend calls.
const strippedAIMessage = rebuildAIMessageWithToolCalls(
lastMessage,
backendToolCalls,
);
// Compose the outgoing message list with the strip applied so any
// post-strip flush attempt sees the final shape.
let outgoingMessages: BaseMessage[] = [
...messages.slice(0, -1),
strippedAIMessage,
];
// Mixed-batch overwrite guard: if a prior stash is still present and
// was NOT flushed in the pre-strip pass above, we are about to
// overwrite the slot. Try one more flush against the post-strip
// `outgoingMessages` before giving up. If still unmatched, warn
// loudly — merging different AIMessage ids into one slot would
// corrupt `originalAIMessageId`, so we accept losing the prior stash
// in exchange for a coherent new one. The warn mirrors the
// "no matching AIMessage" style used by `restore_frontend_tools`.
if (priorSlotPresent && !prior_flushed) {
let lateFlushed = false;
outgoingMessages = outgoingMessages.map((msg) => {
if (isAIMessage(msg) && msg.id === priorOriginalId) {
lateFlushed = true;
const existing = msg.tool_calls ?? [];
return rebuildAIMessageWithToolCalls(msg, [
...existing,
...priorIntercepted!,
]);
}
return msg;
});
if (!lateFlushed) {
const lostIds = priorIntercepted!
.map((c) => c.id ?? "<no-id>")
.join(", ");
// eslint-disable-next-line no-console
console.warn(
`[intercept_frontend_tools] prior intercept slot held id=${priorOriginalId} but no matching AIMessage was found to flush onto (pre- or post-strip); overwriting stash with current mixed-batch intercept. Lost tool-call ids: [${lostIds}]`,
);
}
}
// The outer cast passes the return past a pre-existing pnpm monorepo
// resolution quirk: `@langchain/langgraph@1.1.5` pins `@langchain/core`
// at a different patch level than this agent's direct dep, so our
// imported `AIMessage/BaseMessage` and the graph-state's internal
// version are nominally distinct types though structurally identical
// at runtime. chat_node's `return { messages: [response] }` hits the
// same mismatch implicitly; cf. the baseline tsc errors on that line.
return {
messages: outgoingMessages,
interceptedToolCalls: frontendToolCalls,
originalAIMessageId: lastMessage.id,
} as unknown as Partial<AgentState>;
}
// restore_frontend_tools: reattaches the stashed frontend-action tool_calls
// to the original AIMessage (matched by id) so the CopilotKit runtime can
// dispatch them to the frontend. Mirrors `copilotkitMiddleware.afterAgent`.
function restore_frontend_tools(state: AgentState) {
const interceptedToolCalls = state.interceptedToolCalls;
const originalMessageId = state.originalAIMessageId;
if (
!interceptedToolCalls ||
interceptedToolCalls.length === 0 ||
!originalMessageId
) {
return {};
}
// Widen to our directly-imported `BaseMessage` for `isAIMessage` —
// see the matching cast in `intercept_frontend_tools` above.
const messages = (state.messages ?? []) as unknown as BaseMessage[];
let messageFound = false;
const updatedMessages: BaseMessage[] = messages.map((msg) => {
if (isAIMessage(msg) && msg.id === originalMessageId) {
messageFound = true;
const existing = msg.tool_calls ?? [];
// Preserve all AIMessage metadata (additional_kwargs,
// response_metadata, usage_metadata, name, invalid_tool_calls)
// — a naive rebuild drops them and breaks LangSmith tracing +
// token accounting.
return rebuildAIMessageWithToolCalls(msg, [
...existing,
...interceptedToolCalls,
]);
}
return msg;
});
if (!messageFound) {
// This node is terminal (edge goes to END). Clear both slots so a
// stale stash can't be flushed onto an unrelated AIMessage on a
// later intercept pass. The warn is the diagnostic signal —
// persisting the slot would corrupt future turns rather than help
// diagnose this one.
// eslint-disable-next-line no-console
console.warn(
`[restore_frontend_tools] original AIMessage id=${originalMessageId} not found in messages; clearing stash to avoid cross-turn corruption`,
);
return {
interceptedToolCalls: undefined,
originalAIMessageId: undefined,
} as unknown as Partial<AgentState>;
}
// See note on the matching return in `intercept_frontend_tools`.
return {
messages: updatedMessages,
interceptedToolCalls: undefined,
originalAIMessageId: undefined,
} as unknown as Partial<AgentState>;
}
// The return type is the union of node names plus END, matching the
// shape addConditionalEdges expects from its callback.
function shouldContinue({
messages,
copilotkit,
}: AgentState):
| "intercept_frontend_tools"
| "tool_node"
| "restore_frontend_tools"
| "emit_unknown_tools_notice"
| typeof END {
// Guard the tool-call-carrying variant structurally instead of casting
// BaseMessage to AIMessage.
const lastMessage: BaseMessage | undefined = messages[messages.length - 1];
if (lastMessage === undefined) {
return END;
}
// AIMessage is the only message variant that carries tool_calls. Use
// the `isAIMessage` type predicate from @langchain/core/messages so the
// narrowing is checked rather than cast.
if (!isAIMessage(lastMessage)) {
const kind = lastMessage._getType();
// Log and fall through to END in both dev and prod so a graph-shape
// bug doesn't crash the process. Tradeoff: the user sees the turn
// end silently (no synthetic error message surfaced to the chat).
// Follow-up: emit a synthetic AIMessage from chat_node (not this
// routing function) the next time we observe unexpected internal
// state, so the user sees "I hit an unexpected internal state —
// please try rephrasing."
// eslint-disable-next-line no-console
console.warn("[shouldContinue] unexpected last message type:", kind);
return END;
}
// Evaluate ALL tool calls. If ANY tool call targets a backend tool (i.e.
// not a CopilotKit frontend action), we must route to `tool_node` so the
// backend tool runs — returning END on mixed batches would silently
// drop the backend call.
const toolCalls = lastMessage.tool_calls ?? [];
if (toolCalls.length > 0) {
const actionNames = new Set((copilotkit?.actions ?? []).map((a) => a.name));
// Widen to `Set<string>` because TypeScript's `Set<T>.has` parameter
// is invariant on T — a `Set<"getWeather" | "deleteProverb">` would
// reject a caller-supplied plain `string` at compile time even though
// the runtime answer (`false` for unknown names) is exactly what we
// want.
const backendToolNames = new Set<string>(tools.map((t) => t.name));
let hasBackendTool = false;
let hasFrontendAction = false;
let hasUnknown = false;
for (const toolCall of toolCalls) {
const name = toolCall.name;
if (actionNames.has(name)) {
hasFrontendAction = true;
// Frontend action — handled client-side.
continue;
}
if (backendToolNames.has(name)) {
hasBackendTool = true;
continue;
}
// Unknown name: neither a frontend action nor a registered backend
// tool. Track it so we can route unknown-bearing batches through
// emit_unknown_tools_notice (below), which synthesizes error
// ToolMessages for each unknown call and strips them off the
// AIMessage so the frontend runtime never sees them.
hasUnknown = true;
// eslint-disable-next-line no-console
console.warn(
`[shouldContinue] unknown tool call name '${name}' — will route through emit_unknown_tools_notice unless a known backend tool is also present in this batch`,
);
}
// Mixed batch (backend + frontend action): route through the intercept
// node first so ToolNode doesn't choke on the frontend-action call,
// then tool_node executes backend calls, then chat_node (looped) will
// reach END via the restore node.
//
// Note: if the batch also contains unknown calls, we still prefer
// "tool_node" over the unknown-notice path when a backend call is
// present — ToolNode itself emits an error ToolMessage for unknown
// names and the graph loops back to chat_node with that context.
// The unknown-notice path is reserved for batches that would otherwise
// end the turn without running tool_node.
if (hasBackendTool && hasFrontendAction) {
return "intercept_frontend_tools";
}
if (hasBackendTool) {
return "tool_node";
}
// No backend tool. If the batch carries ANY unknown calls (with or
// without frontend actions), route through emit_unknown_tools_notice
// so:
// (a) error ToolMessages are synthesized for each unknown call,
// keeping the AIMessage+ToolMessage sequence well-formed for
// OpenAI on the next turn (no dangling tool_calls);
// (b) the AIMessage retains its unknown tool_calls alongside the
// knownCalls so every errorToolMessage has a matching
// preceding tool_call.id. The frontend runtime does not
// re-dispatch calls that carry a matching ToolMessage result.
// (c) in the frontend-action + unknown mixed case, the surviving
// frontend-action calls still reach the frontend via the
// terminal restore path (emit_unknown_tools_notice goes to END
// and the rebuilt AIMessage retains both the known frontend
// calls and the unknown calls, the latter pre-resolved by
// their error ToolMessages).
if (hasUnknown) {
return "emit_unknown_tools_notice";
}
}
// All paths that reach here are assistant replies with no pending backend tool_calls.
// Route through restore_frontend_tools; it is a no-op when nothing was intercepted.
return "restore_frontend_tools";
}
// emit_unknown_tools_notice: when the model emits a tool_calls batch that
// includes names the agent cannot dispatch (neither a registered backend
// tool nor a frontend action), chat_node's conditional edge routes here.
// Responsibilities:
//
// 1. Synthesize an error ToolMessage for each unknown tool_call that
// carries a non-empty `call.id`. Without this, the AIMessage's
// unresolved tool_calls leave a dangling tool-use turn — OpenAI
// rejects any AIMessage with tool_calls not followed by matching
// ToolMessages on the NEXT user turn, poisoning the conversation.
// Unknown calls with a missing/empty id are DROPPED from both the
// ToolMessage list AND the AIMessage's tool_calls (emitting
// `tool_call_id: ""` would itself be rejected; keeping the call on
// the AIMessage without a matching ToolMessage re-introduces the
// dangling-reference bug).
// 2. Rebuild the prior AIMessage with rebuildAIMessageWithToolCalls,
// retaining BOTH the knownCalls AND every unknown call whose
// tool_call_id was retained in (1). The retained unknowns are
// what makes the transcript well-formed: each errorToolMessage
// emitted in (1) has a matching `tool_call.id` on the immediately
// preceding AIMessage, which is what OpenAI's chat-completions
// API validates on the next user turn. Stripping the unknowns
// here (as an earlier revision did) while still appending their
// error ToolMessages produced orphaned `tool_call_id`s and
// "tool_call_id does not match any preceding tool_calls" errors
// on the next turn.
// Dropped-id unknowns — those with no usable tool_call_id — are
// still omitted from tool_calls since they have no matching
// ToolMessage result; keeping them would reintroduce the dangling
// reference on the next turn.
// Safety for the frontend: tool_calls on the AIMessage are
// dispatched by the CopilotKit frontend runtime only when the
// model streams TOOL_CALL_START / TOOL_CALL_END events in the
// current turn. This node does not emit those events — it only
// writes to state.messages. On the next turn the frontend sees
// each retained unknown paired with its error ToolMessage in the
// snapshot; pairs that already carry a result are not
// re-dispatched.
// 3. In the PURE-unknown batch (`knownCalls.length === 0`), append a
// user-visible AIMessage notice so the turn doesn't end silently,
// and clear any stale intercept slot from a prior turn.
// In the MIXED frontend-action + unknown batch, do NOT append the
// notice — the surviving frontend-action tool_calls on
// strippedAIMessage need matching ToolMessages on the next turn,
// and a trailing AIMessage(notice) produces an ill-formed OpenAI
// transcript. The surviving calls reach the frontend via the
// outgoing `restore_frontend_tools` → END path.
//
// Routing: outgoing edge goes to `restore_frontend_tools` (not END).
// That node no-ops when the slot is empty, so the pure-unknown case
// still terminates cleanly, while the mixed case gets canonical
// restore-then-END handling and any prior unflushed stash is cleared.
//
// Conditional edges are pure routing functions — they cannot mutate
// state — so the state rewrite lives here.
function emit_unknown_tools_notice(state: AgentState) {
const messages = (state.messages ?? []) as unknown as BaseMessage[];
const lastMessage: BaseMessage | undefined = messages[messages.length - 1];
if (lastMessage === undefined || !isAIMessage(lastMessage)) {
return {};
}
// Mirror shouldContinue's partition logic so the same known-set defines
// what counts as unknown. `tools` is the backend registry; the frontend
// action set comes from state.copilotkit.actions.
const frontendActionNames = new Set(
(state.copilotkit?.actions ?? []).map((a: { name: string }) => a.name),
);
const backendToolNames = new Set<string>(tools.map((t) => t.name));
const allCalls = lastMessage.tool_calls ?? [];
const knownCalls: ToolCall[] = [];
const unknownCalls: ToolCall[] = [];
for (const call of allCalls) {
if (frontendActionNames.has(call.name) || backendToolNames.has(call.name)) {
knownCalls.push(call);
} else {
unknownCalls.push(call);
}
}
if (unknownCalls.length === 0) {
// Nothing unknown to notify about — shouldContinue shouldn't have
// routed here, but be defensive.
return {};
}
// Partition unknowns by whether they carry a usable tool_call_id.
// OpenAI rejects ToolMessages whose `tool_call_id` doesn't match a
// preceding AIMessage tool_call id — including empty strings. The
// only safe handling for an unknown tool_call with a missing/empty
// id is to DROP IT from both the error ToolMessage list AND the
// AIMessage's tool_calls (so no dangling reference remains). This
// mirrors `deleteProverb`'s refusal to emit `tool_call_id: ""`.
const unknownWithId: ToolCall[] = [];
for (const call of unknownCalls) {
const id = call.id;
if (typeof id === "string" && id.length > 0) {
unknownWithId.push(call);
} else {
// eslint-disable-next-line no-console
console.warn(
`[emit_unknown_tools_notice] unknown tool_call '${call.name}' has no id; dropping from both errorToolMessages and strippedAIMessage.tool_calls to avoid emitting a ToolMessage with empty tool_call_id`,
);
}
}
// If every unknown call lacked an id, `unknownWithId` is empty and
// `errorToolMessages` below will be empty — the AIMessage retains
// only `knownCalls` and we still emit the notice in the pure-unknown
// case below (drop-only is still a reportable turn).
// Rebuild the prior AIMessage preserving its id + metadata, retaining
// BOTH knownCalls AND unknownWithId on `tool_calls`. Retaining the
// unknowns is what keeps the OpenAI transcript well-formed on the
// next user turn: every errorToolMessage below references an id
// that still appears on the immediately preceding AIMessage's
// `tool_calls`. Stripping the unknowns while still emitting their
// error ToolMessages produced orphaned `tool_call_id`s — OpenAI's
// chat completions API rejects a ToolMessage whose `tool_call_id`
// does not match a preceding AIMessage `tool_call.id`, so the next
// user turn failed. With the unknowns retained, the AIMessage →
// ToolMessage(result) pairing is intact for every unknown.
//
// Safety for the frontend: the frontend action handler only
// dispatches a tool_call when it receives a live TOOL_CALL_START /
// TOOL_CALL_END event stream during the current agent turn. This
// node DOES NOT emit those events — it only writes to state.messages
// after the model stream is already finalized. On the next turn,
// the frontend sees the pair (AIMessage.tool_call + ToolMessage
// result) already present in the snapshot; pairs that carry a
// result are treated as resolved and are not re-dispatched.
//
// Dropped-id unknowns (no usable tool_call_id) are still omitted
// from tool_calls — they have no matching ToolMessage, so keeping
// them would reintroduce the dangling-id problem on the next turn.
const retainedCalls: ToolCall[] = [...knownCalls, ...unknownWithId];
const strippedAIMessage = rebuildAIMessageWithToolCalls(
lastMessage,
retainedCalls,
);
const errorToolMessages = unknownWithId.map((call) => {
return new ToolMessage({
status: "error",
name: call.name,
// Narrowed: unknownWithId only contains calls whose id is a
// non-empty string.
tool_call_id: call.id as string,
content: `Tool '${call.name}' is not available in this environment.`,
});
});
// Mixed frontend-action + unknown batch: the surviving knownCalls
// are frontend-action calls that still need to reach the frontend
// runtime. Appending an `AIMessage(notice)` here would leave
// strippedAIMessage's frontend-action tool_calls with no matching
// ToolMessages before the trailing notice, producing an ill-formed
// OpenAI transcript on replay. Instead, suppress the notice in the
// mixed case and let the outgoing edge route the surviving calls
// through `restore_frontend_tools` → END for normal dispatch.
//
// Pure-unknown batch (`knownCalls.length === 0`): emit the notice
// as before so the turn doesn't end silently.
const unknownNames = unknownCalls.map((c) => c.name);
const trailingMessages: BaseMessage[] =
knownCalls.length === 0
? [
new AIMessage({
content: `I tried to call tools that aren't available in this environment (${unknownNames.join(
", ",
)}). Cancelling this turn.`,
}),
]
: [];
// Always clear any prior-turn intercept slot before routing to
// `restore_frontend_tools`. Gating the clear on `knownCalls.length === 0`
// is incorrect: in a MIXED batch (knownCalls.length > 0) the surviving
// frontend-action calls ride the stripped AIMessage to `restore_frontend_tools`,
// which consumes `interceptedToolCalls` + `originalAIMessageId`. If a stale
// stash from a PRIOR turn still sits in that slot, it would be grafted
// onto an unrelated AIMessage in THIS turn. Clearing unconditionally
// guarantees restore_frontend_tools only sees state stashed for the
// current turn (which, from this node, is always empty — no stash is
// written here).
const slotClear: Partial<AgentState> = {
interceptedToolCalls: undefined,
originalAIMessageId: undefined,
};
// Sequence on the channel (mixed case):
// [...existing..., strippedAIMessage (replaces lastMessage),
// ToolMessage(unknown1), ..., ToolMessage(unknownN)]
// Pure-unknown case appends a trailing AIMessage(notice).
return {
messages: [
...messages.slice(0, -1),
strippedAIMessage,
...errorToolMessages,
...trailingMessages,
],
...slotClear,
} as unknown as Partial<AgentState>;
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chat_node)
.addNode("tool_node", new ToolNode(tools))
.addNode("intercept_frontend_tools", intercept_frontend_tools)
.addNode("restore_frontend_tools", restore_frontend_tools)
.addNode("emit_unknown_tools_notice", emit_unknown_tools_notice)
.addEdge(START, "chat_node")
.addEdge("intercept_frontend_tools", "tool_node")
.addEdge("tool_node", "chat_node")
.addEdge("restore_frontend_tools", END)
// Route through `restore_frontend_tools` (not END) so any prior
// unflushed intercept stash is cleared and any surviving
// frontend-action tool_calls retained on the stripped AIMessage in
// the mixed-batch case are reattached via the canonical restore
// path before termination. `restore_frontend_tools` no-ops when the
// slot is empty, so the pure-unknown case still terminates cleanly.
.addEdge("emit_unknown_tools_notice", "restore_frontend_tools")
.addConditionalEdges("chat_node", shouldContinue);
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "Node16",
"moduleResolution": "node16",
"resolvePackageJsonExports": true,
"resolvePackageJsonImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true
}
}
@@ -0,0 +1,18 @@
# LangGraph deployment URL (required in production, defaults to http://localhost:8125 in dev)
# LANGGRAPH_DEPLOYMENT_URL=
# LANGSMITH_* variables below are primarily consumed by the agent /
# LangGraph deployment at deploy time (set them in the agent's
# environment, e.g. the LangGraph Cloud deployment or `apps/agent/.env`).
# The Next.js web app itself only observes LANGSMITH_API_KEY here to
# emit a dev-only "missing key" warning and to forward the key through
# to the LangGraphAgent client in apps/web/src/app/api/copilotkit/route.ts.
# LangSmith API key (optional, for production tracing)
# LANGSMITH_API_KEY=
# Enable LangSmith tracing (optional)
# LANGSMITH_TRACING=true
# LangSmith project name (optional; used by LangSmith tracing if LANGSMITH_TRACING=true)
# LANGSMITH_PROJECT=interrupts-langgraph
@@ -0,0 +1,23 @@
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import nextTypescript from "eslint-config-next/typescript";
// Flat-config ESLint setup for Next 16 / React 19 / TS 5. Mirrors the
// sibling `examples/showcases/open-mcp-client/apps/web/eslint.config.mjs`
// so lint behaviour stays consistent across starters. Next 16 removed
// the `next lint` command, so package.json now invokes `eslint .`
// directly against this config.
const eslintConfig = [
...nextCoreWebVitals,
...nextTypescript,
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
],
},
];
export default eslintConfig;
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
serverExternalPackages: ["@copilotkit/runtime"],
};
export default nextConfig;
@@ -0,0 +1,42 @@
{
"name": "web-langgraph-interrupt",
"version": "0.1.7",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint . --max-warnings=0"
},
"dependencies": {
"@copilotkit/react-core": "workspace:*",
"@copilotkit/react-ui": "workspace:*",
"@copilotkit/runtime": "workspace:*",
"next": "16.0.8",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"shiki": "^3.22.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^22.19.11",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.0.8",
"tailwindcss": "^4",
"typescript": "^5.9.3"
},
"engines": {
"node": ">=20.0.0"
},
"nx": {
"targets": {
"build": {
"cache": false
}
}
}
}
@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
@@ -0,0 +1,28 @@
{
"name": "web-langgraph-interrupt",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/web/src",
"projectType": "application",
"targets": {
"dev": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm --filter web-langgraph-interrupt dev"
}
},
"build": {
"executor": "nx:run-commands",
"cache": false,
"options": {
"command": "pnpm --filter web-langgraph-interrupt build"
}
},
"lint": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm --filter web-langgraph-interrupt lint"
}
}
},
"tags": []
}
@@ -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,149 @@
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
import { NextRequest, NextResponse } from "next/server";
// 1. Runtime adapter. Since this starter wires agent responses directly
// (no OpenAI/Anthropic-compatible chat-completions fallback), we use
// the empty adapter: it satisfies the runtime interface without
// dispatching any LLM requests of its own.
const serviceAdapter = new ExperimentalEmptyAdapter();
// 2. LANGGRAPH_DEPLOYMENT_URL handling. In development we fall back to
// localhost:8125 (the port this starter's LangGraph dev server uses —
// see apps/agent/package.json) and warn so the developer sees it. In
// production a missing URL is almost always a misconfiguration — but
// we defer the hard failure to the first request (see POST below)
// rather than throwing at module load. `next build` evaluates route
// modules with NODE_ENV=production during route collection, while
// runtime-only env vars (Vercel secrets, Railway env, Docker ENV at
// container start) are NOT injected at build time. Throwing at module
// load would abort otherwise-valid production builds.
if (
!process.env.LANGGRAPH_DEPLOYMENT_URL &&
process.env.NODE_ENV !== "production"
) {
console.warn(
"[copilotkit/route] LANGGRAPH_DEPLOYMENT_URL is not set; falling back to http://localhost:8125. Set LANGGRAPH_DEPLOYMENT_URL in production.",
);
}
// LangSmith is optional; warn once at module load so a missing key
// surfaces in logs. When absent we omit the langsmithApiKey field
// entirely rather than passing "" — omitting the field disables
// LangSmith tracing cleanly; passing an empty string may be forwarded
// to the SDK. Gate the warn on NODE_ENV so it does not fire during
// `next build` in CI (same rationale as the DEPLOYMENT_URL warn above).
if (!process.env.LANGSMITH_API_KEY && process.env.NODE_ENV !== "production") {
console.warn(
"[copilotkit/route] LANGSMITH_API_KEY is not set; LangSmith tracing is disabled for this session.",
);
}
// 3. Lazy runtime construction. We build the LangGraphAgent + runtime +
// handler on the first request rather than at module load, so that
// `next build` (which evaluates this file with NODE_ENV=production but
// without runtime env vars) does not bake the fallback localhost URL
// into the compiled artifact. The cached closure keeps per-request
// overhead at the same single-allocation cost as the eager form.
// Matches the signature returned by copilotRuntimeNextJSAppRouterEndpoint:
// `(req: Request) => Response | Promise<Response>`. NextRequest extends
// Request, so the POST handler below can still pass its req through
// without an explicit cast.
let cachedHandleRequest:
| ((req: Request) => Response | Promise<Response>)
| null = null;
const getHandleRequest = () => {
if (cachedHandleRequest) return cachedHandleRequest;
const deploymentUrl = process.env.LANGGRAPH_DEPLOYMENT_URL;
if (!deploymentUrl && process.env.NODE_ENV === "production") {
throw new Error(
"[copilotkit/route] LANGGRAPH_DEPLOYMENT_URL is required in production. " +
"Set it to the deployed LangGraph endpoint for this starter.",
);
}
const agent = new LangGraphAgent({
deploymentUrl: deploymentUrl || "http://localhost:8125",
graphId: "default",
// Only pass langsmithApiKey when it's a non-empty string; omitting
// the field disables LangSmith tracing without asking the SDK to
// authenticate with an empty key.
...(process.env.LANGSMITH_API_KEY
? { langsmithApiKey: process.env.LANGSMITH_API_KEY }
: {}),
});
// Register the single LangGraph agent under the `default` name, which
// matches the <CopilotKit agent="default"> prop in layout.tsx. If you
// need to expose this agent under an additional id (e.g. when pointing
// a second frontend at this runtime), add another entry here and
// update the corresponding <CopilotKit agent="..."> prop.
const runtime = new CopilotRuntime({
agents: {
default: agent,
},
});
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
serviceAdapter,
endpoint: "/api/copilotkit",
});
cachedHandleRequest = handleRequest;
return handleRequest;
};
// 4. POST handler. Resolves the cached handler (building it on first
// request), then dispatches. Wrap in try/catch so unhandled exceptions
// surface as a structured 500 rather than a raw Next.js error page,
// and log the failure for observability. Errors inside the streaming
// response are handled by the runtime itself.
export const POST = async (req: NextRequest) => {
// Redact `detail` in production: raw error messages can leak
// internals (stack-adjacent strings, paths, env-var names). Keep
// the full detail in non-production builds so developers see the
// real cause locally.
const isProd = process.env.NODE_ENV === "production";
// Split construction vs dispatch into two try/catch regions so
// logs distinguish runtime-construction failures (bad config / env)
// from dispatch failures inside handleRequest. Response shape is
// unchanged between the two.
let handleRequest: (req: Request) => Response | Promise<Response>;
try {
handleRequest = getHandleRequest();
} catch (err) {
console.error("[copilotkit/route] runtime construction failed:", err);
return NextResponse.json(
{
error: "Internal error while dispatching CopilotKit request.",
...(isProd
? {}
: { detail: err instanceof Error ? err.message : String(err) }),
},
{ status: 500 },
);
}
try {
return await handleRequest(req);
} catch (err) {
console.error("[copilotkit/route] handleRequest dispatch failed:", err);
return NextResponse.json(
{
error: "Internal error while dispatching CopilotKit request.",
...(isProd
? {}
: { detail: err instanceof Error ? err.message : String(err) }),
},
{ status: 500 },
);
}
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Some files were not shown because too many files have changed in this diff Show More