codebook / potato /templates /generated /AI-Assisted-Object-Detection-base_template_v2.html
davidjurgens's picture
Deploy: Potato — Codebook Annotation
aceb1b2 verified
Raw
History Blame Contribute Delete
63.3 kB
<!-- CONFIG_HASH: ca2dc8e0a11ec6944a605c0450095d28 -->
{# 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>AI-Assisted Object Detection</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">AI-Assisted Object Detection</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: ca2dc8e0a11ec6944a605c0450095d28_b775c591a9f133adc4f2cf5244c47050 -->
<!-- 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="object_detection" class="annotation-form image-annotation" action="javascript:void(0)">
<fieldset schema="object_detection">
<legend>Detect and label objects in the image. Use AI assist to get suggestions.</legend>
<!-- Image Annotation Container -->
<div class="image-annotation-container" data-schema="object_detection" data-ai-enabled="true">
<!-- Toolbar -->
<div class="image-annotation-toolbar">
<!-- Tool buttons -->
<div class="tool-group">
<span class="tool-group-label">Tools:</span>
<button type="button" class="tool-btn" data-tool="bbox" title="Bounding Box (B)">□ Box</button>
<button type="button" class="tool-btn" data-tool="polygon" title="Polygon (P)">⬡ Polygon</button>
</div>
<!-- Label selector -->
<div class="label-group">
<span class="tool-group-label">Label:</span>
<button type="button" class="label-btn" data-label="person" data-color="#FF6B6B" title="person (1)" style="--label-color: #FF6B6B;"><span class="label-color-dot" style="background-color: #FF6B6B;"></span>person</button>
<button type="button" class="label-btn" data-label="car" data-color="#4ECDC4" title="car (2)" style="--label-color: #4ECDC4;"><span class="label-color-dot" style="background-color: #4ECDC4;"></span>car</button>
<button type="button" class="label-btn" data-label="dog" data-color="#45B7D1" title="dog (3)" style="--label-color: #45B7D1;"><span class="label-color-dot" style="background-color: #45B7D1;"></span>dog</button>
<button type="button" class="label-btn" data-label="bicycle" data-color="#96CEB4" title="bicycle (4)" style="--label-color: #96CEB4;"><span class="label-color-dot" style="background-color: #96CEB4;"></span>bicycle</button>
<button type="button" class="label-btn" data-label="traffic light" data-color="#FFEAA7" title="traffic light (5)" style="--label-color: #FFEAA7;"><span class="label-color-dot" style="background-color: #FFEAA7;"></span>traffic light</button>
</div>
<!-- Zoom controls -->
<div class="zoom-group">
<button type="button" class="zoom-btn" data-action="zoom-in" title="Zoom In (+)">+</button>
<button type="button" class="zoom-btn" data-action="zoom-out" title="Zoom Out (-)">-</button>
<button type="button" class="zoom-btn" data-action="zoom-fit" title="Fit to View (0)">Fit</button>
<button type="button" class="zoom-btn" data-action="zoom-reset" title="Reset Zoom">100%</button>
</div>
<!-- Edit controls -->
<div class="edit-group">
<button type="button" class="edit-btn" data-action="undo" title="Undo (Ctrl+Z)">Undo</button>
<button type="button" class="edit-btn" data-action="redo" title="Redo (Ctrl+Shift+Z)">Redo</button>
<button type="button" class="edit-btn delete-btn" data-action="delete" title="Delete Selected (Del)">Delete</button>
</div>
<!-- Brush size control (shown when brush/eraser selected) -->
<div class="brush-size-group" style="display: none;">
<span class="tool-group-label">Size:</span>
<input type="range" class="brush-size-slider" min="1" max="100" value="20" title="Brush Size">
<span class="brush-size-value">20</span>
</div>
<!-- Annotation count -->
<div class="count-group">
<span class="annotation-count">Annotations: <span class="count-value">0</span></span>
</div>
</div>
<!-- AI Assistance Toolbar -->
<div class="ai-toolbar">
<div class="ai-toolbar-group">
<span class="ai-toolbar-label">AI Assist:</span>
<button type="button" class="ai-btn" data-action="detect" title="Detect objects in the image"><span class="ai-btn-icon">🔍</span> Detect</button> <button type="button" class="ai-btn" data-action="pre_annotate" title="Auto-detect and pre-annotate all objects"><span class="ai-btn-icon"></span> Auto</button> <button type="button" class="ai-btn" data-action="hint" title="Get a hint for annotation"><span class="ai-btn-icon">💡</span> Hint</button>
</div>
<div class="ai-suggestion-controls" style="display: none;">
<span class="suggestion-count">0 suggestions</span>
<button type="button" class="ai-btn ai-btn-accept" data-action="accept-all" title="Accept all suggestions">
Accept All
</button>
<button type="button" class="ai-btn ai-btn-clear" data-action="clear" title="Clear all suggestions">
Clear
</button>
</div>
<div class="ai-loading-indicator" style="display: none;">
<span class="spinner"></span> Loading...
</div>
</div>
<!-- AI Tooltip Container -->
<div class="ai-tooltip-container" style="display: none;"></div>
<!-- Canvas wrapper -->
<div class="canvas-wrapper">
<canvas id="canvas-object_detection" class="annotation-canvas"></canvas>
<!-- Mask canvas for segmentation (overlaid on top) -->
<canvas id="mask-canvas-object_detection" class="mask-canvas" style="display: none;"></canvas>
</div>
<!-- Hidden input for storing annotation data -->
<input type="hidden"
name="object_detection"
id="input-object_detection"
class="annotation-data-input"
value="">
<!-- Hidden input for storing mask data (RLE encoded) -->
<input type="hidden"
name="object_detection_masks"
id="mask-input-object_detection"
class="mask-data-input"
value="">
</div>
<!-- Initialize the annotation manager -->
<script>
(function() {
// Wait for DOM and dependencies
function initWhenReady() {
if (typeof ImageAnnotationManager === 'undefined') {
setTimeout(initWhenReady, 100);
return;
}
var container = document.querySelector('.image-annotation-container[data-schema="object_detection"]');
if (!container) return;
var config = {"schemaName": "object_detection", "tools": ["bbox", "polygon"], "labels": [{"name": "person", "color": "#FF6B6B", "key_value": "1"}, {"name": "car", "color": "#4ECDC4", "key_value": "2"}, {"name": "dog", "color": "#45B7D1", "key_value": "3"}, {"name": "bicycle", "color": "#96CEB4", "key_value": "4"}, {"name": "traffic light", "color": "#FFEAA7", "key_value": "5"}], "zoomEnabled": true, "panEnabled": true, "minAnnotations": 0, "maxAnnotations": null, "freeformBrushSize": 5, "freeformSimplify": true, "brushSize": 20, "eraserSize": 20, "maskOpacity": 0.5, "aiSupport": true, "aiFeatures": {"detection": true, "pre_annotate": true, "hint": true, "classification": false}, "sourceField": ""};
var canvasId = 'canvas-object_detection';
var inputId = 'input-object_detection';
// Get image URL from the instance display
// Priority: 1) data-image-url on #text-content, 2) img src, 3) text content itself
var instanceContainer = document.getElementById('instance-text');
console.log('[ImageAnnotation] Instance container:', instanceContainer);
var textContent = instanceContainer ? instanceContainer.querySelector('#text-content') : null;
console.log('[ImageAnnotation] Text content element:', textContent);
var imageUrl = null;
// Method 1: Check data-image-url attribute (set by template when has_image_annotation=true)
if (textContent) {
imageUrl = textContent.getAttribute('data-image-url');
if (imageUrl) {
console.log('[ImageAnnotation] Found URL from data-image-url:', imageUrl);
}
}
// Method 2: Fallback to looking for an img element with data-source-url or src
if (!imageUrl && instanceContainer) {
var imgElement = instanceContainer.querySelector('img');
if (imgElement) {
imageUrl = imgElement.getAttribute('data-source-url') || imgElement.src;
console.log('[ImageAnnotation] Found URL from img element:', imageUrl);
}
}
// Method 3: If text content looks like a URL, use it directly
if (!imageUrl && textContent) {
var textVal = textContent.textContent.trim();
if (textVal.match(/^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|svg)/i)) {
imageUrl = textVal;
console.log('[ImageAnnotation] Found URL from text content:', imageUrl);
}
}
// Method 4: Check source_field in instance data (via display fields)
if (!imageUrl && config.sourceField) {
var displayFields = document.querySelectorAll('[data-field-key="' + config.sourceField + '"]');
displayFields.forEach(function(field) {
var url = field.getAttribute('data-source-url') || field.getAttribute('src');
if (url && !imageUrl) {
imageUrl = url;
console.log('[ImageAnnotation] Found URL from source_field display:', imageUrl);
}
});
}
if (!imageUrl) {
console.error('[ImageAnnotation] No image URL found! Check that the instance data contains an image URL.');
}
// Initialize manager
console.log('Initializing ImageAnnotationManager with canvas:', canvasId);
var manager = new ImageAnnotationManager(canvasId, inputId, config);
// Store reference on container
container.annotationManager = manager;
// Load image if available
if (imageUrl) {
console.log('Calling loadImage with:', imageUrl);
manager.loadImage(imageUrl);
} else {
console.warn('No image URL found for annotation');
}
// Wire up toolbar buttons
container.querySelectorAll('.tool-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var tool = this.dataset.tool;
manager.setTool(tool);
container.querySelectorAll('.tool-btn').forEach(function(b) {
b.classList.remove('active');
});
this.classList.add('active');
// Show/hide brush size control for brush/eraser tools
var brushSizeGroup = container.querySelector('.brush-size-group');
if (brushSizeGroup) {
brushSizeGroup.style.display = (tool === 'brush' || tool === 'eraser') ? 'flex' : 'none';
}
});
});
// Wire up brush size slider
var brushSlider = container.querySelector('.brush-size-slider');
var brushSizeValue = container.querySelector('.brush-size-value');
if (brushSlider) {
brushSlider.addEventListener('input', function() {
var size = parseInt(this.value);
if (brushSizeValue) brushSizeValue.textContent = size;
manager.setBrushSize(size);
});
}
container.querySelectorAll('.label-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var label = this.dataset.label;
var color = this.dataset.color;
manager.setLabel(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.zoom(1.2);
else if (action === 'zoom-out') manager.zoom(0.8);
else if (action === 'zoom-fit') manager.zoomFit();
else if (action === 'zoom-reset') manager.zoomReset();
});
});
container.querySelectorAll('.edit-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var action = this.dataset.action;
if (action === 'undo') manager.undo();
else if (action === 'redo') manager.redo();
else if (action === 'delete') manager.deleteSelected();
});
});
// Set default tool and label
var firstToolBtn = container.querySelector('.tool-btn');
if (firstToolBtn) firstToolBtn.click();
var firstLabelBtn = container.querySelector('.label-btn');
if (firstLabelBtn) firstLabelBtn.click();
// Update count display
manager.onAnnotationChange = function(count) {
var countEl = container.querySelector('.count-value');
if (countEl) countEl.textContent = count;
};
// Initialize AI assistant if enabled and VisualAIAssistantManager is available
if (config.aiSupport && typeof VisualAIAssistantManager !== 'undefined') {
var annotationId = Array.from(document.querySelectorAll('.annotation-form')).indexOf(
document.getElementById('object_detection')
);
container.aiAssistant = new VisualAIAssistantManager({
annotationType: 'image_annotation',
annotationId: annotationId >= 0 ? annotationId : 0,
annotationManager: manager
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initWhenReady);
} else {
initWhenReady();
}
})();
</script>
</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>