m-newhauser commited on
Commit
b3f34c3
·
verified ·
1 Parent(s): f17e84a

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -34,3 +34,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  static/assets/fox-laptop.png filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  static/assets/fox-laptop.png filter=lfs diff=lfs merge=lfs -text
37
+ static/assets/long-context-extraction.png filter=lfs diff=lfs merge=lfs -text
38
+ static/assets/page@b4824da6aaeb63052ccfb891e538eea3.webm filter=lfs diff=lfs merge=lfs -text
39
+ static/assets/teaser-gliner25-v2.gif filter=lfs diff=lfs merge=lfs -text
40
+ static/assets/teaser-gliner25-v3.gif filter=lfs diff=lfs merge=lfs -text
41
+ static/assets/teaser-gliner25-v4.gif filter=lfs diff=lfs merge=lfs -text
42
+ static/assets/teaser-gliner25.gif filter=lfs diff=lfs merge=lfs -text
__pycache__/model.cpython-312.pyc CHANGED
Binary files a/__pycache__/model.cpython-312.pyc and b/__pycache__/model.cpython-312.pyc differ
 
app.py CHANGED
@@ -55,8 +55,11 @@ def static_file(name):
55
 
56
  @app.route("/api/config")
57
  def config():
58
- """Demo content for the page: title, subtitle, example chips. No results."""
59
- return {k: v for k, v in model.DEMO.items() if k != "internal"}
 
 
 
60
 
61
 
62
  @app.route("/api/ready")
 
55
 
56
  @app.route("/api/config")
57
  def config():
58
+ """Demo content for the page: title, subtitle, example chips. No results.
59
+ Demos whose examples carry derived fields (inlined texts, result previews)
60
+ expose model.config(); others are served straight from model.DEMO."""
61
+ cfg = model.config() if hasattr(model, "config") else model.DEMO
62
+ return {k: v for k, v in cfg.items() if k != "internal"}
63
 
64
 
65
  @app.route("/api/ready")
model.py CHANGED
@@ -63,6 +63,56 @@ DEMO = {
63
 
64
  _HERE = os.path.dirname(os.path.abspath(__file__))
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  def warmup():
68
  if MOCK:
 
63
 
64
  _HERE = os.path.dirname(os.path.abspath(__file__))
65
 
66
+ def config():
67
+ """DEMO for /api/config, with result previews baked from fixtures."""
68
+ out = {k: v for k, v in DEMO.items() if k != "code"}
69
+ out["mock"] = MOCK
70
+ out["examples"] = []
71
+ for ex in DEMO["examples"]:
72
+ ex2 = {**ex, "code": DEMO["code"]}
73
+ ex2["result"] = _result_preview(ex["fixture"])
74
+ out["examples"].append(ex2)
75
+ return out
76
+
77
+
78
+ def _result_preview(fixture_name, per_type=2):
79
+ """Notebook-style Out[1] preview: the API response shape, truncated per label."""
80
+ with open(os.path.join(_HERE, "fixtures", fixture_name)) as f:
81
+ out = json.load(f)
82
+ entities = {}
83
+ for sp in out["spans"]:
84
+ entities.setdefault(sp["label"], [])
85
+ if len(entities[sp["label"]]) < per_type:
86
+ entities[sp["label"]].append({
87
+ "text": sp["text"],
88
+ "confidence": sp["score"],
89
+ "start": sp["start"],
90
+ "end": sp["end"],
91
+ **({"attributes": {g: [{ "label": a["label"], "score": a["score"]} for a in es]
92
+ for g, es in sp["attributes"].items()}} if sp.get("attributes") else {}),
93
+ })
94
+ counts = {}
95
+ for sp in out["spans"]:
96
+ counts[sp["label"]] = counts.get(sp["label"], 0) + 1
97
+ body = []
98
+ for label, entries in entities.items():
99
+ entry_strs = []
100
+ for e in entries:
101
+ base = (f'{{"text": {json.dumps(e["text"])}, "confidence": {e["confidence"]}, '
102
+ f'"start": {e["start"]}, "end": {e["end"]}')
103
+ if e.get("attributes"):
104
+ attrs = json.dumps(e["attributes"])
105
+ base += f', "attributes": {attrs}'
106
+ base += "}"
107
+ entry_strs.append(base)
108
+ more = counts[label] - len(entries)
109
+ parts = ", ".join(entry_strs)
110
+ if more > 0:
111
+ parts += f",\n # ... {more} more {label} span{'' if more == 1 else 's'}"
112
+ body.append(f' {json.dumps(label)}: [\n {parts}\n ]')
113
+ return f'{{\n "entities": {{\n' + ",\n".join(body) + "\n }\n}"
114
+
115
+
116
 
117
  def warmup():
118
  if MOCK:
static/assets/long-context-extraction.png ADDED

Git LFS Details

  • SHA256: 153070cef20beeda61aa194641bd4a7b71f19ccd360531a5ce50f25c70dfe436
  • Pointer size: 131 Bytes
  • Size of remote file: 396 kB
static/assets/page@b4824da6aaeb63052ccfb891e538eea3.webm ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1998fa63026722c2990bb651bed66a078634d3145b613446c3571724ceeb4884
3
+ size 157063
static/assets/teaser-gliner25-v2.gif ADDED

Git LFS Details

  • SHA256: f83c30372144026b5aa6d72d4558ebd4a70b95b64bfa8a48068074c50f5a1c2a
  • Pointer size: 132 Bytes
  • Size of remote file: 2.99 MB
static/assets/teaser-gliner25-v2.html ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <link rel="preconnect" href="https://fonts.googleapis.com">
6
+ <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap" rel="stylesheet">
7
+ <link href="https://www.nerdfonts.com/assets/css/webfont.css" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --paper: #F4F6F8;
11
+ --ink: #100E1C;
12
+ --c0: #FF6A2A; /* orange: product */
13
+ --c1: #7038FF; /* purple: person */
14
+ --c2: #0B7A78; /* teal: event */
15
+ --c3: #C2183B; /* red: location */
16
+ --ease: cubic-bezier(0.23, 1, 0.32, 1);
17
+ --mono: "0xProto Nerd Font", "0xProto Nerd", "JetBrains Mono", ui-monospace, Menlo, monospace;
18
+ }
19
+ * { margin: 0; padding: 0; box-sizing: border-box; }
20
+ html, body { width: 1252px; height: 374px; }
21
+ body {
22
+ background: var(--paper); overflow: hidden; position: relative;
23
+ font-family: "Geist", "Helvetica Neue", Arial, sans-serif;
24
+ }
25
+
26
+ /* ============ sentence gets extracted, loops ============ */
27
+ .sentence {
28
+ position: absolute; inset: 0;
29
+ display: flex; align-items: center; justify-content: center;
30
+ animation: sentence-fade 7s linear infinite;
31
+ }
32
+ .sentence p {
33
+ font-size: 42px; font-weight: 500; color: var(--ink);
34
+ letter-spacing: -0.015em; white-space: nowrap;
35
+ }
36
+ @keyframes sentence-fade {
37
+ 0% { opacity: 0; }
38
+ 7% { opacity: 1; } /* fully in by 0.49s */
39
+ 70% { opacity: 1; } /* long hold until 4.9s */
40
+ 82% { opacity: 0; } /* out by 5.74s */
41
+ 100% { opacity: 0; } /* rest until loop */
42
+ }
43
+
44
+ .ent {
45
+ position: relative; white-space: nowrap;
46
+ border-radius: 6px; padding: 0 3px; margin: 0 1px;
47
+ border-bottom: 3px solid transparent;
48
+ animation: ent-hl 7s var(--ease) infinite;
49
+ animation-delay: calc(var(--d) * 1s);
50
+ animation-fill-mode: both;
51
+ }
52
+ /* the highlight lands 525ms after the entity's stagger tick */
53
+ @keyframes ent-hl {
54
+ 0% { background-color: rgba(0,0,0,0); border-bottom-color: rgba(0,0,0,0); }
55
+ 7.5% { background-color: var(--bg); border-bottom-color: var(--c); }
56
+ 100% { background-color: var(--bg); border-bottom-color: var(--c); }
57
+ }
58
+
59
+ .chip {
60
+ position: absolute; left: 3px; bottom: calc(100% + 6px);
61
+ font-family: var(--mono); font-size: 15px; font-weight: 600; line-height: 1.5;
62
+ color: var(--c); background: var(--bg);
63
+ border: 1.5px solid var(--c); border-radius: 5px; padding: 0 7px;
64
+ opacity: 0; transform: translateY(4px) scale(0.94);
65
+ animation: chip-in 7s var(--ease) infinite;
66
+ animation-delay: calc((var(--d) + 0.2) * 1s);
67
+ animation-fill-mode: both;
68
+ }
69
+ @keyframes chip-in {
70
+ 0% { opacity: 0; transform: translateY(4px) scale(0.94); }
71
+ 6% { opacity: 1; transform: translateY(0) scale(1); }
72
+ 100% { opacity: 1; transform: translateY(0) scale(1); }
73
+ }
74
+
75
+ /* stagger ticks: 0.9s, 1.5s, 2.1s. orange=company(identity), purple=model, teal=date */
76
+ .e0 { --c: var(--c0); --bg: rgba(255,106,42,0.18); --d: 0.9; }
77
+ .e1 { --c: var(--c1); --bg: rgba(112,56,255,0.16); --d: 1.5; }
78
+ .e2 { --c: var(--c2); --bg: rgba(11,122,120,0.16); --d: 2.1; }
79
+
80
+ </style>
81
+ </head>
82
+ <body>
83
+
84
+ <div class="sentence">
85
+ <p><span class="ent e0">Fastino Labs<span class="chip">company</span></span> launches <span class="ent e1">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="chip">model</span></span> on <span class="ent e2">Monday, August 24th<span class="chip">date</span></span>.</p>
86
+ </div>
87
+
88
+ </body>
89
+ </html>
static/assets/teaser-gliner25-v3.gif ADDED

Git LFS Details

  • SHA256: f2419baa7879e5cde7e3937883da7d66b732d511be64b6f47ac6e3b29722c28e
  • Pointer size: 132 Bytes
  • Size of remote file: 3.05 MB
static/assets/teaser-gliner25-v4.gif ADDED

Git LFS Details

  • SHA256: 9bc55cf875a496a190ba10f5fcbc3b2f3bc6bc90021e1f65141272c8babc8cc8
  • Pointer size: 132 Bytes
  • Size of remote file: 1.49 MB
static/assets/teaser-gliner25.gif ADDED

Git LFS Details

  • SHA256: ac00c7e00dc4b9a5ee394c90f23d488aa40b4b6424b45cd7205f4fa6f81f8b50
  • Pointer size: 131 Bytes
  • Size of remote file: 134 kB
static/assets/teaser-gliner25.svg ADDED
static/assets/transparent-dark-64.png ADDED
static/index.html CHANGED
@@ -9,6 +9,250 @@
9
  <meta property="og:description" content="Sentiment, role, assertion and impact attached to every extracted span.">
10
  <meta property="og:image" content="og-card.png">
11
  <link rel="stylesheet" href="brand.css">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  </head>
13
  <body>
14
  <div class="wrap">
@@ -17,7 +261,6 @@
17
  <h1 id="title"></h1>
18
  <p class="sub" id="subtitle"></p>
19
  </div>
20
- <img class="intro-mascot" src="assets/fox-laptop.png" alt="Fastino fox mascot on a laptop" width="132" height="132">
21
  </header>
22
 
23
  <main>
@@ -26,6 +269,12 @@
26
  <div class="example-list" id="examples"></div>
27
  </section>
28
 
 
 
 
 
 
 
29
  <div class="workspace">
30
  <section class="card">
31
  <div class="panel-header"><div><h2>Text</h2><p>Edit below, or pick an example.</p></div></div>
@@ -34,37 +283,57 @@
34
 
35
  <section class="card">
36
  <div class="panel-header">
37
- <div><h2>Spans and attributes</h2><p>Click a span to inspect its attribute groups.</p></div>
38
- <button type="button" class="code-toggle" id="code-toggle">&lt;/&gt; code</button>
 
 
39
  </div>
40
  <div class="code-cell" id="code-cell" hidden>
41
- <div class="code-cell-bar">
42
- <span class="cell-prompt">In [1]:</span>
43
- <button type="button" class="copy-btn" id="copy-btn" aria-label="Copy code">
 
44
  <span class="copy-label">Copy</span>
45
  <span class="copy-done" hidden>Copied</span>
46
  </button>
47
  </div>
48
  <pre class="code-panel" id="code-panel"></pre>
49
- <div class="cell-out"><span class="cell-prompt">Out [1]:</span><span class="cell-out-hint"># paste into a notebook or python REPL to run</span></div>
50
  </div>
51
- <div id="output" class="output-body"><div class="output-empty">Extracted spans will appear here.</div></div>
52
- <div class="inspector" id="inspector" hidden></div>
53
- <div class="controls">
54
- <div class="legend" id="legend" style="display:contents"></div>
 
 
 
 
 
55
  <div class="stats">
56
- <span>Latency<strong id="stat-ms">-</strong></span>
57
  <span>Spans<strong id="stat-n">-</strong></span>
 
 
58
  </div>
59
  </div>
60
  </section>
61
  </div>
62
 
63
- <p class="footnote">
64
- Demo data. Attribute groups are scoped with <code>applies_to</code>: sentiment on products,
65
  role on people, assertion on conditions. Built by
66
  <a class="labs-mark" href="https://fastino.ai" target="_blank" rel="noopener">Fastino Labs</a>.
67
  </p>
 
 
 
 
 
 
 
 
 
 
68
  </main>
69
  </div>
70
 
@@ -73,6 +342,10 @@
73
  const $ = (id) => document.getElementById(id);
74
  const esc = (v) => v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
75
  let READY = false, timer = null, version = 0, labels = [], lastSpans = [];
 
 
 
 
76
 
77
  /* Badge tone. Semantic hues for sentiment/assertion/impact; everything else
78
  falls back to the span's own label color at low alpha. All pairs AA. */
@@ -101,20 +374,26 @@ function attrBadges(attributes, i, spanColor) {
101
  return html;
102
  }
103
 
104
- function spanColor(label) { return `var(--c${labels.indexOf(label) % 8})`; }
105
-
106
  function render(text, spans) {
107
  let html = "", prev = 0, i = 0;
108
  for (const sp of spans) {
109
  html += esc(text.slice(prev, sp.start));
110
  const c = spanColor(sp.label);
111
- const tip = `${sp.label} - score ${sp.score.toFixed(2)}`;
112
- html += `<span class="span-wrap"><span class="span" data-idx="${i}" style="--span-color:${c};--i:${i}" title="${esc(tip)}">${esc(sp.text)}</span>${attrBadges(sp.attributes, i, c)}</span>`;
 
113
  prev = sp.end; i++;
114
  }
115
  return html + esc(text.slice(prev));
116
  }
117
 
 
 
 
 
 
 
 
118
  function renderLegend(spans) {
119
  const counts = {};
120
  for (const sp of spans) counts[sp.label] = (counts[sp.label] || 0) + 1;
@@ -124,69 +403,115 @@ function renderLegend(spans) {
124
  </span>`).join("");
125
  }
126
 
127
- function renderInspector(idx, text) {
128
- const box = $("inspector");
129
- if (idx == null || !lastSpans[idx]) { box.hidden = true; box.innerHTML = ""; return; }
130
- const sp = lastSpans[idx];
131
- const c = spanColor(sp.label);
132
- const groups = Object.entries(sp.attributes || {});
133
- box.innerHTML = `
134
- <div class="inspector-head">
135
- <span class="span-text">&ldquo;${esc(sp.text)}&rdquo;</span>
136
- <span class="span-label" style="--span-color:${c}">${esc(sp.label)}</span>
137
- <span class="span-score">span score ${sp.score.toFixed(2)} &middot; chars ${sp.start}&ndash;${sp.end}</span>
138
- </div>
139
- ${groups.length ? `<div class="inspector-groups">${groups.map(([group, entries]) => `
140
- <div class="attr-group">
141
- <div class="attr-group-name">${esc(group)}</div>
142
- ${entries.map((e) => `
143
- <div class="attr-row">
144
- <span class="attr-label">${esc(e.label)}</span>
145
- <span class="attr-score">${e.score.toFixed(2)}</span>
146
- </div>
147
- <div class="attr-meter"><i style="width:${(e.score * 100).toFixed(0)}%;--span-color:${c}"></i></div>`).join("")}
148
- </div>`).join("")}</div>`
149
- : `<div class="output-empty">No attributes on this span.</div>`}`;
150
- box.hidden = false;
151
  }
152
 
153
  function setEmpty(state) {
154
  const html = {
155
- idle: '<div class="output-empty"><strong>Nothing extracted yet.</strong>Type text on the left, or click an example above.</div>',
156
- none: '<div class="output-empty"><strong>No spans found.</strong>Free-typed text returns an empty result in mock mode; the canned examples all produce extractions.</div>',
157
- error: '<div class="output-empty"><strong>Inference failed.</strong>The server did not respond. Check the model is still running, then try again.</div>',
 
 
158
  }[state];
159
  $("output").innerHTML = html;
160
  }
161
 
 
 
 
 
 
 
 
162
  async function infer() {
163
  if (!READY) return;
164
  const v = ++version, text = $("input").value;
165
- if (!text.trim()) { setEmpty("idle"); renderLegend(lastSpans = []); renderInspector(null); $("stat-ms").textContent = "-"; $("stat-n").textContent = "-"; return; }
 
 
 
 
 
 
 
166
  let data;
167
  try {
168
  const r = await fetch("api/infer", { method: "POST",
169
  headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) });
170
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
171
  data = await r.json();
172
- } catch (e) { if (v === version) setEmpty("error"); return; }
173
- if (v !== version) return;
 
 
 
 
174
  const spans = data.spans || [];
175
  lastSpans = spans;
176
  labels = [...new Set([...labels, ...spans.map((s) => s.label)])];
177
  $("output").innerHTML = spans.length ? render(text, spans) : "";
178
  if (!spans.length) setEmpty("none");
 
179
  renderLegend(spans);
180
- renderInspector(null);
181
  $("stat-ms").textContent = `${data.ms} ms`;
182
  $("stat-n").textContent = spans.length;
 
 
183
  }
184
 
185
- function inferSoon(delay = 300) { clearTimeout(timer); timer = setTimeout(infer, delay); }
186
  $("input").addEventListener("input", () => inferSoon());
187
- $("code-toggle").addEventListener("click", () => $("code-cell").hidden = !$("code-cell").hidden);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  $("copy-btn").addEventListener("click", async () => {
189
- try { await navigator.clipboard.writeText($("code-panel").textContent); } catch (e) { return; }
 
190
  $("copy-btn").querySelector(".copy-label").hidden = true;
191
  $("copy-btn").querySelector(".copy-done").hidden = false;
192
  setTimeout(() => {
@@ -194,6 +519,34 @@ $("copy-btn").addEventListener("click", async () => {
194
  $("copy-btn").querySelector(".copy-done").hidden = true;
195
  }, 1400);
196
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  $("output").addEventListener("click", (e) => {
198
  const el = e.target.closest(".span");
199
  if (!el) return;
@@ -207,10 +560,10 @@ $("output").addEventListener("click", (e) => {
207
 
208
  (async () => {
209
  const cfg = await fetchConfig();
 
210
  document.title = `${cfg.title.replace(/<[^>]+>/g, "")} - GLiNER 2.5`;
211
  $("title").innerHTML = cfg.title;
212
  $("subtitle").innerHTML = cfg.subtitle;
213
- $("code-panel").textContent = cfg.code || "";
214
 
215
  cfg.examples.forEach((ex, i) => {
216
  const b = document.createElement("button");
@@ -219,13 +572,22 @@ $("output").addEventListener("click", (e) => {
219
  b.addEventListener("click", () => {
220
  document.querySelectorAll(".ex").forEach((x) => x.classList.toggle("active", x === b));
221
  $("input").value = ex.text;
 
 
 
 
 
222
  inferSoon(40);
223
  });
224
  $("examples").append(b);
225
  });
226
 
227
  setEmpty("idle");
228
- if (cfg.examples[0]) $("input").value = cfg.examples[0].text;
 
 
 
 
229
  READY = await waitForServer();
230
  if (READY) infer();
231
  else setEmpty("error");
 
9
  <meta property="og:description" content="Sentiment, role, assertion and impact attached to every extracted span.">
10
  <meta property="og:image" content="og-card.png">
11
  <link rel="stylesheet" href="brand.css">
12
+ <style>
13
+ /* demo-specific: fixed-height workbench. Both cards share one silhouette and
14
+ scroll internally; the rail is a minimap of the whole document (aria-hidden -
15
+ the annotated text itself is the accessible output). */
16
+ /* page fits one viewport: taller top margin, shorter cards, tighter footer.
17
+ Header mascot removed: header sits tighter, cards take the reclaimed space.
18
+ Column widened to 1280 so the boxes run wide; the "white space" lives inside
19
+ the boxes as extra padding instead of big outer margins. */
20
+ .wrap { max-width: 1280px; padding-top: 40px; padding-bottom: 26px; }
21
+ textarea#input, .output-body { padding-left: 24px; padding-right: 24px; }
22
+ .examples-panel { margin-top: 30px; }
23
+ .site-intro .sub { max-width: 92ch; }
24
+ .workspace > .card { height: min(59vh, 488px); }
25
+ textarea#input { resize: none; }
26
+ .doc-cols { display: grid; grid-template-columns: 1fr 42px; flex: 1; min-height: 0; position: relative; overflow: hidden; }
27
+ .doc-cols[hidden] { display: none; }
28
+ .output-body { min-height: 0; }
29
+ .rail { position: relative; margin: 14px 12px 14px 8px; background: var(--ash); border-radius: 6px; min-height: 120px; }
30
+ .rail-tick {
31
+ position: absolute; left: 0; right: 0; height: 3px; border-radius: 2px;
32
+ background: var(--span-color);
33
+ opacity: 0; animation: span-in 380ms var(--ease-out) forwards;
34
+ animation-delay: calc(var(--i) * 55ms);
35
+ }
36
+ .rail-caption, .rail-end {
37
+ position: absolute; left: 0; white-space: nowrap;
38
+ font-family: var(--font-mono); font-size: 10px; color: var(--ink-faint);
39
+ }
40
+ .rail-caption { top: -17px; }
41
+ .rail-end { bottom: -17px; }
42
+ @media (max-width: 860px) {
43
+ .workspace > .card { height: auto; }
44
+ .workspace > .card:last-child { height: min(59vh, 488px); }
45
+ textarea#input { min-height: 380px; }
46
+ .doc-cols { grid-template-columns: 1fr; }
47
+ .rail { display: none; }
48
+ }
49
+ .model-note { font-size: 11px; color: var(--ink-faint); }
50
+ .model-note a { color: var(--ink-soft); text-decoration: underline; text-decoration-color: var(--ash); text-underline-offset: 2px; }
51
+ .model-note a:hover { color: var(--orange-deep); text-decoration-color: var(--orange-deep); }
52
+ /* model attribution moved to the footnote: linked model name, code styling kept */
53
+ .footnote a[href*="huggingface.co"] { color: var(--ink-soft); text-decoration-color: var(--ash); text-underline-offset: 2px; }
54
+ .footnote a[href*="huggingface.co"]:hover { color: var(--orange-deep); text-decoration-color: var(--orange-deep); }
55
+ /* footer link row: centered, fox logo as separator */
56
+ .footer-links {
57
+ display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
58
+ gap: 14px; margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--ash);
59
+ }
60
+ .footer-links a {
61
+ font-size: 13px; font-weight: 500; color: var(--ink-soft); text-decoration: none;
62
+ transition: color 140ms var(--ease-out);
63
+ }
64
+ .footer-links a:hover { color: var(--orange-deep); }
65
+ .footer-links a:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
66
+ .footer-links img { width: 16px; height: 16px; display: block; opacity: 0.9; }
67
+ /* local override of brand.css uppercase transform: mixed-case "Examples" */
68
+ .examples-label { text-transform: none; letter-spacing: 0.04em; }
69
+ /* controls back at the brand-default bottom: the capped output scrolls
70
+ internally, so the footer is always reachable (that's why they were on top) */
71
+ /* notebook Out[1] block replaced by tabs: code | result */
72
+ .code-tab {
73
+ font: inherit; font-family: var(--font-mono); font-size: 11px; font-weight: 600;
74
+ color: var(--ink-faint); background: none; border: 0; border-bottom: 2px solid transparent;
75
+ padding: 4px 10px 5px; cursor: pointer; letter-spacing: 0.02em;
76
+ transition: color 140ms var(--ease-out), border-color 140ms var(--ease-out);
77
+ }
78
+ .code-tab:hover { color: var(--ink-soft); }
79
+ .code-tab.active { color: var(--ink); border-bottom-color: var(--orange); }
80
+ .code-tab:focus-visible { outline: 2px solid var(--orange); outline-offset: -2px; }
81
+ .cell-result { color: var(--ink-soft); }
82
+ .cell-result .jk { color: var(--purple); }
83
+ .cell-result .js { color: var(--ok); }
84
+ .cell-result .jn { color: var(--orange-deep); }
85
+ .cell-result .jc { color: var(--ink-faint); font-style: italic; }
86
+ /* wider side padding on highlighted spans (local override of brand.css 1px 2px) */
87
+ #output .span { padding: 1px 7px; }
88
+ /* grid children must not let wide pre content inflate the tracks */
89
+ .workspace > .card { min-width: 0; }
90
+ .code-cell { max-width: 100%; margin: 0; flex: 1; min-height: 0; display: flex; flex-direction: column; }
91
+ .code-cell[hidden] { display: none; }
92
+ .code-panel { max-width: 100%; flex: 1; min-height: 0; overflow: auto; }
93
+ /* mono only for actual code: the code panel + result JSON. Everything else
94
+ (span labels, tabs, stats, legend counts, copy button) uses the body font. */
95
+ #output .span-label { font-family: var(--font-body); font-weight: 700; letter-spacing: 0.06em; }
96
+ .code-tab { font-family: var(--font-body); font-weight: 600; letter-spacing: 0.04em; }
97
+ .copy-btn { font-family: var(--font-body); }
98
+ .legend-count { font-family: var(--font-body); font-variant-numeric: tabular-nums; }
99
+ .stats strong { font-family: var(--font-body); font-variant-numeric: tabular-nums; }
100
+ .rail-caption, .rail-end { font-family: var(--font-body); font-variant-numeric: tabular-nums; }
101
+ /* label rides inside the span: all-caps bold, vertically centered, right after the text */
102
+ .span-label {
103
+ font-family: var(--font-mono); font-size: 8.5px; font-weight: 700;
104
+ text-transform: uppercase; letter-spacing: 0.05em;
105
+ color: var(--span-color);
106
+ margin-left: 4px; vertical-align: middle; line-height: 1;
107
+ white-space: nowrap;
108
+ opacity: 0; animation: chip-in 280ms var(--ease-out) forwards;
109
+ animation-delay: calc(var(--i) * 40ms + 200ms);
110
+ }
111
+ @keyframes chip-in { from { opacity: 0; } to { opacity: 1; } }
112
+ /* frosted hover pill (approved mock V1-frosted): one line, type | score | chars.
113
+ Above the span, centered; flips below when the span sits near the panel top.
114
+ pointer-events none so it never blocks the neighbouring span. */
115
+ .tip {
116
+ position: absolute; bottom: calc(100% + 7px); left: 50%;
117
+ transform: translateX(-50%) translateY(2px);
118
+ background: color-mix(in srgb, var(--greige) 78%, transparent);
119
+ backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
120
+ color: var(--ink); font-family: var(--font-mono); font-size: 10.5px; line-height: 1;
121
+ padding: 6px 10px; border-radius: 6px; white-space: nowrap;
122
+ border: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
123
+ box-shadow: 0 2px 10px rgba(16, 14, 28, 0.10);
124
+ opacity: 0; pointer-events: none;
125
+ transition: opacity 120ms var(--ease-out), transform 120ms var(--ease-out);
126
+ z-index: 2;
127
+ }
128
+ .span:hover .tip {
129
+ opacity: 1;
130
+ transform: translateX(-50%) translateY(0);
131
+ }
132
+ /* flip below when there is no room above (span near the panel top) */
133
+ .span.flip-below .tip {
134
+ bottom: auto; top: calc(100% + 7px);
135
+ transform: translateX(-50%) translateY(-2px);
136
+ }
137
+ .span.flip-below:hover .tip { transform: translateX(-50%) translateY(0); }
138
+ /* real inference: dim + blur the previous output while the next pass runs
139
+ (blur masks the seam between old results and the incoming scan) */
140
+ .output-body { transition: opacity 160ms var(--ease-out), filter 160ms var(--ease-out); }
141
+ .output-body.busy { opacity: 0.55; filter: blur(1.5px); }
142
+ /* scan line (v3): thin solid 2px line sweeping smoothly up and down with a
143
+ light trail behind it. Two full-height layers — one per direction — painted
144
+ with a short gradient ending in the line; motion is transform-only. Both
145
+ share one cycle: down sweep 0-50%, up sweep 50-100%, so the line turns at
146
+ the top and bottom like the reference. --scan is the scanner's own color
147
+ (FF5E00), independent of the brand --orange. */
148
+ .doc-cols { --scan: #FF5E00; }
149
+ .scan-line {
150
+ position: absolute; inset: 0 42px 0 0;
151
+ pointer-events: none; opacity: 0; z-index: 1;
152
+ background-repeat: no-repeat;
153
+ transition: opacity 200ms var(--ease-out);
154
+ }
155
+ /* downward sweep: line leads at the bottom, trail above it */
156
+ .scan-line.down {
157
+ background-image: linear-gradient(to bottom, transparent,
158
+ color-mix(in srgb, var(--scan) 5%, transparent) 55%,
159
+ color-mix(in srgb, var(--scan) 13%, transparent) calc(100% - 2px),
160
+ var(--scan) calc(100% - 2px));
161
+ background-size: 100% 180px; background-position: 0 100%;
162
+ }
163
+ /* upward sweep: line leads at the top, trail below it */
164
+ .scan-line.up {
165
+ background-image: linear-gradient(to top, transparent,
166
+ color-mix(in srgb, var(--scan) 5%, transparent) 55%,
167
+ color-mix(in srgb, var(--scan) 13%, transparent) calc(100% - 2px),
168
+ var(--scan) calc(100% - 2px));
169
+ background-size: 100% 180px; background-position: 0 0;
170
+ }
171
+ .doc-cols.busy .scan-line.down { opacity: 1; animation: scan-down 2.8s linear infinite; }
172
+ .doc-cols.busy .scan-line.up { opacity: 1; animation: scan-up 2.8s linear infinite; }
173
+ /* layers are doc-height, so translateY(±100%) is one full column. The two
174
+ keyframe sets hand over at 50% with a ~30ms crossfade at each turn. */
175
+ @keyframes scan-down {
176
+ 0% { transform: translateY(-100%); opacity: 0; }
177
+ 1%, 49% { opacity: 1; }
178
+ 50% { transform: translateY(0); opacity: 0; }
179
+ 100% { transform: translateY(0); opacity: 0; }
180
+ }
181
+ @keyframes scan-up {
182
+ 0%, 50% { transform: translateY(100%); opacity: 0; }
183
+ 51%, 99% { opacity: 1; }
184
+ 100% { transform: translateY(0); opacity: 0; }
185
+ }
186
+ /* rail minimap: two glows track the line, same cycle (pseudos survive
187
+ renderRail's innerHTML rewrite) */
188
+ .rail::before, .rail::after {
189
+ content: ""; position: absolute; inset: 0;
190
+ background: linear-gradient(180deg, transparent 0, var(--scan) 30%,
191
+ var(--scan) 70%, transparent 100%);
192
+ background-size: 100% 22px; background-repeat: no-repeat;
193
+ opacity: 0; pointer-events: none;
194
+ transition: opacity 180ms var(--ease-out);
195
+ }
196
+ .rail::before { background-position: 0 0; }
197
+ .rail::after { background-position: 0 100%; }
198
+ .doc-cols.busy .rail::before { opacity: 0.85; animation: rail-down 2.8s linear infinite; }
199
+ .doc-cols.busy .rail::after { opacity: 0.85; animation: rail-up 2.8s linear infinite; }
200
+ @keyframes rail-down {
201
+ 0% { transform: translateY(0); opacity: 0; }
202
+ 1%, 49% { opacity: 0.85; }
203
+ 50% { transform: translateY(calc(100% - 22px)); opacity: 0; }
204
+ 100% { transform: translateY(calc(100% - 22px)); opacity: 0; }
205
+ }
206
+ @keyframes rail-up {
207
+ 0%, 50% { transform: translateY(calc(100% - 22px)); opacity: 0; }
208
+ 51%, 99% { opacity: 0.85; }
209
+ 100% { transform: translateY(0); opacity: 0; }
210
+ }
211
+ @media (prefers-reduced-motion: reduce) {
212
+ /* drop the movement, keep the state feedback: dimmed output + glow parked
213
+ mid-rail. A static line across the doc would read as an artifact: hide. */
214
+ .doc-cols.busy .scan-line { animation: none; opacity: 0; }
215
+ .doc-cols.busy .rail::before { animation: none; opacity: 0; }
216
+ .doc-cols.busy .rail::after { animation: none; opacity: 0.85; transform: translateY(calc(50% - 11px)); }
217
+ }
218
+ /* mobile (placed after .scan-line: same specificity, source order must win):
219
+ no rail column, so the line spans the full doc width */
220
+ @media (max-width: 860px) {
221
+ .doc-cols .scan-line { inset: 0 0 0 0; }
222
+ }
223
+ /* legend + stats stack: chips wrap freely, stats always sit beneath them */
224
+ .controls { flex-direction: column; align-items: stretch; gap: 0; }
225
+ .controls[hidden] { display: none; } /* hidden attr loses to display:flex */
226
+ .legend { display: flex; flex-wrap: wrap; gap: 8px; }
227
+ /* compact chips + denser text: more room for the colored spans section */
228
+ .legend-chip { font-size: 11px; padding: 1px 8px; gap: 5px; }
229
+ .legend-swatch { width: 7px; height: 7px; }
230
+ .legend-count { font-family: var(--font-body); font-variant-numeric: tabular-nums; font-size: 9.5px; }
231
+ textarea#input { font-size: 13.5px; line-height: 1.7; }
232
+ .output-body { font-size: 13.5px; line-height: 1.7; }
233
+ .stats { font-size: 11px; }
234
+ /* footnote code (model id, chunk params) in the kit mono, not browser default */
235
+ .footnote code { font-family: var(--font-mono); }
236
+ .stats { margin-left: 0; margin-top: 10px; }
237
+ /* model strip: mascot + linked model name/size, between examples and workspace */ .model-strip {
238
+ display: flex; align-items: center; gap: 10px;
239
+ background: var(--card); border: 1px solid var(--ash); border-radius: var(--radius);
240
+ padding: 7px 14px; margin: 0 0 18px;
241
+ }
242
+ .model-strip img { width: 26px; height: 26px; display: block; }
243
+ .model-strip-name {
244
+ font-family: var(--font-mono); font-size: 13px; font-weight: 600; color: var(--ink);
245
+ text-decoration: none; border-bottom: 1px solid var(--ash); padding-bottom: 1px;
246
+ transition: color 140ms var(--ease-out), border-color 140ms var(--ease-out);
247
+ }
248
+ .model-strip-name:hover { color: var(--orange-deep); border-bottom-color: var(--orange-deep); }
249
+ .model-strip-name:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
250
+ .model-strip-size { font-family: var(--font-mono); font-size: 12px; color: var(--ink-faint); }
251
+ /* code/counts buttons: bunched together, were 55px apart under space-between */
252
+ .header-actions { display: flex; align-items: center; gap: 8px; }
253
+ /* counts toggle lit state when open */
254
+ /* counts toggle lit state when open */
255
+ </style>
256
  </head>
257
  <body>
258
  <div class="wrap">
 
261
  <h1 id="title"></h1>
262
  <p class="sub" id="subtitle"></p>
263
  </div>
 
264
  </header>
265
 
266
  <main>
 
269
  <div class="example-list" id="examples"></div>
270
  </section>
271
 
272
+ <section class="model-strip" aria-label="Model">
273
+ <img src="assets/fox-laptop.png" alt="" width="26" height="26">
274
+ <a class="model-strip-name" href="https://huggingface.co/fastino/gliner2.5-multi-v1" target="_blank" rel="noopener">GLiNER2.5-Multi-v1</a>
275
+ <span class="model-strip-size">(0.3B)</span>
276
+ </section>
277
+
278
  <div class="workspace">
279
  <section class="card">
280
  <div class="panel-header"><div><h2>Text</h2><p>Edit below, or pick an example.</p></div></div>
 
283
 
284
  <section class="card">
285
  <div class="panel-header">
286
+ <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>
287
+ <div class="header-actions">
288
+ <button type="button" class="code-toggle" id="code-toggle" aria-expanded="false" aria-label="Code">code</button>
289
+ </div>
290
  </div>
291
  <div class="code-cell" id="code-cell" hidden>
292
+ <div class="code-cell-bar" role="tablist" aria-label="Code and result">
293
+ <button type="button" class="code-tab active" id="tab-code" role="tab" aria-selected="true">code</button>
294
+ <button type="button" class="code-tab" id="tab-result" role="tab" aria-selected="false">result</button>
295
+ <button type="button" class="copy-btn" id="copy-btn" aria-label="Copy">
296
  <span class="copy-label">Copy</span>
297
  <span class="copy-done" hidden>Copied</span>
298
  </button>
299
  </div>
300
  <pre class="code-panel" id="code-panel"></pre>
301
+ <pre class="code-panel cell-result" id="cell-result" hidden></pre>
302
  </div>
303
+ <div class="doc-cols">
304
+ <div id="output" class="output-body"></div>
305
+ <div class="inspector" id="inspector" hidden></div>
306
+ <div class="rail" id="rail" aria-hidden="true"></div>
307
+ <div class="scan-line down" aria-hidden="true"></div>
308
+ <div class="scan-line up" aria-hidden="true"></div>
309
+ </div>
310
+ <div class="controls" id="controls">
311
+ <div class="legend" id="legend"></div>
312
  <div class="stats">
313
+ <span title="End-to-end time for one pass over the document.">Latency<strong id="stat-ms">-</strong></span>
314
  <span>Spans<strong id="stat-n">-</strong></span>
315
+ <span>Chars<strong id="stat-chars">-</strong></span>
316
+ <span title="Number of inference passes over the document.">Chunks<strong id="stat-chunks">-</strong></span>
317
  </div>
318
  </div>
319
  </section>
320
  </div>
321
 
322
+ <p class="footnote" style="margin-top: 28px;">
323
+ 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,
324
  role on people, assertion on conditions. Built by
325
  <a class="labs-mark" href="https://fastino.ai" target="_blank" rel="noopener">Fastino Labs</a>.
326
  </p>
327
+
328
+ <nav class="footer-links" aria-label="GLiNER 2.5 links">
329
+ <a href="https://huggingface.co/collections/fastino/gliner25-models" target="_blank" rel="noopener">GLiNER2.5 models</a>
330
+ <img src="assets/transparent-dark-64.png" alt="" width="16" height="16">
331
+ <a href="https://huggingface.co/collections/fastino/gliner25-demos" target="_blank" rel="noopener">GLiNER2.5 demos</a>
332
+ <img src="assets/transparent-dark-64.png" alt="" width="16" height="16">
333
+ <a href="https://github.com/fastino-ai/GLiNER2" target="_blank" rel="noopener">GitHub</a>
334
+ <img src="assets/transparent-dark-64.png" alt="" width="16" height="16">
335
+ <a href="https://fastino.ai/blog/gliner2-5-span-free-information-extraction" target="_blank" rel="noopener">Release blog</a>
336
+ </nav>
337
  </main>
338
  </div>
339
 
 
342
  const $ = (id) => document.getElementById(id);
343
  const esc = (v) => v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
344
  let READY = false, timer = null, version = 0, labels = [], lastSpans = [];
345
+ let REAL = false; // live model (vs instant mock): longer debounce + pending state
346
+
347
+ function spanColor(label) { return `var(--c${labels.indexOf(label) % 8})`; }
348
+
349
 
350
  /* Badge tone. Semantic hues for sentiment/assertion/impact; everything else
351
  falls back to the span's own label color at low alpha. All pairs AA. */
 
374
  return html;
375
  }
376
 
 
 
377
  function render(text, spans) {
378
  let html = "", prev = 0, i = 0;
379
  for (const sp of spans) {
380
  html += esc(text.slice(prev, sp.start));
381
  const c = spanColor(sp.label);
382
+ // frosted hover pill: type | score | chars
383
+ 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>`;
384
+ 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>`;
385
  prev = sp.end; i++;
386
  }
387
  return html + esc(text.slice(prev));
388
  }
389
 
390
+ function renderRail(text, spans) {
391
+ // Minimap: ticks land at char offset / doc length down the fixed-height rail.
392
+ $("rail").innerHTML = spans.map((sp, i) =>
393
+ `<span class="rail-tick" style="top:${(sp.start / text.length * 100).toFixed(1)}%;--span-color:${spanColor(sp.label)};--i:${i}"></span>`
394
+ ).join("");
395
+ }
396
+
397
  function renderLegend(spans) {
398
  const counts = {};
399
  for (const sp of spans) counts[sp.label] = (counts[sp.label] || 0) + 1;
 
403
  </span>`).join("");
404
  }
405
 
406
+ function renderStatChunks(data) {
407
+ $("stat-chunks").textContent = data.chunks ? data.chunks.n_chunks : "-";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
  }
409
 
410
  function setEmpty(state) {
411
  const html = {
412
+ idle: "",
413
+ none: REAL
414
+ ? '<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>'
415
+ : '<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>',
416
+ 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>',
417
  }[state];
418
  $("output").innerHTML = html;
419
  }
420
 
421
+ function setPending(on) {
422
+ $("output").classList.toggle("busy", on);
423
+ document.querySelector(".doc-cols").classList.toggle("busy", on);
424
+ $("output").setAttribute("aria-busy", on ? "true" : "false");
425
+ if (on) $("stat-ms").textContent = "\u2026";
426
+ }
427
+
428
  async function infer() {
429
  if (!READY) return;
430
  const v = ++version, text = $("input").value;
431
+ if (!text.trim()) {
432
+ setPending(false);
433
+ setEmpty("idle"); renderLegend([]); renderRail(" ", []);
434
+ $("stat-ms").textContent = "-"; $("stat-n").textContent = "-";
435
+ $("stat-chars").textContent = "-"; $("stat-chunks").textContent = "-";
436
+ return;
437
+ }
438
+ setPending(true);
439
  let data;
440
  try {
441
  const r = await fetch("api/infer", { method: "POST",
442
  headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) });
443
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
444
  data = await r.json();
445
+ } catch (e) {
446
+ if (v === version) { setPending(false); setEmpty("error"); $("stat-ms").textContent = "-"; }
447
+ return;
448
+ }
449
+ if (v !== version) return; // a newer pass is in flight; it owns the pending state
450
+ setPending(false);
451
  const spans = data.spans || [];
452
  lastSpans = spans;
453
  labels = [...new Set([...labels, ...spans.map((s) => s.label)])];
454
  $("output").innerHTML = spans.length ? render(text, spans) : "";
455
  if (!spans.length) setEmpty("none");
456
+ renderRail(text, spans);
457
  renderLegend(spans);
 
458
  $("stat-ms").textContent = `${data.ms} ms`;
459
  $("stat-n").textContent = spans.length;
460
+ $("stat-chars").textContent = text.length.toLocaleString();
461
+ renderStatChunks(data);
462
  }
463
 
464
+ function inferSoon(delay) { clearTimeout(timer); timer = setTimeout(infer, delay ?? (REAL ? 700 : 300)); }
465
  $("input").addEventListener("input", () => inferSoon());
466
+
467
+ /* flip the tooltip below when the span has no room above (live: the output
468
+ panel scrolls, so a one-shot class at render time would go stale) */
469
+ $("output").addEventListener("mouseover", (e) => {
470
+ const sp = e.target.closest(".span");
471
+ if (!sp) return;
472
+ const outTop = $("output").getBoundingClientRect().top;
473
+ sp.classList.toggle("flip-below", sp.getBoundingClientRect().top - outTop < 34);
474
+ });
475
+
476
+ /* code view replaces the annotated output; toggle swaps between them */
477
+ function setCodeView(open) {
478
+ $("code-cell").hidden = !open;
479
+ document.querySelector(".doc-cols").hidden = open;
480
+ $("controls").hidden = open;
481
+ const btn = $("code-toggle");
482
+ btn.textContent = open ? "close" : "code";
483
+ btn.setAttribute("aria-expanded", open);
484
+ btn.setAttribute("aria-label", open ? "Close code" : "Code");
485
+ }
486
+ $("code-toggle").addEventListener("click", () => setCodeView($("code-cell").hidden));
487
+
488
+
489
+ function renderResult(resultText) {
490
+ // light JSON tinting: keys, strings, numbers, trailing comments
491
+ let html = esc(resultText)
492
+ .replace(/("(?:[^"\\]|\\.)*?")(\s*:)/g, '<span class="jk">$1</span>$2')
493
+ .replace(/: ("(?:[^"\\]|\\.)*?")/g, ': <span class="js">$1</span>')
494
+ .replace(/(:\s)(\d+\.?\d*)/g, '$1<span class="jn">$2</span>')
495
+ .replace(/(#[^\n]*)/g, '<span class="jc">$1</span>');
496
+ $("cell-result").innerHTML = html;
497
+ }
498
+
499
+ let CODE_TAB = "code";
500
+ function setCodeTab(tab) {
501
+ CODE_TAB = tab;
502
+ const isCode = tab === "code";
503
+ $("tab-code").classList.toggle("active", isCode);
504
+ $("tab-result").classList.toggle("active", !isCode);
505
+ $("tab-code").setAttribute("aria-selected", isCode);
506
+ $("tab-result").setAttribute("aria-selected", !isCode);
507
+ $("code-panel").hidden = !isCode;
508
+ $("cell-result").hidden = isCode;
509
+ }
510
+ $("tab-code").addEventListener("click", () => setCodeTab("code"));
511
+ $("tab-result").addEventListener("click", () => setCodeTab("result"));
512
  $("copy-btn").addEventListener("click", async () => {
513
+ const text = CODE_TAB === "code" ? $("code-panel").textContent : $("cell-result").textContent;
514
+ try { await navigator.clipboard.writeText(text); } catch (e) { return; }
515
  $("copy-btn").querySelector(".copy-label").hidden = true;
516
  $("copy-btn").querySelector(".copy-done").hidden = false;
517
  setTimeout(() => {
 
519
  $("copy-btn").querySelector(".copy-done").hidden = true;
520
  }, 1400);
521
  });
522
+
523
+
524
+ function renderInspector(idx) {
525
+ const box = $("inspector");
526
+ if (idx == null || !lastSpans[idx]) { box.hidden = true; box.innerHTML = ""; return; }
527
+ const sp = lastSpans[idx];
528
+ const c = spanColor(sp.label);
529
+ const groups = Object.entries(sp.attributes || {});
530
+ box.innerHTML = `
531
+ <div class="inspector-head">
532
+ <span class="span-text">&ldquo;${esc(sp.text)}&rdquo;</span>
533
+ <span class="span-label" style="--span-color:${c}">${esc(sp.label)}</span>
534
+ <span class="span-score">span score ${sp.score.toFixed(2)} &middot; chars ${sp.start}&ndash;${sp.end}</span>
535
+ </div>
536
+ ${groups.length ? `<div class="inspector-groups">${groups.map(([group, entries]) => `
537
+ <div class="attr-group">
538
+ <div class="attr-group-name">${esc(group)}</div>
539
+ ${entries.map((e) => `
540
+ <div class="attr-row">
541
+ <span class="attr-label">${esc(e.label)}</span>
542
+ <span class="attr-score">${e.score.toFixed(2)}</span>
543
+ </div>
544
+ <div class="attr-meter"><i style="width:${(e.score * 100).toFixed(0)}%;--span-color:${c}"></i></div>`).join("")}
545
+ </div>`).join("")}</div>`
546
+ : `<div class="output-empty">No attributes on this span.</div>`}`;
547
+ box.hidden = false;
548
+ }
549
+
550
  $("output").addEventListener("click", (e) => {
551
  const el = e.target.closest(".span");
552
  if (!el) return;
 
560
 
561
  (async () => {
562
  const cfg = await fetchConfig();
563
+ REAL = cfg.mock === false;
564
  document.title = `${cfg.title.replace(/<[^>]+>/g, "")} - GLiNER 2.5`;
565
  $("title").innerHTML = cfg.title;
566
  $("subtitle").innerHTML = cfg.subtitle;
 
567
 
568
  cfg.examples.forEach((ex, i) => {
569
  const b = document.createElement("button");
 
572
  b.addEventListener("click", () => {
573
  document.querySelectorAll(".ex").forEach((x) => x.classList.toggle("active", x === b));
574
  $("input").value = ex.text;
575
+ labels = []; // per-example schema: reset color mapping
576
+ // old example's spans no longer match the new document: clear before scan
577
+ $("output").innerHTML = ""; renderLegend([]); renderRail(" ", []);
578
+ $("code-panel").textContent = ex.code || "";
579
+ renderResult(ex.result || "");
580
  inferSoon(40);
581
  });
582
  $("examples").append(b);
583
  });
584
 
585
  setEmpty("idle");
586
+ if (cfg.examples[0]) {
587
+ $("input").value = cfg.examples[0].text;
588
+ $("code-panel").textContent = cfg.examples[0].code || "";
589
+ renderResult(cfg.examples[0].result || "");
590
+ }
591
  READY = await waitForServer();
592
  if (READY) infer();
593
  else setEmpty("error");