|
1 | | -import { Component } from '@angular/core'; |
2 | | -import { LegacyChatComponent } from '@cacheplane/chat'; |
| 1 | +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 |
| 2 | +import { Component, computed } from '@angular/core'; |
| 3 | +import { ChatComponent } from '@cacheplane/chat'; |
3 | 4 | import { streamResource } from '@cacheplane/stream-resource'; |
4 | 5 | import { environment } from '../environments/environment'; |
5 | 6 |
|
6 | | -/** |
7 | | - * DurableExecutionComponent demonstrates fault-tolerant multi-step execution |
8 | | - * with `streamResource()`. |
9 | | - * |
10 | | - * This example shows how a graph checkpoints at each node, enabling it to |
11 | | - * resume after failures. The sidebar shows execution status in real time: |
12 | | - * - `stream.status()` as a badge (idle/loading/resolved/error) |
13 | | - * - `stream.hasValue()` indicator for received data |
14 | | - * - A "Retry" button that calls `stream.reload()` when `stream.error()` is set |
15 | | - * |
16 | | - * The backend processes each request through three nodes: |
17 | | - * analyze → plan → generate |
18 | | - * Each node updates `state.step` so the UI can track progress. |
19 | | - */ |
20 | 7 | @Component({ |
21 | 8 | selector: 'app-durable-execution', |
22 | 9 | standalone: true, |
23 | | - imports: [LegacyChatComponent], |
| 10 | + imports: [ChatComponent], |
24 | 11 | template: ` |
25 | | - <cp-chat |
26 | | - [messages]="stream.messages()" |
27 | | - [isLoading]="stream.isLoading()" |
28 | | - [error]="stream.error()" |
29 | | - (sendMessage)="send($event)"> |
30 | | - <ng-template #sidebar> |
31 | | - <h3 style="font-size: 0.8rem; font-weight: 600; margin-bottom: 0.75rem; color: #1a1a2e;">Execution Status</h3> |
32 | | -
|
33 | | - <div style="margin-bottom: 0.75rem;"> |
34 | | - <span style="font-size: 0.7rem; font-weight: 500; color: #555770; text-transform: uppercase; letter-spacing: 0.05em;">Status</span> |
35 | | - <div style="margin-top: 4px;"> |
36 | | - <span [style.background]="statusBadgeColor()" style="display: inline-block; padding: 3px 8px; border-radius: 10px; font-size: 0.72rem; font-weight: 600; color: #fff; font-family: monospace;"> |
37 | | - {{ stream.status() }} |
38 | | - </span> |
39 | | - </div> |
40 | | - </div> |
41 | | -
|
42 | | - <div style="margin-bottom: 0.75rem;"> |
43 | | - <span style="font-size: 0.7rem; font-weight: 500; color: #555770; text-transform: uppercase; letter-spacing: 0.05em;">Data Received</span> |
44 | | - <div style="margin-top: 4px; display: flex; align-items: center; gap: 6px;"> |
45 | | - <span [style.background]="stream.hasValue() ? '#22c55e' : '#d1d5db'" |
46 | | - style="display: inline-block; width: 10px; height: 10px; border-radius: 50%;"></span> |
47 | | - <span style="font-size: 0.8rem; color: #1a1a2e;">{{ stream.hasValue() ? 'Yes' : 'No' }}</span> |
48 | | - </div> |
| 12 | + <div class="flex flex-col h-screen"> |
| 13 | + <!-- Status bar --> |
| 14 | + <div class="flex items-center gap-4 px-5 py-3 border-b" |
| 15 | + style="border-color: var(--chat-border, #333); background: var(--chat-bg-alt, #222);"> |
| 16 | + <!-- Step pipeline --> |
| 17 | + <div class="flex items-center gap-2 text-xs"> |
| 18 | + @for (step of steps; track step) { |
| 19 | + <div class="flex items-center gap-1"> |
| 20 | + <span class="w-2 h-2 rounded-full" |
| 21 | + [style.background]="currentStep() === step ? 'var(--chat-warning-text, #fbbf24)' : isStepComplete(step) ? 'var(--chat-success, #4ade80)' : 'var(--chat-text-muted, #777)'"> |
| 22 | + </span> |
| 23 | + <span [style.color]="currentStep() === step ? 'var(--chat-text, #e0e0e0)' : 'var(--chat-text-muted, #777)'" |
| 24 | + [style.font-weight]="currentStep() === step ? '600' : '400'"> |
| 25 | + {{ step }} |
| 26 | + </span> |
| 27 | + </div> |
| 28 | + @if (!$last) { |
| 29 | + <span style="color: var(--chat-text-muted, #777);">→</span> |
| 30 | + } |
| 31 | + } |
49 | 32 | </div> |
50 | 33 |
|
51 | | - @if (stream.error()) { |
52 | | - <div style="margin-top: 0.75rem; padding: 8px; background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.2); border-radius: 6px;"> |
53 | | - <div style="font-size: 0.72rem; color: #dc2626; margin-bottom: 6px; font-weight: 600;">Execution Failed</div> |
54 | | - <button (click)="stream.reload()" |
55 | | - style="width: 100%; padding: 6px 10px; background: #dc2626; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 0.75rem; font-weight: 600;"> |
| 34 | + <!-- Status badge --> |
| 35 | + <div class="ml-auto flex items-center gap-3 text-xs"> |
| 36 | + <span class="px-2 py-0.5 rounded-full font-medium" |
| 37 | + [style.background]="statusColor()" |
| 38 | + style="color: white;"> |
| 39 | + {{ stream.status() }} |
| 40 | + </span> |
| 41 | + @if (stream.error()) { |
| 42 | + <button class="px-2 py-1 rounded text-xs font-medium transition-colors" |
| 43 | + style="background: var(--chat-error-bg, #2d1515); color: var(--chat-error-text, #f87171);" |
| 44 | + (click)="stream.reload()"> |
56 | 45 | Retry |
57 | 46 | </button> |
58 | | - </div> |
59 | | - } |
60 | | - </ng-template> |
61 | | - </cp-chat> |
| 47 | + } |
| 48 | + </div> |
| 49 | + </div> |
| 50 | +
|
| 51 | + <!-- Chat --> |
| 52 | + <chat [ref]="stream" class="flex-1 min-w-0" /> |
| 53 | + </div> |
62 | 54 | `, |
63 | 55 | }) |
64 | 56 | export class DurableExecutionComponent { |
65 | | - /** |
66 | | - * The streaming resource backing this durable-execution demo. |
67 | | - * |
68 | | - * The graph runs three nodes (analyze → plan → generate), checkpointing |
69 | | - * after each one. If the graph fails partway through, `stream.reload()` |
70 | | - * re-submits the last input so the run can resume from the last checkpoint. |
71 | | - */ |
| 57 | + protected readonly steps = ['analyze', 'plan', 'generate']; |
| 58 | + |
72 | 59 | protected readonly stream = streamResource({ |
73 | 60 | apiUrl: environment.langGraphApiUrl, |
74 | 61 | assistantId: environment.streamingAssistantId, |
75 | 62 | }); |
76 | 63 |
|
77 | | - /** |
78 | | - * Submit a message to be processed through the multi-node graph. |
79 | | - */ |
80 | | - send(text: string): void { |
81 | | - this.stream.submit({ messages: [{ role: 'human', content: text }] }); |
| 64 | + protected readonly currentStep = computed(() => { |
| 65 | + const val = this.stream.value() as Record<string, unknown>; |
| 66 | + return (val?.['step'] as string) ?? ''; |
| 67 | + }); |
| 68 | + |
| 69 | + protected isStepComplete(step: string): boolean { |
| 70 | + const idx = this.steps.indexOf(step); |
| 71 | + const currentIdx = this.steps.indexOf(this.currentStep()); |
| 72 | + return currentIdx > idx; |
82 | 73 | } |
83 | 74 |
|
84 | | - /** |
85 | | - * Returns a colour for the status badge based on the current stream status. |
86 | | - */ |
87 | | - statusBadgeColor(): string { |
| 75 | + protected statusColor(): string { |
88 | 76 | switch (this.stream.status()) { |
89 | 77 | case 'loading': |
90 | | - case 'reloading': return '#2563eb'; |
91 | | - case 'resolved': return '#16a34a'; |
92 | | - case 'error': return '#dc2626'; |
93 | | - default: return '#6b7280'; |
| 78 | + case 'reloading': |
| 79 | + return '#2563eb'; |
| 80 | + case 'resolved': |
| 81 | + return '#16a34a'; |
| 82 | + case 'error': |
| 83 | + return '#dc2626'; |
| 84 | + default: |
| 85 | + return '#6b7280'; |
94 | 86 | } |
95 | 87 | } |
96 | 88 | } |
0 commit comments