quickgrid commited on
Commit
4ecad3c
·
verified ·
1 Parent(s): cedc3d6

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +260 -1238
index.html CHANGED
@@ -1,1264 +1,286 @@
1
  <!DOCTYPE html>
2
- <html lang="en" data-theme="dark">
3
  <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>TokenLens LLM Tokenizer Playground</title>
7
- <meta name="description" content="Visualize how large language models tokenize text. Powered by Transformers.js, runs entirely in your browser." />
8
- <link rel="preconnect" href="https://fonts.googleapis.com" />
9
- <link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,300;12..96,400;12..96,500;12..96,600;12..96,700;12..96,800&family=JetBrains+Mono:wght@300;400;500;700&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet" />
10
- <style>
11
- /* ─── Design Tokens ─────────────────────────────────── */
12
- :root {
13
- --bg: #060b14;
14
- --bg2: #0b1220;
15
- --bg3: #101828;
16
- --bg4: #162035;
17
- --border: #1a2d4a;
18
- --border2: #243d60;
19
- --glow: #1f3d6e;
20
- --text: #dce8f8;
21
- --text2: #7899c0;
22
- --text3: #3d5a80;
23
- --accent: #4d9ef5;
24
- --accent2: #8b6af5;
25
- --green: #34d89a;
26
- --amber: #f5a623;
27
- --red: #f55577;
28
- }
29
- [data-theme="light"] {
30
- --bg: #eef1f8;
31
- --bg2: #e2e7f2;
32
- --bg3: #d3dae8;
33
- --bg4: #c2cce0;
34
- --border: #b8c4d8;
35
- --border2: #a0aec8;
36
- --glow: #c0d0e8;
37
- --text: #1a2236;
38
- --text2: #5a6888;
39
- --text3: #8898b4;
40
- --accent: #2878e0;
41
- --accent2: #6838d8;
42
- --green: #18a060;
43
- --amber: #c88010;
44
- --red: #d83858;
45
- }
46
- /* ─── Reset ─────────────────────────────────────────── */
47
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
48
- html { height: 100%; overflow: hidden; }
49
- body {
50
- background: var(--bg);
51
- color: var(--text);
52
- font-family: 'DM Sans', sans-serif;
53
- height: 100%;
54
- overflow: hidden;
55
- }
56
- /* ─── Background FX ─────────────────────────────────── */
57
- .bg-gradient {
58
- position: fixed; inset: 0; pointer-events: none; z-index: 0;
59
- background:
60
- radial-gradient(ellipse 80% 50% at 20% 10%, rgba(77,158,245,.06) 0%, transparent 70%),
61
- radial-gradient(ellipse 60% 40% at 80% 90%, rgba(139,106,245,.05) 0%, transparent 60%),
62
- radial-gradient(ellipse 40% 30% at 60% 50%, rgba(52,216,154,.03) 0%, transparent 60%);
63
- }
64
- [data-theme="light"] .bg-gradient {
65
- background:
66
- radial-gradient(ellipse 80% 50% at 20% 10%, rgba(40,120,224,.05) 0%, transparent 70%),
67
- radial-gradient(ellipse 60% 40% at 80% 90%, rgba(104,56,216,.04) 0%, transparent 60%),
68
- radial-gradient(ellipse 40% 30% at 60% 50%, rgba(24,160,96,.02) 0%, transparent 60%);
69
- }
70
- .dot-grid {
71
- position: fixed; inset: 0; pointer-events: none; z-index: 0;
72
- background-image: radial-gradient(circle, rgba(77,158,245,.12) 1px, transparent 1px);
73
- background-size: 36px 36px;
74
- mask-image: radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 80%);
75
- }
76
- [data-theme="light"] .dot-grid {
77
- background-image: radial-gradient(circle, rgba(40,120,224,.06) 1px, transparent 1px);
78
- }
79
- /* ─── Layout ─────────────────────────────────────────── */
80
- #app {
81
- position: relative; z-index: 1;
82
- display: flex; flex-direction: column;
83
- height: 100vh; overflow: hidden;
84
- }
85
- /* ─── Header ─────────────────────────────────────────── */
86
- header {
87
- display: flex; align-items: center;
88
- padding: 0 20px; height: 56px;
89
- border-bottom: 1px solid var(--border);
90
- background: rgba(6,11,20,.85);
91
- backdrop-filter: blur(20px);
92
- flex-shrink: 0; z-index: 100; gap: 12px;
93
- }
94
- [data-theme="light"] header { background: rgba(238,241,248,.92); }
95
- .logo {
96
- display: flex; align-items: center; gap: 8px;
97
- text-decoration: none; color: var(--text); flex-shrink: 0;
98
- }
99
- .logo-hex {
100
- width: 30px; height: 30px;
101
- background: linear-gradient(135deg, var(--accent), var(--accent2));
102
- clip-path: polygon(50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%);
103
- display: flex; align-items: center; justify-content: center;
104
- font-size: 12px; font-family: 'JetBrains Mono', monospace; font-weight: 700; color: white;
105
- }
106
- .logo-name {
107
- font-family: 'Bricolage Grotesque', sans-serif;
108
- font-size: 17px; font-weight: 700; letter-spacing: -0.5px;
109
- background: linear-gradient(135deg, #dce8f8 40%, var(--accent));
110
- -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
111
- }
112
- [data-theme="light"] .logo-name {
113
- background: linear-gradient(135deg, #1a2236 40%, var(--accent));
114
- -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
115
- }
116
- .logo-tag {
117
- font-size: 9px; font-family: 'JetBrains Mono', monospace; color: var(--text3);
118
- background: var(--bg3); border: 1px solid var(--border);
119
- padding: 1px 5px; border-radius: 4px; letter-spacing: .5px;
120
- }
121
- .header-divider {
122
- width: 1px; height: 28px; background: var(--border); flex-shrink: 0;
123
- }
124
- /* ─── Search Bar Groups ─────────────────────────────── */
125
- .header-controls {
126
- display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0;
127
- }
128
- .searchbar-group {
129
- position: relative; display: flex; align-items: center; gap: 3px; flex: 1; min-width: 0;
130
- }
131
- .searchbar-label {
132
- font-family: 'JetBrains Mono', monospace; font-size: 11px; font-weight: 700;
133
- width: 22px; height: 22px; border-radius: 5px; display: flex; align-items: center;
134
- justify-content: center; flex-shrink: 0; border: 1px solid var(--border);
135
- }
136
- .searchbar-label.label-a { background: rgba(77,158,245,.15); color: var(--accent); border-color: rgba(77,158,245,.3); }
137
- .searchbar-label.label-b { background: rgba(139,106,245,.15); color: var(--accent2); border-color: rgba(139,106,245,.3); }
138
- .searchbar-input {
139
- flex: 1; min-width: 0; background: var(--bg2); border: 1px solid var(--border);
140
- border-radius: 6px; color: var(--text); font-family: 'JetBrains Mono', monospace;
141
- font-size: 11px; padding: 5px 8px; outline: none; transition: border-color .2s;
142
- }
143
- .searchbar-input:focus { border-color: var(--accent); }
144
- .searchbar-input::placeholder { color: var(--text3); }
145
- .searchbar-dropdown-btn {
146
- width: 24px; height: 24px; border-radius: 5px; border: 1px solid var(--border);
147
- background: var(--bg2); color: var(--text2); cursor: pointer; font-size: 10px;
148
- display: flex; align-items: center; justify-content: center; flex-shrink: 0;
149
- transition: all .15s;
150
- }
151
- .searchbar-dropdown-btn:hover { border-color: var(--border2); color: var(--text); }
152
- .searchbar-load-btn {
153
- padding: 4px 10px; border-radius: 5px; border: 1px solid var(--border2);
154
- background: linear-gradient(135deg, rgba(77,158,245,.12), rgba(139,106,245,.12));
155
- color: var(--accent); font-family: 'DM Sans', sans-serif; font-size: 11px;
156
- font-weight: 500; cursor: pointer; transition: all .15s; white-space: nowrap; flex-shrink: 0;
157
- }
158
- .searchbar-load-btn:hover {
159
- background: linear-gradient(135deg, rgba(77,158,245,.22), rgba(139,106,245,.22));
160
- border-color: var(--accent);
161
- }
162
- /* ─── Dropdown Menu ─────────────────────────────────── */
163
- .dropdown-menu {
164
- position: absolute; top: calc(100% + 6px); left: 22px; right: 0;
165
- min-width: 280px; max-height: 320px; overflow-y: auto;
166
- background: var(--bg3); border: 1px solid var(--border2); border-radius: 8px;
167
- z-index: 200; display: none; padding: 4px;
168
- box-shadow: 0 8px 32px rgba(0,0,0,.4);
169
- }
170
- [data-theme="light"] .dropdown-menu { box-shadow: 0 8px 32px rgba(0,0,0,.12); }
171
- .dropdown-menu.open { display: block; }
172
- .dropdown-item {
173
- padding: 7px 10px; cursor: pointer; border-radius: 5px;
174
- display: flex; align-items: center; gap: 8px; font-size: 12px;
175
- transition: background .12s;
176
- }
177
- .dropdown-item:hover { background: var(--bg4); }
178
- .dropdown-item-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
179
- .dropdown-item-name { font-weight: 600; color: var(--text); font-family: 'Bricolage Grotesque', sans-serif; }
180
- .dropdown-item-detail { font-size: 10px; color: var(--text3); font-family: 'JetBrains Mono', monospace; margin-left: auto; white-space: nowrap; }
181
- /* ─── Icon Toggle Buttons ────────────────────────────── */
182
- .icon-toggle-btn {
183
- width: 32px; height: 32px; border-radius: 7px; border: 1px solid var(--border);
184
- background: var(--bg2); color: var(--text2); cursor: pointer; flex-shrink: 0;
185
- display: flex; align-items: center; justify-content: center; transition: all .15s;
186
- }
187
- .icon-toggle-btn:hover { border-color: var(--border2); color: var(--text); }
188
- .icon-toggle-btn.active { border-color: var(--accent); color: var(--accent); background: rgba(77,158,245,.1); }
189
- .icon-toggle-btn svg { width: 16px; height: 16px; }
190
- /* ─── Mobile Tab Bar ─────────────────────────────────── */
191
- .mobile-tab-bar {
192
- display: none;
193
- border-bottom: 1px solid var(--border);
194
- flex-shrink: 0;
195
- background: var(--bg2);
196
- padding: 0 4px;
197
- }
198
- .mobile-tab {
199
- flex: 1;
200
- padding: 10px 4px;
201
- border: none;
202
- background: transparent;
203
- color: var(--text2);
204
- font-family: 'DM Sans', sans-serif;
205
- font-size: 12px;
206
- font-weight: 500;
207
- cursor: pointer;
208
- border-bottom: 2px solid transparent;
209
- transition: all .15s;
210
- text-align: center;
211
- }
212
- .mobile-tab.active {
213
- color: var(--accent);
214
- border-bottom-color: var(--accent);
215
- }
216
- .mobile-tab.tab-disabled {
217
- opacity: 0.3;
218
- pointer-events: none;
219
- }
220
- /* ─── Main Split ─────────────────────────────────────── */
221
- main {
222
- flex: 1; min-height: 0; overflow: hidden;
223
- display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0;
224
- }
225
- main.single-panel { grid-template-columns: 1fr 2fr; }
226
- /* ─── Left Panel (Input) ─────────────────────────────── */
227
- .input-panel {
228
- border-right: 1px solid var(--border);
229
- display: flex; flex-direction: column; overflow: hidden;
230
- }
231
- .panel-header {
232
- padding: 12px 16px 10px; border-bottom: 1px solid var(--border);
233
- display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
234
- }
235
- .panel-title {
236
- font-family: 'Bricolage Grotesque', sans-serif; font-size: 13px; font-weight: 600;
237
- color: var(--text2); letter-spacing: .3px; display: flex; align-items: center; gap: 6px;
238
- }
239
- .panel-title-icon {
240
- width: 18px; height: 18px; background: var(--bg4); border: 1px solid var(--border);
241
- border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 10px;
242
- }
243
- .sample-btns { display: flex; gap: 4px; flex-wrap: wrap; }
244
- .sample-btn {
245
- font-size: 10px; padding: 3px 8px; border-radius: 5px;
246
- border: 1px solid var(--border); background: var(--bg2); color: var(--text2);
247
- cursor: pointer; font-family: 'DM Sans', sans-serif; transition: all .15s;
248
- }
249
- .sample-btn:hover { border-color: var(--border2); color: var(--text); }
250
- #input-area {
251
- flex: 1; width: 100%; background: transparent; border: none; outline: none;
252
- resize: none; color: var(--text); font-family: 'DM Sans', sans-serif;
253
- font-size: 14px; line-height: 1.7; padding: 14px 16px; min-height: 0; overflow-y: auto;
254
- }
255
- #input-area::placeholder { color: var(--text3); }
256
- .char-counter {
257
- padding: 6px 16px; border-top: 1px solid var(--border); flex-shrink: 0;
258
- font-size: 10px; font-family: 'JetBrains Mono', monospace; color: var(--text3); text-align: right;
259
- }
260
- /* ─── Output Panel ───────────────────────────────────── */
261
- .output-panel {
262
- display: flex; flex-direction: column; overflow: hidden; min-height: 0;
263
- }
264
- .output-panel + .output-panel { border-left: 1px solid var(--border); }
265
- .output-panel-header {
266
- padding: 10px 14px; border-bottom: 1px solid var(--border);
267
- display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; gap: 8px;
268
- }
269
- .model-indicator {
270
- display: flex; align-items: center; gap: 5px; font-size: 11px;
271
- font-family: 'JetBrains Mono', monospace; color: var(--text2); min-width: 0; overflow: hidden;
272
- }
273
- .model-indicator-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
274
- .model-indicator-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
275
- /* Stats row */
276
- .stats-row {
277
- display: grid; grid-template-columns: 1fr 1fr;
278
- border-bottom: 1px solid var(--border); flex-shrink: 0;
279
- }
280
- .stat-card {
281
- padding: 10px 14px; border-right: 1px solid var(--border);
282
- border-bottom: 1px solid var(--border); position: relative; overflow: hidden;
283
- }
284
- .stat-card:nth-child(2n) { border-right: none; }
285
- .stat-card:nth-child(3), .stat-card:nth-child(4) { border-bottom: none; }
286
- .stat-card::after {
287
- content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 2px;
288
- background: linear-gradient(90deg, transparent, var(--accent), transparent);
289
- opacity: 0; transition: opacity .3s;
290
- }
291
- .stat-card.highlight::after { opacity: 1; }
292
- .stat-label {
293
- font-size: 9px; font-family: 'JetBrains Mono', monospace; color: var(--text3);
294
- text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px;
295
- }
296
- .stat-value {
297
- font-family: 'Bricolage Grotesque', sans-serif; font-size: 20px; font-weight: 700;
298
- color: var(--text); line-height: 1; transition: all .3s;
299
- }
300
- .stat-card:nth-child(1) .stat-value { color: var(--accent); }
301
- .stat-card:nth-child(2) .stat-value { color: var(--green); }
302
- .stat-card:nth-child(3) .stat-value { color: var(--amber); }
303
- .stat-card:nth-child(4) .stat-value { color: var(--accent2); }
304
- .stat-sub {
305
- font-size: 9px; color: var(--text3); font-family: 'JetBrains Mono', monospace; margin-top: 2px;
306
- }
307
- /* View toggle */
308
- .view-toggle {
309
- display: flex; padding: 8px 14px; border-bottom: 1px solid var(--border);
310
- gap: 4px; align-items: center; justify-content: space-between; flex-shrink: 0;
311
- }
312
- .toggle-group {
313
- display: flex; gap: 2px; background: var(--bg2); border: 1px solid var(--border);
314
- border-radius: 6px; padding: 2px;
315
- }
316
- .toggle-btn {
317
- padding: 3px 10px; border-radius: 4px; border: none; background: transparent;
318
- color: var(--text2); font-family: 'DM Sans', sans-serif; font-size: 11px;
319
- font-weight: 500; cursor: pointer; transition: all .15s;
320
- }
321
- .toggle-btn.active { background: var(--bg4); color: var(--text); box-shadow: 0 1px 4px rgba(0,0,0,.3); }
322
- /* Token Display */
323
- .token-display {
324
- flex: 1; overflow-y: auto; padding: 14px; min-height: 0;
325
- scrollbar-width: thin; scrollbar-color: var(--border) transparent;
326
- }
327
- .placeholder-msg {
328
- display: flex; flex-direction: column; align-items: center; justify-content: center;
329
- height: 100%; min-height: 120px; gap: 12px; color: var(--text3);
330
- }
331
- .placeholder-icon { font-size: 32px; filter: grayscale(1) opacity(.3); }
332
- .placeholder-msg p {
333
- font-family: 'JetBrains Mono', monospace; font-size: 11px; text-align: center; line-height: 1.6;
334
- }
335
- /* ─── Token Visualization Views ───────────────────────── */
336
- .token-text-view {
337
- font-family: 'JetBrains Mono', monospace; font-size: 13px;
338
- line-height: 2.2; word-break: break-all;
339
- }
340
- .tok {
341
- display: inline; border-radius: 3px; padding: 1px 0;
342
- cursor: default; transition: filter .15s; position: relative;
343
- }
344
- .tok:hover { filter: brightness(1.3); }
345
- .tok-tooltip {
346
- display: none; position: absolute; bottom: 110%; left: 50%;
347
- transform: translateX(-50%); background: var(--bg4); border: 1px solid var(--border2);
348
- border-radius: 5px; padding: 4px 7px; font-size: 10px; white-space: nowrap;
349
- z-index: 50; pointer-events: none; box-shadow: 0 4px 20px rgba(0,0,0,.5);
350
- }
351
- [data-theme="light"] .tok-tooltip { box-shadow: 0 4px 16px rgba(0,0,0,.12); }
352
- .tok:hover .tok-tooltip { display: block; }
353
- .tok-tooltip-id { color: var(--accent); font-weight: 700; }
354
- .tok-tooltip-text { color: var(--text2); }
355
- .tok-space::before { content: '·'; opacity: .3; }
356
- .tok-newline::before { content: '↵'; opacity: .5; }
357
- /* ID VIEW */
358
- .token-id-view { display: flex; flex-wrap: wrap; gap: 5px; }
359
- .tok-id-card {
360
- display: flex; flex-direction: column; align-items: center; border-radius: 6px;
361
- overflow: hidden; border: 1px solid; cursor: default;
362
- transition: transform .15s, box-shadow .15s; min-width: 46px;
363
- }
364
- .tok-id-card:hover { transform: translateY(-2px); box-shadow: 0 4px 16px rgba(0,0,0,.4); }
365
- .tok-id-top {
366
- padding: 2px 5px; font-family: 'JetBrains Mono', monospace; font-size: 10px;
367
- font-weight: 500; width: 100%; text-align: center; border-bottom: 1px solid rgba(255,255,255,.08);
368
- }
369
- [data-theme="light"] .tok-id-top { border-bottom-color: rgba(0,0,0,.06); }
370
- .tok-id-bottom {
371
- padding: 1px 5px 2px; font-family: 'JetBrains Mono', monospace; font-size: 8px;
372
- color: rgba(255,255,255,.4); width: 100%; text-align: center;
373
- }
374
- [data-theme="light"] .tok-id-bottom { color: rgba(0,0,0,.35); }
375
- /* LIST VIEW */
376
- .token-split-view { display: flex; flex-direction: column; gap: 2px; }
377
- .tok-split-row {
378
- display: flex; align-items: stretch; border-radius: 5px; overflow: hidden;
379
- border: 1px solid; font-family: 'JetBrains Mono', monospace; font-size: 11px;
380
- }
381
- .tok-split-idx {
382
- width: 34px; text-align: center; padding: 4px 3px; font-size: 9px;
383
- color: rgba(255,255,255,.3); border-right: 1px solid rgba(255,255,255,.06);
384
- display: flex; align-items: center; justify-content: center;
385
- }
386
- [data-theme="light"] .tok-split-idx { color: rgba(0,0,0,.25); border-right-color: rgba(0,0,0,.06); }
387
- .tok-split-text { flex: 1; padding: 4px 6px; font-size: 12px; }
388
- .tok-split-id {
389
- padding: 4px 6px; font-size: 10px; color: rgba(255,255,255,.45);
390
- border-left: 1px solid rgba(255,255,255,.06); display: flex; align-items: center;
391
- }
392
- [data-theme="light"] .tok-split-id { color: rgba(0,0,0,.35); border-left-color: rgba(0,0,0,.06); }
393
- /* ─── Loading Overlay ────────────────────────────────── */
394
- #loading-overlay {
395
- position: fixed; inset: 0; background: rgba(6,11,20,.92);
396
- backdrop-filter: blur(8px); z-index: 1000;
397
- display: flex; flex-direction: column; align-items: center; justify-content: center;
398
- gap: 20px; transition: opacity .4s;
399
- }
400
- [data-theme="light"] #loading-overlay { background: rgba(238,241,248,.92); }
401
- #loading-overlay.hidden { opacity: 0; pointer-events: none; }
402
- .loading-spinner { width: 48px; height: 48px; position: relative; }
403
- .loading-spinner::before, .loading-spinner::after {
404
- content: ''; position: absolute; border-radius: 50%; border: 2px solid transparent;
405
- }
406
- .loading-spinner::before { inset: 0; border-top-color: var(--accent); animation: spin 1s linear infinite; }
407
- .loading-spinner::after { inset: 7px; border-top-color: var(--accent2); animation: spin .7s linear infinite reverse; }
408
- @keyframes spin { to { transform: rotate(360deg); } }
409
- .loading-text { font-family: 'Bricolage Grotesque', sans-serif; font-size: 18px; font-weight: 600; color: var(--text); }
410
- .loading-sub {
411
- font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text2);
412
- max-width: 340px; text-align: center;
413
- }
414
- .loading-bar-wrap { width: 260px; height: 3px; background: var(--bg3); border-radius: 2px; overflow: hidden; }
415
- .loading-bar {
416
- height: 100%; width: 0%;
417
- background: linear-gradient(90deg, var(--accent), var(--accent2));
418
- border-radius: 2px; transition: width .3s;
419
- }
420
- .loading-file { font-size: 10px; font-family: 'JetBrains Mono', monospace; color: var(--text3); }
421
- /* ─── Error Toast ────────────────────────────────────── */
422
- #toast {
423
- position: fixed; bottom: 24px; left: 50%;
424
- transform: translateX(-50%) translateY(80px);
425
- background: rgba(245,85,119,.15); border: 1px solid rgba(245,85,119,.4);
426
- color: var(--red); padding: 8px 18px; border-radius: 8px;
427
- font-size: 12px; font-family: 'JetBrains Mono', monospace;
428
- z-index: 500; transition: transform .3s; max-width: 460px; text-align: center;
429
- }
430
- #toast.show { transform: translateX(-50%) translateY(0); }
431
- /* ─── Footer ─────────────────────────────────────────── */
432
- footer {
433
- padding: 8px 24px; border-top: 1px solid var(--border);
434
- display: flex; align-items: center; justify-content: space-between;
435
- font-size: 10px; color: var(--text3); font-family: 'JetBrains Mono', monospace;
436
- background: rgba(6,11,20,.8); flex-shrink: 0;
437
- }
438
- [data-theme="light"] footer { background: rgba(238,241,248,.8); }
439
- footer a { color: var(--text2); text-decoration: none; transition: color .15s; }
440
- footer a:hover { color: var(--accent); }
441
- /* ─── Scrollbar ──────────────────────────────────────── */
442
- ::-webkit-scrollbar { width: 5px; }
443
- ::-webkit-scrollbar-track { background: transparent; }
444
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
445
- ::-webkit-scrollbar-thumb:hover { background: var(--border2); }
446
- /* ─── Animations ─────────────────────────────────────── */
447
- @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
448
- .fade-in { animation: fadeIn .2s ease forwards; }
449
- /* ─── Responsive: Tablet (600–900px) ──────────────────── */
450
- @media (min-width: 601px) and (max-width: 900px) {
451
- header {
452
- flex-wrap: wrap;
453
- height: auto;
454
- padding: 8px 12px;
455
- gap: 8px;
456
- }
457
- .header-divider { display: none; }
458
- .header-controls {
459
- flex-basis: 100%;
460
- display: flex;
461
- flex-wrap: wrap;
462
- gap: 8px;
463
- }
464
- .searchbar-group {
465
- flex: 1 1 180px;
466
- }
467
- main {
468
- grid-template-columns: 1fr;
469
- grid-template-rows: auto 1fr 1fr;
470
- }
471
- main.single-panel {
472
- grid-template-columns: 1fr;
473
- grid-template-rows: auto 1fr;
474
- }
475
- .input-panel {
476
- border-right: none;
477
- border-bottom: 1px solid var(--border);
478
- max-height: 25vh;
479
- }
480
- .output-panel {
481
- border-left: none !important;
482
- border-bottom: 1px solid var(--border);
483
- }
484
- .output-panel:last-child {
485
- border-bottom: none;
486
- }
487
- .stat-card { padding: 7px 10px; }
488
- .stat-value { font-size: 17px; }
489
- .stat-sub { font-size: 8px; }
490
- .stat-label { font-size: 8px; }
491
- .output-panel-header { padding: 8px 10px; }
492
- .view-toggle { padding: 6px 10px; }
493
- }
494
- /* ─── Responsive: Mobile (≤600px) ────────────────────── */
495
- @media (max-width: 600px) {
496
- header {
497
- flex-wrap: wrap;
498
- height: auto;
499
- padding: 6px 10px;
500
- gap: 6px;
501
- }
502
- .header-divider { display: none; }
503
- .logo-tag { display: none; }
504
- .header-controls {
505
- flex-basis: 100%;
506
- display: flex;
507
- flex-wrap: wrap;
508
- gap: 6px;
509
- }
510
- .searchbar-group {
511
- flex: 1 1 100%;
512
- }
513
- .searchbar-label { display: none; }
514
- .dropdown-menu {
515
- left: 0 !important;
516
- right: 0 !important;
517
- min-width: unset;
518
- }
519
- .mobile-tab-bar {
520
- display: flex;
521
- }
522
- main {
523
- grid-template-columns: 1fr;
524
- grid-template-rows: 1fr;
525
- }
526
- .input-panel,
527
- .output-panel {
528
- display: none;
529
- }
530
- .mobile-active {
531
- display: flex !important;
532
- }
533
- .input-panel {
534
- border-right: none;
535
- border-bottom: none;
536
- max-height: none;
537
- }
538
- .output-panel {
539
- border-left: none !important;
540
- border-bottom: none;
541
- }
542
- .panel-header {
543
- flex-wrap: wrap;
544
- gap: 6px;
545
- padding: 10px 12px 8px;
546
- }
547
- .sample-btns { width: 100%; }
548
- .sample-btn { font-size: 9px; padding: 3px 6px; }
549
- .output-panel-header { padding: 6px 10px; }
550
- .model-indicator { font-size: 10px; }
551
- .stat-card { padding: 5px 8px; }
552
- .stat-value { font-size: 16px; }
553
- .stat-label { font-size: 7px; letter-spacing: .5px; margin-bottom: 2px; }
554
- .stat-sub { font-size: 7px; }
555
- .view-toggle { padding: 5px 10px; }
556
- .toggle-btn { padding: 3px 8px; font-size: 10px; }
557
- .token-display { padding: 10px; }
558
- .token-text-view { font-size: 12px; line-height: 2; }
559
- .tok-split-idx { width: 28px; font-size: 8px; }
560
- .tok-split-text { font-size: 11px; }
561
- .tok-split-id { font-size: 9px; }
562
- footer {
563
- padding: 6px 12px;
564
- font-size: 9px;
565
- }
566
- footer span:last-child { display: none; }
567
- }
568
- </style>
569
  </head>
570
  <body>
571
- <div class="bg-gradient"></div>
572
- <div class="dot-grid"></div>
573
- <div id="app">
574
- <!-- Header -->
575
- <header>
576
- <div class="logo">
577
- <div class="logo-hex">T</div>
578
- <span class="logo-name">TokenLens</span>
579
- <span class="logo-tag">v1.0</span>
580
- </div>
581
- <div class="header-divider"></div>
582
- <div class="header-controls">
583
- <!-- Search bar A -->
584
- <div class="searchbar-group" id="search-group-0">
585
- <span class="searchbar-label label-a">A</span>
586
- <input class="searchbar-input" id="search-input-0" type="text" placeholder="Model A: HF repo id…" />
587
- <button class="searchbar-dropdown-btn" id="dropdown-btn-0" title="Predefined models">
588
- <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 3.5L5 6.5L8 3.5"/></svg>
589
- </button>
590
- <button class="searchbar-load-btn" id="load-btn-0">Load</button>
591
- <div class="dropdown-menu" id="dropdown-menu-0"></div>
592
- </div>
593
- <!-- Search bar B -->
594
- <div class="searchbar-group" id="search-group-1">
595
- <span class="searchbar-label label-b">B</span>
596
- <input class="searchbar-input" id="search-input-1" type="text" placeholder="Model B: HF repo id…" />
597
- <button class="searchbar-dropdown-btn" id="dropdown-btn-1" title="Predefined models">
598
- <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 3.5L5 6.5L8 3.5"/></svg>
599
- </button>
600
- <button class="searchbar-load-btn" id="load-btn-1">Load</button>
601
- <div class="dropdown-menu" id="dropdown-menu-1"></div>
602
- </div>
603
- <!-- Toggle: show/hide panel B -->
604
- <button class="icon-toggle-btn active" id="panel-toggle" title="Toggle comparison panel">
605
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="3" y="3" width="7" height="18" rx="1"/><rect x="14" y="3" width="7" height="18" rx="1"/></svg>
606
- </button>
607
- <!-- Toggle: light/dark theme -->
608
- <button class="icon-toggle-btn" id="theme-toggle" title="Toggle light/dark theme">
609
- <svg id="theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="display:none"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
610
- <svg id="theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
611
- </button>
612
- </div>
613
- </header>
614
- <!-- Mobile Tab Bar -->
615
- <div class="mobile-tab-bar" id="mobile-tab-bar">
616
- <button class="mobile-tab active" data-tab="input">Input</button>
617
- <button class="mobile-tab" data-tab="panel-0">Model A</button>
618
- <button class="mobile-tab" data-tab="panel-1">Model B</button>
619
- </div>
620
- <!-- Main -->
621
- <main id="main-grid">
622
- <!-- Left: Input -->
623
- <div class="input-panel mobile-active" id="input-panel">
624
- <div class="panel-header">
625
- <div class="panel-title">
626
- <div class="panel-title-icon">✎</div>
627
- Input Text
628
- </div>
629
- <div class="sample-btns">
630
- <button class="sample-btn" data-sample="poetry">Poetry</button>
631
- <button class="sample-btn" data-sample="code">Code</button>
632
- <button class="sample-btn" data-sample="multilingual">Multi</button>
633
- <button class="sample-btn" data-sample="numbers">Numbers</button>
634
- <button class="sample-btn" data-sample="clear">Clear</button>
635
- </div>
636
- </div>
637
- <textarea id="input-area"
638
- placeholder="Type or paste text here to see how tokenizers split it into tokens…
639
-
640
- Try special characters, code, emojis, or multi-lingual text to compare models."></textarea>
641
- <div class="char-counter"><span id="char-count">0</span> characters</div>
642
- </div>
643
- <!-- Visualizer A -->
644
- <div class="output-panel" id="panel-0">
645
- <div class="output-panel-header">
646
- <div class="model-indicator" id="model-indicator-0">
647
- <div class="model-indicator-dot" id="model-dot-0" style="background:#3d5a80"></div>
648
- <span class="model-indicator-name" id="model-label-0">A: no model</span>
649
- </div>
650
- </div>
651
- <div class="stats-row">
652
- <div class="stat-card" id="sc-tokens-0">
653
- <div class="stat-label">Tokens</div>
654
- <div class="stat-value" id="stat-tokens-0">—</div>
655
- <div class="stat-sub" id="stat-model-0">no model loaded</div>
656
- </div>
657
- <div class="stat-card" id="sc-chars-0">
658
- <div class="stat-label">Characters</div>
659
- <div class="stat-value" id="stat-chars-0">—</div>
660
- <div class="stat-sub">total input</div>
661
- </div>
662
- <div class="stat-card" id="sc-words-0">
663
- <div class="stat-label">Words</div>
664
- <div class="stat-value" id="stat-words-0">—</div>
665
- <div class="stat-sub">approx</div>
666
- </div>
667
- <div class="stat-card" id="sc-ratio-0">
668
- <div class="stat-label">Chars/Token</div>
669
- <div class="stat-value" id="stat-ratio-0">—</div>
670
- <div class="stat-sub">efficiency</div>
671
  </div>
672
- </div>
673
- <div class="view-toggle">
674
- <div class="toggle-group" id="toggle-group-0">
675
- <button class="toggle-btn active" data-view="text" data-panel="0">Text View</button>
676
- <button class="toggle-btn" data-view="ids" data-panel="0">ID Grid</button>
677
- <button class="toggle-btn" data-view="list" data-panel="0">Token List</button>
678
- </div>
679
- </div>
680
- <div class="token-display" id="token-display-0">
681
- <div class="placeholder-msg" id="placeholder-0">
682
- <div class="placeholder-icon">⬡</div>
683
- <p>Load a tokenizer using search bar A above<br>then type text to see tokenization</p>
684
- </div>
685
- </div>
686
  </div>
687
- <!-- Visualizer B -->
688
- <div class="output-panel" id="panel-1">
689
- <div class="output-panel-header">
690
- <div class="model-indicator" id="model-indicator-1">
691
- <div class="model-indicator-dot" id="model-dot-1" style="background:#3d5a80"></div>
692
- <span class="model-indicator-name" id="model-label-1">B: no model</span>
693
- </div>
694
- </div>
695
- <div class="stats-row">
696
- <div class="stat-card" id="sc-tokens-1">
697
- <div class="stat-label">Tokens</div>
698
- <div class="stat-value" id="stat-tokens-1">—</div>
699
- <div class="stat-sub" id="stat-model-1">no model loaded</div>
700
- </div>
701
- <div class="stat-card" id="sc-chars-1">
702
- <div class="stat-label">Characters</div>
703
- <div class="stat-value" id="stat-chars-1">—</div>
704
- <div class="stat-sub">total input</div>
705
- </div>
706
- <div class="stat-card" id="sc-words-1">
707
- <div class="stat-label">Words</div>
708
- <div class="stat-value" id="stat-words-1">—</div>
709
- <div class="stat-sub">approx</div>
710
  </div>
711
- <div class="stat-card" id="sc-ratio-1">
712
- <div class="stat-label">Chars/Token</div>
713
- <div class="stat-value" id="stat-ratio-1">—</div>
714
- <div class="stat-sub">efficiency</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
715
  </div>
716
- </div>
717
- <div class="view-toggle">
718
- <div class="toggle-group" id="toggle-group-1">
719
- <button class="toggle-btn active" data-view="text" data-panel="1">Text View</button>
720
- <button class="toggle-btn" data-view="ids" data-panel="1">ID Grid</button>
721
- <button class="toggle-btn" data-view="list" data-panel="1">Token List</button>
722
  </div>
723
- </div>
724
- <div class="token-display" id="token-display-1">
725
- <div class="placeholder-msg" id="placeholder-1">
726
- <div class="placeholder-icon">⬡</div>
727
- <p>Load a tokenizer using search bar B above<br>then type text to see tokenization</p>
728
  </div>
729
- </div>
730
  </div>
731
- </main>
732
- <footer>
733
- <span>TokenLens — Powered by <a href="https://github.com/xenova/transformers.js" target="_blank">Transformers.js</a> · Runs entirely in your browser</span>
734
- <span>Hover tokens to see IDs · Compare tokenizers side by side</span>
735
- </footer>
736
- </div>
737
- <!-- Loading Overlay -->
738
- <div id="loading-overlay">
739
- <div class="loading-spinner"></div>
740
- <div class="loading-text" id="loading-title">Loading Tokenizer</div>
741
- <div class="loading-sub" id="loading-sub">Downloading tokenizer files from Hugging Face Hub…<br>Cached in your browser after first download.</div>
742
- <div class="loading-bar-wrap"><div class="loading-bar" id="loading-bar"></div></div>
743
- <div class="loading-file" id="loading-file"></div>
744
- </div>
745
- <!-- Toast -->
746
- <div id="toast"></div>
747
-
748
- <script type="module">
749
- import { AutoTokenizer, env }
750
- from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2';
751
-
752
- env.allowLocalModels = false;
753
- env.useBrowserCache = true;
754
-
755
- // ── Model Registry ─────────────────────────────────────────
756
- const MODELS = [
757
- { id:'Xenova/gpt2', name:'GPT-2', org:'OpenAI', color:'#10a37f', vocab:'50k', type:'BPE', desc:'Classic GPT-2 BPE tokenizer' },
758
- { id:'Xenova/gpt-4', name:'GPT-4', org:'OpenAI', color:'#10a37f', vocab:'100k', type:'tiktoken cl100k', desc:'Used by GPT-3.5 & GPT-4' },
759
- { id:'Xenova/llama-tokenizer', name:'LLaMA 2', org:'Meta', color:'#0466de', vocab:'32k', type:'SP-BPE', desc:'SentencePiece BPE — LLaMA / LLaMA-2' },
760
- { id:'Xenova/mistral-tokenizer-v1', name:'Mistral', org:'Mistral AI', color:'#ff7722', vocab:'32k', type:'SP-BPE', desc:'Mistral 7B v0.1 tokenizer' },
761
- { id:'Xenova/bert-base-uncased', name:'BERT', org:'Google', color:'#4285f4', vocab:'30k', type:'WordPiece', desc:'BERT-base uncased WordPiece' },
762
- { id:'Xenova/t5-base', name:'T5', org:'Google', color:'#34a853', vocab:'32k', type:'Unigram', desc:'T5 SentencePiece Unigram' },
763
- { id:'Xenova/claude-tokenizer', name:'Claude', org:'Anthropic', color:'#cc785c', vocab:'~100k', type:'BPE', desc:"Anthropic Claude's tokenizer" },
764
- { id:'Xenova/roberta-base', name:'RoBERTa', org:'Meta', color:'#1a73e8', vocab:'50k', type:'BPE', desc:'RoBERTa byte-level BPE' },
765
- ];
766
-
767
- // ── Token Color Palettes ───────────────────────────────────
768
- const PALETTE_DARK = [
769
- { text:'#ff8080', bg:'rgba(255,128,128,.18)', border:'rgba(255,128,128,.35)' },
770
- { text:'#ffb84d', bg:'rgba(255,184, 77,.18)', border:'rgba(255,184, 77,.35)' },
771
- { text:'#ffe066', bg:'rgba(255,224,102,.18)', border:'rgba(255,224,102,.35)' },
772
- { text:'#7aed91', bg:'rgba(122,237,145,.18)', border:'rgba(122,237,145,.35)' },
773
- { text:'#4ddfc0', bg:'rgba( 77,223,192,.18)', border:'rgba( 77,223,192,.35)' },
774
- { text:'#56c8f5', bg:'rgba( 86,200,245,.18)', border:'rgba( 86,200,245,.35)' },
775
- { text:'#748ef8', bg:'rgba(116,142,248,.18)', border:'rgba(116,142,248,.35)' },
776
- { text:'#c484f8', bg:'rgba(196,132,248,.18)', border:'rgba(196,132,248,.35)' },
777
- { text:'#f57cd4', bg:'rgba(245,124,212,.18)', border:'rgba(245,124,212,.35)' },
778
- { text:'#fa8072', bg:'rgba(250,128,114,.18)', border:'rgba(250,128,114,.35)' },
779
- { text:'#8be08b', bg:'rgba(139,224,139,.18)', border:'rgba(139,224,139,.35)' },
780
- { text:'#f0c040', bg:'rgba(240,192, 64,.18)', border:'rgba(240,192, 64,.35)' },
781
- { text:'#60d4e0', bg:'rgba( 96,212,224,.18)', border:'rgba( 96,212,224,.35)' },
782
- { text:'#e89060', bg:'rgba(232,144, 96,.18)', border:'rgba(232,144, 96,.35)' },
783
- ];
784
- const PALETTE_LIGHT = [
785
- { text:'#cc3333', bg:'rgba(204, 51, 51,.12)', border:'rgba(204, 51, 51,.25)' },
786
- { text:'#b87218', bg:'rgba(184,114, 24,.12)', border:'rgba(184,114, 24,.25)' },
787
- { text:'#a08618', bg:'rgba(160,134, 24,.12)', border:'rgba(160,134, 24,.25)' },
788
- { text:'#228838', bg:'rgba( 34,136, 56,.12)', border:'rgba( 34,136, 56,.25)' },
789
- { text:'#1a8870', bg:'rgba( 26,136,112,.12)', border:'rgba( 26,136,112,.25)' },
790
- { text:'#1890b8', bg:'rgba( 24,144,184,.12)', border:'rgba( 24,144,184,.25)' },
791
- { text:'#3850b8', bg:'rgba( 56, 80,184,.12)', border:'rgba( 56, 80,184,.25)' },
792
- { text:'#7830a8', bg:'rgba(120, 48,168,.12)', border:'rgba(120, 48,168,.25)' },
793
- { text:'#b03088', bg:'rgba(176, 48,136,.12)', border:'rgba(176, 48,136,.25)' },
794
- { text:'#b83828', bg:'rgba(184, 56, 40,.12)', border:'rgba(184, 56, 40,.25)' },
795
- { text:'#2a882a', bg:'rgba( 42,136, 42,.12)', border:'rgba( 42,136, 42,.25)' },
796
- { text:'#9a7018', bg:'rgba(154,112, 24,.12)', border:'rgba(154,112, 24,.25)' },
797
- { text:'#1a8898', bg:'rgba( 26,136,152,.12)', border:'rgba( 26,136,152,.25)' },
798
- { text:'#a05020', bg:'rgba(160, 80, 32,.12)', border:'rgba(160, 80, 32,.25)' },
799
- ];
800
- function getPalette() {
801
- return document.documentElement.dataset.theme === 'light' ? PALETTE_LIGHT : PALETTE_DARK;
802
- }
803
-
804
- // ── Sample texts ───────────────────────────────────────────
805
- const SAMPLES = {
806
- poetry: `Two roads diverged in a yellow wood,\nAnd sorry I could not travel both\nAnd be one traveler, long I stood\nAnd looked down one as far as I could\nTo where it bent in the undergrowth;\n— Robert Frost, "The Road Not Taken"`,
807
- code: `async function fetchData(url, retries = 3) {\n for (let i = 0; i < retries; i++) {\n try {\n const res = await fetch(url);\n if (!res.ok) throw new Error(\`HTTP \${res.status}\`);\n return await res.json();\n } catch (e) {\n if (i === retries - 1) throw e;\n await new Promise(r => setTimeout(r, 1000 * 2 ** i));\n }\n }\n}`,
808
- multilingual: `English: The quick brown fox jumps over the lazy dog.\n日本語: 吾輩は猫である。名前はまだない。\n中文: 春眠不觉晓,处处闻啼鸟。\nالعربية: اللغة العربية جميلة ومعبرة.\nΕλληνικά: Η γνώση είναι δύναμη.\nEmoji: 🌍 🦊 ⚡ 🎯 🧬 🤖 🦋`,
809
- numbers: `π ≈ 3.14159265358979323846\ne ≈ 2.71828182845904523536\nφ ≈ 1.61803398874989484820\n1,000,000 × $42.99 = $42,990,000.00\n2024-01-15T08:30:00.000Z\nIPv4: 192.168.1.1 | IPv6: ::1`,
810
- clear: ''
811
- };
812
-
813
- // ── State ──────────────────────────────────────────────────
814
- const panels = [
815
- { tokenizer: null, modelId: null, view: 'text' },
816
- { tokenizer: null, modelId: null, view: 'text' },
817
- ];
818
- let tokenizerCache = {};
819
- let panel1Visible = true;
820
- let debounceTimer = null;
821
- let mobileActiveTab = 'input';
822
-
823
- // ── DOM References ─────────────────────────────────────────
824
- const $overlay = document.getElementById('loading-overlay');
825
- const $loadTitle = document.getElementById('loading-title');
826
- const $loadSub = document.getElementById('loading-sub');
827
- const $loadBar = document.getElementById('loading-bar');
828
- const $loadFile = document.getElementById('loading-file');
829
- const $input = document.getElementById('input-area');
830
- const $charCount = document.getElementById('char-count');
831
- const $toast = document.getElementById('toast');
832
- const $mainGrid = document.getElementById('main-grid');
833
- const $panelToggle = document.getElementById('panel-toggle');
834
- const $themeToggle = document.getElementById('theme-toggle');
835
- const $inputPanel = document.getElementById('input-panel');
836
- const $panel0 = document.getElementById('panel-0');
837
- const $panel1 = document.getElementById('panel-1');
838
- const $mobileTabs = document.querySelectorAll('.mobile-tab');
839
-
840
- // ── Utilities ──────────────────────────────────────────────
841
- function showOverlay(title, sub) {
842
- $loadTitle.textContent = title;
843
- $loadSub.textContent = sub;
844
- $loadBar.style.width = '0%';
845
- $loadFile.textContent = '';
846
- $overlay.classList.remove('hidden');
847
- }
848
- function hideOverlay() { $overlay.classList.add('hidden'); }
849
-
850
- function showToast(msg, duration = 5000) {
851
- $toast.textContent = msg;
852
- $toast.classList.add('show');
853
- setTimeout(() => $toast.classList.remove('show'), duration);
854
- }
855
 
856
- function setStats(idx, tokens, text) {
857
- const chars = text.length;
858
- const words = text.trim() ? text.trim().split(/\s+/).length : 0;
859
- const ratio = tokens > 0 && chars > 0 ? (chars / tokens).toFixed(2) : '—';
860
- document.getElementById(`stat-tokens-${idx}`).textContent = tokens > 0 ? tokens.toLocaleString() : '';
861
- document.getElementById(`stat-chars-${idx}`).textContent = chars > 0 ? chars.toLocaleString() : '—';
862
- document.getElementById(`stat-words-${idx}`).textContent = words > 0 ? words.toLocaleString() : '—';
863
- document.getElementById(`stat-ratio-${idx}`).textContent = ratio;
864
- ['tokens','chars','words','ratio'].forEach(k => {
865
- const el = document.getElementById(`sc-${k}-${idx}`);
866
- el.classList.remove('highlight'); void el.offsetWidth; el.classList.add('highlight');
867
- });
868
- }
869
-
870
- function updateModelIndicator(idx, modelId) {
871
- const preset = MODELS.find(m => m.id === modelId);
872
- const color = preset ? preset.color : '#7899c0';
873
- const name = modelId ? modelId.split('/').pop() : 'no model';
874
- const label = idx === 0 ? 'A' : 'B';
875
- document.getElementById(`model-dot-${idx}`).style.background = color;
876
- document.getElementById(`model-dot-${idx}`).style.boxShadow = `0 0 6px ${color}`;
877
- document.getElementById(`model-label-${idx}`).textContent = `${label}: ${name}`;
878
- document.getElementById(`stat-model-${idx}`).textContent = preset ? `${preset.org} · ${preset.type} · ${preset.vocab} vocab` : modelId || 'no model loaded';
879
- }
880
-
881
- // ── Decode raw token string for display ───────────────────
882
- function decodeTokenString(raw) {
883
- if (!raw) return '';
884
- let s = raw.replace(/^Ġ/, ' ').replace(/Ġ/g, ' ');
885
- s = s.replace(/^▁/, ' ').replace(/▁/g, ' ');
886
- s = s.replace(/Ċ/g, '\n');
887
- s = s.replace(/\r/g, '');
888
- s = s.replace(/<0x([0-9A-Fa-f]{2})>/g, (_, hex) => {
889
- const code = parseInt(hex, 16);
890
- return code < 128 ? String.fromCharCode(code) : `[0x${hex}]`;
891
- });
892
- return s;
893
- }
894
-
895
- // ── Mobile Tab Handling ────────────────────────────────────
896
- function isMobile() { return window.innerWidth <= 600; }
897
-
898
- function applyMobileTab(tabId) {
899
- mobileActiveTab = tabId;
900
- $mobileTabs.forEach(t => t.classList.toggle('active', t.dataset.tab === tabId));
901
- // Remove mobile-active from all panels
902
- $inputPanel.classList.remove('mobile-active');
903
- $panel0.classList.remove('mobile-active');
904
- $panel1.classList.remove('mobile-active');
905
- // Add to the target
906
- if (tabId === 'input') $inputPanel.classList.add('mobile-active');
907
- if (tabId === 'panel-0') $panel0.classList.add('mobile-active');
908
- if (tabId === 'panel-1') $panel1.classList.add('mobile-active');
909
- }
910
-
911
- $mobileTabs.forEach(tab => {
912
- tab.addEventListener('click', () => {
913
- applyMobileTab(tab.dataset.tab);
914
- });
915
- });
916
-
917
- // Handle resize: reset display properties when switching between mobile/desktop
918
- function handleResize() {
919
- if (!isMobile()) {
920
- // Desktop/tablet: remove mobile-active, reset display for all panels
921
- $inputPanel.classList.remove('mobile-active');
922
- $panel0.classList.remove('mobile-active');
923
- $panel1.classList.remove('mobile-active');
924
- $inputPanel.style.display = '';
925
- $panel0.style.display = '';
926
- $panel1.style.display = panel1Visible ? '' : 'none';
927
- } else {
928
- // Mobile: apply mobile tab logic
929
- $inputPanel.style.display = '';
930
- $panel0.style.display = '';
931
- $panel1.style.display = '';
932
- applyMobileTab(mobileActiveTab);
933
- }
934
- updateMobileTabBState();
935
- }
936
-
937
- function updateMobileTabBState() {
938
- const $tabB = document.querySelector('.mobile-tab[data-tab="panel-1"]');
939
- if ($tabB) {
940
- if (panel1Visible) {
941
- $tabB.classList.remove('tab-disabled');
942
- } else {
943
- $tabB.classList.add('tab-disabled');
944
- // If currently on tab B, switch away
945
- if (mobileActiveTab === 'panel-1') {
946
- applyMobileTab('panel-0');
947
- }
948
- }
949
- }
950
- }
951
-
952
- window.addEventListener('resize', handleResize);
953
-
954
- // ── Tokenize for a specific panel ─────────────────────────
955
- async function tokenizeForPanel(idx, text) {
956
- const p = panels[idx];
957
- const $display = document.getElementById(`token-display-${idx}`);
958
- const $placeholder = document.getElementById(`placeholder-${idx}`);
959
-
960
- if (!p.tokenizer || !text.trim()) {
961
- const prevView = $display.querySelector('.token-view-container');
962
- if (prevView) prevView.remove();
963
- if ($placeholder) $placeholder.style.display = 'flex';
964
- setStats(idx, 0, text);
965
- return;
966
- }
967
- try {
968
- if ($placeholder) $placeholder.style.display = 'none';
969
- const encoded = await p.tokenizer(text, { add_special_tokens: false });
970
- const ids = Array.from(encoded.input_ids.data);
971
- let rawTokens;
972
- try { rawTokens = p.tokenizer.model.convert_ids_to_tokens(ids); }
973
- catch { rawTokens = await Promise.all(ids.map(id => p.tokenizer.decode([id], { skip_special_tokens: false }))); }
974
- const tokens = ids.map((id, i) => ({
975
- id, raw: rawTokens[i] || '', display: decodeTokenString(rawTokens[i] || ''),
976
- }));
977
- setStats(idx, tokens.length, text);
978
- renderView(idx, tokens);
979
- } catch (err) {
980
- console.error('Tokenization error:', err);
981
- showToast(`Panel ${idx === 0 ? 'A' : 'B'} error: ${err.message}`);
982
- }
983
- }
984
-
985
- // ── Render Views ───────────────────────────────────────────
986
- function renderView(idx, tokens) {
987
- const view = panels[idx].view;
988
- if (view === 'text') renderTextView(idx, tokens);
989
- else if (view === 'ids') renderIdView(idx, tokens);
990
- else if (view === 'list') renderListView(idx, tokens);
991
- }
992
-
993
- function renderTextView(idx, tokens) {
994
- const PALETTE = getPalette();
995
- const $display = document.getElementById(`token-display-${idx}`);
996
- const container = document.createElement('div');
997
- container.className = 'token-text-view token-view-container fade-in';
998
- tokens.forEach((tok, i) => {
999
- const c = PALETTE[i % PALETTE.length];
1000
- const span = document.createElement('span');
1001
- span.className = 'tok';
1002
- span.style.background = c.bg;
1003
- span.style.color = c.text;
1004
- span.style.borderBottom = `2px solid ${c.border}`;
1005
- const disp = tok.display;
1006
- if (disp === ' ') span.innerHTML = '&nbsp;';
1007
- else if (disp === '\n') span.innerHTML = '↵<br>';
1008
- else if (disp === '\t') span.innerHTML = '→&nbsp;&nbsp;&nbsp;';
1009
- else span.textContent = disp;
1010
- const tip = document.createElement('div');
1011
- tip.className = 'tok-tooltip';
1012
- const rawEsc = tok.raw.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
1013
- tip.innerHTML = `<span class="tok-tooltip-id">#${tok.id}</span> · <span class="tok-tooltip-text">${rawEsc || '(empty)'}</span>`;
1014
- span.appendChild(tip);
1015
- container.appendChild(span);
1016
- });
1017
- const prevView = $display.querySelector('.token-view-container');
1018
- if (prevView) prevView.remove();
1019
- document.getElementById(`placeholder-${idx}`).style.display = 'none';
1020
- $display.appendChild(container);
1021
- }
1022
-
1023
- function renderIdView(idx, tokens) {
1024
- const PALETTE = getPalette();
1025
- const $display = document.getElementById(`token-display-${idx}`);
1026
- const container = document.createElement('div');
1027
- container.className = 'token-id-view token-view-container fade-in';
1028
- tokens.forEach((tok, i) => {
1029
- const c = PALETTE[i % PALETTE.length];
1030
- const card = document.createElement('div');
1031
- card.className = 'tok-id-card';
1032
- card.style.background = c.bg;
1033
- card.style.borderColor = c.border;
1034
- card.title = `Raw: ${tok.raw}`;
1035
- const top = document.createElement('div');
1036
- top.className = 'tok-id-top'; top.style.color = c.text; top.textContent = tok.id;
1037
- const bot = document.createElement('div');
1038
- bot.className = 'tok-id-bottom';
1039
- bot.textContent = tok.display.slice(0, 8).replace(/\n/g,'↵').replace(/\t/g,'→') || '…';
1040
- card.appendChild(top); card.appendChild(bot);
1041
- container.appendChild(card);
1042
- });
1043
- const prevView = $display.querySelector('.token-view-container');
1044
- if (prevView) prevView.remove();
1045
- document.getElementById(`placeholder-${idx}`).style.display = 'none';
1046
- $display.appendChild(container);
1047
- }
1048
-
1049
- function renderListView(idx, tokens) {
1050
- const PALETTE = getPalette();
1051
- const $display = document.getElementById(`token-display-${idx}`);
1052
- const container = document.createElement('div');
1053
- container.className = 'token-split-view token-view-container fade-in';
1054
- tokens.forEach((tok, i) => {
1055
- const c = PALETTE[i % PALETTE.length];
1056
- const row = document.createElement('div');
1057
- row.className = 'tok-split-row';
1058
- row.style.background = c.bg;
1059
- row.style.borderColor = c.border;
1060
- const idxEl = document.createElement('div');
1061
- idxEl.className = 'tok-split-idx'; idxEl.textContent = i;
1062
- const textEl = document.createElement('div');
1063
- textEl.className = 'tok-split-text'; textEl.style.color = c.text;
1064
- textEl.textContent = tok.display.replace(/\n/g,'↵').replace(/\t/g,'→') || '(empty)';
1065
- const idEl = document.createElement('div');
1066
- idEl.className = 'tok-split-id'; idEl.textContent = tok.id;
1067
- row.appendChild(idxEl); row.appendChild(textEl); row.appendChild(idEl);
1068
- container.appendChild(row);
1069
- });
1070
- const prevView = $display.querySelector('.token-view-container');
1071
- if (prevView) prevView.remove();
1072
- document.getElementById(`placeholder-${idx}`).style.display = 'none';
1073
- $display.appendChild(container);
1074
- }
1075
-
1076
- // ── Load Tokenizer ─────────────────────────────────────────
1077
- async function loadModel(idx, modelId) {
1078
- if (tokenizerCache[modelId]) {
1079
- panels[idx].tokenizer = tokenizerCache[modelId];
1080
- panels[idx].modelId = modelId;
1081
- updateModelIndicator(idx, modelId);
1082
- await runTokenize();
1083
- return;
1084
- }
1085
- const displayName = modelId.split('/').pop();
1086
- const label = idx === 0 ? 'A' : 'B';
1087
- showOverlay(
1088
- `Loading ${label}: ${displayName}`,
1089
- `Fetching tokenizer files from Hugging Face Hub.\nCached in IndexedDB after first download.`
1090
- );
1091
- let lastProgress = 0;
1092
- try {
1093
- const tokenizer = await AutoTokenizer.from_pretrained(modelId, {
1094
- progress_callback: (info) => {
1095
- if (info.status === 'downloading') {
1096
- const pct = info.total ? Math.round((info.loaded / info.total) * 100) : lastProgress;
1097
- $loadBar.style.width = pct + '%';
1098
- $loadFile.textContent = info.file || '';
1099
- lastProgress = pct;
1100
- } else if (info.status === 'done') {
1101
- $loadBar.style.width = '100%';
1102
  }
1103
- }
1104
- });
1105
- tokenizerCache[modelId] = tokenizer;
1106
- panels[idx].tokenizer = tokenizer;
1107
- panels[idx].modelId = modelId;
1108
- updateModelIndicator(idx, modelId);
1109
- hideOverlay();
1110
- await runTokenize();
1111
- } catch (err) {
1112
- hideOverlay();
1113
- console.error('Failed to load tokenizer:', err);
1114
- showToast(`Failed to load "${modelId}": ${err.message}`, 8000);
1115
- }
1116
- }
1117
 
1118
- // ── Build Dropdown Menus ───────────────────────��───────────
1119
- function buildDropdowns() {
1120
- [0, 1].forEach(idx => {
1121
- const $menu = document.getElementById(`dropdown-menu-${idx}`);
1122
- $menu.innerHTML = '';
1123
- MODELS.forEach(m => {
1124
- const item = document.createElement('div');
1125
- item.className = 'dropdown-item';
1126
- item.innerHTML = `
1127
- <span class="dropdown-item-dot" style="background:${m.color}"></span>
1128
- <span class="dropdown-item-name">${m.name}</span>
1129
- <span class="dropdown-item-detail">${m.org} · ${m.type}</span>
1130
- `;
1131
- item.addEventListener('click', () => {
1132
- const $input = document.getElementById(`search-input-${idx}`);
1133
- $input.value = m.id;
1134
- $menu.classList.remove('open');
1135
- loadModel(idx, m.id);
1136
- });
1137
- $menu.appendChild(item);
1138
- });
1139
- });
1140
- }
1141
-
1142
- // ── Dropdown toggle ────────────────────────────────────────
1143
- [0, 1].forEach(idx => {
1144
- const $btn = document.getElementById(`dropdown-btn-${idx}`);
1145
- const $menu = document.getElementById(`dropdown-menu-${idx}`);
1146
- $btn.addEventListener('click', (e) => {
1147
- e.stopPropagation();
1148
- const otherIdx = 1 - idx;
1149
- document.getElementById(`dropdown-menu-${otherIdx}`).classList.remove('open');
1150
- $menu.classList.toggle('open');
1151
- });
1152
- });
1153
-
1154
- document.addEventListener('click', () => {
1155
- document.querySelectorAll('.dropdown-menu').forEach(m => m.classList.remove('open'));
1156
- });
1157
- document.querySelectorAll('.dropdown-menu').forEach(m => {
1158
- m.addEventListener('click', e => e.stopPropagation());
1159
- });
1160
-
1161
- // ── Load buttons ───────────────────────────────────────────
1162
- [0, 1].forEach(idx => {
1163
- const $btn = document.getElementById(`load-btn-${idx}`);
1164
- const $input = document.getElementById(`search-input-${idx}`);
1165
- function doLoad() {
1166
- const id = $input.value.trim();
1167
- if (!id) { showToast('Please enter a model ID'); return; }
1168
- loadModel(idx, id);
1169
- }
1170
- $btn.addEventListener('click', doLoad);
1171
- $input.addEventListener('keydown', e => { if (e.key === 'Enter') doLoad(); });
1172
- });
1173
-
1174
- // ── View Toggles ───────────────────────────────────────────
1175
- document.querySelectorAll('.toggle-btn').forEach(btn => {
1176
- btn.addEventListener('click', () => {
1177
- const panelIdx = parseInt(btn.dataset.panel);
1178
- const group = document.getElementById(`toggle-group-${panelIdx}`);
1179
- group.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active'));
1180
- btn.classList.add('active');
1181
- panels[panelIdx].view = btn.dataset.view;
1182
- runTokenize();
1183
- });
1184
- });
1185
-
1186
- // ── Input Handling ─────────────────────────────────────────
1187
- async function runTokenize() {
1188
- const text = $input.value;
1189
- $charCount.textContent = text.length;
1190
- await Promise.all([
1191
- tokenizeForPanel(0, text),
1192
- panel1Visible ? tokenizeForPanel(1, text) : Promise.resolve()
1193
- ]);
1194
- }
1195
 
1196
- $input.addEventListener('input', () => {
1197
- $charCount.textContent = $input.value.length;
1198
- clearTimeout(debounceTimer);
1199
- debounceTimer = setTimeout(runTokenize, 280);
1200
- });
 
 
 
1201
 
1202
- // ── Sample Buttons ─────────────────────────────────────────
1203
- document.querySelectorAll('.sample-btn').forEach(btn => {
1204
- btn.addEventListener('click', () => {
1205
- $input.value = SAMPLES[btn.dataset.sample] ?? '';
1206
- $input.focus();
1207
- runTokenize();
1208
- });
1209
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1210
 
1211
- // ── Panel Toggle ───────────────────────────────────────────
1212
- $panelToggle.addEventListener('click', () => {
1213
- panel1Visible = !panel1Visible;
1214
- $panelToggle.classList.toggle('active', panel1Visible);
1215
- if (!isMobile()) {
1216
- $panel1.style.display = panel1Visible ? '' : 'none';
1217
- }
1218
- const $searchGroup1 = document.getElementById('search-group-1');
1219
- $searchGroup1.style.display = panel1Visible ? '' : 'none';
1220
- if (panel1Visible) {
1221
- $mainGrid.classList.remove('single-panel');
1222
- } else {
1223
- $mainGrid.classList.add('single-panel');
1224
- }
1225
- updateMobileTabBState();
1226
- runTokenize();
1227
- });
1228
 
1229
- // ── Theme Toggle ───────────────────────────────────────────
1230
- const $iconSun = document.getElementById('theme-icon-sun');
1231
- const $iconMoon = document.getElementById('theme-icon-moon');
 
 
 
 
 
 
 
 
 
 
 
 
1232
 
1233
- function setTheme(theme) {
1234
- document.documentElement.dataset.theme = theme;
1235
- if (theme === 'light') {
1236
- $iconSun.style.display = 'none';
1237
- $iconMoon.style.display = 'block';
1238
- } else {
1239
- $iconSun.style.display = 'block';
1240
- $iconMoon.style.display = 'none';
1241
- }
1242
- runTokenize();
1243
- }
1244
 
1245
- $themeToggle.addEventListener('click', () => {
1246
- const current = document.documentElement.dataset.theme;
1247
- setTheme(current === 'dark' ? 'light' : 'dark');
1248
- });
 
1249
 
1250
- // ── Init ───────────────────────────────────────────────────
1251
- buildDropdowns();
1252
- $overlay.classList.add('hidden');
1253
- $input.value = '';
 
 
 
 
 
 
 
 
 
 
 
 
1254
 
1255
- setTheme('dark');
 
 
 
 
 
 
 
 
 
1256
 
1257
- // Set initial mobile state
1258
- handleResize();
 
 
 
 
 
 
1259
 
1260
- document.getElementById('search-input-0').value = MODELS[0].id;
1261
- loadModel(0, MODELS[0].id);
1262
- </script>
1263
  </body>
1264
  </html>
 
1
  <!DOCTYPE html>
2
+ <html lang="en">
3
  <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>RAG Visualizer (HF Spaces)</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.11.0/dist/tf.min.js"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/@xenova/transformers@2.10.0/dist/transformers.min.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/lancedb@0.5.0/dist/lance-wasm.min.js"></script>
10
+ <style>
11
+ :root {
12
+ --primary: #3b82f6;
13
+ --secondary: #1e40af;
14
+ --bg: #f8fafc;
15
+ --text: #0f172a;
16
+ --border: #e2e8f0;
17
+ }
18
+ * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', sans-serif; }
19
+ body {
20
+ background: var(--bg);
21
+ color: var(--text);
22
+ display: grid;
23
+ grid-template-columns: 1fr 1fr;
24
+ grid-template-rows: 1fr auto;
25
+ height: 100vh;
26
+ gap: 1rem;
27
+ padding: 1rem;
28
+ }
29
+ .section {
30
+ background: white;
31
+ border: 1px solid var(--border);
32
+ border-radius: 0.5rem;
33
+ padding: 1rem;
34
+ overflow: auto;
35
+ }
36
+ .chat-window {
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: 1rem;
40
+ height: 100%;
41
+ }
42
+ .chat-messages {
43
+ flex: 1;
44
+ overflow-y: auto;
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 0.5rem;
48
+ }
49
+ .message { padding: 0.5rem; border-radius: 0.25rem; background: var(--border); }
50
+ .message.user { background: var(--primary); color: white; }
51
+ .input-area { display: flex; gap: 0.5rem; }
52
+ input, button { padding: 0.5rem; border: 1px solid var(--border); border-radius: 0.25rem; }
53
+ button { background: var(--primary); color: white; cursor: pointer; }
54
+ button:hover { background: var(--secondary); }
55
+ #vector-table { width: 100%; border-collapse: collapse; }
56
+ #vector-table th, #vector-table td { border: 1px solid var(--border); padding: 0.5rem; text-align: left; }
57
+ #vector-table tr.highlight { background: #fef3c7; animation: pulse 1s; }
58
+ @keyframes pulse { 0% { background: #fef3c7; } 50% { background: #fde68a; } 100% { background: #fef3c7; } }
59
+ #node-editor { height: 300px; border: 1px solid var(--border); border-radius: 0.25rem; }
60
+ .tab { padding: 0.5rem 1rem; background: var(--border); cursor: pointer; }
61
+ .tab.active { background: var(--primary); color: white; }
62
+ .tabs { display: flex; gap: 0.25rem; margin-bottom: 1rem; }
63
+ .tab-content { display: none; }
64
+ .tab-content.active { display: block; }
65
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  </head>
67
  <body>
68
+ <!-- Left: Chat Section -->
69
+ <div class="section">
70
+ <h2>Chat with RAG</h2>
71
+ <div class="chat-window">
72
+ <div class="chat-messages" id="chat-messages"></div>
73
+ <div class="input-area">
74
+ <input type="text" id="user-input" placeholder="Ask a question..." />
75
+ <button onclick="sendMessage()">Send</button>
76
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  </div>
79
+
80
+ <!-- Right: Vector DB + Node Editor -->
81
+ <div class="section">
82
+ <div class="tabs">
83
+ <div class="tab active" onclick="switchTab('vector-db')">Vector DB</div>
84
+ <div class="tab" onclick="switchTab('node-editor')">Node Flow</div>
85
+ <div class="tab" onclick="switchTab('reranker')">Reranker</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  </div>
87
+ <div id="vector-db" class="tab-content active">
88
+ <h2>Vector DB Entries</h2>
89
+ <div class="input-area">
90
+ <input type="text" id="db-input" placeholder="Add text to vector DB..." />
91
+ <button onclick="addToVectorDB()">Add</button>
92
+ </div>
93
+ <table id="vector-table">
94
+ <thead>
95
+ <tr>
96
+ <th>Text</th>
97
+ <th>Metadata</th>
98
+ <th>Date</th>
99
+ <th>Score</th>
100
+ </tr>
101
+ </thead>
102
+ <tbody id="vector-entries"></tbody>
103
+ </table>
104
  </div>
105
+ <div id="node-editor" class="tab-content">
106
+ <h2>Node Flow Editor</h2>
107
+ <div id="node-editor-container" style="height: 100%;"></div>
 
 
 
108
  </div>
109
+ <div id="reranker" class="tab-content">
110
+ <h2>Reranker</h2>
111
+ <div id="reranker-results"></div>
 
 
112
  </div>
 
113
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
+ <script>
116
+ // --- State ---
117
+ let db;
118
+ let pipeline;
119
+ let currentTab = 'vector-db';
120
+ let vectorEntries = [];
121
+
122
+ // --- Init ---
123
+ async function init() {
124
+ // 1. Load Transformers.js pipeline for Qwen3.5-0.8B (quantized)
125
+ console.log("Loading Qwen3.5-0.8B (4-bit quantized)...");
126
+ pipeline = await transformers.pipeline(
127
+ 'text-generation',
128
+ 'Qwen/Qwen3.5-0.8B-4bit',
129
+ { device: 'webgpu' } // or 'webnn'/'cpu'
130
+ );
131
+ console.log("Qwen3.5-0.8B loaded!");
132
+
133
+ // 2. Initialize LanceDB (WASM)
134
+ console.log("Initializing LanceDB (WASM)...");
135
+ await lanceDb.init();
136
+ db = await lanceDb.connect("/lancedb"); // Uses IndexedDB
137
+ const table = await db.createTable("vectors", [
138
+ { vector: [], text: "", metadata: {}, date: new Date().toISOString() }
139
+ ]);
140
+ console.log("LanceDB ready!");
141
+
142
+ // 3. Load existing entries (if any)
143
+ const existing = await table.search().limit(100).execute();
144
+ vectorEntries = existing;
145
+ renderVectorTable();
146
+
147
+ // 4. Init Node Editor (placeholder)
148
+ initNodeEditor();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
+ // --- Chat ---
152
+ function sendMessage() {
153
+ const input = document.getElementById("user-input");
154
+ const message = input.value.trim();
155
+ if (!message) return;
156
+ addMessage("user", message);
157
+ input.value = "";
158
+
159
+ // Simulate RAG workflow
160
+ setTimeout(() => {
161
+ // 1. Embed query (placeholder: use actual embedding model)
162
+ const queryEmbedding = [0.1, 0.2, 0.3]; // Replace with real embedding
163
+
164
+ // 2. Search LanceDB
165
+ searchVectorDB(queryEmbedding).then(results => {
166
+ // 3. Rerank (placeholder)
167
+ const reranked = rerankResults(results);
168
+ addMessage("assistant", `Answer: ${generateResponse(reranked)}`);
169
+
170
+ // 4. Highlight top-K in table
171
+ highlightEntries(reranked.slice(0, 3).map(r => r.id));
172
+ });
173
+ }, 500);
174
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
+ function addMessage(sender, text) {
177
+ const chat = document.getElementById("chat-messages");
178
+ const msg = document.createElement("div");
179
+ msg.className = `message ${sender}`;
180
+ msg.textContent = text;
181
+ chat.appendChild(msg);
182
+ chat.scrollTop = chat.scrollHeight;
183
+ }
184
 
185
+ // --- Vector DB ---
186
+ async function addToVectorDB() {
187
+ const input = document.getElementById("db-input");
188
+ const text = input.value.trim();
189
+ if (!text) return;
190
+ input.value = "";
191
+
192
+ // 1. Embed text (placeholder)
193
+ const embedding = [0.4, 0.5, 0.6]; // Replace with real embedding model
194
+
195
+ // 2. Add to LanceDB
196
+ const table = await db.openTable("vectors");
197
+ const entry = {
198
+ vector: embedding,
199
+ text: text,
200
+ metadata: { source: "user" },
201
+ date: new Date().toISOString()
202
+ };
203
+ await table.add([entry]);
204
+ vectorEntries.push(entry);
205
+ renderVectorTable();
206
+ }
207
 
208
+ async function searchVectorDB(queryEmbedding, k = 3) {
209
+ const table = await db.openTable("vectors");
210
+ const results = await table
211
+ .search(queryEmbedding)
212
+ .limit(k)
213
+ .execute();
214
+ return results;
215
+ }
 
 
 
 
 
 
 
 
 
216
 
217
+ function renderVectorTable() {
218
+ const tbody = document.getElementById("vector-entries");
219
+ tbody.innerHTML = "";
220
+ vectorEntries.forEach((entry, i) => {
221
+ const row = document.createElement("tr");
222
+ row.innerHTML = `
223
+ <td>${entry.text.substring(0, 50)}...</td>
224
+ <td>${JSON.stringify(entry.metadata)}</td>
225
+ <td>${new Date(entry.date).toLocaleString()}</td>
226
+ <td>${entry.score || "N/A"}</td>
227
+ `;
228
+ row.id = `entry-${i}`;
229
+ tbody.appendChild(row);
230
+ });
231
+ }
232
 
233
+ function highlightEntries(ids) {
234
+ document.querySelectorAll("#vector-entries tr").forEach((row, i) => {
235
+ row.classList.toggle("highlight", ids.includes(i));
236
+ });
237
+ }
 
 
 
 
 
 
238
 
239
+ // --- Reranker (Placeholder) ---
240
+ function rerankResults(results) {
241
+ // Replace with actual reranker model
242
+ return results.map((r, i) => ({ ...r, score: 1 - (i * 0.1) }));
243
+ }
244
 
245
+ // --- Node Editor (Placeholder) ---
246
+ function initNodeEditor() {
247
+ const container = document.getElementById("node-editor-container");
248
+ container.innerHTML = `
249
+ <div style="padding: 1rem; background: #f1f5f9; border-radius: 0.25rem;">
250
+ <p>Node flow editor will go here. Use <a href="https://xyflow.com/" target="_blank">xyflow</a> or a custom SVG-based solution.</p>
251
+ <p>Example nodes:</p>
252
+ <ul>
253
+ <li>🔹 Embedding Model (BAAI/bge-small-en-v1.5)</li>
254
+ <li>📊 Vector DB (LanceDB WASM)</li>
255
+ <li>🔄 Reranker (BAAI/bge-reranker-base)</li>
256
+ <li>💬 LLM (Qwen3.5-0.8B)</li>
257
+ </ul>
258
+ </div>
259
+ `;
260
+ }
261
 
262
+ // --- Generation (Placeholder) ---
263
+ async function generateResponse(context) {
264
+ // Replace with actual Qwen3.5-0.8B generation
265
+ const prompt = `Context: ${context.map(c => c.text).join("\n")}\n\nAnswer:`;
266
+ const output = await pipeline(prompt, {
267
+ max_new_tokens: 200,
268
+ temperature: 0.7,
269
+ });
270
+ return output[0].generated_text;
271
+ }
272
 
273
+ // --- Tabs ---
274
+ function switchTab(tabName) {
275
+ document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"));
276
+ document.querySelectorAll(".tab-content").forEach(c => c.classList.remove("active"));
277
+ document.getElementById(tabName).classList.add("active");
278
+ event.target.classList.add("active");
279
+ currentTab = tabName;
280
+ }
281
 
282
+ // --- Start App ---
283
+ init().catch(console.error);
284
+ </script>
285
  </body>
286
  </html>