Spaces:
Sleeping
Sleeping
| 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)); | |
| } | |