Spaces:
Sleeping
Sleeping
| import * as THREE from 'three'; | |
| import { RemoteProvider } from './data/RemoteProvider.js'; | |
| import { CONFIG, STATE, addReliefDoc } from './core/State.js'; | |
| import { injectUI } from './ui/UIManager.js'; | |
| import { updateHUD, refreshHUDMetadata } from './ui/HUD.js'; | |
| import { updateThreadListUI } from './ui/Components.js'; | |
| import { createThread, createGhostThread } from './visualizer/MeshFactory.js'; | |
| import { clearThreads, updateThreadVisibility } from './visualizer/ThreadManager.js'; | |
| import { updateAllThreadPositions } from './visualizer/LayoutEngine.js'; | |
| import { initInstancedMesh, updateInstancedCloud } from './visualizer/Instancer.js'; | |
| import { initScene } from './engine/SceneSetup.js'; | |
| import { setupInputListeners } from './engine/Interaction.js'; | |
| import { updateNavigation } from './engine/Navigation.js'; | |
| import { PALETTE_3D } from './engine/palette.js'; | |
| import { getActiveReliefObjects, renderRelief, renderComparison, updateReliefScale } from './visualizer/ReliefRenderer.js'; | |
| import { initAxisGizmo } from './engine/AxisGizmo.js'; | |
| import { startRenderLoop } from './engine/RenderLoop.js'; | |
| import { computeReliefIndices, fillReliefHUD, setupReliefBookmarkHotkey } from './core/ReliefHUDController.js'; | |
| import { runHunterQuery as runHunterQueryImpl } from './core/HunterController.js'; | |
| import { wireReliefExportButton } from './core/ExportController.js'; | |
| import { bootstrapUiTheme } from './ui/themeBootstrap.js'; | |
| bootstrapUiTheme(); | |
| const { scene, camera, renderer, controls } = initScene(); | |
| initAxisGizmo(); | |
| setupInputListeners(camera, controls); | |
| setupReliefBookmarkHotkey(camera, controls); | |
| const runHunterQuery = (criteria) => runHunterQueryImpl(camera, criteria); | |
| if (CONFIG.debug && typeof window !== 'undefined') { | |
| window.__semlab = { | |
| scene, camera, renderer, controls, STATE, CONFIG, updateNavigation, | |
| renderRelief, updateReliefScale, getActiveReliefObjects, | |
| renderComparison, addReliefDoc, | |
| setComparisonMode: (mode) => { | |
| STATE.comparisonMode = mode; | |
| if (typeof window.applyComparison === 'function') window.applyComparison(); | |
| }, | |
| }; | |
| } | |
| async function fetchAnalysis() { | |
| if (!STATE.provider) return; | |
| const idx = STATE.currentSelectedIndex; | |
| const analysis = await STATE.provider.getDimensionAnalysis(idx); | |
| if (analysis) { | |
| STATE.currentAnalysis = { ...analysis, index: idx }; | |
| updateHUD(camera); | |
| } | |
| } | |
| async function processInput(text, appendMode) { | |
| if (!STATE.provider) return; | |
| if (CONFIG.debug) console.log(`[MAIN] Processing: "${text}" (Append: ${appendMode})`); | |
| if (!appendMode) { | |
| clearThreads(scene); | |
| STATE.focusedThreads.clear(); | |
| } | |
| const data = await STATE.provider.tokenizeAndEmbed(text); | |
| const tokenList = data.tokens || []; | |
| if (tokenList.length === 0) { | |
| console.warn('No tokens returned from backend.'); | |
| return; | |
| } | |
| const total = tokenList.length; | |
| if (CONFIG.debug) console.log(`[MAIN] Received ${total} tokens.`); | |
| if (STATE.engineMode === 'MACRO') { | |
| let allThreads = []; | |
| if (appendMode && STATE.threads.length > 0) { | |
| allThreads = STATE.threads.map(t => ({ | |
| id: t.tokenID, | |
| label: t.label, | |
| vector: t.embedding, | |
| embedding: t.embedding, | |
| color: t.color, | |
| tokenID: t.tokenID, | |
| labelSprite: t.labelSprite, | |
| })); | |
| } else { | |
| if (!appendMode) STATE.threads = []; | |
| allThreads = [...STATE.threads]; | |
| } | |
| for (let i = 0; i < total; i++) { | |
| const { token, id, vector } = tokenList[i]; | |
| allThreads.push({ | |
| id, | |
| label: token, | |
| vector, | |
| embedding: vector, | |
| color: new THREE.Color(CONFIG.identityColors[allThreads.length % CONFIG.identityColors.length]), | |
| tokenID: id, | |
| }); | |
| } | |
| STATE.threads = allThreads; | |
| try { | |
| if (STATE.instancedMesh) { | |
| scene.remove(STATE.instancedMesh); | |
| STATE.instancedMesh.geometry.dispose(); | |
| STATE.instancedMesh.material.dispose(); | |
| STATE.instancedMesh = null; | |
| } | |
| initInstancedMesh(STATE.threads.length, scene); | |
| updateInstancedCloud(STATE.threads); | |
| STATE.gpuError = null; | |
| } catch (e) { | |
| console.error('GPU Render Failed:', e); | |
| STATE.gpuError = 'GPU INIT FAILED'; | |
| STATE.engineMode = 'MICRO'; | |
| } | |
| } | |
| if (STATE.engineMode === 'MICRO') { | |
| if (STATE.gpuError) { | |
| clearThreads(scene); | |
| console.warn('Fell back to CPU. Rebuilding...'); | |
| } | |
| for (let i = 0; i < total; i++) { | |
| const { token, id, vector } = tokenList[i]; | |
| const colorIndex = STATE.threads.length % CONFIG.identityColors.length; | |
| const colorHex = CONFIG.identityColors[colorIndex]; | |
| const thread = createThread(token, vector, scene, colorHex, id); | |
| STATE.threads.push(thread); | |
| if (i % 10 === 0) await new Promise(r => setTimeout(r, 10)); | |
| } | |
| updateAllThreadPositions(); | |
| updateThreadVisibility(); | |
| } | |
| if (!appendMode && STATE.threads.length > 0) { | |
| STATE.currentSelectedIndex = 0; | |
| camera.position.set(-5, 2, CONFIG.inspectionDistance * 2); | |
| camera.lookAt(10, 0, 0); | |
| } | |
| updateThreadListUI(); | |
| updateHUD(camera); | |
| } | |
| async function handleVectorMath(termA, termB, termC, topK = 5) { | |
| if (!STATE.provider) return; | |
| if (CONFIG.debug) console.log(`[MATH] Calculating: ${termA} - ${termB} + ${termC} (Top ${topK})`); | |
| clearThreads(scene); | |
| STATE.focusedThreads.clear(); | |
| try { | |
| const [resA, resB, resC, resultData] = await Promise.all([ | |
| STATE.provider.getEmbedding(termA), | |
| STATE.provider.getEmbedding(termB), | |
| STATE.provider.getEmbedding(termC), | |
| STATE.provider.getArithmetic(termA, termB, termC, topK), | |
| ]); | |
| if (!resA || !resB || !resC || !resultData) { | |
| throw new Error('Failed to fetch data from backend'); | |
| } | |
| const { embedding: vecA, token_id: idA } = resA; | |
| const { embedding: vecB, token_id: idB } = resB; | |
| const { embedding: vecC, token_id: idC } = resC; | |
| const resultVec = resultData.vector; | |
| const results = resultData.results || []; | |
| const topResult = results.length > 0 ? results[0] : { word: 'Unknown', score: 0, token_id: 0 }; | |
| if (STATE.engineMode === 'MACRO') { | |
| STATE.threads.push({ | |
| id: idA, | |
| label: termA, | |
| vector: vecA, | |
| embedding: vecA, | |
| color: new THREE.Color(PALETTE_3D.THREAD_COMPARE_A), | |
| tokenID: idA, | |
| }); | |
| STATE.threads.push({ | |
| id: idB, | |
| label: termB, | |
| vector: vecB, | |
| embedding: vecB, | |
| color: new THREE.Color(PALETTE_3D.THREAD_COMPARE_B), | |
| tokenID: idB, | |
| }); | |
| STATE.threads.push({ | |
| id: idC, | |
| label: termC, | |
| vector: vecC, | |
| embedding: vecC, | |
| color: new THREE.Color(PALETTE_3D.THREAD_COMPARE_C), | |
| tokenID: idC, | |
| }); | |
| STATE.threads.push({ | |
| id: 'ghost-' + topResult.token_id, | |
| label: `RESULT: ${topResult.word}`, | |
| vector: resultVec, | |
| embedding: resultVec, | |
| color: new THREE.Color(PALETTE_3D.THREAD_RESULT), | |
| tokenID: topResult.token_id, | |
| isGhost: true, | |
| }); | |
| } else { | |
| STATE.threads.push(createThread(termA, vecA, scene, PALETTE_3D.THREAD_COMPARE_A, idA)); | |
| STATE.threads.push(createThread(termB, vecB, scene, PALETTE_3D.THREAD_COMPARE_B, idB)); | |
| STATE.threads.push(createThread(termC, vecC, scene, PALETTE_3D.THREAD_COMPARE_C, idC)); | |
| const ghost = createGhostThread(resultVec, `RESULT: ${topResult.word}`, scene, topResult.token_id); | |
| STATE.threads.push(ghost); | |
| } | |
| if (CONFIG.debug) console.log(`[MATH] Result: ${topResult.word} (Sim: ${topResult.score.toFixed(4)})`); | |
| const resultsList = document.getElementById('math-results'); | |
| if (resultsList) { | |
| resultsList.innerHTML = ''; | |
| results.forEach(r => { | |
| const li = document.createElement('li'); | |
| li.style.cssText = 'font-size:10px; color:var(--color-semantic-warning); display:flex; justify-content:space-between; padding:2px 0; border-bottom:1px solid oklch(from var(--color-semantic-warning) l c h / 0.2);'; | |
| li.innerHTML = `<span>${r.word}</span><span style="color:var(--color-neutral-400);">${r.score.toFixed(3)}</span>`; | |
| resultsList.appendChild(li); | |
| }); | |
| } | |
| STATE.currentSelectedIndex = 0; | |
| camera.position.set(-5, 2, CONFIG.inspectionDistance * 2); | |
| camera.lookAt(10, 0, 0); | |
| } catch (e) { | |
| console.error('Math Error:', e); | |
| alert('Error performing vector arithmetic. See console.'); | |
| } | |
| updateThreadListUI(); | |
| updateHUD(camera); | |
| updateAllThreadPositions(); | |
| updateThreadVisibility(); | |
| } | |
| async function handleBatchLoad(lines) { | |
| if (!STATE.provider) return; | |
| const progressContainer = document.getElementById('batch-progress-container'); | |
| const progressBar = document.getElementById('batch-bar'); | |
| const statusText = document.getElementById('batch-status'); | |
| if (progressContainer) progressContainer.style.display = 'block'; | |
| if (CONFIG.debug) console.log(`[BATCH] Starting batch load of ${lines.length} tokens...`); | |
| const total = lines.length; | |
| for (let i = 0; i < total; i++) { | |
| const word = lines[i]; | |
| if (progressBar) progressBar.value = Math.round(((i + 1) / total) * 100); | |
| if (statusText) statusText.innerText = `Loading ${i + 1}/${total}: ${word}`; | |
| await processInput(word, true); | |
| if (i % 5 === 0) await new Promise(r => setTimeout(r, 10)); | |
| } | |
| if (progressContainer) progressContainer.style.display = 'none'; | |
| if (CONFIG.debug) console.log('[BATCH] Complete.'); | |
| } | |
| function handleClearAll() { | |
| clearThreads(scene); | |
| STATE.threads = []; | |
| STATE.focusedThreads.clear(); | |
| STATE.hiddenThreads.clear(); | |
| STATE.hunterResults = []; | |
| STATE.hunterActive = false; | |
| updateThreadListUI(); | |
| updateHUD(camera); | |
| if (CONFIG.debug) console.log('[MAIN] All threads cleared.'); | |
| } | |
| async function init() { | |
| STATE.provider = new RemoteProvider(); | |
| await STATE.provider.init(); | |
| refreshHUDMetadata(); | |
| const input = injectUI(processInput, runHunterQuery, handleVectorMath, handleBatchLoad, handleClearAll); | |
| if (input) { | |
| input.disabled = false; | |
| input.placeholder = 'Ready. Type sentence...'; | |
| input.focus(); | |
| } else { | |
| console.error('[MAIN] Token-search input not found; UI wiring degraded but engine continues.'); | |
| } | |
| wireReliefExportButton(); | |
| STATE.isReady = true; | |
| startRenderLoop({ | |
| scene, | |
| camera, | |
| renderer, | |
| controls, | |
| computeReliefIndices, | |
| fillReliefHUD, | |
| }); | |
| fetchAnalysis(); | |
| } | |
| init(); | |