m-newhauser commited on
Commit
53395c6
·
verified ·
1 Parent(s): 493b437

Upload folder using huggingface_hub

Browse files
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 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:
 
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; /* page background, always */
10
- --greige: #EDE6E3; /* card fills */
11
- --ash: #E9DEDB; /* hover, dividers */
12
- --orange: #FF7345; /* Fastino accent: spans, key metrics */
13
- --ink: #2E2729; /* headings, primary text */
14
- --ink-soft: rgba(46, 39, 41, 0.62); /* secondary text */
15
- --ink-faint: #8A7F7B; /* footnotes */
16
- --warm-gray: #C4BBB7; /* base/comparison data, borders */
17
- --card: #FBF8F6; /* lightest warm surface for input areas */
18
- --ok: #5F8F6E; /* muted warm green, used sparingly */
 
 
 
 
 
 
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, never color alone (always labeled) */
27
- --c0: #FF7345; --c1: #C98A2D; --c2: #5F8F6E; --c3: #8A6FB8;
28
- --c4: #4E7E96; --c5: #B85C74; --c6: #7D8A3C; --c7: #A0785A;
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: 1060px; margin: 0 auto; padding: 28px 22px 48px; }
43
 
44
- /* ---- hero ---- */
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 22px; padding: 12px 16px;
56
- background: var(--greige); border: 1px solid var(--ash); border-radius: 12px;
57
  }
58
  .model-glyph {
59
- width: 34px; height: 34px; border-radius: 9px; flex-shrink: 0;
60
- background: var(--orange); color: #fff;
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: 12.5px; color: var(--ink-faint); font-family: var(--font-mono); }
67
- .load-block { margin-left: auto; min-width: 220px; }
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: 5px; background: var(--ash); border-radius: 99px; overflow: hidden; }
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
- /* ---- workspace cards ---- */
75
- .workspace { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
 
 
 
 
 
 
76
  .workspace.single { grid-template-columns: 1fr; }
77
  .card {
78
- background: var(--greige); border: 1px solid var(--ash); border-radius: 12px;
79
- display: flex; flex-direction: column; min-height: 280px;
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: 220px;
90
  padding: 14px 16px; background: var(--card);
91
- font: inherit; color: var(--ink); border-radius: 0 0 12px 12px;
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); border-radius: 0 0 12px 12px;
97
- font-size: 15px; line-height: 1.9; white-space: pre-wrap; word-break: break-word;
 
98
  }
99
  .output-empty { color: var(--ink-faint); font-size: 13.5px; }
 
100
 
101
- /* ---- span highlighting (staggered reveal) ---- */
102
  .span {
103
- border-radius: 4px; padding: 1px 2px; margin: 0 1px;
104
- background: color-mix(in srgb, var(--span-color) 22%, transparent);
105
  border-bottom: 2px solid var(--span-color);
106
- cursor: default; position: relative;
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
- /* ---- legend / stats ---- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: 8px;
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: 10px;
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: 22px; font-size: 12px; color: var(--ink-faint); }
155
  .footnote a { color: var(--ink-soft); }
156
 
157
- @media (max-width: 860px) { .workspace { grid-template-columns: 1fr; } .load-block { display: none; } }
 
 
 
 
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 GLiNER 2.5</title>
 
 
 
 
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">G</span>
 
 
 
 
 
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</span>
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 below</p></div></div>
52
- <textarea id="input" spellcheck="false"></textarea>
53
  </section>
54
 
55
  <section class="card">
56
  <div class="panel-header">
57
- <div><h2>Spans &amp; attributes</h2><p>Attribute groups attached per span</p></div>
58
  <button type="button" class="code-toggle" id="code-toggle">&lt;/&gt; 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"></strong></span>
66
- <span>Spans<strong id="stat-n"></strong></span>
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
89
- let READY = false, timer = null, version = 0, labels = [];
 
 
 
 
 
 
 
 
 
 
90
 
91
- // sentiment groups get semantic badge colors; everything else uses the group palette
92
- const ATTR_TONE = { positive: "pos", present: "pos", executive: "pos",
93
- negative: "neg", absent: "neg", data_loss: "neg", security_risk: "neg",
94
- neutral: "neu", historical: "neu" };
 
 
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
- const tone = ATTR_TONE[e.label] || "";
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 = `var(--c${labels.indexOf(sp.label) % 8})`;
112
- const tip = `${sp.label} · ${sp.score.toFixed(2)}`;
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:var(--c${labels.indexOf(label) % 8})">
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, "")} GLiNER 2.5`;
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">&lt;/&gt; 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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">&ldquo;${esc(sp.text)}&rdquo;</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)} &middot; chars ${sp.start}&ndash;${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", "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;
 
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;