Xenova HF Staff commited on
Commit
daa5270
·
verified ·
1 Parent(s): 9122a9c

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +740 -17
index.html CHANGED
@@ -1,19 +1,742 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </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, user-scalable=no" />
6
+ <title>pplx-embed web</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ padding: 0;
11
+ overflow: hidden;
12
+ background-color: #121212;
13
+ font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
14
+ color: #ffffff;
15
+ }
16
+
17
+ #canvas-container {
18
+ width: 100vw;
19
+ height: 100vh;
20
+ touch-action: none;
21
+ cursor: grab;
22
+ }
23
+
24
+ #canvas-container:active {
25
+ cursor: grabbing;
26
+ }
27
+
28
+ #ui-layer {
29
+ position: absolute;
30
+ bottom: 40px;
31
+ left: 0;
32
+ width: 100%;
33
+ display: flex;
34
+ flex-direction: column;
35
+ align-items: center;
36
+ gap: 15px;
37
+ pointer-events: none;
38
+ }
39
+
40
+ .header {
41
+ text-align: center;
42
+ pointer-events: auto;
43
+ }
44
+
45
+ .title {
46
+ font-size: 1.2rem;
47
+ font-weight: 600;
48
+ letter-spacing: 1px;
49
+ opacity: 0.9;
50
+ }
51
+
52
+ .subtitle {
53
+ font-size: 0.85rem;
54
+ opacity: 0.5;
55
+ margin-top: 4px;
56
+ }
57
+
58
+ .search-container {
59
+ pointer-events: auto;
60
+ background: rgba(40, 42, 44, 0.8);
61
+ border: 1px solid rgba(255, 255, 255, 0.1);
62
+ padding: 8px 16px;
63
+ border-radius: 30px;
64
+ display: flex;
65
+ align-items: center;
66
+ backdrop-filter: blur(10px);
67
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
68
+ transition: all 0.3s ease;
69
+ }
70
+
71
+ .search-container:focus-within {
72
+ border-color: rgba(255, 255, 255, 0.4);
73
+ box-shadow: 0 10px 40px rgba(255, 255, 255, 0.05);
74
+ }
75
+
76
+ .search-container svg {
77
+ width: 18px;
78
+ height: 18px;
79
+ fill: #888;
80
+ margin-right: 10px;
81
+ }
82
+
83
+ input[type="text"] {
84
+ background: transparent;
85
+ border: none;
86
+ color: white;
87
+ font-size: 1.1rem;
88
+ font-family: inherit;
89
+ outline: none;
90
+ width: 320px;
91
+ }
92
+
93
+ input[type="text"]::placeholder {
94
+ color: #888;
95
+ }
96
+
97
+ input[type="text"]:disabled {
98
+ opacity: 0.6;
99
+ cursor: not-allowed;
100
+ }
101
+
102
+ /* Loading overlay */
103
+ #loading-overlay {
104
+ position: fixed;
105
+ inset: 0;
106
+ z-index: 9999;
107
+ background: #121212;
108
+ display: flex;
109
+ flex-direction: column;
110
+ align-items: center;
111
+ justify-content: center;
112
+ gap: 24px;
113
+ transition: opacity 0.6s ease;
114
+ }
115
+
116
+ #loading-overlay.fade-out {
117
+ opacity: 0;
118
+ pointer-events: none;
119
+ }
120
+
121
+ #loading-overlay .spinner {
122
+ width: 48px;
123
+ height: 48px;
124
+ border: 4px solid rgba(255, 255, 255, 0.15);
125
+ border-top-color: #ffffff;
126
+ border-radius: 50%;
127
+ animation: spin 0.8s linear infinite;
128
+ }
129
+
130
+ @keyframes spin {
131
+ to {
132
+ transform: rotate(360deg);
133
+ }
134
+ }
135
+
136
+ #loading-overlay .loading-title {
137
+ font-size: 3rem;
138
+ font-weight: 700;
139
+ letter-spacing: 1.5px;
140
+ opacity: 0.95;
141
+ }
142
+
143
+ #loading-overlay .loading-status {
144
+ font-size: 0.95rem;
145
+ opacity: 0.5;
146
+ }
147
+ </style>
148
+ </head>
149
+
150
+ <body>
151
+ <!-- Fullscreen loading overlay -->
152
+ <div id="loading-overlay">
153
+ <div class="loading-title">pplx-embed web</div>
154
+ <div class="spinner"></div>
155
+ <div class="loading-status" id="loadingStatus">Loading model...</div>
156
+ </div>
157
+
158
+ <div id="canvas-container"></div>
159
+ <div id="ui-layer">
160
+ <div class="search-container">
161
+ <svg viewBox="0 0 24 24">
162
+ <path
163
+ d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
164
+ />
165
+ </svg>
166
+ <input type="text" id="searchInput" placeholder="Loading semantic search model..." disabled />
167
+ </div>
168
+ <div class="header">
169
+ <div class="title">pplx-embed web</div>
170
+ <div class="subtitle">Drag to spin, semantic search is initializing...</div>
171
+ </div>
172
+ </div>
173
+
174
+ <script type="module">
175
+ import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.module.js";
176
+ import { pipeline } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.0.0-next.4";
177
+
178
+ const DEFAULT_SENTENCES = [
179
+ // Weather
180
+ "The sun peeked through the clouds after a drizzly morning.",
181
+ "A gentle breeze rustled the leaves as we walked along the shoreline.",
182
+ "Heavy rains caused flooding in several low-lying neighborhoods.",
183
+ "It was so hot that even the birds sought shade under the palm trees.",
184
+ "By midnight, the temperature had dropped below freezing.",
185
+ "Thunderstorms lit up the sky with flashes of lightning.",
186
+ "A thick fog settled over the city streets at dawn.",
187
+ "The air smelled of ozone after the sudden hailstorm.",
188
+ "I watched the snowflakes drift silently onto the ground.",
189
+ "A double rainbow appeared after the rain shower.",
190
+ "The humidity soared to uncomfortable levels by midday.",
191
+ "Dust devils formed in the dry desert plains.",
192
+ "The barometer readings indicated an approaching front.",
193
+ "A sudden gust of wind knocked over the garden chairs.",
194
+ "Light drizzle turned into a torrential downpour within minutes.",
195
+
196
+ // Technology
197
+ "The new smartphone features a foldable display and 5G connectivity.",
198
+ "In the world of AI, transformers have revolutionized natural language processing.",
199
+ "Quantum computing promises to solve problems beyond classical computers' reach.",
200
+ "Blockchain technology is being explored for secure voting systems.",
201
+ "Virtual reality headsets are becoming more affordable and accessible.",
202
+ "The rise of electric vehicles is reshaping the automotive industry.",
203
+ "Cloud computing allows businesses to scale resources dynamically.",
204
+ "Machine learning algorithms can now predict stock market trends with surprising accuracy.",
205
+ "Augmented reality applications are transforming retail experiences.",
206
+ "The Internet of Things connects everyday devices to the web for smarter living.",
207
+ "Cybersecurity threats are evolving, requiring constant vigilance.",
208
+ "3D printing is enabling rapid prototyping and custom manufacturing.",
209
+ "Edge computing reduces latency by processing data closer to the source.",
210
+ "Biometric authentication methods are enhancing security in devices.",
211
+ "Wearable technology is tracking health metrics in real-time.",
212
+ "Artificial intelligence is being used to create realistic deepfakes.",
213
+
214
+ // Cooking
215
+ "Preheat the oven to 375°F before you start mixing the batter.",
216
+ "She finely chopped the garlic and sautéed it in two tablespoons of olive oil.",
217
+ "A pinch of saffron adds a beautiful color and aroma to traditional paella.",
218
+ "If the soup is too salty, add a peeled potato to absorb excess sodium.",
219
+ "Let the bread dough rise for at least an hour in a warm, draft-free spot.",
220
+ "Marinate the chicken overnight in a blend of citrus and spices.",
221
+ "Use a cast-iron skillet to sear the steak on high heat.",
222
+ "Whisk the egg whites until they form stiff peaks.",
223
+ "Fold in the chocolate chips gently to keep the batter airy.",
224
+ "Brush the pastry with an egg wash for a golden finish.",
225
+ "Slow-roast the pork shoulder until it falls off the bone.",
226
+ "Garnish the salad with toasted nuts and fresh herbs.",
227
+ "Deglaze the pan with white wine for a rich sauce.",
228
+ "Simmer the curry paste until the aroma intensifies.",
229
+ "Let the risotto rest before serving to thicken slightly.",
230
+
231
+ // Sports
232
+ "He dribbled past two defenders and sank a three-pointer at the buzzer.",
233
+ "The marathon runner kept a steady pace despite the sweltering heat.",
234
+ "Their home team clinched the championship with a last-minute goal.",
235
+ "NASCAR fans cheered as the cars roared around the oval track.",
236
+ "She landed a perfect triple axel at the figure skating championship.",
237
+ "The cyclist pedaled up the steep hill in record time.",
238
+ "He pitched a no-hitter during the high school baseball game.",
239
+ "The quarterback threw a touchdown pass under heavy pressure.",
240
+ "They scored a hat-trick in the hockey final.",
241
+ "The boxer delivered a swift uppercut in the final round.",
242
+ "Surfers caught massive waves at dawn on the Pacific coast.",
243
+ "Fans erupted when the underdog scored the winning goal.",
244
+ "The swimmer broke the national record in the 200m freestyle.",
245
+ "The gymnast executed a flawless routine on the balance beam.",
246
+ "The rugby team celebrated their victory with a traditional haka.",
247
+
248
+ // Finance
249
+ "The stock market rallied after positive earnings reports.",
250
+ "Investors are closely watching interest rate changes by the Federal Reserve.",
251
+ "Cryptocurrency prices have been extremely volatile this year.",
252
+ "Diversification is key to managing investment risk effectively.",
253
+ "Inflation rates have reached a 40-year high, impacting consumer spending.",
254
+ "Many companies are adopting ESG criteria to attract socially conscious investors.",
255
+ "The bond market is reacting to geopolitical tensions and supply chain disruptions.",
256
+ "Venture capital funding for startups has surged in the tech sector.",
257
+ "Exchange-traded funds (ETFs) offer a way to invest in diversified portfolios.",
258
+ "The global economy is recovering from the pandemic, but challenges remain.",
259
+ "Central banks are exploring digital currencies to modernize payment systems.",
260
+ "Retail investors are increasingly participating in the stock market through apps.",
261
+ "Hedge funds are using complex algorithms to gain an edge in trading.",
262
+ "Real estate prices have skyrocketed in urban areas due to low inventory.",
263
+ "The startup raised $10 million in its Series A funding round.",
264
+
265
+ // Music
266
+ "The symphony orchestra played a hauntingly beautiful melody.",
267
+ "She strummed her guitar softly, filling the room with a warm sound.",
268
+ "The DJ mixed tracks seamlessly, keeping the crowd dancing all night.",
269
+ "His voice soared during the high notes of the ballad.",
270
+ "The band played an acoustic set in the intimate coffee shop.",
271
+ "Jazz musicians often improvise solos based on the chord changes.",
272
+ "The opera singer hit the high C with perfect pitch.",
273
+ "The choir harmonized beautifully, filling the church with sound.",
274
+ "He composed a symphony that was performed at the concert hall.",
275
+ "The singer-songwriter wrote heartfelt lyrics about love and loss.",
276
+ "The rock band headlined the festival, drawing a massive crowd.",
277
+ "Hip-hop artists use rhythm and rhyme to tell powerful stories.",
278
+ "The violinist played a virtuosic solo that left the audience in awe.",
279
+ "Folk music often reflects the culture and traditions of a community.",
280
+ "The gospel choir lifted spirits with their uplifting performance.",
281
+
282
+ // History
283
+ "The fall of the Berlin Wall in 1989 marked the end of the Cold War.",
284
+ "Ancient Egypt's pyramids are a testament to their architectural prowess.",
285
+ "Europe's Renaissance period sparked a revival in art and science.",
286
+ "The signing of the Declaration of Independence in 1776 established the United States.",
287
+ "The Industrial Revolution transformed economies and societies worldwide.",
288
+ "Rome was the center of a vast empire that influenced law and governance.",
289
+ "The discovery of the New World by Christopher Columbus in 1492 changed global trade.",
290
+ "The French Revolution in 1789 led to significant political and social change.",
291
+ "World War II was a global conflict that reshaped international relations.",
292
+ "The fall of the Roman Empire in 476 AD marked the beginning of the Middle Ages.",
293
+ "The invention of the printing press revolutionized the spread of knowledge.",
294
+ "The Cold War was characterized by political tension between the U.S. and the Soviet Union.",
295
+ "The ancient Silk Road connected East and West through trade routes.",
296
+ "The signing of the Magna Carta in 1215 established principles of due process.",
297
+ "Exploration during the Age of Discovery expanded European empires across the globe.",
298
+ ];
299
+
300
+ const TOTAL_DOCS = DEFAULT_SENTENCES.length;
301
+ const TOTAL_SPREADS = Math.ceil(TOTAL_DOCS / 2);
302
+ const MODEL_ID = "perplexity-ai/pplx-embed-v1-0.6b";
303
+
304
+ const similarityScores = Array(TOTAL_DOCS + 1).fill(null);
305
+ // slotToDoc[slot] gives the 1-indexed doc ID to display at carousel slot (0-indexed).
306
+ // Default is identity: slot i shows doc i+1.
307
+ const slotToDoc = Array.from({ length: TOTAL_DOCS }, (_, i) => i + 1);
308
+ let extractor = null;
309
+ let docEmbeddings = null;
310
+ let latestSearchToken = 0;
311
+ let searchDebounceId = null;
312
+
313
+ const searchInput = document.getElementById("searchInput");
314
+ const subtitle = document.querySelector(".subtitle");
315
+ const loadingOverlay = document.getElementById("loading-overlay");
316
+ const loadingStatus = document.getElementById("loadingStatus");
317
+
318
+ function dot(a, b) {
319
+ let sum = 0;
320
+ for (let i = 0; i < a.length; i++) {
321
+ sum += a[i] * b[i];
322
+ }
323
+ return sum;
324
+ }
325
+
326
+ function wrapText(ctx, text, x, y, maxWidth, lineHeight, maxLines) {
327
+ const words = text.split(" ");
328
+ const lines = [];
329
+ let line = "";
330
+
331
+ for (let i = 0; i < words.length; i++) {
332
+ const testLine = line ? `${line} ${words[i]}` : words[i];
333
+ const metrics = ctx.measureText(testLine);
334
+ if (metrics.width > maxWidth && line) {
335
+ lines.push(line);
336
+ line = words[i];
337
+ if (lines.length === maxLines - 1) {
338
+ break;
339
+ }
340
+ } else {
341
+ line = testLine;
342
+ }
343
+ }
344
+
345
+ if (line) {
346
+ lines.push(line);
347
+ }
348
+
349
+ if (lines.length > maxLines) {
350
+ lines.length = maxLines;
351
+ }
352
+
353
+ if (lines.length === maxLines && words.join(" ") !== lines.join(" ")) {
354
+ const last = lines[maxLines - 1];
355
+ lines[maxLines - 1] = `${last.replace(/[.,;:!?]?$/, "")}…`;
356
+ }
357
+
358
+ const startY = y - ((lines.length - 1) * lineHeight) / 2;
359
+ lines.forEach((lineText, idx) => {
360
+ ctx.fillText(lineText, x, startY + idx * lineHeight);
361
+ });
362
+ }
363
+
364
+ function formatScore(score) {
365
+ return score == null ? "--" : score.toFixed(3);
366
+ }
367
+
368
+ function jumpToDocument(docId) {
369
+ const targetSpread = Math.floor((docId - 1) / 2);
370
+ const targetAngleDeg = -targetSpread * 60;
371
+
372
+ const currentDeg = (targetBaseY * 180) / Math.PI;
373
+ const fullCylinderDeg = TOTAL_SPREADS * 60;
374
+ let diff = (targetAngleDeg - currentDeg) % fullCylinderDeg;
375
+
376
+ if (diff > fullCylinderDeg / 2) diff -= fullCylinderDeg;
377
+ if (diff < -fullCylinderDeg / 2) diff += fullCylinderDeg;
378
+
379
+ targetBaseY = ((currentDeg + diff) * Math.PI) / 180;
380
+ }
381
+
382
+ function dismissLoadingOverlay() {
383
+ loadingOverlay.classList.add("fade-out");
384
+ loadingOverlay.addEventListener(
385
+ "transitionend",
386
+ () => {
387
+ loadingOverlay.remove();
388
+ },
389
+ { once: true },
390
+ );
391
+ }
392
+
393
+ async function initializeSemanticSearch() {
394
+ loadingStatus.textContent = "Loading model...";
395
+ subtitle.textContent = "Loading embedding model on WebGPU...";
396
+
397
+ try {
398
+ extractor = await pipeline("feature-extraction", MODEL_ID, {
399
+ dtype: "q4",
400
+ device: "webgpu",
401
+ revision: "refs/pr/10",
402
+ });
403
+
404
+ loadingStatus.textContent = "Embedding documents...";
405
+ subtitle.textContent = "Embedding all documents...";
406
+ const embeddingTensor = await extractor(DEFAULT_SENTENCES, {
407
+ pooling: "mean",
408
+ normalize: true,
409
+ });
410
+ docEmbeddings = embeddingTensor.tolist();
411
+
412
+ searchInput.disabled = false;
413
+ searchInput.placeholder = `Semantic search across ${TOTAL_DOCS} docs...`;
414
+ subtitle.textContent = "Drag to spin, or type to search semantically";
415
+ dismissLoadingOverlay();
416
+ } catch (err) {
417
+ console.error("Semantic search initialization failed:", err);
418
+ subtitle.textContent = "Semantic search failed to initialize on this device";
419
+ searchInput.placeholder = "Semantic search unavailable";
420
+ loadingStatus.textContent = "Failed to load model";
421
+ setTimeout(dismissLoadingOverlay, 2000);
422
+ }
423
+ }
424
+
425
+ async function runSemanticSearch(query) {
426
+ if (!extractor || !docEmbeddings) return;
427
+
428
+ const token = ++latestSearchToken;
429
+ idleAutoSpin = false;
430
+ subtitle.textContent = "Searching...";
431
+
432
+ const queryEmbeddingTensor = await extractor([query], {
433
+ pooling: "mean",
434
+ normalize: true,
435
+ });
436
+
437
+ if (token !== latestSearchToken) return;
438
+
439
+ const queryEmbedding = queryEmbeddingTensor.tolist()[0];
440
+
441
+ // Compute similarity for every document
442
+ const scored = [];
443
+ for (let i = 1; i <= TOTAL_DOCS; i++) {
444
+ const score = dot(queryEmbedding, docEmbeddings[i - 1]);
445
+ similarityScores[i] = score;
446
+ scored.push({ docId: i, score });
447
+ }
448
+
449
+ // Sort descending by score
450
+ scored.sort((a, b) => b.score - a.score);
451
+
452
+ const bestDocId = scored[0].docId;
453
+ const bestScore = scored[0].score;
454
+ const bestSlot = bestDocId - 1; // 0-indexed carousel slot
455
+
456
+ // Place best match at its original slot, then alternate left/right
457
+ // rank 0 → bestSlot, rank 1 → bestSlot-1, rank 2 → bestSlot+1,
458
+ // rank 3 → bestSlot-2, rank 4 → bestSlot+2, …
459
+ slotToDoc[bestSlot] = scored[0].docId;
460
+ for (let r = 1; r < TOTAL_DOCS; r++) {
461
+ const offset = Math.ceil(r / 2);
462
+ const dir = r % 2 === 1 ? -1 : 1;
463
+ const slot = (((bestSlot + dir * offset) % TOTAL_DOCS) + TOTAL_DOCS) % TOTAL_DOCS;
464
+ slotToDoc[slot] = scored[r].docId;
465
+ }
466
+
467
+ rebuildAllDocTextures();
468
+ jumpToDocument(bestDocId);
469
+ subtitle.textContent = `Top match: Document #${bestDocId} (${bestScore.toFixed(3)})`;
470
+ }
471
+
472
+ function clearSearchResults() {
473
+ latestSearchToken++;
474
+ for (let i = 0; i < TOTAL_DOCS; i++) {
475
+ slotToDoc[i] = i + 1; // Reset to identity mapping
476
+ similarityScores[i + 1] = null;
477
+ }
478
+ rebuildAllDocTextures();
479
+ idleAutoSpin = true;
480
+ subtitle.textContent = "Drag to spin, or type to search semantically";
481
+ }
482
+
483
+ // --- Setup Scene, Camera, and Renderer ---
484
+ const scene = new THREE.Scene();
485
+ const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 100);
486
+ camera.position.z = 18; // Pulled back slightly for wider panels
487
+
488
+ const renderer = new THREE.WebGLRenderer({
489
+ antialias: true,
490
+ alpha: true,
491
+ });
492
+ renderer.setSize(window.innerWidth, window.innerHeight);
493
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
494
+ document.getElementById("canvas-container").appendChild(renderer.domElement);
495
+
496
+ const logoGroup = new THREE.Group();
497
+ scene.add(logoGroup);
498
+
499
+ // --- Dimensions ---
500
+ const panelWidth = 3.6;
501
+ const panelHeight = 3.6;
502
+ const panelDepth = 0.08;
503
+ const axleHeight = 4.8;
504
+ const panelColor = "#1a1c1d";
505
+ const lineColor = 0xffffff;
506
+
507
+ // --- 1. Central Axle ---
508
+ const axleGeom = new THREE.BoxGeometry(panelDepth, axleHeight, panelDepth);
509
+ const axleMat = new THREE.MeshBasicMaterial({ color: panelColor });
510
+ const axle = new THREE.Mesh(axleGeom, axleMat);
511
+
512
+ const axleEdges = new THREE.LineSegments(new THREE.EdgesGeometry(axleGeom), new THREE.LineBasicMaterial({ color: lineColor }));
513
+ axle.add(axleEdges);
514
+ logoGroup.add(axle);
515
+
516
+ // --- 2. Generate document textures ---
517
+ const docTextures = [];
518
+ function createDocumentTexture(num, score) {
519
+ const canvas = document.createElement("canvas");
520
+ canvas.width = 512;
521
+ canvas.height = Math.round(512 * (panelHeight / panelWidth));
522
+ const ctx = canvas.getContext("2d");
523
+ const sentence = DEFAULT_SENTENCES[num - 1];
524
+
525
+ // Background
526
+ ctx.fillStyle = panelColor;
527
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
528
+
529
+ // Subtle border to make it look like a document card
530
+ ctx.strokeStyle = "rgba(255, 255, 255, 0.1)";
531
+ ctx.lineWidth = 12;
532
+ ctx.strokeRect(10, 10, canvas.width - 20, canvas.height - 20);
533
+
534
+ // Header metadata
535
+ ctx.fillStyle = "#9aa0a6";
536
+ ctx.font = '600 24px "Segoe UI", sans-serif';
537
+ ctx.textAlign = "left";
538
+ ctx.textBaseline = "top";
539
+ ctx.fillText(`Document #${num}`, 30, 24);
540
+
541
+ ctx.textAlign = "right";
542
+ ctx.fillText(`Score ${formatScore(score)}`, canvas.width - 30, 24);
543
+
544
+ // Body sentence
545
+ ctx.fillStyle = "#ffffff";
546
+ ctx.font = '500 28px "Segoe UI", sans-serif';
547
+ ctx.textAlign = "center";
548
+ ctx.textBaseline = "middle";
549
+ wrapText(ctx, sentence, canvas.width / 2, canvas.height / 2 + 12, canvas.width - 80, 38, 5);
550
+
551
+ const texture = new THREE.CanvasTexture(canvas);
552
+ texture.minFilter = THREE.LinearFilter;
553
+ texture.magFilter = THREE.LinearFilter;
554
+ return texture;
555
+ }
556
+
557
+ function rebuildAllDocTextures() {
558
+ for (let i = 1; i <= TOTAL_DOCS; i++) {
559
+ if (docTextures[i]) {
560
+ docTextures[i].dispose();
561
+ }
562
+ docTextures[i] = createDocumentTexture(i, similarityScores[i]);
563
+ }
564
+ }
565
+
566
+ // Pre-generate all textures (1-indexed for convenience)
567
+ for (let i = 1; i <= TOTAL_DOCS; i++) {
568
+ docTextures[i] = createDocumentTexture(i, similarityScores[i]);
569
+ }
570
+
571
+ // --- 3. Create Panels & Dynamic Materials ---
572
+ const panelGeom = new THREE.BoxGeometry(panelWidth, panelHeight, panelDepth);
573
+ panelGeom.translate(panelWidth / 2, 0, 0); // Pivot at the left edge
574
+
575
+ const edgeMat = new THREE.MeshBasicMaterial({ color: panelColor });
576
+
577
+ // We have 6 fins, each with a front and back face. Total 12 faces.
578
+ // We give each face its own distinct material so we can dynamically swap them.
579
+ const faceMaterials = [];
580
+ for (let i = 0; i < 12; i++) {
581
+ faceMaterials.push(new THREE.MeshBasicMaterial({ map: docTextures[1] }));
582
+ }
583
+
584
+ for (let i = 0; i < 6; i++) {
585
+ const frontMat = faceMaterials[i * 2]; // Front face index
586
+ const backMat = faceMaterials[i * 2 + 1]; // Back face index
587
+
588
+ const materials = [
589
+ edgeMat, // Right edge
590
+ edgeMat, // Left edge (inside axle)
591
+ edgeMat, // Top edge
592
+ edgeMat, // Bottom edge
593
+ frontMat, // Front face (+Z local)
594
+ backMat, // Back face (-Z local)
595
+ ];
596
+
597
+ const panel = new THREE.Mesh(panelGeom, materials);
598
+ panel.rotation.y = i * (Math.PI / 3); // 60 degrees
599
+
600
+ const edges = new THREE.LineSegments(new THREE.EdgesGeometry(panelGeom), new THREE.LineBasicMaterial({ color: lineColor }));
601
+ panel.add(edges);
602
+ logoGroup.add(panel);
603
+ }
604
+
605
+ // --- 4. State & Interaction ---
606
+ let targetBaseY = 0; // The logical rotation of the carousel
607
+ let currentBaseY = 0;
608
+
609
+ let mouseTiltX = 0;
610
+ let mouseTiltY = 0;
611
+
612
+ let isDragging = false;
613
+ let previousMouseX = 0;
614
+ let idleAutoSpin = true;
615
+
616
+ // Search logic
617
+ searchInput.addEventListener("input", (e) => {
618
+ const query = e.target.value.trim();
619
+
620
+ if (searchDebounceId) {
621
+ clearTimeout(searchDebounceId);
622
+ }
623
+
624
+ if (!query) {
625
+ clearSearchResults();
626
+ return;
627
+ }
628
+
629
+ searchDebounceId = setTimeout(() => {
630
+ runSemanticSearch(query).catch((err) => {
631
+ console.error("Semantic search failed:", err);
632
+ subtitle.textContent = "Search failed. Please try again.";
633
+ });
634
+ }, 10);
635
+ });
636
+
637
+ // Mouse Dragging Logic
638
+ const container = document.getElementById("canvas-container");
639
+ container.addEventListener("mousedown", (e) => {
640
+ isDragging = true;
641
+ idleAutoSpin = false;
642
+ previousMouseX = e.clientX;
643
+ });
644
+
645
+ window.addEventListener("mouseup", () => (isDragging = false));
646
+
647
+ window.addEventListener("mousemove", (e) => {
648
+ // Mouse parallax tilt
649
+ mouseTiltX = (e.clientX / window.innerWidth) * 2 - 1;
650
+ mouseTiltY = -(e.clientY / window.innerHeight) * 2 + 1;
651
+
652
+ if (isDragging) {
653
+ const deltaX = e.clientX - previousMouseX;
654
+ targetBaseY += deltaX * 0.01; // Drag sensitivity
655
+ previousMouseX = e.clientX;
656
+ }
657
+ });
658
+
659
+ // Touch support
660
+ container.addEventListener("touchstart", (e) => {
661
+ isDragging = true;
662
+ idleAutoSpin = false;
663
+ previousMouseX = e.touches[0].clientX;
664
+ });
665
+ window.addEventListener("touchend", () => (isDragging = false));
666
+ window.addEventListener("touchmove", (e) => {
667
+ if (isDragging) {
668
+ const deltaX = e.touches[0].clientX - previousMouseX;
669
+ targetBaseY += deltaX * 0.01;
670
+ previousMouseX = e.touches[0].clientX;
671
+ }
672
+ });
673
+
674
+ window.addEventListener("resize", () => {
675
+ camera.aspect = window.innerWidth / window.innerHeight;
676
+ camera.updateProjectionMatrix();
677
+ renderer.setSize(window.innerWidth, window.innerHeight);
678
+ });
679
+
680
+ // --- 5. Smart Swap Animation Loop ---
681
+ function animate() {
682
+ requestAnimationFrame(animate);
683
+
684
+ if (idleAutoSpin && !isDragging) {
685
+ targetBaseY -= 0.002; // Slow continuous spin
686
+ }
687
+
688
+ // Smooth interpolation to target rotation
689
+ currentBaseY += (targetBaseY - currentBaseY) * 0.05;
690
+
691
+ // Apply main rotation + slight parallax tilt from mouse
692
+ logoGroup.rotation.y = currentBaseY;
693
+ logoGroup.rotation.x = mouseTiltY * 0.2 + 0.1;
694
+
695
+ // We evaluate the 6 "physical spreads". A spread is the V-shape pocket formed by two adjacent fins.
696
+ // Spread k is formed by the Back face of Fin k (Left) and the Front face of Fin k+1 (Right).
697
+ // When a spread rotates perfectly to the back (+90 deg from center), it is completely hidden.
698
+ // At that exact moment, we update its textures to the next required documents.
699
+ let currentDeg = (currentBaseY * 180) / Math.PI;
700
+
701
+ for (let k = 0; k < 6; k++) {
702
+ // Offset angle: Spread k is centered at k*60 + 30 degrees locally.
703
+ // It faces the back when its world angle crosses +90 degrees.
704
+ // We offset by 90 to make the swap boundary exactly at 0.
705
+ let A_offset = currentDeg + k * 60 - 60;
706
+
707
+ // Calculate how many full rotations this spread has made
708
+ let n_wrap = Math.floor(A_offset / 360);
709
+
710
+ // Determine which "Virtual Spread" this physical spread is currently representing.
711
+ // The -4 offset ensures that at 0 degrees, the front-facing spread (k=4) represents V=0 (Docs 1 & 2).
712
+ let k_virt = k - 6 * n_wrap - 4;
713
+
714
+ // Map virtual spread to carousel slots, then look up via slotToDoc
715
+ let slotLeft = (((2 * k_virt) % TOTAL_DOCS) + TOTAL_DOCS) % TOTAL_DOCS;
716
+ let slotRight = (((2 * k_virt + 1) % TOTAL_DOCS) + TOTAL_DOCS) % TOTAL_DOCS;
717
+ let docLeftId = slotToDoc[slotLeft];
718
+ let docRightId = slotToDoc[slotRight];
719
+
720
+ // Map to the specific left and right faces on the 3D model:
721
+ // Left page is the BACK face of Fin k
722
+ let leftFaceIdx = k * 2 + 1;
723
+ // Right page is the FRONT face of the next fin, Fin (k+1)%6
724
+ let rightFaceIdx = ((k + 1) % 6) * 2;
725
+
726
+ // Apply the textures if they aren't already set
727
+ if (faceMaterials[leftFaceIdx].map !== docTextures[docLeftId]) {
728
+ faceMaterials[leftFaceIdx].map = docTextures[docLeftId];
729
+ }
730
+ if (faceMaterials[rightFaceIdx].map !== docTextures[docRightId]) {
731
+ faceMaterials[rightFaceIdx].map = docTextures[docRightId];
732
+ }
733
+ }
734
+
735
+ renderer.render(scene, camera);
736
+ }
737
+
738
+ initializeSemanticSearch();
739
+ animate();
740
+ </script>
741
+ </body>
742
  </html>