| <!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> |
| |
| |
| |
| |
| |
| |
| |
| .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); } |
| |
| .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-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; } |
| |
| .examples-label { text-transform: none; letter-spacing: 0.04em; } |
| |
| |
| |
| .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; } |
| |
| #output .span { padding: 1px 7px; } |
| |
| .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; } |
| |
| |
| #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; } |
| |
| .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; } } |
| |
| |
| |
| .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); |
| } |
| |
| .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); } |
| |
| |
| .output-body { transition: opacity 160ms var(--ease-out), filter 160ms var(--ease-out); } |
| .output-body.busy { opacity: 0.55; filter: blur(1.5px); } |
| |
| |
| |
| |
| |
| |
| .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); |
| } |
| |
| .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%; |
| } |
| |
| .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; } |
| |
| |
| @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::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) { |
| |
| |
| .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)); } |
| } |
| |
| |
| @media (max-width: 860px) { |
| .doc-cols .scan-line { inset: 0 0 0 0; } |
| } |
| |
| .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; } |
| .legend { display: flex; flex-wrap: wrap; gap: 8px; } |
| |
| .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 { font-family: var(--font-mono); } |
| .stats { margin-left: 0; margin-top: 10px; } |
| .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); } |
| |
| .header-actions { display: flex; align-items: center; gap: 8px; } |
| |
| |
| </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, "&").replace(/</g, "<").replace(/>/g, ">"); |
| let READY = false, timer = null, version = 0, labels = [], lastSpans = []; |
| let REAL = false; |
| |
| |
| const LABEL_COLOR = { procedure: "#871875" }; |
| function spanColor(label) { return LABEL_COLOR[label] || `var(--c${labels.indexOf(label) % 8})`; } |
| |
| |
| |
| |
| 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); |
| |
| 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) { |
| |
| $("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; |
| 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()); |
| |
| |
| |
| $("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); |
| }); |
| |
| |
| function setCodeView(open) { |
| $("code-cell").hidden = !open; |
| document.querySelector(".doc-cols").hidden = open; |
| $("controls").hidden = open ? true : !COUNTS_OPEN; |
| 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)); |
| |
| |
| 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) { |
| |
| 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">“${esc(sp.text)}”</span> |
| <span class="span-label" style="--span-color:${c}">${esc(sp.label)}</span> |
| <span class="span-score">span score ${sp.score.toFixed(2)} · chars ${sp.start}–${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 = []; |
| |
| $("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> |
|
|