Skip to content

Commit 918b4a3

Browse files
Brian Loveblove
authored andcommitted
feat(cockpit): Tier 3 — time-travel checkpoint nav + durable-execution step pipeline
Replace LegacyChatComponent with ChatComponent + ChatTimelineSliderComponent in time-travel, and ChatComponent with a status bar pipeline in durable-execution.
1 parent c303ab9 commit 918b4a3

2 files changed

Lines changed: 85 additions & 134 deletions

File tree

Lines changed: 62 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,88 @@
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';
34
import { streamResource } from '@cacheplane/stream-resource';
45
import { environment } from '../environments/environment';
56

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-
*/
207
@Component({
218
selector: 'app-durable-execution',
229
standalone: true,
23-
imports: [LegacyChatComponent],
10+
imports: [ChatComponent],
2411
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+
}
4932
</div>
5033
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()">
5645
Retry
5746
</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>
6254
`,
6355
})
6456
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+
7259
protected readonly stream = streamResource({
7360
apiUrl: environment.langGraphApiUrl,
7461
assistantId: environment.streamingAssistantId,
7562
});
7663

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;
8273
}
8374

84-
/**
85-
* Returns a colour for the status badge based on the current stream status.
86-
*/
87-
statusBadgeColor(): string {
75+
protected statusColor(): string {
8876
switch (this.stream.status()) {
8977
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';
9486
}
9587
}
9688
}
Lines changed: 23 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,43 @@
1+
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
12
import { Component } from '@angular/core';
2-
import { LegacyChatComponent } from '@cacheplane/chat';
3+
import { ChatComponent, ChatTimelineSliderComponent } from '@cacheplane/chat';
34
import { streamResource } from '@cacheplane/stream-resource';
45
import { environment } from '../environments/environment';
56

6-
/**
7-
* TimeTravelComponent demonstrates replaying and branching conversation history.
8-
*
9-
* Key integration points:
10-
* - `stream.history()` — array of ThreadState snapshots
11-
* - `stream.branch()` — current branch identifier
12-
* - `stream.setBranch(id)` — switch to a different checkpoint
13-
*/
147
@Component({
158
selector: 'app-time-travel',
169
standalone: true,
17-
imports: [LegacyChatComponent],
10+
imports: [ChatComponent, ChatTimelineSliderComponent],
1811
template: `
19-
<cp-chat
20-
[messages]="stream.messages()"
21-
[isLoading]="stream.isLoading()"
22-
[error]="stream.error()"
23-
(sendMessage)="send($event)">
24-
<ng-template #sidebar>
25-
<h3 style="font-size: 0.8rem; font-weight: 600; margin-bottom: 0.75rem; color: #1a1a2e;">History</h3>
26-
@for (state of stream.history(); track $index) {
27-
<button
28-
(click)="selectCheckpoint(state)"
29-
[style.color]="state.checkpoint_id === stream.branch() ? '#004090' : '#555770'"
30-
[style.background]="state.checkpoint_id === stream.branch() ? 'rgba(0,64,144,0.06)' : 'transparent'"
31-
style="display: block; width: 100%; text-align: left; padding: 6px 8px; border: none; cursor: pointer; font-size: 0.75rem; border-radius: 4px; font-family: monospace; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
32-
{{ formatCheckpoint(state) }}
33-
</button>
34-
}
35-
@if (stream.history().length === 0) {
36-
<p style="font-size: 0.75rem; color: #888; margin: 0;">No history yet. Send a message to begin.</p>
37-
}
38-
</ng-template>
39-
</cp-chat>
12+
<div class="flex h-screen">
13+
<chat [ref]="stream" class="flex-1 min-w-0" />
14+
<aside class="w-80 shrink-0 border-l overflow-y-auto"
15+
style="border-color: var(--chat-border, #333); background: var(--chat-bg, #171717);">
16+
<div class="p-4">
17+
<h3 class="text-xs font-semibold uppercase tracking-wide mb-3"
18+
style="color: var(--chat-text-muted, #777);">Time Travel</h3>
19+
<chat-timeline-slider
20+
[ref]="stream"
21+
(replayRequested)="onReplay($event)"
22+
(forkRequested)="onFork($event)"
23+
/>
24+
</div>
25+
</aside>
26+
</div>
4027
`,
4128
})
4229
export class TimeTravelComponent {
43-
/**
44-
* The streaming resource with checkpointing enabled.
45-
*
46-
* `stream.history()` provides an array of ThreadState snapshots for
47-
* the current thread. `stream.branch()` tracks the active checkpoint.
48-
* Call `stream.setBranch(checkpointId)` to replay from a past state.
49-
*/
5030
protected readonly stream = streamResource({
5131
apiUrl: environment.langGraphApiUrl,
5232
assistantId: environment.streamingAssistantId,
5333
});
5434

55-
/**
56-
* Submit a message to the current thread.
57-
*/
58-
send(text: string): void {
59-
this.stream.submit({ messages: [{ role: 'human', content: text }] });
35+
protected onReplay(checkpointId: string): void {
36+
this.stream.setBranch(checkpointId);
6037
}
6138

62-
/**
63-
* Branch the conversation from the selected checkpoint.
64-
* After calling setBranch, the next submit will fork from that point.
65-
*/
66-
selectCheckpoint(state: { checkpoint_id?: string }): void {
67-
if (state.checkpoint_id) {
68-
this.stream.setBranch(state.checkpoint_id);
69-
}
70-
}
71-
72-
/**
73-
* Format a checkpoint for display in the sidebar.
74-
*/
75-
formatCheckpoint(state: { checkpoint_id?: string; created_at?: string }): string {
76-
const id = state.checkpoint_id ?? 'unknown';
77-
const short = id.substring(0, 8);
78-
if (state.created_at) {
79-
const ts = new Date(state.created_at).toLocaleTimeString();
80-
return `${short}... @ ${ts}`;
81-
}
82-
return `${short}...`;
39+
protected onFork(checkpointId: string): void {
40+
this.stream.setBranch(checkpointId);
41+
// Fork: set branch, then next submit creates a new branch from this point
8342
}
8443
}

0 commit comments

Comments
 (0)