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