Skip to content

Commit a77903a

Browse files
bloveclaude
andauthored
feat(examples-chat): Phase 6 — timeline / time travel (#238)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6d9a161 commit a77903a

7 files changed

Lines changed: 87 additions & 2 deletions

File tree

examples/chat/angular/src/app/shell/control-palette.component.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@
8080
<span>Debug {{ debugOpen() ? 'on' : 'off' }}</span>
8181
</button>
8282

83+
<button
84+
type="button"
85+
class="palette__toggle"
86+
[class.is-on]="timelineOpen()"
87+
[attr.aria-pressed]="timelineOpen()"
88+
(click)="toggleTimeline()"
89+
>
90+
<span class="palette__toggle-dot"></span>
91+
<span>Timeline {{ timelineOpen() ? 'on' : 'off' }}</span>
92+
</button>
93+
8394
<button
8495
type="button"
8596
class="palette__action"

examples/chat/angular/src/app/shell/control-palette.component.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@ export class ControlPalette {
3131
readonly theme = input.required<string>();
3232
readonly themeOptions = input.required<readonly { value: string; label: string }[]>();
3333
readonly debugOpen = input.required<boolean>();
34+
readonly timelineOpen = input.required<boolean>();
3435

3536
readonly modeChange = output<DemoMode>();
3637
readonly modelChange = output<string>();
3738
readonly effortChange = output<string>();
3839
readonly genUiModeChange = output<string>();
3940
readonly themeChange = output<string>();
4041
readonly debugOpenChange = output<boolean>();
42+
readonly timelineOpenChange = output<boolean>();
4143
readonly newConversation = output<void>();
4244

4345
protected readonly collapsed = signal<boolean>(this.persistence.read('collapsed') ?? false);
@@ -80,6 +82,10 @@ export class ControlPalette {
8082
this.debugOpenChange.emit(!this.debugOpen());
8183
}
8284

85+
protected toggleTimeline(): void {
86+
this.timelineOpenChange.emit(!this.timelineOpen());
87+
}
88+
8389
protected emitNewConversation(): void {
8490
this.newConversation.emit();
8591
}

examples/chat/angular/src/app/shell/demo-shell.component.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,17 @@
4646
flex-direction: column;
4747
gap: 8px;
4848
}
49+
50+
.demo-shell__timeline-panel {
51+
position: fixed;
52+
right: 16px;
53+
top: 80px;
54+
bottom: 96px;
55+
width: 280px;
56+
background: #1a1d23;
57+
border: 1px solid #303540;
58+
border-radius: 10px;
59+
overflow-y: auto;
60+
z-index: 996;
61+
padding: 8px 0;
62+
}

examples/chat/angular/src/app/shell/demo-shell.component.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
[theme]="theme()"
1313
[themeOptions]="themeOptions()"
1414
[debugOpen]="debugOpen()"
15+
[timelineOpen]="timelineOpen()"
1516
(modeChange)="onModeChange($event)"
1617
(modelChange)="onModelChange($event)"
1718
(effortChange)="onEffortChange($event)"
1819
(genUiModeChange)="onGenUiModeChange($event)"
1920
(themeChange)="onThemeChange($event)"
2021
(debugOpenChange)="onDebugChange($event)"
22+
(timelineOpenChange)="onTimelineChange($event)"
2123
(newConversation)="onNewConversation()"
2224
/>
2325

@@ -38,4 +40,14 @@
3840
<chat-debug [agent]="agent" />
3941
</div>
4042
}
43+
44+
@if (timelineOpen()) {
45+
<div class="demo-shell__timeline-panel" role="region" aria-label="Conversation timeline">
46+
<chat-timeline-slider
47+
[agent]="agent"
48+
(replayRequested)="onTimelineReplay($event)"
49+
(forkRequested)="onTimelineFork($event)"
50+
/>
51+
</div>
52+
}
4153
</div>

examples/chat/angular/src/app/shell/demo-shell.component.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { Router, RouterOutlet, NavigationEnd } from '@angular/router';
1111
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
1212
import { filter, map, startWith } from 'rxjs/operators';
1313
import { agent } from '@ngaf/langgraph';
14-
import { ChatDebugComponent, ChatInterruptPanelComponent, ChatSubagentsComponent, type InterruptAction } from '@ngaf/chat';
14+
import { ChatDebugComponent, ChatInterruptPanelComponent, ChatSubagentsComponent, ChatTimelineSliderComponent, type InterruptAction } from '@ngaf/chat';
1515
import { ControlPalette } from './control-palette.component';
1616
import { PalettePersistence } from './palette-persistence.service';
1717
import { DEMO_AGENT } from './shell-tokens';
@@ -28,7 +28,7 @@ function modeFromUrl(url: string): DemoMode {
2828
@Component({
2929
selector: 'demo-shell',
3030
standalone: true,
31-
imports: [RouterOutlet, ControlPalette, ChatDebugComponent, ChatInterruptPanelComponent, ChatSubagentsComponent],
31+
imports: [RouterOutlet, ControlPalette, ChatDebugComponent, ChatInterruptPanelComponent, ChatSubagentsComponent, ChatTimelineSliderComponent],
3232
changeDetection: ChangeDetectionStrategy.OnPush,
3333
templateUrl: './demo-shell.component.html',
3434
styleUrl: './demo-shell.component.css',
@@ -85,6 +85,8 @@ export class DemoShell {
8585

8686
protected readonly debugOpen = signal<boolean>(this.persistence.read('debug') ?? false);
8787

88+
protected readonly timelineOpen = signal<boolean>(this.persistence.read('timeline') ?? false);
89+
8890
protected readonly modelOptions = signal<readonly { value: string; label: string }[]>([
8991
{ value: 'gpt-5', label: 'gpt-5' },
9092
{ value: 'gpt-5-mini', label: 'gpt-5-mini' },
@@ -181,6 +183,29 @@ export class DemoShell {
181183
this.persistence.write('debug', next);
182184
}
183185

186+
protected onTimelineChange(next: boolean): void {
187+
this.timelineOpen.set(next);
188+
this.persistence.write('timeline', next);
189+
}
190+
191+
protected onTimelineReplay(checkpointId: string): void {
192+
void this.agent.submit(null as never, { checkpointId } as never);
193+
}
194+
195+
protected async onTimelineFork(checkpointId: string): Promise<void> {
196+
await fetch('http://localhost:2024/threads', {
197+
method: 'POST',
198+
headers: { 'Content-Type': 'application/json' },
199+
body: '{}',
200+
})
201+
.then((r) => r.json())
202+
.then((t: { thread_id: string }) => {
203+
this.threadIdSignal.set(t.thread_id);
204+
this.persistence.write('threadId', t.thread_id);
205+
void this.agent.submit(null as never, { checkpointId } as never);
206+
});
207+
}
208+
184209
/**
185210
* Clear persisted thread id and drop the signal. The next submit
186211
* causes the SDK to create a fresh thread server-side; onThreadId

examples/chat/angular/src/app/shell/palette-persistence.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface PaletteState {
1111
debug?: boolean | null;
1212
threadId?: string | null;
1313
collapsed?: boolean | null;
14+
timeline?: boolean | null;
1415
}
1516

1617
type PaletteKey = keyof PaletteState;

examples/chat/smoke/CHECKLIST.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,4 +291,20 @@ Components NOT yet exercised by the demo (deferred to future media-focused sugge
291291

292292
## Time travel / timeline
293293

294+
- [ ] Palette shows "Timeline off" toggle button (next to "Debug off")
295+
- [ ] Click "Timeline off" — button label changes to "Timeline on"; timeline panel appears on the right side of the screen
296+
- [ ] Timeline panel shows `<chat-timeline-slider>` with a "Timeline" heading and checkpoint count
297+
- [ ] Click "Timeline on" — panel unmounts; no console errors; DOM has no `<chat-timeline-slider>` element
298+
- [ ] Timeline open/closed state persists across page reload (stored under `timeline` key in localStorage)
299+
- [ ] Send several messages — each creates a checkpoint listed in the slider (count increments)
300+
- [ ] Each checkpoint entry shows a numbered index pill, a label ("Step N" or step name), and the checkpoint id
301+
- [ ] Hovering a checkpoint entry highlights it (subtle background)
302+
- [ ] Click "Replay" on a checkpoint — agent re-runs from that point with no new input; message list reflects the replayed history
303+
- [ ] After replay: server-side `curl localhost:2024/threads/<id>/state` shows the correct checkpoint was used
304+
- [ ] Click "Fork" on a checkpoint — a new thread is created server-side; agent switches to the new thread and re-runs from that checkpoint
305+
- [ ] After fork: `threadId` in localStorage has changed to the new thread id
306+
- [ ] After fork: the conversation reflects the forked state, not the original thread's later messages
307+
- [ ] Timeline panel scrolls independently when the checkpoint list is taller than the panel
308+
- [ ] Timeline panel does not obscure the chat input or send button at any supported viewport width
309+
294310
## Multi-thread

0 commit comments

Comments
 (0)