chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user