natbutter commited on
Commit
9c40bae
·
1 Parent(s): 50f04d1

first commit

Browse files
Files changed (3) hide show
  1. index.html +325 -15
  2. index.js +96 -74
  3. style.css +1190 -36
index.html CHANGED
@@ -3,27 +3,337 @@
3
 
4
  <head>
5
  <meta charset="UTF-8" />
 
 
 
 
 
 
 
 
 
 
 
6
  <link rel="stylesheet" href="style.css" />
7
 
8
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
9
- <title>Transformers.js - Object Detection</title>
 
 
 
 
 
 
10
  </head>
11
 
12
  <body>
13
- <h1>Object Detection w/ 🤗 Transformers.js</h1>
14
- <label id="container" for="upload">
15
- <svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
16
- <path fill="#000"
17
- d="M3.5 24.3a3 3 0 0 1-1.9-.8c-.5-.5-.8-1.2-.8-1.9V2.9c0-.7.3-1.3.8-1.9.6-.5 1.2-.7 2-.7h18.6c.7 0 1.3.2 1.9.7.5.6.7 1.2.7 2v18.6c0 .7-.2 1.4-.7 1.9a3 3 0 0 1-2 .8H3.6Zm0-2.7h18.7V2.9H3.5v18.7Zm2.7-2.7h13.3c.3 0 .5 0 .6-.3v-.7l-3.7-5a.6.6 0 0 0-.6-.2c-.2 0-.4 0-.5.3l-3.5 4.6-2.4-3.3a.6.6 0 0 0-.6-.3c-.2 0-.4.1-.5.3l-2.7 3.6c-.1.2-.2.4 0 .7.1.2.3.3.6.3Z">
18
- </path>
19
- </svg>
20
- Click to upload image
21
- <label id="example">(or try example)</label>
22
- </label>
23
- <label id="status">Loading model...</label>
24
- <input id="upload" type="file" accept="image/*" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- <script src="index.js" type="module"></script>
 
27
  </body>
28
 
29
  </html>
 
3
 
4
  <head>
5
  <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Teachable LLM - In-Browser Transfer Learning w/ Transformers.js</title>
8
+ <meta name="description"
9
+ content="Interactive demonstration of in-browser text classification using Transformers.js feature extraction and k-Nearest Neighbors with customizable model selection." />
10
+
11
+ <!-- Google Fonts: Google Sans, Google Sans Text, Roboto, Roboto Mono -->
12
+ <link rel="preconnect" href="https://fonts.googleapis.com">
13
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14
+ <link
15
+ href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&family=Google+Sans+Text:wght@400;500;700&family=Roboto:wght@400;500;700&family=Roboto+Mono:wght@400;500&display=swap"
16
+ rel="stylesheet">
17
  <link rel="stylesheet" href="style.css" />
18
 
19
+ <!-- Importmap for @xenova/transformers browser module resolution -->
20
+ <script type="importmap">
21
+ {
22
+ "imports": {
23
+ "@xenova/transformers": "https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2"
24
+ }
25
+ }
26
+ </script>
27
  </head>
28
 
29
  <body>
30
+ <!-- Top Google Four-Color Accent Bar -->
31
+ <div class="google-color-bar">
32
+ <div class="bar-blue"></div>
33
+ <div class="bar-red"></div>
34
+ <div class="bar-yellow"></div>
35
+ <div class="bar-green"></div>
36
+ </div>
37
+
38
+ <div class="app-container">
39
+ <!-- Header -->
40
+ <header class="app-header">
41
+ <div class="brand">
42
+ <div class="brand-icon">
43
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none">
44
+ <path
45
+ d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"
46
+ fill="#4285F4" />
47
+ <circle cx="12" cy="8" r="2.5" fill="#EA4335" />
48
+ <circle cx="8" cy="14" r="2.5" fill="#FBBC04" />
49
+ <circle cx="16" cy="14" r="2.5" fill="#34A853" />
50
+ </svg>
51
+ </div>
52
+ <div>
53
+ <h1 class="brand-title">Teachable LLM</h1>
54
+ <p class="brand-subtitle">Transfer Learning via Transformers.js & k-NN in WebAssembly</p>
55
+ </div>
56
+ </div>
57
+
58
+ <!-- Model Selector & Status Controls -->
59
+ <div class="header-controls">
60
+ <div class="model-selector-card">
61
+ <label for="model-select">Transformer Model:</label>
62
+ <select id="model-select" class="model-select">
63
+ <option value="Xenova/all-MiniLM-L6-v2" selected>all-MiniLM-L6-v2 (384-d, ~23MB - Default)</option>
64
+ <option value="Xenova/all-mpnet-base-v2">all-mpnet-base-v2 (768-d, ~110MB - High Accuracy)</option>
65
+ <option value="Xenova/bge-small-en-v1.5">bge-small-en-v1.5 (384-d, ~67MB - BAAI BGE)</option>
66
+ <option value="Xenova/gte-small">gte-small (384-d, ~67MB - General Embeddings)</option>
67
+ <option value="Xenova/distilbert-base-uncased">distilbert-base-uncased (768-d, ~67MB - DistilBERT)</option>
68
+ </select>
69
+ </div>
70
+
71
+ <!-- Model Load Status Badge -->
72
+ <div class="status-card">
73
+ <div class="status-indicator">
74
+ <span id="status-dot" class="status-dot loading"></span>
75
+ <span id="status-text">Initializing pipeline...</span>
76
+ </div>
77
+ <div id="progress-container" class="progress-container" style="display: none;">
78
+ <div id="progress-bar" class="progress-bar" style="width: 0%;"></div>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </header>
83
+
84
+ <!-- Concept Overview Hero & Pipeline Stepper Card -->
85
+ <section class="hero-card">
86
+ <div class="hero-content">
87
+ <div class="hero-header-row">
88
+ <div>
89
+ <h2>Interactive Architecture Pipeline</h2>
90
+ <p class="hero-subtitle">Explore how Transformers convert text into high-dimensional vectors and perform
91
+ transfer learning in-browser.</p>
92
+ </div>
93
+ <button type="button" id="animate-pipeline-btn" class="btn btn-secondary btn-sm">
94
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" style="margin-right:4px;">
95
+ <polygon points="5,3 19,12 5,21" fill="currentColor" />
96
+ </svg>
97
+ Animate Forward Pass
98
+ </button>
99
+ </div>
100
+
101
+ <div class="stepper-grid">
102
+ <div class="stepper-step active" data-step="1">
103
+ <div class="step-num step-blue">1</div>
104
+ <div class="step-details">
105
+ <h3>Tokenization</h3>
106
+ <p>Subword splitting & position IDs</p>
107
+ </div>
108
+ </div>
109
+ <div class="stepper-step" data-step="2">
110
+ <div class="step-num step-red">2</div>
111
+ <div class="step-details">
112
+ <h3>Self-Attention</h3>
113
+ <p>Multi-Head token interaction</p>
114
+ </div>
115
+ </div>
116
+ <div class="stepper-step" data-step="3">
117
+ <div class="step-num step-yellow">3</div>
118
+ <div class="step-details">
119
+ <h3>Mean Pooling</h3>
120
+ <p>Dense vector fingerprint</p>
121
+ </div>
122
+ </div>
123
+ <div class="stepper-step" data-step="4">
124
+ <div class="step-num step-green">4</div>
125
+ <div class="step-details">
126
+ <h3>2D Space & k-NN</h3>
127
+ <p>PCA Manifold & Class Regions</p>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </section>
133
+
134
+ <!-- Main Content Grid -->
135
+ <main class="main-grid">
136
+ <!-- Left Column: Interactive Controls -->
137
+ <div class="left-column">
138
+ <!-- Training Dataset Management Card -->
139
+ <section class="panel-card">
140
+ <div class="panel-header">
141
+ <h2>1. Training Dataset</h2>
142
+ <span id="dataset-count" class="badge-neutral">0 examples</span>
143
+ </div>
144
+
145
+ <!-- Add Example Form -->
146
+ <form id="add-example-form" class="add-form">
147
+ <div class="form-group">
148
+ <label for="example-text">Text Example</label>
149
+ <input type="text" id="example-text" placeholder="e.g. The service was fast and super friendly!"
150
+ required />
151
+ </div>
152
+
153
+ <div class="form-group">
154
+ <label for="example-label">Class Label</label>
155
+ <div class="label-input-group">
156
+ <input type="text" id="example-label" placeholder="e.g. positive" required />
157
+ <div class="preset-badges">
158
+ <button type="button" class="preset-badge badge-positive"
159
+ data-label="positive">positive</button>
160
+ <button type="button" class="preset-badge badge-negative"
161
+ data-label="negative">negative</button>
162
+ <button type="button" class="preset-badge badge-neutral"
163
+ data-label="neutral">neutral</button>
164
+ </div>
165
+ </div>
166
+ </div>
167
+
168
+ <button type="submit" class="btn btn-secondary">+ Add Example</button>
169
+ </form>
170
+
171
+ <!-- Dataset Example List -->
172
+ <div class="dataset-section">
173
+ <div class="dataset-actions">
174
+ <h3>Current Memory Dataset</h3>
175
+ <div class="btn-group">
176
+ <button type="button" id="reset-demo-btn" class="btn btn-sm btn-outline">Reset Demo
177
+ Dataset</button>
178
+ <button type="button" id="clear-dataset-btn" class="btn btn-sm btn-outline-danger">Clear
179
+ All</button>
180
+ </div>
181
+ </div>
182
+ <div id="dataset-list" class="dataset-list">
183
+ <!-- Populated dynamically by demo.js -->
184
+ </div>
185
+ </div>
186
+ </section>
187
+
188
+ <!-- Inference & Prediction Card -->
189
+ <section class="panel-card">
190
+ <div class="panel-header">
191
+ <h2>2. Run Inference</h2>
192
+ </div>
193
+
194
+ <div class="inference-form">
195
+ <div class="form-group">
196
+ <label for="test-text">Test Input Sentence</label>
197
+ <textarea id="test-text" rows="2"
198
+ placeholder="Type any sentence to test classification..."></textarea>
199
+ </div>
200
+
201
+ <div class="form-row">
202
+ <div class="form-group inline">
203
+ <label for="k-value">Neighbors (k):</label>
204
+ <input type="number" id="k-value" value="3" min="1" max="10" />
205
+ </div>
206
+ <button type="button" id="predict-btn" class="btn btn-primary">Classify Text</button>
207
+ </div>
208
+ </div>
209
+
210
+ <!-- Prediction Result Display -->
211
+ <div id="result-box" class="result-box" style="display: none;">
212
+ <div class="result-header">
213
+ <div class="result-main">
214
+ <span class="result-title">Predicted Class:</span>
215
+ <div id="predicted-label"></div>
216
+ </div>
217
+ <div class="confidence-box">
218
+ <span class="confidence-label">Confidence: <strong id="confidence-val">--</strong></span>
219
+ <div class="confidence-track">
220
+ <div id="confidence-fill" class="confidence-fill" style="width: 0%;"></div>
221
+ </div>
222
+ </div>
223
+ </div>
224
+
225
+ <div id="vector-preview" class="vector-preview"></div>
226
+
227
+ <!-- Top k Neighbors Breakdown -->
228
+ <div class="neighbors-section">
229
+ <h4>Nearest Neighbor Votes</h4>
230
+ <div id="neighbors-list" class="neighbors-list"></div>
231
+ </div>
232
+ </div>
233
+ </section>
234
+ </div>
235
+
236
+ <!-- Right Column: Architecture & Logs -->
237
+ <div class="right-column">
238
+ <!-- Activity Log Card -->
239
+ <section class="panel-card log-panel">
240
+ <div class="panel-header">
241
+ <h2>Activity Console</h2>
242
+ <span class="live-tag">LIVE</span>
243
+ </div>
244
+ <div id="log-output" class="log-output">
245
+ <!-- Streamed console output -->
246
+ </div>
247
+ </section>
248
+
249
+ <!-- Architecture Summary Card -->
250
+ <section class="panel-card info-card">
251
+ <div class="panel-header">
252
+ <h2>Architecture Specs</h2>
253
+ </div>
254
+ <ul class="specs-list">
255
+ <li><strong>Active Model:</strong> <code id="spec-model-name">Xenova/all-MiniLM-L6-v2</code></li>
256
+ <li><strong>Runtime:</strong> ONNX Runtime Web via Transformers.js</li>
257
+ <li><strong>Embedding Dimension:</strong> <span id="spec-embed-dim">384</span></li>
258
+ <li><strong>Pooling Strategy:</strong> Mean pooling with L2 normalization</li>
259
+ <li><strong>Classifier:</strong> k-Nearest Neighbors (Euclidean Distance)</li>
260
+ </ul>
261
+ </section>
262
+ </div>
263
+ </main>
264
+
265
+ <!-- Full-Width Interactive Architecture & Storytelling Visualizer Section -->
266
+ <section class="viz-storytelling-section">
267
+ <div class="section-title-row">
268
+ <div>
269
+ <h2>Deep Learning Visual Explorers</h2>
270
+ <p>Interactive step-by-step breakdown inspired by Transformer Explainer & GAN Lab</p>
271
+ </div>
272
+ </div>
273
+
274
+ <div class="viz-grid">
275
+ <!-- Viz 1: Subword Tokenizer -->
276
+ <div class="panel-card viz-card" id="viz-step-1">
277
+ <div class="panel-header">
278
+ <h2>Stage 1: Subword Tokenization</h2>
279
+ <span class="badge label-custom">WordPiece / BPE</span>
280
+ </div>
281
+ <p class="viz-desc">Input text is mapped into discrete subword token IDs and positional indices.</p>
282
+ <div id="tokenizer-viz-container" class="viz-container"></div>
283
+ </div>
284
+
285
+ <!-- Viz 2: Self-Attention Matrix -->
286
+ <div class="panel-card viz-card" id="viz-step-2">
287
+ <div class="panel-header">
288
+ <h2>Stage 2: Self-Attention Matrix & QKV Arcs</h2>
289
+ <span class="badge badge-negative">Softmax(QK<sup>T</sup>/&radic;d)V</span>
290
+ </div>
291
+ <p class="viz-desc">Tokens attend to each other across Multi-Head Self-Attention layers to capture
292
+ contextual meaning.</p>
293
+ <div id="attn-arcs-container" class="attn-arcs-wrapper"></div>
294
+ <div id="attention-viz-container" class="viz-container"></div>
295
+ </div>
296
+
297
+ <!-- Viz 3: Pooling & Embedding Fingerprint Barcode -->
298
+ <div class="panel-card viz-card" id="viz-step-3">
299
+ <div class="panel-header">
300
+ <h2>Stage 3: Sentence Embedding Fingerprint</h2>
301
+ <span class="badge badge-neutral">Mean Pooling & L2 Norm</span>
302
+ </div>
303
+ <p class="viz-desc">Token vectors are collapsed into a dense 1D fingerprint representing the entire
304
+ sentence's semantics.</p>
305
+ <div id="pooling-viz-container" class="viz-container"></div>
306
+ </div>
307
+
308
+ <!-- Viz 4: 2D Embedding Space & Decision Boundary Canvas (GAN Lab style) -->
309
+ <div class="panel-card viz-card full-width-viz" id="viz-step-4">
310
+ <div class="panel-header">
311
+ <div>
312
+ <h2>Stage 4: 2D Embedding Space & k-NN Decision Boundaries</h2>
313
+ <p class="viz-desc">High-dimensional embeddings projected to 2D via PCA. Background shows memory
314
+ class zones; click canvas to query!</p>
315
+ </div>
316
+ <div id="pca-legend" class="pca-legend"></div>
317
+ </div>
318
+
319
+ <div class="canvas-wrapper">
320
+ <canvas id="pca-canvas" width="760" height="420"></canvas>
321
+ <div class="canvas-hint-overlay">
322
+ <span>💡 Click anywhere on canvas to test a 2D vector coordinate query</span>
323
+ </div>
324
+ </div>
325
+ </div>
326
+ </div>
327
+ </section>
328
+
329
+ <footer class="app-footer">
330
+ <p>Built with <a href="https://github.com/xenova/transformers.js" target="_blank" rel="noopener">Transformers.js</a>
331
+ </p>
332
+ </footer>
333
+ </div>
334
 
335
+ <!-- Main Module Script -->
336
+ <script type="module" src="demo.js"></script>
337
  </body>
338
 
339
  </html>
index.js CHANGED
@@ -1,76 +1,98 @@
1
- import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.7.6';
2
-
3
- // Reference the elements that we will need
4
- const status = document.getElementById('status');
5
- const fileUpload = document.getElementById('upload');
6
- const imageContainer = document.getElementById('container');
7
- const example = document.getElementById('example');
8
-
9
- const EXAMPLE_URL = 'https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/city-streets.jpg';
10
-
11
- // Create a new object detection pipeline
12
- status.textContent = 'Loading model...';
13
- const detector = await pipeline('object-detection', 'Xenova/detr-resnet-50');
14
- status.textContent = 'Ready';
15
-
16
- example.addEventListener('click', (e) => {
17
- e.preventDefault();
18
- detect(EXAMPLE_URL);
19
- });
20
-
21
- fileUpload.addEventListener('change', function (e) {
22
- const file = e.target.files[0];
23
- if (!file) {
24
- return;
25
  }
26
 
27
- const reader = new FileReader();
28
-
29
- // Set up a callback when the file is loaded
30
- reader.onload = e2 => detect(e2.target.result);
31
-
32
- reader.readAsDataURL(file);
33
- });
34
-
35
-
36
- // Detect objects in the image
37
- async function detect(img) {
38
- imageContainer.innerHTML = '';
39
- imageContainer.style.backgroundImage = `url(${img})`;
40
-
41
- status.textContent = 'Analysing...';
42
- const output = await detector(img, {
43
- threshold: 0.5,
44
- percentage: true,
45
- });
46
- status.textContent = '';
47
- output.forEach(renderBox);
48
- }
49
-
50
- // Render a bounding box and label on the image
51
- function renderBox({ box, label }) {
52
- const { xmax, xmin, ymax, ymin } = box;
53
-
54
- // Generate a random color for the box
55
- const color = '#' + Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, 0);
56
-
57
- // Draw the box
58
- const boxElement = document.createElement('div');
59
- boxElement.className = 'bounding-box';
60
- Object.assign(boxElement.style, {
61
- borderColor: color,
62
- left: 100 * xmin + '%',
63
- top: 100 * ymin + '%',
64
- width: 100 * (xmax - xmin) + '%',
65
- height: 100 * (ymax - ymin) + '%',
66
- })
67
-
68
- // Draw label
69
- const labelElement = document.createElement('span');
70
- labelElement.textContent = label;
71
- labelElement.className = 'bounding-box-label';
72
- labelElement.style.backgroundColor = color;
73
-
74
- boxElement.appendChild(labelElement);
75
- imageContainer.appendChild(boxElement);
76
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { pipeline, env } from '@xenova/transformers';
2
+
3
+ // Setup environment for browser execution
4
+ env.allowLocalModels = false;
5
+ env.useBrowserCache = true;
6
+
7
+ export class TeachableTransformer {
8
+ constructor(modelName = 'Xenova/all-MiniLM-L6-v2') {
9
+ this.modelName = modelName;
10
+ this.extractor = null;
11
+ this.dataset = []; // Array of { embedding: number[], label: string, text: string }
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  }
13
 
14
+ // 1. Load the "headless" feature extraction pipeline
15
+ async load(progressCallback = null) {
16
+ console.log(`Loading feature extractor: ${this.modelName}...`);
17
+ // This will download the ~22MB model on the first run and cache it in the browser
18
+ this.extractor = await pipeline('feature-extraction', this.modelName, {
19
+ progress_callback: progressCallback
20
+ });
21
+ console.log('Model loaded successfully!');
22
+ }
23
+
24
+ // 2. Generate a dense mathematical embedding for a given text
25
+ async getEmbedding(text) {
26
+ if (!this.extractor) throw new Error("Model not loaded yet. Call load() first.");
27
+
28
+ // Pass text through the Transformer model to get the feature vector
29
+ const output = await this.extractor(text, {
30
+ pooling: 'mean', // Average the token embeddings into a single sentence embedding
31
+ normalize: true, // Normalize the vector length
32
+ });
33
+
34
+ // Convert Float32Array to standard JavaScript Array
35
+ return Array.from(output.data);
36
+ }
37
+
38
+ // 3. "Training": Add an example to the memory dataset
39
+ async addExample(text, label) {
40
+ console.log(`Extracting features for class '${label}': "${text}"`);
41
+ const embedding = await this.getEmbedding(text);
42
+ this.dataset.push({ text, label, embedding });
43
+ return { text, label, embedding };
44
+ }
45
+
46
+ // Helper: Calculate Euclidean distance between two vectors
47
+ calculateDistance(vecA, vecB) {
48
+ return Math.sqrt(
49
+ vecA.reduce((sum, val, i) => sum + Math.pow(val - vecB[i], 2), 0)
50
+ );
51
+ }
52
+
53
+ // 4. "Inference": Predict the label for a new text using k-Nearest Neighbors
54
+ async predict(text, k = 3) {
55
+ if (this.dataset.length === 0) {
56
+ throw new Error("Dataset is empty. Add examples before predicting.");
57
+ }
58
+
59
+ const inputEmbedding = await this.getEmbedding(text);
60
+
61
+ // Calculate distance from the new text to all examples in our dataset
62
+ const distances = this.dataset.map(example => ({
63
+ label: example.label,
64
+ text: example.text,
65
+ distance: this.calculateDistance(inputEmbedding, example.embedding)
66
+ }));
67
+
68
+ // Sort by distance (ascending) to find the nearest neighbors
69
+ distances.sort((a, b) => a.distance - b.distance);
70
+
71
+ // Get the top 'k' nearest neighbors
72
+ const effectiveK = Math.min(k, distances.length);
73
+ const nearestNeighbors = distances.slice(0, effectiveK);
74
+
75
+ // Count the frequency (votes) of each label among the neighbors
76
+ const labelCounts = {};
77
+ for (const neighbor of nearestNeighbors) {
78
+ labelCounts[neighbor.label] = (labelCounts[neighbor.label] || 0) + 1;
79
+ }
80
+
81
+ // Find the label with the highest vote count
82
+ let bestLabel = null;
83
+ let maxCount = -1;
84
+ for (const [label, count] of Object.entries(labelCounts)) {
85
+ if (count > maxCount) {
86
+ bestLabel = label;
87
+ maxCount = count;
88
+ }
89
+ }
90
+
91
+ return {
92
+ predictedLabel: bestLabel,
93
+ confidence: maxCount / effectiveK,
94
+ nearestNeighbors: nearestNeighbors,
95
+ inputEmbedding: inputEmbedding
96
+ };
97
+ }
98
+ }
style.css CHANGED
@@ -1,76 +1,1230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  * {
2
  box-sizing: border-box;
3
  padding: 0;
4
  margin: 0;
5
- font-family: sans-serif;
6
  }
7
 
8
- html,
9
  body {
10
- height: 100%;
 
 
 
 
11
  }
12
 
13
- body {
14
- padding: 32px;
 
 
 
 
 
 
 
 
15
  }
16
 
17
- body,
18
- #container {
 
 
 
 
 
 
 
19
  display: flex;
20
  flex-direction: column;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  justify-content: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  align-items: center;
 
 
23
  }
24
 
25
- #container {
26
- position: relative;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  gap: 0.4rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- width: 640px;
30
- height: 640px;
31
- max-width: 100%;
32
- max-height: 100%;
33
 
34
- border: 2px dashed #D1D5DB;
35
- border-radius: 0.75rem;
 
 
36
  overflow: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  cursor: pointer;
38
- margin: 1rem;
 
 
 
 
 
39
 
40
- background-size: 100% 100%;
41
- background-position: center;
42
- background-repeat: no-repeat;
43
- font-size: 18px;
44
  }
45
 
46
- #upload {
47
- display: none;
 
 
 
 
 
 
 
 
 
 
 
48
  }
49
 
50
- svg {
51
- pointer-events: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  }
53
 
54
- #example {
55
- font-size: 14px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  text-decoration: underline;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  cursor: pointer;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  }
59
 
60
- #example:hover {
61
- color: #2563EB;
 
 
 
62
  }
63
 
64
- .bounding-box {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  position: absolute;
66
- box-sizing: border-box;
67
- border: solid 2px;
 
 
 
 
 
 
 
 
 
68
  }
69
 
70
- .bounding-box-label {
71
- color: white;
72
  position: absolute;
73
- font-size: 12px;
74
- margin: -16px 0 0 -2px;
75
- padding: 1px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  }
 
1
+ :root {
2
+ /* Google Brand Palette */
3
+ --google-blue: #1a73e8;
4
+ --google-blue-hover: #1765cc;
5
+ --google-blue-light: #e8f0fe;
6
+ --google-blue-border: #aecbfa;
7
+
8
+ --google-red: #ea4335;
9
+ --google-red-hover: #d93025;
10
+ --google-red-light: #fce8e6;
11
+ --google-red-border: #f28b82;
12
+
13
+ --google-yellow: #fbbc04;
14
+ --google-yellow-dark: #b06000;
15
+ --google-yellow-light: #fef7e0;
16
+ --google-yellow-border: #fde293;
17
+
18
+ --google-green: #34a853;
19
+ --google-green-dark: #137333;
20
+ --google-green-light: #e6f4ea;
21
+ --google-green-border: #a8dab5;
22
+
23
+ /* Google Workspace & Flat UI Surface Tokens */
24
+ --bg-color: #f8f9fa;
25
+ --surface-color: #ffffff;
26
+ --surface-variant: #f1f3f4;
27
+ --border-color: #dadce0;
28
+
29
+ --text-primary: #202124;
30
+ --text-secondary: #5f6368;
31
+ --text-tertiary: #80868b;
32
+
33
+ /* Flat minimal shadows */
34
+ --shadow-sm: none;
35
+ --shadow-md: 0 1px 2px rgba(60,64,67,0.08);
36
+ }
37
+
38
  * {
39
  box-sizing: border-box;
40
  padding: 0;
41
  margin: 0;
42
+ font-family: 'Google Sans', 'Google Sans Text', 'Roboto', system-ui, -apple-system, sans-serif;
43
  }
44
 
 
45
  body {
46
+ background-color: var(--bg-color);
47
+ color: var(--text-primary);
48
+ min-height: 100vh;
49
+ padding: 0 0 2rem 0;
50
+ line-height: 1.5;
51
  }
52
 
53
+ /* Google 4-Color Accent Top Bar */
54
+ .google-color-bar {
55
+ display: flex;
56
+ height: 4px;
57
+ width: 100%;
58
+ }
59
+
60
+ .google-color-bar div {
61
+ flex: 1;
62
+ height: 100%;
63
  }
64
 
65
+ .bar-blue { background-color: var(--google-blue); }
66
+ .bar-red { background-color: var(--google-red); }
67
+ .bar-yellow { background-color: var(--google-yellow); }
68
+ .bar-green { background-color: var(--google-green); }
69
+
70
+ .app-container {
71
+ max-width: 1240px;
72
+ margin: 1.5rem auto 0 auto;
73
+ padding: 0 1.5rem;
74
  display: flex;
75
  flex-direction: column;
76
+ gap: 1.5rem;
77
+ }
78
+
79
+ /* Header */
80
+ .app-header {
81
+ display: flex;
82
+ justify-content: space-between;
83
+ align-items: center;
84
+ flex-wrap: wrap;
85
+ gap: 1rem;
86
+ padding-bottom: 1rem;
87
+ border-bottom: 1px solid var(--border-color);
88
+ }
89
+
90
+ .brand {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 0.85rem;
94
+ }
95
+
96
+ .brand-icon {
97
+ width: 44px;
98
+ height: 44px;
99
+ border-radius: 4px;
100
+ background: #ffffff;
101
+ display: flex;
102
+ align-items: center;
103
  justify-content: center;
104
+ border: 1px solid var(--border-color);
105
+ }
106
+
107
+ .brand-title {
108
+ font-family: 'Google Sans', sans-serif;
109
+ font-size: 1.6rem;
110
+ font-weight: 700;
111
+ color: var(--text-primary);
112
+ letter-spacing: -0.02em;
113
+ }
114
+
115
+ .brand-subtitle {
116
+ font-family: 'Google Sans Text', sans-serif;
117
+ font-size: 0.85rem;
118
+ color: var(--text-secondary);
119
+ }
120
+
121
+ .header-controls {
122
+ display: flex;
123
  align-items: center;
124
+ gap: 1rem;
125
+ flex-wrap: wrap;
126
  }
127
 
128
+ /* Model Selector Card */
129
+ .model-selector-card {
130
+ background: var(--surface-color);
131
+ border: 1px solid var(--border-color);
132
+ padding: 0.65rem 1.1rem;
133
+ border-radius: 4px;
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 0.6rem;
137
+ }
138
+
139
+ .model-selector-card label {
140
+ font-size: 0.85rem;
141
+ font-weight: 500;
142
+ color: var(--text-secondary);
143
+ white-space: nowrap;
144
+ }
145
+
146
+ .model-select {
147
+ background: var(--surface-color);
148
+ border: 1px solid var(--border-color);
149
+ border-radius: 4px;
150
+ padding: 0.35rem 0.6rem;
151
+ font-family: 'Google Sans Text', sans-serif;
152
+ font-size: 0.85rem;
153
+ color: var(--text-primary);
154
+ outline: none;
155
+ cursor: pointer;
156
+ transition: border-color 0.2s ease;
157
+ }
158
+
159
+ .model-select:focus {
160
+ border-color: var(--google-blue);
161
+ box-shadow: 0 0 0 1px var(--google-blue);
162
+ }
163
+
164
+ /* Status Card */
165
+ .status-card {
166
+ background: var(--surface-color);
167
+ border: 1px solid var(--border-color);
168
+ padding: 0.65rem 1.1rem;
169
+ border-radius: 4px;
170
+ display: flex;
171
+ flex-direction: column;
172
  gap: 0.4rem;
173
+ min-width: 250px;
174
+ }
175
+
176
+ .status-indicator {
177
+ display: flex;
178
+ align-items: center;
179
+ gap: 0.6rem;
180
+ font-size: 0.85rem;
181
+ font-weight: 500;
182
+ color: var(--text-secondary);
183
+ }
184
+
185
+ .status-dot {
186
+ width: 10px;
187
+ height: 10px;
188
+ border-radius: 2px;
189
+ display: inline-block;
190
+ }
191
+
192
+ .status-dot.loading {
193
+ background-color: var(--google-yellow);
194
+ animation: pulse 1.5s infinite;
195
+ }
196
+
197
+ .status-dot.ready {
198
+ background-color: var(--google-green);
199
+ }
200
+
201
+ .status-dot.error {
202
+ background-color: var(--google-red);
203
+ }
204
 
205
+ @keyframes pulse {
206
+ 0%, 100% { opacity: 1; }
207
+ 50% { opacity: 0.4; }
208
+ }
209
 
210
+ .progress-container {
211
+ height: 4px;
212
+ background: var(--surface-variant);
213
+ border-radius: 2px;
214
  overflow: hidden;
215
+ }
216
+
217
+ .progress-bar {
218
+ height: 100%;
219
+ background: var(--google-blue);
220
+ transition: width 0.3s ease;
221
+ }
222
+
223
+ /* Hero Card */
224
+ .hero-card {
225
+ background: var(--surface-color);
226
+ border: 1px solid var(--border-color);
227
+ border-radius: 4px;
228
+ padding: 1.25rem 1.5rem;
229
+ }
230
+
231
+ .hero-card h2 {
232
+ font-family: 'Google Sans', sans-serif;
233
+ font-size: 0.9rem;
234
+ font-weight: 700;
235
+ text-transform: uppercase;
236
+ letter-spacing: 0.06em;
237
+ color: var(--text-secondary);
238
+ margin-bottom: 0.85rem;
239
+ }
240
+
241
+ .concept-grid {
242
+ display: grid;
243
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
244
+ gap: 1.25rem;
245
+ }
246
+
247
+ .concept-step {
248
+ display: flex;
249
+ gap: 0.85rem;
250
+ align-items: flex-start;
251
+ }
252
+
253
+ .step-num {
254
+ width: 26px;
255
+ height: 26px;
256
+ border-radius: 4px;
257
+ font-weight: 700;
258
+ font-size: 0.85rem;
259
+ display: flex;
260
+ align-items: center;
261
+ justify-content: center;
262
+ flex-shrink: 0;
263
+ }
264
+
265
+ .step-blue { background: var(--google-blue-light); color: var(--google-blue); }
266
+ .step-red { background: var(--google-red-light); color: var(--google-red); }
267
+ .step-green { background: var(--google-green-light); color: var(--google-green); }
268
+
269
+ .concept-step h3 {
270
+ font-family: 'Google Sans', sans-serif;
271
+ font-size: 0.95rem;
272
+ font-weight: 600;
273
+ color: var(--text-primary);
274
+ margin-bottom: 0.2rem;
275
+ }
276
+
277
+ .concept-step p {
278
+ font-size: 0.82rem;
279
+ color: var(--text-secondary);
280
+ }
281
+
282
+ .concept-step code {
283
+ font-family: 'Roboto Mono', monospace;
284
+ font-size: 0.78rem;
285
+ background: var(--surface-variant);
286
+ padding: 0.1rem 0.35rem;
287
+ border-radius: 2px;
288
+ color: var(--text-primary);
289
+ }
290
+
291
+ /* Main Grid */
292
+ .main-grid {
293
+ display: grid;
294
+ grid-template-columns: 1.7fr 1fr;
295
+ gap: 1.5rem;
296
+ }
297
+
298
+ @media (max-width: 900px) {
299
+ .main-grid {
300
+ grid-template-columns: 1fr;
301
+ }
302
+ }
303
+
304
+ .left-column, .right-column {
305
+ display: flex;
306
+ flex-direction: column;
307
+ gap: 1.5rem;
308
+ }
309
+
310
+ /* Panel Cards */
311
+ .panel-card {
312
+ background: var(--surface-color);
313
+ border: 1px solid var(--border-color);
314
+ border-radius: 4px;
315
+ padding: 1.5rem;
316
+ transition: border-color 0.2s ease;
317
+ }
318
+
319
+ .panel-card:hover {
320
+ border-color: #b0b5ba;
321
+ }
322
+
323
+ .panel-header {
324
+ display: flex;
325
+ justify-content: space-between;
326
+ align-items: center;
327
+ margin-bottom: 1.2rem;
328
+ }
329
+
330
+ .panel-header h2 {
331
+ font-family: 'Google Sans', sans-serif;
332
+ font-size: 1.15rem;
333
+ font-weight: 600;
334
+ color: var(--text-primary);
335
+ }
336
+
337
+ /* Forms & Inputs */
338
+ .add-form, .inference-form {
339
+ display: flex;
340
+ flex-direction: column;
341
+ gap: 1rem;
342
+ }
343
+
344
+ .form-group {
345
+ display: flex;
346
+ flex-direction: column;
347
+ gap: 0.4rem;
348
+ }
349
+
350
+ .form-group label {
351
+ font-size: 0.85rem;
352
+ font-weight: 500;
353
+ color: var(--text-secondary);
354
+ }
355
+
356
+ input[type="text"], input[type="number"], textarea {
357
+ background: var(--surface-color);
358
+ border: 1px solid var(--border-color);
359
+ border-radius: 4px;
360
+ padding: 0.65rem 0.85rem;
361
+ color: var(--text-primary);
362
+ font-size: 0.9rem;
363
+ outline: none;
364
+ transition: border-color 0.2s ease;
365
+ }
366
+
367
+ input[type="text"]:focus, input[type="number"]:focus, textarea:focus {
368
+ border-color: var(--google-blue);
369
+ box-shadow: 0 0 0 1px var(--google-blue);
370
+ }
371
+
372
+ .label-input-group {
373
+ display: flex;
374
+ flex-direction: column;
375
+ gap: 0.5rem;
376
+ }
377
+
378
+ .preset-badges {
379
+ display: flex;
380
+ gap: 0.5rem;
381
+ }
382
+
383
+ .preset-badge {
384
+ border: 1px solid transparent;
385
  cursor: pointer;
386
+ font-size: 0.78rem;
387
+ font-weight: 500;
388
+ padding: 0.25rem 0.65rem;
389
+ border-radius: 4px;
390
+ transition: background-color 0.15s ease;
391
+ }
392
 
393
+ .preset-badge:hover {
394
+ filter: brightness(0.95);
 
 
395
  }
396
 
397
+ /* Buttons */
398
+ .btn {
399
+ padding: 0.6rem 1.25rem;
400
+ border-radius: 4px;
401
+ font-family: 'Google Sans', sans-serif;
402
+ font-weight: 600;
403
+ font-size: 0.88rem;
404
+ cursor: pointer;
405
+ border: 1px solid transparent;
406
+ transition: background-color 0.2s ease, border-color 0.2s ease;
407
+ display: inline-flex;
408
+ align-items: center;
409
+ justify-content: center;
410
  }
411
 
412
+ .btn-primary {
413
+ background-color: var(--google-blue);
414
+ color: white;
415
+ }
416
+
417
+ .btn-primary:hover {
418
+ background-color: var(--google-blue-hover);
419
+ }
420
+
421
+ .btn-secondary {
422
+ background-color: var(--google-blue-light);
423
+ color: var(--google-blue);
424
+ border-color: var(--google-blue-border);
425
+ }
426
+
427
+ .btn-secondary:hover {
428
+ background-color: #d2e3fc;
429
+ }
430
+
431
+ .btn-sm {
432
+ padding: 0.35rem 0.85rem;
433
+ font-size: 0.78rem;
434
+ border-radius: 4px;
435
+ }
436
+
437
+ .btn-outline {
438
+ background: transparent;
439
+ border-color: var(--border-color);
440
+ color: var(--text-secondary);
441
+ }
442
+
443
+ .btn-outline:hover {
444
+ background-color: var(--surface-variant);
445
+ color: var(--text-primary);
446
+ }
447
+
448
+ .btn-outline-danger {
449
+ background: transparent;
450
+ border-color: var(--google-red-border);
451
+ color: var(--google-red);
452
+ }
453
+
454
+ .btn-outline-danger:hover {
455
+ background-color: var(--google-red-light);
456
+ }
457
+
458
+ .form-row {
459
+ display: flex;
460
+ justify-content: space-between;
461
+ align-items: center;
462
+ gap: 1rem;
463
+ }
464
+
465
+ .form-group.inline {
466
+ flex-direction: row;
467
+ align-items: center;
468
+ gap: 0.6rem;
469
+ }
470
+
471
+ .form-group.inline input {
472
+ width: 65px;
473
+ text-align: center;
474
+ }
475
+
476
+ /* Dataset List */
477
+ .dataset-section {
478
+ margin-top: 1.5rem;
479
+ padding-top: 1.2rem;
480
+ border-top: 1px solid var(--border-color);
481
+ }
482
+
483
+ .dataset-actions {
484
+ display: flex;
485
+ justify-content: space-between;
486
+ align-items: center;
487
+ margin-bottom: 1rem;
488
+ }
489
+
490
+ .dataset-actions h3 {
491
+ font-family: 'Google Sans', sans-serif;
492
+ font-size: 0.95rem;
493
+ font-weight: 600;
494
+ color: var(--text-primary);
495
+ }
496
+
497
+ .btn-group {
498
+ display: flex;
499
+ gap: 0.5rem;
500
+ }
501
+
502
+ .dataset-list {
503
+ display: grid;
504
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
505
+ gap: 0.85rem;
506
+ max-height: 280px;
507
+ overflow-y: auto;
508
+ padding-right: 0.3rem;
509
+ }
510
+
511
+ .empty-state {
512
+ grid-column: 1 / -1;
513
+ text-align: center;
514
+ padding: 2rem;
515
+ color: var(--text-secondary);
516
+ font-size: 0.85rem;
517
+ background: var(--surface-variant);
518
+ border-radius: 4px;
519
+ }
520
+
521
+ .example-card {
522
+ background: var(--surface-color);
523
+ border: 1px solid var(--border-color);
524
+ border-radius: 4px;
525
+ padding: 0.75rem 0.9rem;
526
+ display: flex;
527
+ flex-direction: column;
528
+ gap: 0.4rem;
529
+ position: relative;
530
+ }
531
+
532
+ .example-header {
533
+ display: flex;
534
+ justify-content: space-between;
535
+ align-items: center;
536
+ }
537
+
538
+ .example-text {
539
+ font-size: 0.85rem;
540
+ color: var(--text-primary);
541
+ }
542
+
543
+ .embedding-meta {
544
+ font-size: 0.72rem;
545
+ color: var(--text-tertiary);
546
+ }
547
+
548
+ .delete-btn {
549
+ background: none;
550
+ border: none;
551
+ color: var(--text-tertiary);
552
+ font-size: 1.2rem;
553
+ line-height: 1;
554
+ cursor: pointer;
555
+ padding: 0 0.2rem;
556
+ }
557
+
558
+ .delete-btn:hover {
559
+ color: var(--google-red);
560
+ }
561
+
562
+ /* Badges */
563
+ .badge {
564
+ display: inline-block;
565
+ padding: 0.25rem 0.65rem;
566
+ border-radius: 4px;
567
+ font-size: 0.75rem;
568
+ font-weight: 600;
569
+ text-transform: lowercase;
570
+ }
571
+
572
+ .badge.lg {
573
+ font-size: 0.95rem;
574
+ padding: 0.4rem 1rem;
575
+ border-radius: 4px;
576
+ }
577
+
578
+ .badge-positive, .label-positive {
579
+ background-color: var(--google-green-light);
580
+ color: var(--google-green-dark);
581
+ border: 1px solid var(--google-green-border);
582
+ }
583
+
584
+ .badge-negative, .label-negative {
585
+ background-color: var(--google-red-light);
586
+ color: var(--google-red-hover);
587
+ border: 1px solid var(--google-red-border);
588
+ }
589
+
590
+ .badge-neutral, .label-neutral {
591
+ background-color: var(--google-yellow-light);
592
+ color: var(--google-yellow-dark);
593
+ border: 1px solid var(--google-yellow-border);
594
+ }
595
+
596
+ .label-custom {
597
+ background-color: var(--google-blue-light);
598
+ color: var(--google-blue-hover);
599
+ border: 1px solid var(--google-blue-border);
600
+ }
601
+
602
+ /* Result Box */
603
+ .result-box {
604
+ margin-top: 1.2rem;
605
+ background: var(--surface-color);
606
+ border: 1px solid var(--google-blue-border);
607
+ border-radius: 4px;
608
+ padding: 1.2rem;
609
+ display: flex;
610
+ flex-direction: column;
611
+ gap: 1rem;
612
+ }
613
+
614
+ .result-header {
615
+ display: flex;
616
+ justify-content: space-between;
617
+ align-items: center;
618
+ flex-wrap: wrap;
619
+ gap: 1rem;
620
+ }
621
+
622
+ .result-main {
623
+ display: flex;
624
+ align-items: center;
625
+ gap: 0.8rem;
626
+ }
627
+
628
+ .result-title {
629
+ font-size: 0.9rem;
630
+ color: var(--text-secondary);
631
+ font-weight: 500;
632
+ }
633
+
634
+ .confidence-box {
635
+ display: flex;
636
+ flex-direction: column;
637
+ gap: 0.3rem;
638
+ min-width: 180px;
639
+ }
640
+
641
+ .confidence-label {
642
+ font-size: 0.8rem;
643
+ color: var(--text-secondary);
644
+ }
645
+
646
+ .confidence-track {
647
+ height: 8px;
648
+ background: var(--surface-variant);
649
+ border-radius: 2px;
650
+ overflow: hidden;
651
+ }
652
+
653
+ .confidence-fill {
654
+ height: 100%;
655
+ background: var(--google-blue);
656
+ border-radius: 2px;
657
+ transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
658
+ }
659
+
660
+ .vector-preview {
661
+ font-family: 'Roboto Mono', monospace;
662
+ font-size: 0.75rem;
663
+ color: var(--text-secondary);
664
+ background: var(--surface-variant);
665
+ padding: 0.55rem 0.8rem;
666
+ border-radius: 4px;
667
+ overflow-x: auto;
668
+ white-space: nowrap;
669
+ }
670
+
671
+ /* Neighbors Section */
672
+ .neighbors-section h4 {
673
+ font-family: 'Google Sans', sans-serif;
674
+ font-size: 0.85rem;
675
+ color: var(--text-secondary);
676
+ margin-bottom: 0.6rem;
677
+ }
678
+
679
+ .neighbors-list {
680
+ display: flex;
681
+ flex-direction: column;
682
+ gap: 0.5rem;
683
+ }
684
+
685
+ .neighbor-item {
686
+ display: flex;
687
+ gap: 0.75rem;
688
+ align-items: center;
689
+ background: var(--surface-variant);
690
+ border: 1px solid var(--border-color);
691
+ padding: 0.55rem 0.8rem;
692
+ border-radius: 4px;
693
+ }
694
+
695
+ .neighbor-rank {
696
+ font-size: 0.8rem;
697
+ font-weight: 700;
698
+ color: var(--google-blue);
699
+ width: 24px;
700
+ }
701
+
702
+ .neighbor-content {
703
+ display: flex;
704
+ flex-direction: column;
705
+ gap: 0.2rem;
706
+ flex-grow: 1;
707
+ }
708
+
709
+ .neighbor-top {
710
+ display: flex;
711
+ justify-content: space-between;
712
+ align-items: center;
713
+ }
714
+
715
+ .distance-tag {
716
+ font-size: 0.72rem;
717
+ font-family: 'Roboto Mono', monospace;
718
+ color: var(--text-secondary);
719
+ }
720
+
721
+ .neighbor-text {
722
+ font-size: 0.82rem;
723
+ color: var(--text-primary);
724
+ }
725
+
726
+ /* Console Log */
727
+ .log-panel {
728
+ display: flex;
729
+ flex-direction: column;
730
+ height: 380px;
731
+ }
732
+
733
+ .live-tag {
734
+ font-size: 0.7rem;
735
+ font-weight: 700;
736
+ color: var(--google-green-dark);
737
+ background: var(--google-green-light);
738
+ padding: 0.15rem 0.5rem;
739
+ border-radius: 4px;
740
+ letter-spacing: 0.05em;
741
+ }
742
+
743
+ .log-output {
744
+ flex-grow: 1;
745
+ background: #202124;
746
+ color: #e8eaed;
747
+ border: 1px solid var(--border-color);
748
+ border-radius: 4px;
749
+ padding: 0.75rem;
750
+ font-family: 'Roboto Mono', monospace;
751
+ font-size: 0.78rem;
752
+ overflow-y: auto;
753
+ display: flex;
754
+ flex-direction: column;
755
+ gap: 0.35rem;
756
+ }
757
+
758
+ .log-entry {
759
+ line-height: 1.4;
760
+ word-break: break-word;
761
  }
762
 
763
+ .log-time {
764
+ color: #9aa0a6;
765
+ margin-right: 0.3rem;
766
+ }
767
+
768
+ .log-info { color: #8ab4f8; }
769
+ .log-success { color: #81c995; }
770
+ .log-warn { color: #fde293; }
771
+ .log-error { color: #f28b82; }
772
+
773
+ /* Specs List */
774
+ .specs-list {
775
+ list-style: none;
776
+ display: flex;
777
+ flex-direction: column;
778
+ gap: 0.6rem;
779
+ font-size: 0.85rem;
780
+ }
781
+
782
+ .specs-list li {
783
+ display: flex;
784
+ justify-content: space-between;
785
+ border-bottom: 1px solid var(--border-color);
786
+ padding-bottom: 0.4rem;
787
+ color: var(--text-secondary);
788
+ }
789
+
790
+ .specs-list strong {
791
+ color: var(--text-primary);
792
+ }
793
+
794
+ .specs-list code {
795
+ font-family: 'Roboto Mono', monospace;
796
+ color: var(--google-blue);
797
+ background: var(--google-blue-light);
798
+ padding: 0.1rem 0.3rem;
799
+ border-radius: 2px;
800
+ }
801
+
802
+ /* Footer */
803
+ .app-footer {
804
+ text-align: center;
805
+ font-size: 0.8rem;
806
+ color: var(--text-tertiary);
807
+ padding-top: 1rem;
808
+ border-top: 1px solid var(--border-color);
809
+ }
810
+
811
+ .app-footer a {
812
+ color: var(--google-blue);
813
+ text-decoration: none;
814
+ }
815
+
816
+ .app-footer a:hover {
817
  text-decoration: underline;
818
+ }
819
+
820
+ /* ==========================================================================
821
+ Visual Storytelling & Interactive Deep Learning Visual Explorers
822
+ ========================================================================== */
823
+
824
+ /* Hero Pipeline Header & Stepper Grid */
825
+ .hero-header-row {
826
+ display: flex;
827
+ justify-content: space-between;
828
+ align-items: center;
829
+ flex-wrap: wrap;
830
+ gap: 1rem;
831
+ margin-bottom: 1.25rem;
832
+ }
833
+
834
+ .hero-subtitle {
835
+ font-size: 0.88rem;
836
+ color: var(--text-secondary);
837
+ margin-top: 0.2rem;
838
+ }
839
+
840
+ .stepper-grid {
841
+ display: grid;
842
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
843
+ gap: 1rem;
844
+ }
845
+
846
+ .stepper-step {
847
+ display: flex;
848
+ align-items: center;
849
+ gap: 0.85rem;
850
+ background: var(--surface-variant);
851
+ border: 1px solid var(--border-color);
852
+ padding: 0.85rem 1rem;
853
+ border-radius: 6px;
854
+ cursor: pointer;
855
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
856
+ }
857
+
858
+ .stepper-step:hover {
859
+ border-color: var(--google-blue);
860
+ background: #ffffff;
861
+ transform: translateY(-2px);
862
+ box-shadow: 0 4px 12px rgba(26, 115, 232, 0.1);
863
+ }
864
+
865
+ .stepper-step.active {
866
+ background: #ffffff;
867
+ border-color: var(--google-blue);
868
+ box-shadow: 0 0 0 2px var(--google-blue-light), 0 2px 8px rgba(0,0,0,0.06);
869
+ }
870
+
871
+ .step-details h3 {
872
+ font-family: 'Google Sans', sans-serif;
873
+ font-size: 0.92rem;
874
+ font-weight: 600;
875
+ color: var(--text-primary);
876
+ }
877
+
878
+ .step-details p {
879
+ font-size: 0.76rem;
880
+ color: var(--text-secondary);
881
+ }
882
+
883
+ .step-yellow {
884
+ background: var(--google-yellow-light);
885
+ color: var(--google-yellow-dark);
886
+ }
887
+
888
+ /* Visual Storytelling Section */
889
+ .viz-storytelling-section {
890
+ display: flex;
891
+ flex-direction: column;
892
+ gap: 1.5rem;
893
+ margin-top: 0.5rem;
894
+ }
895
+
896
+ .section-title-row h2 {
897
+ font-family: 'Google Sans', sans-serif;
898
+ font-size: 1.4rem;
899
+ font-weight: 700;
900
+ color: var(--text-primary);
901
+ }
902
+
903
+ .section-title-row p {
904
+ font-size: 0.88rem;
905
+ color: var(--text-secondary);
906
+ }
907
+
908
+ .viz-grid {
909
+ display: grid;
910
+ grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
911
+ gap: 1.5rem;
912
+ }
913
+
914
+ .viz-card {
915
+ display: flex;
916
+ flex-direction: column;
917
+ gap: 1rem;
918
+ position: relative;
919
+ overflow: hidden;
920
+ transition: transform 0.2s ease, border-color 0.2s ease;
921
+ }
922
+
923
+ .full-width-viz {
924
+ grid-column: 1 / -1;
925
+ }
926
+
927
+ .viz-desc {
928
+ font-size: 0.84rem;
929
+ color: var(--text-secondary);
930
+ }
931
+
932
+ .viz-container {
933
+ background: var(--surface-variant);
934
+ border: 1px solid var(--border-color);
935
+ border-radius: 6px;
936
+ padding: 1rem;
937
+ min-height: 140px;
938
+ display: flex;
939
+ flex-direction: column;
940
+ justify-content: center;
941
+ }
942
+
943
+ .empty-viz {
944
+ text-align: center;
945
+ color: var(--text-tertiary);
946
+ font-size: 0.85rem;
947
+ font-style: italic;
948
+ }
949
+
950
+ .viz-sub-header {
951
+ display: flex;
952
+ justify-content: space-between;
953
+ align-items: center;
954
+ font-size: 0.8rem;
955
+ font-weight: 600;
956
+ color: var(--text-secondary);
957
+ margin-bottom: 0.75rem;
958
+ }
959
+
960
+ /* Tokenizer Visualizer */
961
+ .token-chip-container {
962
+ display: flex;
963
+ flex-wrap: wrap;
964
+ gap: 0.6rem;
965
+ }
966
+
967
+ .token-chip {
968
+ background: #ffffff;
969
+ border: 1px solid var(--google-blue-border);
970
+ border-radius: 4px;
971
+ padding: 0.4rem 0.75rem;
972
+ display: inline-flex;
973
+ align-items: center;
974
+ gap: 0.5rem;
975
+ font-family: 'Roboto Mono', monospace;
976
+ font-size: 0.82rem;
977
+ color: var(--text-primary);
978
+ cursor: pointer;
979
+ transition: all 0.2s ease;
980
+ box-shadow: var(--shadow-md);
981
+ }
982
+
983
+ .token-chip:hover {
984
+ border-color: var(--google-blue);
985
+ background: var(--google-blue-light);
986
+ transform: scale(1.04);
987
+ }
988
+
989
+ .token-chip.special-token {
990
+ border-color: var(--google-red-border);
991
+ background: var(--google-red-light);
992
+ color: var(--google-red-hover);
993
+ }
994
+
995
+ .token-chip.subword-token {
996
+ border-color: var(--google-yellow-border);
997
+ background: var(--google-yellow-light);
998
+ color: var(--google-yellow-dark);
999
+ }
1000
+
1001
+ .token-idx {
1002
+ font-size: 0.7rem;
1003
+ color: var(--text-tertiary);
1004
+ background: rgba(0,0,0,0.05);
1005
+ padding: 0.1rem 0.35rem;
1006
+ border-radius: 3px;
1007
+ }
1008
+
1009
+ /* Self-Attention Visualizer */
1010
+ .attn-controls {
1011
+ display: flex;
1012
+ justify-content: space-between;
1013
+ align-items: center;
1014
+ flex-wrap: wrap;
1015
+ gap: 0.75rem;
1016
+ margin-bottom: 0.85rem;
1017
+ font-size: 0.8rem;
1018
+ }
1019
+
1020
+ .attn-head-select {
1021
+ display: flex;
1022
+ align-items: center;
1023
+ gap: 0.6rem;
1024
+ }
1025
+
1026
+ .attn-head-select label {
1027
+ font-weight: 500;
1028
+ color: var(--text-secondary);
1029
+ }
1030
+
1031
+ .head-pills {
1032
+ display: flex;
1033
+ gap: 0.35rem;
1034
+ }
1035
+
1036
+ .head-pill {
1037
+ background: #ffffff;
1038
+ border: 1px solid var(--border-color);
1039
+ border-radius: 4px;
1040
+ padding: 0.25rem 0.55rem;
1041
+ font-size: 0.75rem;
1042
+ cursor: pointer;
1043
+ color: var(--text-secondary);
1044
+ transition: all 0.15s ease;
1045
+ }
1046
+
1047
+ .head-pill.active, .head-pill:hover {
1048
+ background: var(--google-blue);
1049
+ color: #ffffff;
1050
+ border-color: var(--google-blue);
1051
+ }
1052
+
1053
+ .attn-hint {
1054
+ font-size: 0.75rem;
1055
+ color: var(--text-tertiary);
1056
+ font-style: italic;
1057
+ }
1058
+
1059
+ .attn-arcs-wrapper {
1060
+ position: relative;
1061
+ height: 0;
1062
+ }
1063
+
1064
+ .attn-matrix-table {
1065
+ display: grid;
1066
+ gap: 2px;
1067
+ background: var(--border-color);
1068
+ padding: 2px;
1069
+ border-radius: 4px;
1070
+ overflow-x: auto;
1071
+ }
1072
+
1073
+ .attn-cell {
1074
+ background: #ffffff;
1075
+ padding: 0.4rem 0.5rem;
1076
+ font-size: 0.75rem;
1077
+ text-align: center;
1078
+ display: flex;
1079
+ align-items: center;
1080
+ justify-content: center;
1081
+ font-family: 'Roboto Mono', monospace;
1082
+ user-select: none;
1083
+ }
1084
+
1085
+ .attn-cell.corner-cell {
1086
+ background: var(--surface-variant);
1087
+ font-weight: bold;
1088
+ color: var(--text-tertiary);
1089
+ }
1090
+
1091
+ .attn-cell.col-header, .attn-cell.row-header {
1092
+ background: var(--surface-variant);
1093
+ font-weight: 600;
1094
+ color: var(--text-secondary);
1095
+ }
1096
+
1097
+ .attn-cell.weight-cell {
1098
  cursor: pointer;
1099
+ transition: transform 0.15s ease;
1100
+ }
1101
+
1102
+ .attn-cell.weight-cell:hover {
1103
+ transform: scale(1.15);
1104
+ z-index: 10;
1105
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
1106
+ }
1107
+
1108
+ /* Vector Barcode Fingerprint */
1109
+ .barcode-wrapper {
1110
+ display: flex;
1111
+ flex-direction: column;
1112
+ gap: 0.5rem;
1113
+ background: #ffffff;
1114
+ padding: 0.75rem;
1115
+ border: 1px solid var(--border-color);
1116
+ border-radius: 4px;
1117
+ }
1118
+
1119
+ .barcode-canvas {
1120
+ width: 100%;
1121
+ height: 36px;
1122
+ border-radius: 3px;
1123
+ cursor: crosshair;
1124
+ }
1125
+
1126
+ .norm-badge {
1127
+ font-size: 0.72rem;
1128
+ background: var(--google-green-light);
1129
+ color: var(--google-green-dark);
1130
+ padding: 0.15rem 0.45rem;
1131
+ border-radius: 3px;
1132
+ font-family: 'Roboto Mono', monospace;
1133
+ }
1134
+
1135
+ /* 2D Embedding Space PCA Canvas (GAN Lab style) */
1136
+ .pca-legend {
1137
+ display: flex;
1138
+ gap: 1rem;
1139
+ flex-wrap: wrap;
1140
+ }
1141
+
1142
+ .legend-item {
1143
+ display: flex;
1144
+ align-items: center;
1145
+ gap: 0.4rem;
1146
+ font-size: 0.78rem;
1147
+ font-weight: 500;
1148
+ color: var(--text-secondary);
1149
  }
1150
 
1151
+ .legend-dot {
1152
+ width: 10px;
1153
+ height: 10px;
1154
+ border-radius: 50%;
1155
+ display: inline-block;
1156
  }
1157
 
1158
+ .legend-dot.star-dot {
1159
+ border: 2px solid #ffffff;
1160
+ box-shadow: 0 0 0 1px #1a73e8;
1161
+ }
1162
+
1163
+ .canvas-wrapper {
1164
+ position: relative;
1165
+ background: #ffffff;
1166
+ border: 1px solid var(--border-color);
1167
+ border-radius: 6px;
1168
+ display: flex;
1169
+ justify-content: center;
1170
+ align-items: center;
1171
+ overflow: hidden;
1172
+ }
1173
+
1174
+ #pca-canvas {
1175
+ width: 100%;
1176
+ max-width: 860px;
1177
+ height: 420px;
1178
+ background: #fafafa;
1179
+ }
1180
+
1181
+ .canvas-hint-overlay {
1182
  position: absolute;
1183
+ bottom: 10px;
1184
+ right: 12px;
1185
+ background: rgba(255, 255, 255, 0.92);
1186
+ backdrop-filter: blur(4px);
1187
+ border: 1px solid var(--border-color);
1188
+ border-radius: 4px;
1189
+ padding: 0.3rem 0.65rem;
1190
+ font-size: 0.76rem;
1191
+ color: var(--text-secondary);
1192
+ pointer-events: none;
1193
+ box-shadow: var(--shadow-md);
1194
  }
1195
 
1196
+ /* Global Tooltip Popup */
1197
+ .viz-tooltip {
1198
  position: absolute;
1199
+ z-index: 9999;
1200
+ background: rgba(32, 33, 36, 0.94);
1201
+ color: #ffffff;
1202
+ padding: 0.6rem 0.85rem;
1203
+ border-radius: 6px;
1204
+ font-size: 0.78rem;
1205
+ line-height: 1.45;
1206
+ pointer-events: none;
1207
+ box-shadow: 0 6px 16px rgba(0,0,0,0.25);
1208
+ backdrop-filter: blur(6px);
1209
+ border: 1px solid rgba(255,255,255,0.15);
1210
+ display: none;
1211
+ }
1212
+
1213
+ /* Forward Pass Pulse Highlight Animation */
1214
+ .pulse-highlight {
1215
+ animation: pulseGlow 1.2s cubic-bezier(0.4, 0, 0.2, 1);
1216
+ }
1217
+
1218
+ @keyframes pulseGlow {
1219
+ 0% {
1220
+ box-shadow: 0 0 0 0 rgba(26, 115, 232, 0.5);
1221
+ border-color: var(--google-blue);
1222
+ }
1223
+ 50% {
1224
+ box-shadow: 0 0 0 10px rgba(26, 115, 232, 0);
1225
+ border-color: var(--google-blue);
1226
+ }
1227
+ 100% {
1228
+ box-shadow: 0 0 0 0 rgba(26, 115, 232, 0);
1229
+ }
1230
  }