1import type { AttachmentsConfig } from "@copilotkit/angular";
2import type {
3 FrontendToolConfig,
4 HumanInTheLoopConfig,
5 RenderToolCallConfig,
6} from "@copilotkit/angular";
7import type { Type } from "@angular/core";
8import {
9 ChangeDetectionStrategy,
10 Component,
11 computed,
12 signal,
13} from "@angular/core";
14import {
15 injectAgentStore,
16 registerFrontendTool,
17 registerHumanInTheLoop,
18 registerRenderActivityMessage,
19 registerRenderToolCall,
20} from "@copilotkit/angular";
21import { mcpAppsActivityRendererConfig } from "@copilotkit/angular/mcp-apps";
22import { z } from "zod";
23
24import { agentIdForCurrentIntegration } from "../../feature-agent";
25import { FeatureHeaderComponent } from "../feature-header.component";
26import { SHOWCASE_MCP_APPS_PROVIDER } from "../generated-ui/mcp-apps-provider";
27import { ShowcaseChatHostComponent } from "../showcase-chat-host.component";
28import {
29 BarChartCard,
30 BeautifulToolReasoningCard,
31 FlightSearchCard,
32 MeetingTimePickerCard,
33 PieChartCard,
34} from "./beautiful-chat-cards";
35import { toggleDocumentTheme } from "./beautiful-chat-model";
36import {
37 BeautifulTodoCanvas,
38 readBeautifulTodos,
39} from "./beautiful-todo-canvas";
40import type { BeautifulTodo } from "./beautiful-todo-canvas";
41
42type ToolArgs = Record<string, unknown>;
43
44const ATTACHMENTS: AttachmentsConfig = { enabled: true };
45const chartParameters = z.object({
46 title: z.string(),
47 description: z.string(),
48 data: z.array(z.object({ label: z.string(), value: z.number() })),
49});
50
51@Component({
52 selector: "showcase-beautiful-chat-feature",
53 imports: [
54 BeautifulTodoCanvas,
55 FeatureHeaderComponent,
56 ShowcaseChatHostComponent,
57 ],
58 providers: [SHOWCASE_MCP_APPS_PROVIDER],
59 changeDetection: ChangeDetectionStrategy.OnPush,
60 host: { class: "feature-page" },
61 template: `
62 <showcase-feature-header />
63 <main class="beautiful-page" [class.app-mode]="mode() === 'app'">
64 <nav class="mode-toggle" aria-label="Beautiful Chat view">
65 <button
66 type="button"
67 [attr.aria-pressed]="mode() === 'chat'"
68 (click)="mode.set('chat')"
69 >
70 Chat
71 </button>
72 <button
73 type="button"
74 [attr.aria-pressed]="mode() === 'app'"
75 (click)="mode.set('app')"
76 >
77 App
78 </button>
79 </nav>
80 <section class="chat-pane" aria-label="CopilotKit assistant">
81 <header class="brand">
82 <strong>CopilotKit</strong><span aria-hidden="true">✦</span>
83 </header>
84 <div class="chat-surface">
85 <showcase-chat-host [attachments]="attachments" />
86 </div>
87 </section>
88 <section class="app-pane" aria-label="Shared-state task manager">
89 <showcase-beautiful-todo-canvas
90 [todos]="todos()"
91 [isRunning]="isRunning()"
92 (todosChange)="setTodos($event)"
93 />
94 </section>
95 </main>
96 `,
97 styles: `
98 .beautiful-page {
99 position: relative;
100 display: grid;
101 min-height: 0;
102 grid-template-columns: minmax(0, 1fr) 0;
103 overflow: hidden;
104 color: #14213d;
105 background: #fff;
106 transition: grid-template-columns 180ms ease;
107 }
108 .beautiful-page.app-mode {
109 grid-template-columns: minmax(20rem, 1fr) minmax(0, 2fr);
110 }
111 .mode-toggle {
112 position: absolute;
113 z-index: 4;
114 top: 0.75rem;
115 right: 0.75rem;
116 display: flex;
117 gap: 0.15rem;
118 padding: 0.2rem;
119 border: 1px solid #d8e0ea;
120 border-radius: 999px;
121 background: #eef2f7;
122 }
123 .mode-toggle button {
124 padding: 0.4rem 0.85rem;
125 border: 0;
126 border-radius: 999px;
127 color: #52637a;
128 background: transparent;
129 font: inherit;
130 font-size: 0.8rem;
131 font-weight: 650;
132 cursor: pointer;
133 }
134 .mode-toggle button[aria-pressed="true"] {
135 color: #14213d;
136 background: #fff;
137 box-shadow: 0 1px 4px #94a3b855;
138 }
139 .mode-toggle button:focus-visible {
140 outline: 3px solid #91a7ff;
141 outline-offset: 2px;
142 }
143 .chat-pane {
144 display: grid;
145 min-width: 0;
146 min-height: 0;
147 grid-template-rows: auto minmax(0, 1fr);
148 }
149 .brand {
150 display: flex;
151 align-items: center;
152 gap: 0.4rem;
153 padding: 1rem 1.25rem 0.5rem;
154 font-size: 1.35rem;
155 }
156 .brand span {
157 color: #4263eb;
158 }
159 .chat-surface {
160 min-height: 0;
161 overflow: hidden;
162 }
163 .app-pane {
164 min-width: 0;
165 min-height: 0;
166 overflow: auto;
167 border-left: 1px solid #d8e0ea;
168 }
169 :host-context(.dark) .beautiful-page,
170 :host-context(.dark) .chat-pane {
171 color: #f8fafc;
172 background: #111827;
173 }
174 :host-context(.dark) .brand {
175 color: #f8fafc;
176 }
177 @media (prefers-reduced-motion: reduce) {
178 .beautiful-page {
179 transition: none;
180 }
181 }
182 @media (max-width: 64rem) {
183 .beautiful-page.app-mode {
184 grid-template-columns: 0 minmax(0, 1fr);
185 }
186 .beautiful-page.app-mode .chat-pane {
187 visibility: hidden;
188 }
189 .app-pane {
190 border-left: 0;
191 }
192 }
193 `,
194})
195export class BeautifulChatFeatureComponent {
196 protected readonly mode = signal<"chat" | "app">("chat");
197 protected readonly attachments = ATTACHMENTS;
198 private readonly agentId = agentIdForCurrentIntegration("beautiful-chat");
199 private readonly agentStore = injectAgentStore(this.agentId);
200 protected readonly todos = computed(() =>
201 readBeautifulTodos(this.agentStore().state()),
202 );
203 protected readonly isRunning = computed(() => this.agentStore().isRunning());
204
205 constructor() {
206 registerRenderActivityMessage(mcpAppsActivityRendererConfig);
207 this.registerChart("pieChart", PieChartCard);
208 this.registerChart("barChart", BarChartCard);
209 registerRenderToolCall({
210 name: "search_flights",
211 args: z.object({ flights: z.array(z.record(z.unknown())) }),
212 component: asRenderer(FlightSearchCard),
213 });
214 this.registerMeetingPicker();
215 this.registerThemeTool();
216 this.registerModeTool("enableAppMode", "app");
217 this.registerModeTool("enableChatMode", "chat");
218 registerRenderToolCall({
219 name: "*",
220 args: z.record(z.unknown()),
221 component: asRenderer(BeautifulToolReasoningCard),
222 });
223 }
224
225
226 protected setTodos(todos: BeautifulTodo[]): void {
227 this.agentStore().agent.setState({ todos });
228 }
229
230
231 private registerChart(
232 name: "pieChart" | "barChart",
233 component: Type<unknown>,
234 ): void {
235 registerFrontendTool({
236 name,
237 description: `Render a controlled ${name === "pieChart" ? "pie" : "bar"} chart.`,
238 parameters: chartParameters,
239 component: asFrontendRenderer(component),
240 handler: async (args) => args,
241 });
242 }
243
244
245 private registerMeetingPicker(): void {
246 const config: HumanInTheLoopConfig<ToolArgs> = {
247 name: "scheduleTime",
248 description: "Ask the user to choose a meeting time.",
249 parameters: z.object({
250 reasonForScheduling: z.string(),
251 meetingDuration: z.number(),
252 }),
253 component: asHitlRenderer(MeetingTimePickerCard),
254 };
255 registerHumanInTheLoop(config);
256 }
257
258
259 private registerThemeTool(): void {
260 registerFrontendTool({
261 name: "toggleTheme",
262 description: "Toggle the showcase between light and dark themes.",
263 parameters: z.object({}),
264 followUp: true,
265 handler: async () => ({
266 theme: toggleDocumentTheme(globalThis.document?.documentElement),
267 }),
268 });
269 }
270
271
272 private registerModeTool(name: string, mode: "chat" | "app"): void {
273 registerFrontendTool({
274 name,
275 description: `Enable ${mode} mode.`,
276 parameters: z.object({}),
277 followUp: false,
278 handler: async () => {
279 this.mode.set(mode);
280 return { mode };
281 },
282 });
283 }
284}
285
286
287function asRenderer(
288 component: Type<unknown>,
289): RenderToolCallConfig<ToolArgs>["component"] {
290 return component as unknown as RenderToolCallConfig<ToolArgs>["component"];
291}
292
293
294function asFrontendRenderer(
295 component: Type<unknown>,
296): NonNullable<FrontendToolConfig<ToolArgs>["component"]> {
297 return component as unknown as NonNullable<
298 FrontendToolConfig<ToolArgs>["component"]
299 >;
300}
301
302
303function asHitlRenderer(
304 component: Type<unknown>,
305): HumanInTheLoopConfig<ToolArgs>["component"] {
306 return component as unknown as HumanInTheLoopConfig<ToolArgs>["component"];
307}
308