chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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"]
|
||||
}
|
||||
@@ -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": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
placeholder
|
||||
@@ -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>
|
||||
@@ -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));
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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 Storybook’s 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;
|
||||
}
|
||||
@@ -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: "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>
|
||||
`,
|
||||
})
|
||||
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)
|
||||
- 
|
||||
|
||||
## 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
|
||||
@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 {
|
||||
@Input() text?: string;
|
||||
@Input() inputClass?: string;
|
||||
}
|
||||
|
||||
@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
|
||||
@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 {
|
||||
@Input() text?: string;
|
||||
@Input() inputClass?: string;
|
||||
}
|
||||
|
||||
@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
|
||||
@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 {
|
||||
@Input() inputClass?: string;
|
||||
|
||||
inputValue = '';
|
||||
|
||||
constructor(private chat: ChatState) {}
|
||||
|
||||
handleSend() {
|
||||
const value = this.inputValue.trim();
|
||||
if (value) {
|
||||
this.chat.submitInput(value);
|
||||
this.inputValue = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
@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 {
|
||||
@Input() onClick?: () => void;
|
||||
@Input() inputClass?: string;
|
||||
@Output() clicked = new EventEmitter<void>();
|
||||
|
||||
isHovered = false;
|
||||
|
||||
handleClick() {
|
||||
this.clicked.emit();
|
||||
if (this.onClick) {
|
||||
this.onClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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';
|
||||
|
||||
@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
|
||||
@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 = '';
|
||||
}
|
||||
}
|
||||
|
||||
@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"]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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 Server‑Sent Events; ensure your proxy honors `text/event-stream` and keep‑alive.
|
||||
- The router enables permissive CORS; tighten via Nest’s `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 don’t 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",
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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
Reference in New Issue
Block a user