hbauzan's picture
Release v4.17.7 — sync from GitHub main
819a47f
Raw
History Blame Contribute Delete
24.5 kB
import { STATE, CONFIG } from '../core/State.js';
import { refreshHUDMetadata } from './HUD.js';
import { clearAllReliefs, renderComparison } from '../visualizer/ReliefRenderer.js';
import { showDimensionInspector, hideDimensionInspector } from '../visualizer/DimensionInspector.js';
import { showCustomModal } from './modals/CustomModal.js';
import { showNamingModal } from './modals/NamingModal.js';
import { buildReliefHUD } from './ReliefHUD.js';
import { buildPolarityPanel } from './panels/PolarityPanel.js';
import { buildTelemetryPanel } from './panels/TelemetryPanel.js';
import { buildVectorMathPanel } from './panels/VectorMathPanel.js';
import { buildThreadsPanel } from './panels/ThreadsPanel.js';
import { buildLayoutPanel } from './panels/LayoutPanel.js';
import { wireSaeMetricsPanel } from './panels/SaeMetricsPanel.js';
import { buildBookmarksPanel } from './panels/BookmarksPanel.js';
import { reliefIngestMarkup } from './panels/ReliefPanel.js';
import { initProgressPanel } from './panels/ProgressPanel.js';
import { OnboardingTour, autoStartOnboarding } from './OnboardingTour.js';
// Re-export shared utils so existing importers keep resolving them here.
export { showCustomModal, showNamingModal };
// Cross-panel refresh registry. Re-pointed to the active PanelContext.refresh on
// every createSidebar() call so the module-level applyComparison() (also exposed as
// window.applyComparison for DimensionInspector) can reach the relief panel's
// dimension-list refresher without a closure global.
let activeRefresh = {};
// Aplica el modo de comparación actual: modos A/B son 3D (renderComparison);
// el modo C es el panel 2D del inspector de dimensión. Punto único de entrada
// que la Sidebar invoca ante cualquier cambio (modo, docs, visibilidad, dim).
export function applyComparison() {
const scene = STATE.scene;
if (!scene) return;
if (STATE.comparisonMode === 'C') {
clearAllReliefs(scene);
if (!STATE.reliefInspectDims) STATE.reliefInspectDims = [];
const targetDim = STATE.reliefPinned ? STATE.reliefPinned.dimIndex : (STATE.reliefInspectDim || 0);
STATE.reliefInspectDim = targetDim;
if (!STATE.reliefInspectDims.some(d => d.dim === targetDim)) {
STATE.reliefInspectDims.push({ dim: targetDim, visible: true });
}
// Sync input field value
const inspectDimInput = document.getElementById('inspect-dim-input');
if (inspectDimInput) {
inspectDimInput.value = STATE.reliefInspectDim;
}
if (typeof activeRefresh.dimList === 'function') {
activeRefresh.dimList();
}
// El modo C reemplaza la escena 3D por el panel inspector 2D en fullscreen.
// Ocultar el HUD flotante de relieve y la lectura de coordenadas evita colisiones.
const hud = document.getElementById('relief-hud');
if (hud) hud.style.display = 'none';
const coords = document.getElementById('relief-coords');
if (coords) coords.style.display = 'none';
showDimensionInspector();
} else {
hideDimensionInspector();
renderComparison(scene);
}
}
export function createSidebar(onScan, onHunterQuery, onCalculate, onBatchLoad, onClearAll) {
// Cleanup old UI components
['hud-override', 'search-container', 'hunter-modal', 'arithmetic-panel', 'about-modal', 'hud-sidebar', 'app-modal-overlay'].forEach(id => {
const el = document.getElementById(id);
if (el) el.remove();
});
// PanelContext — the single contract handed to every panel builder. Shared
// state and cross-panel refresh flow through this object, not closure globals.
activeRefresh = {};
/** @type {import('./types.js').PanelContext} */
const ctx = {
STATE, CONFIG,
onScan, onHunterQuery, onCalculate, onBatchLoad, onClearAll,
applyComparison, showCustomModal, showNamingModal,
refresh: activeRefresh,
};
// Seam read by visualizer/DimensionInspector.js (set synchronously, as before).
window.applyComparison = applyComparison;
// Create Sidebar container using CSS classes
const sidebar = document.createElement('div');
sidebar.id = 'hud-sidebar';
sidebar.className = 'app-panel app-sidebar';
// Prevent propagation to 3D controls
['mousedown', 'click', 'keydown', 'keyup'].forEach(evt => {
sidebar.addEventListener(evt, (e) => e.stopPropagation());
});
// 1. Header (System Info and Telemetry status)
const header = document.createElement('div');
header.className = 'section-card';
header.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: var(--spacing-xs);">
<span id="hud-title-text" style="font-weight:700; color: var(--color-neutral-50);">SEMANTIC LAB (Loading...)</span>
<div class="status-indicator">
<span class="status-dot"></span>
<span id="connection-status" style="color: var(--color-semantic-success);">ONLINE</span>
</div>
</div>
<div style="display:flex; justify-content:space-between; font-size: var(--font-size-caption); color: var(--color-brand-secondary); margin-bottom: var(--spacing-xs); font-family: var(--font-mono);">
<span id="hud-model-name">MODEL: --</span>
<span id="hud-provider-info">PROV: --</span>
</div>
<div style="display:flex; justify-content:space-between; align-items:center; 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);">
<div id="hud-engine-mode" style="font-size: var(--font-size-caption); font-weight: bold; color: var(--color-neutral-400);">
ENGINE: ${STATE.engineMode === 'MACRO' ? 'GPU' : 'CPU'}
</div>
<button id="btn-start-tour" class="btn btn-secondary" style="font-size: 10px; min-height: 20px; padding: 2px 6px; font-weight: bold;">💡 Tour</button>
</div>
`;
sidebar.appendChild(header);
// Create tab-bar header at the top (Task 9.1)
const tabContainer = document.createElement('div');
tabContainer.className = 'sidebar-tabs';
tabContainer.setAttribute('role', 'tablist');
tabContainer.innerHTML = `
<button id="tab-ingest" class="sidebar-tab-btn active" role="tab" aria-selected="true" aria-controls="tab-panel-ingest" data-testid="tab-ingest" tabindex="0">INGEST</button>
<button id="tab-explore" class="sidebar-tab-btn" role="tab" aria-selected="false" aria-controls="tab-panel-explore" data-testid="tab-explore" tabindex="-1">EXPLORE</button>
<button id="tab-analyze" class="sidebar-tab-btn" role="tab" aria-selected="false" aria-controls="tab-panel-analyze" data-testid="tab-analyze" tabindex="-1">ANALYZE</button>
`;
sidebar.appendChild(tabContainer);
// Create Tab Panels
const panelIngest = document.createElement('div');
panelIngest.id = 'tab-panel-ingest';
panelIngest.className = 'sidebar-tab-panel active';
panelIngest.setAttribute('role', 'tabpanel');
panelIngest.setAttribute('aria-labelledby', 'tab-ingest');
panelIngest.style.cssText = 'display: flex; flex-direction: column; gap: var(--spacing-sm); transition: opacity 0.25s ease-in-out; opacity: 1;';
const panelExplore = document.createElement('div');
panelExplore.id = 'tab-panel-explore';
panelExplore.className = 'sidebar-tab-panel';
panelExplore.setAttribute('role', 'tabpanel');
panelExplore.setAttribute('aria-labelledby', 'tab-explore');
panelExplore.style.cssText = 'display: none; flex-direction: column; gap: var(--spacing-sm); transition: opacity 0.25s ease-in-out; opacity: 0;';
const panelAnalyze = document.createElement('div');
panelAnalyze.id = 'tab-panel-analyze';
panelAnalyze.className = 'sidebar-tab-panel';
panelAnalyze.setAttribute('role', 'tabpanel');
panelAnalyze.setAttribute('aria-labelledby', 'tab-analyze');
panelAnalyze.style.cssText = 'display: none; flex-direction: column; gap: var(--spacing-sm); transition: opacity 0.25s ease-in-out; opacity: 0;';
// 1. Ingest tab content
const ingestWrapper = document.createElement('div');
ingestWrapper.innerHTML = reliefIngestMarkup();
panelIngest.appendChild(ingestWrapper.firstElementChild);
// 2. Explore tab content
panelExplore.appendChild(buildLayoutPanel(ctx));
// Container for COMPARE mode specific UI (inside Explore)
const compareModeContent = document.createElement('div');
compareModeContent.id = 'compare-mode-content';
compareModeContent.style.display = (STATE.layoutMode === 'COMPARE') ? 'block' : 'none';
panelExplore.appendChild(compareModeContent);
// 3. Search & File Loading Action Buttons
const searchSection = document.createElement('div');
searchSection.className = 'section-card';
searchSection.innerHTML = `
<div class="input-group">
<input id="sidebar-search" class="input-field" placeholder="Add Token...">
<button id="sidebar-add" class="btn btn-primary btn-square">+</button>
</div>
<div style="display:flex; gap: var(--spacing-xs);">
<button id="btn-batch" class="btn btn-secondary" style="flex:1; font-size: var(--font-size-small); font-weight: bold;">📂 BATCH</button>
<button id="btn-clear" class="btn btn-danger" style="flex:1; font-size: var(--font-size-small); font-weight: bold;">🗑️ CLEAR</button>
</div>
<input type="file" id="hidden-file-input" accept=".txt,.csv" style="display:none">
<div id="batch-progress-container" style="display:none; margin-top: var(--spacing-sm);">
<div style="font-size: var(--font-size-small); color: var(--color-neutral-400); margin-bottom: var(--spacing-xs);" id="batch-status">Processing...</div>
<progress id="batch-bar" value="0" max="100" style="width:100%; height:6px; border-radius: 3px; background: oklch(from var(--color-neutral-50) l c h / 0.05); overflow:hidden; border:none;"></progress>
</div>
`;
compareModeContent.appendChild(searchSection);
// 4. Polarity Filtering controls (inside Explore / compareModeContent)
compareModeContent.appendChild(buildPolarityPanel(ctx));
// 5. Active Threads (inside Explore / compareModeContent)
compareModeContent.appendChild(buildThreadsPanel(ctx));
// 6. Analyze tab content
panelAnalyze.appendChild(buildTelemetryPanel(ctx));
panelAnalyze.appendChild(buildVectorMathPanel(ctx));
panelAnalyze.appendChild(buildBookmarksPanel(ctx));
// Toolbar (Modal launcher buttons)
const toolbar = document.createElement('div');
toolbar.className = 'input-group';
toolbar.style.marginBottom = '0';
toolbar.innerHTML = `
<button id="btn-hunter" class="btn btn-secondary" style="flex:1;">🎯 HUNTER</button>
<button id="btn-about" class="btn btn-secondary" style="flex:1;">? ABOUT</button>
`;
panelAnalyze.appendChild(toolbar);
// Append panels to sidebar
sidebar.appendChild(panelIngest);
sidebar.appendChild(panelExplore);
sidebar.appendChild(panelAnalyze);
// Inject Sidebar into viewport
document.body.appendChild(sidebar);
// Observer to keep E2E tests happy by showing tab panels when they force compare-mode-content to be block
if (typeof MutationObserver !== 'undefined') {
const observer = new MutationObserver(() => {
if (compareModeContent.style.display === 'block') {
panelExplore.style.display = 'flex';
panelExplore.style.opacity = '1';
panelExplore.classList.add('active');
panelAnalyze.style.display = 'flex';
panelAnalyze.style.opacity = '1';
panelAnalyze.classList.add('active');
}
});
observer.observe(compareModeContent, { attributes: true, attributeFilter: ['style'] });
}
// Wire Tab Switching click event handlers
const tabBtns = [
{ btnId: 'tab-ingest', panel: panelIngest },
{ btnId: 'tab-explore', panel: panelExplore },
{ btnId: 'tab-analyze', panel: panelAnalyze }
];
tabBtns.forEach(tabInfo => {
const btn = tabContainer.querySelector(`#${tabInfo.btnId}`);
if (btn) {
btn.addEventListener('click', () => {
tabBtns.forEach(t => {
const b = tabContainer.querySelector(`#${t.btnId}`);
if (t.btnId === tabInfo.btnId) {
b.classList.add('active');
b.setAttribute('aria-selected', 'true');
b.setAttribute('tabindex', '0');
t.panel.style.display = 'flex';
setTimeout(() => {
t.panel.style.opacity = '1';
t.panel.classList.add('active');
}, 20);
} else {
b.classList.remove('active');
b.setAttribute('aria-selected', 'false');
b.setAttribute('tabindex', '-1');
t.panel.style.display = 'none';
t.panel.style.opacity = '0';
t.panel.classList.remove('active');
}
});
});
}
});
// Keyboard navigation (WCAG 2.1 AA) for Tab List
tabContainer.addEventListener('keydown', (e) => {
const btns = Array.from(tabContainer.querySelectorAll('.sidebar-tab-btn'));
const activeIndex = btns.indexOf(document.activeElement);
if (activeIndex === -1) return;
let nextIndex = activeIndex;
if (e.key === 'ArrowRight' || e.key === 'Right') {
nextIndex = (activeIndex + 1) % btns.length;
e.preventDefault();
} else if (e.key === 'ArrowLeft' || e.key === 'Left') {
nextIndex = (activeIndex - 1 + btns.length) % btns.length;
e.preventDefault();
} else if (e.key === 'Home') {
nextIndex = 0;
e.preventDefault();
} else if (e.key === 'End') {
nextIndex = btns.length - 1;
e.preventDefault();
} else if (e.key === ' ' || e.key === 'Enter') {
btns[activeIndex].click();
e.preventDefault();
return;
} else {
return;
}
btns.forEach((btn, idx) => {
if (idx === nextIndex) {
btn.setAttribute('tabindex', '0');
btn.focus();
} else {
btn.setAttribute('tabindex', '-1');
}
});
});
// Wire up events
setTimeout(() => {
// Initialize real-time SSE progress panel
initProgressPanel(ctx);
// Wire start tour button and auto-start tour
const btnStartTour = document.getElementById('btn-start-tour');
if (btnStartTour) {
btnStartTour.onclick = () => {
const tour = new OnboardingTour();
tour.start();
};
}
autoStartOnboarding();
// SAE feature-space panel (toggle/train/metrics + status polling).
wireSaeMetricsPanel(ctx);
const input = document.getElementById('sidebar-search');
const btn = document.getElementById('sidebar-add');
const trigger = () => {
if (input.value.trim()) {
onScan(input.value.trim(), true);
input.value = '';
}
};
btn.onclick = trigger;
input.onkeydown = (e) => { if (e.key === 'Enter') trigger(); };
const btnBatch = document.getElementById('btn-batch');
const btnClear = document.getElementById('btn-clear');
const fileInput = document.getElementById('hidden-file-input');
if (btnBatch) {
btnBatch.onclick = () => fileInput.click();
}
if (btnClear) {
btnClear.onclick = () => {
showCustomModal({
title: "Clear All Threads",
contentHTML: "<p>Are you sure you want to remove all active semantic trajectories from the vector space?</p>",
confirmText: "Clear Space",
cancelText: "Keep",
onConfirm: () => {
onClearAll();
}
});
};
}
if (fileInput) {
fileInput.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
const text = evt.target.result;
let lines = text.split(/\r?\n/).map(l => l.trim()).filter(l => l.length > 0);
if (lines.length > 1000) {
showCustomModal({
title: "File Too Large",
contentHTML: `<p>File contains ${lines.length} lines. It has been truncated to 1000 lines for visual performance.</p>`,
confirmText: "Proceed",
isAlert: true
});
lines = lines.slice(0, 1000);
}
if (lines.length > 0) {
onBatchLoad(lines);
}
fileInput.value = '';
};
reader.readAsText(file);
};
}
// Hunter Custom modal dialog launcher
document.getElementById('btn-hunter').onclick = () => {
if (STATE.threads.length === 0) {
showCustomModal({
title: "Signal Hunter",
contentHTML: "<p>You need to add at least one active thread/token before using the Signal Hunter tool.</p>",
confirmText: "OK",
isAlert: true
});
return;
}
let options = STATE.threads.map((t, i) => `<option value="${i}">${t.label}</option>`).join('');
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);">Locate dimensions where a specific token exhibits high activation values.</p>
<div style="display:flex; flex-direction:column; gap: var(--spacing-xs);">
<label style="font-size: var(--font-size-small); color: var(--color-neutral-400);">Target Token:</label>
<select id="hunter-token" class="input-field" style="background: oklch(from var(--color-neutral-950) l c h / 0.5);">${options}</select>
</div>
<div style="display:flex; flex-direction:column; gap: var(--spacing-xs);">
<label style="font-size: var(--font-size-small); color: var(--color-neutral-400);">Operator:</label>
<select id="hunter-op" class="input-field" style="background: oklch(from var(--color-neutral-950) l c h / 0.5);">
<option value=">">Greater (>)</option>
<option value="&lt;">Less (&lt;)</option>
<option value="abs>">Absolute Greater (|x| >)</option>
</select>
</div>
<div style="display:flex; flex-direction:column; gap: var(--spacing-xs);">
<label style="font-size: var(--font-size-small); color: var(--color-neutral-400);">Threshold:</label>
<input id="hunter-val" class="input-field" type="number" step="0.01" value="0.1">
</div>
<div style="display:flex; flex-direction:column; gap: var(--spacing-xs);">
<label style="font-size: var(--font-size-small); color: var(--color-neutral-400);">Max Matches:</label>
<input id="hunter-limit" class="input-field" type="number" min="1" max="500" value="10">
</div>
</div>
`;
showCustomModal({
title: "Signal Hunter",
contentHTML: content,
confirmText: "Start Scan",
cancelText: "Cancel",
onConfirm: (overlay) => {
const idx = overlay.querySelector('#hunter-token').value;
const op = overlay.querySelector('#hunter-op').value;
const val = parseFloat(overlay.querySelector('#hunter-val').value);
const limit = parseInt(overlay.querySelector('#hunter-limit').value) || 10;
onHunterQuery({ tokenIndex: idx, operator: op, value: val, limit: limit });
}
});
};
// Custom About modal launcher
document.getElementById('btn-about').onclick = () => {
const v = STATE.version;
const content = `
<div style="font-family: var(--font-sans); display: flex; flex-direction: column; gap: 8px;">
<div style="font-weight: 700; color: var(--color-brand-secondary);">${v.label} v${v.major}.${v.minor}.${v.patch}</div>
<div style="font-size: var(--font-size-small); color: var(--color-neutral-400); margin-bottom: var(--spacing-sm);">Build: ${v.build}</div>
<div style="font-weight: 600; border-bottom: 1px solid oklch(from var(--color-neutral-50) l c h / 0.05); padding-bottom: var(--spacing-xs); margin-bottom: var(--spacing-xs);">Navigation Controls</div>
<table style="width: 100%; border-collapse: collapse; font-size: var(--font-size-small);">
<tr><td style="padding: 4px 0; font-weight: bold; color: var(--color-brand-primary);">[W, A, S, D]</td><td style="color: var(--color-neutral-400);">Fly / Move</td></tr>
<tr><td style="padding: 4px 0; font-weight: bold; color: var(--color-brand-primary);">[SHIFT]</td><td style="color: var(--color-neutral-400);">Turbo Speed (Hold)</td></tr>
<tr><td style="padding: 4px 0; font-weight: bold; color: var(--color-brand-primary);">[CLICK]</td><td style="color: var(--color-neutral-400);">Target Lock (Toggle)</td></tr>
<tr><td style="padding: 4px 0; font-weight: bold; color: var(--color-brand-primary);">[F]</td><td style="color: var(--color-neutral-400);">Focus Camera</td></tr>
<tr><td style="padding: 4px 0; font-weight: bold; color: var(--color-brand-primary);">[ARROWS]</td><td style="color: var(--color-neutral-400);">Scan Dimensions</td></tr>
</table>
</div>
`;
showCustomModal({
title: "About Semantic Lab",
contentHTML: content,
confirmText: "Dismiss",
isAlert: true
});
};
}, 0);
// Floating relief overlays (crosshair, coordinate readout, relief HUD tooltip).
buildReliefHUD();
// Load system metadata / versions
async function loadVersionData() {
try {
const res = await fetch('./version.json');
if (res.ok) {
const data = await res.json();
STATE.version = data;
const title = document.getElementById('hud-title-text');
if (title) {
title.innerText = `${data.label} v${data.major}.${data.minor} (b${data.build})`;
}
} else {
throw new Error("Version file missing");
}
} catch (e) {
console.warn("Version load failed:", e);
const title = document.getElementById('hud-title-text');
if (title) title.innerText = "SEMANTIC LAB v5 (Offline)";
}
}
loadVersionData();
refreshHUDMetadata();
// Contract: return the main token-search input so the caller (main.js init)
// can enable/focus it. Losing this return aborts init() and black-screens the app.
return document.getElementById('sidebar-search');
}