| |
| {# 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>Annotation Persistence Test</title> |
|
|
| |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> |
|
|
| |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> |
|
|
| |
| <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}"> |
|
|
| |
| <link rel="stylesheet" href="{{ url_for('static', filename='span-styles.css') }}"> |
|
|
| {% set frontend_assets = frontend_assets | default({}) %} |
|
|
| |
| {% if frontend_assets.image_annotation | default(false) %} |
| <link rel="stylesheet" href="{{ url_for('static', filename='image-annotation.css') }}?v=2"> |
| {% endif %} |
|
|
| |
| {% if frontend_assets.audio_annotation | default(false) %} |
| <link rel="stylesheet" href="{{ url_for('static', filename='audio-annotation.css') }}?v=2"> |
| {% endif %} |
|
|
| |
| {% if frontend_assets.video_annotation | default(false) %} |
| <link rel="stylesheet" href="{{ url_for('static', filename='video-annotation.css') }}"> |
| {% endif %} |
|
|
| |
| {% if frontend_assets.span_link | default(false) %} |
| <link rel="stylesheet" href="{{ url_for('static', filename='span-link-styles.css') }}"> |
| {% endif %} |
|
|
| |
| {% if frontend_assets.event_annotation | default(false) %} |
| <link rel="stylesheet" href="{{ url_for('static', filename='event-annotation.css') }}"> |
| {% endif %} |
|
|
| |
| <link rel="stylesheet" href="{{ url_for('static', filename='spreadsheet-display.css') }}"> |
|
|
| |
| <link rel="stylesheet" href="{{ url_for('static', filename='coding-trace.css') }}"> |
|
|
| |
| {% if frontend_assets.live_coding_agent | default(false) %} |
| <script src="{{ url_for('static', filename='live-coding-agent-viewer.js') }}" defer></script> |
| {% endif %} |
|
|
| |
| <link rel="stylesheet" href="{{ url_for('static', filename='css/format-displays.css') }}"> |
|
|
| |
| {% if frontend_assets.document_bbox | default(false) %} |
| <link rel="stylesheet" href="{{ url_for('static', filename='document-bbox.css') }}"> |
| {% endif %} |
|
|
| |
| {% if frontend_assets.pdf_bbox | default(false) %} |
| <link rel="stylesheet" href="{{ url_for('static', filename='pdf-annotation.css') }}"> |
| {% endif %} |
|
|
| |
| <link rel="stylesheet" href="{{ url_for('static', filename='display-logic.css') }}"> |
|
|
| |
| {% if frontend_assets.coreference | default(false) %} |
| <link rel="stylesheet" href="{{ url_for('static', filename='css/coreference.css') }}"> |
| {% endif %} |
|
|
| |
| {% if frontend_assets.segmentation_tools | default(false) %} |
| <link rel="stylesheet" href="{{ url_for('static', filename='css/segmentation.css') }}"> |
| {% endif %} |
|
|
| |
| {% if frontend_assets.conversation_tree | default(false) %} |
| <link rel="stylesheet" href="{{ url_for('static', filename='css/conversation-tree.css') }}"> |
| {% endif %} |
|
|
| |
| {% if frontend_assets.tracking | default(false) %} |
| <link rel="stylesheet" href="{{ url_for('static', filename='css/tracking.css') }}"> |
| {% endif %} |
|
|
| |
| {% if frontend_assets.triage | default(false) %} |
| <link rel="stylesheet" href="{{ url_for('static', filename='css/triage.css') }}"> |
| {% endif %} |
|
|
| |
| <link rel="stylesheet" href="{{ url_for('static', filename='css/entity-linking.css') }}"> |
|
|
| |
| {% 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) %} |
| |
| <link rel="stylesheet" href="{{ url_for('static', filename='css/agent-chat.css') }}"> |
| {% endif %} |
|
|
| {% if chat_enabled | default(false) %} |
| |
| <link rel="stylesheet" href="{{ url_for('static', filename='css/llm-chat-sidebar.css') }}?v=1"> |
| {% endif %} |
|
|
| |
| <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"> |
|
|
| |
| <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> |
|
|
| |
| {{ PROJECT_BASE_CSS | safe }} |
| </head> |
|
|
| <body> |
| |
| <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">Annotation Persistence Test</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"> |
| |
| <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"> |
| |
| <div id="loading-state" class="text-center py-5"> |
| <div class="loading-spinner mb-3"></div> |
| <p>{{ ui_lang.loading }}</p> |
| </div> |
|
|
| |
| <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> |
|
|
| |
| <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 %} |
| |
| <div class="mb-4"> |
| {% if has_instance_display | default(false) %} |
| |
| {{ display_html | safe }} |
| |
| <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> |
| |
| <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> |
| |
| <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) %} |
| |
| |
| <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> |
|
|
|
|
|
|
| |
| <div id="status" class="status" style="display: none;"></div> |
| |
| <input type="hidden" id="instance_id" name="instance_id" value="{{instance_id}}"> |
|
|
| |
| |
| |
| |
| |
| <div id="instance-json-data" style="display: none;" |
| data-instance-json='{{ (instance_record if instance_record is defined and instance_record else {}) | tojson }}'></div> |
|
|
| |
| {% 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 %} |
|
|
| |
| {% 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'); |
| |
| 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 %} |
|
|
| |
| <div id="annotation-forms"> |
| |
| |
| |
| |
| |
|
|
| <div class="annotation_schema"> |
|
|
| <form id="quality_rating" class="annotation-form likert shadcn-likert-container" action="javascript:void(0)" data-annotation-id="0" data-annotation-type="likert" data-schema-name="quality_rating" data-grid-columns="1"> |
| <div class="ai-help none"><div class="tooltip"></div></div> |
| <fieldset schema="quality_rating"> |
| <legend class="shadcn-likert-title">Rate quality from 1 to 5</legend> |
| <div class="shadcn-likert-scale" style="max-width: min(100%, calc(300px + 5 * 40px + 250px));"> |
| <div class="shadcn-likert-endpoint">1</div> |
| <div class="shadcn-likert-options"> |
| <div class="shadcn-likert-track"></div> |
| |
| <div class="shadcn-likert-option"> |
| <input class="quality_rating shadcn-likert-input annotation-input" |
| type="radio" |
| id="quality_rating_1_radio" |
| name="quality_rating" |
| value="1" |
| schema="quality_rating" |
| label_name="1" |
| selection_constraint="single" |
| validation="" |
| onclick="onlyOne(this);registerAnnotation(this);"> |
| <label class="shadcn-likert-button" for="quality_rating_1_radio"></label> |
| <span class="shadcn-likert-label">1</span> |
| </div> |
| |
| <div class="shadcn-likert-option"> |
| <input class="quality_rating shadcn-likert-input annotation-input" |
| type="radio" |
| id="quality_rating_2_radio" |
| name="quality_rating" |
| value="2" |
| schema="quality_rating" |
| label_name="2" |
| selection_constraint="single" |
| validation="" |
| onclick="onlyOne(this);registerAnnotation(this);"> |
| <label class="shadcn-likert-button" for="quality_rating_2_radio"></label> |
| <span class="shadcn-likert-label">2</span> |
| </div> |
| |
| <div class="shadcn-likert-option"> |
| <input class="quality_rating shadcn-likert-input annotation-input" |
| type="radio" |
| id="quality_rating_3_radio" |
| name="quality_rating" |
| value="3" |
| schema="quality_rating" |
| label_name="3" |
| selection_constraint="single" |
| validation="" |
| onclick="onlyOne(this);registerAnnotation(this);"> |
| <label class="shadcn-likert-button" for="quality_rating_3_radio"></label> |
| <span class="shadcn-likert-label">3</span> |
| </div> |
| |
| <div class="shadcn-likert-option"> |
| <input class="quality_rating shadcn-likert-input annotation-input" |
| type="radio" |
| id="quality_rating_4_radio" |
| name="quality_rating" |
| value="4" |
| schema="quality_rating" |
| label_name="4" |
| selection_constraint="single" |
| validation="" |
| onclick="onlyOne(this);registerAnnotation(this);"> |
| <label class="shadcn-likert-button" for="quality_rating_4_radio"></label> |
| <span class="shadcn-likert-label">4</span> |
| </div> |
| |
| <div class="shadcn-likert-option"> |
| <input class="quality_rating shadcn-likert-input annotation-input" |
| type="radio" |
| id="quality_rating_5_radio" |
| name="quality_rating" |
| value="5" |
| schema="quality_rating" |
| label_name="5" |
| selection_constraint="single" |
| validation="" |
| onclick="onlyOne(this);registerAnnotation(this);"> |
| <label class="shadcn-likert-button" for="quality_rating_5_radio"></label> |
| <span class="shadcn-likert-label">5</span> |
| </div> |
| |
| </div> |
| <div class="shadcn-likert-endpoint">5</div> |
| </div> |
| |
| </fieldset></form> |
| |
|
|
| <form id="sentiment" class="annotation-form radio shadcn-radio-container" action="javascript:void(0)" data-annotation-id="1" data-annotation-type="radio" data-schema-name="sentiment" data-grid-columns="1"> |
| <div class="ai-help none"><div class="tooltip"></div></div> |
| <fieldset schema="sentiment"> |
| <legend class="shadcn-radio-title">Select sentiment</legend> |
| <div class="shadcn-radio-options"> |
| |
| <div class="shadcn-radio-option"> |
| <input class="sentiment shadcn-radio-input annotation-input" |
| type="radio" |
| id="sentiment_Positive_radio" |
| name="sentiment" |
| value="Positive" |
| selection_constraint="single" |
| schema="sentiment" |
| label_name="Positive" |
| onclick="onlyOne(this);registerAnnotation(this);" |
| validation="" |
| > |
| <label for="sentiment_Positive_radio" class="shadcn-radio-label" >Positive</label> |
| </div> |
| |
| <div class="shadcn-radio-option"> |
| <input class="sentiment shadcn-radio-input annotation-input" |
| type="radio" |
| id="sentiment_Neutral_radio" |
| name="sentiment" |
| value="Neutral" |
| selection_constraint="single" |
| schema="sentiment" |
| label_name="Neutral" |
| onclick="onlyOne(this);registerAnnotation(this);" |
| validation="" |
| > |
| <label for="sentiment_Neutral_radio" class="shadcn-radio-label" >Neutral</label> |
| </div> |
| |
| <div class="shadcn-radio-option"> |
| <input class="sentiment shadcn-radio-input annotation-input" |
| type="radio" |
| id="sentiment_Negative_radio" |
| name="sentiment" |
| value="Negative" |
| selection_constraint="single" |
| schema="sentiment" |
| label_name="Negative" |
| onclick="onlyOne(this);registerAnnotation(this);" |
| validation="" |
| > |
| <label for="sentiment_Negative_radio" class="shadcn-radio-label" >Negative</label> |
| </div> |
| </div></fieldset></form> |
|
|
| <form id="complexity" class="annotation-form slider" action="javascript:void(0)" data-annotation-id="2" > |
| <div class="ai-help none"><div class="tooltip"></div></div> |
| <fieldset schema="complexity"> |
| <legend class="custom-slider-title">Rate complexity</legend> |
|
|
| <div class="custom-slider-container" id="customSlider_complexity_slider_range" tabindex="0"> |
| |
| <input type="range" |
| min="1" |
| max="10" |
| step="1" |
| value="5" |
| class="custom-slider-input annotation-input" |
| onclick="registerAnnotation(this);" |
| oninput="updateCustomSlider(this);" |
| label_name="slider" |
| name="complexity:::slider" |
| id="complexity_slider_range" |
| schema="complexity" |
| validation=""> |
|
|
| |
| <div class="custom-slider-track"> |
| <div class="custom-slider-track-active" id="sliderTrackActive_complexity_slider_range"></div> |
| <div class="custom-slider-thumb" id="sliderThumb_complexity_slider_range"> |
| |
| <div class="slider-tooltip" id="sliderTooltip_complexity_slider_range">5</div> |
| </div> |
| </div> |
| <div class="custom-slider-ticks" id="sliderTicks_complexity_slider_range"></div> |
| </div> |
| </fieldset> |
| </form> |
|
|
| <script> |
| |
| (function() { |
| const sliderId = "complexity_slider_range"; |
| const sliderInput = document.getElementById(sliderId); |
| |
| if (!sliderInput) return; |
| |
| |
| setupCustomSlider(sliderInput); |
| |
| |
| sliderInput.addEventListener('input', function() { |
| updateCustomSlider(this); |
| }); |
| })(); |
| |
| function setupCustomSlider(sliderInput) { |
| const sliderId = sliderInput.id; |
| const min = parseInt(sliderInput.min); |
| const max = parseInt(sliderInput.max); |
| const step = parseInt(sliderInput.step) || 1; |
| |
| |
| const container = document.getElementById('customSlider_' + sliderId); |
| const sliderThumb = document.getElementById('sliderThumb_' + sliderId); |
| const sliderTrackActive = document.getElementById('sliderTrackActive_' + sliderId); |
| const sliderTicks = document.getElementById('sliderTicks_' + sliderId); |
| const tooltip = document.getElementById('sliderTooltip_' + sliderId); |
| |
| if (!container || !sliderThumb || !sliderTrackActive || !sliderTicks || !tooltip) return; |
| |
| |
| updateSliderPosition(5, min, max, sliderThumb, sliderTrackActive, tooltip); |
| |
| |
| createTicks(min, max, step, sliderTicks); |
| |
| container.addEventListener('click', function(e) { |
| |
| if (e.target === sliderThumb || e.target === sliderInput) return; |
| |
| |
| const rect = container.getBoundingClientRect(); |
| const clickPosition = e.clientX - rect.left; |
| const percentClicked = (clickPosition / rect.width) * 100; |
| |
| |
| let newValue = min + (percentClicked / 100) * (max - min); |
| |
| |
| newValue = Math.round(newValue / step) * step; |
| |
| |
| newValue = Math.max(min, Math.min(max, newValue)); |
| |
| |
| sliderInput.value = newValue; |
| |
| |
| updateSliderPosition(newValue, min, max, sliderThumb, sliderTrackActive, tooltip); |
| |
| |
| const event = new Event('input', { bubbles: true }); |
| sliderInput.dispatchEvent(event); |
| |
| |
| if (typeof registerAnnotation === 'function') { |
| registerAnnotation(sliderInput); |
| } |
| }); |
| |
| |
| container.addEventListener('keydown', function(e) { |
| |
| if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return; |
| |
| |
| e.preventDefault(); |
| |
| |
| let currentValue = parseFloat(sliderInput.value); |
| |
| |
| |
| if (e.key === 'ArrowDown') { |
| currentValue = Math.max(min, currentValue - step); |
| } else if (e.key === 'ArrowUp') { |
| currentValue = Math.min(max, currentValue + step); |
| } |
| |
| |
| sliderInput.value = currentValue; |
| |
| |
| updateSliderPosition(currentValue, min, max, sliderThumb, sliderTrackActive, tooltip); |
| |
| |
| const event = new Event('input', { bubbles: true }); |
| sliderInput.dispatchEvent(event); |
| |
| |
| if (typeof registerAnnotation === 'function') { |
| registerAnnotation(sliderInput); |
| } |
| }); |
| |
| |
| container.addEventListener('focus', function() { |
| container.classList.add('focus'); |
| }); |
| |
| container.addEventListener('blur', function() { |
| container.classList.remove('focus'); |
| }); |
| |
| |
| let isDragging = false; |
| |
| |
| sliderThumb.addEventListener('mousedown', function(e) { |
| isDragging = true; |
| e.preventDefault(); |
| |
| |
| sliderThumb.classList.add('dragging'); |
| |
| |
| document.addEventListener('mousemove', handleMouseDrag); |
| document.addEventListener('mouseup', stopDrag); |
| }); |
| |
| |
| sliderThumb.addEventListener('touchstart', function(e) { |
| isDragging = true; |
| e.preventDefault(); |
| |
| |
| sliderThumb.classList.add('dragging'); |
| |
| |
| document.addEventListener('touchmove', handleTouchDrag); |
| document.addEventListener('touchend', stopDrag); |
| document.addEventListener('touchcancel', stopDrag); |
| }); |
| |
| |
| function handleMouseDrag(e) { |
| if (!isDragging) return; |
| |
| |
| const rect = container.getBoundingClientRect(); |
| |
| |
| let position = e.clientX - rect.left; |
| position = Math.max(0, Math.min(position, rect.width)); |
| |
| |
| updateSliderFromPosition(position, rect.width); |
| } |
| |
| |
| function handleTouchDrag(e) { |
| if (!isDragging || !e.touches[0]) return; |
| |
| const touch = e.touches[0]; |
| const rect = container.getBoundingClientRect(); |
| |
| |
| let position = touch.clientX - rect.left; |
| position = Math.max(0, Math.min(position, rect.width)); |
| |
| |
| updateSliderFromPosition(position, rect.width); |
| } |
| |
| |
| function updateSliderFromPosition(position, width) { |
| |
| const percent = (position / width) * 100; |
| |
| |
| let newValue = min + (percent / 100) * (max - min); |
| |
| |
| newValue = Math.round(newValue / step) * step; |
| |
| |
| newValue = Math.max(min, Math.min(max, newValue)); |
| |
| |
| sliderInput.value = newValue; |
| |
| |
| updateSliderPosition(newValue, min, max, sliderThumb, sliderTrackActive, tooltip); |
| |
| |
| const event = new Event('input', { bubbles: true }); |
| sliderInput.dispatchEvent(event); |
| |
| |
| if (typeof registerAnnotation === 'function') { |
| registerAnnotation(sliderInput); |
| } |
| } |
| |
| |
| function stopDrag() { |
| if (!isDragging) return; |
| |
| isDragging = false; |
| sliderThumb.classList.remove('dragging'); |
| |
| |
| document.removeEventListener('mousemove', handleMouseDrag); |
| document.removeEventListener('touchmove', handleTouchDrag); |
| document.removeEventListener('mouseup', stopDrag); |
| document.removeEventListener('touchend', stopDrag); |
| document.removeEventListener('touchcancel', stopDrag); |
| } |
| } |
| |
| function updateCustomSlider(sliderInput) { |
| const sliderId = sliderInput.id; |
| const min = parseInt(sliderInput.min); |
| const max = parseInt(sliderInput.max); |
| |
| const sliderThumb = document.getElementById('sliderThumb_' + sliderId); |
| const sliderTrackActive = document.getElementById('sliderTrackActive_' + sliderId); |
| const tooltip = document.getElementById('sliderTooltip_' + sliderId); |
| |
| if (!sliderThumb || !sliderTrackActive || !tooltip) return; |
| |
| updateSliderPosition(sliderInput.value, min, max, sliderThumb, sliderTrackActive, tooltip); |
| } |
| |
| function updateSliderPosition(value, min, max, thumbElement, trackElement, tooltipElement) { |
| const percent = ((value - min) / (max - min)) * 100; |
| thumbElement.style.left = `${percent}%`; |
| trackElement.style.width = `${percent}%`; |
| |
| |
| if (tooltipElement) { |
| tooltipElement.textContent = value; |
| } |
| } |
| |
| function createTicks(min, max, step, tickContainer) { |
| |
| tickContainer.innerHTML = ''; |
| |
| |
| const ticks = calculateOptimalTicks(min, max, step); |
| |
| ticks.forEach(tick => { |
| const percent = ((tick.value - min) / (max - min)) * 100; |
| |
| const tickElement = document.createElement('div'); |
| tickElement.className = 'custom-slider-tick'; |
| tickElement.style.left = `${percent}%`; |
| |
| const tickMark = document.createElement('div'); |
| tickMark.className = tick.showLabel ? 'custom-slider-tick-mark major' : 'custom-slider-tick-mark'; |
| tickElement.appendChild(tickMark); |
| |
| if (tick.showLabel) { |
| const tickLabel = document.createElement('div'); |
| tickLabel.className = 'custom-slider-tick-label'; |
| tickLabel.textContent = tick.value; |
| tickElement.appendChild(tickLabel); |
| } |
| |
| tickContainer.appendChild(tickElement); |
| }); |
| } |
| |
| function calculateOptimalTicks(min, max, sliderStep, maxTicks = 8) { |
| const range = max - min; |
| |
| |
| const possibleValues = []; |
| for (let value = min; value <= max; value += sliderStep) { |
| possibleValues.push(Math.round(value)); |
| } |
| |
| |
| if (possibleValues.length <= maxTicks) { |
| return possibleValues.map(value => ({ value, showLabel: true })); |
| } |
| |
| |
| const targetInterval = range / (maxTicks - 1); |
| |
| |
| const candidates = []; |
| for (let magnitude = 1; magnitude <= range; magnitude *= 10) { |
| candidates.push(magnitude); |
| candidates.push(2 * magnitude); |
| candidates.push(5 * magnitude); |
| } |
| |
| |
| let bestInterval = candidates[0]; |
| let bestDiff = Math.abs(candidates[0] - targetInterval); |
| |
| for (const candidate of candidates) { |
| const diff = Math.abs(candidate - targetInterval); |
| if (diff < bestDiff) { |
| bestInterval = candidate; |
| bestDiff = diff; |
| } |
| } |
| |
| |
| const majorTicks = []; |
| |
| |
| majorTicks.push({ value: min, showLabel: true }); |
| |
| |
| for (let tickValue = bestInterval; tickValue < max; tickValue += bestInterval) { |
| if (tickValue > min) { |
| majorTicks.push({ value: tickValue, showLabel: true }); |
| } |
| } |
| |
| |
| if (majorTicks[majorTicks.length - 1].value !== max) { |
| majorTicks.push({ value: max, showLabel: true }); |
| } |
| |
| |
| const allTicks = [...majorTicks]; |
| |
| |
| if (majorTicks.length > 1) { |
| const majorSpacing = bestInterval; |
| if (majorSpacing > sliderStep * 2) { |
| possibleValues.forEach(value => { |
| if (!allTicks.some(t => t.value === value)) { |
| allTicks.push({ value, showLabel: false }); |
| } |
| }); |
| } |
| } |
| |
| return allTicks.sort((a, b) => a.value - b.value); |
| } |
| </script> |
| |
|
|
| <form id="summary" class="annotation-form textbox shadcn-textbox-container " action="javascript:void(0)" data-annotation-id="3" data-annotation-type="text" data-schema-name="summary" data-grid-columns="1"> |
| <div class="ai-help none"><div class="tooltip"></div></div> |
| <fieldset schema_name="summary"> |
| |
| <legend class="shadcn-textbox-title">Enter your summary here</legend> |
| |
| <div class="shadcn-textbox-item"> |
| |
| |
| <input class="summary shadcn-textbox-input annotation-input" |
| type="text" |
| id="summary_text_box_text" |
| name="summary:::text_box" |
| validation="" |
| schema="summary" |
| label_name="text_box" |
| |
| style="" |
| > |
| </div></fieldset></form> |
|
|
| </div> |
|
|
| </div> |
|
|
| |
| {{ var_elems | safe }} |
| </div> |
| </div> |
| </div> |
|
|
| |
| <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_html | safe }} |
| {% endif %} |
|
|
| |
| <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">·</span> |
| <a href="https://github.com/davidjurgens/potato" target="_blank" rel="noopener noreferrer">GitHub</a> |
| <span class="footer-sep">·</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> |
|
|
| |
| <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> |
|
|
| |
| <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 }}, |
| ui_config: {} |
| }; |
| |
| |
| if (!window.config.ui_debug) { |
| |
| window._originalConsole = { |
| log: console.log, |
| debug: console.debug, |
| info: console.info, |
| warn: console.warn |
| }; |
| |
| console.log = function() {}; |
| console.debug = function() {}; |
| console.info = function() {}; |
| |
| } |
| |
| |
| 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> |
|
|
| |
| <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> |
| |
| <script src="{{ url_for('static', filename='display-logic.js') }}?v=1"></script> |
| |
| {% 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> |
|
|
| |
| {% if frontend_assets.image_annotation | default(false) %} |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script> |
| |
| <script src="{{ url_for('static', filename='image-annotation.js') }}?v=2"></script> |
| {% endif %} |
| |
| {% 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> |
| |
| <script src="{{ url_for('static', filename='option_highlight_manager.js') }}?v=2"></script> |
| {% endif %} |
|
|
| |
| {% 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 %} |
| |
| {% if frontend_assets.audio_annotation | default(false) %} |
| <script src="{{ url_for('static', filename='audio-annotation.js') }}?v=2"></script> |
| {% endif %} |
| |
| {% if frontend_assets.video_annotation | default(false) %} |
| <script src="{{ url_for('static', filename='video-annotation.js') }}?v=10"></script> |
| {% endif %} |
|
|
| |
| {% if frontend_assets.span_link | default(false) %} |
| <script src="{{ url_for('static', filename='span-link-manager.js') }}?v=7"></script> |
| {% endif %} |
|
|
| |
| {% if frontend_assets.event_annotation | default(false) %} |
| <script src="{{ url_for('static', filename='event-annotation.js') }}?v=1"></script> |
| {% endif %} |
|
|
| |
| <script src="{{ url_for('static', filename='entity-linking.js') }}?v=1"></script> |
|
|
| |
| {% if frontend_assets.coreference | default(false) %} |
| <script src="{{ url_for('static', filename='coreference-manager.js') }}?v=1"></script> |
| {% endif %} |
|
|
| |
| {% if frontend_assets.segmentation_tools | default(false) %} |
| <script src="{{ url_for('static', filename='segmentation-tools.js') }}?v=1"></script> |
| {% endif %} |
|
|
| |
| {% if frontend_assets.conversation_tree | default(false) %} |
| <script src="{{ url_for('static', filename='conversation-tree.js') }}?v=1"></script> |
| {% endif %} |
|
|
| |
| {% 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 %} |
|
|
| |
| {% if frontend_assets.triage | default(false) %} |
| <script src="{{ url_for('static', filename='triage.js') }}?v=1"></script> |
| {% endif %} |
|
|
| |
| {% if frontend_assets.tiered_annotation | default(false) %} |
| <script src="{{ url_for('static', filename='tiered-annotation.js') }}?v=1"></script> |
| {% endif %} |
|
|
| |
| {% if frontend_assets.document_bbox | default(false) %} |
| <script src="{{ url_for('static', filename='document-bbox.js') }}?v=3"></script> |
| {% endif %} |
|
|
| |
| {% 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) %} |
| |
| <script src="{{ url_for('static', filename='agent-chat.js') }}?v=1"></script> |
| {% endif %} |
|
|
| {% if chat_enabled | default(false) %} |
| |
| <script src="{{ url_for('static', filename='llm-chat-sidebar.js') }}?v=1"></script> |
| {% endif %} |
|
|
| |
| {% 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 %} |
| |
| {% if frontend_assets.web_agent_recorder | default(false) %} |
| <script src="{{ url_for('static', filename='web-agent-recorder.js') }}?v=1"></script> |
| {% endif %} |
| |
| {% 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 %} |
|
|
| |
| <script> |
| window.addEventListener('keydown', function(e) { |
| console.log('[GLOBAL DEBUG] keydown:', e.key, 'code:', e.code, 'target:', e.target.tagName); |
| }, true); |
| </script> |
|
|
| |
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| 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) { |
| |
| instanceTextContainer.style.setProperty('--max-instance-height', maxHeight + 'px'); |
| |
| |
| instanceTextContainer.classList.add('max-height-limited'); |
| |
| console.log('Applied maximum instance height:', maxHeight + 'px'); |
| } |
| } |
| }); |
| </script> |
|
|
| |
| |
| {% if is_annotation_page %} |
| <button type="button" id="memo-panel-toggle" class="memo-panel-toggle" hidden> |
| 📝 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">×</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…"></textarea> |
| <div id="memo-anchor-wrap" class="memo-anchor-hint" hidden> |
| <label> |
| <input type="checkbox" id="memo-anchor-check"> |
| Attach to selected text: “<span id="memo-anchor-quote"></span>” |
| </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> |
|
|
| |
| |
| |
| <button type="button" id="search-panel-toggle" class="search-panel-toggle" hidden> |
| 🔍 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">×</button> |
| </div> |
| <div class="search-form"> |
| <input type="text" id="search-q" placeholder="Search instance text…" |
| 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> |
|
|
| |
| |
| |
| <button type="button" id="cb-panel-toggle" class="cb-panel-toggle" hidden> |
| 🏷 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">×</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">×</button> |
| </div> |
| <div id="cb-tree" class="cb-tree" aria-live="polite"> |
| <div class="cb-empty">Loading…</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…" |
| 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…" |
| 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> |
|
|