| <!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=20260516f"> |
| <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> |
|
|
| |
| |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <button @click="modal='models'" class="pill" |
| :class="(state.provider === 'openrouter' && state.models.length >= 2) ? 'pill-moe' : ''" |
| :title="state.models.join(', ')"> |
| <span class="pill-icon" |
| x-text="(state.provider === 'openrouter' && state.models.length >= 2) ? '🧠' : '🤖'"></span> |
| <span class="pill-label" |
| x-text="(state.provider === 'openrouter' && state.models.length >= 2) ? 'MoE' : 'Model'"></span> |
| <span class="pill-value" |
| x-text="state.models.length + ((state.provider === 'openrouter' && state.models.length >= 2) ? ' active' : '')"></span> |
| </button> |
|
|
| |
| <button @click="modal='key'" class="pill" :class="hasKey ? 'pill-ok' : 'pill-warn'" |
| :title="localKey ? state.provider + ' key stored in this tab only. Click to manage.' : (state.provider === 'openrouter' && state.has_env_key ? 'Using server-side OPENROUTER_API_KEY env.' : 'No key yet for ' + state.provider)"> |
| <span class="pill-icon" x-text="hasKey ? '🔑' : '⚠'"></span> |
| <span class="pill-label capitalize" x-text="hasKey ? state.provider : 'Add'"></span> |
| <span class="pill-value" |
| x-text="localKey ? 'tab-only' : ((state.provider === 'openrouter' && state.has_env_key) ? 'shared env' : 'API key')"></span> |
| </button> |
|
|
| |
| <button @click="modal='cite'" class="pill" title="Cite this work"> |
| <span class="pill-icon">📚</span> |
| <span class="pill-label">Cite</span> |
| </button> |
|
|
| <div class="flex-1"></div> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| |
| |
| <div class="max-w-[1400px] mx-auto px-5 py-6 grid grid-cols-12 gap-6"> |
|
|
| |
| <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> |
|
|
| <div class="rounded-lg border-2 border-red-400 bg-red-50 text-red-800 p-3 text-xs leading-relaxed"> |
| <strong>⚠ Heavy use?</strong> Duplicate this Space to your own account for intensive use and to avoid data |
| loss — shared instances may be reset at any time. |
| </div> |
|
|
| <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 class="col-span-9 space-y-4" tabindex="0" @keydown="handleKey($event)" x-ref="main"> |
|
|
| |
| <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> |
|
|
| |
| <section |
| x-show="state.provider === 'openrouter' && 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.provider === 'openrouter' && 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> |
|
|
| |
| <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> |
|
|
| |
| <template x-for="(sent, sidx) in state.sentences" :key="sent.id + '-' + 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 x-show="sent.status === 'done' && !sent.validated" |
| @click="setValidated(sidx, true)" |
| class="btn btn-secondary text-xs" |
| title="Compute per-model accuracy against your current annotation. Doesn't affect export."> |
| 📊 Score |
| </button> |
| <span x-show="sent.validated" |
| class="inline-flex items-center gap-1 text-xs text-accent-700 font-medium px-2 py-1 rounded bg-accent-50 border border-accent-200 cursor-pointer" |
| @click="setValidated(sidx, false)" |
| title="Click to hide the scores"> |
| 📊 Scored |
| </span> |
| <button @click="addSentenceToIcl(sidx)" :disabled="sent.status !== 'done'" |
| class="btn btn-ghost text-xs" |
| :class="{'opacity-50': sent.status !== 'done'}" |
| title="Add this annotation to the ICL pool as a few-shot example (also reveals scores)."> |
| 📥 to ICL |
| </button> |
| </header> |
|
|
| |
| <div class="px-4 pt-2 -mb-1 flex flex-wrap items-center gap-1.5" |
| x-show="sent.validated && Object.keys(sent.per_model || {}).length > 0"> |
| <span class="text-[10px] uppercase tracking-wider text-accent-700 font-semibold mr-1" |
| title="% of task-meaningful fields each model got right, compared to your current annotation">match · your version</span> |
| <template x-for="m in (sent._accuracy || modelAccuracy(sent))" :key="m.model"> |
| <span class="accuracy-pill" :class="accuracyClass(m.pct)" |
| :title="`${m.model}: ${m.correct}/${m.total} fields match your current annotation`"> |
| <span class="font-mono opacity-80" x-text="modelShort(m.model)"></span> |
| <strong x-text="m.pct + '%'"></strong> |
| </span> |
| </template> |
| </div> |
|
|
| <div class="p-4"> |
| <div class="tokens-flow"> |
| <template x-for="(tok, tidx) in sent.tokens" :key="sent.id + '-' + 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> |
|
|
| |
| |
| |
|
|
| |
| <template x-if="modal === 'token' && editor.tok"> |
| <div class="modal-backdrop" @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"> |
| |
| <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> |
|
|
| |
| <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 && editor.tok[f.name] === v ? 'chip-active' : ''" |
| @click="if (editor.tok) editor.tok[f.name] = v" |
| x-text="v"></button> |
| </template> |
| </div> |
| </div> |
| </template> |
|
|
| |
| <template x-if="f.type === 'string'"> |
| <input class="input" |
| type="text" |
| :value="editor.tok ? (editor.tok[f.name] || '') : ''" |
| @input="if (editor.tok) editor.tok[f.name] = $event.target.value" |
| :placeholder="f.name"> |
| </template> |
|
|
| |
| <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 ? ((editor.tok[f.name] || {})[sub.name] || '') : ''" |
| @input="if (editor.tok) editor.tok[f.name] = {...(editor.tok[f.name] || {}), [sub.name]: $event.target.value}"> |
| </div> |
| </template> |
| </div> |
| </template> |
| </div> |
| </template> |
| </div> |
|
|
| |
| <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> |
|
|
| |
| <div class="px-5 pb-3" |
| x-show="editor.tok && 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 & 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> |
| </template> |
|
|
| |
| <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> |
|
|
| |
| <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"> |
| |
| <div> |
| <label class="lbl">Provider</label> |
| <div class="grid grid-cols-3 gap-2"> |
| <template x-for="p in state.providers" :key="p"> |
| <button class="preset-card text-xs capitalize" |
| :class="state.provider === p ? 'preset-card-active' : ''" |
| @click="setProvider(p)"> |
| <div class="font-semibold" x-text="p"></div> |
| <div class="text-[10px] text-ink-500 mt-0.5 font-mono normal-case truncate" |
| x-text="p === 'openrouter' ? 'openrouter.ai/api/v1 · MoE' : p === 'mistral' ? 'api.mistral.ai/v1' : p === 'openai' ? 'api.openai.com/v1' : p === 'ilaas' ? 'llm.ilaas.fr/v1' : p"></div> |
| </button> |
| </template> |
| </div> |
| <p class="text-[11px] text-ink-500 mt-1.5"> |
| <span x-show="state.provider === 'openrouter'">🧠 OpenRouter supports MoE — you can pick several models.</span> |
| <span x-show="state.provider !== 'openrouter'">Single-model only. For Mixture-of-Experts (parallel models + vote), use <strong>OpenRouter</strong>.</span> |
| </p> |
| </div> |
|
|
| |
| <div> |
| <label class="lbl">Models for <span x-text="state.provider"></span></label> |
| <div class="space-y-1.5 max-h-60 overflow-y-auto border border-ink-200 rounded-lg p-1"> |
| <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> |
|
|
| |
| <div> |
| <label class="lbl">Add a custom model slug for <span x-text="state.provider"></span></label> |
| <div class="flex gap-2 mt-1"> |
| <input class="input flex-1" type="text" x-model="modelEditor.custom" |
| placeholder="model-id (e.g. mistral-medium-2505)"> |
| <button class="btn btn-secondary" @click="addCustomModel()">Add</button> |
| </div> |
| </div> |
|
|
| |
| <div x-show="state.provider === 'openrouter'"> |
| <label class="lbl">MoE priority (tie-break order, comma-separated slugs)</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> |
|
|
| |
| <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 capitalize"><span x-text="state.provider"></span> API key</h2> |
| <button @click="closeModal()" class="btn btn-ghost btn-icon">✕</button> |
| </header> |
| <div class="p-5 space-y-3"> |
| |
| <div> |
| <label class="lbl">Provider</label> |
| <div class="flex gap-1.5"> |
| <template x-for="p in state.providers" :key="p"> |
| <button class="chip capitalize" |
| :class="state.provider === p ? 'chip-active' : ''" |
| @click="setProvider(p)" x-text="p"></button> |
| </template> |
| </div> |
| </div> |
|
|
| <div class="rounded-lg bg-emerald-50 border border-emerald-200 p-3 text-xs text-emerald-900"> |
| <strong>🔒 Tab-scoped storage.</strong> Keys live only in this browser tab's <code>sessionStorage</code> |
| (one slot per provider). Sent as <code>X-API-Key</code> + <code>X-LLM-Provider</code> headers — never |
| stored on the server. |
| </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 <span |
| x-text="state.provider"></span> key is set in this tab.</span> |
| <button class="btn btn-ghost text-xs text-red-600" @click="clearKey()">Clear</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="state.provider === 'openrouter' ? 'sk-or-v1-…' : state.provider === 'openai' ? 'sk-…' : state.provider === 'mistral' ? 'Mistral key' : state.provider === 'ilaas' ? 'ILAAS key' : 'API key'" |
| 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 ${state.provider} and save if it works`"> |
| <span x-show="!keyEditor.testing">Test & 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 && state.provider === 'openrouter'"> |
| <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" x-show="state.provider === 'openrouter'" |
| href="https://openrouter.ai/keys">openrouter.ai/keys</a> |
| <a class="text-accent-600 underline" target="_blank" x-show="state.provider === 'mistral'" |
| href="https://console.mistral.ai/api-keys">console.mistral.ai/api-keys</a> |
| <a class="text-accent-600 underline" target="_blank" x-show="state.provider === 'openai'" |
| href="https://platform.openai.com/api-keys">platform.openai.com/api-keys</a> |
| <a class="text-accent-600 underline" |
| target="_blank" |
| x-show="state.provider === 'ilaas'" |
| href="https://www.ilaas.fr/services-inference/"> |
| ilaas.fr/services-inference |
| </a> |
| </p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <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"> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| |
| <div class="modal-backdrop" x-show="modal === 'cite'" @click.self="closeModal()" x-transition style="display:none"> |
| <div class="modal-card max-w-2xl"> |
| <header class="modal-header"> |
| <h2 class="font-semibold">Cite this work</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">If you use this tool, please cite:</p> |
| <pre id="bibtex-block" |
| class="text-[11px] bg-ink-50 border border-ink-200 rounded-lg p-3 overflow-auto font-mono whitespace-pre">@inproceedings{vidal-gorene-etal-2026-resourced, |
| title = "Under-resourced studies of under-resourced languages: lemmatization and {POS}-tagging with {LLM} annotators for historical {A}rmenian, {G}eorgian, {G}reek and {S}yriac", |
| author = "Vidal-Gor{\`e}ne, Chahan and |
| Kindt, Bastien and |
| Cafiero, Florian", |
| editor = "Hettiarachchi, Hansi and |
| Ranasinghe, Tharindu and |
| Plum, Alistair and |
| Rayson, Paul and |
| Mitkov, Ruslan and |
| Gaber, Mohamed and |
| Premasiri, Damith and |
| Tan, Fiona Anting and |
| Uyangodage, Lasitha", |
| booktitle = "Proceedings of the Second Workshop on Language Models for Low-Resource Languages ({L}o{R}es{LM} 2026)", |
| month = mar, |
| year = "2026", |
| address = "Rabat, Morocco", |
| publisher = "Association for Computational Linguistics", |
| url = "https://aclanthology.org/2026.loreslm-1.28/", |
| doi = "10.18653/v1/2026.loreslm-1.28", |
| pages = "324--334", |
| ISBN = "979-8-89176-377-7" |
| }</pre> |
| <div class="flex gap-2"> |
| <button class="btn btn-primary" |
| @click="navigator.clipboard.writeText(document.getElementById('bibtex-block').innerText); $event.target.textContent='✓ Copied'; setTimeout(()=>$event.target.textContent='Copy BibTeX', 1500)"> |
| Copy BibTeX |
| </button> |
| <a class="btn btn-secondary" target="_blank" href="https://aclanthology.org/2026.loreslm-1.28/">Open on |
| ACL Anthology ↗</a> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <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 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> |
|
|
| |
| <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> |
|
|
| |
| <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"></script> |
| </body> |
| </html> |
|
|