/** * DimensionInspector.js * Modo C de comparación: inspector 2D de UNA dimensión a través de los chunks de * varios documentos. Elegimos un panel HTML/canvas (no líneas 3D) porque comparar * perfiles exactos (valores y cruces por cero) se lee mucho mejor en un plano 2D * con ejes y grilla que dentro de la escena 3D con cámara libre. * * Eje X = chunk_index (re-muestreado a una longitud común para alinear documentos * de distinto largo, coherente con STATE.reliefAlignMode). Eje Y = activación de la * dimensión fijada. Una serie coloreada por documento, con leyenda. */ import { STATE, visibleReliefDocs, removeReliefDoc, getMaxDimensions } from '../core/State.js'; import { dimensionSeries, resampleSeries } from './ReliefMath.js'; const PANEL_ID = 'dimension-inspector'; const CANVAS_ID = 'dimension-inspector-canvas'; function _hexCss(hex) { return '#' + (hex & 0xffffff).toString(16).padStart(6, '0'); } // Helpers for multi-dimension plotting function _getSeriesColor(baseColorHex, dIdx, totalDims) { if (totalDims <= 1) return _hexCss(baseColorHex); // Convert hex integer to RGB const r = ((baseColorHex >> 16) & 0xff) / 255; const g = ((baseColorHex >> 8) & 0xff) / 255; const b = (baseColorHex & 0xff) / 255; // RGB to HSL const max = Math.max(r, g, b), min = Math.min(r, g, b); let h, s, l = (max + min) / 2; if (max === min) { h = s = 0; // achromatic } else { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } // Shift hue and lightness slightly for different dimensions of the same document h = (h + (dIdx / totalDims) * 0.18) % 1.0; l = Math.max(0.4, Math.min(0.75, l + (dIdx - (totalDims - 1) / 2) * 0.08)); return `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`; } function _getLineDash(dIdx) { const dashes = [ [], // solid [6, 4], // dashed [2, 3], // dotted [8, 4, 2, 4], // dash-dotted [12, 6], // long dashed ]; return dashes[dIdx % dashes.length]; } function _getLegendSvg(dIdx, color) { const dasharray = _getLineDash(dIdx).join(','); return ``; } // Crea el panel una sola vez y lo devuelve. function _ensurePanel() { let panel = document.getElementById(PANEL_ID); if (panel) return panel; panel = document.createElement('div'); panel.id = PANEL_ID; panel.className = 'fullscreen-inspector'; panel.style.display = 'none'; const header = document.createElement('div'); header.className = 'inspector-header'; const titleCol = document.createElement('div'); titleCol.className = 'inspector-title-col'; const title = document.createElement('span'); title.id = PANEL_ID + '-title'; title.className = 'inspector-title'; title.textContent = 'Inspector de dimensión'; titleCol.appendChild(title); header.appendChild(titleCol); const actions = document.createElement('div'); actions.className = 'inspector-actions'; const aiButton = document.createElement('button'); aiButton.id = PANEL_ID + '-ai-btn'; aiButton.className = 'btn btn-secondary'; aiButton.style.cssText = 'font-size: 10px; min-height: 24px; padding: 2px 8px; display: flex; align-items: center; gap: 4px; cursor: pointer;'; aiButton.innerHTML = '🤖 Nombrar (IA)'; aiButton.onclick = (e) => { e.stopPropagation(); const dim = STATE.reliefInspectDim | 0; document.dispatchEvent(new CustomEvent('open-naming-modal', { detail: { dimIndex: dim } })); }; actions.appendChild(aiButton); const exportBtn = document.createElement('button'); exportBtn.id = PANEL_ID + '-export-btn'; exportBtn.className = 'btn btn-secondary'; exportBtn.style.cssText = 'font-size: 10px; min-height: 24px; padding: 2px 8px; display: flex; align-items: center; gap: 4px; cursor: pointer;'; exportBtn.innerHTML = '📋 Exportar para LLM'; exportBtn.onclick = async (e) => { e.stopPropagation(); const dim = STATE.reliefInspectDim | 0; const space = STATE.featureSpace || 'RAW'; const originalLabel = exportBtn.innerHTML; try { exportBtn.disabled = true; exportBtn.innerHTML = '⏳ Copiando...'; const markdownString = await exportDimensionContext(dim, space); await navigator.clipboard.writeText(markdownString); exportBtn.innerHTML = '¡Copiado!'; } catch (err) { console.error("Error copying dimension context to clipboard:", err); exportBtn.innerHTML = '❌ Error'; } finally { setTimeout(() => { exportBtn.disabled = false; exportBtn.innerHTML = originalLabel; }, 2000); } }; actions.appendChild(exportBtn); const closeBtn = document.createElement('button'); closeBtn.className = 'inspector-close-btn'; closeBtn.innerHTML = '✕'; closeBtn.onclick = (e) => { e.stopPropagation(); const compModeSelect = document.getElementById('comparison-mode-selector'); if (compModeSelect) { compModeSelect.value = 'A'; compModeSelect.dispatchEvent(new Event('change')); } else { STATE.comparisonMode = 'A'; if (typeof window.applyComparison === 'function') window.applyComparison(); } }; actions.appendChild(closeBtn); header.appendChild(actions); // Body container const body = document.createElement('div'); body.id = PANEL_ID + '-body'; body.className = 'inspector-body'; // Left Column (Canvas) const leftCol = document.createElement('div'); leftCol.className = 'inspector-left-col'; const canvas = document.createElement('canvas'); canvas.id = CANVAS_ID; canvas.style.cssText = 'flex:1; width:100%; height:100%; display:block;'; leftCol.appendChild(canvas); // Right Column (Controls/Legend) const rightCol = document.createElement('div'); rightCol.id = PANEL_ID + '-controls'; rightCol.className = 'inspector-right-col'; // Dimension Section const dimSec = document.createElement('div'); dimSec.style.cssText = 'display:flex; flex-direction:column; gap:4px;'; const dimHeader = document.createElement('div'); dimHeader.style.cssText = 'font-weight:bold; font-size:10px; color:var(--color-brand-primary); letter-spacing:0.5px;'; dimHeader.textContent = 'DIMENSIONES'; dimSec.appendChild(dimHeader); const dimAddRow = document.createElement('div'); dimAddRow.style.cssText = 'display:flex; align-items:center; gap:4px; margin-bottom:2px;'; const maxInspectorDim = getMaxDimensions() - 1; dimAddRow.innerHTML = ` `; dimSec.appendChild(dimAddRow); const dimSelectRow = document.createElement('div'); dimSelectRow.style.cssText = 'display:flex; align-items:center; gap:4px; margin-bottom:2px;'; dimSelectRow.innerHTML = ` `; dimSec.appendChild(dimSelectRow); const dimList = document.createElement('div'); dimList.id = 'inspector-dim-list'; dimList.style.cssText = 'display:flex; flex-direction:column; gap:3px; max-height:130px; overflow-y:auto; padding:3px; border:1px solid oklch(from var(--color-neutral-50) l c h / 0.06); border-radius:4px; background:oklch(from var(--color-neutral-950) l c h / 0.25);'; dimSec.appendChild(dimList); // Document Section const docSec = document.createElement('div'); docSec.style.cssText = 'display:flex; flex-direction:column; gap:4px; margin-top:2px;'; const docHeader = document.createElement('div'); docHeader.style.cssText = 'font-weight:bold; font-size:10px; color:var(--color-brand-secondary); letter-spacing:0.5px;'; docHeader.textContent = 'DOCUMENTOS (PDF)'; docSec.appendChild(docHeader); const docAddRow = document.createElement('div'); docAddRow.style.cssText = 'display:flex; align-items:center; gap:4px; margin-bottom:2px;'; docAddRow.innerHTML = ` `; docSec.appendChild(docAddRow); const docList = document.createElement('div'); docList.id = 'inspector-doc-list'; docList.style.cssText = 'display:flex; flex-direction:column; gap:3px; max-height:130px; overflow-y:auto; padding:3px; border:1px solid oklch(from var(--color-neutral-50) l c h / 0.06); border-radius:4px; background:oklch(from var(--color-neutral-950) l c h / 0.25);'; docSec.appendChild(docList); rightCol.appendChild(dimSec); rightCol.appendChild(docSec); body.appendChild(leftCol); body.appendChild(rightCol); panel.appendChild(header); panel.appendChild(body); const host = document.getElementById('app') || document.body; host.appendChild(panel); // Bind add dimension button const dimInput = dimAddRow.querySelector('#inspector-dim-input'); const dimAddBtn = dimAddRow.querySelector('#inspector-dim-add-btn'); const triggerAddDim = () => { const val = parseInt(dimInput.value, 10); const maxDim = getMaxDimensions() - 1; if (!Number.isNaN(val) && val >= 0 && val <= maxDim) { STATE.reliefInspectDim = val; if (!STATE.reliefInspectDims) STATE.reliefInspectDims = []; if (!STATE.reliefInspectDims.some(d => d.dim === val)) { STATE.reliefInspectDims.push({ dim: val, visible: true }); } // Sync to sidebar const sidebarInput = document.getElementById('inspect-dim-input'); if (sidebarInput) sidebarInput.value = val; if (typeof window.sidebarRefreshDims === 'function') window.sidebarRefreshDims(); renderDimensionInspector(); } }; dimAddBtn.onclick = triggerAddDim; dimInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); triggerAddDim(); } }; const dimSelectDropdown = dimSelectRow.querySelector('#inspector-dim-select-dropdown'); if (dimSelectDropdown) { dimSelectDropdown.onchange = function() { const val = parseInt(this.value, 10); STATE.reliefInspectDim = val; if (!STATE.reliefInspectDims) STATE.reliefInspectDims = []; if (!STATE.reliefInspectDims.some(d => d.dim === val)) { STATE.reliefInspectDims.push({ dim: val, visible: true }); } const sidebarInput = document.getElementById('inspect-dim-input'); if (sidebarInput) sidebarInput.value = val; if (typeof window.sidebarRefreshDims === 'function') window.sidebarRefreshDims(); renderDimensionInspector(); }; } // Bind add PDF button const pdfSelectEl = docAddRow.querySelector('#inspector-pdf-select'); const pdfAddBtn = docAddRow.querySelector('#inspector-pdf-add-btn'); pdfAddBtn.onclick = () => { const val = pdfSelectEl.value; if (val) { const sidebarSelect = document.getElementById('relief-file-selector'); const sidebarBtn = document.getElementById('btn-relief-visualize'); if (sidebarSelect && sidebarBtn) { sidebarSelect.value = val; sidebarBtn.click(); } } }; // Keep inspectorPdfSelect in sync with updates window.inspectorRefreshPdfSelect = () => { const sidebarSelect = document.getElementById('relief-file-selector'); if (sidebarSelect && pdfSelectEl) { const currentSelVal = pdfSelectEl.value; pdfSelectEl.innerHTML = ''; Array.from(sidebarSelect.options).forEach(opt => { if (opt.value) { // Skip empty placeholder const newOpt = document.createElement('option'); newOpt.value = opt.value; newOpt.textContent = opt.textContent; pdfSelectEl.appendChild(newOpt); } }); if (currentSelVal && Array.from(pdfSelectEl.options).some(o => o.value === currentSelVal)) { pdfSelectEl.value = currentSelVal; } } }; if (typeof ResizeObserver !== 'undefined') { const resizeObserver = new ResizeObserver(() => { if (panel.style.display !== 'none' && panel.classList.contains('show')) { renderDimensionInspector(); } }); resizeObserver.observe(leftCol); } return panel; } export function hideDimensionInspector() { const panel = document.getElementById(PANEL_ID); if (panel) { panel.classList.remove('show'); setTimeout(() => { if (!panel.classList.contains('show')) { panel.style.display = 'none'; } }, 300); } } export function showDimensionInspector() { const panel = _ensurePanel(); panel.style.display = 'flex'; void panel.offsetWidth; // force layout recalculation panel.classList.add('show'); if (typeof window.inspectorRefreshPdfSelect === 'function') { window.inspectorRefreshPdfSelect(); } if (typeof window.refreshAllDimensionSelects === 'function') { window.refreshAllDimensionSelects(); } renderDimensionInspector(); } export async function exportDimensionContext(dimIndex, space) { const spaceKey = `${space.toLowerCase()}_${dimIndex}`; const dictEntry = STATE.dimensionDictionary ? STATE.dimensionDictionary[spaceKey] : null; const name = dictEntry ? dictEntry.name : (STATE.dimensionLabels[spaceKey] || 'Sin nombre'); const rationale = dictEntry ? dictEntry.rationale : 'Sin justificación registrada.'; const activeFilenames = STATE.reliefDocs.filter(d => d.visible).map(d => d.filename); let topPos = []; let topNeg = []; if (STATE.provider && STATE.provider.ready) { try { const response = await STATE.provider.getTopChunksForDimension(dimIndex, activeFilenames, space, 8); topPos = response.top_activators || []; topNeg = response.bottom_activators || []; } catch (err) { console.error("Error fetching top chunks for dimension:", err); } } const lines = []; lines.push(`# Dimensión #${dimIndex} (${space.toUpperCase()})`); lines.push(``); lines.push(`- **Nombre Semántico:** ${name}`); lines.push(`- **Justificación:** ${rationale}`); lines.push(``); lines.push(`## Top Activadores (+)`); if (topPos.length > 0) { topPos.forEach(c => { lines.push(`- **[Valor: ${c.value.toFixed(4)}]** ${c.text.trim()} *(Archivo: ${c.filename}, Chunk: ${c.chunk_index})*`); }); } else { lines.push(`- No se encontraron activadores positivos.`); } lines.push(``); lines.push(`## Bottom Activadores (-)`); if (topNeg.length > 0) { topNeg.forEach(c => { lines.push(`- **[Valor: ${c.value.toFixed(4)}]** ${c.text.trim()} *(Archivo: ${c.filename}, Chunk: ${c.chunk_index})*`); }); } else { lines.push(`- No se encontraron activadores negativos/inhibidores.`); } return lines.join('\n'); } function _renderControls() { const dimList = document.getElementById('inspector-dim-list'); const docList = document.getElementById('inspector-doc-list'); if (!dimList || !docList) return; // 1. Render dimensions list dimList.innerHTML = ''; const dims = STATE.reliefInspectDims || []; if (dims.length === 0) { dimList.innerHTML = 'Sin dims comparadas.'; } else { const prefix = (STATE.featureSpace || 'RAW').toLowerCase(); dims.forEach((dObj, idx) => { const dim = dObj.dim; const savedName = STATE.dimensionLabels[`${prefix}_${dim}`]; const label = savedName ? `#${dim} [${savedName}]` : `#${dim}`; const row = document.createElement('div'); row.style.cssText = 'display:flex; align-items:center; justify-content:space-between; gap:4px; font-size:10px; padding:2px 4px; border-radius:2px; transition:background 0.15s; color:var(--color-neutral-300);'; row.onmouseenter = () => row.style.background = 'oklch(from var(--color-neutral-50) l c h / 0.03)'; row.onmouseleave = () => row.style.background = ''; // Swatch for this dimension pattern const svgIcon = _getLegendSvg(idx, 'var(--color-neutral-400)'); row.innerHTML = `