codebook / potato /templates /solo /annotate.html
davidjurgens's picture
Deploy: Potato — Codebook Annotation
aceb1b2 verified
Raw
History Blame Contribute Delete
12.2 kB
{% extends "solo/base_solo.html" %}
{% block title %}Annotate · Solo Mode · Potato{% endblock %}
{% block extra_head %}
<style>
.annotate-header {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: var(--space-md, 1rem);
margin-bottom: var(--space-md, 1rem);
}
.annotate-header h1 {
font-size: var(--text-md, 1.0625rem);
margin: 0;
color: var(--muted-foreground, #71717a);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.annotate-counter {
font-size: var(--text-xs, 0.75rem);
color: var(--muted-foreground, #71717a);
font-variant-numeric: tabular-nums;
}
.llm-suggestion-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-md, 1rem);
margin-bottom: var(--space-md, 1rem);
flex-wrap: wrap;
}
.llm-suggestion-row .suggestion-label {
font-size: var(--text-sm, 0.8125rem);
color: var(--muted-foreground, #71717a);
}
.llm-suggestion-row .suggested-badge {
font-weight: 600;
color: var(--primary, #6e56cf);
padding: 0 var(--space-xs, 0.25rem);
}
.confidence-label {
font-size: var(--text-xs, 0.75rem);
color: var(--muted-foreground, #71717a);
margin-top: 2px;
}
.confidence-label .qualifier {
font-weight: 600;
color: var(--foreground, #09090b);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-right: var(--space-xs, 0.25rem);
}
.completion-card {
text-align: center;
padding: var(--space-xl, 2rem) var(--space-lg, 1.5rem);
}
.completion-card h1 {
font-size: var(--text-xl, 1.25rem);
margin: 0 0 var(--space-sm, 0.5rem) 0;
}
.completion-card p {
color: var(--muted-foreground, #71717a);
margin: 0 auto var(--space-lg, 1.5rem) auto;
max-width: 40ch;
font-size: var(--text-sm, 0.8125rem);
}
.completion-stats {
display: inline-grid;
grid-template-columns: auto auto;
gap: var(--space-xs, 0.25rem) var(--space-lg, 1.5rem);
text-align: left;
margin: 0 auto var(--space-lg, 1.5rem) auto;
font-size: var(--text-sm, 0.8125rem);
}
.completion-stats dt {
color: var(--muted-foreground, #71717a);
}
.completion-stats dd {
margin: 0;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--foreground, #09090b);
}
</style>
{% endblock %}
{% block content %}
{% if instance %}
<div class="annotate-header">
<h1>Annotate</h1>
{% if stats %}
{% set total = stats.human_labeled + stats.remaining %}
<span class="annotate-counter" aria-live="polite">
Instance {{ stats.human_labeled + 1 }} of {{ total }}
</span>
{% endif %}
</div>
<form method="POST" action="{{ url_for('solo_mode.annotate') }}" id="annotate-form">
<input type="hidden" name="instance_id" value="{{ instance.id }}">
<input type="hidden" name="annotation" id="selected-label" value="">
<div class="instance-text" id="instance-text">
{{ instance.text }}
</div>
{% if llm_prediction %}
{% set conf = (llm_prediction.confidence * 100)|int %}
{% set qualifier = 'Low confidence' if llm_prediction.confidence < 0.5
else ('Moderate confidence' if llm_prediction.confidence < 0.8 else 'High confidence') %}
<div class="llm-suggestion-row">
<div>
<div class="suggestion-label">
The LLM suggests
<span class="suggested-badge">{{ llm_prediction.label }}</span>
</div>
<div class="confidence-label">
<span class="qualifier">{{ qualifier }}</span>· {{ conf }}%
</div>
</div>
<button type="button"
class="btn btn-secondary"
id="accept-llm-btn"
data-llm-label="{{ llm_prediction.label }}"
aria-keyshortcuts="A">
Accept (A)
</button>
</div>
<div class="llm-reasoning">
<div class="llm-reasoning-header" aria-expanded="false">
<strong>Reasoning</strong>
<span class="disclosure-icon" aria-hidden="true"></span>
</div>
<div class="llm-reasoning-content" hidden>
{% if llm_prediction.reasoning %}
<p style="margin: 0 0 var(--space-sm, 0.5rem);">{{ llm_prediction.reasoning }}</p>
{% endif %}
<div class="confidence-bar" role="progressbar"
aria-label="LLM confidence"
aria-valuenow="{{ conf }}" aria-valuemin="0" aria-valuemax="100">
{% set conf_class = 'confidence-low' if llm_prediction.confidence < 0.5
else ('confidence-mid' if llm_prediction.confidence < 0.8 else 'confidence-high') %}
<div class="confidence-fill {{ conf_class }}"
style="width: {{ conf }}%"></div>
</div>
</div>
</div>
{% endif %}
<fieldset style="border: none; padding: 0; margin: 0;">
<legend class="form-help" style="margin-bottom: var(--space-sm, 0.5rem);">
Pick a label (use number keys, or click)
</legend>
<div class="label-buttons" role="group" aria-label="Annotation labels">
{% for label in labels %}
<button type="button"
class="label-btn{% if llm_prediction and llm_prediction.label == label %} suggested{% endif %}"
data-label="{{ label }}"
aria-pressed="false">
{% if loop.index <= 9 %}
<span class="label-key" aria-hidden="true">{{ loop.index }}</span>
{% endif %}
<span>{{ label }}</span>
</button>
{% endfor %}
</div>
</fieldset>
<div class="solo-nav">
<span></span>
<button type="submit" class="btn btn-primary" id="submit-btn" disabled>
<span id="submit-label">Submit</span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M5 12h14M13 6l6 6-6 6"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
</form>
{% elif message %}
<div class="completion-card" role="status" aria-live="polite">
{% if phase in ('completed', 'final_validation', 'autonomous_labeling') %}
<h1>You're done with this batch</h1>
<p>
{% if phase == 'completed' %}
The workflow is complete. Export your annotations from the dashboard.
{% else %}
{{ message }}. The LLM is taking it from here while the dataset finishes labeling.
{% endif %}
</p>
{% else %}
<h1>Caught up for now</h1>
<p>{{ message }}. New instances may appear shortly &mdash; check the dashboard for progress.</p>
{% endif %}
{% if stats %}
<dl class="completion-stats">
<dt>You labeled</dt><dd>{{ stats.human_labeled }}</dd>
<dt>LLM labeled</dt><dd>{{ stats.llm_labeled }}</dd>
<dt>Agreement</dt><dd>{{ (stats.agreement_rate * 100)|int }}%</dd>
<dt>Remaining</dt><dd>{{ stats.remaining }}</dd>
</dl>
{% endif %}
<div>
<a href="{{ url_for('solo_mode.status') }}" class="btn btn-primary">
Go to dashboard
</a>
</div>
</div>
{% endif %}
{% endblock %}
{% block stats %}
{# Drop redundant "Phase" row; lead with progress, which is what the user wants here. #}
{% if stats %}
{% set total = stats.human_labeled + stats.remaining %}
<div class="stat-item">
<span class="stat-label">Progress</span>
<span class="stat-value">{{ stats.human_labeled }} / {{ total }}</span>
</div>
<div class="stat-item">
<span class="stat-label">Remaining</span>
<span class="stat-value">{{ stats.remaining }}</span>
</div>
<div class="stat-item">
<span class="stat-label">LLM labeled</span>
<span class="stat-value">{{ stats.llm_labeled }}</span>
</div>
<div class="stat-item">
<span class="stat-label">Agreement</span>
<span class="stat-value">{{ (stats.agreement_rate * 100)|int }}%</span>
</div>
{% else %}
{{ super() }}
{% endif %}
{% endblock %}
{% block sidebar_extra %}
{% if labels and instance %}
<h3>Shortcuts</h3>
<div style="font-size: var(--text-sm, 0.8125rem); color: var(--muted-foreground, #71717a);">
{% for label in labels[:9] %}
<div style="margin-bottom: var(--space-xs, 0.25rem); display: flex; gap: var(--space-sm, 0.5rem); align-items: center;">
<kbd>{{ loop.index }}</kbd>
<span>{{ label }}</span>
</div>
{% endfor %}
{% if llm_prediction %}
<div style="margin-bottom: var(--space-xs, 0.25rem); display: flex; gap: var(--space-sm, 0.5rem); align-items: center;">
<kbd>A</kbd>
<span>Accept LLM</span>
</div>
{% endif %}
<div style="margin-top: var(--space-sm, 0.5rem); display: flex; gap: var(--space-sm, 0.5rem); align-items: center;">
<kbd>Enter</kbd>
<span>Submit</span>
</div>
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
<script>
const labels = {{ labels|tojson|safe }};
const submitBtn = document.getElementById('submit-btn');
const submitLabel = document.getElementById('submit-label');
const selectedInput = document.getElementById('selected-label');
const form = document.getElementById('annotate-form');
function selectLabel(label) {
if (!label) return;
document.querySelectorAll('.label-btn').forEach(b => {
const isMatch = b.dataset.label === label;
b.classList.toggle('selected', isMatch);
b.setAttribute('aria-pressed', String(isMatch));
});
if (selectedInput) selectedInput.value = label;
if (submitBtn) {
submitBtn.disabled = false;
if (submitLabel) submitLabel.textContent = `Submit: ${label}`;
}
}
// Label button clicks (delegated)
document.querySelectorAll('.label-btn').forEach(btn => {
btn.addEventListener('click', () => selectLabel(btn.dataset.label));
});
// One-click accept LLM suggestion
const acceptBtn = document.getElementById('accept-llm-btn');
if (acceptBtn) {
acceptBtn.addEventListener('click', () => {
selectLabel(acceptBtn.dataset.llmLabel);
form && form.requestSubmit && form.requestSubmit();
});
}
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
// Don't intercept when an input/textarea is focused
const tag = (document.activeElement && document.activeElement.tagName) || '';
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
if (e.key >= '1' && e.key <= '9') {
const idx = parseInt(e.key, 10) - 1;
if (idx < labels.length) selectLabel(labels[idx]);
} else if ((e.key === 'a' || e.key === 'A') && acceptBtn) {
acceptBtn.click();
} else if (e.key === 'Enter') {
// Only submit when a label is actually selected, not just because
// the button became visually enabled.
if (selectedInput && selectedInput.value) {
e.preventDefault();
form && form.requestSubmit && form.requestSubmit();
}
}
});
</script>
{% endblock %}