llm-semantic-visualizer / src /ui /panels /TelemetryPanel.js
hbauzan's picture
Release v4.17.7 — sync from GitHub main
819a47f
Raw
History Blame Contribute Delete
12.5 kB
// 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;
}