codebook / potato /templates /generated /Audio-Segmentation-Demo-base_template_v2.html
davidjurgens's picture
Deploy: Potato — Codebook Annotation
aceb1b2 verified
Raw
History Blame Contribute Delete
72.4 kB
<!-- CONFIG_HASH: 1604170235c77bc5d0c57a9b33ee34a0 -->
{# Default ui_lang values if not provided by context processor #}
{% if ui_lang is not defined %}
{% set ui_lang = {'next_button': 'Next', 'previous_button': 'Previous', 'jump_prev_unannotated': 'Previous unannotated', 'jump_next_unannotated': 'Next unannotated', 'labeled_badge': 'Labeled', 'not_labeled_badge': 'Not labeled', 'submit_button': 'Submit', 'progress_label': 'Progress', 'go_button': 'Go', 'logout': 'Logout', 'loading': 'Loading annotation interface...', 'error_heading': 'Error', 'retry_button': 'Retry', 'adjudicate': 'Adjudicate', 'codebook': 'Codebook', 'instructions_heading': 'Instructions', 'text_to_annotate': 'Text to Annotate:', 'video_to_annotate': 'Video to Annotate:', 'audio_to_annotate': 'Audio to Annotate:', 'powered_by': 'Powered by', 'cite_us': 'Cite Us', 'html_lang': 'en', 'html_dir': 'ltr'} %}
{% endif %}
<!DOCTYPE html>
<html lang="{{ ui_lang.html_lang|default('en') }}" dir="{{ ui_lang.html_dir|default('ltr') }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Audio Segmentation Demo</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Custom Styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
<!-- Span Annotation Styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='span-styles.css') }}">
{% set frontend_assets = frontend_assets | default({}) %}
<!-- Image Annotation Styles -->
{% if frontend_assets.image_annotation | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='image-annotation.css') }}?v=2">
{% endif %}
<!-- Audio Annotation Styles -->
{% if frontend_assets.audio_annotation | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='audio-annotation.css') }}?v=2">
{% endif %}
<!-- Video Annotation Styles -->
{% if frontend_assets.video_annotation | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='video-annotation.css') }}">
{% endif %}
<!-- Span Link Styles -->
{% if frontend_assets.span_link | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='span-link-styles.css') }}">
{% endif %}
<!-- Event Annotation Styles -->
{% if frontend_assets.event_annotation | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='event-annotation.css') }}">
{% endif %}
<!-- Spreadsheet Display Styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='spreadsheet-display.css') }}">
<!-- Coding Trace Display Styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='coding-trace.css') }}">
<!-- Live Coding Agent Viewer -->
{% if frontend_assets.live_coding_agent | default(false) %}
<script src="{{ url_for('static', filename='live-coding-agent-viewer.js') }}" defer></script>
{% endif %}
<!-- Format Display Styles (document, code, etc.) -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/format-displays.css') }}">
<!-- Document Bounding Box Styles -->
{% if frontend_assets.document_bbox | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='document-bbox.css') }}">
{% endif %}
<!-- PDF Annotation Styles -->
{% if frontend_assets.pdf_bbox | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='pdf-annotation.css') }}">
{% endif %}
<!-- Display Logic Styles (conditional schema branching) -->
<link rel="stylesheet" href="{{ url_for('static', filename='display-logic.css') }}">
<!-- Coreference Chain Styles -->
{% if frontend_assets.coreference | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/coreference.css') }}">
{% endif %}
<!-- Segmentation Tool Styles -->
{% if frontend_assets.segmentation_tools | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/segmentation.css') }}">
{% endif %}
<!-- Conversation Tree Styles -->
{% if frontend_assets.conversation_tree | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/conversation-tree.css') }}">
{% endif %}
<!-- Video Tracking Styles -->
{% if frontend_assets.tracking | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/tracking.css') }}">
{% endif %}
<!-- Triage Styles -->
{% if frontend_assets.triage | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/triage.css') }}">
{% endif %}
<!-- Entity Linking Styles (knowledge base linking) -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/entity-linking.css') }}">
<!-- Tiered Annotation Styles (ELAN-style hierarchical annotation) -->
{% if frontend_assets.tiered_annotation | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/tiered-annotation.css') }}">
{% endif %}
{% if agent_proxy_enabled | default(false) %}
<!-- Agent Chat Styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/agent-chat.css') }}">
{% endif %}
{% if chat_enabled | default(false) %}
<!-- LLM Chat Sidebar Styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/llm-chat-sidebar.css') }}?v=1">
{% endif %}
<!-- Universal Memos sidebar styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/memos.css') }}?v=2">
<link rel="stylesheet" href="{{ url_for('static', filename='css/search.css') }}?v=2">
<link rel="stylesheet" href="{{ url_for('static', filename='css/codebook.css') }}?v=10">
<!-- jQuery (required for span annotation) -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<!-- Project-level base CSS (injected from base_css config option) -->
{{ PROJECT_BASE_CSS | safe }}
</head>
<body>
<!-- Header navigation bar -->
<nav class="potato-navbar" role="navigation">
<div class="navbar-inner">
<div class="navbar-brand">
{% if header_logo_url %}
<img class="header-logo" src="{{ header_logo_url }}" alt="Logo">
{% endif %}
<span class="task-name">Audio Segmentation Demo</span>
</div>
<div class="navbar-center">
<div class="annotation-status-indicator" id="annotation-status">
<span class="status-badge {{ annotation_status }}">
{% if annotation_status == 'labeled' %}
{{ ui_lang.labeled_badge }}
{% elif annotation_status == 'in_progress' %}
{{ ui_lang.in_progress_badge }}
{% else %}
{{ ui_lang.not_labeled_badge }}
{% endif %}
</span>
</div>
<div class="navbar-divider"></div>
<div class="progress-section">
<span class="progress-label">{{ ui_lang.progress_label }}</span>
<span class="fw-medium" id="progress-counter">{{finished}}/{{total_count}}</span>
</div>
{% if ibws_round_info %}
<div class="navbar-divider"></div>
<div class="ibws-round-banner" id="ibws-round-banner">
<span class="ibws-round-label">Round {{ ibws_round_info.current_round }}{% if ibws_round_info.max_rounds %} of {{ ibws_round_info.max_rounds }}{% endif %}</span>
<span class="ibws-round-detail">{{ ibws_round_info.terminal_items }}/{{ ibws_round_info.total_items }} ranked</span>
</div>
{% endif %}
<div class="navbar-divider"></div>
{% if instance_index is defined %}
<div class="instance-number-section">
<span class="instance-number-label">Instance</span>
<span class="fw-medium" id="instance-number">#{{ instance_index|int + 1 }}</span>
</div>
{% endif %}
{% if not jumping_to_id_disabled %}
<div class="navbar-divider"></div>
<div class="nav-controls">
<button type="button" class="nav-control-btn" id="jump-unannotated-prev-btn"
onclick="jumpToUnannotatedPrev()" title="{{ ui_lang.jump_prev_unannotated }}">
<i class="fas fa-backward"></i>
</button>
<button type="button" class="nav-control-btn" id="jump-unannotated-btn"
onclick="jumpToUnannotated()" title="{{ ui_lang.jump_next_unannotated }}">
<i class="fas fa-forward"></i>
</button>
</div>
<div class="navbar-divider"></div>
<div class="goto-section">
<input type="number" id="go_to" placeholder="#"
value="" min="0" max="{{total_count}}" required
onfocusin="user_input()" onfocusout="user_input_leave()">
<button type="button" class="goto-btn" id="go-to-btn">{{ ui_lang.go_button }}</button>
</div>
{% endif %}
</div>
<div class="navbar-end">
{% if is_adjudicator %}
<a href="/adjudicate" class="adjudicate-btn" title="Adjudication Mode">
<i class="fas fa-gavel"></i> {{ ui_lang.adjudicate }}
</a>
{% endif %}
{% if annotation_codebook_url %}
<a href="{{ annotation_codebook_url | replace('data_files/', '/media/') }}"
class="codebook-btn"
target="_blank"
rel="noopener noreferrer"
title="Open annotation codebook">
<i class="fas fa-book-open"></i> {{ ui_lang.codebook }}
</a>
{% endif %}
<div class="user-pill">
<span class="username" id="username-display">{{username}}</span>
<a href="/logout" class="logout-btn">{{ ui_lang.logout }}</a>
</div>
</div>
</div>
</nav>
<div class="container-fluid">
<!-- Main annotation area -->
<div id="task_layout" class="shadcn-card mb-2 {% if has_image_annotation or has_video_annotation or has_audio_annotation %}media-annotation{% else %}text-annotation{% endif %}">
<div class="shadcn-card-content">
<!-- Loading state -->
<div id="loading-state" class="text-center py-5">
<div class="loading-spinner mb-3"></div>
<p>{{ ui_lang.loading }}</p>
</div>
<!-- Error state -->
<div id="error-state" class="error-message" style="display: none;">
<h5><i class="fas fa-exclamation-triangle me-2"></i>{{ ui_lang.error_heading }}</h5>
<p id="error-message-text"></p>
<button id="error-retry-btn" class="shadcn-button shadcn-button-primary" onclick="loadCurrentInstance()">
<i class="fas fa-redo me-2"></i>{{ ui_lang.retry_button }}
</button>
<a id="error-done-link" href="/done" class="shadcn-button shadcn-button-primary" style="display: none;">
<i class="fas fa-check me-2"></i>Finish
</a>
</div>
<!-- Main content area -->
<div id="main-content" style="display: none;">
{% if annotation_instructions | default('') %}
<details class="annotation-instructions-banner" open>
<summary>{{ ui_lang.instructions_heading|default('Instructions') }}</summary>
<div class="annotation-instructions-content">
{{ annotation_instructions | safe }}
</div>
</details>
{% endif %}
<!-- Instance display area -->
<div class="mb-4">
{% if has_instance_display | default(false) %}
<!-- New instance_display mode: explicit content display configuration -->
{{ display_html | safe }}
<!-- Hidden element for legacy JS compatibility -->
<div id="instance-text" style="display: none;">
<div id="text-content" data-original-text="{{instance_plain_text | sanitize_html}}">{{instance | sanitize_html}}</div>
</div>
{% elif has_video_annotation | default(false) %}
<h5 class="mb-3">{{ ui_lang.video_to_annotate|default('Video to Annotate:') }}</h5>
<!-- For video annotation, keep the text-content element accessible for JS but visually hidden -->
<div id="instance-text" class="instance-text-container" style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0);">
<div id="text-content" data-original-text="{{instance_plain_text | sanitize_html}}">{{instance | sanitize_html}}</div>
</div>
{% elif has_audio_annotation | default(false) %}
<h5 class="mb-3">{{ ui_lang.audio_to_annotate|default('Audio to Annotate:') }}</h5>
<!-- For audio annotation, keep the text-content element accessible for JS but visually hidden -->
<div id="instance-text" class="instance-text-container" style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0);">
<div id="text-content" data-original-text="{{instance_plain_text | sanitize_html}}">{{instance | sanitize_html}}</div>
</div>
{% elif has_image_annotation | default(false) %}
<!-- For image annotation, hide this section - the image is loaded directly into the annotation canvas -->
<!-- Store the image URL in a hidden element for JavaScript to access -->
<div id="instance-text" style="display: none;">
<div id="text-content" data-original-text="{{instance_plain_text | sanitize_html}}" data-image-url="{{instance | sanitize_html}}">{{instance | sanitize_html}}</div>
</div>
{% else %}
{% if is_annotation_page %}
{% if ui_lang.text_to_annotate|default('Text to Annotate:') %}
<h5 class="instance-text-heading">{{ ui_lang.text_to_annotate|default('Text to Annotate:') }}</h5>
{% endif %}
<div id="instance-text" class="p-3 border rounded instance-text-container" style="background-color: var(--light-bg); position: relative;">
<div id="text-content" data-original-text="{{instance_plain_text | sanitize_html}}" style="position: relative; z-index: 1; pointer-events: auto;">{{instance | sanitize_html}}
<div id="span-overlays" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; z-index: 2;">
</div>
</div>
</div>
{% else %}
<div id="instance-text" style="display: none;">
<div id="text-content" data-original-text=""></div>
</div>
{% endif %}
{% endif %}
</div>
<!-- Status messages -->
<div id="status" class="status" style="display: none;"></div>
<!-- Hidden input for instance_id -->
<input type="hidden" id="instance_id" name="instance_id" value="{{instance_id}}">
<!-- Full instance record as JSON, consumed by schemas that
bind to structured instance fields (process_reward,
trajectory_eval) via document.querySelector('[data-instance-json]').
tojson escapes single quotes so the single-quoted
attribute is safe even when content contains apostrophes. -->
<div id="instance-json-data" style="display: none;"
data-instance-json='{{ (instance_record if instance_record is defined and instance_record else {}) | tojson }}'></div>
<!-- Signal-based triage flag (why this item was prioritized) -->
{% if triage_info is defined and triage_info %}
<div class="triage-flag" id="triage-flag" role="note"
aria-label="Triage: this item was prioritized in the queue">
<span class="triage-flag-icon" aria-hidden="true">!</span>
<span class="triage-flag-text">
Prioritized for review<span class="triage-flag-sep"> · </span><span class="triage-flag-reason">{{ triage_info.reason }}</span>
</span>
</div>
{% endif %}
<!-- LLM-judge inline suggestion (judge ↔ human alignment) -->
{% if judge_prediction is defined and judge_prediction %}
{% set jp = judge_prediction %}
{% set jconf = (jp.confidence * 100)|int %}
{% set jqual = 'Low confidence' if jp.confidence < 0.5 else ('Moderate confidence' if jp.confidence < 0.8 else 'High confidence') %}
<div class="judge-suggestion" id="judge-suggestion"
data-schema="{{ jp.schema }}" data-judge-label="{{ jp.label }}">
<div class="judge-suggestion-row">
<div class="judge-suggestion-main">
<span class="judge-badge-icon" aria-hidden="true"></span>
<span class="judge-suggestion-label">
Judge suggests
<span class="judge-suggested-value">{{ jp.label }}</span>
for <span class="judge-schema-name">{{ jp.schema }}</span>
</span>
<span class="judge-confidence">{{ jqual }} · {{ jconf }}%</span>
</div>
<button type="button" class="btn btn-secondary judge-accept-btn"
id="judge-accept-btn"
data-schema="{{ jp.schema }}" data-judge-label="{{ jp.label }}">
Accept
</button>
{% if jp.running and jp.running.n %}
<span class="judge-running"
title="Running agreement between you/your team and the judge on this task (Cohen's κ)">
{{ (jp.running.agreement_rate * 100)|int }}% agree (n={{ jp.running.n }}){% if jp.running.kappa is not none %} · κ {{ '%.2f'|format(jp.running.kappa) }}{% endif %}
</span>
{% endif %}
</div>
{% if jp.reasoning %}
<details class="judge-reasoning">
<summary>Judge reasoning</summary>
<p>{{ jp.reasoning }}</p>
</details>
{% endif %}
</div>
<script>
(function() {
var btn = document.getElementById('judge-accept-btn');
if (!btn) return;
btn.addEventListener('click', function() {
var schema = btn.getAttribute('data-schema');
var label = btn.getAttribute('data-judge-label');
// Check the matching radio/checkbox input for this schema.
var inputs = document.querySelectorAll(
'input.annotation-input[schema="' + schema + '"]');
var matched = false;
inputs.forEach(function(inp) {
if (inp.value === label || inp.getAttribute('label_name') === label) {
inp.checked = true;
inp.dispatchEvent(new Event('change', { bubbles: true }));
matched = true;
}
});
if (matched) { btn.textContent = 'Accepted'; btn.disabled = true; }
});
})();
</script>
{% endif %}
<!-- Annotation forms -->
<div id="annotation-forms">
<!-- CONFIG_HASH: 1604170235c77bc5d0c57a9b33ee34a0_64563b0e063838603a8555141ec69387 -->
<!-- Generated annotation layout file -->
<!-- This file was automatically generated based on the annotation schemes in your config -->
<!-- You can customize this file to modify the layout of your annotation interface -->
<!-- Changes to this file will be preserved across server restarts -->
<div class="annotation_schema">
<form id="audio_segmentation" class="annotation-form audio-annotation" action="javascript:void(0)">
<fieldset schema="audio_segmentation">
<legend>Listen to the audio and create segments for different content types. Use [ and ] keys to set segment boundaries, then press Enter to create a segment.</legend>
<!-- Audio Annotation Container -->
<div class="audio-annotation-container" data-schema="audio_segmentation">
<!-- 1. Full waveform overview at top -->
<div class="waveform-wrapper">
<div class="overview-label" style="font-size: 0.85em; color: #666; margin-bottom: 4px;">Full Audio Overview (click to navigate)</div>
<div id="overview-audio_segmentation" class="overview-container" style="margin-bottom: 12px;"></div>
<!-- 2. Zoomed waveform below -->
<div class="zoomview-label" style="font-size: 0.85em; color: #666; margin-bottom: 4px;">Zoomed View (right-click drag to annotate)</div>
<div id="waveform-audio_segmentation" class="waveform-container"></div>
</div>
<!-- 3. Toolbar with annotation buttons -->
<div class="audio-annotation-toolbar" style="margin-top: 12px;">
<!-- Playback controls -->
<div class="playback-group">
<button type="button" class="playback-btn" data-action="play" title="Play or pause audio playback. Keyboard shortcut: Space">
<span class="play-icon"></span>
<span class="pause-icon" style="display:none;"></span>
</button>
<button type="button" class="playback-btn" data-action="stop" title="Stop playback and return to the beginning"></button>
<span class="time-display" title="Current playback position / Total duration">
<span class="current-time">0:00</span> / <span class="total-time">0:00</span>
</span>
</div>
<div class="playback-rate-group">
<span class="tool-group-label" title="Adjust playback speed">Speed:</span>
<select class="playback-rate-select" title="Change audio playback speed (0.5x to 2x)">
<option value="0.5">0.5x</option>
<option value="0.75">0.75x</option>
<option value="1" selected>1x</option>
<option value="1.25">1.25x</option>
<option value="1.5">1.5x</option>
<option value="2">2x</option>
</select>
</div>
<!-- Label selector (for label/both modes) -->
<div class="label-group">
<span class="tool-group-label" title="Select a label before creating segments">Label:</span>
<button type="button" class="label-btn" data-label="speech" data-color="#4ECDC4" title="Select 'speech' as the label for new segments. Keyboard shortcut: 1" style="--label-color: #4ECDC4;"><span class="label-color-dot" style="background-color: #4ECDC4;"></span>speech</button><button type="button" class="label-btn" data-label="music" data-color="#FF6B6B" title="Select 'music' as the label for new segments. Keyboard shortcut: 2" style="--label-color: #FF6B6B;"><span class="label-color-dot" style="background-color: #FF6B6B;"></span>music</button><button type="button" class="label-btn" data-label="silence" data-color="#95A5A6" title="Select 'silence' as the label for new segments. Keyboard shortcut: 3" style="--label-color: #95A5A6;"><span class="label-color-dot" style="background-color: #95A5A6;"></span>silence</button><button type="button" class="label-btn" data-label="noise" data-color="#F39C12" title="Select 'noise' as the label for new segments. Keyboard shortcut: 4" style="--label-color: #F39C12;"><span class="label-color-dot" style="background-color: #F39C12;"></span>noise</button>
</div>
<!-- Zoom controls -->
<div class="zoom-group">
<button type="button" class="zoom-btn" data-action="zoom-in" title="Zoom in to see more detail. Keyboard shortcut: + or =">+</button>
<button type="button" class="zoom-btn" data-action="zoom-out" title="Zoom out to see more of the audio. Keyboard shortcut: -">-</button>
<button type="button" class="zoom-btn" data-action="zoom-fit" title="Fit the entire audio in the view. Keyboard shortcut: 0">Fit</button>
</div>
<!-- Segment controls -->
<div class="segment-group">
<button type="button" class="segment-btn" data-action="set-start" title="Mark the start of a new segment at the current playback position. Keyboard shortcut: [">[</button>
<button type="button" class="segment-btn" data-action="set-end" title="Mark the end of a new segment at the current playback position. Keyboard shortcut: ]">]</button>
<button type="button" class="segment-btn" data-action="create-segment" title="Create a segment from the marked start and end positions. Keyboard shortcut: Enter">+ Segment</button>
<button type="button" class="segment-btn delete-btn" data-action="delete-segment" title="Delete the currently selected segment. Keyboard shortcut: Delete or Backspace" disabled>Delete</button>
</div>
<!-- Segment count -->
<div class="count-group">
<span class="segment-count" title="Number of segments you have created">Segments: <span class="count-value">0</span></span>
</div>
<!-- Help toggle button -->
<div class="help-toggle" style="margin-left: auto;">
<button type="button" class="help-toggle-btn" onclick="this.closest('.audio-annotation-container').querySelector('.help-panel').classList.toggle('collapsed')" title="Show or hide usage instructions" style="background: none; border: 1px solid #ccc; border-radius: 4px; padding: 4px 8px; cursor: pointer; font-size: 0.85em;">
❓ Help
</button>
</div>
</div>
<!-- 4. Help/Instructions Panel -->
<div class="help-panel" style="background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; padding: 12px; margin-top: 10px; font-size: 0.9em;">
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
<div>
<strong style="color: #495057;">🖱️ Creating Segments:</strong>
<ul style="margin: 5px 0 0 0; padding-left: 20px; color: #666;">
<li><strong>Left-click</strong> to navigate through the audio</li>
<li><strong>Right-click + drag</strong> to create a segment</li>
<li>Or use <strong>[ and ]</strong> keys to mark start/end while playing</li>
<li><strong>Drag segment edges</strong> to resize</li>
</ul>
</div>
<div>
<strong style="color: #495057;">⌨️ Keyboard Shortcuts:</strong>
<ul style="margin: 5px 0 0 0; padding-left: 20px; color: #666;">
<li><strong>Space</strong> - Play/Pause</li>
<li><strong>[ ]</strong> - Mark segment start/end</li>
<li><strong>Enter</strong> - Create segment from markers</li>
<li><strong>Delete</strong> - Delete selected segment</li>
<li><strong>+/-/0</strong> - Zoom in/out/fit</li>
<li><strong>← →</strong> - Skip 5s (Shift: 30s)</li>
</ul>
</div>
<div>
<strong style="color: #495057;">📝 Quick Start:</strong>
<ol style="margin: 5px 0 0 0; padding-left: 20px; color: #666;">
<li>Select a label above</li>
<li>Right-click and drag on waveform to create segment</li>
<li>Adjust edges by dragging if needed</li>
</ol>
</div>
</div>
</div>
<style>
.help-panel.collapsed { display: none; }
</style>
<!-- Segment list -->
<div class="segment-list-container" style="margin-top: 12px;">
<h4>Segments</h4>
<div id="segment-list-audio_segmentation" class="segment-list"></div>
</div>
<!-- Segment questions panel (for questions/both modes) -->
<div id="segment-questions-audio_segmentation" class="segment-questions-panel" style="display: none;">
<h4>Segment Details</h4>
<div class="segment-questions-content"></div>
</div>
<!-- Hidden input for storing annotation data -->
<input type="hidden"
name="audio_segmentation"
id="input-audio_segmentation"
class="annotation-data-input"
value="">
<!-- Hidden audio element -->
<audio id="audio-audio_segmentation" style="display: none;"></audio>
</div>
<!-- Initialize the annotation manager -->
<script>
(function() {
console.log('[AudioAnnotation] Initialization script started for schema: audio_segmentation');
function initWhenReady() {
if (typeof AudioAnnotationManager === 'undefined') {
console.log('[AudioAnnotation] Waiting for AudioAnnotationManager...');
setTimeout(initWhenReady, 100);
return;
}
// Peaks.js exports as lowercase 'peaks', create alias for uppercase 'Peaks' if needed
if (typeof Peaks === 'undefined' && typeof peaks !== 'undefined') {
window.Peaks = peaks;
console.log('[AudioAnnotation] Created Peaks alias from lowercase peaks');
}
if (typeof Peaks === 'undefined' && typeof peaks === 'undefined') {
console.log('[AudioAnnotation] Waiting for Peaks.js (checked both Peaks and peaks)...');
setTimeout(initWhenReady, 100);
return;
}
console.log('[AudioAnnotation] AudioAnnotationManager and Peaks.js loaded');
var container = document.querySelector('.audio-annotation-container[data-schema="audio_segmentation"]');
if (!container) {
console.error('[AudioAnnotation] Container not found for schema: audio_segmentation');
return;
}
// Wait for waveform container to be visible and have dimensions
// (main-content starts hidden and is shown after API data loads)
var waveformContainer = document.getElementById('waveform-audio_segmentation');
if (waveformContainer) {
var rect = waveformContainer.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
console.log('[AudioAnnotation] Waveform container not visible yet, waiting...');
setTimeout(initWhenReady, 100);
return;
}
}
console.log('[AudioAnnotation] Container found and visible:', container);
var config = {"schemaName": "audio_segmentation", "mode": "label", "labels": [{"name": "speech", "color": "#4ECDC4", "key_value": "1"}, {"name": "music", "color": "#FF6B6B", "key_value": "2"}, {"name": "silence", "color": "#95A5A6", "key_value": "3"}, {"name": "noise", "color": "#F39C12", "key_value": "4"}], "segmentSchemes": [], "minSegments": 1, "maxSegments": null, "zoomEnabled": true, "playbackRateControl": true, "sourceField": "", "waveform": true, "spectrogram": false, "spectrogramOptions": {"fft_size": 2048, "hop_length": 512, "frequency_range": [0, 8000], "color_map": "viridis"}};
var waveformId = 'waveform-audio_segmentation';
var overviewId = 'overview-audio_segmentation';
var audioId = 'audio-audio_segmentation';
var inputId = 'input-audio_segmentation';
var segmentListId = 'segment-list-audio_segmentation';
var questionsId = 'segment-questions-audio_segmentation';
var spectrogramId = config.spectrogram ? 'spectrogram-audio_segmentation' : null;
var spectrogramCanvasId = config.spectrogram ? 'spectrogram-canvas-audio_segmentation' : null;
var spectrogramPlayheadId = config.spectrogram ? 'spectrogram-playhead-audio_segmentation' : null;
// Get audio URL from various sources
var audioUrl = null;
// 1. First try to find from source_field in instance_display field
if (config.sourceField) {
var displayField = document.querySelector('[data-field-key="' + config.sourceField + '"]');
console.log('[AudioAnnotation] Looking for display field:', config.sourceField, displayField);
if (displayField) {
// Check for data-source-url attribute
var sourceUrl = displayField.dataset.sourceUrl;
if (sourceUrl) {
audioUrl = sourceUrl;
console.log('[AudioAnnotation] Found audio URL from display field data-source-url:', audioUrl);
}
// Check for audio element inside display field
var audioEl = displayField.querySelector('audio');
if (!audioUrl && audioEl) {
var source = audioEl.querySelector('source');
audioUrl = source ? source.src : audioEl.src;
console.log('[AudioAnnotation] Found audio URL from display field audio element:', audioUrl);
}
}
}
// 2. Try instance-display container's data-instance-fields attribute
if (!audioUrl && config.sourceField) {
var instanceDisplayContainer = document.querySelector('.instance-display-container[data-instance-fields]');
if (instanceDisplayContainer) {
try {
var instanceFields = JSON.parse(instanceDisplayContainer.dataset.instanceFields);
if (instanceFields[config.sourceField]) {
audioUrl = instanceFields[config.sourceField];
console.log('[AudioAnnotation] Found audio URL from instance fields:', audioUrl);
}
} catch (e) {
console.warn('[AudioAnnotation] Failed to parse instance fields:', e);
}
}
}
// 3. Fallback: Try instance-text or text-content containers
if (!audioUrl) {
var instanceContainer = document.getElementById('instance-text') || document.getElementById('text-content');
console.log('[AudioAnnotation] Fallback - Instance container:', instanceContainer);
if (instanceContainer) {
var text = instanceContainer.textContent || instanceContainer.innerText;
// Check if it's a URL
if (text && (text.trim().startsWith('http') || text.trim().endsWith('.mp3') || text.trim().endsWith('.wav'))) {
audioUrl = text.trim();
console.log('[AudioAnnotation] Found audio URL in text:', audioUrl);
}
// Check for audio element
var audioEl = instanceContainer.querySelector('audio');
if (audioEl && audioEl.src) {
audioUrl = audioEl.src;
console.log('[AudioAnnotation] Found audio URL in audio element:', audioUrl);
}
}
}
// Initialize manager
console.log('[AudioAnnotation] Creating AudioAnnotationManager with audioUrl:', audioUrl);
var manager = new AudioAnnotationManager({
container: container,
waveformId: waveformId,
overviewId: overviewId,
audioId: audioId,
inputId: inputId,
segmentListId: segmentListId,
questionsId: questionsId,
spectrogramId: spectrogramId,
spectrogramCanvasId: spectrogramCanvasId,
spectrogramPlayheadId: spectrogramPlayheadId,
config: config
});
// Store reference on container
container.audioAnnotationManager = manager;
// Load audio if available
if (audioUrl) {
// Use proxy for external URLs to avoid CORS issues
var finalAudioUrl = audioUrl;
if (audioUrl.startsWith('http://') || audioUrl.startsWith('https://')) {
// Check if it's an external URL (not same origin)
var currentOrigin = window.location.origin;
if (!audioUrl.startsWith(currentOrigin)) {
finalAudioUrl = '/api/audio/proxy?url=' + encodeURIComponent(audioUrl);
console.log('[AudioAnnotation] Using proxy for external URL:', finalAudioUrl);
}
}
console.log('[AudioAnnotation] Calling loadAudio with:', finalAudioUrl);
manager.loadAudio(finalAudioUrl);
} else {
console.warn('[AudioAnnotation] No audio URL found, waveform will not be loaded');
}
// Wire up toolbar buttons
container.querySelectorAll('.playback-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var action = this.dataset.action;
if (action === 'play') manager.togglePlayPause();
else if (action === 'stop') manager.stop();
});
});
container.querySelectorAll('.label-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var label = this.dataset.label;
var color = this.dataset.color;
manager.setActiveLabel(label, color);
container.querySelectorAll('.label-btn').forEach(function(b) {
b.classList.remove('active');
});
this.classList.add('active');
});
});
container.querySelectorAll('.zoom-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var action = this.dataset.action;
if (action === 'zoom-in') manager.zoomIn();
else if (action === 'zoom-out') manager.zoomOut();
else if (action === 'zoom-fit') manager.zoomToFit();
});
});
container.querySelectorAll('.segment-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var action = this.dataset.action;
if (action === 'set-start') manager.setSelectionStart();
else if (action === 'set-end') manager.setSelectionEnd();
else if (action === 'create-segment') manager.createSegmentFromSelection();
else if (action === 'delete-segment') manager.deleteSelectedSegment();
});
});
// Playback rate control
var rateSelect = container.querySelector('.playback-rate-select');
if (rateSelect) {
rateSelect.addEventListener('change', function() {
manager.setPlaybackRate(parseFloat(this.value));
});
}
// Set default label
var firstLabelBtn = container.querySelector('.label-btn');
if (firstLabelBtn) firstLabelBtn.click();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initWhenReady);
} else {
initWhenReady();
}
})();
</script>
</fieldset>
</form>
<form id="audio_quality" class="annotation-form radio shadcn-radio-container" action="javascript:void(0)" data-annotation-id="1" data-annotation-type="radio" data-schema-name="audio_quality" data-grid-columns="1">
<fieldset schema="audio_quality">
<legend class="shadcn-radio-title">How is the overall audio quality?</legend>
<div class="shadcn-radio-options">
<div class="shadcn-radio-option">
<input class="audio_quality shadcn-radio-input annotation-input"
type="radio"
id="audio_quality_Clear_radio"
name="audio_quality"
value="Clear"
selection_constraint="single"
schema="audio_quality"
label_name="Clear"
onclick="onlyOne(this);registerAnnotation(this);"
validation=""
data-key="c">
<label for="audio_quality_Clear_radio" class="shadcn-radio-label" >Clear <span class="keybinding-badge">C</span></label>
</div>
<div class="shadcn-radio-option">
<input class="audio_quality shadcn-radio-input annotation-input"
type="radio"
id="audio_quality_Noisy_radio"
name="audio_quality"
value="Noisy"
selection_constraint="single"
schema="audio_quality"
label_name="Noisy"
onclick="onlyOne(this);registerAnnotation(this);"
validation=""
data-key="n">
<label for="audio_quality_Noisy_radio" class="shadcn-radio-label" >Noisy <span class="keybinding-badge">N</span></label>
</div>
<div class="shadcn-radio-option">
<input class="audio_quality shadcn-radio-input annotation-input"
type="radio"
id="audio_quality_Very noisy_radio"
name="audio_quality"
value="Very noisy"
selection_constraint="single"
schema="audio_quality"
label_name="Very noisy"
onclick="onlyOne(this);registerAnnotation(this);"
validation=""
data-key="v">
<label for="audio_quality_Very noisy_radio" class="shadcn-radio-label" >Very Noisy <span class="keybinding-badge">V</span></label>
</div>
</div></fieldset></form>
</div>
</div>
<!-- Structured data elements (var_elems) -->
{{ var_elems | safe }}
</div>
</div>
</div>
<!-- Navigation buttons -->
<div class="potato-nav">
{% if can_go_back | default(true) %}
<button class="shadcn-button shadcn-button-outline" id="prev-btn" onclick="navigateToPrevious()">
<i class="fas fa-arrow-left me-2"></i>{{ ui_lang.previous_button }}
</button>
{% else %}
<div id="prev-btn-placeholder"></div>
{% endif %}
<button class="shadcn-button shadcn-button-primary" id="next-btn" onclick="navigateToNext()">
{{ ui_lang.next_button }}<i class="fas fa-arrow-right ms-2"></i>
</button>
</div>
{% if custom_footer_html | default('') %}
<!-- Custom footer (e.g., HF Space promotional banner) -->
{{ custom_footer_html | safe }}
{% endif %}
<!-- Footer -->
<footer class="potato-footer">
{{ ui_lang.powered_by|default('Powered by') }} <a href="https://potatoannotator.com/" target="_blank" rel="noopener noreferrer">Potato</a>
<span class="footer-sep">&middot;</span>
<a href="https://github.com/davidjurgens/potato" target="_blank" rel="noopener noreferrer">GitHub</a>
<span class="footer-sep">&middot;</span>
<a href="https://github.com/davidjurgens/potato#cite-us" target="_blank" rel="noopener noreferrer">{{ ui_lang.cite_us|default('Cite Us') }}</a>
</footer>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Configuration from server -->
<script type="text/javascript">
window.config = {
annotation_task_name: {{ annotation_task_name | tojson }},
is_annotation_page: {{ 'true' if is_annotation_page else 'false' }},
debug: {{ 'true' if debug_mode else 'false' }},
ui_debug: {{ 'true' if ui_debug else 'false' }},
server_debug: {{ 'true' if server_debug else 'false' }},
debug_phase: {{ debug_phase | tojson if debug_phase else 'null' }},
api_key: "test_api_key",
username: {{ username | tojson }},
url_prefix: {{ url_prefix | default('') | tojson }},
ui_config: {}
};
window.potatoUrl = function(path) {
var prefix = (window.config && window.config.url_prefix) || '';
if (!prefix || typeof path !== 'string' || path.charAt(0) !== '/') {
return path;
}
// Leave protocol-relative URLs ("//host/...") untouched — they are
// absolute to another origin, not app-root-relative.
if (path.charAt(1) === '/') {
return path;
}
if (path.indexOf(prefix + '/') === 0 || path === prefix) {
return path;
}
return prefix + path;
};
(function() {
var prefix = (window.config && window.config.url_prefix) || '';
if (!prefix) return;
var originalFetch = window.fetch;
if (originalFetch) {
window.fetch = function(input, init) {
if (typeof input === 'string') {
input = window.potatoUrl(input);
} else if (input && typeof input.url === 'string') {
var prefixedUrl = window.potatoUrl(input.url);
if (prefixedUrl !== input.url) {
input = new Request(prefixedUrl, input);
}
}
return originalFetch.call(this, input, init);
};
}
var originalSendBeacon = navigator.sendBeacon;
if (originalSendBeacon) {
navigator.sendBeacon = function(url, data) {
return originalSendBeacon.call(this, window.potatoUrl(url), data);
};
}
// Server-Sent Events (live agent / live coding viewers) open
// root-relative stream URLs that fetch/sendBeacon patching misses.
// Wrap the constructor so new EventSource('/api/.../stream') resolves
// under the mounted prefix. Note: SSE behind a path-prefix proxy also
// needs `proxy_buffering off` on the stream location (see docs).
var OriginalEventSource = window.EventSource;
if (OriginalEventSource) {
window.EventSource = function(url, config) {
return new OriginalEventSource(window.potatoUrl(url), config);
};
// Preserve instanceof checks and inherited members.
window.EventSource.prototype = OriginalEventSource.prototype;
['CONNECTING', 'OPEN', 'CLOSED'].forEach(function(key) {
window.EventSource[key] = OriginalEventSource[key];
});
}
function prefixAttribute(root, selector, attribute) {
var elements = [];
if (root.matches && root.matches(selector)) {
elements.push(root);
}
root.querySelectorAll(selector).forEach(function(element) {
elements.push(element);
});
elements.forEach(function(element) {
var value = element.getAttribute(attribute);
if (value && value.charAt(0) === '/') {
element.setAttribute(attribute, window.potatoUrl(value));
}
});
}
function rewriteRootRelativeUrls(root) {
root = root || document;
prefixAttribute(root, 'a[href^="/"]', 'href');
prefixAttribute(root, 'form[action^="/"]', 'action');
prefixAttribute(root, 'img[src^="/"]', 'src');
prefixAttribute(root, 'video[src^="/"]', 'src');
prefixAttribute(root, 'audio[src^="/"]', 'src');
prefixAttribute(root, 'source[src^="/"]', 'src');
}
document.addEventListener('DOMContentLoaded', function() {
rewriteRootRelativeUrls(document);
if (window.MutationObserver) {
new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1) {
rewriteRootRelativeUrls(node);
}
});
});
}).observe(document.documentElement, {childList: true, subtree: true});
}
});
})();
// Conditionally enable/disable console logging based on ui_debug setting
if (!window.config.ui_debug) {
// Store original console methods
window._originalConsole = {
log: console.log,
debug: console.debug,
info: console.info,
warn: console.warn
};
// Override with no-ops (keep error for critical issues)
console.log = function() {};
console.debug = function() {};
console.info = function() {};
// Keep warn for important warnings
}
// Helper function to enable UI logging at runtime (for debugging in browser console)
window.enableUIDebug = function() {
if (window._originalConsole) {
console.log = window._originalConsole.log;
console.debug = window._originalConsole.debug;
console.info = window._originalConsole.info;
console.warn = window._originalConsole.warn;
console.log('UI debug logging enabled');
}
};
</script>
<script id="ui-config" type="application/json">{% if ui_config %}{{ ui_config | tojson | safe }}{% else %}{}{% endif %}</script>
<script>
(function(){
try {
var el = document.getElementById('ui-config');
if (el && el.textContent && el.textContent.trim().length > 0) {
window.config.ui_config = JSON.parse(el.textContent);
}
} catch (e) { console.warn('Failed to parse ui_config', e); }
})();
</script>
<!-- Load annotation JavaScript -->
<script src="{{ url_for('static', filename='interaction_tracker.js') }}?v=1"></script>
<script src="{{ url_for('static', filename='span-core.js') }}?v=11"></script>
<script src="{{ url_for('static', filename='ai_assistant_manager.js') }}?v=1"></script>
<!-- Display Logic Manager (conditional schema branching) - must load before annotation.js -->
<script src="{{ url_for('static', filename='display-logic.js') }}?v=1"></script>
<!-- Instance display manager (for new instance_display configuration) -->
{% if has_instance_display | default(false) %}
<script src="{{ url_for('static', filename='instance-display.js') }}?v=2"></script>
{% endif %}
<script src="{{ url_for('static', filename='annotation.js') }}?v=23"></script>
<!-- Fabric.js for image annotation (loaded from CDN) -->
{% if frontend_assets.image_annotation | default(false) %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>
<!-- Image annotation manager -->
<script src="{{ url_for('static', filename='image-annotation.js') }}?v=2"></script>
{% endif %}
<!-- Visual AI Assistant for image/video annotation (loaded conditionally) -->
{% if ai_enabled and ((frontend_assets.image_annotation | default(false)) or (frontend_assets.video_annotation | default(false))) %}
<script src="{{ url_for('static', filename='visual_ai_assistant.js') }}?v=1"></script>
<!-- Option Highlight Manager for dimming less-likely options -->
<script src="{{ url_for('static', filename='option_highlight_manager.js') }}?v=2"></script>
{% endif %}
<!-- Peaks.js for audio/video annotation (stored locally for reliability) -->
{% if (frontend_assets.audio_annotation | default(false)) or (frontend_assets.video_annotation | default(false)) %}
<script src="{{ url_for('static', filename='peaks.min.js') }}"></script>
{% endif %}
<!-- Audio annotation manager -->
{% if frontend_assets.audio_annotation | default(false) %}
<script src="{{ url_for('static', filename='audio-annotation.js') }}?v=2"></script>
{% endif %}
<!-- Video annotation manager -->
{% if frontend_assets.video_annotation | default(false) %}
<script src="{{ url_for('static', filename='video-annotation.js') }}?v=10"></script>
{% endif %}
<!-- Span link manager -->
{% if frontend_assets.span_link | default(false) %}
<script src="{{ url_for('static', filename='span-link-manager.js') }}?v=7"></script>
{% endif %}
<!-- Event annotation manager -->
{% if frontend_assets.event_annotation | default(false) %}
<script src="{{ url_for('static', filename='event-annotation.js') }}?v=1"></script>
{% endif %}
<!-- Entity linking for knowledge bases (Wikidata, UMLS) -->
<script src="{{ url_for('static', filename='entity-linking.js') }}?v=1"></script>
<!-- Coreference chain manager -->
{% if frontend_assets.coreference | default(false) %}
<script src="{{ url_for('static', filename='coreference-manager.js') }}?v=1"></script>
{% endif %}
<!-- Segmentation tools -->
{% if frontend_assets.segmentation_tools | default(false) %}
<script src="{{ url_for('static', filename='segmentation-tools.js') }}?v=1"></script>
{% endif %}
<!-- Conversation tree interaction -->
{% if frontend_assets.conversation_tree | default(false) %}
<script src="{{ url_for('static', filename='conversation-tree.js') }}?v=1"></script>
{% endif %}
<!-- Video tracking interpolation and UI -->
{% if frontend_assets.tracking | default(false) %}
<script src="{{ url_for('static', filename='tracking-interpolation.js') }}?v=1"></script>
<script src="{{ url_for('static', filename='tracking-ui.js') }}?v=1"></script>
{% endif %}
<!-- Triage annotation manager -->
{% if frontend_assets.triage | default(false) %}
<script src="{{ url_for('static', filename='triage.js') }}?v=1"></script>
{% endif %}
<!-- Tiered annotation manager (ELAN-style hierarchical annotation) -->
{% if frontend_assets.tiered_annotation | default(false) %}
<script src="{{ url_for('static', filename='tiered-annotation.js') }}?v=1"></script>
{% endif %}
<!-- Document bounding box annotation -->
{% if frontend_assets.document_bbox | default(false) %}
<script src="{{ url_for('static', filename='document-bbox.js') }}?v=3"></script>
{% endif %}
<!-- PDF bounding box annotation -->
{% if frontend_assets.pdf_bbox | default(false) %}
<script src="{{ url_for('static', filename='pdf-bbox.js') }}?v=3"></script>
{% endif %}
{% if agent_proxy_enabled | default(false) %}
<!-- Agent Chat UI -->
<script src="{{ url_for('static', filename='agent-chat.js') }}?v=1"></script>
{% endif %}
{% if chat_enabled | default(false) %}
<!-- LLM Chat Sidebar -->
<script src="{{ url_for('static', filename='llm-chat-sidebar.js') }}?v=1"></script>
{% endif %}
<!-- Web Agent Trace Viewer (overlays + step navigation) -->
{% if frontend_assets.web_agent_viewer | default(false) %}
<script src="{{ url_for('static', filename='web-agent-overlays.js') }}?v=1"></script>
<script src="{{ url_for('static', filename='web-agent-viewer.js') }}?v=1"></script>
{% endif %}
{% if frontend_assets.web_agent_playback | default(false) %}
<script src="{{ url_for('static', filename='web-agent-playback.js') }}?v=1"></script>
{% endif %}
<!-- Web Agent Interaction Recorder (creation mode) -->
{% if frontend_assets.web_agent_recorder | default(false) %}
<script src="{{ url_for('static', filename='web-agent-recorder.js') }}?v=1"></script>
{% endif %}
<!-- Live Agent Viewer (real-time agent interaction) -->
{% if live_agent_enabled | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/live-agent.css') }}?v=1">
<script src="{{ url_for('static', filename='live-agent-viewer.js') }}?v=1"></script>
{% endif %}
<!-- Global keyboard debug (temporary) -->
<script>
window.addEventListener('keydown', function(e) {
console.log('[GLOBAL DEBUG] keydown:', e.key, 'code:', e.code, 'target:', e.target.tagName);
}, true);
</script>
<!-- Instance height limit functionality -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Apply maximum instance height if configured
if (window.config && window.config.ui_config && window.config.ui_config.max_instance_height) {
const maxHeight = window.config.ui_config.max_instance_height;
const instanceTextContainer = document.getElementById('instance-text');
if (instanceTextContainer) {
// Set CSS custom property for max height
instanceTextContainer.style.setProperty('--max-instance-height', maxHeight + 'px');
// Add the max-height-limited class to enable scrolling
instanceTextContainer.classList.add('max-height-limited');
console.log('Applied maximum instance height:', maxHeight + 'px');
}
}
});
</script>
<!-- Universal Memos sidebar (self-hides if annotation_ui.memos is off:
the API returns 503 and the toggle stays hidden) -->
{% if is_annotation_page %}
<button type="button" id="memo-panel-toggle" class="memo-panel-toggle" hidden>
&#128221; Notes
</button>
<div id="memo-panel" class="memo-panel" hidden aria-label="Notes panel">
<div class="memo-panel-header">
<span>Notes</span>
<button type="button" id="memo-panel-close" aria-label="Close notes">&times;</button>
</div>
<div id="memo-list" class="memo-list"></div>
<div class="memo-composer">
<textarea id="memo-new-body" placeholder="Add a note about this instance&hellip;"></textarea>
<div id="memo-anchor-wrap" class="memo-anchor-hint" hidden>
<label>
<input type="checkbox" id="memo-anchor-check">
Attach to selected text: &ldquo;<span id="memo-anchor-quote"></span>&rdquo;
</label>
</div>
<div class="memo-composer-row">
<label>Visibility
<select id="memo-new-visibility">
<option value="private">Private</option>
<option value="shared">Shared</option>
</select>
</label>
<button type="button" id="memo-add-btn" class="memo-primary">Add note</button>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='memos.js') }}?v=1"></script>
<!-- Annotator search-and-claim sidebar (self-hides unless
search.annotator_claim is enabled: the enable-probe only gets
400 when the feature is on and the user is authed). -->
<button type="button" id="search-panel-toggle" class="search-panel-toggle" hidden>
&#128269; Find
</button>
<div id="search-panel" class="search-panel" hidden aria-label="Search panel">
<div class="search-panel-header">
<span>Find instances</span>
<button type="button" id="search-panel-close" aria-label="Close search">&times;</button>
</div>
<div class="search-form">
<input type="text" id="search-q" placeholder="Search instance text&hellip;"
aria-label="Search query">
<button type="button" id="search-go" class="search-primary">Search</button>
</div>
<div id="search-results" class="search-results" aria-live="polite">
<div class="search-empty">Search to find instances, then Claim
one to add it to your queue.</div>
</div>
</div>
<script src="{{ url_for('static', filename='search.js') }}?v=3"></script>
<!-- Universal Codebook tray (self-hides unless the codebook is
enabled: the enable-probe is not 200 otherwise). The composer
only appears when codebook_mode is extensible/open. -->
<button type="button" id="cb-panel-toggle" class="cb-panel-toggle" hidden>
&#127991; Codebook
</button>
<div id="cb-panel" class="cb-panel" hidden role="region"
aria-label="Codebook panel">
<div class="cb-panel-header">
<span>Codebook</span>
<button type="button" id="cb-panel-close"
aria-label="Close codebook">&times;</button>
</div>
<div id="cb-stale-banner" class="cb-stale-banner" hidden>
<span class="cb-stale-msg" id="cb-stale-msg"
role="status"></span>
<button type="button" id="cb-stale-dismiss"
class="cb-stale-x"
aria-label="Dismiss this notice">&times;</button>
</div>
<div id="cb-tree" class="cb-tree" aria-live="polite">
<div class="cb-empty">Loading&hellip;</div>
</div>
<div class="cb-worklist-section" id="cb-worklist-section" hidden>
<h2 id="cb-worklist-head" class="cb-worklist-head">Review</h2>
<div id="cb-worklist" class="cb-worklist"
aria-labelledby="cb-worklist-head">
<div class="cb-empty">Nothing to review.</div>
</div>
</div>
<div id="cb-admin-section" class="cb-admin-section" hidden>
<h2 class="cb-admin-head">Curate (admin)</h2>
<div class="cb-admin-group">
<label class="cb-admin-label"
for="cb-merge-src">Merge a code into another</label>
<div class="cb-admin-row">
<select id="cb-merge-src" class="cb-admin-select"
aria-label="Code to merge from"></select>
<span class="cb-admin-arrow" aria-hidden="true"></span>
<select id="cb-merge-dst" class="cb-admin-select"
aria-label="Code to merge into"></select>
</div>
<button type="button" id="cb-merge-btn"
class="cb-primary">Merge</button>
</div>
<div class="cb-admin-group">
<label class="cb-admin-label"
for="cb-split-src">Split a code by annotator</label>
<div class="cb-admin-row">
<select id="cb-split-src" class="cb-admin-select"
aria-label="Code to split"></select>
<input type="text" id="cb-split-annotator"
class="cb-admin-input"
placeholder="annotator"
aria-label="Annotator to split out">
</div>
<input type="text" id="cb-split-name"
class="cb-admin-input"
placeholder="New code name&hellip;"
aria-label="New code name">
<button type="button" id="cb-split-btn"
class="cb-primary">Split</button>
</div>
<div id="cb-admin-error" class="cb-error" hidden
role="alert" tabindex="-1"></div>
<div id="cb-admin-status" class="cb-admin-status"
role="status" aria-live="polite"></div>
<div class="cb-admin-group">
<h3 class="cb-admin-sub" id="cb-proposals-head">
Pending proposals</h3>
<div id="cb-proposals" class="cb-proposals"
aria-labelledby="cb-proposals-head"
aria-live="polite">
<div class="cb-empty">No pending proposals.</div>
</div>
</div>
<details class="cb-admin-group cb-changes-wrap">
<summary class="cb-admin-sub">Recent changes</summary>
<ul id="cb-changes" class="cb-changes-list"></ul>
</details>
</div>
<div id="cb-composer" class="cb-composer" hidden>
<div class="cb-composer-row">
<input type="text" id="cb-new-name"
placeholder="Add a code&hellip;"
aria-label="New code name">
<button type="button" id="cb-add-btn"
class="cb-primary">Add</button>
</div>
<div id="cb-error" class="cb-error" hidden></div>
<div id="cb-mode-hint" class="cb-mode-hint"></div>
</div>
</div>
<script src="{{ url_for('static', filename='codebook.js') }}?v=10"></script>
{% endif %}
</body>
</html>