Date: 2026-01-17 Status: Design Phase Owner: OpenAdapt Viewer Team
- Executive Summary
- Current State Analysis
- Design Goals
- Visual Design Specifications
- Component Architecture
- Data Flow Design
- Implementation Phases
- Integration Guide
- User Interaction Patterns
- Technical Implementation
- Testing Strategy
- Accessibility & Responsiveness
Transform the capture viewer and other viewers into episode-aware applications with an intelligent, interactive timeline that displays episode boundaries, enables quick navigation, and provides rich context about the user's workflow.
- Episode Labels on Timeline: Visual markers showing episode names above the timeline
- Episode Boundaries: Clear vertical dividers showing where one episode ends and another begins
- Click-to-Navigate: Jump to any episode by clicking its label
- Current Episode Context: Always show which episode the user is viewing
- Automatic Episode Loading: Fetch episodes from catalog or JSON files
- Episode-Aware Playback: Auto-advance between episodes, skip episodes, view episode-specific details
- Reusable Component: Apply timeline to capture, segmentation, synthetic demo, and benchmark viewers
- Users can identify which episode they're viewing in < 2 seconds
- Navigation between episodes takes < 1 click
- Timeline renders smoothly with 1-20 episodes
- Component reusable across 4+ viewer types
Episode Data Structure (from test_episodes.json):
{
"episode_id": "episode_001",
"name": "Navigate to System Settings",
"description": "User opens System Settings...",
"start_time": 0.0,
"end_time": 3.5,
"duration": 3.5,
"steps": ["Click System Settings icon", "Wait for window", "Click Displays"],
"boundary_confidence": 0.92,
"coherence_score": 0.88,
"screenshots": {
"thumbnail": "../path/to/thumbnail.png",
"key_frames": [...]
}
}Current Capture Viewer (/Users/abrichr/oa/src/openadapt-viewer/capture_viewer.html):
- ✅ Playback controls (play/pause, prev/next step)
- ✅ Timeline scrubber (clickable progress bar)
- ✅ Speed controls (0.5x, 1x, 2x, 4x)
- ✅ URL parameters for highlighting (
highlight_start,highlight_end,episode_name) - ❌ No episode labels on timeline
- ❌ No episode boundaries visualization
- ❌ No episode navigation buttons
- ❌ Timeline is just a gradient progress bar
Current Segmentation Viewer (/Users/abrichr/oa/src/openadapt-viewer/segmentation_viewer.html):
- ✅ Episode cards in grid layout
- ✅ Episode detail view with steps
- ✅ Links to capture viewer with URL parameters
- ✅ Search and filter episodes
- ❌ No timeline visualization
- ❌ No preview of episode timeline
Gap Analysis:
┌─────────────────────────────────────────────────────────────┐
│ CURRENT: │
│ Timeline: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ (just a gradient bar, no episode info) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ DESIRED: │
│ Episodes: [Navigate to Settings] [Disable Night Shift] │
│ ├─────────────┤├──────────────┤ │
│ Timeline: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ ↑ ↑ ↑ │
│ 0s 3.5s 6.7s │
│ ● │
│ (current position) │
└─────────────────────────────────────────────────────────────┘
- Visibility: Users should instantly see episode structure
- Navigation: One-click jump to any episode
- Context: Always know which episode you're viewing
- Performance: Smooth with 1-20 episodes
- Reusability: Works across all viewer types
- Progressive Enhancement: Works without episodes (graceful degradation)
- Accessibility: Keyboard navigation, screen reader support
- Mobile-Friendly: Responsive design for tablet/phone
- Customizable: Episode colors, labels, animations configurable
- Extensible: Easy to add features (bookmarks, comparison, analytics)
- ❌ Real-time episode generation (pre-computed only)
- ❌ Episode editing/refinement in viewer (read-only)
- ❌ Multi-viewer synchronization (future feature)
- ❌ Episode analytics dashboard (separate viewer)
┌──────────────────────────────────────────────────────────────────┐
│ EPISODE TIMELINE COMPONENT │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Current: Episode 2 of 2 - "Disable Night Shift" │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Episode Labels (hover to see description) │ │
│ │ │ │
│ │ [Navigate to System Settings] [Disable Night Shift] │ │
│ │ ├──────────────────────┤├─────────────────────┤ │ │
│ ├──────────────────────────────────────────────────────────────┤ │
│ │ Timeline Track │ │
│ │ │ │
│ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │
│ │ │ │ │ │ │
│ │ 0.0s 3.5s 6.7s │ │
│ │ ● │ │
│ │ (current position: 4.2s) │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ [◄ Prev Episode] [Play/Pause] [Next Episode ►] │
│ │
└──────────────────────────────────────────────────────────────────┘
Episode Colors (rotating palette):
/* Episode 1 */
--episode-1-bg: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); /* Blue */
--episode-1-text: #ffffff;
--episode-1-border: #3b82f6;
/* Episode 2 */
--episode-2-bg: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); /* Purple */
--episode-2-text: #ffffff;
--episode-2-border: #8b5cf6;
/* Episode 3 */
--episode-3-bg: linear-gradient(135deg, #ec4899 0%, #db2777 100%); /* Pink */
--episode-3-text: #ffffff;
--episode-3-border: #ec4899;
/* Episode 4 */
--episode-4-bg: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); /* Orange */
--episode-4-text: #ffffff;
--episode-4-border: #f59e0b;
/* Episode 5+ (cycle back to blue with slight variation) */
--episode-5-bg: linear-gradient(135deg, #10b981 0%, #059669 100%); /* Green */
--episode-5-text: #ffffff;
--episode-5-border: #10b981;State Colors:
/* Current episode (active) */
--episode-current: brightness(1.2) drop-shadow(0 0 8px currentColor);
/* Past episodes (dimmed) */
--episode-past: opacity(0.6);
/* Future episodes (more dimmed) */
--episode-future: opacity(0.4);
/* Hover state */
--episode-hover: brightness(1.1) scale(1.02);/* Episode label */
.oa-episode-label {
font-family: var(--oa-font-sans);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.3px;
text-transform: none; /* Keep original casing */
}
/* Current episode display */
.oa-episode-current {
font-size: 16px;
font-weight: 700;
color: var(--oa-text-primary);
}
/* Episode description (tooltip) */
.oa-episode-description {
font-size: 14px;
font-weight: 400;
line-height: 1.5;
color: var(--oa-text-secondary);
}
/* Episode metadata */
.oa-episode-meta {
font-size: 11px;
font-weight: 500;
color: var(--oa-text-muted);
}:root {
/* Timeline dimensions */
--oa-timeline-height: 8px;
--oa-timeline-border-radius: 4px;
/* Episode label dimensions */
--oa-episode-label-height: 32px;
--oa-episode-label-padding: 6px 12px;
--oa-episode-label-gap: 8px; /* Gap between labels and timeline */
/* Episode boundary marker */
--oa-episode-boundary-width: 2px;
--oa-episode-boundary-height: 40px; /* Extends above and below timeline */
/* Current position marker */
--oa-current-marker-size: 12px;
--oa-current-marker-color: var(--oa-accent);
/* Container spacing */
--oa-episode-timeline-padding: 16px;
--oa-episode-timeline-margin: 16px 0;
}Episode Label States:
- Default: Semi-transparent, subtle gradient
- Current: Bright, glowing border, full opacity
- Hover: Scale up slightly, show tooltip
- Past: Dimmed opacity (50%)
- Future: More dimmed opacity (30%)
- Clicked: Brief pulse animation
Timeline States:
- Default: Dark gray background
- Episode Segment: Colored based on episode
- Current Position: Circular marker with accent color
- Hover: Show timestamp tooltip
- Dragging: Change cursor, show preview time
Purpose: Orchestrates all sub-components and manages state
Props:
interface EpisodeTimelineProps {
episodes: Episode[]; // Array of episode objects
currentTime: number; // Current playback position (seconds)
totalDuration: number; // Total recording duration
currentStep?: number; // Optional: current step index
onSeek: (time: number) => void; // Callback when user seeks
onEpisodeClick: (episodeId: string, startTime: number) => void;
onEpisodeChange?: (episode: Episode) => void; // Fired when crossing boundary
// Optional configuration
config?: {
showLabels?: boolean; // Default: true
showBoundaries?: boolean; // Default: true
enableClickNavigation?: boolean; // Default: true
enableAutoAdvance?: boolean; // Default: false
colorScheme?: 'auto' | 'blue' | 'purple' | 'custom';
labelTruncate?: number; // Max chars, default: 30
}
}
interface Episode {
episode_id: string;
name: string;
description: string;
start_time: number;
end_time: number;
duration: number;
steps: string[];
boundary_confidence?: number;
coherence_score?: number;
screenshots?: {
thumbnail?: string;
key_frames?: KeyFrame[];
};
}State:
interface EpisodeTimelineState {
currentEpisodeIndex: number; // Which episode we're in (0-indexed)
hoveredEpisodeId: string | null; // For tooltip display
isDragging: boolean; // User is dragging timeline
previewTime: number | null; // Time shown when hovering
}Methods:
class EpisodeTimeline {
// Core methods
getCurrentEpisode(): Episode | null;
getEpisodeAtTime(time: number): Episode | null;
seekToEpisode(episodeId: string): void;
nextEpisode(): void;
prevEpisode(): void;
// Utility methods
getEpisodeColor(index: number): string;
formatEpisodeDuration(episode: Episode): string;
calculateEpisodePosition(episode: Episode): { left: string, width: string };
// Event handlers
handleLabelClick(episodeId: string): void;
handleTimelineClick(event: MouseEvent): void;
handleLabelHover(episodeId: string): void;
handleTimelineHover(event: MouseEvent): void;
}Purpose: Renders episode labels above timeline
<div class="oa-episode-labels">
<div class="oa-episode-label"
data-episode-id="episode_001"
style="left: 0%; width: 52.2%"
@click="handleLabelClick"
@mouseenter="handleLabelHover">
<span class="oa-episode-label-text">Navigate to System Settings</span>
<span class="oa-episode-label-duration">3.5s</span>
</div>
<div class="oa-episode-label oa-episode-current"
data-episode-id="episode_002"
style="left: 52.2%; width: 47.8%">
<span class="oa-episode-label-text">Disable Night Shift</span>
<span class="oa-episode-label-duration">3.2s</span>
</div>
</div>Purpose: Renders the timeline track with episode segments
<div class="oa-timeline-track" @click="handleTimelineClick">
<!-- Episode segments -->
<div class="oa-episode-segment"
data-episode-id="episode_001"
style="left: 0%; width: 52.2%; background: var(--episode-1-bg)">
</div>
<div class="oa-episode-segment oa-episode-current"
data-episode-id="episode_002"
style="left: 52.2%; width: 47.8%; background: var(--episode-2-bg)">
</div>
<!-- Episode boundaries -->
<div class="oa-episode-boundary" style="left: 52.2%"></div>
<!-- Current position marker -->
<div class="oa-current-marker" style="left: 62.7%"></div>
</div>Purpose: Time markers below timeline
<div class="oa-timeline-markers">
<span class="oa-timeline-marker">0.0s</span>
<span class="oa-timeline-marker oa-episode-boundary-marker">3.5s</span>
<span class="oa-timeline-marker">6.7s</span>
</div>Purpose: Show episode details on hover
<div class="oa-episode-tooltip"
style="left: 200px; top: -80px"
x-show="hoveredEpisodeId">
<div class="oa-episode-tooltip-header">
<strong>Disable Night Shift</strong>
<span class="oa-episode-tooltip-meta">3.2s • 3 steps</span>
</div>
<div class="oa-episode-tooltip-description">
User scrolls down to find Night Shift settings and toggles it off.
</div>
<div class="oa-episode-tooltip-confidence">
Confidence: 95%
</div>
</div>Purpose: Navigation buttons for episodes
<div class="oa-episode-controls">
<button class="oa-episode-nav-btn"
@click="prevEpisode()"
:disabled="currentEpisodeIndex === 0">
<svg>...</svg>
<span>Previous Episode</span>
</button>
<div class="oa-episode-current-indicator">
Episode <strong>2</strong> of <strong>2</strong>
</div>
<button class="oa-episode-nav-btn"
@click="nextEpisode()"
:disabled="currentEpisodeIndex >= episodes.length - 1">
<span>Next Episode</span>
<svg>...</svg>
</button>
</div>EpisodeTimeline (root)
├── EpisodeContext (current episode display)
├── EpisodeLabels
│ └── EpisodeLabel (×N)
│ └── EpisodeTooltip (on hover)
├── EpisodeTrack
│ ├── EpisodeSegment (×N)
│ ├── EpisodeBoundary (×N-1)
│ └── CurrentMarker
├── EpisodeMarkers
│ └── TimeMarker (×N+1)
└── EpisodeControls
├── PrevButton
├── CurrentIndicator
└── NextButton
Best for: Segmentation viewer, when episodes are pre-computed
┌─────────────────────────────────────────────────────────┐
│ User opens capture viewer with ?episodes=path.json │
└──────────────────────┬──────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Check URL parameter for episode file path │
└──────────────────────┬──────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ fetch(episodesPath).then(data => ...) │
└──────────────────────┬──────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Parse episode JSON (validate schema) │
└──────────────────────┬──────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Render EpisodeTimeline component with episodes │
└──────────────────────┬──────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ If highlight_start param exists, jump to that episode │
└─────────────────────────────────────────────────────────┘
Implementation:
async function loadEpisodes() {
const params = new URLSearchParams(window.location.search);
const episodesPath = params.get('episodes') ||
'../episodes/recording_123_episodes.json';
try {
const response = await fetch(episodesPath);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
return data.episodes || [];
} catch (error) {
console.error('Failed to load episodes:', error);
return null; // Graceful degradation
}
}Best for: Capture viewer integrated with catalog system
┌─────────────────────────────────────────────────────────┐
│ Capture viewer knows recording_id │
└──────────────────────┬──────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Query catalog for segmentation results │
│ catalog.getSegmentationResults(recording_id) │
└──────────────────────┬──────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Catalog returns episode file path or null │
└──────────────────────┬──────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ If episodes exist, fetch JSON and render timeline │
│ If not, show viewer without episode features │
└─────────────────────────────────────────────────────────┘
Implementation:
async function loadEpisodesFromCatalog(recordingId) {
if (!window.CATALOG_API) return null;
try {
const result = await window.CATALOG_API.getSegmentationResults(recordingId);
if (result && result.episode_file_path) {
const response = await fetch(result.episode_file_path);
const data = await response.json();
return data.episodes;
}
return null;
} catch (error) {
console.error('Failed to load from catalog:', error);
return null;
}
}Best for: Unified recording format with inline episodes
┌─────────────────────────────────────────────────────────┐
│ Recording JSON includes "episodes" field │
│ { │
│ "recording_id": "...", │
│ "steps": [...], │
│ "episodes": [...] ← Embedded │
│ } │
└──────────────────────┬──────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Capture viewer reads episodes directly from data │
└──────────────────────┬──────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ If episodes array exists and has items, render timeline │
└─────────────────────────────────────────────────────────┘
Implementation:
function initializeViewer(recordingData) {
const hasEpisodes = recordingData.episodes &&
recordingData.episodes.length > 0;
if (hasEpisodes) {
renderEpisodeTimeline(recordingData.episodes);
} else {
renderSimpleTimeline();
}
}Use Option B (Catalog API) for loose coupling:
✅ Pros:
- Separation of concerns (episodes managed separately)
- Easy to update episodes without regenerating viewer
- Works with existing catalog infrastructure
- Falls back gracefully if episodes not available
❌ Cons:
- Requires catalog system to be set up
- Extra network request
Fallback chain:
- Try catalog API → 2. Try URL parameter → 3. Try default path → 4. No episodes (graceful)
Goal: Get episode labels and basic navigation working
Deliverables:
-
EpisodeTimeline.jscomponent (standalone JS) - Episode labels rendered above timeline
- Click episode label to jump to start time
- Current episode indicator
- Basic CSS styling (colors, spacing)
- Integration with capture_viewer.html
- Load episodes from JSON file (Option A)
Success Criteria:
- Episodes displayed with correct names
- Clicking label jumps to episode start
- Current episode updates as playback progresses
- Works with 1-5 episodes
Code Example:
// Phase 1: Minimal implementation
class EpisodeTimeline {
constructor(episodes, currentTime, onSeek) {
this.episodes = episodes;
this.currentTime = currentTime;
this.onSeek = onSeek;
}
render(container) {
const html = `
<div class="oa-episode-labels">
${this.episodes.map(ep => this.renderLabel(ep)).join('')}
</div>
<div class="oa-timeline-track">
${this.episodes.map(ep => this.renderSegment(ep)).join('')}
</div>
`;
container.innerHTML = html;
this.attachListeners(container);
}
renderLabel(episode) {
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-id="${episode.episode_id}"
style="left: ${left}%; width: ${width}%">
${episode.name}
</div>
`;
}
attachListeners(container) {
container.querySelectorAll('.oa-episode-label').forEach(label => {
label.addEventListener('click', (e) => {
const episodeId = e.currentTarget.dataset.id;
const episode = this.episodes.find(ep => ep.episode_id === episodeId);
this.onSeek(episode.start_time);
});
});
}
}Goal: Add polish and advanced features
Deliverables:
- Episode descriptions (tooltip on hover)
- Episode navigation buttons (Prev/Next)
- Episode color coding (5-color palette)
- Episode boundary markers
- Current position marker on timeline
- Auto-advance option (jump to next episode when current ends)
- Episode progress indicator ("Step 2 of 3 in episode")
- Smooth animations (seek, transitions)
Success Criteria:
- Hover shows episode description
- Prev/Next buttons work correctly
- Episodes visually distinct with colors
- Boundaries clearly visible
- Smooth seek animation (300ms)
Goal: Production-ready with mobile support
Deliverables:
- Mobile responsive layout (labels stack on small screens)
- Touch interactions (swipe between episodes)
- Keyboard shortcuts (←/→ for prev/next episode, 1-9 for jump)
- Accessibility (ARIA labels, focus indicators)
- Episode transition animations
- Toast notifications ("Entering Episode 2")
- Episode metadata display (confidence, coherence)
- Performance optimization (render only visible labels)
Success Criteria:
- Works on mobile (375px width)
- Keyboard navigation works
- Screen reader announces episode changes
- Smooth on 20 episodes
- Passes WCAG 2.1 AA
Goal: Power user features and analytics
Deliverables:
- Episode bookmarks (save favorite moments)
- Episode comparison mode (side-by-side)
- Episode analytics (view counts, skip rate)
- User refinement (adjust boundaries, suggest names)
- Episode export (JSON, CSV, screenshots)
- Episode search (find episodes by name/description)
- Episode thumbnails in timeline
Success Criteria:
- Bookmarks persist in localStorage
- Analytics tracked and visualized
- User can adjust boundaries and save
- Export generates valid JSON
File: /Users/abrichr/oa/src/openadapt-viewer/capture_viewer.html
Steps:
- Add Episode Loading Logic:
// Add to Alpine.js component
x-data="{
episodes: [],
currentEpisodeIndex: -1,
async init() {
// Existing init code...
// Load episodes
this.episodes = await loadEpisodesFromCatalog(this.recordingId);
if (this.episodes && this.episodes.length > 0) {
this.updateCurrentEpisode();
}
},
updateCurrentEpisode() {
if (!this.episodes.length) return;
const currentStep = this.steps[this.currentStep];
if (!currentStep) return;
const currentTime = currentStep.timestamp;
// Find which episode we're in
for (let i = 0; i < this.episodes.length; i++) {
const ep = this.episodes[i];
if (currentTime >= ep.start_time && currentTime < ep.end_time) {
if (this.currentEpisodeIndex !== i) {
this.currentEpisodeIndex = i;
this.showEpisodeTransition(ep);
}
break;
}
}
},
showEpisodeTransition(episode) {
// Show toast notification
console.log('Entering episode:', episode.name);
},
seekToEpisode(episodeId) {
const episode = this.episodes.find(ep => ep.episode_id === episodeId);
if (!episode) return;
// Find first step in this episode
for (let i = 0; i < this.steps.length; i++) {
if (this.steps[i].timestamp >= episode.start_time) {
this.currentStep = i;
break;
}
}
}
}"- Add Episode Timeline HTML:
<!-- Insert after playback controls, before action details -->
<template x-if="episodes && episodes.length > 0">
<div class="oa-episode-timeline-container">
<!-- Current episode indicator -->
<div class="oa-episode-current-indicator" x-show="currentEpisodeIndex >= 0">
<span class="oa-episode-current-label">Current Episode:</span>
<strong x-text="`${currentEpisodeIndex + 1} of ${episodes.length}`"></strong>
<span>-</span>
<span x-text="episodes[currentEpisodeIndex]?.name"></span>
</div>
<!-- Episode labels -->
<div class="oa-episode-labels">
<template x-for="(episode, idx) in episodes" :key="episode.episode_id">
<div class="oa-episode-label"
:class="{ 'oa-episode-current': idx === currentEpisodeIndex }"
:style="`left: ${(episode.start_time / totalDuration) * 100}%;
width: ${((episode.end_time - episode.start_time) / totalDuration) * 100}%;
background: var(--episode-${(idx % 5) + 1}-bg);`"
@click="seekToEpisode(episode.episode_id)"
:title="episode.description">
<span class="oa-episode-label-text" x-text="episode.name"></span>
</div>
</template>
</div>
<!-- Timeline with episode boundaries (modify existing timeline) -->
<div class="oa-timeline-track" @click="handleTimelineClick">
<!-- Existing progress bar... -->
<!-- Add episode boundaries -->
<template x-for="(episode, idx) in episodes.slice(0, -1)" :key="`boundary-${idx}`">
<div class="oa-episode-boundary"
:style="`left: ${(episode.end_time / totalDuration) * 100}%`">
</div>
</template>
</div>
<!-- Episode navigation controls -->
<div class="oa-episode-controls">
<button @click="prevEpisode()"
:disabled="currentEpisodeIndex <= 0"
class="oa-episode-nav-btn">
◄ Previous Episode
</button>
<button @click="nextEpisode()"
:disabled="currentEpisodeIndex >= episodes.length - 1"
class="oa-episode-nav-btn">
Next Episode ►
</button>
</div>
</div>
</template>- Add CSS Styles:
/* Episode Timeline Styles */
.oa-episode-timeline-container {
margin: 16px 0;
padding: 16px;
background: var(--oa-bg-secondary);
border-radius: var(--oa-border-radius-lg);
}
.oa-episode-current-indicator {
margin-bottom: 16px;
padding: 12px;
background: var(--oa-bg-tertiary);
border-radius: var(--oa-border-radius);
font-size: 14px;
color: var(--oa-text-secondary);
}
.oa-episode-current-indicator strong {
color: var(--oa-accent);
font-weight: 700;
}
.oa-episode-labels {
position: relative;
height: 32px;
margin-bottom: 8px;
}
.oa-episode-label {
position: absolute;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
border: 2px solid transparent;
opacity: 0.7;
}
.oa-episode-label:hover {
opacity: 1;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.oa-episode-label.oa-episode-current {
opacity: 1;
border-color: var(--oa-accent);
box-shadow: 0 0 12px var(--oa-accent);
}
.oa-episode-label-text {
font-size: 12px;
font-weight: 600;
color: white;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.oa-episode-boundary {
position: absolute;
top: -4px;
bottom: -4px;
width: 2px;
background: rgba(255, 255, 255, 0.3);
z-index: 10;
pointer-events: none;
}
.oa-episode-controls {
margin-top: 12px;
display: flex;
gap: 12px;
justify-content: center;
}
.oa-episode-nav-btn {
padding: 8px 16px;
background: var(--oa-bg-tertiary);
border: 1px solid var(--oa-border-color);
border-radius: var(--oa-border-radius);
color: var(--oa-text-primary);
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.oa-episode-nav-btn:hover:not(:disabled) {
background: var(--oa-accent-dim);
color: var(--oa-accent);
border-color: var(--oa-accent);
}
.oa-episode-nav-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Episode color variables */
:root {
--episode-1-bg: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
--episode-2-bg: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
--episode-3-bg: linear-gradient(135deg, #ec4899 0%, #db2777 100%);
--episode-4-bg: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
--episode-5-bg: linear-gradient(135deg, #10b981 0%, #059669 100%);
}- Add Helper Methods:
// Add to Alpine.js component methods
prevEpisode() {
if (this.currentEpisodeIndex > 0) {
const prevEp = this.episodes[this.currentEpisodeIndex - 1];
this.seekToEpisode(prevEp.episode_id);
}
},
nextEpisode() {
if (this.currentEpisodeIndex < this.episodes.length - 1) {
const nextEp = this.episodes[this.currentEpisodeIndex + 1];
this.seekToEpisode(nextEp.episode_id);
}
},
get totalDuration() {
if (!this.steps.length) return 0;
const lastStep = this.steps[this.steps.length - 1];
return lastStep.timestamp + (lastStep.duration || 0);
}File: /Users/abrichr/oa/src/openadapt-viewer/segmentation_viewer.html
Enhancement: Add mini timeline preview to episode cards
<!-- In episode card rendering -->
<div class="episode-card">
<div class="episode-thumbnail">...</div>
<div class="episode-content">
<div class="episode-name">...</div>
<div class="episode-description">...</div>
<!-- NEW: Mini timeline preview -->
<div class="episode-mini-timeline">
<div class="timeline-track">
<div class="timeline-segment"
:style="`width: ${(episode.duration / totalRecordingDuration) * 100}%`">
</div>
</div>
<div class="timeline-label">
<span>{{episode.start_time_formatted}}</span>
<span>{{episode.end_time_formatted}}</span>
</div>
</div>
</div>
</div>This completes Part 1 of the design document covering:
- Executive Summary
- Current State Analysis
- Design Goals
- Visual Design Specifications
- Component Architecture
- Data Flow Design
- Implementation Phases
- Integration Guide (partial)
The document continues in the next section with:
- User Interaction Patterns
- Technical Implementation Details
- Testing Strategy
- Accessibility & Responsiveness
- Advanced Features
- Appendices
Would you like me to continue with the remaining sections?