Date: January 17, 2026 Problem: Timeline functionality degraded in refactored benchmark viewer. Should support click-to-play-segment, visual segment indicators, and repeat functionality.
Status: Timeline functionality exists but is underutilized in benchmark viewer. The refactored benchmark_viewer.html has basic timeline clicking but lacks:
- Segment-based playback - no way to click individual task segments to play them
- Visual segment indicators - timeline doesn't show task boundaries
- Repeat/loop functionality - no auto-repeat for segments
- Keyboard shortcuts - limited navigation controls
Best implementation found: episode_timeline.js component (used in capture_viewer.html and segmentation_viewer.html) provides the foundation but needs adaptation for benchmark task-based viewing.
File: /Users/abrichr/oa/src/openadapt-viewer/benchmark_viewer.html
Timeline Features:
<div class="oa-timeline-track">
<div class="oa-timeline-progress"></div>
</div>Capabilities:
- ✅ Basic progress bar visual
- ✅ CSS styling with gradient
- ❌ No click interaction
- ❌ No segment visualization
- ❌ No task boundaries
- ❌ No playback controls
- ❌ Static implementation (no Alpine.js integration)
Assessment: MINIMAL - Just visual styling, no functionality.
File: /Users/abrichr/oa/src/openadapt-viewer/capture_viewer.html
Timeline Features:
// Episode Timeline integration
episodeTimeline: null,
initializeEpisodeTimeline() {
this.episodeTimeline = new EpisodeTimeline({
container: document.getElementById('episode-timeline-container'),
episodes: this.episodes,
currentTime: this.getCurrentTime(),
totalDuration: this.getTotalDuration(),
onSeek: (time) => this.seekToTime(time),
onEpisodeChange: (episode) => {
console.log('Episode changed:', episode.name);
}
});
}Capabilities:
- ✅ Episode labels - Visual labels above timeline
- ✅ Click navigation - Click episode labels to jump
- ✅ Keyboard shortcuts - ←/→ for prev/next episode
- ✅ Current episode tracking - Shows which episode you're in
- ✅ Episode boundaries - Visual dividers between episodes
- ✅ Tooltips - Hover for episode details
- ✅ Auto-update - Syncs with playback
- ✅ Mobile responsive
Assessment: EXCELLENT - Full-featured timeline component with all desired functionality.
File: /Users/abrichr/oa/src/openadapt-viewer/segmentation_viewer.html
Timeline Features:
function renderTimeline(episode) {
const segment = document.createElement('div');
segment.className = 'timeline-segment';
const startPercent = (episodeStart / totalDuration) * 100;
const widthPercent = (episodeDuration / totalDuration) * 100;
segment.style.left = startPercent + '%';
segment.style.width = widthPercent + '%';
segment.textContent = formatDuration(episodeDuration);
}Capabilities:
- ✅ Segment visualization - Shows episode duration
- ✅ Hover effects - Segment highlights on hover
- ✅ Duration display - Shows time in segment
- ❌ No click-to-seek (displays only)
- ❌ No multiple segments (single episode view)
CSS:
.timeline-segment {
position: absolute;
height: 100%;
background: linear-gradient(135deg, #00d9ff 0%, #00ff88 100%);
border-radius: 20px;
transition: all 0.3s ease;
}
.timeline-segment:hover {
filter: brightness(1.2);
z-index: 10;
}Assessment: GOOD - Visual segments with hover effects, but limited to single episode view.
File: /Users/abrichr/oa/src/openadapt-viewer/synthetic_demo_viewer.html
Timeline Features: NONE
Assessment: N/A - Demo viewer doesn't need timeline (text-based demos).
File: /Users/abrichr/oa/src/openadapt_viewer/components/episode_timeline.js
Core Features:
class EpisodeTimeline {
constructor(options) {
this.episodes = options.episodes || [];
this.currentTime = options.currentTime || 0;
this.totalDuration = options.totalDuration;
this.onSeek = options.onSeek || (() => {});
this.onEpisodeChange = options.onEpisodeChange || (() => {});
}
renderLabels() {
const labels = this.episodes.map((episode, index) => {
const left = (episode.start_time / this.totalDuration) * 100;
const width = ((episode.end_time - episode.start_time) / this.totalDuration) * 100;
return `<div class="oa-episode-label"
data-episode-id="${episode.episode_id}"
style="left: ${left}%; width: ${width}%;"
role="button">
<span>${episode.name}</span>
</div>`;
});
}
}Capabilities:
- ✅ Reusable component - Can be integrated anywhere
- ✅ Event-driven - onSeek, onEpisodeChange callbacks
- ✅ Visual labels - Episode names displayed
- ✅ Click navigation - Jump to episode start
- ✅ Keyboard shortcuts - Full keyboard control
- ✅ State management - Tracks current episode
- ✅ Accessibility - ARIA labels, keyboard support
- ✅ Configurable - Many options
Assessment: BEST-IN-CLASS - Production-ready component, needs adaptation for task-based view.
Current State: No segment concept exists in benchmark viewer.
Desired Behavior:
- User clicks a task segment in timeline
- Viewer jumps to first step of that task
- Plays through all steps of that task
- Option to loop/repeat the task
- Visual indicator shows "looping task X"
Implementation Needed:
// Alpine.js data
{
playbackMode: 'normal', // or 'loop-task'
loopingTaskId: null,
playTask(taskId) {
const task = this.tasks.find(t => t.id === taskId);
this.currentStep = task.firstStepIndex;
this.playbackMode = 'loop-task';
this.loopingTaskId = taskId;
this.startPlayback();
},
onStepAdvance() {
if (this.playbackMode === 'loop-task') {
const task = this.tasks.find(t => t.id === this.loopingTaskId);
if (this.currentStep >= task.lastStepIndex) {
this.currentStep = task.firstStepIndex; // Loop back
}
}
}
}Current State: Timeline is a simple progress bar with no task boundaries.
Desired Behavior:
Timeline:
┌──────────────────────────────────────────────────────┐
│ Task 001 │ Task 002 │ Task 003 │ Task 004 │
│ ████████ │ ████ │ ██████ │ ███ │
└──────────────────────────────────────────────────────┘
↑ Current position
Implementation Needed:
<div class="oa-timeline-track">
<!-- Render segments for each task -->
<template x-for="task in tasks">
<div class="oa-task-segment"
:style="`left: ${taskStartPercent(task)}%;
width: ${taskWidthPercent(task)}%;`"
:class="{'active': task.id === currentTask.id}"
@click="playTask(task.id)">
<span class="task-label" x-text="task.id"></span>
</div>
</template>
<!-- Current position marker -->
<div class="oa-current-marker" :style="`left: ${currentProgress}%`"></div>
</div>CSS Needed:
.oa-task-segment {
position: absolute;
height: 100%;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border-right: 2px solid rgba(255, 255, 255, 0.2);
cursor: pointer;
transition: all 0.2s ease;
}
.oa-task-segment:hover {
filter: brightness(1.3);
transform: translateY(-2px);
}
.oa-task-segment.active {
background: linear-gradient(135deg, #00d4aa 0%, #00ff88 100%);
box-shadow: 0 0 0 2px var(--oa-accent);
}
.task-label {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
font-size: 10px;
color: var(--oa-text-muted);
white-space: nowrap;
}Current State: Only global progress tracking.
Desired Behavior:
- Each task segment shows its own completion percentage
- Visual fill shows how far through the current task
- Completed tasks are fully filled, current task partially filled, future tasks empty
Implementation:
taskProgress(task) {
const taskSteps = this.getTaskSteps(task);
const completedSteps = taskSteps.filter((_, idx) =>
taskSteps[idx] <= this.currentStep
).length;
return (completedSteps / taskSteps.length) * 100;
}<div class="oa-task-segment">
<div class="task-progress-fill"
:style="`width: ${taskProgress(task)}%`"></div>
</div>Current State: No keyboard navigation for tasks.
Desired Keyboard Controls:
1-9: Jump to task 1-9 directly[/]: Previous/Next taskL: Toggle loop mode for current taskHome: Jump to first taskEnd: Jump to last task- Space, ←, →: Already implemented for steps
Implementation:
@keydown.window="handleKeyPress($event)"
handleKeyPress(e) {
// Number keys for direct task access
if (e.key >= '1' && e.key <= '9') {
const taskIndex = parseInt(e.key) - 1;
if (taskIndex < this.tasks.length) {
this.playTask(this.tasks[taskIndex].id);
}
}
// [ ] for prev/next task
if (e.key === '[') this.prevTask();
if (e.key === ']') this.nextTask();
// L for loop toggle
if (e.key === 'l' || e.key === 'L') {
this.toggleLoopMode();
}
}Current State: ❌ Not implemented in benchmark viewer (though in capture viewer)
Desired Behavior: Speed selector: 0.5x, 1x, 2x, 4x
Implementation: Already exists in capture_viewer.html, can be copied directly.
| Feature | benchmark_viewer.html | capture_viewer.html | segmentation_viewer.html | episode_timeline.js |
|---|---|---|---|---|
| Visual Segments | ❌ | ✅ | ✅ | ✅ |
| Click to Seek | ❌ | ✅ | ❌ | ✅ |
| Segment Labels | ❌ | ✅ | ✅ | ✅ |
| Current Indicator | ❌ | ✅ | ✅ | ✅ |
| Hover Tooltips | ❌ | ✅ | ❌ | ✅ |
| Keyboard Nav | ❌ | ✅ | ❌ | ✅ |
| Progress Per Segment | ❌ | ✅ | ✅ | ✅ |
| Segment Repeat/Loop | ❌ | ❌ | ❌ | ❌ |
| Playback Speed | ❌ | ✅ | ❌ | ❌ |
| Alpine.js Integration | ❌ | ✅ | ✅ |
Legend: ✅ = Implemented, ❌ = Missing,
// Episode timeline integration with full state management
episodeTimeline: null,
episodes: [],
currentEpisodeIndex: -1,
async init() {
await this.loadEpisodes();
if (this.episodes.length > 0) {
this.$nextTick(() => {
this.initializeEpisodeTimeline();
});
}
},
initializeEpisodeTimeline() {
this.episodeTimeline = new EpisodeTimeline({
container: document.getElementById('episode-timeline-container'),
episodes: this.episodes,
currentTime: this.getCurrentTime(),
totalDuration: this.getTotalDuration(),
onSeek: (time) => this.seekToTime(time),
onEpisodeChange: (episode) => {
console.log('Episode changed:', episode.name);
}
});
}Key Learnings:
- Use EpisodeTimeline component
- Integrate with Alpine.js lifecycle
- Provide callbacks for seek and change events
- Update timeline on every time change via
x-effect
.timeline-segment {
position: absolute;
height: 100%;
background: linear-gradient(135deg, #00d9ff 0%, #00ff88 100%);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
color: #0a0a0f;
font-size: 0.85em;
font-weight: bold;
transition: all 0.3s ease;
}
.timeline-segment:hover {
filter: brightness(1.2);
z-index: 10;
}Key Learnings:
- Use absolute positioning for segments
- Gradients make segments visually appealing
- Hover effects improve interactivity
- Show duration inside segment
handleLabelClick(e) {
if (!this.config.enableClickNavigation) return;
const episodeId = e.currentTarget.dataset.episodeId;
const episode = this.episodes.find(ep => ep.episode_id === episodeId);
if (episode) {
this.seekToEpisode(episode);
}
}
handleKeydown(e) {
switch(e.key) {
case 'ArrowLeft':
e.preventDefault();
this.prevEpisode();
break;
case 'ArrowRight':
e.preventDefault();
this.nextEpisode();
break;
case 'Home':
this.seekToEpisode(this.episodes[0]);
break;
// Number keys 1-9 for direct access
default:
if (e.key >= '1' && e.key <= '9') {
const index = parseInt(e.key) - 1;
if (index < this.episodes.length) {
this.seekToEpisode(this.episodes[index]);
}
}
}
}Key Learnings:
- Comprehensive keyboard shortcuts
- Click handlers with data attributes
- Configurable navigation
- Number key direct access
Goal: Show task boundaries in timeline
Steps:
- Map benchmark tasks to "episodes" format
- Calculate task start/end times from step indices
- Render visual segments for each task
- Add task labels above timeline
Code:
// In Alpine.js component
tasks: [],
init() {
this.tasks = this.computeTaskSegments();
},
computeTaskSegments() {
return benchmarkTasks.map(task => ({
id: task.id,
name: task.id,
start_time: this.steps[task.firstStepIndex].timestamp,
end_time: this.steps[task.lastStepIndex].timestamp,
duration: task.duration,
steps: task.steps
}));
}Goal: Click task segment to jump to that task
Steps:
- Add click handlers to task segments
- Calculate clicked task from mouse position
- Seek to task start time
- Update current task indicator
Code:
seekToTask(taskId) {
const task = this.tasks.find(t => t.id === taskId);
if (task) {
// Find first step of this task
this.currentStep = this.findStepIndex(task.start_time);
this.currentTaskId = taskId;
}
}Goal: Play task segment on repeat
Steps:
- Add "Loop Task" button/toggle
- Track loop mode state
- Auto-restart task when reaching last step
- Visual indicator for looping
Code:
playbackMode: 'normal', // or 'loop-task'
loopingTaskId: null,
toggleLoopMode() {
if (this.playbackMode === 'loop-task') {
this.playbackMode = 'normal';
this.loopingTaskId = null;
} else {
this.playbackMode = 'loop-task';
this.loopingTaskId = this.currentTaskId;
}
},
// Modify playback advance logic
onPlaybackTick() {
if (this.currentStep < this.totalSteps - 1) {
this.currentStep++;
} else if (this.playbackMode === 'loop-task') {
// Restart task
const task = this.tasks.find(t => t.id === this.loopingTaskId);
this.currentStep = this.findStepIndex(task.start_time);
} else {
this.stopPlayback();
}
}Goal: Polish and keyboard navigation
Steps:
- Add keyboard shortcuts (1-9, [, ], L)
- Hover tooltips for task segments
- Visual feedback (current task highlight)
- Progress fill per task
<div x-data="benchmarkViewer()" x-init="init()">
<!-- Task Timeline -->
<div class="oa-timeline-section">
<div class="oa-timeline-labels">
<template x-for="task in tasks" :key="task.id">
<div class="oa-task-label"
:style="`left: ${taskPosition(task)}%; width: ${taskWidth(task)}%;`"
:class="{'active': task.id === currentTaskId}"
@click="seekToTask(task.id)">
<span x-text="task.name"></span>
</div>
</template>
</div>
<div class="oa-timeline-track" @click="handleTimelineClick($event)">
<!-- Task segments -->
<template x-for="task in tasks" :key="task.id">
<div class="oa-task-segment"
:style="`left: ${taskPosition(task)}%; width: ${taskWidth(task)}%;`"
:class="{'active': task.id === currentTaskId, 'looping': task.id === loopingTaskId}">
<!-- Progress fill within segment -->
<div class="task-progress"
:style="`width: ${taskProgress(task)}%`"></div>
</div>
</template>
<!-- Current position marker -->
<div class="oa-current-marker" :style="`left: ${currentProgress}%`"></div>
</div>
</div>
<!-- Loop controls -->
<div class="oa-loop-controls">
<button @click="toggleLoopMode()"
:class="{'active': playbackMode === 'loop-task'}">
<span x-show="playbackMode !== 'loop-task'">Loop Task</span>
<span x-show="playbackMode === 'loop-task'">Stop Loop</span>
</button>
<span x-show="playbackMode === 'loop-task'" x-text="`Looping: ${loopingTaskId}`"></span>
</div>
</div>function benchmarkViewer() {
return {
// ... existing data ...
tasks: [],
currentTaskId: null,
playbackMode: 'normal', // or 'loop-task'
loopingTaskId: null,
init() {
this.tasks = this.computeTaskSegments();
this.updateCurrentTask();
},
computeTaskSegments() {
// Convert benchmark tasks to timeline segments
return benchmarkData.tasks.map(task => ({
id: task.id,
name: task.id,
start_time: this.steps[task.firstStepIndex].timestamp,
end_time: this.steps[task.lastStepIndex].timestamp,
duration: task.duration,
firstStepIndex: task.firstStepIndex,
lastStepIndex: task.lastStepIndex
}));
},
taskPosition(task) {
return (task.start_time / this.getTotalDuration()) * 100;
},
taskWidth(task) {
return (task.duration / this.getTotalDuration()) * 100;
},
taskProgress(task) {
if (this.currentStep < task.firstStepIndex) return 0;
if (this.currentStep > task.lastStepIndex) return 100;
const taskStepCount = task.lastStepIndex - task.firstStepIndex + 1;
const completedSteps = this.currentStep - task.firstStepIndex + 1;
return (completedSteps / taskStepCount) * 100;
},
seekToTask(taskId) {
const task = this.tasks.find(t => t.id === taskId);
if (task) {
this.currentStep = task.firstStepIndex;
this.currentTaskId = taskId;
}
},
updateCurrentTask() {
// Find which task we're currently in
for (const task of this.tasks) {
if (this.currentStep >= task.firstStepIndex &&
this.currentStep <= task.lastStepIndex) {
this.currentTaskId = task.id;
break;
}
}
},
toggleLoopMode() {
if (this.playbackMode === 'loop-task') {
this.playbackMode = 'normal';
this.loopingTaskId = null;
} else {
this.playbackMode = 'loop-task';
this.loopingTaskId = this.currentTaskId;
}
},
// Override nextStep to support looping
nextStep() {
if (this.currentStep < this.totalSteps - 1) {
this.currentStep++;
} else if (this.playbackMode === 'loop-task') {
// Loop back to start of current task
const task = this.tasks.find(t => t.id === this.loopingTaskId);
this.currentStep = task.firstStepIndex;
}
this.updateCurrentTask();
},
handleTimelineClick(e) {
const rect = e.currentTarget.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const percent = clickX / rect.width;
const targetTime = percent * this.getTotalDuration();
// Find step closest to this time
for (let i = 0; i < this.steps.length; i++) {
if (this.steps[i].timestamp >= targetTime) {
this.currentStep = i;
this.updateCurrentTask();
break;
}
}
}
};
}/* Task Timeline Section */
.oa-timeline-section {
margin-bottom: 24px;
}
.oa-timeline-labels {
position: relative;
height: 24px;
margin-bottom: 8px;
}
.oa-task-label {
position: absolute;
top: 0;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
color: var(--oa-text-muted);
cursor: pointer;
transition: all 0.2s ease;
border-radius: 4px;
background: rgba(255, 255, 255, 0.03);
}
.oa-task-label:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--oa-text-primary);
}
.oa-task-label.active {
background: var(--oa-accent-dim);
color: var(--oa-accent);
}
/* Task Segments */
.oa-task-segment {
position: absolute;
top: 0;
height: 100%;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border-right: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
transition: all 0.2s ease;
overflow: hidden;
}
.oa-task-segment:hover {
filter: brightness(1.3);
transform: translateY(-1px);
}
.oa-task-segment.active {
background: linear-gradient(135deg, #00d4aa 0%, #00ff88 100%);
box-shadow: 0 0 0 2px var(--oa-accent);
}
.oa-task-segment.looping {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* Task Progress Fill */
.task-progress {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: rgba(255, 255, 255, 0.3);
transition: width 0.2s ease;
}
/* Current Position Marker */
.oa-current-marker {
position: absolute;
top: -4px;
width: 3px;
height: calc(100% + 8px);
background: var(--oa-accent);
box-shadow: 0 0 8px var(--oa-accent);
transition: left 0.1s linear;
z-index: 10;
}
/* Loop Controls */
.oa-loop-controls {
display: flex;
align-items: center;
gap: 12px;
margin-top: 12px;
}
.oa-loop-controls button {
padding: 8px 16px;
border: 1px solid var(--oa-border-color);
border-radius: 6px;
background: var(--oa-bg-tertiary);
color: var(--oa-text-primary);
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s ease;
}
.oa-loop-controls button:hover {
background: var(--oa-accent-dim);
border-color: var(--oa-accent);
color: var(--oa-accent);
}
.oa-loop-controls button.active {
background: var(--oa-warning-bg);
border-color: var(--oa-warning);
color: var(--oa-warning);
}- Task segment calculation (start/end times)
- Task progress calculation
- Loop mode logic
- Keyboard shortcut handlers
- Click segment → jumps to task
- Loop mode → restarts task at end
- Keyboard shortcuts → navigate tasks
- Visual updates → segments highlight correctly
- Test with 5-task benchmark
- Test with 20-task benchmark
- Test loop mode for each task
- Test all keyboard shortcuts
- Test mobile responsiveness
- Comprehensive Comparison Document (this file)
- Enhanced Timeline Component Code
- HTML template
- Alpine.js logic
- CSS styling
- Updated benchmark_viewer.html
- Integrated task segments
- Click-to-seek functionality
- Loop mode controls
- Keyboard shortcuts
- Documentation
- User guide for timeline features
- Developer guide for extending timeline
- Keyboard shortcuts reference
- Tests
- Automated tests for core logic
- Manual test scenarios
- Visual regression tests
- ✅ Complete this analysis document
- Extract task segment calculation logic
- Create visual segments in benchmark_viewer.html
- Add task labels above timeline
- Implement click-to-seek for tasks
- Add loop mode functionality
- Visual feedback for current/looping task
- Test with sample benchmark data
- Keyboard shortcuts
- Hover tooltips
- Progress indicators per task
- Performance optimization for 100+ tasks
/Users/abrichr/oa/src/openadapt-viewer/benchmark_viewer.html- Current refactored version (minimal timeline)/Users/abrichr/oa/src/openadapt-viewer/capture_viewer.html- Best reference implementation/Users/abrichr/oa/src/openadapt-viewer/segmentation_viewer.html- Episode-based timeline/Users/abrichr/oa/src/openadapt-viewer/src/openadapt_viewer/components/episode_timeline.js- Reusable component/Users/abrichr/oa/src/openadapt-viewer/test_benchmark_refactored.html- Test file/Users/abrichr/oa/src/openadapt-viewer/viewers/benchmark/minimal_viewer.html- Minimal reference
/Users/abrichr/oa/src/openadapt-viewer/CLAUDE.md- Episode Timeline Component section/Users/abrichr/oa/src/openadapt-viewer/EPISODE_TIMELINE_DESIGN.md- Component design doc/Users/abrichr/oa/src/openadapt-viewer/EPISODE_TIMELINE_QUICKSTART.md- Integration guide
Analysis completed: January 17, 2026 Estimated implementation time: 8-12 hours Priority: P0 (timeline degradation blocks effective benchmark review)