Skip to content

Commit cd22975

Browse files
committed
fix(viewer): show full hotkey labels and add live auto-follow toggle
- LiveControlBar legend was truncating labels via split(' ')[0], cutting 'Using AI' to 'Using' and 'Trial & error' to 'Trial'. Show full labels. - Add 'Follow' button next to zoom controls (live sessions only): when on, the zoomed window slides right as new events arrive, preserving width. Disabled automatically on manual pan/zoom-in/zoom-out so the user can freely inspect history. On by default; resets per session.
1 parent 6ee30ee commit cd22975

3 files changed

Lines changed: 49 additions & 2 deletions

File tree

recording-viewer/src/App.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1722,6 +1722,17 @@
17221722
font-size: 12px;
17231723
font-weight: 400;
17241724
}
1725+
.zoom-btn.follow {
1726+
width: auto;
1727+
padding: 0 10px;
1728+
font-size: 12px;
1729+
font-weight: 500;
1730+
}
1731+
.zoom-btn.follow.active {
1732+
background: rgba(239, 68, 68, 0.15);
1733+
color: #ef4444;
1734+
border-color: rgba(239, 68, 68, 0.3);
1735+
}
17251736

17261737
/* ── Follow playback button ─────────────────────────────────────────── */
17271738

recording-viewer/src/App.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export function RecordingViewerApp({ authStatus }: RecordingViewerAppProps) {
136136
setIsVideoPlaying(false);
137137
videoTimeRef.current = 0;
138138
setZoomedXDomain(null);
139+
setAutoFollowLive(true);
139140
const firstSessionStart = events.find(e => e.type === 'sessionStart') as SessionStartEvent | undefined;
140141
const schemaVersion = resolveSchemaVersion(metadata, firstSessionStart);
141142
setSession({ metadata, events, fileName: sessionId, schemaVersion, replayEq, annotations: loadedAnnotations });
@@ -203,6 +204,7 @@ export function RecordingViewerApp({ authStatus }: RecordingViewerAppProps) {
203204
setViewMode('timeline');
204205
setScrollToTimestamp(null);
205206
setZoomedXDomain(null);
207+
setAutoFollowLive(true);
206208
setStickyLive(false);
207209
setEndedLiveSessionId(null);
208210
setSession(loaded);
@@ -218,6 +220,7 @@ export function RecordingViewerApp({ authStatus }: RecordingViewerAppProps) {
218220
setViewMode('timeline');
219221
setScrollToTimestamp(null);
220222
setZoomedXDomain(null);
223+
setAutoFollowLive(true);
221224
setSession(null);
222225
}, []);
223226

@@ -281,6 +284,7 @@ export function RecordingViewerApp({ authStatus }: RecordingViewerAppProps) {
281284
const [viewMode, setViewMode] = useState<'timeline' | 'list'>('timeline');
282285
const [scrollToTimestamp, setScrollToTimestamp] = useState<number | null>(null);
283286
const [zoomedXDomain, setZoomedXDomain] = useState<[number, number] | null>(null);
287+
const [autoFollowLive, setAutoFollowLive] = useState(true);
284288

285289
const handleViewInList = useCallback((timestamp: number) => {
286290
setScrollToTimestamp(timestamp);
@@ -328,8 +332,29 @@ export function RecordingViewerApp({ authStatus }: RecordingViewerAppProps) {
328332

329333
const effectiveXDomain = zoomedXDomain ?? xDomain;
330334

335+
// Slide the zoomed window right when new live events arrive, preserving width.
336+
// Only active when isLiveSession + autoFollowLive + currently zoomed in.
337+
useEffect(() => {
338+
if (!autoFollowLive || !isLiveSession || !zoomedXDomain || !xDomain) return;
339+
if (xDomain[1] <= zoomedXDomain[1]) return;
340+
const range = zoomedXDomain[1] - zoomedXDomain[0];
341+
setZoomedXDomain([xDomain[1] - range, xDomain[1]]);
342+
}, [autoFollowLive, isLiveSession, xDomain, zoomedXDomain]);
343+
344+
const handleToggleAutoFollow = useCallback(() => {
345+
setAutoFollowLive((prev) => {
346+
const next = !prev;
347+
if (next && zoomedXDomain && xDomain) {
348+
const range = zoomedXDomain[1] - zoomedXDomain[0];
349+
setZoomedXDomain([xDomain[1] - range, xDomain[1]]);
350+
}
351+
return next;
352+
});
353+
}, [xDomain, zoomedXDomain]);
354+
331355
const handleZoomChange = useCallback((domain: [number, number] | null) => {
332356
setZoomedXDomain(domain);
357+
setAutoFollowLive(false);
333358
}, []);
334359

335360
const handleZoomIn = useCallback(() => {
@@ -341,6 +366,7 @@ export function RecordingViewerApp({ authStatus }: RecordingViewerAppProps) {
341366
if (newRange < 2000) return;
342367
const center = (min + max) / 2;
343368
setZoomedXDomain([center - newRange / 2, center + newRange / 2]);
369+
setAutoFollowLive(false);
344370
}, [xDomain, zoomedXDomain]);
345371

346372
const handleZoomOut = useCallback(() => {
@@ -354,6 +380,7 @@ export function RecordingViewerApp({ authStatus }: RecordingViewerAppProps) {
354380
setZoomedXDomain(null);
355381
return;
356382
}
383+
setAutoFollowLive(false);
357384
let newMin = (min + max) / 2 - newRange / 2;
358385
let newMax = (min + max) / 2 + newRange / 2;
359386
if (newMin < xDomain[0]) { newMin = xDomain[0]; newMax = newMin + newRange; }
@@ -480,6 +507,15 @@ export function RecordingViewerApp({ authStatus }: RecordingViewerAppProps) {
480507
{zoomedXDomain && (
481508
<button className="zoom-btn reset" onClick={() => setZoomedXDomain(null)} title="Reset zoom">Reset</button>
482509
)}
510+
{isLiveSession && (
511+
<button
512+
className={`zoom-btn follow ${autoFollowLive ? 'active' : ''}`}
513+
onClick={handleToggleAutoFollow}
514+
title={autoFollowLive ? 'Auto-follow latest events (on)' : 'Auto-follow latest events (off)'}
515+
>
516+
Follow
517+
</button>
518+
)}
483519
</div>
484520
)}
485521
{!writesDisabled && (

recording-viewer/src/components/LiveControlBar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ export function LiveControlBar({
4949
<div className="live-legend">
5050
<strong>Struggle:</strong>
5151
{STRUGGLE_LABELS.map((s, i) => (
52-
<span key={s.value} style={{ color: s.color }}>{i + 1}={s.label.split(' ')[0]}</span>
52+
<span key={s.value} style={{ color: s.color }}>{i + 1}={s.label}</span>
5353
))}
5454
<strong>Context:</strong>
5555
{CONTEXT_LABELS.map((s, i) => (
56-
<span key={s.value} style={{ color: s.color }}>{'qwert'[i]}={s.label.split(' ')[0]}</span>
56+
<span key={s.value} style={{ color: s.color }}>{'qwert'[i]}={s.label}</span>
5757
))}
5858
</div>
5959
{toastVisible && (

0 commit comments

Comments
 (0)