dhuser's picture
Update logos
b9880fe
raw
history blame
44.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LREC 2026 — LLM Annotator</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'ui-monospace', 'monospace'],
},
colors: {
ink: { 950: '#0b0f19', 900: '#111827', 800: '#1f2937', 700: '#374151', 500: '#6b7280', 400: '#9ca3af', 300: '#d1d5db', 200: '#e5e7eb', 100: '#f3f4f6', 50: '#f9fafb' },
accent: { 50: '#eef2ff', 100: '#e0e7ff', 200: '#c7d2fe', 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca' },
}
}
}
}
</script>
<link rel="stylesheet" href="/static/styles.css?v=20260516b">
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.10/dist/cdn.min.js"></script>
</head>
<body class="font-sans bg-ink-50 text-ink-900 min-h-screen" x-data="annotator()" x-init="init()" x-cloak>
<!-- =====================================================================
TOP BAR
===================================================================== -->
<header class="sticky top-0 z-40 bg-white/95 backdrop-blur border-b border-ink-200">
<div class="max-w-[1400px] mx-auto px-5 py-3 flex items-center gap-4">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-accent-500 to-accent-700 flex items-center justify-center text-white text-lg"></div>
<div>
<h1 class="font-semibold text-ink-900 leading-tight">LLM-as-Annotator</h1>
<div class="text-[11px] text-ink-500 leading-tight">LREC 2026 · <a class="hover:text-accent-600 inline-flex items-center gap-1 align-middle" target="_blank" href="https://aclanthology.org/2026.loreslm-1.28/" title="Read on ACL Anthology"><img src="/static/logos/acl-anthology.svg" alt="ACL Anthology" class="inline-block h-3 w-3 align-middle">paper: Vidal-Gorène et al. 2026, LoResLM →</a></div>
</div>
</div>
<span class="text-ink-300">·</span>
<!-- Task pill -->
<button @click="modal='task'" class="pill" :title="schema?.task_name">
<span class="pill-icon">📋</span>
<span class="pill-label">Task</span>
<span class="pill-value truncate max-w-[160px]" x-text="schema?.task_name || 'choose…'"></span>
</button>
<!-- Models pill -->
<button @click="modal='models'" class="pill"
:class="state.models.length >= 2 ? 'pill-moe' : ''"
:title="state.models.join(', ')">
<span class="pill-icon" x-text="state.models.length >= 2 ? '🧠' : '🤖'"></span>
<span class="pill-label" x-text="state.models.length >= 2 ? 'MoE' : 'Model'"></span>
<span class="pill-value" x-text="state.models.length + (state.models.length >= 2 ? ' active' : '')"></span>
</button>
<!-- API key pill -->
<button @click="modal='key'" class="pill" :class="hasKey ? 'pill-ok' : 'pill-warn'"
:title="localKey ? 'Key stored in this tab only (sessionStorage). Click to manage.' : (state.has_env_key ? 'Using server-side OPENROUTER_API_KEY env.' : 'No key yet.')">
<span class="pill-icon" x-text="hasKey ? '🔑' : '⚠'"></span>
<span class="pill-label">OpenRouter</span>
<span class="pill-value" x-text="localKey ? 'tab-only' : (state.has_env_key ? 'shared env' : 'add key')"></span>
</button>
<div class="flex-1"></div>
<!-- Selection bulk bar -->
<template x-if="selection.size > 0">
<div class="flex items-center gap-2 mr-2">
<span class="text-xs text-ink-500" x-text="selection.size + ' selected'"></span>
<button @click="modal='bulk'" class="btn btn-secondary text-xs">Apply tag…</button>
<button @click="clearSelection()" class="btn btn-ghost text-xs">Clear</button>
</div>
</template>
<button @click="modal='help'" class="btn btn-ghost" title="Keyboard shortcuts & help">?</button>
<button @click="modal='advanced'" class="btn btn-ghost" title="Advanced settings"></button>
<button @click="annotateAll()"
:disabled="!canRun || loading"
class="btn btn-primary"
:class="{'opacity-50 cursor-not-allowed': !canRun || loading}">
<span x-show="!loading">▶ Annotate all</span>
<span x-show="loading" class="flex items-center gap-2"><span class="spinner"></span><span x-text="progressText"></span></span>
</button>
</div>
<!-- Sub-bar with corpus context -->
<div class="max-w-[1400px] mx-auto px-5 pb-2 flex items-center gap-3 text-xs text-ink-500">
<span x-show="state.language">📍 <span class="text-ink-700 font-medium" x-text="state.language"></span></span>
<span x-show="state.sentences.length > 0">·</span>
<span x-show="state.sentences.length > 0">
<span class="text-ink-700 font-medium" x-text="state.sentences.length"></span> sentences,
<span class="text-ink-700 font-medium" x-text="totalTokens"></span> tokens
</span>
<span x-show="totalDisagreements > 0">· ⚠ <span class="text-amber-700 font-medium" x-text="totalDisagreements"></span> disagreements</span>
<span class="flex-1"></span>
<span>ICL pool · v<span class="text-ink-700 font-medium" x-text="state.icl_pool.version"></span> · <span class="text-ink-700 font-medium" x-text="state.icl_pool.size"></span> ex</span>
<button @click="modal='icl'" class="text-accent-600 hover:underline">view</button>
<span>·</span>
<button @click="modal='exports'" class="text-accent-600 hover:underline">export</button>
</div>
</header>
<!-- =====================================================================
LAYOUT
===================================================================== -->
<div class="max-w-[1400px] mx-auto px-5 py-6 grid grid-cols-12 gap-6">
<!-- Sidebar -->
<aside class="col-span-3 space-y-5">
<section class="card">
<h3 class="card-title">🚀 Quick start</h3>
<p class="text-xs text-ink-500 mb-3">Load a sandbox corpus from the LREC sandbox in one click.</p>
<div class="space-y-2">
<template x-for="ex in state.exercises" :key="ex.idx">
<button @click="loadExercise(ex.idx)"
class="w-full text-left p-3 rounded-lg border border-ink-200 hover:border-accent-300 hover:bg-accent-50 transition">
<div class="font-medium text-sm" x-text="ex.title.split('—')[1]?.trim() || ex.title"></div>
<div class="text-[11px] text-ink-500 mt-0.5" x-text="ex.summary.slice(0,90)+'…'"></div>
</button>
</template>
</div>
</section>
<section class="card">
<h3 class="card-title">📝 Your text</h3>
<button @click="modal='paste'" class="btn btn-secondary w-full">+ Paste text</button>
<button @click="clearCorpus()" x-show="state.sentences.length > 0" class="btn btn-ghost w-full mt-2 text-xs">Clear corpus only</button>
<button @click="resetAll()" class="btn btn-ghost w-full mt-1 text-xs text-red-600 hover:!bg-red-50">
🗑 Reset everything
</button>
</section>
<section class="card" x-show="state.sentences.length > 0">
<h3 class="card-title">⚡ Shortcuts</h3>
<ul class="text-[11px] text-ink-500 space-y-1 font-mono">
<li><kbd>j</kbd> / <kbd>k</kbd> next / prev token</li>
<li><kbd>e</kbd> edit focused token</li>
<li><kbd>1</kbd><kbd>9</kbd> assign tag (in editor)</li>
<li><kbd>x</kbd> toggle selection</li>
<li><kbd>r</kbd> re-annotate sentence</li>
<li><kbd>esc</kbd> close popup</li>
</ul>
</section>
</aside>
<!-- Main corpus panel -->
<main class="col-span-9 space-y-4" tabindex="0" @keydown="handleKey($event)" x-ref="main">
<!-- Guide banner — contextual, updates with state -->
<section x-show="guide && !guideDismissed" class="rounded-xl border border-accent-200 bg-gradient-to-br from-accent-50 to-purple-50 p-4 flex items-start gap-4" x-transition>
<div class="text-2xl shrink-0" x-text="guide?.icon || '📘'"></div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<span class="text-[11px] font-semibold uppercase tracking-wider text-accent-700" x-text="`Step ${guide?.step}/5`"></span>
<span class="text-[11px] text-ink-500">·</span>
<span class="text-sm font-semibold text-ink-900" x-text="guide?.title"></span>
</div>
<p class="text-xs text-ink-700 mt-1 leading-relaxed" x-html="guide?.body"></p>
<div class="flex flex-wrap gap-2 mt-3" x-show="guide?.actions?.length > 0">
<template x-for="(a, i) in (guide?.actions || [])" :key="i">
<button @click="runGuideAction(a)"
class="text-xs"
:class="i === 0 ? 'btn btn-primary' : 'btn btn-secondary'"
x-text="a.label"></button>
</template>
</div>
</div>
<div class="flex flex-col gap-1 shrink-0">
<button @click="guideDismissed=true; localStorage.setItem('guideDismissed','1')" class="btn btn-ghost btn-icon" title="Hide guide"></button>
</div>
</section>
<!-- MoE explainer — when ≥2 models are selected -->
<section x-show="state.models.length >= 2 && !moeBannerDismissed && state.sentences.length > 0"
class="rounded-xl border border-amber-200 bg-amber-50/60 px-4 py-2.5 text-xs text-amber-900 flex items-center gap-3" x-transition>
<span class="text-base">🧠</span>
<div class="flex-1">
<strong>Mixture-of-Experts is ON.</strong>
<span x-text="state.models.length"></span> models will run in parallel — answers are voted per token, and contested tokens (⚠) are highlighted for your review.
</div>
<button @click="moeBannerDismissed=true" class="btn btn-ghost btn-icon"></button>
</section>
<section x-show="state.models.length === 1 && state.sentences.length > 0 && !moeHintDismissed"
class="rounded-xl border border-ink-200 bg-white px-4 py-2.5 text-xs text-ink-700 flex items-center gap-3" x-transition>
<span class="text-base">💡</span>
<div class="flex-1">
Single-model mode. Add a 2nd model in <button class="text-accent-600 underline" @click="modal='models'">Models</button> to compare per token (Mixture-of-Experts).
</div>
<button @click="moeHintDismissed=true" class="btn btn-ghost btn-icon"></button>
</section>
<!-- Empty state -->
<template x-if="state.sentences.length === 0">
<div class="card text-center py-16">
<div class="text-5xl mb-4">📜</div>
<h2 class="text-xl font-semibold mb-2">No corpus loaded</h2>
<p class="text-ink-500 mb-6 max-w-md mx-auto">Pick a sandbox example from the sidebar, or paste your own text.</p>
<div class="flex justify-center gap-2">
<button @click="loadExercise(0)" class="btn btn-primary">Try Ancient Greek</button>
<button @click="modal='paste'" class="btn btn-secondary">Paste my text</button>
</div>
</div>
</template>
<!-- Sentence cards -->
<template x-for="(sent, sidx) in state.sentences" :key="sidx + '-' + rev">
<article class="card !p-0 overflow-hidden">
<header class="px-4 py-2.5 bg-ink-50 border-b border-ink-200 flex items-center gap-3">
<span class="font-mono text-xs text-ink-500" x-text="sent.id"></span>
<span class="text-xs" x-show="sent.n_disagreements > 0">
<span class="dot dot-warn"></span>
<span class="text-amber-700" x-text="sent.n_disagreements + ' disagree'"></span>
</span>
<span class="text-xs" x-show="sent.status === 'done' && sent.n_disagreements === 0">
<span class="dot dot-ok"></span>
<span class="text-emerald-700">consensus</span>
</span>
<span class="text-xs" x-show="sent.status === 'annotating'">
<span class="spinner-sm"></span>
<span class="text-accent-600 ml-1">annotating…</span>
</span>
<span class="text-xs" x-show="sent.status === 'pending'">
<span class="dot dot-pending"></span>
<span class="text-ink-500">pending</span>
</span>
<span class="text-xs text-red-600" x-show="sent.status === 'error'" x-text="'⚠ ' + (sent.error || 'error').slice(0,80)"></span>
<div class="flex-1"></div>
<button @click="annotateOne(sidx)" :disabled="loading" class="btn btn-secondary text-xs">
<span x-show="sent.status === 'done'">↻ Re-annotate</span>
<span x-show="sent.status !== 'done'">▶ Annotate</span>
</button>
<button @click="addSentenceToIcl(sidx)" :disabled="sent.status !== 'done'"
class="btn btn-ghost text-xs"
:class="{'opacity-50': sent.status !== 'done'}"
title="Add this sentence's corrected annotation to the ICL pool">
📥 to ICL
</button>
</header>
<div class="p-4">
<div class="tokens-flow">
<template x-for="(tok, tidx) in sent.tokens" :key="tidx + '-' + rev">
<button class="token"
:class="tokenClass(sent, sidx, tidx, tok)"
:title="tokenTooltip(sent, tidx)"
@click="onTokenClick($event, sidx, tidx)"
@contextmenu.prevent="openTokenContext($event, sidx, tidx)"
:data-sent="sidx" :data-tok="tidx"
x-ref="`tok-${sidx}-${tidx}`">
<span class="token-surface" x-text="tok.surface"></span>
<span class="token-tag" x-text="primaryTag(tok)" x-show="primaryTag(tok)"></span>
</button>
</template>
</div>
</div>
</article>
</template>
</main>
</div>
<!-- =====================================================================
MODALS
===================================================================== -->
<!-- Token editor -->
<div class="modal-backdrop" x-show="modal === 'token' && editor.tok" @click.self="closeModal()" x-transition>
<div class="modal-card max-w-2xl">
<header class="modal-header">
<div>
<div class="text-xs text-ink-500">Editing token</div>
<div class="font-mono text-xl mt-0.5" x-text="editor.tok?.surface"></div>
</div>
<div class="flex items-center gap-2">
<button @click="moveToken(-1)" class="btn btn-ghost btn-icon" title="Previous (←)"></button>
<button @click="moveToken(1)" class="btn btn-ghost btn-icon" title="Next (→)"></button>
<button @click="closeModal()" class="btn btn-ghost btn-icon" title="Close (Esc)"></button>
</div>
</header>
<div class="grid grid-cols-2 gap-5 p-5">
<!-- Fields -->
<div class="space-y-4">
<template x-for="f in schemaFields" :key="f.name">
<div>
<label class="lbl" x-text="f.name + (f.nullable ? ' (optional)' : '')"></label>
<!-- enum -->
<template x-if="f.type === 'enum'">
<div>
<input class="input" type="text" :placeholder="'Search ' + f.name + '…'"
x-model="editor.search[f.name]"
@input.debounce.50ms="refreshFilter(f.name, f.values)">
<div class="chips mt-2 max-h-44 overflow-y-auto">
<template x-for="v in (editor.filtered[f.name] || f.values)" :key="v">
<button type="button"
class="chip"
:class="editor.tok[f.name] === v ? 'chip-active' : ''"
@click="editor.tok[f.name] = v"
x-text="v"></button>
</template>
</div>
</div>
</template>
<!-- string -->
<template x-if="f.type === 'string'">
<input class="input" type="text" x-model="editor.tok[f.name]" :placeholder="f.name">
</template>
<!-- object -->
<template x-if="f.type === 'object'">
<div class="space-y-2">
<template x-for="sub in f.subfields" :key="sub.name">
<div class="flex items-center gap-2">
<label class="text-xs text-ink-500 w-24" x-text="sub.name"></label>
<input class="input flex-1" type="text"
:value="(editor.tok[f.name] || {})[sub.name] || ''"
@input="editor.tok[f.name] = {...(editor.tok[f.name]||{}), [sub.name]: $event.target.value}">
</div>
</template>
</div>
</template>
</div>
</template>
</div>
<!-- Per-model votes & metadata -->
<div class="space-y-4">
<div>
<label class="lbl">Per-model output</label>
<div class="text-xs space-y-1.5 mt-1.5" x-show="editor.perModel && Object.keys(editor.perModel).length > 0">
<template x-for="[m, ann] in Object.entries(editor.perModel || {})" :key="m">
<div class="flex items-start gap-2 p-2 rounded bg-ink-50">
<span class="font-mono text-[11px] text-ink-700 min-w-[110px] truncate" :title="m" x-text="modelShort(m)"></span>
<span class="text-[11px] text-ink-900" x-text="modelTokenSummary(ann, editor.tidx)"></span>
<button class="ml-auto text-[10px] text-accent-600 hover:underline"
@click="adoptFromModel(m)">adopt</button>
</div>
</template>
</div>
<p class="text-[11px] text-ink-500 mt-1.5" x-show="!editor.perModel || Object.keys(editor.perModel || {}).length === 0">
(run annotation first to see per-model output)
</p>
</div>
<div>
<label class="lbl">Disagreements on this token</label>
<div class="text-xs space-y-1 mt-1.5">
<template x-for="d in editor.disagreementCells" :key="d.field_path">
<div class="text-amber-700"><span class="font-mono" x-text="d.field_path"></span>: agreement <span x-text="(d.agreement_ratio*100).toFixed(0)+'%'"></span></div>
</template>
<div x-show="!editor.disagreementCells || editor.disagreementCells.length === 0" class="text-ink-500">none — consensus reached.</div>
</div>
</div>
<div>
<label class="lbl">Re-ask one model</label>
<div class="flex flex-wrap gap-1.5 mt-1.5">
<template x-for="m in state.models" :key="m">
<button class="chip chip-sm" @click="reaskOneToken(m)" x-text="modelShort(m)"></button>
</template>
</div>
</div>
</div>
</div>
<!-- Propagate option (shown when other tokens share the surface AND user has changed something) -->
<div class="px-5 pb-3" x-show="matchingTokenCount() > 0 && Object.keys(fieldChanges()).length > 0" x-transition>
<label class="flex items-start gap-2 p-3 rounded-lg border border-accent-200 bg-accent-50 cursor-pointer">
<input type="checkbox" x-model="editor.propagateToSimilar" class="mt-0.5">
<div class="text-xs leading-relaxed flex-1">
<div>Also apply to <strong x-text="matchingTokenCount()"></strong> other
<code class="font-mono bg-white px-1.5 py-0.5 rounded border border-accent-200" x-text="editor.tok.surface"></code>
token<span x-show="matchingTokenCount() > 1">s</span> in the corpus.
</div>
<div class="text-ink-500 mt-1">
Changes: <span class="font-mono text-ink-700" x-text="fieldChangesSummary()"></span>
</div>
</div>
</label>
</div>
<footer class="modal-footer">
<span class="text-xs text-ink-500" x-show="editor.autoAdvance">↵ saves &amp; auto-advances to next disagreement</span>
<span class="flex-1"></span>
<label class="text-xs text-ink-500 mr-3 flex items-center gap-1.5"><input type="checkbox" x-model="editor.autoAdvance"> auto-advance</label>
<button class="btn btn-ghost" @click="closeModal()">Cancel</button>
<button class="btn btn-primary" @click="saveToken()">Save (↵)</button>
</footer>
</div>
</div>
<!-- Task -->
<div class="modal-backdrop" x-show="modal === 'task'" @click.self="closeModal()" x-transition>
<div class="modal-card max-w-3xl">
<header class="modal-header">
<h2 class="font-semibold">Task & label schema</h2>
<button @click="closeModal()" class="btn btn-ghost btn-icon"></button>
</header>
<div class="p-5 space-y-5">
<div>
<label class="lbl">Preset</label>
<div class="grid grid-cols-2 gap-2 mt-2">
<template x-for="p in state.presets" :key="p.key">
<button class="preset-card"
:class="currentPresetMatches(p.key) ? 'preset-card-active' : ''"
@click="setPreset(p.key)">
<div class="font-medium text-sm" x-text="p.label"></div>
</button>
</template>
</div>
</div>
<div>
<label class="lbl">Current schema fields (advanced)</label>
<details class="mt-1">
<summary class="text-xs text-ink-500 cursor-pointer">edit fields…</summary>
<textarea class="input font-mono text-xs mt-2" rows="14" x-model="taskEditor.json"
@change="applyTaskJson()"></textarea>
<p class="text-[11px] text-ink-500 mt-1">JSON of the internal schema (fields with name/type/values/nullable/aggregator). Click outside to apply.</p>
</details>
</div>
</div>
</div>
</div>
<!-- Models -->
<div class="modal-backdrop" x-show="modal === 'models'" @click.self="closeModal()" x-transition>
<div class="modal-card max-w-2xl">
<header class="modal-header">
<h2 class="font-semibold">Models</h2>
<button @click="closeModal()" class="btn btn-ghost btn-icon"></button>
</header>
<div class="p-5 space-y-4">
<p class="text-xs text-ink-500">Pick one model for single inference, or 2+ to enable <strong>Mixture-of-Experts</strong> (parallel calls + majority vote, with disagreements highlighted on each token).</p>
<div class="space-y-1.5 max-h-72 overflow-y-auto">
<template x-for="m in allDisplayableModels()" :key="m">
<label class="flex items-center gap-3 p-2 rounded hover:bg-ink-50 cursor-pointer">
<input type="checkbox" :checked="state.models.includes(m)" @change="toggleModel(m)">
<span class="font-mono text-sm" x-text="m"></span>
<span class="badge badge-uploaded text-[10px]" x-show="!state.curated_models.includes(m)">custom</span>
</label>
</template>
</div>
<div>
<label class="lbl">Custom OpenRouter slug</label>
<div class="flex gap-2 mt-1">
<input class="input flex-1" type="text" x-model="modelEditor.custom" placeholder="provider/model-id">
<button class="btn btn-secondary" @click="addCustomModel()">Add</button>
</div>
</div>
<div>
<label class="lbl">MoE priority (tie-break order)</label>
<input class="input" type="text" x-model="modelEditor.priority"
@change="saveSettings({priority: modelEditor.priority.split(',').map(s => s.trim()).filter(Boolean)})"
placeholder="model1, model2, model3">
</div>
</div>
</div>
</div>
<!-- API key -->
<div class="modal-backdrop" x-show="modal === 'key'" @click.self="closeModal()" x-transition>
<div class="modal-card max-w-md">
<header class="modal-header">
<h2 class="font-semibold">OpenRouter API key</h2>
<button @click="closeModal()" class="btn btn-ghost btn-icon"></button>
</header>
<div class="p-5 space-y-3">
<div class="rounded-lg bg-emerald-50 border border-emerald-200 p-3 text-xs text-emerald-900">
<strong>🔒 Tab-scoped storage.</strong> Your key lives only in this browser tab's <code>sessionStorage</code>. It is sent as an <code>X-OpenRouter-Key</code> header on each annotation request and <strong>never stored on the server</strong>. Closing the tab wipes it. Use <strong>Clear key</strong> below at any time.
</div>
<template x-if="localKey">
<div class="rounded-lg bg-ink-50 border border-ink-200 p-3 text-xs flex items-center gap-2">
<span class="text-emerald-700"></span>
<span class="text-ink-700 flex-1">A key is set in this tab.</span>
<button class="btn btn-ghost text-xs text-red-600" @click="clearKey()">Clear key</button>
</div>
</template>
<label class="lbl" x-text="localKey ? 'Replace with a new key' : 'Paste your key, then press Enter (or click Save)'"></label>
<input class="input" type="password" placeholder="sk-or-v1-…"
x-model="keyEditor.value"
@keydown.enter.prevent="saveKey()"
x-ref="keyInput"
autocomplete="off"
autocorrect="off"
spellcheck="false">
<div class="flex items-center gap-2">
<button class="btn btn-primary flex-1" @click="saveKey()" x-text="localKey ? 'Replace key' : 'Save key (this tab only)'"></button>
<button class="btn btn-secondary" @click="testKey(true)" :disabled="keyEditor.testing"
title="Test against OpenRouter and save if it works">
<span x-show="!keyEditor.testing">Test &amp; save</span>
<span x-show="keyEditor.testing" class="spinner-sm"></span>
</button>
</div>
<div class="text-xs" x-show="keyEditor.result" :class="keyEditor.ok ? 'text-emerald-700' : 'text-red-600'" x-text="keyEditor.result"></div>
<template x-if="state.has_env_key">
<div class="text-[11px] text-ink-500 border-t border-ink-100 pt-2">
ⓘ A server-side <code>OPENROUTER_API_KEY</code> is also configured. If you don't set a tab-key, the server fallback is used.
</div>
</template>
<p class="text-[11px] text-ink-400">Get a key → <a class="text-accent-600 underline" target="_blank" href="https://openrouter.ai/keys">openrouter.ai/keys</a></p>
</div>
</div>
</div>
<!-- Paste -->
<div class="modal-backdrop" x-show="modal === 'paste'" @click.self="closeModal()" x-transition>
<div class="modal-card max-w-3xl">
<header class="modal-header">
<h2 class="font-semibold">Load your own corpus</h2>
<button @click="closeModal()" class="btn btn-ghost btn-icon"></button>
</header>
<div class="p-5 space-y-5">
<!-- 1. Text -->
<div>
<label class="lbl">① Your text</label>
<textarea class="input font-mono text-sm" rows="6" x-model="pasteEditor.text"
placeholder="One sentence per line. With 'one token per line', put each token on its own line and separate sentences with a blank line."></textarea>
<div class="grid grid-cols-2 gap-3 mt-2">
<div>
<label class="lbl">Tokenization</label>
<select class="input" x-model="pasteEditor.tokenizer">
<option value="whitespace">Split on whitespace</option>
<option value="as_is">One token per line</option>
<option value="newline">Newline-separated</option>
</select>
</div>
<div>
<label class="lbl">Language (free text)</label>
<input class="input" type="text" x-model="pasteEditor.language" placeholder="e.g. Ancient Greek">
</div>
</div>
</div>
<!-- 2. Task preset -->
<div>
<label class="lbl">② What should the LLM annotate?</label>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
<template x-for="p in state.presets" :key="p.key">
<button class="preset-card text-xs"
:class="pasteEditor.presetKey === p.key ? 'preset-card-active' : ''"
@click="pasteEditor.presetKey = p.key">
<div class="font-medium" x-text="p.label"></div>
</button>
</template>
</div>
</div>
<!-- 3. Custom inline editor — only when 'custom' is picked -->
<div x-show="pasteEditor.presetKey === 'custom'" class="rounded-lg border border-accent-200 bg-accent-50/60 p-3 space-y-3" x-transition>
<div>
<label class="lbl">Task name</label>
<input class="input" type="text" x-model="pasteEditor.customTaskName" placeholder="e.g. Medieval NER, Diplomatic tag…">
</div>
<div>
<label class="lbl">Your tag set (the LLM picks one per token)</label>
<div class="flex flex-wrap gap-1.5 mb-2" x-show="pasteEditor.customTags.length > 0">
<template x-for="(t, i) in pasteEditor.customTags" :key="t + '-' + i">
<span class="chip chip-active inline-flex items-center gap-1.5">
<span x-text="t"></span>
<button type="button" @click="pasteEditor.customTags.splice(i, 1)" class="hover:opacity-70 leading-none"></button>
</span>
</template>
</div>
<div class="flex gap-2">
<input class="input flex-1" type="text"
x-model="pasteEditor.customTagInput"
@keydown="onTagKeydown($event)"
@blur="if ((pasteEditor.customTagInput || '').trim()) addCustomTag()"
placeholder="Type a tag, then Enter or comma…">
<button type="button" class="btn btn-secondary text-xs whitespace-nowrap"
@click="addCustomTag()"
:disabled="!(pasteEditor.customTagInput || '').trim()">+ Add</button>
</div>
<p class="text-[11px] text-ink-500 mt-1">Press <kbd>Enter</kbd>, <kbd>,</kbd> or <kbd>;</kbd> to add. You can also paste <code>PER, LOC, ORG, WORK</code> and press Enter to add them all at once.</p>
<label class="flex items-center gap-2 mt-3 text-sm cursor-pointer">
<input type="checkbox" x-model="pasteEditor.includeNone">
<span>Add an <strong>"O"</strong> tag for tokens that match <em>none</em> of your labels
<span class="text-ink-500">(recommended — without it, the LLM is forced to pick a label for every token)</span>
</span>
</label>
</div>
<div>
<label class="lbl">Self-reported metadata, per token</label>
<div class="flex flex-wrap gap-x-4 gap-y-1.5 mt-1">
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="pasteEditor.includeConfidence">
<span>Confidence <span class="text-ink-500">(low / medium / high)</span></span>
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="pasteEditor.includeComment">
<span>Optional comment</span>
</label>
</div>
</div>
<p class="text-[11px] text-ink-500">
Need lemmatization, morphology, or several tag columns? Use one of the POS / Lemma presets above, or open <strong>📋 Task</strong> after loading for finer control.
</p>
</div>
<div class="flex justify-end gap-2 pt-1">
<button class="btn btn-ghost" @click="closeModal()">Cancel</button>
<button class="btn btn-primary"
@click="loadPaste()"
:disabled="!pasteEditor.text || (pasteEditor.presetKey === 'custom' && pasteEditor.customTags.length === 0)">
Load corpus
</button>
</div>
</div>
</div>
</div>
<!-- ICL Pool -->
<div class="modal-backdrop" x-show="modal === 'icl'" @click.self="closeModal()" x-transition>
<div class="modal-card max-w-2xl">
<header class="modal-header">
<h2 class="font-semibold">ICL pool · v<span x-text="state.icl_pool.version"></span></h2>
<button @click="closeModal()" class="btn btn-ghost btn-icon"></button>
</header>
<div class="p-5 space-y-3">
<p class="text-xs text-ink-500">Corrected sentences live here and are re-injected as few-shot examples on the next annotation run. The pool filters by <code>(language, schema_hash)</code> so cross-task contamination cannot happen.</p>
<div class="max-h-72 overflow-y-auto border border-ink-200 rounded">
<table class="w-full text-xs">
<thead class="bg-ink-50 sticky top-0">
<tr><th class="th">#</th><th class="th">lang</th><th class="th">source</th><th class="th">preview</th></tr>
</thead>
<tbody>
<template x-for="e in state.icl_pool.entries" :key="e.idx">
<tr class="border-t border-ink-100">
<td class="td font-mono" x-text="e.idx"></td>
<td class="td" x-text="e.language"></td>
<td class="td">
<span class="badge" :class="'badge-' + e.source" x-text="e.source"></span>
</td>
<td class="td font-mono text-ink-700" x-text="e.preview"></td>
</tr>
</template>
<tr x-show="state.icl_pool.size === 0">
<td colspan="4" class="td text-center text-ink-500 py-6">empty — correct a sentence and click 📥 to ICL.</td>
</tr>
</tbody>
</table>
</div>
<div class="flex gap-2">
<a class="btn btn-secondary" href="/api/icl/download" download>⬇ Download .jsonl</a>
<button class="btn btn-ghost" @click="clearIcl()">Clear pool</button>
</div>
</div>
</div>
</div>
<!-- Exports -->
<div class="modal-backdrop" x-show="modal === 'exports'" @click.self="closeModal()" x-transition>
<div class="modal-card max-w-md">
<header class="modal-header">
<h2 class="font-semibold">Export corpus</h2>
<button @click="closeModal()" class="btn btn-ghost btn-icon"></button>
</header>
<div class="p-5 space-y-2">
<p class="text-xs text-ink-500 mb-3">All sentences in the corpus, with their current (possibly corrected) annotations.</p>
<a class="btn btn-secondary w-full justify-start" href="/api/export/tsv" download>
<span class="mr-2"></span>TSV <span class="text-ink-400 text-xs ml-auto">round-trip with PIE baseline</span>
</a>
<a class="btn btn-secondary w-full justify-start" href="/api/export/json" download>
<span class="mr-2"></span>JSON <span class="text-ink-400 text-xs ml-auto">schema-conformant</span>
</a>
<a class="btn btn-secondary w-full justify-start" href="/api/export/conllu" download>
<span class="mr-2"></span>CoNLL-U <span class="text-ink-400 text-xs ml-auto">UD-standard</span>
</a>
<a class="btn btn-secondary w-full justify-start" href="/api/export/jsonl" download>
<span class="mr-2"></span>JSONL <span class="text-ink-400 text-xs ml-auto">fine-tune format</span>
</a>
</div>
</div>
</div>
<!-- Bulk apply -->
<div class="modal-backdrop" x-show="modal === 'bulk'" @click.self="closeModal()" x-transition>
<div class="modal-card max-w-md">
<header class="modal-header">
<h2 class="font-semibold">Apply to <span x-text="selection.size"></span> tokens</h2>
<button @click="closeModal()" class="btn btn-ghost btn-icon"></button>
</header>
<div class="p-5 space-y-3">
<div>
<label class="lbl">Field</label>
<select class="input" x-model="bulkEditor.field">
<template x-for="f in schemaFields" :key="f.name">
<option :value="f.name" x-text="f.name + ' (' + f.type + ')'"></option>
</template>
</select>
</div>
<div>
<label class="lbl">Value</label>
<template x-if="bulkSelectedField()?.type === 'enum'">
<select class="input" x-model="bulkEditor.value">
<template x-for="v in (bulkSelectedField()?.values || [])" :key="v">
<option :value="v" x-text="v"></option>
</template>
</select>
</template>
<template x-if="bulkSelectedField()?.type !== 'enum'">
<input class="input" type="text" x-model="bulkEditor.value">
</template>
</div>
<div class="flex justify-end gap-2">
<button class="btn btn-ghost" @click="closeModal()">Cancel</button>
<button class="btn btn-primary" @click="applyBulk()">Apply</button>
</div>
</div>
</div>
</div>
<!-- Advanced (prompt + temperature + n_icl) -->
<div class="modal-backdrop" x-show="modal === 'advanced'" @click.self="closeModal()" x-transition>
<div class="modal-card max-w-3xl">
<header class="modal-header">
<h2 class="font-semibold">⚙ Advanced settings</h2>
<button @click="closeModal()" class="btn btn-ghost btn-icon"></button>
</header>
<div class="p-5 space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="lbl">N few-shot examples (per request)</label>
<input class="input" type="number" min="0" max="50" x-model.number="advEditor.n_icl">
</div>
<div>
<label class="lbl">Temperature</label>
<input class="input" type="number" min="0" max="1" step="0.05" x-model.number="advEditor.temperature">
</div>
</div>
<div>
<label class="lbl">System prompt</label>
<textarea class="input font-mono text-xs" rows="6" x-model="advEditor.system_prompt"></textarea>
</div>
<div>
<label class="lbl">User template (variables: {tokens}, {tagset}, {schema}, {few_shot_examples}, {language}, …)</label>
<textarea class="input font-mono text-xs" rows="14" x-model="advEditor.user_template"></textarea>
</div>
<div class="flex justify-end gap-2">
<button class="btn btn-ghost" @click="closeModal()">Cancel</button>
<button class="btn btn-primary" @click="saveAdvanced()">Save</button>
</div>
</div>
</div>
</div>
<!-- Help -->
<div class="modal-backdrop" x-show="modal === 'help'" @click.self="closeModal()" x-transition>
<div class="modal-card max-w-2xl">
<header class="modal-header">
<h2 class="font-semibold">Help</h2>
<button @click="closeModal()" class="btn btn-ghost btn-icon"></button>
</header>
<div class="p-5 space-y-4">
<div>
<h3 class="font-semibold text-sm mb-2">Keyboard shortcuts</h3>
<table class="text-xs w-full">
<tr class="border-t border-ink-100"><td class="py-1.5 font-mono"><kbd>j</kbd> / <kbd>k</kbd></td><td>focus next / previous token</td></tr>
<tr class="border-t border-ink-100"><td class="py-1.5 font-mono"><kbd>e</kbd> / <kbd></kbd></td><td>edit focused token</td></tr>
<tr class="border-t border-ink-100"><td class="py-1.5 font-mono"><kbd>1</kbd><kbd>9</kbd></td><td>(in editor) assign the i-th visible tag</td></tr>
<tr class="border-t border-ink-100"><td class="py-1.5 font-mono"><kbd>x</kbd></td><td>toggle selection of focused token</td></tr>
<tr class="border-t border-ink-100"><td class="py-1.5 font-mono"><kbd>r</kbd></td><td>re-annotate the focused sentence</td></tr>
<tr class="border-t border-ink-100"><td class="py-1.5 font-mono"><kbd></kbd></td><td>save token edit and advance to next disagreement</td></tr>
<tr class="border-t border-ink-100"><td class="py-1.5 font-mono"><kbd>Esc</kbd></td><td>close popup</td></tr>
</table>
</div>
<div>
<h3 class="font-semibold text-sm mb-2">Workflow</h3>
<ol class="text-xs space-y-1.5 list-decimal list-inside text-ink-700">
<li>Pick a sandbox example (sidebar) or paste your text.</li>
<li>Choose a task preset (top bar → 📋 Task).</li>
<li>Set OpenRouter API key (top bar → 🔑) and one or more models.</li>
<li>Click <strong>▶ Annotate all</strong>. Disagreement tokens turn amber.</li>
<li>Click a token to fix it. Click <strong>📥 to ICL</strong> to feed corrections back.</li>
<li>Export TSV / JSON / CoNLL-U / JSONL when done.</li>
</ol>
</div>
</div>
</div>
</div>
<!-- =====================================================================
FOOTER LOGOS
===================================================================== -->
<footer class="orgs-footer">
<div class="max-w-[1400px] mx-auto px-5 py-2.5 flex flex-wrap items-center justify-center gap-x-5 gap-y-2">
<img src="/static/logos/LREC2026.png" alt="LREC 2026" title="LREC 2026" class="org-logo">
<img src="/static/logos/chartes.png" alt="École nationale des chartes — PSL" title="École nationale des chartes — PSL" class="org-logo">
<img src="/static/logos/cjm.png" alt="Centre Jean-Mabillon" title="Centre Jean-Mabillon" class="org-logo">
<img src="/static/logos/culturelab.png" alt="PSL CultureLab" title="PSL CultureLab (ANR-10-IDEX-0001)" class="org-logo">
<img src="/static/logos/DALiH_logo_200.png" alt="DALiH" title="ANR DALiH (ANR-21-CE38-0006)" class="org-logo">
</div>
</footer>
<!-- Token right-click context -->
<div x-show="ctxMenu.open"
class="fixed z-50 bg-white shadow-xl border border-ink-200 rounded-lg py-1.5 text-sm min-w-[180px]"
:style="`top:${ctxMenu.y}px; left:${ctxMenu.x}px`"
@click.outside="ctxMenu.open=false">
<button class="ctx-item" @click="openTokenEditor(ctxMenu.s, ctxMenu.t); ctxMenu.open=false">✎ Edit token</button>
<button class="ctx-item" @click="toggleSelectionIdx(ctxMenu.s, ctxMenu.t); ctxMenu.open=false">⊕ Toggle selection</button>
<div class="border-t border-ink-100 my-1"></div>
<template x-for="m in state.models" :key="m">
<button class="ctx-item text-xs" @click="reaskOneTokenAt(ctxMenu.s, ctxMenu.t, m); ctxMenu.open=false">
↻ Re-ask <span class="font-mono" x-text="modelShort(m)"></span>
</button>
</template>
</div>
<!-- Toast -->
<div class="fixed bottom-4 right-4 z-50 space-y-2">
<template x-for="t in toasts" :key="t.id">
<div class="toast" :class="'toast-' + t.kind" x-text="t.msg"></div>
</template>
</div>
<script src="/static/app.js?v=20260516b"></script>
</body>
</html>