11// SPDX-License-Identifier: MIT
2- import { Component } from '@angular/core' ;
3- import { ChatComponent , ChatInterruptPanelComponent , ChatWelcomeSuggestionComponent , views , type InterruptAction } from '@threadplane/chat' ;
2+ import { Component , ChangeDetectionStrategy , signal } from '@angular/core' ;
3+ import { ChatComponent , ChatApprovalCardComponent , ChatWelcomeSuggestionComponent , type ChatApprovalAction } from '@threadplane/chat' ;
44import { agent } from '@threadplane/langgraph' ;
55import { ExampleChatLayoutComponent } from '@threadplane/example-layouts' ;
6- import { signalStateStore } from '@threadplane/render ' ;
6+ import { CurrencyPipe } from '@angular/common ' ;
77import { environment } from '../environments/environment' ;
8- import { ApprovalCardComponent } from './views/approval-card.component' ;
98
109const WELCOME_SUGGESTIONS = [
11- { label : 'Approve a tool call' , value : 'Book a flight to Paris for next Tuesday.' } ,
10+ { label : 'Refund a duplicate charge' , value : 'Refund $47.50 to customer cus_a8x2k — they were charged twice for the same order.' } ,
11+ { label : 'Refund a chargeback' , value : 'Refund $129.00 to customer cus_z19fp who opened a chargeback for unrecognized activity.' } ,
1212] as const ;
1313
1414/**
15- * InterruptsComponent demonstrates human-in-the-loop with `agent()` .
15+ * Refund authorization cockpit example .
1616 *
17- * The LangGraph backend pauses execution when it needs human approval.
18- * The `stream.interrupt()` signal provides the interrupt data, and
19- * `stream.submit({ resume })` resumes execution with the human's decision .
17+ * The LangGraph backend acknowledges the refund draft, then pauses at
18+ * `request_approval` with a structured interrupt payload of the form
19+ * `{ kind: 'refund_approval', amount, customer_id, reason }` .
2020 *
21- * Key integration points:
22- * - `stream.interrupt()` — current pause data (undefined when not interrupted)
23- * - `ChatInterruptPanelComponent` — renders the approval UI with action buttons
24- * - `stream.submit({ resume })` — resumes the graph with a payload
21+ * The frontend uses `ChatApprovalCardComponent` to render the native-dialog
22+ * modal and emit a `ChatApprovalAction` ('approve' | 'edit' | 'cancel').
23+ * The handler maps each action to a structured resume payload back to the
24+ * graph.
2525 */
2626@Component ( {
2727 selector : 'app-interrupts' ,
2828 standalone : true ,
29- imports : [ ChatComponent , ChatInterruptPanelComponent , ChatWelcomeSuggestionComponent , ExampleChatLayoutComponent ] ,
29+ imports : [
30+ ChatComponent ,
31+ ChatApprovalCardComponent ,
32+ ChatWelcomeSuggestionComponent ,
33+ ExampleChatLayoutComponent ,
34+ CurrencyPipe ,
35+ ] ,
36+ changeDetection : ChangeDetectionStrategy . OnPush ,
3037 template : `
3138 <example-chat-layout>
3239 <div main class="flex flex-col h-full">
33- <chat [agent]="agent" [views]="ui" [store]="uiStore" class="flex-1 min-w-0">
40+ <chat [agent]="agent" class="flex-1 min-w-0">
3441 <div chatWelcomeSuggestions>
3542 @for (s of suggestions; track s.value) {
3643 <chat-welcome-suggestion
@@ -41,48 +48,69 @@ const WELCOME_SUGGESTIONS = [
4148 }
4249 </div>
4350 </chat>
44- @if (agent.interrupt()) {
45- <div class="p-4" style="border-top: 1px solid var(--ngaf-chat-separator);">
46- <chat-interrupt-panel [agent]="agent" (action)="onInterruptAction($event)" />
47- </div>
48- }
51+
52+ <chat-approval-card
53+ [agent]="agent"
54+ matchKind="refund_approval"
55+ title="Refund approval required"
56+ [showEdit]="true"
57+ (action)="onAction($event)"
58+ >
59+ <ng-template #body let-payload>
60+ <div style="display:flex; flex-direction:column; gap:6px;">
61+ <div><span style="color:var(--ngaf-chat-text-muted); margin-right:6px;">Amount</span><strong>{{ payload.amount | currency }}</strong></div>
62+ <div><span style="color:var(--ngaf-chat-text-muted); margin-right:6px;">Customer</span><code>{{ payload.customer_id }}</code></div>
63+ @if (payload.reason) {
64+ <div style="font-style:italic; color:var(--ngaf-chat-text-muted); margin-top:4px;">{{ payload.reason }}</div>
65+ }
66+ @if (editing()) {
67+ <div style="margin-top:10px; display:flex; gap:6px; align-items:center;">
68+ <label style="color:var(--ngaf-chat-text-muted); font-size:12px;">Edit amount</label>
69+ <input type="number" step="0.01" [value]="editAmount() ?? payload.amount" (input)="editAmount.set(+($any($event.target).value))" style="padding:4px 8px; border:1px solid var(--ngaf-chat-separator); border-radius:6px; width:120px;" />
70+ <button type="button" (click)="submitEdit(payload)" style="padding:4px 10px; background:var(--ngaf-chat-primary); color:var(--ngaf-chat-on-primary); border:0; border-radius:6px; font-size:12px; cursor:pointer;">Save</button>
71+ </div>
72+ }
73+ </div>
74+ </ng-template>
75+ </chat-approval-card>
4976 </div>
5077 </example-chat-layout>
5178 ` ,
5279} )
5380export class InterruptsComponent {
54- readonly ui = views ( { 'approval-card' : ApprovalCardComponent } ) ;
55- readonly uiStore = signalStateStore ( { } ) ;
5681 protected readonly suggestions = WELCOME_SUGGESTIONS ;
82+ protected readonly editing = signal ( false ) ;
83+ protected readonly editAmount = signal < number | null > ( null ) ;
5784
58- protected send ( text : string ) : void {
59- void this . agent . submit ( { message : text } ) ;
60- }
61-
62- /**
63- * The streaming resource with interrupt support.
64- *
65- * When the LangGraph backend calls `interrupt()`, the `stream.interrupt()`
66- * signal emits the interrupt payload for display via ChatInterruptPanelComponent.
67- */
6885 protected readonly agent = agent ( {
6986 apiUrl : environment . langGraphApiUrl ,
7087 assistantId : environment . streamingAssistantId ,
7188 } ) ;
7289
73- /**
74- * Handle an interrupt action from the panel.
75- *
76- * Submitting a resume payload continues the graph.
77- *
78- * In a production app, 'edit' would let the user modify the response
79- * before approval, and 'respond' would send a reply payload.
80- * For this demo, all actions simply resume the graph.
81- */
82- protected onInterruptAction ( action : InterruptAction ) : void {
83- // In a production app, 'edit' would let the user modify the response before approval.
84- // For this demo, all actions simply resume the graph.
85- void action ; // Each branch intentionally does the same thing in this demo
86- void this . agent . submit ( { resume : true } ) ;
90+ protected send ( text : string ) : void {
91+ void this . agent . submit ( { message : text } ) ;
92+ }
93+
94+ protected onAction ( action : ChatApprovalAction ) : void {
95+ if ( action === 'approve' ) {
96+ void this . agent . submit ( { resume : { approved : true } } ) ;
97+ this . resetEdit ( ) ;
98+ } else if ( action === 'cancel' ) {
99+ void this . agent . submit ( { resume : { approved : false } } ) ;
100+ this . resetEdit ( ) ;
101+ } else if ( action === 'edit' ) {
102+ this . editing . set ( true ) ;
103+ }
104+ }
105+
106+ protected submitEdit ( payload : { amount : number } ) : void {
107+ const next = this . editAmount ( ) ?? payload . amount ;
108+ void this . agent . submit ( { resume : { approved : true , amount : next } } ) ;
109+ this . resetEdit ( ) ;
110+ }
111+
112+ private resetEdit ( ) : void {
113+ this . editing . set ( false ) ;
114+ this . editAmount . set ( null ) ;
87115 }
88116}
0 commit comments