Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,96 +1,88 @@
import { Component } from '@angular/core';
import { LegacyChatComponent } from '@cacheplane/chat';
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
import { Component, computed } from '@angular/core';
import { ChatComponent } from '@cacheplane/chat';
import { streamResource } from '@cacheplane/stream-resource';
import { environment } from '../environments/environment';

/**
* DurableExecutionComponent demonstrates fault-tolerant multi-step execution
* with `streamResource()`.
*
* This example shows how a graph checkpoints at each node, enabling it to
* resume after failures. The sidebar shows execution status in real time:
* - `stream.status()` as a badge (idle/loading/resolved/error)
* - `stream.hasValue()` indicator for received data
* - A "Retry" button that calls `stream.reload()` when `stream.error()` is set
*
* The backend processes each request through three nodes:
* analyze → plan → generate
* Each node updates `state.step` so the UI can track progress.
*/
@Component({
selector: 'app-durable-execution',
standalone: true,
imports: [LegacyChatComponent],
imports: [ChatComponent],
template: `
<cp-chat
[messages]="stream.messages()"
[isLoading]="stream.isLoading()"
[error]="stream.error()"
(sendMessage)="send($event)">
<ng-template #sidebar>
<h3 style="font-size: 0.8rem; font-weight: 600; margin-bottom: 0.75rem; color: #1a1a2e;">Execution Status</h3>

<div style="margin-bottom: 0.75rem;">
<span style="font-size: 0.7rem; font-weight: 500; color: #555770; text-transform: uppercase; letter-spacing: 0.05em;">Status</span>
<div style="margin-top: 4px;">
<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;">
{{ stream.status() }}
</span>
</div>
</div>

<div style="margin-bottom: 0.75rem;">
<span style="font-size: 0.7rem; font-weight: 500; color: #555770; text-transform: uppercase; letter-spacing: 0.05em;">Data Received</span>
<div style="margin-top: 4px; display: flex; align-items: center; gap: 6px;">
<span [style.background]="stream.hasValue() ? '#22c55e' : '#d1d5db'"
style="display: inline-block; width: 10px; height: 10px; border-radius: 50%;"></span>
<span style="font-size: 0.8rem; color: #1a1a2e;">{{ stream.hasValue() ? 'Yes' : 'No' }}</span>
</div>
<div class="flex flex-col h-screen">
<!-- Status bar -->
<div class="flex items-center gap-4 px-5 py-3 border-b"
style="border-color: var(--chat-border, #333); background: var(--chat-bg-alt, #222);">
<!-- Step pipeline -->
<div class="flex items-center gap-2 text-xs">
@for (step of steps; track step) {
<div class="flex items-center gap-1">
<span class="w-2 h-2 rounded-full"
[style.background]="currentStep() === step ? 'var(--chat-warning-text, #fbbf24)' : isStepComplete(step) ? 'var(--chat-success, #4ade80)' : 'var(--chat-text-muted, #777)'">
</span>
<span [style.color]="currentStep() === step ? 'var(--chat-text, #e0e0e0)' : 'var(--chat-text-muted, #777)'"
[style.font-weight]="currentStep() === step ? '600' : '400'">
{{ step }}
</span>
</div>
@if (!$last) {
<span style="color: var(--chat-text-muted, #777);">→</span>
}
}
</div>

@if (stream.error()) {
<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;">
<div style="font-size: 0.72rem; color: #dc2626; margin-bottom: 6px; font-weight: 600;">Execution Failed</div>
<button (click)="stream.reload()"
style="width: 100%; padding: 6px 10px; background: #dc2626; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 0.75rem; font-weight: 600;">
<!-- Status badge -->
<div class="ml-auto flex items-center gap-3 text-xs">
<span class="px-2 py-0.5 rounded-full font-medium"
[style.background]="statusColor()"
style="color: white;">
{{ stream.status() }}
</span>
@if (stream.error()) {
<button class="px-2 py-1 rounded text-xs font-medium transition-colors"
style="background: var(--chat-error-bg, #2d1515); color: var(--chat-error-text, #f87171);"
(click)="stream.reload()">
Retry
</button>
</div>
}
</ng-template>
</cp-chat>
}
</div>
</div>

<!-- Chat -->
<chat [ref]="stream" class="flex-1 min-w-0" />
</div>
`,
})
export class DurableExecutionComponent {
/**
* The streaming resource backing this durable-execution demo.
*
* The graph runs three nodes (analyze → plan → generate), checkpointing
* after each one. If the graph fails partway through, `stream.reload()`
* re-submits the last input so the run can resume from the last checkpoint.
*/
protected readonly steps = ['analyze', 'plan', 'generate'];

protected readonly stream = streamResource({
apiUrl: environment.langGraphApiUrl,
assistantId: environment.streamingAssistantId,
});

/**
* Submit a message to be processed through the multi-node graph.
*/
send(text: string): void {
this.stream.submit({ messages: [{ role: 'human', content: text }] });
protected readonly currentStep = computed(() => {
const val = this.stream.value() as Record<string, unknown>;
return (val?.['step'] as string) ?? '';
});

protected isStepComplete(step: string): boolean {
const idx = this.steps.indexOf(step);
const currentIdx = this.steps.indexOf(this.currentStep());
return currentIdx > idx;
}

/**
* Returns a colour for the status badge based on the current stream status.
*/
statusBadgeColor(): string {
protected statusColor(): string {
switch (this.stream.status()) {
case 'loading':
case 'reloading': return '#2563eb';
case 'resolved': return '#16a34a';
case 'error': return '#dc2626';
default: return '#6b7280';
case 'reloading':
return '#2563eb';
case 'resolved':
return '#16a34a';
case 'error':
return '#dc2626';
default:
return '#6b7280';
}
}
}
Original file line number Diff line number Diff line change
@@ -1,84 +1,43 @@
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
import { Component } from '@angular/core';
import { LegacyChatComponent } from '@cacheplane/chat';
import { ChatComponent, ChatTimelineSliderComponent } from '@cacheplane/chat';
import { streamResource } from '@cacheplane/stream-resource';
import { environment } from '../environments/environment';

/**
* TimeTravelComponent demonstrates replaying and branching conversation history.
*
* Key integration points:
* - `stream.history()` — array of ThreadState snapshots
* - `stream.branch()` — current branch identifier
* - `stream.setBranch(id)` — switch to a different checkpoint
*/
@Component({
selector: 'app-time-travel',
standalone: true,
imports: [LegacyChatComponent],
imports: [ChatComponent, ChatTimelineSliderComponent],
template: `
<cp-chat
[messages]="stream.messages()"
[isLoading]="stream.isLoading()"
[error]="stream.error()"
(sendMessage)="send($event)">
<ng-template #sidebar>
<h3 style="font-size: 0.8rem; font-weight: 600; margin-bottom: 0.75rem; color: #1a1a2e;">History</h3>
@for (state of stream.history(); track $index) {
<button
(click)="selectCheckpoint(state)"
[style.color]="state.checkpoint_id === stream.branch() ? '#004090' : '#555770'"
[style.background]="state.checkpoint_id === stream.branch() ? 'rgba(0,64,144,0.06)' : 'transparent'"
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;">
{{ formatCheckpoint(state) }}
</button>
}
@if (stream.history().length === 0) {
<p style="font-size: 0.75rem; color: #888; margin: 0;">No history yet. Send a message to begin.</p>
}
</ng-template>
</cp-chat>
<div class="flex h-screen">
<chat [ref]="stream" class="flex-1 min-w-0" />
<aside class="w-80 shrink-0 border-l overflow-y-auto"
style="border-color: var(--chat-border, #333); background: var(--chat-bg, #171717);">
<div class="p-4">
<h3 class="text-xs font-semibold uppercase tracking-wide mb-3"
style="color: var(--chat-text-muted, #777);">Time Travel</h3>
<chat-timeline-slider
[ref]="stream"
(replayRequested)="onReplay($event)"
(forkRequested)="onFork($event)"
/>
</div>
</aside>
</div>
`,
})
export class TimeTravelComponent {
/**
* The streaming resource with checkpointing enabled.
*
* `stream.history()` provides an array of ThreadState snapshots for
* the current thread. `stream.branch()` tracks the active checkpoint.
* Call `stream.setBranch(checkpointId)` to replay from a past state.
*/
protected readonly stream = streamResource({
apiUrl: environment.langGraphApiUrl,
assistantId: environment.streamingAssistantId,
});

/**
* Submit a message to the current thread.
*/
send(text: string): void {
this.stream.submit({ messages: [{ role: 'human', content: text }] });
protected onReplay(checkpointId: string): void {
this.stream.setBranch(checkpointId);
}

/**
* Branch the conversation from the selected checkpoint.
* After calling setBranch, the next submit will fork from that point.
*/
selectCheckpoint(state: { checkpoint_id?: string }): void {
if (state.checkpoint_id) {
this.stream.setBranch(state.checkpoint_id);
}
}

/**
* Format a checkpoint for display in the sidebar.
*/
formatCheckpoint(state: { checkpoint_id?: string; created_at?: string }): string {
const id = state.checkpoint_id ?? 'unknown';
const short = id.substring(0, 8);
if (state.created_at) {
const ts = new Date(state.created_at).toLocaleTimeString();
return `${short}... @ ${ts}`;
}
return `${short}...`;
protected onFork(checkpointId: string): void {
this.stream.setBranch(checkpointId);
// Fork: set branch, then next submit creates a new branch from this point
}
}
Loading