import * as THREE from 'three'; import { PALETTE_3D } from '../engine/palette.js'; export const CONFIG = { debug: import.meta.env.DEV, // true in dev, false in production build pointSpacing: 0.1, amplitudeScale: 15.0, depthScale: 5.0, moveSpeed: 4.0, // Layout configuration threadSpacing: 2.0, threadWidth: 1.0, threadThickness: 1.0, compareSpacing: 0.0, // Spacing for COMPARE mode runBaseMultiplier: 2.0, runAcceleration: 10.0, runMaxMultiplier: 20.0, damping: 8.0, acceleration: 50.0, inspectionDistance: 3.0, // Colores de IDENTIDAD (Para la línea conectora) identityColors: PALETTE_3D.STATE_PALETTE, colors: { ghost: PALETTE_3D.STATE_GHOST // Auri Pink / Empatía }, // Z-Axis Configuration zModes: { LINEAR: 'linear', LOG: 'log', RADIAL: 'radial' }, zScale: { linear: 0.1, log: 0.01 }, // Calibrated: Log 0.01 * ID 1000 = 10.0 radialScale: 0.5, // Controls the density of the spiral/rotation // Rendering Configuration // gpuThreshold removed - Manual switching only }; const envDevice = (import.meta.env.DEVICE || import.meta.env.VITE_DEVICE || 'AUTO').toUpperCase().trim(); const defaultEngineMode = (envDevice === 'CPU') ? 'MICRO' : 'MACRO'; export const STATE = { version: { label: 'SEMANTIC LAB', major: 0, minor: 0, patch: 0, build: 'dev' }, modelName: 'Detecting...', providerType: 'Initializing...', appMode: 'local', isSandboxed: false, isReady: false, engineMode: defaultEngineMode, // Configured via DEVICE env var (default: MACRO/GPU) instancedMesh: null, // For MACRO mode gpuError: null, // Store GPU error message currentZMode: 'linear', // Default Z-Mode layoutMode: 'BLANKET', // 'COMPARE' or 'BLANKET' // --- Relieve Semántico (sub-modo de BLANKET) --- blanketSubMode: 'RELIEF', // 'THREADS' | 'RELIEF' reliefRenderMode: 'RIBBONS', // 'MESH' | 'POINTS' | 'RIBBONS' reliefScaleX: 0.05, // Compresión horizontal entre dimensiones (rango: 0.005–0.2) reliefScaleY: 15.0, // Amplificación de picos semánticos (rango: 1–50) reliefScaleZ: 1.5, // Profundidad entre chunks (rango: 0.1–10) reliefThickness: 2.0, // Grosor de puntos cuadrados en modo RIBBONS (rango: 1–10) reliefData: null, // Documento bajo inspección (HUD/export/búsqueda single-doc): apunta a una entrada de reliefDocs hasAutoLoadedRelief: false, // Flag para evitar bucles de carga automática en el arranque reliefAutoLoadInFlight: false, // True mientras corre la autocarga de boot: suprime el modal bloqueante de error (degrada in-panel) // --- Comparación multi-documento (Fase 1) --- // Colección de documentos activos en la sesión de comparación. Cada entrada: // { id, filename, matrix, text_metadata, chunks_count, dimensions_count, color, label, visible } // `color` es un hex 0xRRGGBB tomado de la paleta categórica (CONFIG.identityColors). reliefDocs: [], comparisonMode: 'A', // 'A' lado a lado | 'B' overlay/delta | 'C' inspector de dimensión (panel 2D) reliefAlignMode: 'NORMALIZED',// Alineación de ejes Z para modos B/C: 'NORMALIZED' [0,1] (re-muestreo) | 'INDEX' (recorta al más corto) reliefSideGap: 12.0, // Modo A: separación entre terrenos contiguos (en unidades de mundo, sobre X) reliefBSubMode: 'OVERLAY', // Modo B: 'OVERLAY' (relieves translúcidos) | 'DELTA' (A - B divergente) reliefDeltaPair: { a: null, b: null }, // Modo B/Delta: ids de los dos documentos a restar (A - B) reliefInspectDim: 0, // Modo C: índice de dimensión fijada para el inspector 2D reliefInspectDims: [{ dim: 0, visible: true }], // Modo C: lista de dimensiones a comparar ({ dim: number, visible: boolean }) reliefHover: null, // { chunkIndex, dimIndex } bajo la mira en el frame actual (volátil) reliefPinned: null, // { chunkIndex, dimIndex } fijado por clic — congela el HUD y habilita exportar reliefIngestChunkSize: 512, reliefIngestChunkOverlap: 50, reliefIngestNormalize: true, reliefIngestClean: false, reliefSearchQuery: '', reliefSearchResults: null, radialStep: 2.4, // Approx Golden Angle analyzer: null, provider: null, vertexCount: 0, threads: [], currentSelectedIndex: 0, manifest: null, polarityMode: 'ALL', // 'ALL', 'POS', 'NEG' dimensionLabels: JSON.parse(localStorage.getItem('semantic_labels') || '{}'), dimensionDictionary: {}, featureSpace: 'RAW', // 'RAW' | 'SAE' saeHiddenDim: 8192, // Dinámico según la configuración del modelo SAE cargado // --- Multi-modelo (Fase 4, experimental) --- // Eje composable e independiente del toggle RAW/SAE. Cada modelo es un espacio // vectorial propio (mpnet 768D, minilm 384D); las dimensiones X NO son comparables // entre modelos. La comparacion lado a lado lee matrices CACHEADAS por modelo. models: [], // registro recibido de GET /models: [{ key, hf_name, dim, table, notes }] activeModelKey: 'mpnet', // modelo vivo en el backend (uno por vez) defaultModelKey: 'mpnet', // modelo base del estudio (no se abandona) selectedModelKey: 'mpnet', // seleccion de la UI para cargar/re-ingestar/comparar filesModels: {}, // { filename: [modelKey,...] } estado de cache por documento // Signal Hunter State hunterResults: [], hunterIndex: 0, hunterActive: false, // Vector Arithmetic State ghostThread: null, arithmeticActive: false, // Navigation isLockedOn: false, isTargetLocked: false, // New Target Lock State railLock: false, // Rail Lock restaurado currentRunMultiplier: 1.0, // Dynamic Turbo State // Physics velocity: new THREE.Vector3(), direction: new THREE.Vector3(), moveState: { forward: false, backward: false, left: false, right: false, up: false, down: false, shift: false }, // Focus System focusedThreads: new Set(), hiddenThreads: new Set(), // Analysis Cache dimensionCache: {}, // --- Math Similarity & Bookmarks (Phase 5) --- similarityTolerance: 0.01, similarityCoverage: 80, dimensionStrengthLimit: 100, bookmarks: (() => { try { const raw = localStorage.getItem('sem_visualizer_bookmarks'); if (raw) { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) return parsed; } } catch (e) { console.error("Failed to parse bookmarks from localStorage:", e); } return []; })() }; /** * Retorna el número máximo de dimensiones activas según el espacio (RAW vs SAE) * y los documentos cargados en memoria. * @returns {number} */ export function getMaxDimensions() { if (STATE.featureSpace === 'SAE') { if (STATE.reliefDocs && STATE.reliefDocs[0] && STATE.reliefDocs[0].dimensions_count) { return STATE.reliefDocs[0].dimensions_count; } return STATE.saeHiddenDim || 8192; } if (STATE.reliefDocs && STATE.reliefDocs[0] && STATE.reliefDocs[0].dimensions_count) { return STATE.reliefDocs[0].dimensions_count; } return STATE.vertexCount || 768; } // --------------------------------------------------------------------------- // Gestión de la colección multi-documento (Fase 1) // --------------------------------------------------------------------------- let _reliefDocSeq = 0; // Color categórico estable para el documento en la posición `index` de la paleta. export function reliefDocColor(index) { const palette = CONFIG.identityColors; return palette[index % palette.length]; } // Etiqueta legible y corta para un documento. Los PDFs con clave tipo UUID // (nombre original perdido) caen a un fragmento del texto o a los primeros chars. function _deriveDocLabel(docData) { const name = docData.filename || ''; const isUuidKey = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.pdf$/i.test(name) || /^upload_[0-9a-f-]+\.pdf$/i.test(name); if (isUuidKey) { const preview = (docData.text_metadata && docData.text_metadata[0]) || ''; return preview ? preview.slice(0, 24) : name.slice(0, 8); } return name.replace(/\.pdf$/i, ''); } // Devuelve el documento activo para un (filename, modelKey) dado, o null. // La identidad de un documento en la colección es (filename × modelo): el MISMO PDF // bajo dos modelos son dos entradas distintas (espacios vectoriales incompatibles). export function getReliefDocByKey(filename, modelKey) { return STATE.reliefDocs.find(d => d.filename === filename && d.modelKey === modelKey) || null; } export function getReliefDoc(id) { return STATE.reliefDocs.find(d => d.id === id) || null; } // Agrega un documento a la colección activa (idempotente por filename × modelo). // `docData` es lo que devuelve /get_relief_matrix: { filename, matrix, text_metadata, // chunks_count, dimensions_count, model_key }. Asigna id, color de paleta, label y // visible=true. Devuelve la entrada (nueva o existente). export function addReliefDoc(docData) { const modelKey = docData.model_key || docData.modelKey || STATE.activeModelKey || 'mpnet'; const existing = getReliefDocByKey(docData.filename, modelKey); if (existing) { // Refrescar la matriz por si se re-ingestó, conservando color/label/visible/id. existing.matrix = docData.matrix; existing.text_metadata = docData.text_metadata; existing.chunks_count = docData.chunks_count; existing.dimensions_count = docData.dimensions_count; return existing; } const index = STATE.reliefDocs.length; const baseLabel = _deriveDocLabel(docData); const doc = { id: `doc-${++_reliefDocSeq}`, filename: docData.filename, modelKey, matrix: docData.matrix, text_metadata: docData.text_metadata || [], chunks_count: docData.chunks_count || (docData.matrix ? docData.matrix.length : 0), dimensions_count: docData.dimensions_count || (docData.matrix && docData.matrix[0] ? docData.matrix[0].length : 768), color: reliefDocColor(index), // Etiqueta multi-modelo: incluye la clave del modelo para distinguir el mismo // PDF bajo distintos espacios en la lista y en el render lado a lado (Modo A). label: `${baseLabel} · ${modelKey}`, baseLabel, visible: true, }; STATE.reliefDocs.push(doc); return doc; } // Quita un documento de la colección. Si era el documento bajo inspección, // reapunta reliefData al primer documento restante (o null). export function removeReliefDoc(id) { const idx = STATE.reliefDocs.findIndex(d => d.id === id); if (idx === -1) return; const removed = STATE.reliefDocs[idx]; STATE.reliefDocs.splice(idx, 1); // Reasignar colores por posición para que la paleta quede contigua. STATE.reliefDocs.forEach((d, i) => { d.color = reliefDocColor(i); }); if (STATE.reliefData === removed) { STATE.reliefData = STATE.reliefDocs[0] || null; } if (STATE.reliefDeltaPair.a === id) STATE.reliefDeltaPair.a = null; if (STATE.reliefDeltaPair.b === id) STATE.reliefDeltaPair.b = null; } // Documentos visibles, en orden de inserción. export function visibleReliefDocs() { return STATE.reliefDocs.filter(d => d.visible); } // Guardar Etiqueta Manual export function saveLabel(index, label, rationale = '', confidence = 'high', evidence = {}, source = 'human') { const space = (STATE.featureSpace || 'RAW').toLowerCase(); const key = `${space}_${index}`; if (!label || label.trim() === '') { delete STATE.dimensionLabels[key]; if (STATE.dimensionDictionary) { delete STATE.dimensionDictionary[key]; } } else { const uppercaseLabel = label.toUpperCase(); STATE.dimensionLabels[key] = uppercaseLabel; if (!STATE.dimensionDictionary) { STATE.dimensionDictionary = {}; } STATE.dimensionDictionary[key] = { name: label, rationale: rationale, confidence: confidence, evidence_snapshot: evidence, source: source, updated_at: new Date().toISOString() }; } localStorage.setItem('semantic_labels', JSON.stringify(STATE.dimensionLabels)); // Persist to server dimension dictionary if provider is ready if (STATE.provider && STATE.provider.ready) { if (!label || label.trim() === '') { STATE.provider.updateDimensionDictionary(key, { name: '', rationale: '', confidence: 'low', evidence_snapshot: {}, source: 'human', updated_at: new Date().toISOString() }).catch(err => console.error("Failed to delete dimension on server:", err)); } else { const entry = STATE.dimensionDictionary[key]; STATE.provider.updateDimensionDictionary(key, entry) .catch(err => console.error("Failed to save dimension on server:", err)); } } } // --- Bookmarking (Phase 5) --- let _lastBookmarkId = 0; function getUniqueBookmarkId() { let id = Date.now(); if (id <= _lastBookmarkId) { id = _lastBookmarkId + 1; } _lastBookmarkId = id; return id; } export function addBookmark(name, docId, chunkIndex, dimIndex, value) { const bookmark = { id: getUniqueBookmarkId(), name, docId, chunkIndex, dimIndex, value, createdAt: new Date().toISOString() }; if (!STATE.bookmarks) { STATE.bookmarks = []; } STATE.bookmarks.push(bookmark); localStorage.setItem('sem_visualizer_bookmarks', JSON.stringify(STATE.bookmarks)); return bookmark; } export function removeBookmark(id) { if (!STATE.bookmarks) return; STATE.bookmarks = STATE.bookmarks.filter(bm => bm.id !== id); localStorage.setItem('sem_visualizer_bookmarks', JSON.stringify(STATE.bookmarks)); }