hbauzan's picture
Release v4.17.7 — sync from GitHub main
819a47f
Raw
History Blame Contribute Delete
10.5 kB
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();