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