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