codebook / potato /templates /generated /Agent-Trajectory-Evaluation-base_template_v2.html
davidjurgens's picture
Deploy: Potato — Codebook Annotation
aceb1b2 verified
Raw
History Blame Contribute Delete
64.7 kB
<!-- CONFIG_HASH: a26ad7a42ddec00ebb17dd83674016ae -->
{# 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>Agent Trajectory Evaluation</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">Agent Trajectory Evaluation</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: a26ad7a42ddec00ebb17dd83674016ae_3964352617691f36c1da1545fcf44452 -->
<!-- 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="step_evaluation" class="annotation-form trajectory-eval-container"
action="javascript:void(0)"
data-annotation-id="0"
data-annotation-type="trajectory_eval"
data-schema-name="step_evaluation"
data-steps-key="steps"
data-step-text-key="action"
data-grid-columns="1">
<fieldset schema="step_evaluation">
<legend class="traj-eval-title">Evaluate each agent step for correctness and errors</legend>
<div class="traj-score" id="step_evaluation-score">
Score: <strong id="step_evaluation-score-value">100</strong> / 100
</div>
<!-- Step cards rendered by JS from instance data -->
<div class="traj-steps-container" id="step_evaluation-steps"></div>
<input type="hidden"
class="annotation-input trajectory-eval-data-input"
id="step_evaluation_step_evaluation_hidden"
name="step_evaluation:::step_evaluation"
schema="step_evaluation"
label_name="step_evaluation"
validation=""
value="">
</fieldset>
</form>
<script>
(function() {
var _trajState = {};
var SCHEMA = 'step_evaluation';
var CONFIG = {"steps_key": "steps", "step_text_key": "action", "correctness_options": ["correct", "incorrect", "partially_correct"], "error_types": [{"name": "reasoning", "subtypes": ["logical_error", "factual_error", "planning_error"]}, {"name": "execution", "subtypes": ["wrong_tool", "wrong_args", "api_error"]}, {"name": "safety", "subtypes": ["harmful_action", "data_leak", "scope_violation"]}], "severities": [{"name": "minor", "weight": -1}, {"name": "major", "weight": -5}, {"name": "critical", "weight": -10}], "show_score": true, "max_score": 100};
var TYPE_OPTIONS_HTML = "<option value=\"\">-- select error type --</option><optgroup label=\"reasoning\"><option value=\"reasoning::logical_error\">logical_error</option><option value=\"reasoning::factual_error\">factual_error</option><option value=\"reasoning::planning_error\">planning_error</option></optgroup><optgroup label=\"execution\"><option value=\"execution::wrong_tool\">wrong_tool</option><option value=\"execution::wrong_args\">wrong_args</option><option value=\"execution::api_error\">api_error</option></optgroup><optgroup label=\"safety\"><option value=\"safety::harmful_action\">harmful_action</option><option value=\"safety::data_leak\">data_leak</option><option value=\"safety::scope_violation\">scope_violation</option></optgroup>";
var SEVERITY_RADIOS_TEMPLATE = "\n <label class=\"traj-severity-option\">\n <input type=\"radio\" class=\"traj-severity-radio\"\n name=\"traj-severity-STEPIDX-step_evaluation\"\n value=\"minor\">\n <span>minor (-1)</span>\n </label>\n <label class=\"traj-severity-option\">\n <input type=\"radio\" class=\"traj-severity-radio\"\n name=\"traj-severity-STEPIDX-step_evaluation\"\n value=\"major\">\n <span>major (-5)</span>\n </label>\n <label class=\"traj-severity-option\">\n <input type=\"radio\" class=\"traj-severity-radio\"\n name=\"traj-severity-STEPIDX-step_evaluation\"\n value=\"critical\">\n <span>critical (-10)</span>\n </label>";
var CORRECTNESS_BTNS_TEMPLATE = "<button type=\"button\" class=\"traj-correctness-btn traj-correctness-correct\" data-value=\"correct\">Correct</button> <button type=\"button\" class=\"traj-correctness-btn traj-correctness-incorrect\" data-value=\"incorrect\">Incorrect</button> <button type=\"button\" class=\"traj-correctness-btn traj-correctness-partially_correct\" data-value=\"partially_correct\">Partially Correct</button> ";
function getState() {
if (!_trajState[SCHEMA]) {
_trajState[SCHEMA] = { steps: [] };
}
return _trajState[SCHEMA];
}
/* ---- build step cards from instance data ---- */
function buildStepCards() {
var container = document.getElementById(SCHEMA + '-steps');
if (!container) return;
// Read steps from the instance data embedded in the page
var steps = [];
try {
var instanceDataEl = document.querySelector('[data-instance-json]');
if (instanceDataEl) {
var instanceData = JSON.parse(instanceDataEl.getAttribute('data-instance-json'));
steps = instanceData[CONFIG.steps_key] || [];
}
} catch(e) {}
// Fallback: try reading from the displayed text content
if (!steps.length) {
var textEl = document.getElementById('text-content') || document.getElementById('instance-text');
if (textEl) {
try {
var parsed = JSON.parse(textEl.textContent || textEl.innerText);
if (parsed && parsed[CONFIG.steps_key]) {
steps = parsed[CONFIG.steps_key];
}
} catch(e2) {}
}
}
if (!steps.length) {
// Show waiting state instead of error — steps may arrive
// dynamically via live agent SSE events
container.innerHTML = '<div class="traj-no-steps traj-waiting-steps">' +
'Waiting for agent steps… ' +
'<span style="font-size:0.85em;color:#999;">(steps will appear here as the agent runs)</span></div>';
return;
}
var state = getState();
if (!state.steps.length) {
steps.forEach(function(_, i) {
state.steps.push({ step_index: i, correctness: null });
});
}
container.innerHTML = '';
steps.forEach(function(step, idx) {
_appendStepCard(container, step, idx);
});
attachStepHandlers();
}
/* ---- render a single step card ---- */
function _buildStepCardHtml(step, idx) {
var stepText = typeof step === 'string'
? step
: (step[CONFIG.step_text_key] || step.action_type || JSON.stringify(step));
// Include the thought if available (VLM chain-of-thought)
var thoughtHtml = '';
if (step.thought) {
thoughtHtml = '<div class="traj-step-thought">' +
'<span class="traj-thought-label">Thought:</span> ' +
escapeHtml(step.thought) + '</div>';
}
var corBtns = CORRECTNESS_BTNS_TEMPLATE.replace(/STEPIDX/g, idx);
var sevRadios = SEVERITY_RADIOS_TEMPLATE.replace(/STEPIDX/g, idx);
return '<div class="traj-step-card" data-step-index="' + idx + '">' +
'<div class="traj-step-header">' +
'<span class="traj-step-number">Step ' + (idx + 1) + '</span>' +
'<span class="traj-step-status" id="' + SCHEMA + '-status-' + idx + '"></span>' +
'</div>' +
'<div class="traj-step-text">' + escapeHtml(stepText) + '</div>' +
thoughtHtml +
'<div class="traj-correctness-row">' + corBtns + '</div>' +
'<div class="traj-error-details" id="' + SCHEMA + '-error-' + idx + '" style="display:none;">' +
'<div class="traj-field"><label>Error Type:</label>' +
'<select class="traj-error-type" data-step="' + idx + '">' + TYPE_OPTIONS_HTML + '</select>' +
'</div>' +
'<div class="traj-field"><label>Severity:</label>' +
'<div class="traj-severity-group">' + sevRadios + '</div>' +
'</div>' +
'<div class="traj-field"><label>Rationale:</label>' +
'<textarea class="traj-rationale" data-step="' + idx + '" rows="2" placeholder="Explain the error..."></textarea>' +
'</div>' +
'</div>' +
'</div>';
}
function _appendStepCard(container, step, idx) {
var div = document.createElement('div');
div.innerHTML = _buildStepCardHtml(step, idx);
var card = div.firstElementChild;
container.appendChild(card);
_attachSingleCardHandlers(card, idx);
}
/* ---- add a step dynamically (called by live agent SSE) ---- */
function addStep(stepData) {
var container = document.getElementById(SCHEMA + '-steps');
if (!container) return;
// Clear the "waiting" message if present
var waiting = container.querySelector('.traj-waiting-steps');
if (waiting) waiting.remove();
var state = getState();
var idx = state.steps.length;
state.steps.push({ step_index: idx, correctness: null });
_appendStepCard(container, stepData, idx);
saveState();
}
/* ---- event handlers ---- */
function _attachSingleCardHandlers(card, idx) {
card.querySelectorAll('.traj-correctness-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var value = btn.getAttribute('data-value');
var state = getState();
if (state.steps[idx] && state.steps[idx].correctness === value) {
state.steps[idx] = { step_index: idx, correctness: null };
} else {
if (!state.steps[idx]) state.steps[idx] = { step_index: idx };
state.steps[idx].correctness = value;
}
card.querySelectorAll('.traj-correctness-btn').forEach(function(b) {
b.classList.remove('selected');
});
if (state.steps[idx].correctness === value) {
btn.classList.add('selected');
}
var errorDiv = document.getElementById(SCHEMA + '-error-' + idx);
if (errorDiv) {
errorDiv.style.display = (value === 'correct' || !state.steps[idx].correctness) ? 'none' : 'block';
}
updateStepStatus(idx);
saveState();
});
});
card.querySelectorAll('.traj-error-type').forEach(function(sel) {
sel.addEventListener('change', function() {
var state = getState();
if (!state.steps[idx]) return;
var parts = sel.value.split('::');
state.steps[idx].error_type = parts[0] || '';
state.steps[idx].error_subtype = parts[1] || '';
saveState();
});
});
card.querySelectorAll('.traj-severity-group input[type="radio"]').forEach(function(radio) {
radio.addEventListener('change', function() {
var state = getState();
if (!state.steps[idx]) return;
state.steps[idx].severity = radio.value;
updateScore();
saveState();
});
});
card.querySelectorAll('.traj-rationale').forEach(function(ta) {
ta.addEventListener('input', function() {
var state = getState();
if (!state.steps[idx]) return;
state.steps[idx].rationale = ta.value;
saveState();
});
});
}
function attachStepHandlers() {
var container = document.getElementById(SCHEMA + '-steps');
if (!container) return;
container.querySelectorAll('.traj-step-card').forEach(function(card) {
var idx = parseInt(card.getAttribute('data-step-index'), 10);
_attachSingleCardHandlers(card, idx);
});
}
function updateStepStatus(idx) {
var statusEl = document.getElementById(SCHEMA + '-status-' + idx);
if (!statusEl) return;
var state = getState();
var s = state.steps[idx];
if (!s || !s.correctness) {
statusEl.textContent = '';
statusEl.className = 'traj-step-status';
} else {
statusEl.textContent = s.correctness.replace('_', ' ');
statusEl.className = 'traj-step-status traj-status-' + s.correctness;
}
}
function updateScore() {
if (!CONFIG.show_score) return;
var scoreEl = document.getElementById(SCHEMA + '-score-value');
if (!scoreEl) return;
var state = getState();
var penalty = 0;
state.steps.forEach(function(s) {
if (s.severity) {
var sev = CONFIG.severities.find(function(sv) { return sv.name === s.severity; });
if (sev) penalty += Math.abs(sev.weight);
}
});
scoreEl.textContent = Math.max(0, CONFIG.max_score - penalty);
}
/* ---- persistence ---- */
function saveState() {
var state = getState();
var score = CONFIG.max_score;
state.steps.forEach(function(s) {
if (s.severity) {
var sev = CONFIG.severities.find(function(sv) { return sv.name === s.severity; });
if (sev) score -= Math.abs(sev.weight);
}
});
score = Math.max(0, score);
var data = JSON.stringify({ steps: state.steps, score: score });
var input = document.getElementById(SCHEMA).querySelector('.trajectory-eval-data-input');
if (input) {
input.value = data;
input.setAttribute('data-modified', 'true');
input.dispatchEvent(new Event('change', { bubbles: true }));
}
}
function restoreFromHiddenInput() {
var input = document.getElementById(SCHEMA).querySelector('.trajectory-eval-data-input');
if (!input) return false;
var val = input.getAttribute('value') || input.value;
if (!val) return false;
try {
var data = JSON.parse(val);
if (data && data.steps && data.steps.length) {
var state = getState();
state.steps = data.steps;
return true;
}
} catch(e) {}
return false;
}
function restoreVisualState() {
var state = getState();
var container = document.getElementById(SCHEMA + '-steps');
if (!container) return;
state.steps.forEach(function(s, idx) {
var card = container.querySelector('[data-step-index="' + idx + '"]');
if (!card) return;
// Restore correctness button
if (s.correctness) {
card.querySelectorAll('.traj-correctness-btn').forEach(function(btn) {
if (btn.getAttribute('data-value') === s.correctness) {
btn.classList.add('selected');
}
});
// Show error details if not correct
var errorDiv = document.getElementById(SCHEMA + '-error-' + idx);
if (errorDiv && s.correctness !== 'correct') {
errorDiv.style.display = 'block';
}
}
// Restore error type
if (s.error_type) {
var typeVal = s.error_subtype ? s.error_type + '::' + s.error_subtype : s.error_type;
var sel = card.querySelector('.traj-error-type');
if (sel) sel.value = typeVal;
}
// Restore severity
if (s.severity) {
var radio = card.querySelector('input[type="radio"][value="' + s.severity + '"]');
if (radio) radio.checked = true;
}
// Restore rationale
if (s.rationale) {
var ta = card.querySelector('.traj-rationale');
if (ta) ta.value = s.rationale;
}
updateStepStatus(idx);
});
updateScore();
}
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
/* ---- initialization ---- */
// CRITICAL: Check hidden input for server-restored value BEFORE
// building default state (prevents IIFE overwrite bug).
var hadServerData = restoreFromHiddenInput();
// Build step cards — uses a MutationObserver to wait for instance data
// to appear if it hasn't yet (async loading).
function tryBuildCards() {
buildStepCards();
if (hadServerData) {
restoreVisualState();
}
}
if (document.readyState === 'complete') {
tryBuildCards();
} else {
document.addEventListener('DOMContentLoaded', tryBuildCards);
}
// Expose for annotation.js restore pipeline
window._trajState = _trajState;
window._trajGetState = getState;
window._trajBuildStepCards = buildStepCards;
window._trajRestoreVisualState = restoreVisualState;
window._trajSaveState = saveState;
window._trajAddStep = addStep;
// Listen for live agent step events so trajectory cards appear
// as the agent runs (dispatched by live-agent-viewer.js SSE handler)
document.addEventListener('live-agent-step', function(e) {
if (e.detail) addStep(e.detail);
});
})();
</script>
</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>