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