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
11 changes: 11 additions & 0 deletions examples/chat/angular/src/app/shell/control-palette.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@
<span>Debug {{ debugOpen() ? 'on' : 'off' }}</span>
</button>

<button
type="button"
class="palette__toggle"
[class.is-on]="timelineOpen()"
[attr.aria-pressed]="timelineOpen()"
(click)="toggleTimeline()"
>
<span class="palette__toggle-dot"></span>
<span>Timeline {{ timelineOpen() ? 'on' : 'off' }}</span>
</button>

<button
type="button"
class="palette__action"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ export class ControlPalette {
readonly theme = input.required<string>();
readonly themeOptions = input.required<readonly { value: string; label: string }[]>();
readonly debugOpen = input.required<boolean>();
readonly timelineOpen = input.required<boolean>();

readonly modeChange = output<DemoMode>();
readonly modelChange = output<string>();
readonly effortChange = output<string>();
readonly genUiModeChange = output<string>();
readonly themeChange = output<string>();
readonly debugOpenChange = output<boolean>();
readonly timelineOpenChange = output<boolean>();
readonly newConversation = output<void>();

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

protected toggleTimeline(): void {
this.timelineOpenChange.emit(!this.timelineOpen());
}

protected emitNewConversation(): void {
this.newConversation.emit();
}
Expand Down
14 changes: 14 additions & 0 deletions examples/chat/angular/src/app/shell/demo-shell.component.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,17 @@
flex-direction: column;
gap: 8px;
}

.demo-shell__timeline-panel {
position: fixed;
right: 16px;
top: 80px;
bottom: 96px;
width: 280px;
background: #1a1d23;
border: 1px solid #303540;
border-radius: 10px;
overflow-y: auto;
z-index: 996;
padding: 8px 0;
}
12 changes: 12 additions & 0 deletions examples/chat/angular/src/app/shell/demo-shell.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
[theme]="theme()"
[themeOptions]="themeOptions()"
[debugOpen]="debugOpen()"
[timelineOpen]="timelineOpen()"
(modeChange)="onModeChange($event)"
(modelChange)="onModelChange($event)"
(effortChange)="onEffortChange($event)"
(genUiModeChange)="onGenUiModeChange($event)"
(themeChange)="onThemeChange($event)"
(debugOpenChange)="onDebugChange($event)"
(timelineOpenChange)="onTimelineChange($event)"
(newConversation)="onNewConversation()"
/>

Expand All @@ -38,4 +40,14 @@
<chat-debug [agent]="agent" />
</div>
}

@if (timelineOpen()) {
<div class="demo-shell__timeline-panel" role="region" aria-label="Conversation timeline">
<chat-timeline-slider
[agent]="agent"
(replayRequested)="onTimelineReplay($event)"
(forkRequested)="onTimelineFork($event)"
/>
</div>
}
</div>
29 changes: 27 additions & 2 deletions examples/chat/angular/src/app/shell/demo-shell.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Router, RouterOutlet, NavigationEnd } from '@angular/router';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { filter, map, startWith } from 'rxjs/operators';
import { agent } from '@ngaf/langgraph';
import { ChatDebugComponent, ChatInterruptPanelComponent, ChatSubagentsComponent, type InterruptAction } from '@ngaf/chat';
import { ChatDebugComponent, ChatInterruptPanelComponent, ChatSubagentsComponent, ChatTimelineSliderComponent, type InterruptAction } from '@ngaf/chat';
import { ControlPalette } from './control-palette.component';
import { PalettePersistence } from './palette-persistence.service';
import { DEMO_AGENT } from './shell-tokens';
Expand All @@ -28,7 +28,7 @@ function modeFromUrl(url: string): DemoMode {
@Component({
selector: 'demo-shell',
standalone: true,
imports: [RouterOutlet, ControlPalette, ChatDebugComponent, ChatInterruptPanelComponent, ChatSubagentsComponent],
imports: [RouterOutlet, ControlPalette, ChatDebugComponent, ChatInterruptPanelComponent, ChatSubagentsComponent, ChatTimelineSliderComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './demo-shell.component.html',
styleUrl: './demo-shell.component.css',
Expand Down Expand Up @@ -85,6 +85,8 @@ export class DemoShell {

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

protected readonly timelineOpen = signal<boolean>(this.persistence.read('timeline') ?? false);

protected readonly modelOptions = signal<readonly { value: string; label: string }[]>([
{ value: 'gpt-5', label: 'gpt-5' },
{ value: 'gpt-5-mini', label: 'gpt-5-mini' },
Expand Down Expand Up @@ -181,6 +183,29 @@ export class DemoShell {
this.persistence.write('debug', next);
}

protected onTimelineChange(next: boolean): void {
this.timelineOpen.set(next);
this.persistence.write('timeline', next);
}

protected onTimelineReplay(checkpointId: string): void {
void this.agent.submit(null as never, { checkpointId } as never);
}

protected async onTimelineFork(checkpointId: string): Promise<void> {
await fetch('http://localhost:2024/threads', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}',
})
.then((r) => r.json())
.then((t: { thread_id: string }) => {
this.threadIdSignal.set(t.thread_id);
this.persistence.write('threadId', t.thread_id);
void this.agent.submit(null as never, { checkpointId } as never);
});
}

/**
* Clear persisted thread id and drop the signal. The next submit
* causes the SDK to create a fresh thread server-side; onThreadId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface PaletteState {
debug?: boolean | null;
threadId?: string | null;
collapsed?: boolean | null;
timeline?: boolean | null;
}

type PaletteKey = keyof PaletteState;
Expand Down
16 changes: 16 additions & 0 deletions examples/chat/smoke/CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,4 +291,20 @@ Components NOT yet exercised by the demo (deferred to future media-focused sugge

## Time travel / timeline

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

## Multi-thread
Loading