m-newhauser's picture
Upload folder using huggingface_hub
2017a3d verified
Raw
History Blame Contribute Delete
30.4 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Span attributes - GLiNER 2.5</title>
<link rel="icon" href="favicon.svg" type="image/svg+xml">
<meta property="og:title" content="GLiNER 2.5 - Span attributes">
<meta property="og:description" content="Sentiment, role, assertion and impact attached to every extracted span.">
<meta property="og:image" content="og-card.png">
<link rel="stylesheet" href="brand.css">
<style>
/* demo-specific: fixed-height workbench. Both cards share one silhouette and
scroll internally; the rail is a minimap of the whole document (aria-hidden -
the annotated text itself is the accessible output). */
/* page fits one viewport: taller top margin, shorter cards, tighter footer.
Header mascot removed: header sits tighter, cards take the reclaimed space.
Column widened to 1280 so the boxes run wide; the "white space" lives inside
the boxes as extra padding instead of big outer margins. */
.wrap { max-width: 1280px; padding-top: 40px; padding-bottom: 26px; }
textarea#input, .output-body { padding-left: 24px; padding-right: 24px; }
.examples-panel { margin-top: 30px; }
.site-intro .sub { max-width: 92ch; }
.workspace > .card { height: min(59vh, 488px); }
textarea#input { resize: none; }
.doc-cols { display: grid; grid-template-columns: 1fr 42px; flex: 1; min-height: 0; position: relative; overflow: hidden; }
.doc-cols[hidden] { display: none; }
.output-body { min-height: 0; }
.rail { position: relative; margin: 14px 12px 14px 8px; background: var(--ash); border-radius: 6px; min-height: 120px; }
.rail-tick {
position: absolute; left: 0; right: 0; height: 3px; border-radius: 2px;
background: var(--span-color);
opacity: 0; animation: span-in 380ms var(--ease-out) forwards;
animation-delay: calc(var(--i) * 55ms);
}
.rail-caption, .rail-end {
position: absolute; left: 0; white-space: nowrap;
font-family: var(--font-mono); font-size: 10px; color: var(--ink-faint);
}
.rail-caption { top: -17px; }
.rail-end { bottom: -17px; }
@media (max-width: 860px) {
.workspace > .card { height: auto; }
.workspace > .card:last-child { height: min(59vh, 488px); }
textarea#input { min-height: 380px; }
.doc-cols { grid-template-columns: 1fr; }
.rail { display: none; }
}
.model-note { font-size: 11px; color: var(--ink-faint); }
.model-note a { color: var(--ink-soft); text-decoration: underline; text-decoration-color: var(--ash); text-underline-offset: 2px; }
.model-note a:hover { color: var(--orange-deep); text-decoration-color: var(--orange-deep); }
/* model attribution moved to the footnote: linked model name, code styling kept */
.footnote a[href*="huggingface.co"] { color: var(--ink-soft); text-decoration-color: var(--ash); text-underline-offset: 2px; }
.footnote a[href*="huggingface.co"]:hover { color: var(--orange-deep); text-decoration-color: var(--orange-deep); }
/* footer link row: centered, fox logo as separator */
.footer-links {
display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
gap: 14px; margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--ash);
}
.footer-links a {
font-size: 13px; font-weight: 500; color: var(--ink-soft); text-decoration: none;
transition: color 140ms var(--ease-out);
}
.footer-links a:hover { color: var(--orange-deep); }
.footer-links a:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.footer-links img { width: 16px; height: 16px; display: block; opacity: 0.9; }
/* local override of brand.css uppercase transform: mixed-case "Examples" */
.examples-label { text-transform: none; letter-spacing: 0.04em; }
/* controls back at the brand-default bottom: the capped output scrolls
internally, so the footer is always reachable (that's why they were on top) */
/* notebook Out[1] block replaced by tabs: code | result */
.code-tab {
font: inherit; font-family: var(--font-mono); font-size: 11px; font-weight: 600;
color: var(--ink-faint); background: none; border: 0; border-bottom: 2px solid transparent;
padding: 4px 10px 5px; cursor: pointer; letter-spacing: 0.02em;
transition: color 140ms var(--ease-out), border-color 140ms var(--ease-out);
}
.code-tab:hover { color: var(--ink-soft); }
.code-tab.active { color: var(--ink); border-bottom-color: var(--orange); }
.code-tab:focus-visible { outline: 2px solid var(--orange); outline-offset: -2px; }
.cell-result { color: var(--ink-soft); }
.cell-result .jk { color: var(--purple); }
.cell-result .js { color: var(--ok); }
.cell-result .jn { color: var(--orange-deep); }
.cell-result .jc { color: var(--ink-faint); font-style: italic; }
/* wider side padding on highlighted spans (local override of brand.css 1px 2px) */
#output .span { padding: 1px 7px; }
/* grid children must not let wide pre content inflate the tracks */
.workspace > .card { min-width: 0; }
.code-cell { max-width: 100%; margin: 0; flex: 1; min-height: 0; display: flex; flex-direction: column; }
.code-cell[hidden] { display: none; }
.code-panel { max-width: 100%; flex: 1; min-height: 0; overflow: auto; }
/* mono only for actual code: the code panel + result JSON. Everything else
(span labels, tabs, stats, legend counts, copy button) uses the body font. */
#output .span-label { font-family: var(--font-body); font-weight: 700; letter-spacing: 0.06em; }
.code-tab { font-family: var(--font-body); font-weight: 600; letter-spacing: 0.04em; }
.copy-btn { font-family: var(--font-body); }
.legend-count { font-family: var(--font-body); font-variant-numeric: tabular-nums; }
.stats strong { font-family: var(--font-body); font-variant-numeric: tabular-nums; }
.rail-caption, .rail-end { font-family: var(--font-body); font-variant-numeric: tabular-nums; }
/* label rides inside the span: all-caps bold, vertically centered, right after the text */
.span-label {
font-family: var(--font-mono); font-size: 8.5px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.05em;
color: var(--span-color);
margin-left: 4px; vertical-align: middle; line-height: 1;
white-space: nowrap;
opacity: 0; animation: chip-in 280ms var(--ease-out) forwards;
animation-delay: calc(var(--i) * 40ms + 200ms);
}
@keyframes chip-in { from { opacity: 0; } to { opacity: 1; } }
/* frosted hover pill (approved mock V1-frosted): one line, type | score | chars.
Above the span, centered; flips below when the span sits near the panel top.
pointer-events none so it never blocks the neighbouring span. */
.tip {
position: absolute; bottom: calc(100% + 7px); left: 50%;
transform: translateX(-50%) translateY(2px);
background: color-mix(in srgb, var(--greige) 78%, transparent);
backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
color: var(--ink); font-family: var(--font-mono); font-size: 10.5px; line-height: 1;
padding: 6px 10px; border-radius: 6px; white-space: nowrap;
border: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
box-shadow: 0 2px 10px rgba(16, 14, 28, 0.10);
opacity: 0; pointer-events: none;
transition: opacity 120ms var(--ease-out), transform 120ms var(--ease-out);
z-index: 2;
}
.span:hover .tip {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* flip below when there is no room above (span near the panel top) */
.span.flip-below .tip {
bottom: auto; top: calc(100% + 7px);
transform: translateX(-50%) translateY(-2px);
}
.span.flip-below:hover .tip { transform: translateX(-50%) translateY(0); }
/* real inference: dim + blur the previous output while the next pass runs
(blur masks the seam between old results and the incoming scan) */
.output-body { transition: opacity 160ms var(--ease-out), filter 160ms var(--ease-out); }
.output-body.busy { opacity: 0.55; filter: blur(1.5px); }
/* scan line (v3): thin solid 2px line sweeping smoothly up and down with a
light trail behind it. Two full-height layers — one per direction — painted
with a short gradient ending in the line; motion is transform-only. Both
share one cycle: down sweep 0-50%, up sweep 50-100%, so the line turns at
the top and bottom like the reference. --scan is the scanner's own color
(FF5E00), independent of the brand --orange. */
.doc-cols { --scan: #FF5E00; }
.scan-line {
position: absolute; inset: 0 42px 0 0;
pointer-events: none; opacity: 0; z-index: 1;
background-repeat: no-repeat;
transition: opacity 200ms var(--ease-out);
}
/* downward sweep: line leads at the bottom, trail above it */
.scan-line.down {
background-image: linear-gradient(to bottom, transparent,
color-mix(in srgb, var(--scan) 5%, transparent) 55%,
color-mix(in srgb, var(--scan) 13%, transparent) calc(100% - 2px),
var(--scan) calc(100% - 2px));
background-size: 100% 180px; background-position: 0 100%;
}
/* upward sweep: line leads at the top, trail below it */
.scan-line.up {
background-image: linear-gradient(to top, transparent,
color-mix(in srgb, var(--scan) 5%, transparent) 55%,
color-mix(in srgb, var(--scan) 13%, transparent) calc(100% - 2px),
var(--scan) calc(100% - 2px));
background-size: 100% 180px; background-position: 0 0;
}
.doc-cols.busy .scan-line.down { opacity: 1; animation: scan-down 2.8s linear infinite; }
.doc-cols.busy .scan-line.up { opacity: 1; animation: scan-up 2.8s linear infinite; }
/* layers are doc-height, so translateY(±100%) is one full column. The two
keyframe sets hand over at 50% with a ~30ms crossfade at each turn. */
@keyframes scan-down {
0% { transform: translateY(-100%); opacity: 0; }
1%, 49% { opacity: 1; }
50% { transform: translateY(0); opacity: 0; }
100% { transform: translateY(0); opacity: 0; }
}
@keyframes scan-up {
0%, 50% { transform: translateY(100%); opacity: 0; }
51%, 99% { opacity: 1; }
100% { transform: translateY(0); opacity: 0; }
}
/* rail minimap: two glows track the line, same cycle (pseudos survive
renderRail's innerHTML rewrite) */
.rail::before, .rail::after {
content: ""; position: absolute; inset: 0;
background: linear-gradient(180deg, transparent 0, var(--scan) 30%,
var(--scan) 70%, transparent 100%);
background-size: 100% 22px; background-repeat: no-repeat;
opacity: 0; pointer-events: none;
transition: opacity 180ms var(--ease-out);
}
.rail::before { background-position: 0 0; }
.rail::after { background-position: 0 100%; }
.doc-cols.busy .rail::before { opacity: 0.85; animation: rail-down 2.8s linear infinite; }
.doc-cols.busy .rail::after { opacity: 0.85; animation: rail-up 2.8s linear infinite; }
@keyframes rail-down {
0% { transform: translateY(0); opacity: 0; }
1%, 49% { opacity: 0.85; }
50% { transform: translateY(calc(100% - 22px)); opacity: 0; }
100% { transform: translateY(calc(100% - 22px)); opacity: 0; }
}
@keyframes rail-up {
0%, 50% { transform: translateY(calc(100% - 22px)); opacity: 0; }
51%, 99% { opacity: 0.85; }
100% { transform: translateY(0); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
/* drop the movement, keep the state feedback: dimmed output + glow parked
mid-rail. A static line across the doc would read as an artifact: hide. */
.doc-cols.busy .scan-line { animation: none; opacity: 0; }
.doc-cols.busy .rail::before { animation: none; opacity: 0; }
.doc-cols.busy .rail::after { animation: none; opacity: 0.85; transform: translateY(calc(50% - 11px)); }
}
/* mobile (placed after .scan-line: same specificity, source order must win):
no rail column, so the line spans the full doc width */
@media (max-width: 860px) {
.doc-cols .scan-line { inset: 0 0 0 0; }
}
/* legend + stats stack: chips wrap freely, stats always sit beneath them */
.header-actions { display: flex; align-items: center; gap: 8px; }
#counts-toggle.on { background: var(--greige); color: var(--ink); }
.controls { flex-direction: column; align-items: stretch; gap: 0; }
.controls[hidden] { display: none; } /* hidden attr loses to display:flex */
.legend { display: flex; flex-wrap: wrap; gap: 8px; }
/* compact chips + denser text: more room for the colored spans section */
.legend-chip { font-size: 11px; padding: 1px 8px; gap: 5px; }
.legend-swatch { width: 7px; height: 7px; }
.legend-count { font-family: var(--font-body); font-variant-numeric: tabular-nums; font-size: 9.5px; }
textarea#input { font-size: 13.5px; line-height: 1.7; }
.output-body { font-size: 13.5px; line-height: 1.7; }
.stats { font-size: 11px; }
/* footnote code (model id, chunk params) in the kit mono, not browser default */
.footnote code { font-family: var(--font-mono); }
.stats { margin-left: 0; margin-top: 10px; }
/* model strip: mascot + linked model name/size, between examples and workspace */ .model-strip {
display: flex; align-items: center; gap: 10px;
background: var(--card); border: 1px solid var(--ash); border-radius: var(--radius);
padding: 7px 14px; margin: 0 0 18px;
}
.model-strip img { width: 26px; height: 26px; display: block; }
.model-strip-name {
font-family: var(--font-mono); font-size: 13px; font-weight: 600; color: var(--ink);
text-decoration: none; border-bottom: 1px solid var(--ash); padding-bottom: 1px;
transition: color 140ms var(--ease-out), border-color 140ms var(--ease-out);
}
.model-strip-name:hover { color: var(--orange-deep); border-bottom-color: var(--orange-deep); }
.model-strip-name:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.model-strip-size { font-family: var(--font-mono); font-size: 12px; color: var(--ink-faint); }
/* code/counts buttons: bunched together, were 55px apart under space-between */
.header-actions { display: flex; align-items: center; gap: 8px; }
/* counts toggle lit state when open */
/* counts toggle lit state when open */
</style>
</head>
<body>
<div class="wrap">
<header class="site-intro">
<div class="intro-copy">
<h1 id="title"></h1>
<p class="sub" id="subtitle"></p>
</div>
</header>
<main>
<section class="examples-panel" aria-label="Examples">
<span class="examples-label">Examples</span>
<div class="example-list" id="examples"></div>
</section>
<section class="model-strip" aria-label="Model">
<img src="assets/fox-laptop.png" alt="" width="26" height="26">
<a class="model-strip-name" href="https://huggingface.co/fastino/gliner2.5-multi-v1" target="_blank" rel="noopener">GLiNER2.5-Multi-v1</a>
<span class="model-strip-size">(0.3B)</span>
</section>
<div class="workspace">
<section class="card">
<div class="panel-header"><div><h2>Text</h2><p>Edit below, or pick an example.</p></div></div>
<textarea id="input" spellcheck="false" aria-label="Input text"></textarea>
</section>
<section class="card">
<div class="panel-header">
<div><h2>Spans and attributes</h2><p>Click a span to inspect its attribute groups. Each span carries its exact position along with a confidence score.</p></div>
<div class="header-actions">
<div class="header-actions">
<button type="button" class="code-toggle" id="code-toggle" aria-expanded="false" aria-label="Code">code</button>
<button type="button" class="code-toggle" id="counts-toggle" aria-expanded="false" aria-label="Span counts">counts</button>
</div>
</div>
</div>
<div class="code-cell" id="code-cell" hidden>
<div class="code-cell-bar" role="tablist" aria-label="Code and result">
<button type="button" class="code-tab active" id="tab-code" role="tab" aria-selected="true">code</button>
<button type="button" class="code-tab" id="tab-result" role="tab" aria-selected="false">result</button>
<button type="button" class="copy-btn" id="copy-btn" aria-label="Copy">
<span class="copy-label">Copy</span>
<span class="copy-done" hidden>Copied</span>
</button>
</div>
<pre class="code-panel" id="code-panel"></pre>
<pre class="code-panel cell-result" id="cell-result" hidden></pre>
</div>
<div class="doc-cols">
<div id="output" class="output-body"></div>
<div class="inspector" id="inspector" hidden></div>
<div class="rail" id="rail" aria-hidden="true"></div>
<div class="scan-line down" aria-hidden="true"></div>
<div class="scan-line up" aria-hidden="true"></div>
</div>
<div class="controls" id="controls" hidden>
<div class="legend" id="legend"></div>
<div class="stats">
<span title="End-to-end time for one pass over the document.">Latency<strong id="stat-ms">-</strong></span>
<span>Spans<strong id="stat-n">-</strong></span>
<span>Chars<strong id="stat-chars">-</strong></span>
<span title="Number of inference passes over the document.">Chunks<strong id="stat-chunks">-</strong></span>
</div>
</div>
</section>
</div>
<p class="footnote" style="margin-top: 28px;">
Extraction runs <a href="https://huggingface.co/fastino/gliner2.5-multi-v1" target="_blank" rel="noopener"><code>fastino/gliner2.5-multi-v1</code></a> live on CPU. Attribute groups are scoped with <code>applies_to</code>: sentiment on products,
role on people, assertion on conditions. Built by
<a class="labs-mark" href="https://fastino.ai" target="_blank" rel="noopener">Fastino Labs</a>.
</p>
<nav class="footer-links" aria-label="GLiNER 2.5 links">
<a href="https://huggingface.co/collections/fastino/gliner25-models" target="_blank" rel="noopener">GLiNER2.5 models</a>
<img src="assets/transparent-dark-64.png" alt="" width="16" height="16">
<a href="https://huggingface.co/collections/fastino/gliner25-demos" target="_blank" rel="noopener">GLiNER2.5 demos</a>
<img src="assets/transparent-dark-64.png" alt="" width="16" height="16">
<a href="https://github.com/fastino-ai/GLiNER2" target="_blank" rel="noopener">GitHub</a>
<img src="assets/transparent-dark-64.png" alt="" width="16" height="16">
<a href="https://fastino.ai/blog/gliner2-5-span-free-information-extraction" target="_blank" rel="noopener">Release blog</a>
</nav>
</main>
</div>
<script src="strip.js"></script>
<script>
const $ = (id) => document.getElementById(id);
const esc = (v) => v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
let READY = false, timer = null, version = 0, labels = [], lastSpans = [];
let REAL = false; // live model (vs instant mock): longer debounce + pending state
/* pinned brand colors for specific labels; others fall back to palette order */
const LABEL_COLOR = { procedure: "#871875" };
function spanColor(label) { return LABEL_COLOR[label] || `var(--c${labels.indexOf(label) % 8})`; }
/* Badge tone. Semantic hues for sentiment/assertion/impact; everything else
falls back to the span's own label color at low alpha. All pairs AA. */
const TONE = {
positive: ["#0B7A78", "rgba(11,122,120,0.12)"], present: ["#0B7A78", "rgba(11,122,120,0.12)"],
executive: ["#7038FF", "rgba(112,56,255,0.10)"],
negative: ["#C2183B", "rgba(194,24,59,0.10)"], absent: ["#C2183B", "rgba(194,24,59,0.10)"],
data_loss: ["#C2183B", "rgba(194,24,59,0.10)"], security_risk: ["#C2183B", "rgba(194,24,59,0.10)"],
neutral: ["#55606E", "rgba(85,96,110,0.10)"], historical: ["#55606E", "rgba(85,96,110,0.10)"],
};
function badgeStyle(label, spanColor) {
const t = TONE[label];
const base = t ? t[0] : spanColor;
const bg = t ? t[1] : `color-mix(in srgb, ${spanColor} 12%, transparent)`;
return `--badge-text:${base};--badge-bg:${bg};--badge-border:${base}`;
}
function attrBadges(attributes, i, spanColor) {
let html = "";
for (const [group, entries] of Object.entries(attributes || {})) {
for (const e of entries) {
html += `<span class="attr" style="--i:${i};${badgeStyle(e.label, spanColor)}" title="${esc(group)} · score ${e.score.toFixed(2)}">${esc(e.label)}</span>`;
}
}
return html;
}
function render(text, spans) {
let html = "", prev = 0, i = 0;
for (const sp of spans) {
html += esc(text.slice(prev, sp.start));
const c = spanColor(sp.label);
// frosted hover pill: type | score | chars
const tip = `<span class="tip"><span class="t-type">${esc(sp.label)}</span><span class="t-sep">|</span>${sp.score.toFixed(2)}<span class="t-sep">|</span><span class="t-off">chars ${sp.start} to ${sp.end}</span></span>`;
html += `<span class="span-wrap"><span class="span" data-idx="${i}" style="--span-color:${c};--i:${i}" data-start="${sp.start}" data-end="${sp.end}" data-score="${sp.score}">${esc(sp.text)}<span class="span-label" style="--i:${i}">${esc(sp.label)}</span>${tip}</span>${attrBadges(sp.attributes, i, c)}</span>`;
prev = sp.end; i++;
}
return html + esc(text.slice(prev));
}
function renderRail(text, spans) {
// Minimap: ticks land at char offset / doc length down the fixed-height rail.
$("rail").innerHTML = spans.map((sp, i) =>
`<span class="rail-tick" style="top:${(sp.start / text.length * 100).toFixed(1)}%;--span-color:${spanColor(sp.label)};--i:${i}"></span>`
).join("");
}
function renderLegend(spans) {
const counts = {};
for (const sp of spans) counts[sp.label] = (counts[sp.label] || 0) + 1;
$("legend").innerHTML = Object.entries(counts).map(([label, n]) =>
`<span class="legend-chip" style="--span-color:${spanColor(label)}" title="${n} ${esc(label)} span${n > 1 ? "s" : ""}">
<span class="legend-swatch"></span>${esc(label)}<span class="legend-count">${n}</span>
</span>`).join("");
}
function renderStatChunks(data) {
$("stat-chunks").textContent = data.chunks ? data.chunks.n_chunks : "-";
}
function setEmpty(state) {
const html = {
idle: "",
none: REAL
? '<div class="output-empty"><strong>No spans found.</strong>The model read the whole document and found no matches. Try different text or switch examples.</div>'
: '<div class="output-empty"><strong>No spans found.</strong>Free-typed text returns no result in mock mode. Click an example to see extractions.</div>',
error: '<div class="output-empty"><strong>Inference failed.</strong>The server did not respond. Check that the model is still running and try again.</div>',
}[state];
$("output").innerHTML = html;
}
function setPending(on) {
$("output").classList.toggle("busy", on);
document.querySelector(".doc-cols").classList.toggle("busy", on);
$("output").setAttribute("aria-busy", on ? "true" : "false");
if (on) $("stat-ms").textContent = "\u2026";
}
async function infer() {
if (!READY) return;
const v = ++version, text = $("input").value;
if (!text.trim()) {
setPending(false);
setEmpty("idle"); renderLegend([]); renderRail(" ", []);
$("stat-ms").textContent = "-"; $("stat-n").textContent = "-";
$("stat-chars").textContent = "-"; $("stat-chunks").textContent = "-";
return;
}
setPending(true);
let data;
try {
const r = await fetch("api/infer", { method: "POST",
headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
data = await r.json();
} catch (e) {
if (v === version) { setPending(false); setEmpty("error"); $("stat-ms").textContent = "-"; }
return;
}
if (v !== version) return; // a newer pass is in flight; it owns the pending state
setPending(false);
const spans = data.spans || [];
lastSpans = spans;
labels = [...new Set([...labels, ...spans.map((s) => s.label)])];
$("output").innerHTML = spans.length ? render(text, spans) : "";
if (!spans.length) setEmpty("none");
renderRail(text, spans);
renderLegend(spans);
$("stat-ms").textContent = `${data.ms} ms`;
$("stat-n").textContent = spans.length;
$("stat-chars").textContent = text.length.toLocaleString();
renderStatChunks(data);
}
function inferSoon(delay) { clearTimeout(timer); timer = setTimeout(infer, delay ?? (REAL ? 700 : 300)); }
$("input").addEventListener("input", () => inferSoon());
/* flip the tooltip below when the span has no room above (live: the output
panel scrolls, so a one-shot class at render time would go stale) */
$("output").addEventListener("mouseover", (e) => {
const sp = e.target.closest(".span");
if (!sp) return;
const outTop = $("output").getBoundingClientRect().top;
sp.classList.toggle("flip-below", sp.getBoundingClientRect().top - outTop < 34);
});
/* code view replaces the annotated output; toggle swaps between them */
function setCodeView(open) {
$("code-cell").hidden = !open;
document.querySelector(".doc-cols").hidden = open;
$("controls").hidden = open ? true : !COUNTS_OPEN; // counts belong to the viz
const btn = $("code-toggle");
btn.textContent = open ? "close" : "code";
btn.setAttribute("aria-expanded", open);
btn.setAttribute("aria-label", open ? "Close code" : "Code");
}
$("code-toggle").addEventListener("click", () => setCodeView($("code-cell").hidden));
/* counts toggle: optional show/hide of the legend + stats strip */
let COUNTS_OPEN = false;
function setCountsView(open) {
COUNTS_OPEN = open;
$("controls").hidden = !open;
const btn = $("counts-toggle");
btn.textContent = open ? "hide" : "counts";
btn.setAttribute("aria-expanded", open);
btn.classList.toggle("on", open);
}
$("counts-toggle").addEventListener("click", () => setCountsView($("controls").hidden));
function renderResult(resultText) {
// light JSON tinting: keys, strings, numbers, trailing comments
let html = esc(resultText)
.replace(/("(?:[^"\\]|\\.)*?")(\s*:)/g, '<span class="jk">$1</span>$2')
.replace(/: ("(?:[^"\\]|\\.)*?")/g, ': <span class="js">$1</span>')
.replace(/(:\s)(\d+\.?\d*)/g, '$1<span class="jn">$2</span>')
.replace(/(#[^\n]*)/g, '<span class="jc">$1</span>');
$("cell-result").innerHTML = html;
}
let CODE_TAB = "code";
function setCodeTab(tab) {
CODE_TAB = tab;
const isCode = tab === "code";
$("tab-code").classList.toggle("active", isCode);
$("tab-result").classList.toggle("active", !isCode);
$("tab-code").setAttribute("aria-selected", isCode);
$("tab-result").setAttribute("aria-selected", !isCode);
$("code-panel").hidden = !isCode;
$("cell-result").hidden = isCode;
}
$("tab-code").addEventListener("click", () => setCodeTab("code"));
$("tab-result").addEventListener("click", () => setCodeTab("result"));
$("copy-btn").addEventListener("click", async () => {
const text = CODE_TAB === "code" ? $("code-panel").textContent : $("cell-result").textContent;
try { await navigator.clipboard.writeText(text); } catch (e) { return; }
$("copy-btn").querySelector(".copy-label").hidden = true;
$("copy-btn").querySelector(".copy-done").hidden = false;
setTimeout(() => {
$("copy-btn").querySelector(".copy-label").hidden = false;
$("copy-btn").querySelector(".copy-done").hidden = true;
}, 1400);
});
function renderInspector(idx) {
const box = $("inspector");
if (idx == null || !lastSpans[idx]) { box.hidden = true; box.innerHTML = ""; return; }
const sp = lastSpans[idx];
const c = spanColor(sp.label);
const groups = Object.entries(sp.attributes || {});
box.innerHTML = `
<div class="inspector-head">
<span class="span-text">&ldquo;${esc(sp.text)}&rdquo;</span>
<span class="span-label" style="--span-color:${c}">${esc(sp.label)}</span>
<span class="span-score">span score ${sp.score.toFixed(2)} &middot; chars ${sp.start}&ndash;${sp.end}</span>
</div>
${groups.length ? `<div class="inspector-groups">${groups.map(([group, entries]) => `
<div class="attr-group">
<div class="attr-group-name">${esc(group)}</div>
${entries.map((e) => `
<div class="attr-row">
<span class="attr-label">${esc(e.label)}</span>
<span class="attr-score">${e.score.toFixed(2)}</span>
</div>
<div class="attr-meter"><i style="width:${(e.score * 100).toFixed(0)}%;--span-color:${c}"></i></div>`).join("")}
</div>`).join("")}</div>`
: `<div class="output-empty">No attributes on this span.</div>`}`;
box.hidden = false;
}
$("output").addEventListener("click", (e) => {
const el = e.target.closest(".span");
if (!el) return;
const idx = +el.dataset.idx;
const wasSelected = el.classList.contains("selected");
document.querySelectorAll(".span.selected").forEach((x) => x.classList.remove("selected"));
if (wasSelected) { renderInspector(null); return; }
el.classList.add("selected");
renderInspector(idx);
});
(async () => {
const cfg = await fetchConfig();
REAL = cfg.mock === false;
document.title = `${cfg.title.replace(/<[^>]+>/g, "")} - GLiNER 2.5`;
$("title").innerHTML = cfg.title;
$("subtitle").innerHTML = cfg.subtitle;
cfg.examples.forEach((ex, i) => {
const b = document.createElement("button");
b.type = "button"; b.className = "ex" + (i === 0 ? " active" : "");
b.textContent = ex.chip;
b.addEventListener("click", () => {
document.querySelectorAll(".ex").forEach((x) => x.classList.toggle("active", x === b));
$("input").value = ex.text;
labels = []; // per-example schema: reset color mapping
// old example's spans no longer match the new document: clear before scan
$("output").innerHTML = ""; renderLegend([]); renderRail(" ", []);
$("code-panel").textContent = ex.code || "";
renderResult(ex.result || "");
inferSoon(40);
});
$("examples").append(b);
});
setEmpty("idle");
if (cfg.examples[0]) {
$("input").value = cfg.examples[0].text;
$("code-panel").textContent = cfg.examples[0].code || "";
renderResult(cfg.examples[0].result || "");
}
READY = await waitForServer();
if (READY) infer();
else setEmpty("error");
})();
</script>
</body>
</html>