Upload folder using huggingface_hub
Browse files- app.py +1 -1
- model.py +1 -1
- static/brand-tile.png +0 -0
- static/brand.css +134 -61
- static/favicon.svg +7 -0
- static/index.html +105 -48
- static/og-card.png +0 -0
- static/strip.js +2 -2
app.py
CHANGED
|
@@ -33,7 +33,7 @@ def index():
|
|
| 33 |
|
| 34 |
@app.route("/<path:name>")
|
| 35 |
def static_file(name):
|
| 36 |
-
if name in ("brand.css", "strip.js", "favicon.svg"):
|
| 37 |
return send_from_directory(STATIC, name)
|
| 38 |
return ("not found", 404)
|
| 39 |
|
|
|
|
| 33 |
|
| 34 |
@app.route("/<path:name>")
|
| 35 |
def static_file(name):
|
| 36 |
+
if name in ("brand.css", "strip.js", "favicon.svg", "og-card.png", "brand-tile.png"):
|
| 37 |
return send_from_directory(STATIC, name)
|
| 38 |
return ("not found", 404)
|
| 39 |
|
model.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""Span attributes demo
|
| 2 |
role, assertion, impact), single- or multi-label, scoped with applies_to.
|
| 3 |
|
| 4 |
MOCK until GLiNER 2.5 ships. Response contract:
|
|
|
|
| 1 |
+
"""Span attributes demo: entities carry per-span attribute groups (sentiment,
|
| 2 |
role, assertion, impact), single- or multi-label, scoped with applies_to.
|
| 3 |
|
| 4 |
MOCK until GLiNER 2.5 ships. Response contract:
|
static/brand-tile.png
ADDED
|
static/brand.css
CHANGED
|
@@ -1,31 +1,40 @@
|
|
| 1 |
/* Fastino System A brand tokens + shared chrome for GLiNER 2.5 demos.
|
| 2 |
-
Warm, flat, measured: linen background, orange accent, no gradients/shadows.
|
|
|
|
| 3 |
|
| 4 |
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap");
|
| 5 |
/* 0xProto Nerd: not on Google Fonts, use the Nerd Fonts CDN, fall back gracefully */
|
| 6 |
@import url("https://www.nerdfonts.com/assets/css/webfont.css");
|
| 7 |
|
| 8 |
:root {
|
| 9 |
-
--linen: #F2ECE9;
|
| 10 |
-
--greige: #EDE6E3;
|
| 11 |
-
--ash: #
|
| 12 |
-
--orange: #FF7345;
|
| 13 |
-
--
|
| 14 |
-
--ink
|
| 15 |
-
--ink-
|
| 16 |
-
--
|
| 17 |
-
--
|
| 18 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
--font-head: "Geist", "Helvetica Neue", Arial, sans-serif;
|
| 21 |
--font-body: "Geist", "Helvetica Neue", Arial, sans-serif;
|
| 22 |
--font-mono: "0xProto Nerd Font", "0xProto Nerd", "JetBrains Mono", ui-monospace, Menlo, monospace;
|
| 23 |
|
| 24 |
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
/* span label palette: warm, distinguishable,
|
| 27 |
-
--c0: #
|
| 28 |
-
--c4: #
|
| 29 |
}
|
| 30 |
|
| 31 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
@@ -39,122 +48,186 @@ body {
|
|
| 39 |
-webkit-font-smoothing: antialiased;
|
| 40 |
}
|
| 41 |
|
| 42 |
-
.wrap { max-width:
|
| 43 |
|
| 44 |
-
/* ----
|
| 45 |
.site-intro h1 {
|
| 46 |
font-family: var(--font-head); font-weight: 700;
|
| 47 |
font-size: 34px; letter-spacing: -0.015em; line-height: 1.15;
|
| 48 |
}
|
| 49 |
-
.site-intro h1 em { font-style: normal; color: var(--orange); }
|
| 50 |
-
.site-intro .sub { margin-top: 6px; color: var(--ink-soft); font-size: 15px; }
|
| 51 |
|
| 52 |
/* ---- model strip ---- */
|
| 53 |
.model-strip {
|
| 54 |
display: flex; align-items: center; gap: 14px;
|
| 55 |
-
margin: 18px 0
|
| 56 |
-
background: var(--greige); border: 1px solid var(--ash); border-radius:
|
| 57 |
}
|
| 58 |
.model-glyph {
|
| 59 |
-
width:
|
| 60 |
-
background: var(--
|
| 61 |
display: grid; place-items: center;
|
| 62 |
-
font-family: var(--font-head); font-weight: 700; font-size: 16px;
|
| 63 |
}
|
|
|
|
| 64 |
.model-name-row a { color: var(--ink); font-weight: 600; text-decoration: none; border-bottom: 1px solid var(--warm-gray); }
|
| 65 |
-
.model-name-row a:hover { border-bottom-color: var(--orange); }
|
| 66 |
-
.model-variant { margin-left: 8px; font-size:
|
| 67 |
-
.load-block { margin-left: auto; min-width:
|
| 68 |
.load-meta { display: flex; justify-content: space-between; font-size: 12px; color: var(--ink-soft); margin-bottom: 5px; }
|
| 69 |
#load-percent { font-family: var(--font-mono); }
|
| 70 |
-
.load-track { height:
|
| 71 |
#load-progress { display: block; height: 100%; width: 0; background: var(--orange); transition: width 400ms var(--ease-out); }
|
| 72 |
.model-strip.is-ready #load-progress { background: var(--ok); }
|
|
|
|
| 73 |
|
| 74 |
-
/* -
|
| 75 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
.workspace.single { grid-template-columns: 1fr; }
|
| 77 |
.card {
|
| 78 |
-
background: var(--greige); border: 1px solid var(--ash); border-radius:
|
| 79 |
-
display: flex; flex-direction: column;
|
| 80 |
}
|
| 81 |
.panel-header {
|
| 82 |
-
display: flex; align-items: flex-start; justify-content: space-between;
|
| 83 |
padding: 13px 16px 10px; border-bottom: 1px solid var(--ash);
|
| 84 |
}
|
| 85 |
.panel-header h2 { font-size: 14px; font-weight: 600; }
|
| 86 |
.panel-header p { font-size: 12px; color: var(--ink-faint); margin-top: 1px; }
|
| 87 |
|
| 88 |
textarea, .editor {
|
| 89 |
-
flex: 1; width: 100%; border: 0; resize: vertical; min-height:
|
| 90 |
padding: 14px 16px; background: var(--card);
|
| 91 |
-
font: inherit; color: var(--ink); border-radius: 0 0
|
| 92 |
}
|
| 93 |
textarea:focus, .editor:focus { outline: 2px solid var(--ash); outline-offset: -2px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
|
|
|
| 95 |
.output-body {
|
| 96 |
-
flex: 1; padding: 14px 16px; background: var(--card);
|
| 97 |
-
font-size: 15px; line-height: 1.
|
|
|
|
| 98 |
}
|
| 99 |
.output-empty { color: var(--ink-faint); font-size: 13.5px; }
|
|
|
|
| 100 |
|
| 101 |
-
/* ---- span highlighting (staggered reveal) ---- */
|
| 102 |
.span {
|
| 103 |
-
border-radius:
|
| 104 |
-
background: color-mix(in srgb, var(--span-color)
|
| 105 |
border-bottom: 2px solid var(--span-color);
|
| 106 |
-
cursor:
|
| 107 |
opacity: 0; transform: translateY(3px);
|
| 108 |
animation: span-in 380ms var(--ease-out) forwards;
|
| 109 |
animation-delay: calc(var(--i) * 55ms);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
}
|
| 111 |
@keyframes span-in { to { opacity: 1; transform: translateY(0); } }
|
| 112 |
|
| 113 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
.controls { padding: 10px 16px 13px; border-top: 1px solid var(--ash); display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
| 115 |
.legend-chip {
|
| 116 |
display: inline-flex; align-items: center; gap: 6px;
|
| 117 |
font-size: 12px; font-weight: 500; color: var(--ink);
|
| 118 |
-
background: var(--linen); border: 1px solid var(--ash); border-radius: 99px; padding: 3px 10px;
|
| 119 |
}
|
| 120 |
.legend-swatch { width: 9px; height: 9px; border-radius: 3px; background: var(--span-color); }
|
| 121 |
.legend-count { font-family: var(--font-mono); color: var(--ink-faint); font-size: 11px; }
|
| 122 |
-
|
| 123 |
.stats { display: flex; gap: 18px; margin-left: auto; font-size: 12px; color: var(--ink-faint); }
|
| 124 |
.stats strong { font-family: var(--font-mono); font-weight: 500; color: var(--ink); margin-left: 4px; }
|
| 125 |
|
| 126 |
-
/* ---- example chips ---- */
|
| 127 |
-
.examples-panel { margin-top: 18px; }
|
| 128 |
-
.examples-label { font-size: 12px; color: var(--ink-faint); display: block; margin-bottom: 8px; }
|
| 129 |
-
.example-list { display: flex; flex-wrap: wrap; gap: 8px; }
|
| 130 |
-
.ex {
|
| 131 |
-
font: inherit; font-size: 13px; font-weight: 500; color: var(--ink);
|
| 132 |
-
background: var(--greige); border: 1px solid var(--ash); border-radius: 99px;
|
| 133 |
-
padding: 6px 14px; cursor: pointer;
|
| 134 |
-
transition: background 160ms var(--ease-out), border-color 160ms var(--ease-out);
|
| 135 |
-
}
|
| 136 |
-
.ex:hover { background: var(--ash); }
|
| 137 |
-
.ex.active { background: var(--ink); color: var(--linen); border-color: var(--ink); }
|
| 138 |
-
|
| 139 |
/* ---- view-as-code toggle ---- */
|
| 140 |
.code-toggle {
|
| 141 |
font: inherit; font-size: 12px; font-weight: 500; color: var(--ink-soft);
|
| 142 |
-
background: var(--linen); border: 1px solid var(--ash); border-radius:
|
| 143 |
-
padding: 4px 11px; cursor: pointer;
|
|
|
|
| 144 |
}
|
| 145 |
.code-toggle:hover { background: var(--ash); }
|
|
|
|
| 146 |
.code-panel {
|
| 147 |
display: none; margin: 0 16px 14px; padding: 12px 14px;
|
| 148 |
-
background: var(--ink); color: var(--linen); border-radius:
|
| 149 |
font-family: var(--font-mono); font-size: 12.5px; line-height: 1.6;
|
| 150 |
white-space: pre; overflow-x: auto;
|
| 151 |
}
|
| 152 |
.code-panel.open { display: block; }
|
| 153 |
|
| 154 |
-
.footnote { margin-top:
|
| 155 |
.footnote a { color: var(--ink-soft); }
|
| 156 |
|
| 157 |
-
@media (max-width: 860px) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
@media (prefers-reduced-motion: reduce) {
|
| 160 |
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
|
|
|
|
| 1 |
/* Fastino System A brand tokens + shared chrome for GLiNER 2.5 demos.
|
| 2 |
+
Warm, flat, measured: linen background, orange accent, no gradients/shadows.
|
| 3 |
+
Contrast-audited per WCAG AA; tokens below are the single source of truth. */
|
| 4 |
|
| 5 |
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap");
|
| 6 |
/* 0xProto Nerd: not on Google Fonts, use the Nerd Fonts CDN, fall back gracefully */
|
| 7 |
@import url("https://www.nerdfonts.com/assets/css/webfont.css");
|
| 8 |
|
| 9 |
:root {
|
| 10 |
+
--linen: #F2ECE9; /* page background, always */
|
| 11 |
+
--greige: #EDE6E3; /* card fills */
|
| 12 |
+
--ash: #E2D6D2; /* hover, dividers */
|
| 13 |
+
--orange: #FF7345; /* Fastino accent: spans, key metrics (fills only, never text) */
|
| 14 |
+
--orange-deep: #C2451B; /* orange when used as text on linen; AA 4.6:1 */
|
| 15 |
+
--ink: #2E2729; /* headings, primary text */
|
| 16 |
+
--ink-soft: rgba(46, 39, 41, 0.72); /* secondary text; AA 4.8:1 on linen */
|
| 17 |
+
--ink-faint: #7A6F6B; /* footnotes, metadata; AA 4.6:1 on linen */
|
| 18 |
+
--warm-gray: #B0A39E; /* borders, comparison data */
|
| 19 |
+
--card: #FBF8F6; /* lightest warm surface for input areas */
|
| 20 |
+
--ok: #3F6E50; /* AA 5.1:1 on linen when text */
|
| 21 |
+
--ok-bg: rgba(63, 110, 80, 0.13);
|
| 22 |
+
--neg: #A84545;
|
| 23 |
+
--neg-bg: rgba(168, 69, 69, 0.12);
|
| 24 |
+
--neu: #6E6460;
|
| 25 |
+
--neu-bg: rgba(110, 100, 96, 0.10);
|
| 26 |
|
| 27 |
--font-head: "Geist", "Helvetica Neue", Arial, sans-serif;
|
| 28 |
--font-body: "Geist", "Helvetica Neue", Arial, sans-serif;
|
| 29 |
--font-mono: "0xProto Nerd Font", "0xProto Nerd", "JetBrains Mono", ui-monospace, Menlo, monospace;
|
| 30 |
|
| 31 |
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
| 32 |
+
--radius: 12px;
|
| 33 |
+
--radius-s: 6px;
|
| 34 |
|
| 35 |
+
/* span label palette: warm, distinguishable, always paired with a text label */
|
| 36 |
+
--c0: #E85D2A; --c1: #A96B14; --c2: #3F6E50; --c3: #6E50A0;
|
| 37 |
+
--c4: #2F6880; --c5: #A63D5C; --c6: #5F6D1F; --c7: #7A5634;
|
| 38 |
}
|
| 39 |
|
| 40 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
| 48 |
-webkit-font-smoothing: antialiased;
|
| 49 |
}
|
| 50 |
|
| 51 |
+
.wrap { max-width: 1160px; margin: 0 auto; padding: 32px 22px 56px; }
|
| 52 |
|
| 53 |
+
/* ---- intro ---- */
|
| 54 |
.site-intro h1 {
|
| 55 |
font-family: var(--font-head); font-weight: 700;
|
| 56 |
font-size: 34px; letter-spacing: -0.015em; line-height: 1.15;
|
| 57 |
}
|
| 58 |
+
.site-intro h1 em { font-style: normal; color: var(--orange-deep); }
|
| 59 |
+
.site-intro .sub { margin-top: 6px; color: var(--ink-soft); font-size: 15px; max-width: 56ch; }
|
| 60 |
|
| 61 |
/* ---- model strip ---- */
|
| 62 |
.model-strip {
|
| 63 |
display: flex; align-items: center; gap: 14px;
|
| 64 |
+
margin: 18px 0 20px; padding: 11px 16px;
|
| 65 |
+
background: var(--greige); border: 1px solid var(--ash); border-radius: var(--radius);
|
| 66 |
}
|
| 67 |
.model-glyph {
|
| 68 |
+
width: 30px; height: 30px; border-radius: var(--radius-s); flex-shrink: 0;
|
| 69 |
+
background: var(--ink);
|
| 70 |
display: grid; place-items: center;
|
|
|
|
| 71 |
}
|
| 72 |
+
.model-glyph svg { width: 15px; height: 15px; color: var(--linen); }
|
| 73 |
.model-name-row a { color: var(--ink); font-weight: 600; text-decoration: none; border-bottom: 1px solid var(--warm-gray); }
|
| 74 |
+
.model-name-row a:hover { border-bottom-color: var(--orange-deep); color: var(--orange-deep); }
|
| 75 |
+
.model-variant { margin-left: 8px; font-size: 12px; color: var(--ink-faint); font-family: var(--font-mono); }
|
| 76 |
+
.load-block { margin-left: auto; min-width: 210px; }
|
| 77 |
.load-meta { display: flex; justify-content: space-between; font-size: 12px; color: var(--ink-soft); margin-bottom: 5px; }
|
| 78 |
#load-percent { font-family: var(--font-mono); }
|
| 79 |
+
.load-track { height: 4px; background: var(--ash); border-radius: 99px; overflow: hidden; }
|
| 80 |
#load-progress { display: block; height: 100%; width: 0; background: var(--orange); transition: width 400ms var(--ease-out); }
|
| 81 |
.model-strip.is-ready #load-progress { background: var(--ok); }
|
| 82 |
+
.model-strip.is-error #load-progress { background: var(--neg); }
|
| 83 |
|
| 84 |
+
/* ready-state stats cluster (progress bar retires once loaded) */
|
| 85 |
+
.model-stats { display: none; align-items: baseline; gap: 18px; font-size: 12px; color: var(--ink-faint); }
|
| 86 |
+
.model-strip.is-ready .model-stats { display: flex; }
|
| 87 |
+
.model-strip.is-ready .load-block { visibility: hidden; height: 0; min-width: 0; }
|
| 88 |
+
.model-stats b { font-family: var(--font-mono); font-weight: 500; color: var(--ink); }
|
| 89 |
+
|
| 90 |
+
/* ---- workspace ---- */
|
| 91 |
+
.workspace { display: grid; grid-template-columns: 5fr 7fr; gap: 16px; align-items: stretch; }
|
| 92 |
.workspace.single { grid-template-columns: 1fr; }
|
| 93 |
.card {
|
| 94 |
+
background: var(--greige); border: 1px solid var(--ash); border-radius: var(--radius);
|
| 95 |
+
display: flex; flex-direction: column;
|
| 96 |
}
|
| 97 |
.panel-header {
|
| 98 |
+
display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
|
| 99 |
padding: 13px 16px 10px; border-bottom: 1px solid var(--ash);
|
| 100 |
}
|
| 101 |
.panel-header h2 { font-size: 14px; font-weight: 600; }
|
| 102 |
.panel-header p { font-size: 12px; color: var(--ink-faint); margin-top: 1px; }
|
| 103 |
|
| 104 |
textarea, .editor {
|
| 105 |
+
flex: 1; width: 100%; border: 0; resize: vertical; min-height: 160px;
|
| 106 |
padding: 14px 16px; background: var(--card);
|
| 107 |
+
font: inherit; color: var(--ink); border-radius: 0 0 var(--radius) var(--radius);
|
| 108 |
}
|
| 109 |
textarea:focus, .editor:focus { outline: 2px solid var(--ash); outline-offset: -2px; }
|
| 110 |
+
textarea:focus-visible { outline: 2px solid var(--orange-deep); }
|
| 111 |
+
|
| 112 |
+
/* ---- example chips (relocated above workspace) ---- */
|
| 113 |
+
.examples-panel { margin: 0 0 14px; }
|
| 114 |
+
.examples-label {
|
| 115 |
+
font-size: 11px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
|
| 116 |
+
color: var(--ink-faint); display: block; margin-bottom: 8px;
|
| 117 |
+
}
|
| 118 |
+
.example-list { display: flex; flex-wrap: wrap; gap: 8px; }
|
| 119 |
+
.ex {
|
| 120 |
+
font: inherit; font-size: 13px; font-weight: 500; color: var(--ink);
|
| 121 |
+
background: var(--greige); border: 1px solid var(--ash); border-radius: 99px;
|
| 122 |
+
padding: 6px 14px; cursor: pointer;
|
| 123 |
+
transition: background 160ms var(--ease-out), border-color 160ms var(--ease-out), transform 80ms var(--ease-out);
|
| 124 |
+
}
|
| 125 |
+
.ex:hover { background: var(--ash); }
|
| 126 |
+
.ex:active { transform: scale(0.97); }
|
| 127 |
+
.ex:focus-visible { outline: 2px solid var(--orange-deep); outline-offset: 2px; }
|
| 128 |
+
.ex.active { background: var(--ink); color: var(--linen); border-color: var(--ink); }
|
| 129 |
|
| 130 |
+
/* ---- output body ---- */
|
| 131 |
.output-body {
|
| 132 |
+
flex: 1; padding: 14px 16px; background: var(--card);
|
| 133 |
+
font-size: 15px; line-height: 1.95; white-space: pre-wrap; word-break: break-word;
|
| 134 |
+
overflow-y: auto;
|
| 135 |
}
|
| 136 |
.output-empty { color: var(--ink-faint); font-size: 13.5px; }
|
| 137 |
+
.output-empty strong { display: block; color: var(--ink-soft); font-weight: 600; font-size: 15px; margin-bottom: 4px; }
|
| 138 |
|
| 139 |
+
/* ---- inline span highlighting (staggered reveal) ---- */
|
| 140 |
.span {
|
| 141 |
+
border-radius: var(--radius-s); padding: 1px 2px; margin: 0 1px;
|
| 142 |
+
background: color-mix(in srgb, var(--span-color) 20%, transparent);
|
| 143 |
border-bottom: 2px solid var(--span-color);
|
| 144 |
+
cursor: pointer; position: relative;
|
| 145 |
opacity: 0; transform: translateY(3px);
|
| 146 |
animation: span-in 380ms var(--ease-out) forwards;
|
| 147 |
animation-delay: calc(var(--i) * 55ms);
|
| 148 |
+
transition: background 140ms var(--ease-out);
|
| 149 |
+
}
|
| 150 |
+
.span:hover { background: color-mix(in srgb, var(--span-color) 30%, transparent); }
|
| 151 |
+
.span.selected {
|
| 152 |
+
background: color-mix(in srgb, var(--span-color) 40%, transparent);
|
| 153 |
+
box-shadow: 0 0 0 1px var(--span-color);
|
| 154 |
}
|
| 155 |
@keyframes span-in { to { opacity: 1; transform: translateY(0); } }
|
| 156 |
|
| 157 |
+
/* attribute badges attached inline after a span; tone colors resolved in JS
|
| 158 |
+
(semantic tone wins, else the span's label color at low alpha) */
|
| 159 |
+
.attr {
|
| 160 |
+
display: inline-block; vertical-align: super;
|
| 161 |
+
font-family: var(--font-mono); font-size: 10px; font-weight: 600;
|
| 162 |
+
border-radius: 3px; padding: 0 4px; margin-left: 2px; line-height: 1.7;
|
| 163 |
+
color: var(--badge-text); background: var(--badge-bg);
|
| 164 |
+
border: 1px solid var(--badge-border);
|
| 165 |
+
opacity: 0; animation: span-in 320ms var(--ease-out) forwards;
|
| 166 |
+
animation-delay: calc(var(--i) * 55ms + 160ms);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/* ---- span inspector (detail panel under the text) ---- */
|
| 170 |
+
.inspector {
|
| 171 |
+
border-top: 1px solid var(--ash);
|
| 172 |
+
padding: 12px 16px 14px;
|
| 173 |
+
background: var(--card);
|
| 174 |
+
border-radius: 0 0 var(--radius) var(--radius);
|
| 175 |
+
}
|
| 176 |
+
.inspector-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; }
|
| 177 |
+
.inspector-head .span-text { font-size: 16px; font-weight: 600; }
|
| 178 |
+
.inspector-head .span-label {
|
| 179 |
+
font-family: var(--font-mono); font-size: 12px; font-weight: 600;
|
| 180 |
+
color: var(--span-color, var(--ink)); background: color-mix(in srgb, var(--span-color, var(--warm-gray)) 14%, transparent);
|
| 181 |
+
border: 1px solid var(--span-color, var(--warm-gray));
|
| 182 |
+
border-radius: var(--radius-s); padding: 1px 7px;
|
| 183 |
+
}
|
| 184 |
+
.inspector-head .span-score { font-family: var(--font-mono); font-size: 12px; color: var(--ink-faint); margin-left: auto; }
|
| 185 |
+
.inspector-groups { display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); gap: 10px; }
|
| 186 |
+
.attr-group { border: 1px solid var(--ash); border-radius: var(--radius-s); padding: 9px 11px; background: var(--greige); }
|
| 187 |
+
.attr-group-name { font-family: var(--font-mono); font-size: 11px; color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 7px; }
|
| 188 |
+
.attr-row { display: flex; align-items: baseline; gap: 8px; font-size: 13px; padding: 2px 0; }
|
| 189 |
+
.attr-row .attr-label { font-weight: 500; }
|
| 190 |
+
.attr-row .attr-score { margin-left: auto; font-family: var(--font-mono); font-size: 11px; color: var(--ink-faint); }
|
| 191 |
+
.attr-meter { height: 3px; border-radius: 2px; background: var(--ash); margin-top: 3px; overflow: hidden; }
|
| 192 |
+
.attr-meter i { display: block; height: 100%; background: var(--span-color, var(--orange)); border-radius: 2px; }
|
| 193 |
+
|
| 194 |
+
/* ---- legend / stats footer ---- */
|
| 195 |
.controls { padding: 10px 16px 13px; border-top: 1px solid var(--ash); display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
| 196 |
.legend-chip {
|
| 197 |
display: inline-flex; align-items: center; gap: 6px;
|
| 198 |
font-size: 12px; font-weight: 500; color: var(--ink);
|
| 199 |
+
background: var(--linen); border: 1px solid var(--ash); border-radius: 99px; padding: 3px 10px; cursor: pointer;
|
| 200 |
}
|
| 201 |
.legend-swatch { width: 9px; height: 9px; border-radius: 3px; background: var(--span-color); }
|
| 202 |
.legend-count { font-family: var(--font-mono); color: var(--ink-faint); font-size: 11px; }
|
|
|
|
| 203 |
.stats { display: flex; gap: 18px; margin-left: auto; font-size: 12px; color: var(--ink-faint); }
|
| 204 |
.stats strong { font-family: var(--font-mono); font-weight: 500; color: var(--ink); margin-left: 4px; }
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
/* ---- view-as-code toggle ---- */
|
| 207 |
.code-toggle {
|
| 208 |
font: inherit; font-size: 12px; font-weight: 500; color: var(--ink-soft);
|
| 209 |
+
background: var(--linen); border: 1px solid var(--ash); border-radius: var(--radius-s);
|
| 210 |
+
padding: 4px 11px; cursor: pointer; display: inline-flex; align-items: center; gap: 5px;
|
| 211 |
+
transition: background 160ms var(--ease-out);
|
| 212 |
}
|
| 213 |
.code-toggle:hover { background: var(--ash); }
|
| 214 |
+
.code-toggle:focus-visible { outline: 2px solid var(--orange-deep); outline-offset: 2px; }
|
| 215 |
.code-panel {
|
| 216 |
display: none; margin: 0 16px 14px; padding: 12px 14px;
|
| 217 |
+
background: var(--ink); color: var(--linen); border-radius: var(--radius-s);
|
| 218 |
font-family: var(--font-mono); font-size: 12.5px; line-height: 1.6;
|
| 219 |
white-space: pre; overflow-x: auto;
|
| 220 |
}
|
| 221 |
.code-panel.open { display: block; }
|
| 222 |
|
| 223 |
+
.footnote { margin-top: 20px; font-size: 12px; color: var(--ink-faint); }
|
| 224 |
.footnote a { color: var(--ink-soft); }
|
| 225 |
|
| 226 |
+
@media (max-width: 860px) {
|
| 227 |
+
.workspace { grid-template-columns: 1fr; }
|
| 228 |
+
.load-block { display: none; }
|
| 229 |
+
.inspector-groups { grid-template-columns: 1fr; }
|
| 230 |
+
}
|
| 231 |
|
| 232 |
@media (prefers-reduced-motion: reduce) {
|
| 233 |
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
|
static/favicon.svg
ADDED
|
|
static/index.html
CHANGED
|
@@ -3,23 +3,12 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
-
<title>Span attributes
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
<link rel="stylesheet" href="brand.css">
|
| 8 |
-
<style>
|
| 9 |
-
/* demo-specific: attribute badges attached to spans */
|
| 10 |
-
.span-wrap { position: relative; }
|
| 11 |
-
.attr {
|
| 12 |
-
display: inline-block; vertical-align: super;
|
| 13 |
-
font-family: var(--font-mono); font-size: 10px; font-weight: 500;
|
| 14 |
-
color: #fff; background: var(--attr-color); border-radius: 4px;
|
| 15 |
-
padding: 0 4px; margin-left: 2px; line-height: 1.6;
|
| 16 |
-
opacity: 0; animation: span-in 320ms var(--ease-out) forwards;
|
| 17 |
-
animation-delay: calc(var(--i) * 55ms + 160ms);
|
| 18 |
-
}
|
| 19 |
-
.attr.pos { background: var(--ok); }
|
| 20 |
-
.attr.neg { background: #B85C5C; }
|
| 21 |
-
.attr.neu { background: var(--warm-gray); }
|
| 22 |
-
</style>
|
| 23 |
</head>
|
| 24 |
<body>
|
| 25 |
<div class="wrap">
|
|
@@ -29,16 +18,24 @@
|
|
| 29 |
</header>
|
| 30 |
|
| 31 |
<section class="model-strip is-loading" id="model-strip" aria-label="Model status">
|
| 32 |
-
<span class="model-glyph" aria-hidden="true">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
<div class="model-copy">
|
| 34 |
<div class="model-name-row">
|
| 35 |
<a id="model-link" href="#" target="_blank" rel="noopener"></a>
|
| 36 |
<span class="model-variant" id="model-variant"></span>
|
| 37 |
</div>
|
| 38 |
</div>
|
|
|
|
|
|
|
|
|
|
| 39 |
<div class="load-block">
|
| 40 |
<div class="load-meta">
|
| 41 |
-
<span id="status" role="status" aria-live="polite">Connecting
|
| 42 |
<span id="load-percent">0%</span>
|
| 43 |
</div>
|
| 44 |
<div class="load-track"><span id="load-progress"></span></div>
|
|
@@ -46,34 +43,35 @@
|
|
| 46 |
</section>
|
| 47 |
|
| 48 |
<main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
<div class="workspace">
|
| 50 |
<section class="card">
|
| 51 |
-
<div class="panel-header"><div><h2>Text</h2><p>Edit or pick an example
|
| 52 |
-
<textarea id="input" spellcheck="false"></textarea>
|
| 53 |
</section>
|
| 54 |
|
| 55 |
<section class="card">
|
| 56 |
<div class="panel-header">
|
| 57 |
-
<div><h2>Spans
|
| 58 |
<button type="button" class="code-toggle" id="code-toggle"></> schema</button>
|
| 59 |
</div>
|
| 60 |
<pre class="code-panel" id="code-panel"></pre>
|
| 61 |
<div id="output" class="output-body"><div class="output-empty">Extracted spans will appear here.</div></div>
|
|
|
|
| 62 |
<div class="controls">
|
| 63 |
<div class="legend" id="legend" style="display:contents"></div>
|
| 64 |
<div class="stats">
|
| 65 |
-
<span>Latency<strong id="stat-ms">
|
| 66 |
-
<span>Spans<strong id="stat-n">
|
| 67 |
</div>
|
| 68 |
</div>
|
| 69 |
</section>
|
| 70 |
</div>
|
| 71 |
|
| 72 |
-
<section class="examples-panel" aria-label="Examples">
|
| 73 |
-
<span class="examples-label">Examples</span>
|
| 74 |
-
<div class="example-list" id="examples"></div>
|
| 75 |
-
</section>
|
| 76 |
-
|
| 77 |
<p class="footnote">
|
| 78 |
Demo data. Attribute groups are scoped with <code>applies_to</code>: sentiment on products,
|
| 79 |
role on people, assertion on conditions. GLiNER 2.5 by
|
|
@@ -86,31 +84,44 @@
|
|
| 86 |
<script>
|
| 87 |
const $ = (id) => document.getElementById(id);
|
| 88 |
const esc = (v) => v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
| 89 |
-
let READY = false, timer = null, version = 0, labels = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
-
|
| 92 |
-
const
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
| 95 |
|
| 96 |
-
function attrBadges(attributes, i) {
|
| 97 |
let html = "";
|
| 98 |
for (const [group, entries] of Object.entries(attributes || {})) {
|
| 99 |
for (const e of entries) {
|
| 100 |
-
|
| 101 |
-
html += `<span class="attr ${tone}" style="--i:${i}" title="${esc(group)} · ${e.score.toFixed(2)}">${esc(e.label)}</span>`;
|
| 102 |
}
|
| 103 |
}
|
| 104 |
return html;
|
| 105 |
}
|
| 106 |
|
|
|
|
|
|
|
| 107 |
function render(text, spans) {
|
| 108 |
let html = "", prev = 0, i = 0;
|
| 109 |
for (const sp of spans) {
|
| 110 |
html += esc(text.slice(prev, sp.start));
|
| 111 |
-
const c =
|
| 112 |
-
const tip = `${sp.label}
|
| 113 |
-
html += `<span class="span-wrap"><span class="span" style="--span-color:${c};--i:${i}" title="${esc(tip)}">${esc(sp.text)}</span>${attrBadges(sp.attributes, i)}</span>`;
|
| 114 |
prev = sp.end; i++;
|
| 115 |
}
|
| 116 |
return html + esc(text.slice(prev));
|
|
@@ -120,31 +131,65 @@ function renderLegend(spans) {
|
|
| 120 |
const counts = {};
|
| 121 |
for (const sp of spans) counts[sp.label] = (counts[sp.label] || 0) + 1;
|
| 122 |
$("legend").innerHTML = Object.entries(counts).map(([label, n]) =>
|
| 123 |
-
`<span class="legend-chip" style="--span-color:
|
| 124 |
<span class="legend-swatch"></span>${esc(label)}<span class="legend-count">${n}</span>
|
| 125 |
</span>`).join("");
|
| 126 |
}
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
async function infer() {
|
| 129 |
if (!READY) return;
|
| 130 |
const v = ++version, text = $("input").value;
|
| 131 |
-
if (!text.trim()) {
|
| 132 |
-
$("output").innerHTML = '<div class="output-empty">Extracted spans will appear here.</div>';
|
| 133 |
-
$("legend").innerHTML = ""; $("stat-ms").textContent = "—"; $("stat-n").textContent = "—";
|
| 134 |
-
return;
|
| 135 |
-
}
|
| 136 |
let data;
|
| 137 |
try {
|
| 138 |
const r = await fetch("api/infer", { method: "POST",
|
| 139 |
headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) });
|
| 140 |
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
| 141 |
data = await r.json();
|
| 142 |
-
} catch (e) { return; }
|
| 143 |
if (v !== version) return;
|
| 144 |
const spans = data.spans || [];
|
|
|
|
| 145 |
labels = [...new Set([...labels, ...spans.map((s) => s.label)])];
|
| 146 |
-
$("output").innerHTML = render(text, spans);
|
|
|
|
| 147 |
renderLegend(spans);
|
|
|
|
| 148 |
$("stat-ms").textContent = `${data.ms} ms`;
|
| 149 |
$("stat-n").textContent = spans.length;
|
| 150 |
}
|
|
@@ -152,10 +197,20 @@ async function infer() {
|
|
| 152 |
function inferSoon(delay = 300) { clearTimeout(timer); timer = setTimeout(infer, delay); }
|
| 153 |
$("input").addEventListener("input", () => inferSoon());
|
| 154 |
$("code-toggle").addEventListener("click", () => $("code-panel").classList.toggle("open"));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
(async () => {
|
| 157 |
const cfg = await fetchConfig();
|
| 158 |
-
document.title = `${cfg.title.replace(/<[^>]+>/g, "")}
|
| 159 |
$("title").innerHTML = cfg.title;
|
| 160 |
$("subtitle").innerHTML = cfg.subtitle;
|
| 161 |
$("model-link").textContent = cfg.model_name;
|
|
@@ -175,9 +230,11 @@ $("code-toggle").addEventListener("click", () => $("code-panel").classList.toggl
|
|
| 175 |
$("examples").append(b);
|
| 176 |
});
|
| 177 |
|
|
|
|
| 178 |
if (cfg.examples[0]) $("input").value = cfg.examples[0].text;
|
| 179 |
READY = await waitForServer();
|
| 180 |
stripSet(READY ? "Model ready" : "Model unavailable", READY ? "ready" : "error", 100);
|
|
|
|
| 181 |
if (READY) infer();
|
| 182 |
})();
|
| 183 |
</script>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>Span attributes - GLiNER 2.5</title>
|
| 7 |
+
<link rel="icon" href="favicon.svg" type="image/svg+xml">
|
| 8 |
+
<meta property="og:title" content="GLiNER 2.5 - Span attributes">
|
| 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">
|
|
|
|
| 18 |
</header>
|
| 19 |
|
| 20 |
<section class="model-strip is-loading" id="model-strip" aria-label="Model status">
|
| 21 |
+
<span class="model-glyph" aria-hidden="true">
|
| 22 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
| 23 |
+
<circle cx="5.5" cy="12" r="2.2"/><circle cx="18.5" cy="5" r="2.2"/><circle cx="18.5" cy="19" r="2.2"/>
|
| 24 |
+
<path d="M7.3 10.8 16.6 6M7.3 13.2 16.6 18"/>
|
| 25 |
+
</svg>
|
| 26 |
+
</span>
|
| 27 |
<div class="model-copy">
|
| 28 |
<div class="model-name-row">
|
| 29 |
<a id="model-link" href="#" target="_blank" rel="noopener"></a>
|
| 30 |
<span class="model-variant" id="model-variant"></span>
|
| 31 |
</div>
|
| 32 |
</div>
|
| 33 |
+
<div class="model-stats" aria-hidden="true">
|
| 34 |
+
<span id="strip-state">Ready</span>
|
| 35 |
+
</div>
|
| 36 |
<div class="load-block">
|
| 37 |
<div class="load-meta">
|
| 38 |
+
<span id="status" role="status" aria-live="polite">Connecting...</span>
|
| 39 |
<span id="load-percent">0%</span>
|
| 40 |
</div>
|
| 41 |
<div class="load-track"><span id="load-progress"></span></div>
|
|
|
|
| 43 |
</section>
|
| 44 |
|
| 45 |
<main>
|
| 46 |
+
<section class="examples-panel" aria-label="Examples">
|
| 47 |
+
<span class="examples-label">Examples</span>
|
| 48 |
+
<div class="example-list" id="examples"></div>
|
| 49 |
+
</section>
|
| 50 |
+
|
| 51 |
<div class="workspace">
|
| 52 |
<section class="card">
|
| 53 |
+
<div class="panel-header"><div><h2>Text</h2><p>Edit below, or pick an example.</p></div></div>
|
| 54 |
+
<textarea id="input" spellcheck="false" aria-label="Input text"></textarea>
|
| 55 |
</section>
|
| 56 |
|
| 57 |
<section class="card">
|
| 58 |
<div class="panel-header">
|
| 59 |
+
<div><h2>Spans and attributes</h2><p>Click a span to inspect its attribute groups.</p></div>
|
| 60 |
<button type="button" class="code-toggle" id="code-toggle"></> schema</button>
|
| 61 |
</div>
|
| 62 |
<pre class="code-panel" id="code-panel"></pre>
|
| 63 |
<div id="output" class="output-body"><div class="output-empty">Extracted spans will appear here.</div></div>
|
| 64 |
+
<div class="inspector" id="inspector" hidden></div>
|
| 65 |
<div class="controls">
|
| 66 |
<div class="legend" id="legend" style="display:contents"></div>
|
| 67 |
<div class="stats">
|
| 68 |
+
<span>Latency<strong id="stat-ms">-</strong></span>
|
| 69 |
+
<span>Spans<strong id="stat-n">-</strong></span>
|
| 70 |
</div>
|
| 71 |
</div>
|
| 72 |
</section>
|
| 73 |
</div>
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
<p class="footnote">
|
| 76 |
Demo data. Attribute groups are scoped with <code>applies_to</code>: sentiment on products,
|
| 77 |
role on people, assertion on conditions. GLiNER 2.5 by
|
|
|
|
| 84 |
<script>
|
| 85 |
const $ = (id) => document.getElementById(id);
|
| 86 |
const esc = (v) => v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
| 87 |
+
let READY = false, timer = null, version = 0, labels = [], lastSpans = [];
|
| 88 |
+
|
| 89 |
+
/* Badge tone. Semantic labels for sentiment/assertion/impact; everything else
|
| 90 |
+
falls back to the span's own label color at low alpha. All combos pass AA. */
|
| 91 |
+
const TONE = {
|
| 92 |
+
positive: ["#3F6E50", "rgba(63,110,80,0.13)"], present: ["#3F6E50", "rgba(63,110,80,0.13)"],
|
| 93 |
+
executive: ["#3F6E50", "rgba(63,110,80,0.13)"],
|
| 94 |
+
negative: ["#A84545", "rgba(168,69,69,0.12)"], absent: ["#A84545", "rgba(168,69,69,0.12)"],
|
| 95 |
+
data_loss: ["#A84545", "rgba(168,69,69,0.12)"], security_risk: ["#A84545", "rgba(168,69,69,0.12)"],
|
| 96 |
+
neutral: ["#6E6460", "rgba(110,100,96,0.10)"], historical: ["#6E6460", "rgba(110,100,96,0.10)"],
|
| 97 |
+
};
|
| 98 |
|
| 99 |
+
function badgeStyle(label, spanColor) {
|
| 100 |
+
const t = TONE[label];
|
| 101 |
+
const base = t ? t[0] : spanColor;
|
| 102 |
+
const bg = t ? t[1] : `color-mix(in srgb, ${spanColor} 12%, transparent)`;
|
| 103 |
+
return `--badge-text:${base};--badge-bg:${bg};--badge-border:${base}`;
|
| 104 |
+
}
|
| 105 |
|
| 106 |
+
function attrBadges(attributes, i, spanColor) {
|
| 107 |
let html = "";
|
| 108 |
for (const [group, entries] of Object.entries(attributes || {})) {
|
| 109 |
for (const e of entries) {
|
| 110 |
+
html += `<span class="attr" style="--i:${i};${badgeStyle(e.label, spanColor)}" title="${esc(group)} · score ${e.score.toFixed(2)}">${esc(e.label)}</span>`;
|
|
|
|
| 111 |
}
|
| 112 |
}
|
| 113 |
return html;
|
| 114 |
}
|
| 115 |
|
| 116 |
+
function spanColor(label) { return `var(--c${labels.indexOf(label) % 8})`; }
|
| 117 |
+
|
| 118 |
function render(text, spans) {
|
| 119 |
let html = "", prev = 0, i = 0;
|
| 120 |
for (const sp of spans) {
|
| 121 |
html += esc(text.slice(prev, sp.start));
|
| 122 |
+
const c = spanColor(sp.label);
|
| 123 |
+
const tip = `${sp.label} - score ${sp.score.toFixed(2)}`;
|
| 124 |
+
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>`;
|
| 125 |
prev = sp.end; i++;
|
| 126 |
}
|
| 127 |
return html + esc(text.slice(prev));
|
|
|
|
| 131 |
const counts = {};
|
| 132 |
for (const sp of spans) counts[sp.label] = (counts[sp.label] || 0) + 1;
|
| 133 |
$("legend").innerHTML = Object.entries(counts).map(([label, n]) =>
|
| 134 |
+
`<span class="legend-chip" style="--span-color:${spanColor(label)}" title="${n} ${esc(label)} span${n > 1 ? "s" : ""}">
|
| 135 |
<span class="legend-swatch"></span>${esc(label)}<span class="legend-count">${n}</span>
|
| 136 |
</span>`).join("");
|
| 137 |
}
|
| 138 |
|
| 139 |
+
function renderInspector(idx, text) {
|
| 140 |
+
const box = $("inspector");
|
| 141 |
+
if (idx == null || !lastSpans[idx]) { box.hidden = true; box.innerHTML = ""; return; }
|
| 142 |
+
const sp = lastSpans[idx];
|
| 143 |
+
const c = spanColor(sp.label);
|
| 144 |
+
const groups = Object.entries(sp.attributes || {});
|
| 145 |
+
box.innerHTML = `
|
| 146 |
+
<div class="inspector-head">
|
| 147 |
+
<span class="span-text">“${esc(sp.text)}”</span>
|
| 148 |
+
<span class="span-label" style="--span-color:${c}">${esc(sp.label)}</span>
|
| 149 |
+
<span class="span-score">span score ${sp.score.toFixed(2)} · chars ${sp.start}–${sp.end}</span>
|
| 150 |
+
</div>
|
| 151 |
+
${groups.length ? `<div class="inspector-groups">${groups.map(([group, entries]) => `
|
| 152 |
+
<div class="attr-group">
|
| 153 |
+
<div class="attr-group-name">${esc(group)}</div>
|
| 154 |
+
${entries.map((e) => `
|
| 155 |
+
<div class="attr-row">
|
| 156 |
+
<span class="attr-label">${esc(e.label)}</span>
|
| 157 |
+
<span class="attr-score">${e.score.toFixed(2)}</span>
|
| 158 |
+
</div>
|
| 159 |
+
<div class="attr-meter"><i style="width:${(e.score * 100).toFixed(0)}%;--span-color:${c}"></i></div>`).join("")}
|
| 160 |
+
</div>`).join("")}</div>`
|
| 161 |
+
: `<div class="output-empty">No attributes on this span.</div>`}`;
|
| 162 |
+
box.hidden = false;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
function setEmpty(state) {
|
| 166 |
+
const html = {
|
| 167 |
+
idle: '<div class="output-empty"><strong>Nothing extracted yet.</strong>Type text on the left, or click an example above.</div>',
|
| 168 |
+
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>',
|
| 169 |
+
error: '<div class="output-empty"><strong>Inference failed.</strong>The server did not respond. Check the model is still running, then try again.</div>',
|
| 170 |
+
}[state];
|
| 171 |
+
$("output").innerHTML = html;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
async function infer() {
|
| 175 |
if (!READY) return;
|
| 176 |
const v = ++version, text = $("input").value;
|
| 177 |
+
if (!text.trim()) { setEmpty("idle"); renderLegend(lastSpans = []); renderInspector(null); $("stat-ms").textContent = "-"; $("stat-n").textContent = "-"; return; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
let data;
|
| 179 |
try {
|
| 180 |
const r = await fetch("api/infer", { method: "POST",
|
| 181 |
headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) });
|
| 182 |
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
| 183 |
data = await r.json();
|
| 184 |
+
} catch (e) { if (v === version) setEmpty("error"); return; }
|
| 185 |
if (v !== version) return;
|
| 186 |
const spans = data.spans || [];
|
| 187 |
+
lastSpans = spans;
|
| 188 |
labels = [...new Set([...labels, ...spans.map((s) => s.label)])];
|
| 189 |
+
$("output").innerHTML = spans.length ? render(text, spans) : "";
|
| 190 |
+
if (!spans.length) setEmpty("none");
|
| 191 |
renderLegend(spans);
|
| 192 |
+
renderInspector(null);
|
| 193 |
$("stat-ms").textContent = `${data.ms} ms`;
|
| 194 |
$("stat-n").textContent = spans.length;
|
| 195 |
}
|
|
|
|
| 197 |
function inferSoon(delay = 300) { clearTimeout(timer); timer = setTimeout(infer, delay); }
|
| 198 |
$("input").addEventListener("input", () => inferSoon());
|
| 199 |
$("code-toggle").addEventListener("click", () => $("code-panel").classList.toggle("open"));
|
| 200 |
+
$("output").addEventListener("click", (e) => {
|
| 201 |
+
const el = e.target.closest(".span");
|
| 202 |
+
if (!el) return;
|
| 203 |
+
const idx = +el.dataset.idx;
|
| 204 |
+
const wasSelected = el.classList.contains("selected");
|
| 205 |
+
document.querySelectorAll(".span.selected").forEach((x) => x.classList.remove("selected"));
|
| 206 |
+
if (wasSelected) { renderInspector(null); return; }
|
| 207 |
+
el.classList.add("selected");
|
| 208 |
+
renderInspector(idx);
|
| 209 |
+
});
|
| 210 |
|
| 211 |
(async () => {
|
| 212 |
const cfg = await fetchConfig();
|
| 213 |
+
document.title = `${cfg.title.replace(/<[^>]+>/g, "")} - GLiNER 2.5`;
|
| 214 |
$("title").innerHTML = cfg.title;
|
| 215 |
$("subtitle").innerHTML = cfg.subtitle;
|
| 216 |
$("model-link").textContent = cfg.model_name;
|
|
|
|
| 230 |
$("examples").append(b);
|
| 231 |
});
|
| 232 |
|
| 233 |
+
setEmpty("idle");
|
| 234 |
if (cfg.examples[0]) $("input").value = cfg.examples[0].text;
|
| 235 |
READY = await waitForServer();
|
| 236 |
stripSet(READY ? "Model ready" : "Model unavailable", READY ? "ready" : "error", 100);
|
| 237 |
+
$("strip-state").textContent = READY ? "Ready" : "Unavailable";
|
| 238 |
if (READY) infer();
|
| 239 |
})();
|
| 240 |
</script>
|
static/og-card.png
ADDED
|
static/strip.js
CHANGED
|
@@ -31,13 +31,13 @@ function stripSet(message, tone, pct) {
|
|
| 31 |
}
|
| 32 |
|
| 33 |
async function waitForServer() {
|
| 34 |
-
stripSet("Connecting to server
|
| 35 |
for (let attempt = 0; attempt < 300; attempt++) {
|
| 36 |
try {
|
| 37 |
const r = await fetch("api/ready", { cache: "no-store" });
|
| 38 |
if (r.ok && (await r.json()).ready) return true;
|
| 39 |
} catch (e) { /* still starting */ }
|
| 40 |
-
stripSet("Warming up the model
|
| 41 |
await new Promise((r) => setTimeout(r, 1000));
|
| 42 |
}
|
| 43 |
return false;
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
async function waitForServer() {
|
| 34 |
+
stripSet("Connecting to server...", "loading", 12);
|
| 35 |
for (let attempt = 0; attempt < 300; attempt++) {
|
| 36 |
try {
|
| 37 |
const r = await fetch("api/ready", { cache: "no-store" });
|
| 38 |
if (r.ok && (await r.json()).ready) return true;
|
| 39 |
} catch (e) { /* still starting */ }
|
| 40 |
+
stripSet("Warming up the model...", "preparing", Math.min(92, 12 + attempt * 4));
|
| 41 |
await new Promise((r) => setTimeout(r, 1000));
|
| 42 |
}
|
| 43 |
return false;
|