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 = `${r.word}${r.score.toFixed(3)}`; 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();