diff --git a/README.md b/README.md index e92ae46..3f38f3b 100644 --- a/README.md +++ b/README.md @@ -1,382 +1,185 @@ # openadapt-viewer -[![Release](https://github.com/OpenAdaptAI/openadapt-viewer/actions/workflows/release.yml/badge.svg)](https://github.com/OpenAdaptAI/openadapt-viewer/actions/workflows/release.yml) -[![PyPI version](https://img.shields.io/pypi/v/openadapt-viewer.svg)](https://pypi.org/project/openadapt-viewer/) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/) +[![Test](https://github.com/OpenAdaptAI/openadapt-viewer/actions/workflows/test.yml/badge.svg)](https://github.com/OpenAdaptAI/openadapt-viewer/actions/workflows/test.yml) +[![PyPI](https://img.shields.io/pypi/v/openadapt-viewer.svg)](https://pypi.org/project/openadapt-viewer/) +[![Python](https://img.shields.io/pypi/pyversions/openadapt-viewer)](https://pypi.org/project/openadapt-viewer/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -Reusable component library for OpenAdapt visualization. Build standalone HTML viewers for training dashboards, benchmark results, capture playback, and demo retrieval. +An eval finished overnight and left you a directory of JSON, a SQLite file, and +a folder of screenshots. This turns that into one HTML file you can open, +scroll, and mail to a colleague: metric cards, a filterable task list, click +markers drawn over the screenshots, and playback controls for stepping through +a recording frame by frame. -## Features +It's for people building on OpenAdapt who want to look at a run without +standing up a server. It renders once and writes a file. If you want a +dashboard that refreshes while a job is still running, look elsewhere. -- **Component-based**: Reusable building blocks (screenshot, playback, metrics, filters) -- **Composable**: Combine components to build custom viewers -- **Standalone HTML**: Generated files work offline, no server required -- **Event transcript**: Real-time audio transcription synchronized with playback -- **Consistent styling**: Shared CSS variables and dark mode support -- **Alpine.js integration**: Lightweight interactivity out of the box +[PyPI](https://pypi.org/project/openadapt-viewer/) · +[Component reference](docs/COMPONENTS.md) · +[openadapt-capture](https://github.com/OpenAdaptAI/openadapt-capture), which +records what this plays back -## Installation +## Sixty seconds ```bash pip install openadapt-viewer +openadapt-viewer demo --tasks 10 --output viewer.html ``` -Or with uv: -```bash -uv add openadapt-viewer +``` +Generating demo viewer with 10 sample tasks... +Generated: viewer.html ``` -## Quick Start +Open `viewer.html` in a browser and you get this: -### Using Components +![The demo viewer: summary cards, per-domain results, filter dropdowns, the task list, and the detail panel for the selected task](docs/images/demo_viewer.png) -```python -from openadapt_viewer.components import ( - screenshot_display, - playback_controls, - metrics_grid, - filter_bar, - badge, -) +Real output, macOS, 2026-08-28, with `task_003` clicked. The demo's pass and +fail values come from an unseeded `random.random()`, so your success rate +will probably differ and your tasks won't be these tasks. Rerun the picture with +`python scripts/generate_demo_screenshot.py`. -# Screenshot with click overlays -html = screenshot_display( - image_path="screenshot.png", - overlays=[ - {"type": "click", "x": 0.5, "y": 0.3, "label": "H", "variant": "human"}, - {"type": "click", "x": 0.6, "y": 0.4, "label": "AI", "variant": "predicted"}, - ], -) +## Build a page out of parts -# Metrics cards -html = metrics_grid([ - {"label": "Total Tasks", "value": 100}, - {"label": "Passed", "value": 75, "color": "success"}, - {"label": "Failed", "value": 25, "color": "error"}, - {"label": "Success Rate", "value": "75%", "color": "accent"}, -]) -``` +Each component is a function that returns HTML text. Call `badge("Pass", +color="success")` and you get this back, nothing more: -### Using PageBuilder +```html + + Pass + +``` -Build complete pages from components: +Paste that into a template you already own, or hand the pieces to +`PageBuilder` and let it write the whole document: ```python from openadapt_viewer.builders import PageBuilder -from openadapt_viewer.components import metrics_grid, screenshot_display - -builder = PageBuilder(title="My Viewer", include_alpine=True) - -builder.add_header( - title="Benchmark Results", - subtitle="Model: gpt-5.1", - nav_tabs=[ - {"href": "dashboard.html", "label": "Training"}, - {"href": "viewer.html", "label": "Viewer", "active": True}, - ], -) +from openadapt_viewer.components import badge, metrics_grid -builder.add_section( +page = PageBuilder(title="Nightly eval", dark_mode=True) +page.add_header(title="Nightly eval", subtitle="run 2026-08-28") +page.add_section( metrics_grid([ - {"label": "Tasks", "value": 100}, - {"label": "Passed", "value": 75, "color": "success"}, + {"label": "Tasks", "value": 42}, + {"label": "Passed", "value": 39, "color": "success"}, + {"label": "Failed", "value": 3, "color": "error"}, ]), title="Summary", ) - -# Render to file -builder.render_to_file("output.html") +page.add_section(badge("Pass", color="success")) +print(page.render_to_file("eval.html")) ``` -### Ready-to-Use Viewers - -5 production viewers available: - -1. **Benchmark Viewer** - Visualize benchmark evaluation results -2. **Capture Viewer** - Playback recorded GUI interactions -3. **Training Dashboard** - Monitor ML training progress (via openadapt-ml) -4. **Retrieval Viewer** - Display demo search results (via openadapt-retrieval) -5. **Segmentation Viewer** - View episode segmentation results - -```python -from openadapt_viewer.viewers.benchmark import generate_benchmark_html - -# From benchmark results directory -generate_benchmark_html( - data_path="benchmark_results/run_001/", - output_path="viewer.html", -) ``` - -All viewers use the canonical component-based pattern. See `VIEWER_PATTERNS.md` for details. - -## CLI Usage - -```bash -# Generate demo benchmark viewer -openadapt-viewer demo --tasks 10 --output viewer.html - -# Generate from benchmark results -openadapt-viewer benchmark --data results/run_001/ --output viewer.html +eval.html ``` -## Components - -All components return HTML strings that can be composed together. Use them with PageBuilder or embed inline. - -### Core Components - -| Component | Description | Example Use Case | -|-----------|-------------|-----------------| -| `screenshot_display` | Screenshot with click/highlight overlays | Capture frames, demo screenshots | -| `playback_controls` | Play/pause/speed controls for step playback | Video-like playback | -| `timeline` | Progress bar for step navigation | Scrub through recordings | -| `action_display` | Format actions (click, type, scroll, etc.) | Display action details | -| `metrics_card` | Single statistic card | Individual metric display | -| `metrics_grid` | Grid of metric cards | Summary dashboards | -| `filter_bar` | Filter dropdowns with optional search | Filter and search data | -| `filter_dropdown` | Single dropdown filter | Domain/status filters | -| `selectable_list` | List with selection support | Task lists, file lists | -| `list_item` | Individual list item | Custom list entries | -| `badge` | Status badges (pass/fail, etc.) | Status indicators | +15 KB with the stylesheet inlined. The page renders dark and the sun button in +the header switches it; the `dark_mode` argument is ignored. There +are 22 components: screenshot overlays, action timelines, filter bars, +side-by-side comparison views, failure-analysis panels, and the small stuff +like badges and metric cards. [docs/COMPONENTS.md](docs/COMPONENTS.md) has +every signature. -### Enhanced Components +## Play back a capture -| Component | Description | Example Use Case | -|-----------|-------------|-----------------| -| `video_playback` | Video playback from screenshot sequences | Smooth capture playback | -| `video_playback_with_actions` | Video + synchronized action overlay | Capture with action overlay | -| `action_timeline` | Timeline with action markers | Action sequence view | -| `action_timeline_vertical` | Vertical action timeline | Compact action view | -| `comparison_view` | Side-by-side comparison | Before/after, A/B test | -| `overlay_comparison` | Overlay comparison with slider | Image comparison | -| `action_type_filter` | Filter by action type | Filter clicks/types/scrolls | -| `action_type_pills` | Action type pill buttons | Quick action filtering | -| `action_type_dropdown` | Action type dropdown | Compact action filter | -| `failure_analysis_panel` | Failure analysis dashboard | Benchmark failure analysis | -| `failure_summary_card` | Failure summary card | Individual failure details | - -**Total: 22 components** available for building viewers. - -See `VIEWER_PATTERNS.md` for complete usage examples. - -## Project Structure +```python +from openadapt_viewer.viewers.capture.generator import generate_capture_html + +steps = [ + {"timestamp": 0.0, "duration": 1.2, "action": {"type": "click", "x": 0.42, "y": 0.31}}, + {"timestamp": 1.2, "duration": 2.4, "action": {"type": "type", "text": "Jane Doe"}}, + {"timestamp": 3.6, "duration": 0.9, "action": {"type": "click", "x": 0.78, "y": 0.64}}, +] + +print(generate_capture_html( + capture_id="turn-off-nightshift", + goal="Turn off Night Shift in Display settings", + steps=steps, + output_path="capture_viewer.html", +)) +``` ``` -src/openadapt_viewer/ -├── components/ # Reusable UI building blocks -│ ├── screenshot.py # Screenshot with overlays -│ ├── playback.py # Playback controls -│ ├── timeline.py # Progress bar -│ ├── action_display.py # Action formatting -│ ├── metrics.py # Stats cards -│ ├── filters.py # Filter dropdowns -│ ├── list_view.py # Selectable lists -│ └── badge.py # Status badges -├── builders/ # High-level page builders -│ └── page_builder.py # PageBuilder class -├── styles/ # Shared CSS -│ └── core.css # CSS variables and base styles -├── core/ # Core utilities -│ ├── types.py # Pydantic models -│ └── html_builder.py # Jinja2 utilities -├── viewers/ # Full viewer implementations -│ └── benchmark/ # Benchmark results viewer -├── examples/ # Reference implementations -│ ├── benchmark_example.py -│ ├── training_example.py -│ ├── capture_example.py -│ └── retrieval_example.py -└── templates/ # Jinja2 templates +capture_viewer.html ``` -## Audio Transcript Feature - -The viewer includes a powerful **audio transcript** feature that displays real-time transcription of captured audio alongside the visual playback. This is particularly useful for: - -- **Debugging workflows**: See what was said at each step -- **Documentation**: Auto-generate narrative descriptions of recorded sessions -- **Analysis**: Correlate verbal instructions with UI actions -- **Training**: Review narrated demonstrations with synchronized visuals - -### Key Capabilities - -The transcript panel provides: - -- **Timestamped transcription**: Each transcript segment is stamped with its time in the recording (e.g., `0:00.00`, `0:05.60`) -- **Synchronized playback**: Transcript automatically highlights and scrolls as the video plays -- **Searchable text**: Find specific moments in long recordings by searching transcript content -- **Copy functionality**: Export transcript text for documentation or analysis - -### How It Works - -When captures are recorded with audio (using `openadapt-capture`'s audio recording features), the viewer automatically: - -1. Displays the transcript in a dedicated panel in the sidebar -2. Timestamps each transcript segment relative to the recording start time -3. Syncs transcript highlighting with the current playback position -4. Updates the displayed transcript as you navigate through events - -The transcript appears alongside the event list and event details, providing a complete picture of what happened during the recording. - -## Synthetic Demo Viewer +Give a step a `"screenshot"` key holding a path or a data URI and the player +shows the frame with a marker on it. Click coordinates are fractions of the +frame, not pixels, so `0.42` means 42% across. -**NEW:** Interactive browser-based viewer for synthetic WAA demonstration data. - -### Quick Start +A whole recording written by openadapt-capture goes through the benchmark +viewer instead. Point it at the directory, which needs `episodes.json` and a +`recording.db` inside it: ```bash -# Open the synthetic demo viewer -open synthetic_demo_viewer.html +openadapt-viewer benchmark --data turn-off-nightshift --output viewer.html ``` -### What It Shows - -- **82 synthetic demos** across 6 domains (notepad, paint, clock, browser, file_explorer, office) -- **Filter by domain** and select specific tasks -- **View demo content** with syntax-highlighted steps -- **See how demos are used** in actual API prompts -- **Impact comparison**: 33% → 100% accuracy improvement with demo-conditioned prompting -- **Action reference**: All 8 action types (CLICK, TYPE, WAIT, etc.) - -### Purpose - -Synthetic demos are **AI-generated example trajectories** that show step-by-step how to complete Windows automation tasks. They are included in prompts when calling Claude/GPT APIs during benchmark evaluation - this is called **demo-conditioned prompting**. - -**Impact:** Improved first-action accuracy from 33% to 100%! - -### Documentation - -- **Quick Start**: `QUICK_REFERENCE.md` - One-page overview -- **Complete Guide**: `SYNTHETIC_DEMOS_EXPLAINED.md` - Full explanation -- **Examples**: `DEMO_EXAMPLES_SHOWCASE.md` - 5 diverse demo examples -- **Master Index**: `SYNTHETIC_DEMO_INDEX.md` - Central navigation hub - -### Features - -- Beautiful dark theme matching OpenAdapt style -- Domain filtering (All, Notepad, Paint, Clock, Browser, File Explorer, Office) -- Task selector with estimated step counts -- Dual-panel display: demo content + prompt usage -- Side-by-side impact comparison (with vs without demos) -- Complete action types reference -- Fully self-contained (no external dependencies) -- Works offline - -See `SYNTHETIC_DEMO_INDEX.md` for complete documentation. - ---- - -## Screenshots - -### Full Viewer Interface - -The viewer provides a complete interface for exploring captured GUI interactions with playback controls, timeline navigation, event details, and **real-time audio transcript**. - -![Turn off Night Shift - Full Viewer](docs/images/turn-off-nightshift_full.png) -*Interactive viewer showing the "Turn off Night Shift" workflow with screenshot display (center), event list (right sidebar top), and **audio transcript** (right sidebar bottom)* - -### Playback Controls - -Step through captures with playback controls, timeline scrubbing, and keyboard shortcuts (Space to play/pause, arrow keys to navigate). - -![Playback Controls](docs/images/turn-off-nightshift_controls.png) -*Timeline and playback controls with overlay toggle, plus event details and **synchronized transcript panel*** - -### Event List, Details, and Transcript - -Browse all captured events with detailed information about each action. The **transcript panel** displays timestamped audio transcription that syncs with playback, showing exactly what was said at each moment in the recording. - -![Event List](docs/images/turn-off-nightshift_events.png) -*Event list sidebar showing captured actions with timing and type information, plus **live audio transcript with timestamps*** - -### Demo Workflow - -![Demo Workflow](docs/images/demo_new_full.png) -*Example demo workflow viewer* - -## Examples - -Run the examples to see how different OpenAdapt packages can use the component library: - -```bash -# Benchmark results (openadapt-evals) -python -m openadapt_viewer.examples.benchmark_example - -# Training dashboard (openadapt-ml) -python -m openadapt_viewer.examples.training_example - -# Capture playback (openadapt-capture) -python -m openadapt_viewer.examples.capture_example - -# Retrieval results (openadapt-retrieval) -python -m openadapt_viewer.examples.retrieval_example ``` - -### Generating Screenshots - -To regenerate the README screenshots: - -```bash -# Install playwright (one-time setup) -uv pip install "openadapt-viewer[screenshots]" -uv run playwright install chromium - -# Install openadapt-capture (required) -cd ../openadapt-capture -uv pip install -e . -cd ../openadapt-viewer - -# Generate screenshots -uv run python scripts/generate_readme_screenshots.py - -# Or with custom options -uv run python scripts/generate_readme_screenshots.py \ - --capture-dir /path/to/openadapt-capture \ - --output-dir docs/images \ - --max-events 50 +Generating benchmark viewer from: turn-off-nightshift +Generated: viewer.html ``` -The script will: -1. Load captures from `openadapt-capture` (turn-off-nightshift and demo_new) -2. Generate interactive HTML viewers -3. Take screenshots using Playwright -4. Save screenshots to `docs/images/` +Set `$OPENADAPT_CAPTURE_RECORDING` to that directory and you can drop `--data`. +There's also `openadapt-viewer catalog scan --capture-dir DIR`, which indexes +recordings into `~/.openadapt/catalog.db` so the segmentation viewer can find +them; `catalog stats` prints the counts. + +## What it doesn't do + +The generated page loads Alpine.js from `cdn.jsdelivr.net`, so it isn't +offline-safe. Block that request and the summary cards and the filter dropdowns +still paint, because their markup sits in the file, but the task list comes up +empty and clicking does nothing. `benchmark --standalone` does not help: the +flag reaches the generator and the generator ignores it. + +Five more things to know before you file a bug: + +- The benchmark viewer writes a real recording's screenshots into the page as + absolute local paths, so a viewer built from a recording loses its images the + moment you move or mail the file. Only `demo` inlines them. + +- The capture viewer writes `` + into the page, resolved against wherever the HTML ends up. That file and its + companion `episode_timeline.js` ship inside the installed package, so unless + your output happens to land at the root of a source checkout, the episode + timeline renders unstyled and inert. +- `screenshot_display` links images by path. It inlines them as base64 only + when you pass `embed_image=True`. Move the HTML away from the PNGs without + that flag and the images break. +- Give `benchmark --data` a directory holding a pre-2026-07-17 `capture.db` + and it neither reads it nor says so. `LegacyCaptureError` subclasses + `FileNotFoundError`, the generator's fallback catches it, and you get a + viewer with zero tasks and a success message. The migration script named in + that error never reaches you. +- `openadapt_viewer.__version__` reads `0.1.0`, which is not the packaged + version. Read the version from package metadata instead. ## Development ```bash -# Clone and install -git clone https://github.com/OpenAdaptAI/openadapt-viewer.git -cd openadapt-viewer +git clone https://github.com/OpenAdaptAI/openadapt-viewer && cd openadapt-viewer uv sync --all-extras - -# Run tests -uv run pytest tests/ -v - -# Run linter uv run ruff check . +uv run pytest tests/ -v ``` -## Integration - -Used by other OpenAdapt packages: - -- **openadapt-ml**: Training dashboards and model comparison -- **openadapt-evals**: Benchmark result visualization -- **openadapt-capture**: Capture recording playback -- **openadapt-retrieval**: Demo search result display +Two test modules read recordings that openadapt-capture actually wrote, rather +than fixtures written to match this repo's idea of the schema. Clone it +alongside and point them at it: -## Documentation +```bash +export OPENADAPT_CAPTURE_DIR=/path/to/openadapt-capture +export OPENADAPT_CAPTURE_EXAMPLES=$OPENADAPT_CAPTURE_DIR/examples/captures +``` -- **[VIEWER_PATTERNS.md](VIEWER_PATTERNS.md)** - Canonical pattern for building viewers (MUST READ for new viewers) -- **[MIGRATION_GUIDE.md](MIGRATION_GUIDE.md)** - Step-by-step guide for converting inline viewers to component-based -- **[ARCHITECTURE.md](ARCHITECTURE.md)** - System architecture and design patterns -- **[CATALOG_SYSTEM.md](CATALOG_SYSTEM.md)** - Automatic recording discovery and indexing -- **[SEARCH_FUNCTIONALITY.md](SEARCH_FUNCTIONALITY.md)** - Token-based search implementation -- **[EPISODE_TIMELINE_QUICKSTART.md](EPISODE_TIMELINE_QUICKSTART.md)** - Adding episode timelines to viewers +CI runs Python 3.10 and 3.11 on Ubuntu and macOS. `ruff check .` is a blocking +gate over the whole repository, so lint before you push. ## License -MIT License - see LICENSE file for details. +MIT. See [LICENSE](LICENSE). diff --git a/docs/COMPONENTS.md b/docs/COMPONENTS.md new file mode 100644 index 0000000..1a6f2b5 --- /dev/null +++ b/docs/COMPONENTS.md @@ -0,0 +1,198 @@ +# Component reference + +Every component in `openadapt_viewer.components` is a plain function that +returns a string of HTML. Nothing renders on its own and nothing holds state, +so you can concatenate the output, drop it into a Jinja template you already +have, or pass it to `PageBuilder.add_section`. + +Interactive components emit Alpine.js directives. They need `PageBuilder(..., +include_alpine=True)`, which is the default, and they need the browser to reach +`cdn.jsdelivr.net` when the page opens. + +To regenerate this list after changing a signature: + +```python +import inspect +import openadapt_viewer.components as c + +for name in c.__all__: + fn = getattr(c, name) + print(f"{name}{inspect.signature(fn)}") +``` + +### `screenshot_display` + +Render a screenshot with optional overlays. + +```python +screenshot_display(image_path: 'str | Path | None' = None, width: 'int' = 800, height: 'int' = 450, overlays: 'list[Overlay] | None' = None, caption: 'str | None' = None, embed_image: 'bool' = False, placeholder_text: 'str' = 'No screenshot available', class_name: 'str' = '') -> 'str' +``` + +### `playback_controls` + +Render playback controls for step navigation. + +```python +playback_controls(step_count: 'int' = 1, initial_step: 'int' = 0, speeds: 'list[float] | None' = None, default_speed: 'float' = 1.0, show_step_counter: 'bool' = True, alpine_data_name: 'str' = 'playback', class_name: 'str' = '') -> 'str' +``` + +### `timeline` + +Render a timeline progress bar. + +```python +timeline(step_count: 'int' = 1, current_step: 'int' = 0, step_labels: 'list[str] | None' = None, clickable: 'bool' = True, show_markers: 'bool' = False, alpine_data_name: 'str' = 'playback', class_name: 'str' = '') -> 'str' +``` + +### `action_display` + +Render an action display with badge and details. + +```python +action_display(action_type: 'str | None' = None, action_details: 'dict[str, Any] | None' = None, show_badge: 'bool' = True, show_details: 'bool' = True, show_reasoning: 'bool' = False, reasoning: 'str | None' = None, class_name: 'str' = '') -> 'str' +``` + +### `metrics_card` + +Render a single metrics card. + +```python +metrics_card(label: 'str', value: 'str | float', change: 'float | None' = None, color: 'str' = 'default', icon: 'str | None' = None, class_name: 'str' = '') -> 'str' +``` + +### `metrics_grid` + +Render a grid of metrics cards. + +```python +metrics_grid(cards: 'list[dict[str, Any]]', columns: 'int' = 4, class_name: 'str' = '') -> 'str' +``` + +### `filter_bar` + +Render a filter bar with multiple dropdowns and optional search. + +```python +filter_bar(filters: 'list[FilterConfig]', search_placeholder: 'str | None' = None, search_model: 'str | None' = None, alpine_data_name: 'str' = 'filters', class_name: 'str' = '') -> 'str' +``` + +### `filter_dropdown` + +Render a single filter dropdown. + +```python +filter_dropdown(filter_id: 'str', label: 'str', options: 'list[FilterOption] | list[str]', default_value: 'str' = '', alpine_model: 'str | None' = None, class_name: 'str' = '') -> 'str' +``` + +### `selectable_list` + +Render a list with selection support. + +```python +selectable_list(items: 'list[ListItemConfig]', title: 'str | None' = None, subtitle: 'str | None' = None, max_height: 'str' = '600px', alpine_data_name: 'str' = 'list', selected_item_var: 'str' = 'selectedItem', on_select: 'str | None' = None, class_name: 'str' = '') -> 'str' +``` + +### `list_item` + +Render a single list item. + +```python +list_item(item_id: 'str', title: 'str', subtitle: 'str | None' = None, badge: 'str | None' = None, badge_color: 'str' = 'info', selected: 'bool' = False, click_handler: 'str | None' = None, class_name: 'str' = '') -> 'str' +``` + +### `badge` + +Render a status badge. + +```python +badge(text: 'str', color: 'str' = 'info', size: 'str' = 'md', class_name: 'str' = '') -> 'str' +``` + +### `video_playback` + +Render a video playback component from screenshot frames. + +```python +video_playback(frames: 'list[ScreenshotFrame] | None' = None, width: 'int' = 960, height: 'int' = 540, autoplay: 'bool' = False, loop: 'bool' = False, show_controls: 'bool' = True, show_timeline: 'bool' = True, show_frame_counter: 'bool' = True, default_fps: 'float' = 2.0, speeds: 'list[float] | None' = None, embed_images: 'bool' = False, alpine_data_name: 'str' = 'videoPlayer', class_name: 'str' = '') -> 'str' +``` + +### `video_playback_with_actions` + +Video playback with integrated action details panel. + +```python +video_playback_with_actions(frames: 'list[ScreenshotFrame] | None' = None, width: 'int' = 960, height: 'int' = 540, show_action_details: 'bool' = True, **kwargs) -> 'str' +``` + +### `action_timeline` + +Render an action timeline with seek functionality. + +```python +action_timeline(actions: 'list[TimelineAction] | None' = None, duration: 'float | None' = None, current_time: 'float' = 0, width: 'str' = '100%', height: 'int' = 60, show_labels: 'bool' = True, show_time_markers: 'bool' = True, clickable: 'bool' = True, alpine_sync_var: 'str | None' = None, on_seek: 'str | None' = None, class_name: 'str' = '') -> 'str' +``` + +### `action_timeline_vertical` + +Render a vertical action list/timeline. + +```python +action_timeline_vertical(actions: 'list[TimelineAction] | None' = None, height: 'str' = '400px', show_details: 'bool' = True, clickable: 'bool' = True, alpine_sync_var: 'str | None' = None, class_name: 'str' = '') -> 'str' +``` + +### `comparison_view` + +Render a side-by-side comparison view. + +```python +comparison_view(left_data: 'ComparisonData | None' = None, right_data: 'ComparisonData | None' = None, width: 'int' = 1200, height: 'int' = 450, show_diff: 'bool' = True, sync_playback: 'bool' = True, show_actions: 'bool' = True, click_tolerance: 'float' = 0.05, class_name: 'str' = '') -> 'str' +``` + +### `overlay_comparison` + +Render a single screenshot with overlays for both human and predicted actions. + +```python +overlay_comparison(base_screenshot: 'str | None' = None, human_click: 'dict | None' = None, predicted_click: 'dict | None' = None, width: 'int' = 800, height: 'int' = 450, show_distance: 'bool' = True, class_name: 'str' = '') -> 'str' +``` + +### `action_type_filter` + +Render an action type filter component. + +```python +action_type_filter(action_types: 'list[ActionTypeConfig] | None' = None, selected_types: 'list[str] | None' = None, show_counts: 'bool' = True, show_all_option: 'bool' = True, multi_select: 'bool' = True, alpine_model: 'str | None' = None, on_change: 'str | None' = None, layout: 'str' = 'horizontal', class_name: 'str' = '') -> 'str' +``` + +### `action_type_pills` + +Render a compact pill-style action type filter. + +```python +action_type_pills(action_types: 'list[ActionTypeConfig] | None' = None, selected_types: 'list[str] | None' = None, alpine_model: 'str | None' = None, on_change: 'str | None' = None, class_name: 'str' = '') -> 'str' +``` + +### `action_type_dropdown` + +Render a dropdown-style action type filter with checkboxes. + +```python +action_type_dropdown(action_types: 'list[ActionTypeConfig] | None' = None, selected_types: 'list[str] | None' = None, placeholder: 'str' = 'Filter by action type', alpine_model: 'str | None' = None, on_change: 'str | None' = None, class_name: 'str' = '') -> 'str' +``` + +### `failure_analysis_panel` + +Render a comprehensive failure analysis panel. + +```python +failure_analysis_panel(failures: 'list[FailureRecord] | None' = None, total_tasks: 'int' = 0, show_categories: 'bool' = True, show_list: 'bool' = True, show_details: 'bool' = True, on_select_failure: 'str | None' = None, class_name: 'str' = '') -> 'str' +``` + +### `failure_summary_card` + +Render a compact failure summary card. + +```python +failure_summary_card(total_failures: 'int' = 0, total_tasks: 'int' = 0, top_error_type: 'str | None' = None, top_error_count: 'int' = 0, class_name: 'str' = '') -> 'str' +``` + diff --git a/docs/images/demo_viewer.png b/docs/images/demo_viewer.png new file mode 100644 index 0000000..1fda578 Binary files /dev/null and b/docs/images/demo_viewer.png differ diff --git a/scripts/generate_demo_screenshot.py b/scripts/generate_demo_screenshot.py new file mode 100755 index 0000000..fb4f21d --- /dev/null +++ b/scripts/generate_demo_screenshot.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Regenerate docs/images/demo_viewer.png, the screenshot in the README. + +The README shows the output of `openadapt-viewer demo`, so this script runs +that command and photographs the result rather than mocking up a page. The +demo's pass/fail data is unseeded, so a regenerated image will not match the +committed one task for task. That is expected; the point is that the layout, +the controls and the chrome in the README are what the tool actually emits. + + uv run python scripts/generate_demo_screenshot.py + +Needs a browser (`uv run playwright install chromium`) and, because the +generated page pulls Alpine.js from a CDN, a network connection. Without +Alpine the task click below does nothing and the detail panel stays empty. +""" + +from __future__ import annotations + +import argparse +import subprocess +import sys +import tempfile +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +DEFAULT_OUTPUT = REPO_ROOT / "docs" / "images" / "demo_viewer.png" + +# Wide enough for the four summary cards to sit on one row, which is how the +# viewer is meant to be read. +VIEWPORT_WIDTH = 1100 +SELECTED_TASK = "task_003" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", "-o", type=Path, default=DEFAULT_OUTPUT) + parser.add_argument("--tasks", "-n", type=int, default=10) + args = parser.parse_args() + + try: + from playwright.sync_api import sync_playwright + except ImportError: + print("playwright is not installed: uv sync --all-extras", file=sys.stderr) + return 1 + + with tempfile.TemporaryDirectory() as tmp: + page_path = Path(tmp) / "viewer.html" + subprocess.run( + [ + sys.executable, + "-m", + "openadapt_viewer.cli", + "demo", + "--tasks", + str(args.tasks), + "--output", + str(page_path), + ], + check=True, + ) + + args.output.parent.mkdir(parents=True, exist_ok=True) + with sync_playwright() as playwright: + browser = playwright.chromium.launch() + page = browser.new_page( + viewport={"width": VIEWPORT_WIDTH, "height": 900}, + device_scale_factor=2, + ) + page.goto(page_path.as_uri()) + # Alpine is fetched from the CDN; give it time to hydrate. + page.wait_for_timeout(3000) + page.get_by_text(SELECTED_TASK, exact=True).first.click() + page.wait_for_timeout(1500) + page.screenshot(path=str(args.output), full_page=True) + browser.close() + + print(f"Wrote {args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())