chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
# Angular Demo Server
Minimal Hono server for the Angular CopilotKit demo, matching the React demo behavior.
## Setup
1. Add your OpenAI API key to `.env`:
```
OPENAI_API_KEY=sk-...
```
2. Install dependencies (from repository root):
```bash
pnpm install
```
3. Start the server:
```bash
pnpm --filter @copilotkit/angular-demo-server dev
```
The server will be available at http://localhost:3001/api/copilotkit
## Testing
To verify the server is running:
```bash
curl http://localhost:3001/api/copilotkit/info
```
You should see JSON with agents and version information.
## Using with Angular Storybook
1. Start the demo server (Terminal A):
```bash
pnpm --filter @copilotkit/angular-demo-server dev
```
2. Start Angular Storybook (Terminal B):
```bash
pnpm --filter storybook-angular dev
```
3. Open http://localhost:6007 and navigate to "Live/CopilotChat"
@@ -0,0 +1,24 @@
{
"name": "@copilotkit/angular-demo-server",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "tsx --env-file-if-exists ../demo/.env --env-file-if-exists .env src/index.ts",
"start": "tsx --env-file-if-exists ../demo/.env --env-file-if-exists .env src/index.ts"
},
"dependencies": {
"@ag-ui/langgraph": "^0.0.11",
"@ai-sdk/openai": "3.0.36",
"@copilotkit/demo-agents": "workspace:^",
"@copilotkit/runtime": "workspace:^",
"@hono/node-server": "^1.13.6",
"hono": "^4.11.4"
},
"devDependencies": {
"nodemon": "^3.1.7",
"tsx": "^4.19.2",
"typescript": "^5.6.3"
}
}
@@ -0,0 +1,144 @@
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { cors } from "hono/cors";
import {
BuiltInAgent,
CopilotRuntime,
createCopilotEndpoint,
InMemoryAgentRunner,
} from "@copilotkit/runtime/v2";
import type { BuiltInAgentClassicConfig } from "@copilotkit/runtime/v2";
import { createOpenAI } from "@ai-sdk/openai";
import { SlowToolCallStreamingAgent } from "@copilotkit/demo-agents";
const openRouterApiKey = process.env.OPENROUTER_API_KEY?.trim();
const openAIApiKey = process.env.OPENAI_API_KEY?.trim();
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
const DEFAULT_OPENROUTER_MODEL = "anthropic/claude-sonnet-4.6";
const DEFAULT_OPENROUTER_MAX_OUTPUT_TOKENS = 16_384;
function determineOpenRouterModelId(): string {
const configuredModel = process.env.OPENROUTER_MODEL?.trim();
if (!configuredModel) {
return DEFAULT_OPENROUTER_MODEL;
}
if (configuredModel.includes("/")) {
return configuredModel;
}
return `openai/${configuredModel}`;
}
function determineMaxOutputTokens(): number | undefined {
if (!openRouterApiKey) {
return undefined;
}
const configuredLimit = Number(
process.env.OPENROUTER_MAX_OUTPUT_TOKENS?.trim(),
);
if (Number.isSafeInteger(configuredLimit) && configuredLimit > 0) {
return configuredLimit;
}
return DEFAULT_OPENROUTER_MAX_OUTPUT_TOKENS;
}
function determineModel(): BuiltInAgentClassicConfig["model"] {
if (openRouterApiKey) {
const openrouter = createOpenAI({
apiKey: openRouterApiKey,
baseURL: process.env.OPENROUTER_BASE_URL?.trim() || OPENROUTER_BASE_URL,
});
return openrouter(determineOpenRouterModelId());
}
if (openAIApiKey) {
return "openai/gpt-5.2";
}
if (process.env.ANTHROPIC_API_KEY?.trim()) {
return "anthropic/claude-3-7-sonnet-20250219";
}
if (process.env.GOOGLE_API_KEY?.trim()) {
return "google/gemini-2.5-pro";
}
return "openai/gpt-5.2";
}
const builtInAgent = new BuiltInAgent({
model: determineModel(),
maxOutputTokens: determineMaxOutputTokens(),
prompt:
"You are a helpful AI assistant. Use reasoning to answer the user's question. If you don't know the answer, say you don't know.",
providerOptions: {
...(openAIApiKey
? { openai: { reasoningEffort: "high", reasoningSummary: "detailed" } }
: {}),
...(!openAIApiKey &&
!openRouterApiKey &&
!!process.env.ANTHROPIC_API_KEY?.trim() && {
anthropic: { thinking: { type: "enabled", budgetTokens: 5000 } },
}),
},
});
const agents = {
default: builtInAgent,
"slow-tools": new SlowToolCallStreamingAgent(),
};
const runtime = new CopilotRuntime({
agents,
runner: new InMemoryAgentRunner(),
a2ui: {},
openGenerativeUI: true,
});
// Create a main app with CORS enabled
const app = new Hono();
// Enable CORS for local dev (Angular demo at http://localhost:4200)
app.use(
"*",
cors({
origin: "http://localhost:4200",
allowMethods: ["GET", "POST", "OPTIONS", "PUT", "DELETE"],
allowHeaders: [
"Content-Type",
"Authorization",
"X-Requested-With",
"x-copilotcloud-public-api-key",
],
exposeHeaders: ["Content-Type"],
credentials: true,
maxAge: 86400,
}),
);
// Create the CopilotKit endpoint
const copilotApp = createCopilotEndpoint({
runtime,
basePath: "/api/copilotkit",
});
// Mount the CopilotKit app
app.route("/", copilotApp);
const port = Number(process.env.PORT || 3001);
const server = serve({ fetch: app.fetch, port });
server.on("error", (error: NodeJS.ErrnoException) => {
if (error.code === "EADDRINUSE") {
console.error(
`Port ${port} is already in use. Stop the existing process or set PORT to another value.`,
);
process.exit(1);
}
throw error;
});
console.log(
`CopilotKit runtime listening at http://localhost:${port}/api/copilotkit`,
);
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "dist"
},
"include": ["src"]
}
+63
View File
@@ -0,0 +1,63 @@
{
"$schema": "https://json.schemastore.org/angular-cli",
"version": 1,
"projects": {
"angular-demo": {
"projectType": "application",
"root": ".",
"sourceRoot": "src",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/angular-demo",
"index": "src/index.html",
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets"],
"styles": [
"../../../../packages/angular/src/styles/generated.css",
"src/styles.css"
],
"optimization": false
}
},
"serve": {
"builder": "@angular/build:dev-server",
"options": {
"buildTarget": "angular-demo:build"
}
}
}
}
},
"cli": {
"analytics": "980356bd-241e-4f9c-91f9-6c7714a97a43"
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}
+62
View File
@@ -0,0 +1,62 @@
{
"name": "@copilotkit/angular-demo",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "ng serve",
"build": "ng build",
"dev": "pnpm -s run serve:clean",
"serve:clean": "rimraf .angular || true && ng serve",
"clean": "rimraf dist .angular"
},
"dependencies": {
"@ag-ui/client": "0.0.53",
"@ag-ui/core": "0.0.53",
"@ag-ui/encoder": "0.0.53",
"@ag-ui/proto": "0.0.53",
"@a2ui/web_core": "0.9.0",
"@angular/animations": "^21.2.15",
"@angular/cdk": "^21.2.13",
"@angular/common": "^21.2.15",
"@angular/compiler": "^21.2.15",
"@angular/core": "^21.2.15",
"@angular/forms": "^21.2.15",
"@angular/platform-browser": "^21.2.15",
"@angular/platform-browser-dynamic": "^21.2.15",
"@angular/router": "^21.2.15",
"@copilotkit/a2ui-renderer": "workspace:*",
"@copilotkit/core": "workspace:*",
"@copilotkit/shared": "workspace:*",
"@copilotkit/web-inspector": "workspace:*",
"@copilotkit/angular": "workspace:*",
"@jetbrains/websandbox": "^1.1.3",
"@tanstack/pacer": "^0.20.1",
"clsx": "^2.1.1",
"compare-versions": "^6.1.1",
"fast-json-patch": "^3.1.1",
"highlight.js": "^11.11.1",
"katex": "^0.16.22",
"lit": "^3.3.1",
"lucide": "^0.525.0",
"lucide-angular": "^0.540.0",
"marked": "^16.2.0",
"partial-json": "^0.1.7",
"phoenix": "^1.8.4",
"rxjs": "^7.8.1",
"tailwind-merge": "^2.6.0",
"tslib": "^2.8.1",
"untruncate-json": "^0.0.1",
"uuid": "^11.1.0",
"zod": "^3.25.75",
"zod-to-json-schema": "^3.24.6"
},
"devDependencies": {
"@angular/build": "^21.2.13",
"@angular/cli": "^21.2.13",
"@angular/compiler-cli": "^21.2.15",
"nodemon": "^3.1.7",
"rimraf": "^6.0.1",
"typescript": "5.9.3"
}
}
@@ -0,0 +1,60 @@
import { Component, inject } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import {
ActivatedRoute,
NavigationEnd,
Router,
RouterOutlet,
} from "@angular/router";
import { filter, map, startWith } from "rxjs";
import { DemoWebInspectorComponent } from "./components/demo-web-inspector.component";
@Component({
selector: "app-root",
standalone: true,
imports: [RouterOutlet, DemoWebInspectorComponent],
template: `
<div class="demo-shell">
<router-outlet />
@if (showInspector()) {
<angular-demo-web-inspector />
}
</div>
`,
styles: `
.demo-shell {
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
overflow: hidden;
display: block;
}
`,
})
export class AppComponent {
private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);
/**
* The web inspector is shown on every route except those that opt out via
* `data: { inspector: false }` (currently the headless route).
*/
protected readonly showInspector = toSignal(
this.router.events.pipe(
filter((event) => event instanceof NavigationEnd),
map(() => this.inspectorEnabled()),
startWith(this.inspectorEnabled()),
),
{ initialValue: true },
);
private inspectorEnabled(): boolean {
let route = this.route;
while (route.firstChild) {
route = route.firstChild;
}
return route.snapshot.data["inspector"] !== false;
}
}
@@ -0,0 +1,44 @@
import type { ApplicationConfig } from "@angular/core";
import { importProvidersFrom } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { provideRouter } from "@angular/router";
import {
provideCopilotKit,
provideCopilotChatLabels,
} from "@copilotkit/angular";
import { WildcardToolRenderComponent } from "./components/wildcard-tool-render.component";
import { a2uiDemoSandboxFunctions } from "./routes/a2ui/a2ui-demo-sandbox-functions";
import { routes } from "./app.routes";
import { z } from "zod";
export const appConfig: ApplicationConfig = {
providers: [
importProvidersFrom(BrowserModule),
provideRouter(routes),
provideCopilotKit({
runtimeUrl: "http://localhost:3001/api/copilotkit",
licenseKey: "ck_pub_00000000000000000000000000000000",
renderToolCalls: [
{
name: "*",
args: z.record(z.string(), z.unknown()),
component: WildcardToolRenderComponent,
},
],
suggestionsConfig: [
{
instructions:
"Suggest follow-up tasks based on the current page content",
available: "always",
},
],
humanInTheLoop: [],
openGenerativeUI: { sandboxFunctions: a2uiDemoSandboxFunctions },
}),
provideCopilotChatLabels({
chatInputPlaceholder: "Ask me anything...",
chatDisclaimerText:
"CopilotKit Angular Demo - AI responses may need verification.",
}),
],
};
@@ -0,0 +1,57 @@
import type { Routes } from "@angular/router";
export const routes: Routes = [
{
// Default landing: redirect the root to the A2UI demo.
path: "",
pathMatch: "full",
redirectTo: "a2ui-demo",
},
{
path: "a2ui-demo",
title: "A2UI Demo",
loadComponent: () =>
import("./routes/a2ui/a2ui-demo.component").then(
(m) => m.A2UIDemoComponent,
),
},
{
path: "headless",
title: "Headless Chat",
loadComponent: () =>
import("./routes/headless/headless-chat.component").then(
(m) => m.HeadlessChatComponent,
),
// The web inspector is hidden on the headless route.
data: { inspector: false },
},
{
path: "custom-input",
title: "Custom Input",
loadComponent: () =>
import("./routes/custom-input/custom-input-chat.component").then(
(m) => m.CustomInputChatComponent,
),
},
{
path: "ukg-port",
title: "UKG Port",
loadComponent: () =>
import("./routes/ukg-port/co-pilot-port.component").then(
(m) => m.CoPilotPortComponent,
),
},
{
path: "default",
title: "Default Chat",
loadComponent: () =>
import("./routes/default/default-chat.component").then(
(m) => m.DefaultChatComponent,
),
},
{
// Unknown paths fall back to the default landing (A2UI demo).
path: "**",
redirectTo: "a2ui-demo",
},
];
@@ -0,0 +1,37 @@
import { afterNextRender, Component, DestroyRef, inject } from "@angular/core";
import { CopilotKit } from "@copilotkit/angular";
import { WEB_INSPECTOR_TAG } from "@copilotkit/web-inspector";
import type { WebInspectorElement } from "@copilotkit/web-inspector";
@Component({
selector: "angular-demo-web-inspector",
standalone: true,
template: "",
})
export class DemoWebInspectorComponent {
readonly #copilotKit = inject(CopilotKit);
readonly #destroyRef = inject(DestroyRef);
constructor() {
afterNextRender(() => {
const existing =
document.querySelector<WebInspectorElement>(WEB_INSPECTOR_TAG);
const inspector =
existing ??
(document.createElement(WEB_INSPECTOR_TAG) as WebInspectorElement);
inspector.core = this.#copilotKit.core;
inspector.setAttribute("auto-attach-core", "false");
if (!existing) {
document.body.appendChild(inspector);
}
this.#destroyRef.onDestroy(() => {
if (inspector.isConnected) {
inspector.remove();
}
});
});
}
}
@@ -0,0 +1,280 @@
import {
ChangeDetectionStrategy,
Component,
computed,
input,
linkedSignal,
} from "@angular/core";
import { CommonModule } from "@angular/common";
import {
Check,
ChevronDown,
LoaderCircle,
LucideAngularModule,
Wrench,
} from "lucide-angular";
import type { AngularToolCall, ToolRenderer } from "@copilotkit/angular";
type WildcardToolArgs = Record<string, unknown>;
type ToolEntry = {
key: string;
value: string;
};
@Component({
selector: "wildcard-tool-render",
standalone: true,
imports: [CommonModule, LucideAngularModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="copilot-tool-reasoning" data-testid="wildcard-tool-render">
<button
type="button"
class="copilot-tool-summary"
[class.copilot-tool-summary--static]="!hasDetails()"
[attr.aria-expanded]="hasDetails() ? open() : null"
(click)="toggle()"
>
@if (isRunning()) {
<lucide-angular
[img]="LoaderCircleIcon"
[size]="14"
class="copilot-tool-icon copilot-tool-icon--spin"
/>
} @else {
<lucide-angular
[img]="CheckIcon"
[size]="14"
class="copilot-tool-icon copilot-tool-icon--complete"
/>
}
<lucide-angular [img]="WrenchIcon" [size]="14" class="copilot-tool-icon" />
<span class="copilot-tool-name">{{ toolName() }}</span>
<span class="copilot-tool-status">{{ statusLabel() }}</span>
@if (hasDetails()) {
<lucide-angular
[img]="ChevronDownIcon"
[size]="14"
class="copilot-tool-chevron"
[class.copilot-tool-chevron--open]="open()"
/>
}
</button>
@if (hasDetails()) {
<div
class="copilot-tool-details-wrap"
[style.grid-template-rows]="open() ? '1fr' : '0fr'"
>
<div class="copilot-tool-details-clip">
<div class="copilot-tool-details">
@for (entry of entries(); track entry.key) {
<div class="copilot-tool-entry">
<span class="copilot-tool-entry-key">{{ entry.key }}:</span>
<span class="copilot-tool-entry-value">{{ entry.value }}</span>
</div>
}
@if (resultSummary(); as result) {
<div class="copilot-tool-entry">
<span class="copilot-tool-entry-key">result:</span>
<span class="copilot-tool-entry-value">{{ result }}</span>
</div>
}
</div>
</div>
</div>
}
</div>
`,
styles: [
`
.copilot-tool-reasoning {
margin: 6px 0;
color: var(--muted-foreground, #737373);
}
.copilot-tool-summary {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
border: 0;
background: transparent;
color: inherit;
cursor: pointer;
padding: 4px 0;
font: inherit;
font-size: 14px;
text-align: left;
transition: color 0.15s ease;
}
.copilot-tool-summary:hover {
color: var(--foreground, #171717);
}
.copilot-tool-summary--static {
cursor: default;
}
.copilot-tool-summary--static:hover {
color: inherit;
}
.copilot-tool-icon {
width: 14px;
height: 14px;
flex: 0 0 14px;
}
.copilot-tool-icon--spin {
animation: copilot-tool-spin 1s linear infinite;
}
.copilot-tool-icon--complete {
color: #10b981;
}
.copilot-tool-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--foreground, #171717);
font-family:
var(--font-code), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
font-weight: 500;
}
.copilot-tool-status {
flex: 0 0 auto;
font-size: 12px;
color: var(--muted-foreground, #737373);
}
.copilot-tool-chevron {
margin-left: auto;
width: 14px;
height: 14px;
flex: 0 0 14px;
transition: transform 0.2s ease;
}
.copilot-tool-chevron--open {
transform: rotate(180deg);
}
.copilot-tool-details-wrap {
display: grid;
transition: grid-template-rows 0.2s ease;
}
.copilot-tool-details-clip {
overflow: hidden;
}
.copilot-tool-details {
margin: 6px 0 0 22px;
padding: 8px 12px;
display: grid;
gap: 4px;
border-radius: 6px;
background: var(--secondary, #f5f5f5);
}
.copilot-tool-entry {
min-width: 0;
display: flex;
gap: 8px;
font-family:
var(--font-code), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
font-size: 12px;
line-height: 1.4;
}
.copilot-tool-entry-key {
flex: 0 0 auto;
color: var(--muted-foreground, #737373);
}
.copilot-tool-entry-value {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--foreground, #171717);
}
@keyframes copilot-tool-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
`,
],
})
export class WildcardToolRenderComponent implements ToolRenderer<WildcardToolArgs> {
readonly toolCall = input.required<AngularToolCall<WildcardToolArgs>>();
protected readonly LoaderCircleIcon = LoaderCircle;
protected readonly CheckIcon = Check;
protected readonly ChevronDownIcon = ChevronDown;
protected readonly WrenchIcon = Wrench;
protected readonly isRunning = computed(
() => this.toolCall().status !== "complete",
);
protected readonly open = linkedSignal(() => this.isRunning());
protected readonly toolName = computed(() => this.toolCall().name ?? "tool");
protected readonly entries = computed<ToolEntry[]>(() =>
Object.entries(this.toolCall().args ?? {}).map(([key, value]) => ({
key,
value: this.formatValue(value),
})),
);
protected readonly resultSummary = computed(() => {
const toolCall = this.toolCall();
if (toolCall.status !== "complete") return undefined;
if (!toolCall.result) return undefined;
return this.formatValue(toolCall.result);
});
protected readonly hasDetails = computed(
() => this.entries().length > 0 || this.resultSummary() !== undefined,
);
protected readonly statusLabel = computed(() =>
this.isRunning() ? "Running" : "Complete",
);
private formatValue(value: unknown): string {
if (Array.isArray(value)) return `[${value.length} items]`;
if (typeof value === "object" && value !== null) {
return `{${Object.keys(value).length} keys}`;
}
if (typeof value === "string") return `"${value}"`;
if (value === undefined) return "undefined";
return String(value);
}
protected toggle(): void {
if (!this.hasDetails()) return;
this.open.update((value) => !value);
}
}
@@ -0,0 +1,39 @@
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
import { CopilotChatInput, injectChatState } from "@copilotkit/angular";
import type { ToolsMenuItem } from "@copilotkit/angular";
@Component({
selector: "a2ui-demo-input",
standalone: true,
imports: [CopilotChatInput],
template: `
<copilot-chat-input [inputClass]="inputClass() ?? ''" [toolsMenu]="toolsMenu" />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class A2UIDemoInputComponent {
readonly inputClass = input<string | undefined>();
private readonly chatState = injectChatState();
readonly toolsMenu: (ToolsMenuItem | "-")[] = [
{
label: "Say hi to CopilotKit",
action: () => {
this.chatState.changeInput(
"Hello Copilot! 👋 Could you help me with something?",
);
},
},
"-",
{
label: "Open CopilotKit Docs",
action: () => {
window.open(
"https://docs.copilotkit.ai",
"_blank",
"noopener,noreferrer",
);
},
},
];
}
@@ -0,0 +1,36 @@
import type { SandboxFunction } from "@copilotkit/angular";
import { z } from "zod";
export type Theme = "light" | "dark";
type ThemeHandler = (mode: Theme) => void;
let currentThemeHandler: ThemeHandler | undefined;
const setThemeParameters = z.object({
mode: z.enum(["light", "dark"]).describe("The theme mode to set"),
});
export function bindA2UIDemoThemeHandler(handler: ThemeHandler): () => void {
currentThemeHandler = handler;
return () => {
if (currentThemeHandler === handler) {
currentThemeHandler = undefined;
}
};
}
export const a2uiDemoSandboxFunctions: SandboxFunction[] = [
{
name: "setTheme",
description:
"Switch the host application theme between light and dark mode. " +
"Call this when the user asks to change the theme or when generating UI with a theme toggle.",
parameters: setThemeParameters,
handler: async (args) => {
const { mode } = setThemeParameters.parse(args);
currentThemeHandler?.(mode);
return `Theme set to ${mode}`;
},
},
];
@@ -0,0 +1,275 @@
import {
ChangeDetectionStrategy,
Component,
DestroyRef,
computed,
inject,
signal,
} from "@angular/core";
import {
CopilotChat,
connectAgentContext,
provideCopilotChatLabels,
registerFrontendTool,
} from "@copilotkit/angular";
import type { AttachmentsConfig } from "@copilotkit/angular";
import { A2UIDemoInputComponent } from "./a2ui-demo-input.component";
import { z } from "zod";
import { bindA2UIDemoThemeHandler } from "./a2ui-demo-sandbox-functions";
import type { Theme } from "./a2ui-demo-sandbox-functions";
type ThreadId = "thread---a" | "thread---b" | "thread---c";
const themeColors = {
light: {
bg: "oklch(1 0 0)",
text: "oklch(0.145 0 0)",
border: "oklch(0.922 0 0)",
muted: "oklch(0.97 0 0)",
},
dark: {
bg: "oklch(0.145 0 0)",
text: "oklch(0.985 0 0)",
border: "oklch(0.269 0 0)",
muted: "oklch(0.269 0 0)",
},
} satisfies Record<Theme, Record<"bg" | "text" | "border" | "muted", string>>;
const threadOptions: Array<{ id: ThreadId | undefined; label: string }> = [
{ id: undefined, label: "Stateless" },
{ id: "thread---a", label: "Thread A" },
{ id: "thread---b", label: "Thread B" },
{ id: "thread---c", label: "Thread C" },
];
@Component({
selector: "a2ui-demo",
standalone: true,
imports: [CopilotChat],
template: `
<div
class="a2ui-demo-root"
[class.dark]="theme() === 'dark'"
[style.background-color]="colors().bg"
[style.color]="colors().text"
>
<div class="a2ui-demo-shell" data-testid="a2ui-demo-shell">
<div class="a2ui-demo-toolbar" data-testid="a2ui-demo-toolbar">
<button
type="button"
class="a2ui-demo-theme-toggle"
(click)="toggleTheme()"
[attr.aria-label]="
'Switch to ' + (theme() === 'light' ? 'dark' : 'light') + ' mode'
"
[style.border]="'1px solid ' + colors().border"
[style.background-color]="colors().muted"
[style.color]="colors().text"
>
@if (theme() === "light") {
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
} @else {
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</svg>
}
</button>
<div class="a2ui-demo-thread-tabs">
@for (option of threadOptions; track option.label) {
<button
type="button"
class="a2ui-demo-thread-tab"
(click)="selectThread(option.id)"
[attr.aria-pressed]="option.id === selectedThreadId()"
[style.border]="threadBorder(option.id)"
[style.background-color]="threadBackground(option.id)"
[style.color]="threadColor(option.id)"
>
{{ option.label }}
</button>
}
</div>
</div>
<div class="a2ui-demo-chat">
@if (selectedThreadId(); as threadId) {
<copilot-chat
[class]="theme() === 'dark' ? 'dark' : undefined"
[threadId]="threadId"
[inputComponent]="inputComponent"
[attachments]="attachments"
/>
} @else {
<copilot-chat
[class]="theme() === 'dark' ? 'dark' : undefined"
[inputComponent]="inputComponent"
[attachments]="attachments"
/>
}
</div>
</div>
</div>
`,
styles: [
`
.a2ui-demo-root {
height: 100vh;
margin: 0;
padding: 0;
overflow: hidden;
font-family: Arial, Helvetica, sans-serif;
line-height: 1.5;
transition:
background-color 0.3s,
color 0.3s;
}
.a2ui-demo-shell {
display: flex;
flex-direction: column;
height: 100%;
padding: 16px;
gap: 16px;
}
.a2ui-demo-toolbar {
display: flex;
gap: 10px;
align-items: center;
}
.a2ui-demo-theme-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
cursor: pointer;
transition: all 0.15s ease-in-out;
}
.a2ui-demo-thread-tabs {
flex: 1;
display: flex;
gap: 10px;
justify-content: center;
}
.a2ui-demo-thread-tab {
padding: 6px 14px;
border-radius: 20px;
font-weight: 600;
font-size: 0.85rem;
line-height: 1.5;
cursor: pointer;
transition: all 0.15s ease-in-out;
}
.a2ui-demo-chat {
flex: 1;
min-height: 0;
}
`,
],
providers: [
provideCopilotChatLabels({
chatInputPlaceholder: "Type a message...",
chatDisclaimerText:
"AI can make mistakes. Please verify important information.",
}),
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class A2UIDemoComponent {
private readonly destroyRef = inject(DestroyRef);
readonly inputComponent = A2UIDemoInputComponent;
readonly threadOptions = threadOptions;
readonly attachments: AttachmentsConfig = {
enabled: true,
accept: "image/*,audio/*,video/*,.pdf,.txt,.md,application/pdf,text/*",
};
readonly theme = signal<Theme>("light");
readonly selectedThreadId = signal<ThreadId | undefined>(undefined);
readonly colors = computed(() => themeColors[this.theme()]);
readonly agentContext = computed(() => ({
description: "The current Thread ID is:",
value: this.selectedThreadId() ?? "stateless",
}));
constructor() {
connectAgentContext(this.agentContext);
this.destroyRef.onDestroy(
bindA2UIDemoThemeHandler((mode) => this.theme.set(mode)),
);
registerFrontendTool<{ name: string }>({
name: "sayHello",
description: "Use this tool to greet the user by name.",
parameters: z.object({
name: z.string(),
}),
handler: async ({ name }) => {
window.alert(`Hello ${name}`);
return `Hello ${name}`;
},
});
}
toggleTheme(): void {
this.theme.update((theme) => (theme === "light" ? "dark" : "light"));
}
selectThread(threadId: ThreadId | undefined): void {
this.selectedThreadId.set(threadId);
}
threadBorder(threadId: ThreadId | undefined): string {
return threadId === this.selectedThreadId()
? `2px solid ${this.colors().text}`
: `1px solid ${this.colors().border}`;
}
threadBackground(threadId: ThreadId | undefined): string {
return threadId === this.selectedThreadId()
? this.colors().text
: this.colors().bg;
}
threadColor(threadId: ThreadId | undefined): string {
return threadId === this.selectedThreadId()
? this.colors().bg
: this.colors().text;
}
}
@@ -0,0 +1,168 @@
import {
ChangeDetectionStrategy,
Component,
inject,
input,
} from "@angular/core";
import { FormsModule } from "@angular/forms";
import { injectChatState } from "@copilotkit/angular";
@Component({
selector: "nextgen-custom-input",
standalone: true,
imports: [FormsModule],
template: `
<form
class="ck-input-wrapper"
(ngSubmit)="submit()"
[class.ck-disabled]="inProgress()"
novalidate
autocomplete="off"
>
<button
type="button"
class="ck-icon"
[disabled]="inProgress()"
title="Add attachment"
>
</button>
<label for="ck-message" class="sr-only">Message</label>
<input
id="ck-message"
name="message"
class="ck-input"
type="text"
[(ngModel)]="value"
(ngModelChange)="chatState.changeInput($event)"
[disabled]="inProgress()"
placeholder="Ask anything…"
autocapitalize="sentences"
autocomplete="off"
spellcheck="true"
maxlength="4000"
(keydown)="onKeyDown($event)"
(compositionstart)="composing = true"
(compositionend)="composing = false"
/>
<button
type="submit"
class="ck-send"
[disabled]="inProgress() || !canSend"
[attr.aria-label]="'Send message'"
>
</button>
</form>
`,
styles: [
`
.ck-input-wrapper {
display: flex;
align-items: center;
gap: 8px;
background: #fff;
border-radius: 16px;
padding: 10px 12px;
margin: 8px;
border: 1px solid #eee;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
}
.ck-input-wrapper.ck-disabled {
opacity: 0.7;
pointer-events: none;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.ck-icon {
width: 36px;
height: 36px;
border-radius: 50%;
border: 1px solid #ddd;
background: #fff;
display: grid;
place-items: center;
cursor: pointer;
}
.ck-input {
flex: 1;
border: none;
outline: none;
background: transparent;
font-size: 14px;
color: #333;
padding: 8px 4px;
}
.ck-input::placeholder {
color: #a0a0a0;
}
.ck-send {
width: 36px;
height: 36px;
border-radius: 50%;
border: none;
display: grid;
place-items: center;
background: #000;
color: #fff;
cursor: pointer;
font-size: 16px;
}
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomChatInputComponent {
readonly inProgress = input(false);
readonly inputClass = input<string>();
value = "";
composing = false;
readonly chatState = injectChatState();
get valueTrimmed(): string {
return this.value.trim();
}
get canSend(): boolean {
return this.valueTrimmed.length > 0;
}
async submit(): Promise<void> {
if (this.inProgress()) return;
if (!this.canSend) return;
this.chatState.submitInput(this.valueTrimmed);
this.value = "";
}
onKeyDown(e: KeyboardEvent): void {
if (this.composing) return;
if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
this.submit();
}
}
}
@@ -0,0 +1,30 @@
import { ChangeDetectionStrategy, Component } from "@angular/core";
import {
CopilotChatView,
CopilotChat,
provideCopilotChatLabels,
} from "@copilotkit/angular";
import { CustomChatInputComponent } from "./custom-chat-input.component";
@Component({
selector: "nextgen-custom-input-chat",
standalone: true,
imports: [CopilotChat],
template: `
<div style="display: block; height: 100vh">
<copilot-chat [inputComponent]="customInput" />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
// Optional: tweak labels/placeholders shown by CopilotKit
provideCopilotChatLabels({
chatInputPlaceholder: "Ask anything...",
chatDisclaimerText: "AI can make mistakes. Verify important info.",
}),
],
})
export class CustomInputChatComponent {
customInput = CustomChatInputComponent;
}
@@ -0,0 +1,13 @@
import { Component } from "@angular/core";
import { CopilotChat } from "@copilotkit/angular";
@Component({
selector: "default-chat",
standalone: true,
imports: [CopilotChat],
template: `
<copilot-chat [threadId]="'xyz'" />
`,
})
export class DefaultChatComponent {}
@@ -0,0 +1,235 @@
import type { OnDestroy, OnInit } from "@angular/core";
import {
Component,
ChangeDetectionStrategy,
computed,
inject,
input,
signal,
} from "@angular/core";
import { TitleCasePipe } from "@angular/common";
import { FormsModule } from "@angular/forms";
import type {
HumanInTheLoopToolCall,
HumanInTheLoopToolRenderer,
} from "@copilotkit/angular";
import {
connectAgentContext,
CopilotKit,
injectAgentStore,
registerHumanInTheLoop,
} from "@copilotkit/angular";
import { RenderToolCalls } from "@copilotkit/angular";
import { WEB_INSPECTOR_TAG } from "@copilotkit/web-inspector";
import type { WebInspectorElement } from "@copilotkit/web-inspector";
import { z } from "zod";
@Component({
selector: "require-approval",
standalone: true,
imports: [FormsModule],
template: `
<div>Require approval</div>
<button (click)="respond({ approved: true })">Approve</button>
<button (click)="respond({ approved: false })">Deny</button>
`,
})
export class RequireApprovalComponent implements HumanInTheLoopToolRenderer {
toolCall =
input.required<
HumanInTheLoopToolCall<{ action: string; reason: string }>
>();
respond(result: { approved: boolean }) {
this.toolCall().respond(result);
}
}
@Component({
selector: "headless-chat",
standalone: true,
imports: [FormsModule, RenderToolCalls, TitleCasePipe],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div
class="headless-container"
style="display: flex; flex-direction: column; height: 100vh; width: 100vw"
>
<div
class="messages"
style="
flex: 1;
overflow: auto;
padding: 16px;
background: #f9fafb;
color: #111827;
"
>
@for (m of messages(); track m) {
<div style="margin-bottom: 16px">
<div style="font-weight: 600; color: #374151">
{{ m.role | titlecase }}
</div>
<div style="white-space: pre-wrap">{{ m.content }}</div>
@if (m.role === "assistant") {
<copilot-render-tool-calls
[message]="m"
[messages]="messages()"
[isLoading]="isRunning()"
/>
}
</div>
}
@if (isRunning()) {
<div style="opacity: 0.9; color: #6b7280">Thinking…</div>
}
</div>
<form
(ngSubmit)="send()"
style="
display: flex;
gap: 8px;
padding: 12px;
background: #ffffff;
border-top: 1px solid #e5e7eb;
"
>
<input
name="message"
[(ngModel)]="inputValue"
[disabled]="isRunning()"
placeholder="Type a message…"
style="
flex: 1;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid #d1d5db;
background: #ffffff;
color: #111827;
outline: none;
"
/>
<button
type="submit"
[disabled]="!inputValue.trim() || isRunning()"
style="
padding: 10px 14px;
border-radius: 8px;
border: 1px solid #1d4ed8;
background: #2563eb;
color: #ffffff;
cursor: pointer;
"
>
Send
</button>
<button
type="button"
(click)="clearThreads()"
style="
padding: 10px 14px;
border-radius: 8px;
border: 1px solid #d1d5db;
background: #ffffff;
color: #374151;
cursor: pointer;
"
>
Clear threads
</button>
</form>
</div>
`,
})
export class HeadlessChatComponent implements OnInit, OnDestroy {
readonly agentStore = injectAgentStore("openai");
readonly agent = computed(() => this.agentStore()?.agent);
readonly isRunning = computed(() => !!this.agentStore()?.isRunning());
readonly messages = computed(() => this.agentStore()?.messages());
readonly copilotkit = inject(CopilotKit);
inputValue = "";
private inspectorElement: WebInspectorElement | null = null;
constructor() {
registerHumanInTheLoop({
name: "requireApproval",
description: "Requires human approval before proceeding",
parameters: z.object({
action: z.string(),
reason: z.string(),
}),
component: RequireApprovalComponent,
});
connectAgentContext(
signal({
value: "voice-mode",
description: "active",
}),
);
}
ngOnInit(): void {
if (typeof document === "undefined") return;
const existing =
document.querySelector<WebInspectorElement>(WEB_INSPECTOR_TAG);
const inspector =
existing ??
(document.createElement(WEB_INSPECTOR_TAG) as WebInspectorElement);
inspector.core = this.copilotkit.core;
inspector.setAttribute("auto-attach-core", "false");
if (!existing) {
document.body.appendChild(inspector);
}
this.inspectorElement = inspector;
}
ngOnDestroy(): void {
if (this.inspectorElement && this.inspectorElement.isConnected) {
this.inspectorElement.remove();
}
this.inspectorElement = null;
}
async clearThreads() {
const runtimeUrl = this.copilotkit.core?.runtimeUrl;
if (!runtimeUrl) return;
const url = runtimeUrl.replace(/\/$/, "");
try {
const res = await fetch(`${url}/threads/clear`, {
method: "POST",
credentials: "include",
});
if (!res.ok) {
console.error(
`Failed to clear threads: HTTP ${res.status} ${res.statusText}`,
);
}
} catch (err) {
console.error("Failed to clear threads", err);
}
}
async send() {
const content = this.inputValue.trim();
const agent = this.agent();
const isRunning = this.isRunning();
if (!agent || !content || isRunning) return;
agent.addMessage({ id: crypto.randomUUID(), role: "user", content });
this.inputValue = "";
try {
await this.copilotkit.core.runAgent({ agent });
} catch (e) {
console.error("Agent run error", e);
}
}
}
@@ -0,0 +1,24 @@
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { CopilotChatView, provideCopilotChatLabels } from "@copilotkit/angular";
import { CustomChatInputComponent } from "../custom-input/custom-chat-input.component";
@Component({
selector: "ukg-co-pilot-port",
standalone: true,
imports: [CopilotChatView],
template: `
<div style="display: block; height: 100vh">
<copilot-chat-view [inputComponent]="customInput" />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
provideCopilotChatLabels({
chatInputPlaceholder: "Ask anything... (UKG port)",
chatDisclaimerText: "AI may be inaccurate (UKG PORT).",
}),
],
})
export class CoPilotPortComponent {
customInput = CustomChatInputComponent;
}
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
placeholder
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>CopilotKit Angular Demo</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
import { provideZonelessChangeDetection } from "@angular/core";
import { bootstrapApplication } from "@angular/platform-browser";
import { AppComponent } from "./app/app.component";
import { appConfig } from "./app/app.config";
bootstrapApplication(AppComponent, {
...appConfig,
providers: [provideZonelessChangeDetection(), ...appConfig.providers],
}).catch((err) => console.error(err));
+9
View File
@@ -0,0 +1,9 @@
html,
body {
margin: 0;
height: 100%;
}
body {
font-family: Arial, Helvetica, sans-serif;
}
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}
+37
View File
@@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": false,
"esModuleInterop": true,
"module": "ES2020",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noPropertyAccessFromIndexSignature": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"baseUrl": ".",
"paths": {
"@copilotkit/angular": ["../../../../packages/angular/src/index.ts"],
"@copilotkit/angular/*": ["../../../../packages/angular/src/*"],
"@copilotkit/core": ["node_modules/@copilotkit/core"],
"@copilotkit/shared": ["node_modules/@copilotkit/shared"],
"@copilotkit/web-inspector": ["node_modules/@copilotkit/web-inspector"],
"@copilotkit/a2ui-renderer": ["node_modules/@copilotkit/a2ui-renderer"],
"@copilotkit/a2ui-renderer/web-components": [
"node_modules/@copilotkit/a2ui-renderer/web-components"
],
"@copilotkit/a2ui-renderer/web-components/define": [
"node_modules/@copilotkit/a2ui-renderer/web-components/define"
]
}
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
},
"exclude": ["dist", "node_modules"]
}
@@ -0,0 +1,22 @@
import type { StorybookConfig } from "@storybook/angular";
const config: StorybookConfig = {
framework: {
name: "@storybook/angular",
options: {},
},
stories: ["../stories/**/*.stories.@(ts|tsx|mdx)"],
addons: ["@storybook/addon-themes"],
webpackFinal: async (cfg) => {
// Suppress size warnings for development
cfg.performance = {
...cfg.performance,
maxAssetSize: 5000000, // 5MB
maxEntrypointSize: 5000000, // 5MB
};
return cfg;
},
};
export default config;
@@ -0,0 +1,97 @@
/* Storybook dark mode custom background */
html.dark {
background-color: #212121;
}
html.dark body {
background-color: #212121;
}
/* Ensure the story canvas also uses the dark background */
html.dark .docs-story,
html.dark .sb-show-main,
html.dark .sb-main-padded {
background-color: #212121;
}
/* Custom disclaimer demo styles (global to ensure they apply in stories) */
.custom-disclaimer {
background: linear-gradient(
90deg,
#ff6b6b 0%,
#4ecdc4 50%,
#45b7d1 100%
) !important;
color: white;
font-weight: 600;
font-size: 14px;
padding: 16px 24px;
margin: 12px auto;
border-radius: 12px;
text-align: center;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
animation: disclaimer-pulse 3s ease-in-out infinite;
position: relative;
overflow: hidden;
}
.custom-disclaimer::before {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(
45deg,
transparent,
rgba(255, 255, 255, 0.1),
transparent
);
transform: rotate(45deg);
animation: disclaimer-shimmer 3s infinite;
}
@keyframes disclaimer-pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.02);
}
}
@keyframes disclaimer-shimmer {
0% {
transform: translateX(-100%) translateY(-100%) rotate(45deg);
}
100% {
transform: translateX(100%) translateY(100%) rotate(45deg);
}
}
.custom-disclaimer-text {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
gap: 8px;
}
.custom-disclaimer-icon {
position: relative;
z-index: 1;
font-size: 20px;
animation: disclaimer-bounce 2s infinite;
}
@keyframes disclaimer-bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
@@ -0,0 +1,55 @@
import type { Preview } from "@storybook/angular";
import { withThemeByClassName } from "@storybook/addon-themes";
const preview: Preview = {
parameters: {
// Disable the backgrounds addon to avoid conflicts with dark mode
backgrounds: { disable: true },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
docs: {
toc: true,
// Canvas (bottom) code panel behavior
canvas: { sourceState: "shown" }, // Show source code by default
// Enable the separate Code panel in Docs tab
codePanel: true,
// Configure source display
source: {
type: "dynamic", // Update snippet as args/Controls change
// Ensure the code pane reflects the actual story template
transform: (src: string, ctx: any) => {
try {
// Prefer the currently rendered story function
const storyResult = (ctx?.storyFn || ctx?.originalStoryFn)?.(
ctx?.args || {},
);
if (
storyResult &&
typeof storyResult === "object" &&
"template" in storyResult
) {
return (storyResult as any).template as string;
}
} catch {}
// Fallback to Storybooks generated source
return src;
},
},
},
},
decorators: [
withThemeByClassName({
themes: {
light: "", // default = no extra class
dark: "dark", // adds class="dark" to <html> in the preview iframe
},
defaultTheme: "light",
}),
],
};
export default preview;
@@ -0,0 +1,18 @@
/* Import the pre-built Tailwind CSS */
@import "../../../../../packages/angular/dist/styles.css";
/* Storybook dark mode custom background */
html.dark {
background-color: #212121;
}
html.dark body {
background-color: #212121;
}
/* Ensure the story canvas also uses the dark background */
html.dark .docs-story,
html.dark .sb-show-main,
html.dark .sb-main-padded {
background-color: #212121;
}
+101
View File
@@ -0,0 +1,101 @@
# @copilotkit-storybook/angular
## 0.0.6-next.1
### Patch Changes
- Updated dependencies [1ceb963]
- @copilotkit/core@1.55.0-next.7
## 0.0.6-next.0
### Patch Changes
- @copilotkitnext/core@1.54.1-next.0
## 0.0.5
### Patch Changes
- Updated dependencies [eb1e0bf]
- Updated dependencies [3780c6a]
- Updated dependencies [c80498e]
- Updated dependencies [f1571ef]
- @copilotkitnext/core@1.54.0
## 0.0.5-next.1
### Patch Changes
- @copilotkitnext/core@1.54.0-next.3
## 0.0.5-next.0
### Patch Changes
- @copilotkit/core@1.53.1-next.0
## 0.0.4
### Patch Changes
- Updated dependencies [1510f64]
- @copilotkit/core@1.53.0
## 0.0.4-next.1
### Patch Changes
- @copilotkit/core@1.53.0-next.5
## 0.0.4-next.0
### Patch Changes
- @copilotkit/core@1.52.2-next.0
## 0.0.3
### Patch Changes
- @copilotkit/core@1.52.1
## 0.0.3-next.0
### Patch Changes
- @copilotkit/core@1.52.1-next.0
## 0.0.2
### Patch Changes
- Updated dependencies [d77f347]
- Updated dependencies [ef0f539]
- @copilotkit/core@1.52.0
## 0.0.2-next.1
### Patch Changes
- @copilotkit/core@1.52.0-next.5
## 0.0.2-next.0
### Patch Changes
- Updated dependencies [ef0f539]
- @copilotkit/core@1.51.5-next.0
## 0.0.1
### Patch Changes
- Updated dependencies [c998f30]
- @copilotkit/core@1.51.4
## 0.0.1-next.0
### Patch Changes
- @copilotkit/core@1.51.4-next.0
@@ -0,0 +1,76 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"storybook-angular": {
"projectType": "application",
"root": "",
"sourceRoot": "stories",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser-esbuild",
"options": {
"outputPath": "dist",
"index": "stories/index.html",
"main": "stories/main.ts",
"polyfills": [],
"tsConfig": "tsconfig.json",
"assets": [],
"styles": [
"../../../../packages/angular/dist/styles.css",
".storybook/preview.css"
],
"scripts": []
},
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false
},
"development": {
"optimization": false,
"sourceMap": true
}
},
"defaultConfiguration": "development"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "storybook-angular:build:production"
},
"development": {
"buildTarget": "storybook-angular:build:development"
}
},
"defaultConfiguration": "development"
},
"storybook": {
"builder": "@storybook/angular:start-storybook",
"options": {
"browserTarget": "storybook-angular:build:development",
"configDir": ".storybook",
"port": 6007,
"compodoc": false
}
},
"build-storybook": {
"builder": "@storybook/angular:build-storybook",
"options": {
"browserTarget": "storybook-angular:build:production",
"configDir": ".storybook",
"outputDir": "storybook-static",
"compodoc": false
}
}
}
}
},
"cli": {
"analytics": false
}
}
@@ -0,0 +1,44 @@
import { Component, Input } from "@angular/core";
@Component({
selector: "custom-disclaimer",
standalone: true,
template: `
<div
[class]="inputClass"
style="
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
margin: 10px;
font-family: &quot;Segoe UI&quot;, Tahoma, Geneva, Verdana, sans-serif;
"
>
<h3 style="margin: 0 0 10px 0; font-size: 20px">
✨ Custom Disclaimer Component ✨
</h3>
<p style="margin: 0; font-size: 14px; opacity: 0.9">
{{
text || "This is a custom disclaimer demonstrating component overrides!"
}}
</p>
<div
style="
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid rgba(255, 255, 255, 0.3);
font-size: 12px;
opacity: 0.7;
"
>
🎨 Styled with custom gradients and animations
</div>
</div>
`,
})
export class CustomDisclaimerComponent {
@Input() text?: string;
@Input() inputClass?: string;
}
@@ -0,0 +1,68 @@
import { Component, Input } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormsModule } from "@angular/forms";
import type { ChatState } from "@copilotkit/angular";
@Component({
selector: "custom-input",
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div
[class]="inputClass"
style="
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
padding: 20px;
border-radius: 15px;
margin: 10px;
"
>
<input
type="text"
[(ngModel)]="inputValue"
placeholder="💬 Ask me anything..."
style="
width: 100%;
padding: 15px;
border: 2px solid white;
border-radius: 10px;
font-size: 16px;
background: rgba(255, 255, 255, 0.9);
color: #333;
outline: none;
"
(keyup.enter)="handleSend()"
/>
<button
style="
margin-top: 10px;
padding: 10px 20px;
background: white;
color: #f5576c;
border: none;
border-radius: 5px;
font-weight: bold;
cursor: pointer;
"
(click)="handleSend()"
>
Send Message ✨
</button>
</div>
`,
})
export class CustomInputComponent {
@Input() inputClass?: string;
inputValue = "";
constructor(private chat: ChatState) {}
handleSend() {
const value = this.inputValue.trim();
if (value) {
this.chat.submitInput(value);
this.inputValue = "";
}
}
}
@@ -0,0 +1,62 @@
import { Component, Input, Output, EventEmitter } from "@angular/core";
import { CommonModule } from "@angular/common";
@Component({
selector: "custom-scroll-button",
standalone: true,
imports: [CommonModule],
template: `
<button
type="button"
(click)="handleClick()"
[class]="inputClass"
[class.hover]="isHovered"
(mouseenter)="isHovered = true"
(mouseleave)="isHovered = false"
style="
position: fixed;
bottom: 100px;
right: 20px;
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: 3px solid white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
cursor: pointer;
pointer-events: auto;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s;
z-index: 1000;
"
[style.transform]="isHovered ? 'scale(1.1)' : 'scale(1)'"
>
<span style="color: white; font-size: 24px; pointer-events: none">⬇️</span>
</button>
`,
styles: [
`
button.hover {
transform: scale(1.1);
}
`,
],
})
export class CustomScrollButtonComponent {
@Input() onClick?: () => void;
@Input() inputClass?: string;
@Output() clicked = new EventEmitter<void>();
isHovered = false;
handleClick() {
// Emit the clicked event for the slot system to handle
this.clicked.emit();
// Also call onClick if provided for backward compatibility
if (this.onClick) {
this.onClick();
}
}
}
@@ -0,0 +1,27 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { CommonModule } from "@angular/common";
@Component({
selector: "custom-send-button",
standalone: true,
imports: [CommonModule],
template: `
<button
[disabled]="disabled"
(click)="handleClick()"
class="cpk:rounded-full cpk:w-10 cpk:h-10 cpk:bg-blue-500 cpk:text-white cpk:hover:bg-blue-600 cpk:transition-colors cpk:mr-2 cpk:disabled:opacity-50 cpk:disabled:cursor-not-allowed"
>
✈️
</button>
`,
})
export class CustomSendButtonComponent {
@Input() disabled = false;
@Output() clicked = new EventEmitter<void>();
handleClick(): void {
if (!this.disabled) {
this.clicked.emit();
}
}
}
@@ -0,0 +1,46 @@
{
"name": "@copilotkit-storybook/angular",
"version": "0.0.6-next.1",
"private": true,
"scripts": {
"dev": "pnpm --filter @copilotkit/angular run build:css && sleep 1 && ng run storybook-angular:storybook",
"build": "ng run storybook-angular:build-storybook",
"storybook:dev": "pnpm --filter @copilotkit/angular run build:css && sleep 1 && ng run storybook-angular:storybook",
"storybook:build": "ng run storybook-angular:build-storybook"
},
"dependencies": {
"@ag-ui/client": "0.0.51",
"@angular/animations": "^21.2.15",
"@angular/common": "^21.2.15",
"@angular/compiler": "^21.2.15",
"@angular/core": "^21.2.15",
"@angular/forms": "^21.2.15",
"@angular/platform-browser": "^21.2.15",
"@angular/platform-browser-dynamic": "^21.2.15",
"@copilotkit/a2ui-renderer": "workspace:*",
"@copilotkit/core": "workspace:^",
"@copilotkit/shared": "workspace:*",
"rxjs": "^7.8.1",
"tslib": "^2.8.1",
"zod": "^3.25.75"
},
"devDependencies": {
"@angular-devkit/build-angular": "^21.2.13",
"@angular/cli": "^21.2.13",
"@angular/compiler-cli": "^21.2.15",
"@copilotkit/typescript-config": "workspace:*",
"@copilotkit/angular": "workspace:*",
"@storybook/addon-themes": "10.3.6",
"@storybook/angular": "10.3.6",
"@tailwindcss/postcss": "^4.1.12",
"@types/node": "^22",
"autoprefixer": "^10.4.21",
"css-loader": "^7.1.2",
"postcss": "^8.4.31",
"postcss-loader": "^8.1.1",
"storybook": "10.3.6",
"style-loader": "^4.0.0",
"tailwindcss": "^4.1.11",
"typescript": "5.9.3"
}
}
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};
@@ -0,0 +1,733 @@
import type { Meta, StoryObj } from "@storybook/angular";
import { moduleMetadata } from "@storybook/angular";
import { fn } from "storybook/test";
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import {
CopilotChatAssistantMessage,
provideCopilotChatLabels,
} from "@copilotkit/angular";
import type { AssistantMessage } from "@ag-ui/client";
// Simple default message
const simpleMessage: AssistantMessage = {
id: "simple-message",
content: "Hello! How can I help you today?",
role: "assistant",
};
// Comprehensive markdown test content
const markdownTestMessage: AssistantMessage = {
id: "test-message",
content: `# Markdown Test Message
This message tests various markdown features including **bold**, *italic*, and \`inline code\`.
## Code Blocks with Copy Buttons
Here are some code examples to test the copy functionality:
### JavaScript/TypeScript
\`\`\`javascript
function greet(name) {
console.log(\`Hello, \${name}!\`);
return \`Welcome, \${name}\`;
}
// Usage
greet("World");
\`\`\`
### Python
\`\`\`python
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Generate sequence
for i in range(10):
print(fibonacci(i))
\`\`\`
### SQL
\`\`\`sql
SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2023-01-01'
GROUP BY u.id, u.name
ORDER BY order_count DESC;
\`\`\`
### JSON
\`\`\`json
{
"name": "CopilotKit",
"version": "2.0.0",
"dependencies": {
"react": "^18.0.0",
"react-markdown": "^10.1.0"
}
}
\`\`\`
### Shell/Bash
\`\`\`bash
#!/bin/bash
echo "Building project..."
npm install
npm run build
echo "Build complete!"
\`\`\`
## Inline Code Testing
Here's some \`inline code\` that should not have a copy button. You can also have \`npm install\` or \`const variable = "value"\` inline.
## Links and Images
- [External link](https://example.com)
- [Internal link](#section)
- ![Alt text](https://picsum.photos/150/100)
## Blockquotes
> This is a blockquote
>
> It can span multiple lines
>
> > And can be nested
## Tables
| Feature | Supported | Notes |
|---------|-----------|-------|
| Headers | ✅ | All levels |
| Lists | ✅ | Nested support |
| Code | ✅ | Syntax highlighting |
| Links | ✅ | External & internal |
| Copy Button | ✅ | On code blocks only |
## Horizontal Rule
---
## Miscellaneous
- [ ] Unchecked task
- [x] Checked task
- Emoji support: 🚀 ✨ 💡 🎉
### Math (if supported)
Inline math: $E = mc^2$
Block math:
$$
\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}
$$
---
*End of markdown test content*`,
role: "assistant",
};
// Message with code blocks and inline literals
const codeBlocksTestMessage: AssistantMessage = {
id: "msg-code-blocks-test",
content:
"# Code Blocks and Inline Literals Test\n\n" +
"This message demonstrates code syntax highlighting with various languages and inline code usage. " +
"When you want to reference a variable like `userName` or a function like `getData()`, you can use inline code blocks.\n\n" +
"## JavaScript Example\n" +
"Here's how you might handle user authentication in JavaScript, so that you can verify credentials:\n\n" +
"```javascript\n" +
"const authenticateUser = async (email, password) => {\n" +
" try {\n" +
" const response = await fetch('/api/auth', {\n" +
" method: 'POST',\n" +
" headers: { 'Content-Type': 'application/json' },\n" +
" body: JSON.stringify({ email, password })\n" +
" });\n" +
" \n" +
" if (!response.ok) {\n" +
" throw new Error('Authentication failed');\n" +
" }\n" +
" \n" +
" return await response.json();\n" +
" } catch (error) {\n" +
" console.error('Error:', error);\n" +
" return null;\n" +
" }\n" +
"};\n" +
"```\n\n" +
"## Python Data Processing\n" +
"Python is great for data manipulation, so here's an example with pandas:\n\n" +
"```python\n" +
"import pandas as pd\n" +
"import numpy as np\n\n" +
"def process_user_data(csv_file):\n" +
" # Read the data\n" +
" df = pd.read_csv(csv_file)\n" +
" \n" +
" # Clean the data\n" +
" df['age'] = pd.to_numeric(df['age'], errors='coerce')\n" +
" df = df.dropna(subset=['age'])\n" +
" \n" +
" # Calculate statistics\n" +
" stats = {\n" +
" 'mean_age': df['age'].mean(),\n" +
" 'median_age': df['age'].median(),\n" +
" 'total_users': len(df)\n" +
" }\n" +
" \n" +
" return stats\n\n" +
"# Usage\n" +
"result = process_user_data('users.csv')\n" +
"print(f\"Average age: {result['mean_age']:.1f}\")\n" +
"```\n\n" +
"## SQL Database Query\n" +
"When working with databases, you might use SQL queries like `SELECT * FROM users` or more complex ones:\n\n" +
"```sql\n" +
"SELECT \n" +
" u.id,\n" +
" u.username,\n" +
" u.email,\n" +
" COUNT(p.id) as post_count,\n" +
" MAX(p.created_at) as last_post_date\n" +
"FROM users u\n" +
"LEFT JOIN posts p ON u.id = p.user_id\n" +
"WHERE u.active = true\n" +
" AND u.created_at >= '2023-01-01'\n" +
"GROUP BY u.id, u.username, u.email\n" +
"HAVING COUNT(p.id) > 0\n" +
"ORDER BY post_count DESC, last_post_date DESC\n" +
"LIMIT 50;\n" +
"```\n\n" +
"## Shell/Bash Commands\n" +
"For deployment scripts, you might use bash commands. The `chmod` command changes permissions, so you can make files executable:\n\n" +
"```bash\n" +
"#!/bin/bash\n\n" +
"# Deploy script\n" +
'APP_NAME="my-app"\n' +
"VERSION=$(git describe --tags --abbrev=0)\n\n" +
'echo "Deploying $APP_NAME version $VERSION"\n\n' +
"# Build the application\n" +
"npm install\n" +
"npm run build\n\n" +
"# Create deployment package\n" +
'tar -czf "${APP_NAME}-${VERSION}.tar.gz" dist/\n\n' +
"# Upload to server\n" +
'scp "${APP_NAME}-${VERSION}.tar.gz" user@server:/opt/deployments/\n\n' +
"# Extract and restart\n" +
"ssh user@server << EOF\n" +
" cd /opt/deployments\n" +
" tar -xzf ${APP_NAME}-${VERSION}.tar.gz\n" +
" sudo systemctl restart ${APP_NAME}\n" +
' echo "Deployment complete"\n' +
"EOF\n" +
"```\n\n" +
"## TypeScript Interface\n" +
"TypeScript helps with type safety, so you can define interfaces like:\n\n" +
"```typescript\n" +
"interface UserProfile {\n" +
" id: string;\n" +
" username: string;\n" +
" email: string;\n" +
" preferences: {\n" +
" theme: 'light' | 'dark';\n" +
" language: string;\n" +
" notifications: boolean;\n" +
" };\n" +
" lastLoginAt: Date | null;\n" +
"}\n\n" +
"class UserService {\n" +
" private users: Map<string, UserProfile> = new Map();\n\n" +
" async createUser(data: Omit<UserProfile, 'id' | 'lastLoginAt'>): Promise<UserProfile> {\n" +
" const user: UserProfile = {\n" +
" ...data,\n" +
" id: crypto.randomUUID(),\n" +
" lastLoginAt: null,\n" +
" };\n" +
" \n" +
" this.users.set(user.id, user);\n" +
" return user;\n" +
" }\n" +
" \n" +
" updateLastLogin(userId: string): void {\n" +
" const user = this.users.get(userId);\n" +
" if (user) {\n" +
" user.lastLoginAt = new Date();\n" +
" }\n" +
" }\n" +
"}\n" +
"```\n\n" +
"## CSS Styling\n" +
"For styling components, you can use CSS classes like `.container` or `#header`:\n\n" +
"```css\n" +
".user-profile-card {\n" +
" background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n" +
" border-radius: 12px;\n" +
" padding: 24px;\n" +
" box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);\n" +
" transition: transform 0.3s ease;\n" +
"}\n\n" +
".user-profile-card:hover {\n" +
" transform: translateY(-4px);\n" +
"}\n\n" +
".user-avatar {\n" +
" width: 80px;\n" +
" height: 80px;\n" +
" border-radius: 50%;\n" +
" border: 3px solid rgba(255, 255, 255, 0.2);\n" +
" object-fit: cover;\n" +
"}\n\n" +
"@media (max-width: 768px) {\n" +
" .user-profile-card {\n" +
" padding: 16px;\n" +
" margin: 8px;\n" +
" }\n" +
"}\n" +
"```\n\n" +
"All these examples show how inline code like `const`, `function`, and `class` can be mixed with code blocks to create comprehensive documentation.",
role: "assistant",
};
const meta: Meta<CopilotChatAssistantMessage> = {
title: "UI/CopilotChatAssistantMessage",
component: CopilotChatAssistantMessage,
parameters: {
docs: {
source: {
language: "html",
type: "dynamic",
},
},
},
decorators: [
moduleMetadata({
imports: [CommonModule, CopilotChatAssistantMessage],
providers: [provideCopilotChatLabels({})],
}),
],
render: (args) => ({
props: {
...args,
},
template: `
<div style="display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 16px;">
<div style="width: 100%; max-width: 640px;">
<copilot-chat-assistant-message
[message]="message"
[toolbarVisible]="toolbarVisible"
(thumbsUp)="thumbsUp($event)"
(thumbsDown)="thumbsDown($event)"
(readAloud)="readAloud($event)"
(regenerate)="regenerate($event)">
</copilot-chat-assistant-message>
</div>
</div>
`,
}),
args: {
message: simpleMessage,
toolbarVisible: true,
thumbsUp: fn(),
thumbsDown: fn(),
readAloud: fn(),
regenerate: fn(),
},
argTypes: {
message: {
description: "The assistant message to display",
control: { type: "object" },
},
toolbarVisible: {
description: "Whether to show the toolbar",
control: { type: "boolean" },
},
},
};
export default meta;
type Story = StoryObj<CopilotChatAssistantMessage>;
export const Default: Story = {
args: {
message: simpleMessage,
toolbarVisible: true,
},
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatAssistantMessage } from '@copilotkit/angular';
import { AssistantMessage } from '@ag-ui/client';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatAssistantMessage],
template: \`
<copilot-chat-assistant-message
[message]="message"
[toolbarVisible]="true"
(thumbsUp)="onThumbsUp($event)"
(thumbsDown)="onThumbsDown($event)"
(readAloud)="onReadAloud($event)"
(regenerate)="onRegenerate($event)">
</copilot-chat-assistant-message>
\`
})
export class ChatComponent {
message: AssistantMessage = {
id: 'simple-message',
content: 'Hello! How can I help you today?',
role: 'assistant',
};
onThumbsUp(event: any): void {
console.log('Thumbs up clicked!');
}
onThumbsDown(event: any): void {
console.log('Thumbs down clicked!');
}
onReadAloud(event: any): void {
console.log('Read aloud clicked!');
}
onRegenerate(event: any): void {
console.log('Regenerate clicked!');
}
}`,
language: "typescript",
},
},
},
};
export const TestAllMarkdownFeatures: Story = {
args: {
message: markdownTestMessage,
toolbarVisible: true,
},
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatAssistantMessage } from '@copilotkit/angular';
import { AssistantMessage } from '@ag-ui/client';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatAssistantMessage],
template: \`
<copilot-chat-assistant-message
[message]="message"
[toolbarVisible]="true">
</copilot-chat-assistant-message>
\`
})
export class ChatComponent {
message: AssistantMessage = {
id: 'test-message',
content: \`# Markdown Test Message
This message tests various markdown features including **bold**, *italic*, and \\\`inline code\\\`.
## Code Blocks
\\\`\\\`\\\`javascript
function greet(name) {
console.log(\\\`Hello, \\${name}!\\\`);
return \\\`Welcome, \\${name}\\\`;
}
\\\`\\\`\\\`
## Links and Tables
- [External link](https://example.com)
| Feature | Supported | Notes |
|---------|-----------|-------|
| Headers | ✅ | All levels |
| Lists | ✅ | Nested support |
| Code | ✅ | Syntax highlighting |\`,
role: 'assistant',
};
}`,
language: "typescript",
},
},
},
};
export const WithToolbarButtons: Story = {
args: {
message: simpleMessage,
toolbarVisible: true,
},
render: (args) => ({
props: {
...args,
onThumbsUp: (event: any) => {
alert("Thumbs up clicked!");
},
onThumbsDown: (event: any) => {
alert("Thumbs down clicked!");
},
onReadAloud: (event: any) => {
alert("Read aloud clicked!");
},
onRegenerate: (event: any) => {
alert("Regenerate clicked!");
},
},
template: `
<div style="display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 16px;">
<div style="width: 100%; max-width: 640px;">
<copilot-chat-assistant-message
[message]="message"
[toolbarVisible]="toolbarVisible"
(thumbsUp)="onThumbsUp($event)"
(thumbsDown)="onThumbsDown($event)"
(readAloud)="onReadAloud($event)"
(regenerate)="onRegenerate($event)">
</copilot-chat-assistant-message>
</div>
</div>
`,
}),
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatAssistantMessage } from '@copilotkit/angular';
import { AssistantMessage } from '@ag-ui/client';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatAssistantMessage],
template: \`
<copilot-chat-assistant-message
[message]="message"
[toolbarVisible]="true"
(thumbsUp)="onThumbsUp($event)"
(thumbsDown)="onThumbsDown($event)"
(readAloud)="onReadAloud($event)"
(regenerate)="onRegenerate($event)">
</copilot-chat-assistant-message>
\`
})
export class ChatComponent {
message: AssistantMessage = {
id: 'simple-message',
content: 'Hello! How can I help you today?',
role: 'assistant',
};
onThumbsUp(event: any): void {
alert('Thumbs up clicked!');
}
onThumbsDown(event: any): void {
alert('Thumbs down clicked!');
}
onReadAloud(event: any): void {
alert('Read aloud clicked!');
}
onRegenerate(event: any): void {
alert('Regenerate clicked!');
}
}`,
language: "typescript",
},
},
},
};
export const WithAdditionalToolbarItems: Story = {
render: (args) => ({
props: {
message: simpleMessage,
onThumbsUp: (event: any) => console.log("Thumbs up clicked!"),
onThumbsDown: (event: any) => console.log("Thumbs down clicked!"),
onReadAloud: (event: any) => console.log("Read aloud clicked!"),
onRegenerate: (event: any) => console.log("Regenerate clicked!"),
onCustom1: () => alert("Custom button 1 clicked!"),
onCustom2: () => alert("Custom button 2 clicked!"),
},
template: `
<div style="display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 16px;">
<div style="width: 100%; max-width: 640px;">
<copilot-chat-assistant-message
[message]="message"
[toolbarVisible]="true"
(thumbsUp)="onThumbsUp($event)"
(thumbsDown)="onThumbsDown($event)"
(readAloud)="onReadAloud($event)"
(regenerate)="onRegenerate($event)"
[additionalToolbarItems]="additionalItems">
<ng-template #additionalItems>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:bg-gray-100 cpk:hover:bg-gray-200 cpk:flex cpk:items-center cpk:justify-center"
(click)="onCustom1()"
title="Custom Action 1">
📌
</button>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:bg-gray-100 cpk:hover:bg-gray-200 cpk:flex cpk:items-center cpk:justify-center"
(click)="onCustom2()"
title="Custom Action 2">
❤️
</button>
</ng-template>
</copilot-chat-assistant-message>
</div>
</div>
`,
}),
parameters: {
docs: {
source: {
type: "code",
code: `import { Component, ViewChild, TemplateRef } from '@angular/core';
import { CopilotChatAssistantMessage } from '@copilotkit/angular';
import { AssistantMessage } from '@ag-ui/client';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatAssistantMessage],
template: \`
<ng-template #additionalItems>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:bg-gray-100 cpk:hover:bg-gray-200 cpk:flex cpk:items-center cpk:justify-center"
(click)="onCustom1()"
title="Custom Action 1">
📌
</button>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:bg-gray-100 cpk:hover:bg-gray-200 cpk:flex cpk:items-center cpk:justify-center"
(click)="onCustom2()"
title="Custom Action 2">
❤️
</button>
</ng-template>
<copilot-chat-assistant-message
[message]="message"
[toolbarVisible]="true"
[additionalToolbarItems]="additionalItems"
(thumbsUp)="onThumbsUp($event)"
(thumbsDown)="onThumbsDown($event)"
(readAloud)="onReadAloud($event)"
(regenerate)="onRegenerate($event)">
</copilot-chat-assistant-message>
\`
})
export class ChatComponent {
@ViewChild('additionalItems') additionalItems!: TemplateRef<any>;
message: AssistantMessage = {
id: 'simple-message',
content: 'Hello! How can I help you today?',
role: 'assistant',
};
onThumbsUp(event: any): void {
console.log('Thumbs up clicked!');
}
onThumbsDown(event: any): void {
console.log('Thumbs down clicked!');
}
onReadAloud(event: any): void {
console.log('Read aloud clicked!');
}
onRegenerate(event: any): void {
console.log('Regenerate clicked!');
}
onCustom1(): void {
alert('Custom button 1 clicked!');
}
onCustom2(): void {
alert('Custom button 2 clicked!');
}
}`,
language: "typescript",
},
},
},
};
export const CodeBlocksWithLanguages: Story = {
args: {
message: codeBlocksTestMessage,
toolbarVisible: true,
},
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatAssistantMessage } from '@copilotkit/angular';
import { AssistantMessage } from '@ag-ui/client';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatAssistantMessage],
template: \`
<copilot-chat-assistant-message
[message]="message"
[toolbarVisible]="true">
</copilot-chat-assistant-message>
\`
})
export class ChatComponent {
message: AssistantMessage = {
id: 'msg-code-blocks-test',
content: \`# Code Blocks Test
## JavaScript Example
\\\`\\\`\\\`javascript
const authenticateUser = async (email, password) => {
try {
const response = await fetch('/api/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
return await response.json();
} catch (error) {
console.error('Error:', error);
return null;
}
};
\\\`\\\`\\\`
## Python Example
\\\`\\\`\\\`python
import pandas as pd
def process_user_data(csv_file):
df = pd.read_csv(csv_file)
df['age'] = pd.to_numeric(df['age'], errors='coerce')
return df
\\\`\\\`\\\`\`,
role: 'assistant',
};
}`,
language: "typescript",
},
},
},
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,585 @@
import type { Meta, StoryObj } from "@storybook/angular";
import { moduleMetadata } from "@storybook/angular";
import { CommonModule } from "@angular/common";
import {
CopilotChatUserMessage,
provideCopilotChatLabels,
} from "@copilotkit/angular";
import type { UserMessage } from "@ag-ui/client";
// Simple default message
const simpleMessage: UserMessage = {
id: "simple-user-message",
content: "Hello! Can you help me build an Angular component?",
role: "user",
};
// Longer user message
const longMessage: UserMessage = {
id: "long-user-message",
content: `I need help with creating a complex Angular component that handles user authentication. Here are my requirements:
1. The component should have login and signup forms
2. It needs to integrate with Firebase Auth
3. Should handle form validation
4. Must be responsive and work on mobile
5. Include forgot password functionality
6. Support social login (Google, GitHub)
Can you help me implement this step by step? I'm particularly struggling with the form validation and state management parts.`,
role: "user",
};
// Code-related user message
const codeMessage: UserMessage = {
id: "code-user-message",
content: `I'm getting this error in my Angular app:
TypeError: Cannot read property 'map' of undefined
The error happens in this component:
@Component({
selector: 'app-user-list',
template: \`
<div *ngFor="let user of users">
{{ user.name }}
</div>
\`
})
export class UserListComponent {
@Input() users: User[];
}
How can I fix this?`,
role: "user",
};
// Short question
const shortMessage: UserMessage = {
id: "short-user-message",
content: "What's the difference between signals and observables in Angular?",
role: "user",
};
const meta: Meta<CopilotChatUserMessage> = {
title: "UI/CopilotChatUserMessage",
component: CopilotChatUserMessage,
decorators: [
moduleMetadata({
imports: [CommonModule, CopilotChatUserMessage],
providers: [provideCopilotChatLabels({})],
}),
],
render: (args) => ({
props: {
...args,
},
template: `
<div style="display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 16px;">
<div style="width: 100%; max-width: 640px;">
<copilot-chat-user-message
[message]="message"
[branchIndex]="branchIndex"
[numberOfBranches]="numberOfBranches"
[inputClass]="inputClass"
[additionalToolbarItems]="additionalToolbarItems"
(editMessage)="editMessage && editMessage($event)"
(switchToBranch)="switchToBranch && switchToBranch($event)"
></copilot-chat-user-message>
</div>
</div>
`,
}),
args: {
message: simpleMessage,
editMessage: () => console.log("Edit clicked!"),
},
};
export default meta;
type Story = StoryObj<CopilotChatUserMessage>;
export const Default: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatUserMessage, UserMessage } from '@copilotkit/angular';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatUserMessage],
template: \`
<copilot-chat-user-message
[message]="message"
(editMessage)="onEditMessage($event)">
</copilot-chat-user-message>
\`
})
export class ChatComponent {
message: UserMessage = {
id: 'user-1',
content: 'Hello! Can you help me build an Angular component?',
role: 'user',
timestamp: new Date(),
};
onEditMessage(event: any): void {
console.log('Edit message:', event);
}
}`,
language: "typescript",
},
},
},
};
export const LongMessage: Story = {
args: {
message: longMessage,
},
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatUserMessage, UserMessage } from '@copilotkit/angular';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatUserMessage],
template: \`
<copilot-chat-user-message
[message]="message"
(editMessage)="onEditMessage($event)">
</copilot-chat-user-message>
\`
})
export class ChatComponent {
message: UserMessage = {
id: 'long-user-message',
content: \`I need help with creating a complex Angular component that handles user authentication. Here are my requirements:
1. The component should have login and signup forms
2. It needs to integrate with Firebase Auth
3. Should handle form validation
4. Must be responsive and work on mobile
5. Include forgot password functionality
6. Support social login (Google, GitHub)
Can you help me implement this step by step? I'm particularly struggling with the form validation and state management parts.\`,
role: 'user',
timestamp: new Date(),
};
onEditMessage(event: any): void {
console.log('Edit message:', event);
}
}`,
language: "typescript",
},
},
},
};
export const WithEditButton: Story = {
args: {
message: simpleMessage,
editMessage: () => alert("Edit message clicked!"),
},
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatUserMessage, UserMessage } from '@copilotkit/angular';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatUserMessage],
template: \`
<copilot-chat-user-message
[message]="message"
(editMessage)="onEditMessage($event)">
</copilot-chat-user-message>
\`
})
export class ChatComponent {
message: UserMessage = {
id: 'simple-user-message',
content: 'Hello! Can you help me build an Angular component?',
role: 'user',
timestamp: new Date(),
};
onEditMessage(event: any): void {
alert('Edit message clicked!');
console.log('Edit message:', event);
}
}`,
language: "typescript",
},
},
},
};
export const WithoutEditButton: Story = {
args: {
message: simpleMessage,
editMessage: undefined,
},
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatUserMessage, UserMessage } from '@copilotkit/angular';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatUserMessage],
template: \`
<copilot-chat-user-message
[message]="message">
</copilot-chat-user-message>
\`
})
export class ChatComponent {
message: UserMessage = {
id: 'simple-user-message',
content: 'Hello! Can you help me build an Angular component?',
role: 'user',
timestamp: new Date(),
};
// No edit handler - edit button won't appear
}`,
language: "typescript",
},
},
},
};
export const CodeRelatedMessage: Story = {
args: {
message: codeMessage,
editMessage: () => alert("Edit code message clicked!"),
},
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatUserMessage, UserMessage } from '@copilotkit/angular';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatUserMessage],
template: \`
<copilot-chat-user-message
[message]="message"
(editMessage)="onEditMessage($event)">
</copilot-chat-user-message>
\`
})
export class ChatComponent {
message: UserMessage = {
id: 'code-user-message',
content: \`I'm getting this error in my Angular app:
TypeError: Cannot read property 'map' of undefined
The error happens in this component:
@Component({
selector: 'app-user-list',
template: \\\`
<div *ngFor="let user of users">
{{ user.name }}
</div>
\\\`
})
export class UserListComponent {
@Input() users: User[];
}
How can I fix this?\`,
role: 'user',
timestamp: new Date(),
};
onEditMessage(event: any): void {
alert('Edit code message clicked!');
}
}`,
language: "typescript",
},
},
},
};
export const ShortQuestion: Story = {
args: {
message: shortMessage,
editMessage: () => console.log("Edit short message clicked!"),
},
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CopilotChatUserMessage, UserMessage } from '@copilotkit/angular';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatUserMessage],
template: \`
<copilot-chat-user-message
[message]="message"
(editMessage)="onEditMessage($event)">
</copilot-chat-user-message>
\`
})
export class ChatComponent {
message: UserMessage = {
id: 'short-user-message',
content: "What's the difference between signals and observables in Angular?",
role: 'user',
timestamp: new Date(),
};
onEditMessage(event: any): void {
console.log('Edit short message clicked!');
}
}`,
language: "typescript",
},
},
},
};
export const WithAdditionalToolbarItems: Story = {
render: () => ({
props: {
message: simpleMessage,
editMessage: () => console.log("Edit clicked!"),
},
template: `
<ng-template #additionalItems>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:bg-gray-100 cpk:hover:bg-gray-200 cpk:flex cpk:items-center cpk:justify-center"
(click)="alert('Custom button 1 clicked!')"
title="Custom Action 1">
📎
</button>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:bg-gray-100 cpk:hover:bg-gray-200 cpk:flex cpk:items-center cpk:justify-center"
(click)="alert('Custom button 2 clicked!')"
title="Custom Action 2">
🔄
</button>
</ng-template>
<div style="display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 16px;">
<div style="width: 100%; max-width: 640px;">
<copilot-chat-user-message
[message]="message"
[additionalToolbarItems]="additionalItems"
(editMessage)="editMessage($event)"
></copilot-chat-user-message>
</div>
</div>
`,
}),
parameters: {
docs: {
source: {
type: "code",
code: `import { Component, ViewChild, TemplateRef } from '@angular/core';
import { CopilotChatUserMessage, UserMessage } from '@copilotkit/angular';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CopilotChatUserMessage],
template: \`
<ng-template #additionalItems>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:bg-gray-100 cpk:hover:bg-gray-200 cpk:flex cpk:items-center cpk:justify-center"
(click)="onCustomAction1()"
title="Custom Action 1">
📎
</button>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:bg-gray-100 cpk:hover:bg-gray-200 cpk:flex cpk:items-center cpk:justify-center"
(click)="onCustomAction2()"
title="Custom Action 2">
🔄
</button>
</ng-template>
<copilot-chat-user-message
[message]="message"
[additionalToolbarItems]="additionalItems"
(editMessage)="onEditMessage($event)">
</copilot-chat-user-message>
\`
})
export class ChatComponent {
@ViewChild('additionalItems') additionalItems!: TemplateRef<any>;
message: UserMessage = {
id: 'simple-user-message',
content: 'Hello! Can you help me build an Angular component?',
role: 'user',
timestamp: new Date(),
};
onEditMessage(event: any): void {
console.log('Edit clicked!');
}
onCustomAction1(): void {
alert('Custom button 1 clicked!');
}
onCustomAction2(): void {
alert('Custom button 2 clicked!');
}
}`,
language: "typescript",
},
},
},
};
export const CustomAppearance: Story = {
args: {
message: simpleMessage,
editMessage: () => console.log("Edit clicked!"),
inputClass:
"cpk:bg-blue-50 cpk:border cpk:border-blue-200 cpk:rounded-lg cpk:p-4",
},
render: () => ({
props: {
message: simpleMessage,
editMessage: () => console.log("Edit clicked!"),
},
template: `
<ng-template #messageRenderer let-content="content">
<div class="cpk:prose cpk:dark:prose-invert cpk:bg-muted cpk:relative cpk:max-w-[80%] cpk:rounded-[18px] cpk:px-4 cpk:py-1.5 cpk:data-[multiline]:py-3 cpk:inline-block cpk:whitespace-pre-wrap cpk:text-blue-900 cpk:font-medium">
{{ content }}
</div>
</ng-template>
<ng-template #toolbar>
<div class="cpk:w-full cpk:bg-transparent cpk:flex cpk:items-center cpk:justify-end cpk:-mr-[5px] cpk:mt-[8px] cpk:invisible cpk:group-hover:visible">
<div class="cpk:flex cpk:items-center cpk:gap-1 cpk:justify-end">
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:text-blue-600 cpk:hover:bg-blue-100 cpk:flex cpk:items-center cpk:justify-center"
(click)="handleCopy()">
📋
</button>
<button
class="cpk:h-8 cpk:w-8 cpk:p-0 cpk:rounded-md cpk:text-blue-600 cpk:hover:bg-blue-100 cpk:flex cpk:items-center cpk:justify-center"
(click)="editMessage({message: message})">
✏️
</button>
</div>
</div>
</ng-template>
<div style="display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 16px;">
<div style="width: 100%; max-width: 640px;">
<copilot-chat-user-message
[message]="message"
inputClass="cpk:bg-blue-50 cpk:border-blue-200 cpk:rounded-lg cpk:p-4"
(editMessage)="editMessage($event)">
<ng-template #messageRenderer let-content="content">
<div class="cpk:prose cpk:dark:prose-invert cpk:bg-muted cpk:relative cpk:max-w-[80%] cpk:rounded-[18px] cpk:px-4 cpk:py-1.5 cpk:data-[multiline]:py-3 cpk:inline-block cpk:whitespace-pre-wrap cpk:text-blue-900 cpk:font-medium">
{{ content }}
</div>
</ng-template>
</copilot-chat-user-message>
</div>
</div>
`,
}),
};
export const CustomComponents: Story = {
args: {
message: simpleMessage,
editMessage: () => console.log("Edit clicked!"),
inputClass:
"cpk:bg-gradient-to-r cpk:from-purple-100 cpk:to-pink-100 cpk:rounded-xl cpk:p-4 cpk:shadow-sm",
},
render: () => ({
props: {
message: simpleMessage,
editMessage: () => console.log("Edit clicked!"),
},
template: `
<ng-template #messageRenderer let-content="content">
<div class="cpk:font-mono cpk:text-purple-800 cpk:bg-white/50 cpk:rounded-lg cpk:px-3 cpk:py-2 cpk:inline-block">
💬 {{ content }}
</div>
</ng-template>
<div style="display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 16px;">
<div style="width: 100%; max-width: 640px;">
<copilot-chat-user-message
[message]="message"
inputClass="cpk:bg-gradient-to-r cpk:from-purple-100 cpk:to-pink-100 cpk:rounded-xl cpk:p-4 cpk:shadow-sm"
(editMessage)="editMessage($event)">
<ng-template #messageRenderer let-content="content">
<div class="cpk:font-mono cpk:text-purple-800 cpk:bg-white/50 cpk:rounded-lg cpk:px-3 cpk:py-2 cpk:inline-block">
💬 {{ content }}
</div>
</ng-template>
</copilot-chat-user-message>
</div>
</div>
`,
}),
};
export const WithBranchNavigation: Story = {
args: {
message: {
id: "branch-message",
content:
"This message has multiple branches. You can navigate between them using the branch controls.",
role: "user",
},
editMessage: () => console.log("Edit clicked!"),
branchIndex: 2,
numberOfBranches: 3,
switchToBranch: ({ branchIndex }) =>
console.log(`Switching to branch ${branchIndex + 1}`),
},
};
export const WithManyBranches: Story = {
args: {
message: {
id: "many-branches-message",
content:
"This is branch 5 of 10. Use the navigation arrows to explore different variations of this message.",
role: "user",
},
editMessage: () => console.log("Edit clicked!"),
branchIndex: 4,
numberOfBranches: 10,
switchToBranch: ({ branchIndex }) =>
alert(`Would switch to branch ${branchIndex + 1} of 10`),
},
};
@@ -0,0 +1,216 @@
import type { Meta, StoryObj } from "@storybook/angular";
import { moduleMetadata } from "@storybook/angular";
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
provideCopilotChatLabels,
provideCopilotKit,
} from "@copilotkit/angular";
import type { Message } from "@ag-ui/client";
const meta: Meta<CopilotChatView> = {
title: "UI/CopilotChatView/Custom Actions",
component: CopilotChatView,
decorators: [
moduleMetadata({
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: "Type a message...",
chatDisclaimerText:
"AI can make mistakes. Please verify important information.",
}),
],
}),
],
parameters: {
layout: "fullscreen",
},
};
export default meta;
type Story = StoryObj<CopilotChatView>;
export const ThumbsUpDown: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
import { Message } from '@ag-ui/client';
// Custom disclaimer component
&#64;Component({
selector: 'custom-disclaimer',
standalone: true,
template: \`
<div
[class]="inputClass"
style="
text-align: center;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-size: 14px;
margin: 8px 16px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
">
🎨 This chat interface is fully customizable!
</div>
\`
})
class CustomDisclaimerComponent {
&#64;Input() text?: string;
&#64;Input() inputClass?: string;
}
&#64;Component({
selector: 'app-chat-actions',
standalone: true,
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
CustomDisclaimerComponent
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: "Type a message...",
chatDisclaimerText:
"AI can make mistakes. Please verify important information.",
})
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[disclaimerComponent]="customDisclaimerComponent"
(assistantMessageThumbsUp)="onThumbsUp($event)"
(assistantMessageThumbsDown)="onThumbsDown($event)">
</copilot-chat-view>
</div>
\`
})
export class ChatActionsComponent {
messages: Message[] = [
{
id: 'user-1',
content: 'Hello! Can you help me with TypeScript?',
role: 'user'
},
{
id: 'assistant-1',
content: 'Of course! TypeScript is a superset of JavaScript that adds static typing. What would you like to know?',
role: 'assistant'
}
];
customDisclaimerComponent = CustomDisclaimerComponent;
onThumbsUp(event: any) {
console.log('Thumbs up!', event);
alert('You liked this message!');
}
onThumbsDown(event: any) {
console.log('Thumbs down!', event);
alert('You disliked this message!');
}
}`,
language: "typescript",
},
},
},
render: () => {
// Custom disclaimer component
@Component({
selector: "custom-disclaimer",
standalone: true,
template: `
<div
[class]="inputClass"
style="
text-align: center;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-size: 14px;
margin: 8px 16px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
"
>
🎨 This chat interface is fully customizable!
</div>
`,
})
class CustomDisclaimerComponent {
// Accept slot-provided inputs to avoid NG0303
@Input() text?: string;
@Input() inputClass?: string;
}
const messages: Message[] = [
{
id: "user-1",
content: "Hello! Can you help me with TypeScript?",
role: "user" as const,
},
{
id: "assistant-1",
content:
"Of course! TypeScript is a superset of JavaScript that adds static typing. What would you like to know?",
role: "assistant" as const,
},
];
const onThumbsUp = (event: any) => {
console.log("Thumbs up!", event);
alert("You liked this message!");
};
const onThumbsDown = (event: any) => {
console.log("Thumbs down!", event);
alert("You disliked this message!");
};
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[disclaimerComponent]="customDisclaimerComponent"
(assistantMessageThumbsUp)="onThumbsUp($event)"
(assistantMessageThumbsDown)="onThumbsDown($event)">
</copilot-chat-view>
</div>
`,
props: {
messages,
customDisclaimerComponent: CustomDisclaimerComponent,
onThumbsUp,
onThumbsDown,
},
};
},
};
@@ -0,0 +1,180 @@
import type { Meta, StoryObj } from "@storybook/angular";
import { moduleMetadata } from "@storybook/angular";
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
provideCopilotChatLabels,
provideCopilotKit,
} from "@copilotkit/angular";
import type { Message } from "@ag-ui/client";
const meta: Meta<CopilotChatView> = {
title: "UI/CopilotChatView/Customized with CSS",
component: CopilotChatView,
decorators: [
moduleMetadata({
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: "Type a message...",
chatDisclaimerText:
"AI can make mistakes. Please verify important information.",
}),
],
}),
],
parameters: {
layout: "fullscreen",
},
};
export default meta;
type Story = StoryObj<CopilotChatView>;
// Story with custom disclaimer text
export const CustomDisclaimerText: Story = {
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "Hello!",
role: "user" as const,
},
{
id: "assistant-1",
content: "Hi there! How can I help you today?",
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[disclaimerText]="'This is a custom disclaimer message for your chat interface.'">
</copilot-chat-view>
</div>
`,
props: {
messages,
},
};
},
};
export const AnimatedDisclaimer: Story = {
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "Hello! Can you help me with styling?",
role: "user" as const,
},
{
id: "assistant-1",
content: `Absolutely! I can help you with CSS styling, design patterns, and UI/UX best practices. What specific styling challenge are you working on?`,
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<style>
.custom-disclaimer {
background: linear-gradient(90deg, #FF6B6B 0%, #4ECDC4 50%, #45B7D1 100%);
color: white;
font-weight: 600;
font-size: 14px;
padding: 16px 24px;
margin: 12px 20px;
border-radius: 12px;
text-align: center;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
animation: pulse 3s ease-in-out infinite;
position: relative;
overflow: hidden;
}
.custom-disclaimer::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(
45deg,
transparent,
rgba(255, 255, 255, 0.1),
transparent
);
transform: rotate(45deg);
animation: shimmer 3s infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.02);
}
}
@keyframes shimmer {
0% {
transform: translateX(-100%) translateY(-100%) rotate(45deg);
}
100% {
transform: translateX(100%) translateY(100%) rotate(45deg);
}
}
.custom-disclaimer-text {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.custom-disclaimer-icon {
font-size: 20px;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
</style>
<copilot-chat-view
[messages]="messages"
[disclaimerClass]="'custom-disclaimer'"
[disclaimerText]="'✨ Styled with custom CSS classes - AI responses may need verification ✨'">
</copilot-chat-view>
</div>
`,
props: {
messages,
},
};
},
};
@@ -0,0 +1,834 @@
import type { Meta, StoryObj } from "@storybook/angular";
import { moduleMetadata } from "@storybook/angular";
import { CommonModule } from "@angular/common";
import { Component, Injectable, signal } from "@angular/core";
import { FormsModule } from "@angular/forms";
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
ChatState,
provideCopilotChatLabels,
provideCopilotKit,
} from "@copilotkit/angular";
import type { Message } from "@ag-ui/client";
import { CustomDisclaimerComponent } from "../components/custom-disclaimer.component";
import { CustomInputComponent } from "../components/custom-input.component";
import { CustomScrollButtonComponent } from "../components/custom-scroll-button.component";
@Injectable()
class StoryChatState extends ChatState {
readonly inputValue = signal<string>("");
submitInput(value: string): void {
const trimmed = value.trim();
if (!trimmed) return;
console.log("[Storybook] submitInput", trimmed);
this.inputValue.set("");
}
changeInput(value: string): void {
this.inputValue.set(value);
}
}
const meta: Meta<CopilotChatView> = {
title: "UI/CopilotChatView/Customized with Components",
component: CopilotChatView,
decorators: [
moduleMetadata({
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: "Type a message...",
chatDisclaimerText:
"AI can make mistakes. Please verify important information.",
}),
{ provide: ChatState, useClass: StoryChatState },
],
}),
],
parameters: {
layout: "fullscreen",
},
};
export default meta;
type Story = StoryObj<CopilotChatView>;
export const CustomDisclaimer: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
import { Message } from '@ag-ui/client';
// Custom disclaimer component
&#64;Component({
selector: 'custom-disclaimer',
standalone: true,
template: \`
<div [class]="inputClass" style="
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
margin: 10px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
">
<h3 style="margin: 0 0 10px 0; font-size: 20px;">
✨ Custom Disclaimer Component ✨
</h3>
<p style="margin: 0; font-size: 14px; opacity: 0.9;">
{{ '{' }}{{ '{' }} text || 'This is a custom disclaimer demonstrating component overrides!' {{ '}' }}{{ '}' }}
</p>
<div style="
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid rgba(255, 255, 255, 0.3);
font-size: 12px;
opacity: 0.7;
">
🎨 Styled with custom gradients and animations
</div>
</div>
\`
})
class CustomDisclaimerComponent {
&#64;Input() text?: string;
&#64;Input() inputClass?: string;
}
&#64;Component({
selector: 'app-custom-disclaimer',
standalone: true,
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
CustomDisclaimerComponent
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: "Type a message...",
chatDisclaimerText:
"AI can make mistakes. Please verify important information.",
})
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[disclaimerComponent]="customDisclaimerComponent">
</copilot-chat-view>
</div>
\`
})
export class CustomDisclaimerExampleComponent {
messages: Message[] = [
{
id: 'user-1',
content: 'Hello! Can you help me with TypeScript?',
role: 'user'
},
{
id: 'assistant-1',
content: 'Of course! TypeScript is a superset of JavaScript that adds static typing. What would you like to know?',
role: 'assistant'
}
];
customDisclaimerComponent = CustomDisclaimerComponent;
}`,
language: "typescript",
},
},
},
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "Hello! Can you help me with TypeScript?",
role: "user" as const,
},
{
id: "assistant-1",
content:
"Of course! TypeScript is a superset of JavaScript that adds static typing. What would you like to know?",
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[disclaimerComponent]="customDisclaimerComponent">
</copilot-chat-view>
</div>
`,
props: {
messages,
customDisclaimerComponent: CustomDisclaimerComponent,
},
};
},
};
export const CustomInput: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
ChatState,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
import { Message } from '@ag-ui/client';
// Custom input component
&#64;Component({
selector: 'custom-input',
standalone: true,
imports: [CommonModule, FormsModule],
template: \`
<div [class]="inputClass" style="
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
padding: 20px;
border-radius: 15px;
margin: 10px;
">
<input
type="text"
[(ngModel)]="inputValue"
placeholder="💬 Ask me anything..."
style="
width: 100%;
padding: 15px;
border: 2px solid white;
border-radius: 10px;
font-size: 16px;
background: rgba(255, 255, 255, 0.9);
color: #333;
outline: none;
"
(keyup.enter)="handleSend()"
/>
<button
style="
margin-top: 10px;
padding: 10px 20px;
background: white;
color: #f5576c;
border: none;
border-radius: 5px;
font-weight: bold;
cursor: pointer;
"
(click)="handleSend()">
Send Message ✨
</button>
</div>
\`
})
class CustomInputComponent {
&#64;Input() inputClass?: string;
inputValue = '';
constructor(private chat: ChatState) {}
handleSend() {
const value = this.inputValue.trim();
if (value) {
this.chat.submitInput(value);
this.inputValue = '';
}
}
}
&#64;Component({
selector: 'app-custom-input',
standalone: true,
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
CustomInputComponent
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: 'Type a message...',
chatDisclaimerText: 'AI can make mistakes. Please verify important information.'
})
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[inputComponent]="customInputComponent">
</copilot-chat-view>
</div>
\`
})
export class CustomInputExampleComponent {
messages: Message[] = [
{
id: 'user-1',
content: 'Check out this custom input!',
role: 'user'
},
{
id: 'assistant-1',
content: 'That's a beautiful custom input component! The gradient and styling look great.',
role: 'assistant'
}
];
customInputComponent = CustomInputComponent;
}`,
language: "typescript",
},
},
},
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "Check out this custom input!",
role: "user" as const,
},
{
id: "assistant-1",
content:
"That's a beautiful custom input component! The gradient and styling look great.",
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[inputComponent]="customInputComponent">
</copilot-chat-view>
</div>
`,
props: {
messages,
customInputComponent: CustomInputComponent,
},
};
},
};
export const CustomScrollButton: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
import { Message } from '@ag-ui/client';
// Custom scroll button component
&#64;Component({
selector: 'custom-scroll-button',
standalone: true,
imports: [CommonModule],
template: \`
<button
type="button"
(click)="handleClick()"
[class]="inputClass"
[class.hover]="isHovered"
(mouseenter)="isHovered = true"
(mouseleave)="isHovered = false"
style="
position: fixed;
bottom: 100px;
right: 20px;
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: 3px solid white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
cursor: pointer;
pointer-events: auto;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s;
z-index: 1000;
"
[style.transform]="isHovered ? 'scale(1.1)' : 'scale(1)'">
<span style="color: white; font-size: 24px; pointer-events: none;">⬇️</span>
</button>
\`,
styles: [\`
button.hover {
transform: scale(1.1);
}
\`]
})
class CustomScrollButtonComponent {
&#64;Input() onClick?: () => void;
&#64;Input() inputClass?: string;
&#64;Output() clicked = new EventEmitter<void>();
isHovered = false;
handleClick() {
this.clicked.emit();
if (this.onClick) {
this.onClick();
}
}
}
&#64;Component({
selector: 'app-custom-scroll',
standalone: true,
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
CustomScrollButtonComponent
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: 'Type a message...',
chatDisclaimerText: 'AI can make mistakes. Please verify important information.'
})
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[autoScroll]="false"
[scrollToBottomButtonComponent]="scrollToBottomButtonComponent">
</copilot-chat-view>
</div>
\`
})
export class CustomScrollButtonExampleComponent {
messages: Message[] = [];
scrollToBottomButtonComponent = CustomScrollButtonComponent;
constructor() {
// Generate many messages to show scroll behavior
for (let i = 0; i < 20; i++) {
this.messages.push({
id: \`msg-\${i}\`,
content: \`Message \${i}: This is a test message to demonstrate the custom scroll button.\`,
role: i % 2 === 0 ? 'user' : 'assistant'
} as Message);
}
}
}`,
language: "typescript",
},
},
},
render: () => {
// Generate many messages to show scroll behavior
const messages: Message[] = [];
for (let i = 0; i < 20; i++) {
messages.push({
id: `msg-${i}`,
content: `Message ${i}: This is a test message to demonstrate the custom scroll button.`,
role: i % 2 === 0 ? "user" : "assistant",
} as Message);
}
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[autoScroll]="false"
[scrollToBottomButtonComponent]="scrollToBottomButtonComponent">
</copilot-chat-view>
</div>
`,
props: {
messages,
scrollToBottomButtonComponent: CustomScrollButtonComponent,
},
};
},
};
export const NoFeatherEffect: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
import { Message } from '@ag-ui/client';
&#64;Component({
selector: 'app-no-feather',
standalone: true,
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: 'Type a message...',
chatDisclaimerText: 'AI can make mistakes. Please verify important information.'
})
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[featherComponent]="null">
</copilot-chat-view>
</div>
\`
})
export class NoFeatherEffectComponent {
messages: Message[] = [
{
id: 'user-1',
content: 'Hello!',
role: 'user'
},
{
id: 'assistant-1',
content: 'Hi there! How can I help you today?',
role: 'assistant'
}
];
}`,
language: "typescript",
},
},
},
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "Hello!",
role: "user" as const,
},
{
id: "assistant-1",
content: "Hi there! How can I help you today?",
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[featherComponent]="null">
</copilot-chat-view>
</div>
`,
props: {
messages,
},
};
},
};
export const CustomInputServiceBased: Story = {
name: "Custom Input via Service (Recommended)",
parameters: {
docs: {
description: {
story: `
Demonstrates the recommended approach for custom inputs using service injection.
This pattern uses \`ChatState.submitInput()\` to submit messages,
which is the idiomatic Angular approach for cross-component communication.
**Key differences from React:**
- Angular uses dependency injection with services
- React uses callback props (e.g., \`onSubmitMessage\`)
- Both achieve the same result with framework-appropriate patterns
`,
},
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
ChatState,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
import { Message } from '@ag-ui/client';
// Minimal custom input component with service injection
&#64;Component({
selector: 'service-based-input',
standalone: true,
imports: [CommonModule, FormsModule],
template: \`
<div style="
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
padding: 20px;
border-radius: 15px;
margin: 10px;
">
<h4 style="color: white; margin: 0 0 10px 0;">
Service-Based Custom Input
</h4>
<div style="display: flex; gap: 10px;">
<input
type="text"
[(ngModel)]="value"
placeholder="Type your message..."
style="
flex: 1;
padding: 12px;
border: 2px solid white;
border-radius: 8px;
font-size: 14px;
background: rgba(255, 255, 255, 0.95);
color: #333;
outline: none;
"
(keyup.enter)="submit()"
/>
<button
style="
padding: 12px 24px;
background: white;
color: #059669;
border: none;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
"
(click)="submit()">
Submit
</button>
</div>
<p style="color: rgba(255, 255, 255, 0.9); font-size: 12px; margin: 8px 0 0 0;">
This component uses ChatState.submitInput()
</p>
</div>
\`
})
class ServiceBasedInputComponent {
value = '';
constructor(private chat: ChatState) {}
submit() {
const trimmedValue = this.value.trim();
if (!trimmedValue) return;
// Use the service to submit the message
this.chat.submitInput(trimmedValue);
this.value = '';
}
}
&#64;Component({
selector: 'app-service-based-example',
standalone: true,
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
ServiceBasedInputComponent
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: 'Type a message...',
chatDisclaimerText: 'AI can make mistakes. Please verify important information.'
})
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[inputComponent]="customInputComponent">
</copilot-chat-view>
</div>
\`
})
export class ServiceBasedExampleComponent {
messages: Message[] = [
{
id: 'user-1',
content: 'How does the service-based approach work?',
role: 'user'
},
{
id: 'assistant-1',
content: 'The service-based approach uses Angular\\'s dependency injection to access ChatState, which provides the submitInput() method for sending messages. This is the idiomatic Angular pattern!',
role: 'assistant'
}
];
customInputComponent = ServiceBasedInputComponent;
}`,
language: "typescript",
},
},
},
render: () => {
// Define the service-based input component inline for the story
@Component({
selector: "story-service-input",
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div
style="
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
padding: 20px;
border-radius: 15px;
margin: 10px;
"
>
<h4 style="color: white; margin: 0 0 10px 0">Service-Based Custom Input</h4>
<div style="display: flex; gap: 10px">
<input
type="text"
[(ngModel)]="value"
placeholder="Type your message..."
style="
flex: 1;
padding: 12px;
border: 2px solid white;
border-radius: 8px;
font-size: 14px;
background: rgba(255, 255, 255, 0.95);
color: #333;
outline: none;
"
(keyup.enter)="submit()"
/>
<button
style="
padding: 12px 24px;
background: white;
color: #059669;
border: none;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
"
(click)="submit()"
>
Submit
</button>
</div>
<p
style="color: rgba(255, 255, 255, 0.9); font-size: 12px; margin: 8px 0 0 0"
>
This component uses ChatState.submitInput()
</p>
</div>
`,
})
class StoryServiceInputComponent {
value = "";
constructor(private chat: ChatState) {}
submit() {
const trimmedValue = this.value.trim();
if (!trimmedValue) return;
this.chat.submitInput(trimmedValue);
this.value = "";
}
}
const messages: Message[] = [
{
id: "user-1",
content: "How does the service-based approach work?",
role: "user" as const,
},
{
id: "assistant-1",
content:
"The service-based approach uses Angular's dependency injection to access ChatState, which provides the submitInput() method for sending messages. This is the idiomatic Angular pattern!",
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[inputComponent]="customInputComponent">
</copilot-chat-view>
</div>
`,
props: {
messages,
customInputComponent: StoryServiceInputComponent,
},
};
},
};
@@ -0,0 +1,452 @@
import type { Meta, StoryObj } from "@storybook/angular";
import { moduleMetadata } from "@storybook/angular";
import { CommonModule } from "@angular/common";
import { Component, Injectable, Input, signal } from "@angular/core";
import { FormsModule } from "@angular/forms";
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
ChatState,
provideCopilotChatLabels,
provideCopilotKit,
} from "@copilotkit/angular";
import type { Message } from "@ag-ui/client";
@Injectable()
class StoryChatState extends ChatState {
readonly inputValue = signal<string>("");
submitInput(value: string): void {
const trimmed = value.trim();
if (!trimmed) return;
console.log("[Storybook] submitInput", trimmed);
this.inputValue.set("");
}
changeInput(value: string): void {
this.inputValue.set(value);
}
}
// Custom input components defined after imports
const meta: Meta<CopilotChatView> = {
title: "UI/CopilotChatView/Customized with Templates",
component: CopilotChatView,
decorators: [
moduleMetadata({
imports: [
CommonModule,
FormsModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: "Type a message...",
chatDisclaimerText:
"AI can make mistakes. Please verify important information.",
}),
{ provide: ChatState, useClass: StoryChatState },
],
}),
],
parameters: {
layout: "fullscreen",
},
};
export default meta;
type Story = StoryObj<CopilotChatView>;
export const CustomDisclaimerTemplate: Story = {
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "How do I use templates for customization?",
role: "user" as const,
},
{
id: "assistant-1",
content:
"Templates provide a powerful way to customize components! You can use ng-template with template references to inject custom HTML directly into the component slots.",
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<!-- Custom Disclaimer Template -->
<ng-template #customDisclaimer>
<div style="
text-align: center;
padding: 16px;
background: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 100%);
color: white;
font-size: 14px;
font-weight: 600;
margin: 12px 20px;
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
position: relative;
overflow: hidden;
">
<div style="
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent);
transform: rotate(45deg);
animation: shimmer 3s infinite;
"></div>
<span style="position: relative; z-index: 1;">
⚡ Template-based customization - AI assistance at your fingertips!
</span>
</div>
</ng-template>
<copilot-chat-view
[messages]="messages"
[disclaimerTemplate]="customDisclaimer">
</copilot-chat-view>
</div>
`,
props: {
messages,
},
};
},
};
// Custom input component for template story
@Component({
selector: "template-custom-input",
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div
style="
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 24px;
margin: 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
"
>
<div style="display: flex; gap: 12px; max-width: 1200px; margin: 0 auto">
<input
type="text"
[(ngModel)]="inputValue"
placeholder="✨ Type your message here..."
style="
flex: 1;
padding: 16px 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
font-size: 16px;
background: rgba(255, 255, 255, 0.95);
color: #333;
outline: none;
transition: all 0.3s ease;
"
(keyup.enter)="sendMessage()"
/>
<button
style="
padding: 16px 32px;
background: white;
color: #667eea;
border: none;
border-radius: 12px;
font-weight: bold;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
"
(click)="sendMessage()"
>
Send
</button>
</div>
<div
style="
text-align: center;
margin-top: 8px;
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
"
>
Press Enter to send • Powered by Templates
</div>
</div>
`,
})
class TemplateCustomInputComponent {
inputValue = "";
constructor(private chat: ChatState) {}
sendMessage() {
const value = this.inputValue.trim();
if (value) {
this.chat.submitInput(value);
this.inputValue = "";
}
}
}
export const CustomInputTemplate: Story = {
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "This input is created with a component!",
role: "user" as const,
},
{
id: "assistant-1",
content:
"Yes! Components with service injection provide complete control over the input area, including custom styling and behavior.",
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[inputComponent]="customInputComponent">
</copilot-chat-view>
</div>
`,
props: {
messages,
customInputComponent: TemplateCustomInputComponent,
},
};
},
};
export const CustomScrollButtonTemplate: Story = {
render: () => {
// Generate many messages to show scroll behavior
const messages: Message[] = [];
for (let i = 0; i < 25; i++) {
messages.push({
id: `msg-${i}`,
content:
i % 2 === 0
? `User message ${i}: Template-based scroll button demonstration!`
: `Assistant response ${i}: Templates provide maximum flexibility for UI customization, allowing you to create exactly the experience you want.`,
role: i % 2 === 0 ? "user" : "assistant",
} as Message);
}
// Simple click handler without DOM manipulation
const handleScroll = (onClick: () => void) => {
onClick();
};
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<!-- Custom Scroll Button Template -->
<ng-template #customScrollButton let-onClick="onClick">
<button
(click)="handleScroll(onClick)"
(mouseenter)="isHovered = true"
(mouseleave)="isHovered = false"
style="
position: fixed;
bottom: 100px;
right: 30px;
width: 64px;
height: 64px;
border-radius: 50%;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
border: 3px solid white;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1000;
"
[style.transform]="isHovered ? 'scale(1.15) rotate(360deg)' : 'scale(1) rotate(0deg)'">
<div style="
display: flex;
flex-direction: column;
align-items: center;
color: white;
">
<span style="font-size: 24px;">↓</span>
<span style="font-size: 10px; margin-top: -4px;">SCROLL</span>
</div>
</button>
</ng-template>
<copilot-chat-view
[messages]="messages"
[autoScroll]="false"
[scrollToBottomButtonTemplate]="customScrollButton">
</copilot-chat-view>
</div>
`,
props: {
messages,
handleScroll,
isHovered: false,
},
};
},
};
// Custom input component for combined story
@Component({
selector: "combined-custom-input",
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div
style="
background: linear-gradient(90deg, #00d2ff 0%, #3a47d5 100%);
padding: 20px;
"
>
<input
type="text"
[(ngModel)]="inputValue"
placeholder="Template-powered input..."
style="
width: calc(100% - 100px);
padding: 12px;
border: 2px solid white;
border-radius: 8px;
font-size: 16px;
outline: none;
"
(keyup.enter)="sendMessage()"
/>
<button
style="
width: 80px;
padding: 12px;
margin-left: 10px;
background: white;
color: #3a47d5;
border: none;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
"
(click)="sendMessage()"
>
Go
</button>
</div>
`,
})
class CombinedCustomInputComponent {
inputValue = "";
constructor(private chat: ChatState) {}
sendMessage() {
const value = this.inputValue.trim();
if (value) {
this.chat.submitInput(value);
this.inputValue = "";
}
}
}
export const AllTemplatesCombined: Story = {
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "Show me all templates working together!",
role: "user" as const,
},
{
id: "assistant-1",
content:
"Here you can see custom disclaimer, input, and scroll button templates all working in harmony!",
role: "assistant" as const,
},
{
id: "user-2",
content: "This is amazing flexibility!",
role: "user" as const,
},
{
id: "assistant-2",
content:
"Templates give you complete control over every aspect of the chat interface while maintaining the core functionality.",
role: "assistant" as const,
},
];
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<!-- Combined Templates -->
<ng-template #disclaimer>
<div style="
text-align: center;
padding: 12px;
background: linear-gradient(90deg, #00d2ff 0%, #3a47d5 100%);
color: white;
font-size: 13px;
margin: 8px 16px;
border-radius: 8px;
">
🚀 All custom templates active!
</div>
</ng-template>
<ng-template #scrollBtn let-onClick="onClick">
<button
(click)="onClick()"
style="
position: fixed;
bottom: 90px;
right: 20px;
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(90deg, #00d2ff 0%, #3a47d5 100%);
border: 2px solid white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
cursor: pointer;
color: white;
font-size: 20px;
">
</button>
</ng-template>
<copilot-chat-view
[messages]="messages"
[autoScroll]="false"
[disclaimerTemplate]="disclaimer"
[inputComponent]="customInputComponent"
[scrollToBottomButtonTemplate]="scrollBtn">
</copilot-chat-view>
</div>
`,
props: {
messages,
customInputComponent: CombinedCustomInputComponent,
},
};
},
};
@@ -0,0 +1,353 @@
import type { Meta, StoryObj } from "@storybook/angular";
import { moduleMetadata } from "@storybook/angular";
import { CommonModule } from "@angular/common";
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
provideCopilotChatLabels,
provideCopilotKit,
} from "@copilotkit/angular";
import type { Message } from "@ag-ui/client";
const meta: Meta<CopilotChatView> = {
title: "UI/CopilotChatView/Basic Examples",
component: CopilotChatView,
decorators: [
moduleMetadata({
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: "Type a message...",
chatDisclaimerText:
"AI can make mistakes. Please verify important information.",
}),
],
}),
],
parameters: {
layout: "fullscreen",
},
};
export default meta;
type Story = StoryObj<CopilotChatView>;
// Default story
export const Default: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
import { Message } from '@ag-ui/client';
@Component({
selector: 'app-chat',
standalone: true,
imports: [
CommonModule,
CopilotChatView,
CopilotChatMessageView,
CopilotChatInput
],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: 'Type a message...',
chatDisclaimerText: 'AI can make mistakes. Please verify important information.'
}),
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
(assistantMessageThumbsUp)="onThumbsUp($event)"
(assistantMessageThumbsDown)="onThumbsDown($event)">
</copilot-chat-view>
</div>
\`
})
export class ChatComponent {
messages: Message[] = [
{
id: 'user-1',
content: 'Hello! How can I integrate CopilotKit with my Angular app?',
role: 'user'
},
{
id: 'assistant-1',
content: \`To integrate CopilotKit with your Angular app, follow these steps:
1. Install the package:
\\\`\\\`\\\`bash
npm install @copilotkit/angular
\\\`\\\`\\\`
2. Import and configure in your component:
\\\`\\\`\\\`typescript
import { provideCopilotKit } from '@copilotkit/angular';
@Component({
providers: [provideCopilotKit({})]
})
\\\`\\\`\\\`
3. Use the chat components in your template!\`,
role: 'assistant'
},
{
id: 'user-2',
content: 'That looks great! Can I customize the appearance?',
role: 'user'
},
{
id: 'assistant-2',
content: 'Yes! CopilotKit is highly customizable. You can customize the appearance using Tailwind CSS classes or by providing your own custom components through the slot system.',
role: 'assistant'
}
];
onThumbsUp(event: any) {
alert('Thumbs up! You liked this message.');
console.log('Thumbs up event:', event);
}
onThumbsDown(event: any) {
alert('Thumbs down! You disliked this message.');
console.log('Thumbs down event:', event);
}
}`,
language: "typescript",
},
},
},
render: () => {
const messages: Message[] = [
{
id: "user-1",
content: "Hello! How can I integrate CopilotKit with my Angular app?",
role: "user" as const,
},
{
id: "assistant-1",
content: `To integrate CopilotKit with your Angular app, follow these steps:
1. Install the package:
\`\`\`bash
npm install @copilotkit/angular
\`\`\`
2. Import and configure in your component:
\`\`\`typescript
import { provideCopilotKit } from '@copilotkit/angular';
@Component({
providers: [provideCopilotKit({})]
})
\`\`\`
3. Use the chat components in your template!`,
role: "assistant" as const,
},
{
id: "user-2",
content: "That looks great! Can I customize the appearance?",
role: "user" as const,
},
{
id: "assistant-2",
content:
"Yes! CopilotKit is highly customizable. You can customize the appearance using Tailwind CSS classes or by providing your own custom components through the slot system.",
role: "assistant" as const,
},
];
const onThumbsUp = (event: any) => {
alert("Thumbs up! You liked this message.");
console.log("Thumbs up event:", event);
};
const onThumbsDown = (event: any) => {
alert("Thumbs down! You disliked this message.");
console.log("Thumbs down event:", event);
};
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
(assistantMessageThumbsUp)="onThumbsUp($event)"
(assistantMessageThumbsDown)="onThumbsDown($event)">
</copilot-chat-view>
</div>
`,
props: {
messages,
onThumbsUp,
onThumbsDown,
},
};
},
};
// Story with manual scroll
export const ManualScroll: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
CopilotChatView,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
import { Message } from '@ag-ui/client';
@Component({
selector: 'app-chat-scroll',
standalone: true,
imports: [CommonModule, CopilotChatView],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: 'Type a message...',
chatDisclaimerText: 'AI can make mistakes. Please verify important information.'
})
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[autoScroll]="false">
</copilot-chat-view>
</div>
\`
})
export class ChatScrollComponent {
messages: Message[] = [];
constructor() {
// Generate many messages to show scroll behavior
for (let i = 0; i < 20; i++) {
if (i % 2 === 0) {
this.messages.push({
id: \`user-\${i}\`,
content: \`User message \${i}: This is a test message to demonstrate scrolling behavior.\`,
role: 'user'
});
} else {
this.messages.push({
id: \`assistant-\${i}\`,
content: \`Assistant response \${i}: This is a longer response to demonstrate how the chat interface handles various message lengths and scrolling behavior when there are many messages in the conversation.\`,
role: 'assistant'
});
}
}
}
}`,
language: "typescript",
},
},
},
render: () => {
// Generate many messages to show scroll behavior
const messages: Message[] = [];
for (let i = 0; i < 20; i++) {
if (i % 2 === 0) {
messages.push({
id: `user-${i}`,
content: `User message ${i}: This is a test message to demonstrate scrolling behavior.`,
role: "user" as const,
});
} else {
messages.push({
id: `assistant-${i}`,
content: `Assistant response ${i}: This is a longer response to demonstrate how the chat interface handles various message lengths and scrolling behavior when there are many messages in the conversation.`,
role: "assistant" as const,
});
}
}
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="messages"
[autoScroll]="false">
</copilot-chat-view>
</div>
`,
props: {
messages,
},
};
},
};
// Story with empty state
export const EmptyState: Story = {
parameters: {
docs: {
source: {
type: "code",
code: `import { Component } from '@angular/core';
import {
CopilotChatView,
provideCopilotKit,
provideCopilotChatLabels
} from '@copilotkit/angular';
@Component({
selector: 'app-chat-empty',
standalone: true,
imports: [CopilotChatView],
providers: [
provideCopilotKit({}),
provideCopilotChatLabels({
chatInputPlaceholder: 'Type a message...',
chatDisclaimerText: 'AI can make mistakes. Please verify important information.'
})
],
template: \`
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view [messages]="[]">
</copilot-chat-view>
</div>
\`
})
export class EmptyChatComponent {}`,
language: "typescript",
},
},
},
render: () => {
return {
template: `
<div style="height: 100vh; margin: 0; padding: 0; overflow: hidden;">
<copilot-chat-view
[messages]="[]">
</copilot-chat-view>
</div>
`,
props: {},
};
},
};
@@ -0,0 +1,120 @@
import { Meta } from "@storybook/blocks";
<Meta title="Welcome" />
# CopilotKit Angular Components
Welcome to the **CopilotKit Angular Storybook**! This is the dedicated documentation and showcase for all Angular components in the CopilotKit ecosystem.
## What is CopilotKit?
CopilotKit is a comprehensive toolkit for building AI-powered copilot experiences in your applications. This Angular package provides native Angular components that integrate seamlessly with your Angular applications.
## Available Components
### Chat Components
- **CopilotChatInput** - A feature-rich chat input component with voice recording, tools menu, and file attachments
- **CopilotChatTextarea** - An auto-resizing textarea component for chat interfaces
- **CopilotChatView** - Complete chat interface with message history
- **CopilotChatUserMessage** - User message display component
- **CopilotAssistantMessage** - Assistant message display component
### Context Management
- **CopilotAgentContextProvider** - Provides agent context to child components
- **CopilotChatConfigurationProvider** - Configures chat components globally
## Getting Started
### Installation
```bash
npm install @copilotkit/angular
# or
pnpm add @copilotkit/angular
```
### Basic Usage
```typescript
import { Component } from "@angular/core";
import { CopilotChatInput } from "@copilotkit/angular";
import { provideCopilotChatConfiguration } from "@copilotkit/angular";
@Component({
selector: "app-chat",
standalone: true,
imports: [CopilotChatInput],
providers: [
provideCopilotChatConfiguration({
labels: {
chatInputPlaceholder: "Type a message...",
},
}),
],
template: `
<copilot-chat-input
(submitMessage)="onSubmitMessage($event)"
></copilot-chat-input>
`,
})
export class ChatComponent {
onSubmitMessage(message: string): void {
console.log("Message:", message);
}
}
```
## Features
### Angular 19+ Support
Built with the latest Angular features including:
- Standalone components
- Signals for reactive state management
- OnPush change detection
- TypeScript strict mode
### Full TypeScript Support
All components are fully typed with comprehensive TypeScript definitions.
### Customizable
- Custom CSS classes for styling
- Configurable labels and placeholders
- Event emitters for all interactions
- Flexible slot system for content projection
## Development
This Storybook runs alongside the [React Storybook](http://localhost:6006) on port 6007.
### Available Scripts
```bash
# Run Angular Storybook in development mode
pnpm --filter storybook-angular dev
# Build Angular Storybook for production
pnpm --filter storybook-angular build
```
## Links
- [CopilotKit Documentation](https://docs.copilotkit.ai)
- [GitHub Repository](https://github.com/CopilotKit/CopilotKit)
- [React Components Storybook](http://localhost:6006)
## Support
For issues, feature requests, or questions:
- [GitHub Issues](https://github.com/CopilotKit/CopilotKit/issues)
- [Discord Community](https://discord.gg/copilotkit)
---
_Built with Angular 19 and Storybook 8_
@@ -0,0 +1,35 @@
{
"extends": "@copilotkit/typescript-config/base.json",
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022", "dom"],
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"useDefineForClassFields": false,
"baseUrl": ".",
"paths": {
"@copilotkit/angular": ["../../../../packages/angular/src/index.ts"],
"@copilotkit/angular/*": ["../../../../packages/angular/src/*"],
"@copilotkit/a2ui-renderer": ["node_modules/@copilotkit/a2ui-renderer"],
"@copilotkit/a2ui-renderer/web-components": [
"node_modules/@copilotkit/a2ui-renderer/web-components"
],
"@copilotkit/a2ui-renderer/web-components/define": [
"node_modules/@copilotkit/a2ui-renderer/web-components/define"
],
"@copilotkit/core": ["node_modules/@copilotkit/core"],
"@copilotkit/shared": ["node_modules/@copilotkit/shared"]
},
"types": ["node"]
},
"include": ["**/*.ts", "**/*.tsx", ".storybook/**/*"],
"exclude": ["node_modules", "dist"]
}