1import {
2 ChangeDetectionStrategy,
3 Component,
4 computed,
5 inject,
6} from "@angular/core";
7import { ActivatedRoute } from "@angular/router";
8import { injectInterrupt } from "@copilotkit/angular";
9
10import { agentIdForRoute } from "../../feature-agent";
11import { FeatureHeaderComponent } from "../feature-header.component";
12import { ShowcaseChatHostComponent } from "../showcase-chat-host.component";
13import { parseInterruptPayload } from "./interrupt-payload";
14import type { InterruptSlot } from "./interrupt-payload";
15
16@Component({
17 selector: "showcase-interrupt-feature",
18 imports: [FeatureHeaderComponent, ShowcaseChatHostComponent],
19 changeDetection: ChangeDetectionStrategy.OnPush,
20 host: { class: "feature-page" },
21 template: `
22 <showcase-feature-header />
23 <main class="interrupt-layout" [class.interrupt-layout-headless]="isHeadless">
24 @if (isHeadless) {
25 <section
26 class="interrupt-app-surface"
27 data-testid="interrupt-headless-app-surface"
28 aria-label="Scheduling application surface"
29 >
30 <header>
31 <span>Headless interrupt</span>
32 <h2>Scheduling</h2>
33 </header>
34 @if (controller.hasInterrupt()) {
35 <article
36 class="interrupt-picker"
37 data-testid="interrupt-headless-popup"
38 role="dialog"
39 aria-modal="false"
40 aria-labelledby="headless-interrupt-title"
41 >
42 <span>Pick a time</span>
43 <h3 id="headless-interrupt-title">{{ payload().topic }}</h3>
44 @if (payload().attendee; as attendee) {
45 <p>with {{ attendee }}</p>
46 }
47 <div class="interrupt-slots">
48 @for (slot of payload().slots; track slot.iso) {
49 <button
50 type="button"
51 [attr.data-testid]="'interrupt-headless-slot-' + slot.iso"
52 (click)="resolve(slot)"
53 >
54 {{ slot.label }}
55 </button>
56 }
57 </div>
58 <button
59 type="button"
60 data-testid="interrupt-headless-cancel"
61 (click)="cancel()"
62 >
63 Cancel
64 </button>
65 </article>
66 } @else {
67 <section class="interrupt-empty" data-testid="interrupt-headless-empty">
68 <span aria-hidden="true">◷</span>
69 <h3>Nothing scheduled yet</h3>
70 <p>
71 Ask the assistant to book something. Its picker will appear here.
72 </p>
73 </section>
74 }
75 </section>
76 }
77
78 <section class="chat-surface" aria-label="Interrupt chat demonstration">
79 @if (!isHeadless && controller.hasInterrupt()) {
80 <article class="interrupt-picker" data-testid="time-picker-card">
81 <span>Pick a time</span>
82 <h2>{{ payload().topic }}</h2>
83 @if (pickedLabel; as label) {
84 <p data-testid="time-picker-picked">Booked {{ label }}</p>
85 } @else {
86 <div class="interrupt-slots">
87 @for (slot of payload().slots; track slot.iso) {
88 <button
89 type="button"
90 data-testid="time-picker-slot"
91 (click)="resolve(slot)"
92 >
93 {{ slot.label }}
94 </button>
95 }
96 </div>
97 }
98 </article>
99 }
100 @if (controller.error()) {
101 <p class="interrupt-error" role="alert">
102 The decision could not be submitted. Please try again deliberately.
103 </p>
104 }
105 <showcase-chat-host />
106 </section>
107 </main>
108 `,
109})
110export class InterruptFeatureComponent {
111 private readonly route = inject(ActivatedRoute);
112 protected readonly feature =
113 (this.route.snapshot.data["feature"] as string | undefined) ??
114 "gen-ui-interrupt";
115 protected readonly isHeadless = this.feature === "interrupt-headless";
116 private readonly agentId = agentIdForRoute(this.feature, this.route);
117 protected readonly controller = injectInterrupt({ agentId: this.agentId });
118 protected readonly payload = computed(() =>
119 parseInterruptPayload(this.controller.event()?.value),
120 );
121 protected pickedLabel: string | null = null;
122
123
124 protected resolve(slot: InterruptSlot): void {
125 this.pickedLabel = slot.label;
126 this.controller
127 .resolve({
128 chosen_time: slot.iso,
129 chosen_label: slot.label,
130 })
131 .catch(() => undefined);
132 }
133
134
135 protected cancel(): void {
136 this.controller.cancel().catch(() => undefined);
137 }
138}
139