Spaces:
Sleeping
Sleeping
| // Current Dimension / telemetry panel (extracted from Sidebar.js in H4 4.5). | |
| // Shows the selected dimension index + label, the [ETIQUETAR]/[NOMBRAR (IA)] | |
| // triggers, and the ANALYZE interpretation block. | |
| import { STATE, saveLabel, getMaxDimensions } from '../../core/State.js'; | |
| import { updateHUD } from '../HUD.js'; | |
| import { getHighVarianceDimensions } from '../../visualizer/ReliefMath.js'; | |
| /** @typedef {import('../types.js').PanelContext} PanelContext */ | |
| /** | |
| * Build the telemetry card and wire labeling + analysis triggers. | |
| * @param {PanelContext} ctx | |
| * @returns {HTMLElement} | |
| */ | |
| export function buildTelemetryPanel(ctx) { | |
| const { showCustomModal, showNamingModal } = ctx; | |
| const initialMaxDim = getMaxDimensions() - 1; | |
| const telemetry = document.createElement('div'); | |
| telemetry.id = 'telemetry-section'; | |
| telemetry.className = 'section-card'; | |
| telemetry.style.cssText = "text-align: center;"; | |
| telemetry.innerHTML = ` | |
| <div class="section-title">Current Dimension</div> | |
| <div style="font-size: 24px; font-weight: 700; color: var(--color-neutral-50); margin: var(--spacing-xs) 0; display: flex; align-items: center; justify-content: center; gap: 8px;"> | |
| IDX: <input id="telemetry-idx" type="number" min="0" max="${initialMaxDim}" value="0" style="background:transparent; border:none; border-bottom:2px solid var(--color-brand-primary); color:var(--color-neutral-50); width:60px; font-family: var(--font-mono); font-weight:700; font-size:24px; text-align:center; outline:none; padding: 0;"> | |
| </div> | |
| <div id="hud-dim-label" style="color: var(--color-brand-secondary); font-weight: 700; font-size: var(--font-size-h3); min-height: 18px; margin: var(--spacing-xs) 0; cursor: pointer; text-transform: uppercase; transition: color var(--motion-fast);"></div> | |
| <div style="display:flex; justify-content:center; gap:12px; margin-top: var(--spacing-xxs);"> | |
| <span id="btn-trigger-label" style="font-size: var(--font-size-small); color: var(--color-brand-primary); cursor: pointer; font-weight: 600;">[LABEL]</span> | |
| <span id="btn-ai-label-trigger" style="font-size: var(--font-size-small); color: var(--color-brand-secondary); cursor: pointer; font-weight: 600;">[🤖 NAME (AI)]</span> | |
| </div> | |
| <!-- Variance Intensity Slider & Dimension Selector (Phase 5) --> | |
| <div style="margin-top: var(--spacing-sm); border-top: 1px solid oklch(from var(--color-neutral-50) l c h / 0.05); padding-top: var(--spacing-xs); display:flex; flex-direction:column; gap:4px; text-align:left;"> | |
| <div style="display:flex; justify-content:space-between; align-items:center;"> | |
| <label for="dimension-strength-slider" style="font-size: var(--font-size-caption); color: var(--color-neutral-400);">Contrast limit (variance):</label> | |
| <span id="dimension-strength-val" style="font-size: var(--font-size-caption); color: var(--color-brand-primary); font-family: var(--font-mono);">${STATE.dimensionStrengthLimit}</span> | |
| </div> | |
| <input type="range" id="dimension-strength-slider" min="2" max="${Math.min(100, STATE.vertexCount || 768)}" step="1" value="${STATE.dimensionStrengthLimit}"> | |
| <label for="telemetry-dim-select" style="font-size: var(--font-size-caption); color: var(--color-neutral-400); margin-top: 4px;">Select dimension:</label> | |
| <select id="telemetry-dim-select" class="input-field" style="background: oklch(from var(--color-neutral-950) l c h / 0.5); font-size: var(--font-size-caption); margin-bottom: 2px;"> | |
| </select> | |
| </div> | |
| <!-- Dimension Interpretation --> | |
| <div id="dimension-interpretation" style="margin-top: var(--spacing-md); border-top: 1px solid oklch(from var(--color-neutral-50) l c h / 0.05); padding-top: var(--spacing-sm);"> | |
| <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: var(--spacing-xs);"> | |
| <span style="font-size: var(--font-size-small); color: var(--color-neutral-400); font-weight:600;">INTERPRETATION:</span> | |
| <button id="btn-analyze-dim" class="btn btn-secondary" style="font-size: var(--font-size-small); min-height: 24px; padding: 2px var(--spacing-sm);">🧠 ANALYZE</button> | |
| </div> | |
| <div id="dim-interp-label" style="color: var(--color-semantic-success); font-size: var(--font-size-body); min-height: 18px; font-style:italic; font-weight: 600;">[Waiting...]</div> | |
| <!-- Details Container --> | |
| <div id="dim-details-container" style="display:none; margin-top: var(--spacing-xs); text-align: left; font-family: var(--font-mono); font-size: var(--font-size-small);"> | |
| <div style="color: var(--color-semantic-success); margin-bottom: var(--spacing-xxs); word-break: break-all;"> | |
| <span style="font-weight:700;">(+):</span> <span id="dim-pos-list"></span> | |
| </div> | |
| <div style="color: var(--color-semantic-error); word-break: break-all;"> | |
| <span style="font-weight:700;">(-):</span> <span id="dim-neg-list"></span> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| // Set up Dimension label prompt overlay & API analysis triggers | |
| setTimeout(() => { | |
| const handleLabeling = (e) => { | |
| e.stopPropagation(); | |
| const currentIdx = STATE.currentSelectedIndex; | |
| const prefix = (STATE.featureSpace || 'RAW').toLowerCase(); | |
| const currentLabel = STATE.dimensionLabels[`${prefix}_${currentIdx}`] || ""; | |
| const content = ` | |
| <div style="display:flex; flex-direction:column; gap: var(--spacing-sm);"> | |
| <p style="font-size: var(--font-size-small); color: var(--color-neutral-400);">Assign a custom tag to explain the conceptual axis of Dimension ${currentIdx}.</p> | |
| <input id="modal-label-input" class="input-field" value="${currentLabel}" placeholder="e.g. GENDER, PLURALITY..." style="text-transform: uppercase;"> | |
| </div> | |
| `; | |
| showCustomModal({ | |
| title: `Label Dimension ${currentIdx}`, | |
| contentHTML: content, | |
| confirmText: "Save", | |
| cancelText: "Cancel", | |
| onConfirm: (overlay) => { | |
| const input = overlay.querySelector('#modal-label-input'); | |
| if (input) { | |
| saveLabel(currentIdx, input.value.trim()); | |
| updateHUD(); | |
| } | |
| } | |
| }); | |
| setTimeout(() => { | |
| const input = document.getElementById('modal-label-input'); | |
| if (input) { | |
| input.focus(); | |
| input.select(); | |
| } | |
| }, 50); | |
| }; | |
| const labelText = document.getElementById('hud-dim-label'); | |
| const triggerText = document.getElementById('btn-trigger-label'); | |
| const aiTriggerText = document.getElementById('btn-ai-label-trigger'); | |
| if (labelText) labelText.onclick = handleLabeling; | |
| if (triggerText) triggerText.onclick = handleLabeling; | |
| if (aiTriggerText) { | |
| aiTriggerText.onclick = (e) => { | |
| e.stopPropagation(); | |
| showNamingModal(STATE.currentSelectedIndex); | |
| }; | |
| } | |
| const analyzeBtn = document.getElementById('btn-analyze-dim'); | |
| if (analyzeBtn) { | |
| analyzeBtn.onclick = async (e) => { | |
| e.stopPropagation(); | |
| const labelEl = document.getElementById('dim-interp-label'); | |
| const inputEl = document.getElementById('telemetry-idx'); | |
| let targetIdx = STATE.currentSelectedIndex; | |
| if (inputEl) { | |
| const raw = parseInt(inputEl.value, 10); | |
| const maxDim = getMaxDimensions() - 1; | |
| if (!isNaN(raw) && raw >= 0 && raw <= maxDim) { | |
| targetIdx = raw; | |
| } else { | |
| showCustomModal({ | |
| title: "Invalid Input", | |
| contentHTML: `<p>Dimension must be between 0 and ${maxDim}</p>`, | |
| confirmText: "OK", | |
| isAlert: true | |
| }); | |
| return; | |
| } | |
| } | |
| if (labelEl) labelEl.innerHTML = '<span style="color:var(--color-semantic-warning)">⏳ Analyzing...</span>'; | |
| if (STATE.provider) { | |
| STATE.currentSelectedIndex = targetIdx; | |
| STATE.hoveredDimension = targetIdx; | |
| const analysis = await STATE.provider.getDimensionAnalysis(targetIdx); | |
| if (analysis) { | |
| STATE.currentAnalysis = { ...analysis, index: targetIdx }; | |
| updateHUD(); | |
| } else { | |
| if (labelEl) labelEl.innerText = "Analysis Failed"; | |
| } | |
| } | |
| }; | |
| } | |
| // Listen to AI Naming trigger from DimensionInspector (circular-dep free) | |
| document.addEventListener('open-naming-modal', (e) => { | |
| showNamingModal(e.detail.dimIndex); | |
| }); | |
| // Wire variance slider & select (Phase 5) | |
| const strengthSlider = document.getElementById('dimension-strength-slider'); | |
| const strengthVal = document.getElementById('dimension-strength-val'); | |
| const telemetryDimSelect = document.getElementById('telemetry-dim-select'); | |
| if (strengthSlider) { | |
| strengthSlider.oninput = function() { | |
| const val = parseInt(this.value, 10); | |
| STATE.dimensionStrengthLimit = val; | |
| if (strengthVal) strengthVal.textContent = val; | |
| refreshAllDimensionSelects(); | |
| }; | |
| } | |
| if (telemetryDimSelect) { | |
| telemetryDimSelect.onchange = function() { | |
| const val = parseInt(this.value, 10); | |
| STATE.currentSelectedIndex = val; | |
| const idxInput = document.getElementById('telemetry-idx'); | |
| if (idxInput) idxInput.value = val; | |
| updateHUD(); | |
| }; | |
| } | |
| refreshAllDimensionSelects(); | |
| }, 0); | |
| return telemetry; | |
| } | |
| /** | |
| * Recalcula y repopula todos los dropdowns de selección de dimensiones en la app. | |
| * Filtra según la varianza y el límite configurado en `STATE.dimensionStrengthLimit`. | |
| */ | |
| export function refreshAllDimensionSelects() { | |
| const limit = STATE.dimensionStrengthLimit || 100; | |
| const matrix = STATE.reliefData ? STATE.reliefData.matrix : null; | |
| const highVarDims = getHighVarianceDimensions(matrix, limit); | |
| const prefix = (STATE.featureSpace || 'RAW').toLowerCase(); | |
| const populateSelect = (selectEl, currentVal) => { | |
| if (!selectEl) return; | |
| selectEl.innerHTML = ''; | |
| highVarDims.forEach(({ dimIndex, variance }) => { | |
| const savedName = STATE.dimensionLabels ? STATE.dimensionLabels[`${prefix}_${dimIndex}`] : null; | |
| let label = savedName ? `#${dimIndex} [${savedName}]` : `#${dimIndex}`; | |
| if (variance > 0) { | |
| label += ` (var: ${variance.toFixed(4)})`; | |
| } | |
| const opt = document.createElement('option'); | |
| opt.value = dimIndex; | |
| opt.textContent = label; | |
| selectEl.appendChild(opt); | |
| }); | |
| // Select current value if it exists in the list | |
| if (currentVal !== undefined && Array.from(selectEl.options).some(o => parseInt(o.value) === currentVal)) { | |
| selectEl.value = currentVal; | |
| } | |
| }; | |
| // 1. Update Telemetry Panel selector | |
| const telemetrySelect = document.getElementById('telemetry-dim-select'); | |
| populateSelect(telemetrySelect, STATE.currentSelectedIndex); | |
| // 2. Update Relief Panel selector | |
| const reliefSelect = document.getElementById('inspect-dim-select'); | |
| populateSelect(reliefSelect, STATE.reliefInspectDim); | |
| // 3. Update Fullscreen Dimension Inspector selector | |
| const inspectorSelect = document.getElementById('inspector-dim-select-dropdown'); | |
| populateSelect(inspectorSelect, STATE.reliefInspectDim); | |
| } | |
| if (typeof window !== 'undefined') { | |
| window.refreshAllDimensionSelects = refreshAllDimensionSelects; | |
| } | |