llm-semantic-visualizer / src /ui /modals /NamingModal.js
hbauzan's picture
Release v4.17.7 — sync from GitHub main
819a47f
Raw
History Blame Contribute Delete
16.1 kB
// AI dimension-naming modal (Phase 2), extracted from Sidebar.js in H4.
// Pulls top/bottom activator evidence for a dimension, asks the remote provider
// (Gemini) for a proposed name, and lets the user accept/edit/save the label.
import { STATE, saveLabel } from '../../core/State.js';
import { updateHUD } from '../HUD.js';
import { renderDimensionInspector } from '../../visualizer/DimensionInspector.js';
import { showCustomModal } from './CustomModal.js';
/**
* Open the "Nombrar Dimensión" modal for the given dimension index.
* @param {number} dimIndex - Dimension to name (0-based).
* @returns {void}
*/
export function showNamingModal(dimIndex) {
const activeFilenames = STATE.reliefDocs.filter(d => d.visible).map(d => d.filename);
// HTML structure for modal content
const contentHTML = `
<div style="display:flex; flex-direction:column; gap: var(--spacing-sm); max-width: 600px; width: 90vw; max-height: 80vh; overflow-y: auto; padding-right: 4px;" class="custom-scrollbar">
<p style="font-size: var(--font-size-small); color: var(--color-neutral-400);">
Analyzing semantic evidence for Dimension <strong>#${dimIndex}</strong>.
</p>
<!-- Evidencia Section -->
<div style="border: 1px solid oklch(from var(--color-neutral-50) l c h / 0.05); border-radius: 6px; background: oklch(from var(--color-neutral-950) l c h / 0.25); padding: 8px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 6px;">
<span style="font-size: var(--font-size-small); font-weight:700; color: var(--color-neutral-300);">Chunk Evidence (+ / −)</span>
<button id="modal-btn-copy-prompt" class="btn btn-secondary" style="font-size:9px; min-height:18px; padding:0 6px; cursor:pointer;" disabled>📋 Copy Gemini Prompt</button>
</div>
<div style="max-height: 180px; overflow-y:auto; font-size:10px; font-family:var(--font-mono); display:flex; flex-direction:column; gap: 8px;" class="custom-scrollbar">
<div>
<strong style="color:var(--color-semantic-success);">(+) Top Activators:</strong>
<ul id="modal-evidence-pos" style="padding-left:14px; margin-top:2px;">
<li style="color:var(--color-neutral-600);">Fetching chunks...</li>
</ul>
</div>
<div>
<strong style="color:var(--color-semantic-error);">(-) Bottom Activators:</strong>
<ul id="modal-evidence-neg" style="padding-left:14px; margin-top:2px;">
<li style="color:var(--color-neutral-600);">Fetching chunks...</li>
</ul>
</div>
</div>
</div>
<!-- AI Proposal / Input Section -->
<div id="modal-ai-result-section" style="border: 1px solid var(--color-brand-border); border-radius:6px; background: oklch(from var(--color-brand-primary) l c h / 0.05); padding: 8px; display:flex; flex-direction:column; gap: 6px;">
<div style="font-weight:700; font-size: var(--font-size-small); color:var(--color-brand-secondary);">Gemini AI Proposal</div>
<div id="modal-proposal-loading" style="font-style:italic; color:var(--color-neutral-400);">⏳ Querying remote model...</div>
<div id="modal-proposal-display" style="display:none; flex-direction:column; gap:6px;">
<div><strong>Proposed name:</strong> <span id="modal-proposal-name" style="color: var(--color-neutral-50); font-weight:bold; font-size:14px; text-transform:uppercase;"></span></div>
<div><strong>Rationale:</strong> <span id="modal-proposal-rationale" style="font-style:italic; font-size:11px; color:var(--color-neutral-200);"></span></div>
<div style="display:flex; align-items:center; gap:4px;">
<strong>Confidence:</strong>
<span id="modal-proposal-confidence" style="text-transform:uppercase; font-weight:bold; font-size:10px; padding:1px 6px; border-radius:4px;"></span>
</div>
<div style="display:flex; gap: 6px; margin-top:4px; border-top:1px solid oklch(from var(--color-neutral-50) l c h / 0.05); padding-top:6px;">
<button id="modal-btn-accept" class="btn btn-accent" style="font-size:11px; min-height:24px; padding: 2px 8px; cursor:pointer;">Accept Proposal</button>
<button id="modal-btn-edit-proposal" class="btn btn-secondary" style="font-size:11px; min-height:24px; padding: 2px 8px; cursor:pointer;">Edit</button>
</div>
</div>
</div>
<!-- Edit Form / Manual Entry -->
<div id="modal-edit-section" style="display:none; flex-direction:column; gap: var(--spacing-sm); border-top:1px solid oklch(from var(--color-neutral-50) l c h / 0.05); padding-top:8px;">
<div style="display:flex; flex-direction:column; gap:2px;">
<label style="font-size: var(--font-size-caption); color: var(--color-neutral-400);">Dimension Name:</label>
<input id="modal-input-name" class="input-field" placeholder="e.g. SPATIAL CONCEPT" style="text-transform: uppercase;">
</div>
<div style="display:flex; flex-direction:column; gap:2px;">
<label style="font-size: var(--font-size-caption); color: var(--color-neutral-400);">Rationale / Justification:</label>
<textarea id="modal-input-rationale" class="input-field" placeholder="Explain why this name fits the evidence..." style="min-height: 40px; resize: vertical; font-family: var(--font-sans);"></textarea>
</div>
<div style="display:flex; align-items:center; gap: 8px;">
<label style="font-size: var(--font-size-caption); color: var(--color-neutral-400);">Confidence:</label>
<select id="modal-select-confidence" class="input-field" style="max-width:120px; background: oklch(from var(--color-neutral-950) l c h / 0.5);">
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
</div>
</div>
</div>
`;
// API response storage
let apiData = null;
showCustomModal({
title: `Name Dimension #${dimIndex}`,
contentHTML: contentHTML,
confirmText: "Save",
cancelText: "Close",
onConfirm: (overlay) => {
const inputName = overlay.querySelector('#modal-input-name');
const inputRationale = overlay.querySelector('#modal-input-rationale');
const selectConfidence = overlay.querySelector('#modal-select-confidence');
const finalName = inputName ? inputName.value.trim() : "";
const finalRationale = inputRationale ? inputRationale.value.trim() : "";
const finalConfidence = selectConfidence ? selectConfidence.value : "high";
if (finalName) {
saveLabel(
dimIndex,
finalName,
finalRationale,
finalConfidence,
apiData ? apiData.evidence : {},
'human'
);
updateHUD();
if (STATE.comparisonMode === 'C') {
renderDimensionInspector();
}
}
}
});
const confirmBtn = document.getElementById('modal-confirm-btn');
if (confirmBtn) confirmBtn.style.display = 'none';
(async () => {
const overlay = document.getElementById('app-modal-overlay');
if (!overlay) return;
const posList = overlay.querySelector('#modal-evidence-pos');
const negList = overlay.querySelector('#modal-evidence-neg');
const copyBtn = overlay.querySelector('#modal-btn-copy-prompt');
const loadingEl = overlay.querySelector('#modal-proposal-loading');
const displayEl = overlay.querySelector('#modal-proposal-display');
const nameEl = overlay.querySelector('#modal-proposal-name');
const rationaleEl = overlay.querySelector('#modal-proposal-rationale');
const confidenceEl = overlay.querySelector('#modal-proposal-confidence');
const editSection = overlay.querySelector('#modal-edit-section');
try {
if (!STATE.provider) throw new Error("No provider active");
apiData = await STATE.provider.nameDimension(dimIndex, activeFilenames, STATE.featureSpace || 'RAW');
if (posList && apiData.evidence && apiData.evidence.top_activators) {
posList.innerHTML = apiData.evidence.top_activators.map(c =>
`<li style="margin-bottom:4px;"><strong>[Val: ${c.value.toFixed(3)}]</strong> ${c.text.slice(0, 160)}… <span style="color:var(--color-brand-primary); font-size:8px;">(F: ${c.filename})</span></li>`
).join('') || '<li>No evidence.</li>';
}
if (negList && apiData.evidence && apiData.evidence.bottom_activators) {
negList.innerHTML = apiData.evidence.bottom_activators.map(c =>
`<li style="margin-bottom:4px;"><strong>[Val: ${c.value.toFixed(3)}]</strong> ${c.text.slice(0, 160)}… <span style="color:var(--color-brand-primary); font-size:8px;">(F: ${c.filename})</span></li>`
).join('') || '<li>No evidence.</li>';
}
if (copyBtn && apiData.prompt) {
copyBtn.removeAttribute('disabled');
copyBtn.onclick = () => {
navigator.clipboard.writeText(apiData.prompt)
.then(() => {
copyBtn.textContent = '✅ Copied!';
setTimeout(() => { copyBtn.textContent = '📋 Copy Gemini Prompt'; }, 2000);
})
.catch(err => console.error("Clipboard copy failed:", err));
};
}
if (loadingEl) loadingEl.style.display = 'none';
if (apiData.has_api && apiData.success && apiData.proposal) {
if (displayEl) displayEl.style.display = 'flex';
if (nameEl) nameEl.textContent = apiData.proposal.name;
if (rationaleEl) rationaleEl.textContent = apiData.proposal.rationale;
if (confidenceEl) {
confidenceEl.textContent = apiData.proposal.confidence;
const conf = apiData.proposal.confidence.toLowerCase();
if (conf === 'high') {
confidenceEl.style.color = 'var(--color-neutral-50)';
confidenceEl.style.backgroundColor = 'oklch(from var(--color-semantic-success) l c h / 0.3)';
confidenceEl.style.border = '1px solid var(--color-semantic-success)';
} else if (conf === 'medium') {
confidenceEl.style.color = 'var(--color-neutral-50)';
confidenceEl.style.backgroundColor = 'oklch(from var(--color-semantic-warning) l c h / 0.3)';
confidenceEl.style.border = '1px solid var(--color-semantic-warning)';
} else {
confidenceEl.style.color = 'var(--color-neutral-50)';
confidenceEl.style.backgroundColor = 'oklch(from var(--color-semantic-error) l c h / 0.3)';
confidenceEl.style.border = '1px solid var(--color-semantic-error)';
}
}
const acceptBtn = overlay.querySelector('#modal-btn-accept');
if (acceptBtn) {
acceptBtn.onclick = () => {
saveLabel(
dimIndex,
apiData.proposal.name,
apiData.proposal.rationale,
apiData.proposal.confidence,
apiData.evidence,
'ai'
);
updateHUD();
if (STATE.comparisonMode === 'C') {
renderDimensionInspector();
}
overlay.classList.remove('active');
setTimeout(() => overlay.remove(), 300);
};
}
const editProposalBtn = overlay.querySelector('#modal-btn-edit-proposal');
if (editProposalBtn) {
editProposalBtn.onclick = () => {
if (displayEl) displayEl.style.display = 'none';
if (editSection) editSection.style.display = 'flex';
if (confirmBtn) confirmBtn.style.display = 'block';
const inputName = overlay.querySelector('#modal-input-name');
const inputRationale = overlay.querySelector('#modal-input-rationale');
const selectConfidence = overlay.querySelector('#modal-select-confidence');
if (inputName) inputName.value = apiData.proposal.name;
if (inputRationale) inputRationale.value = apiData.proposal.rationale;
if (selectConfidence) selectConfidence.value = apiData.proposal.confidence;
setTimeout(() => {
if (inputName) { inputName.focus(); inputName.select(); }
}, 50);
};
}
} else {
if (loadingEl) {
if (apiData.error) {
loadingEl.textContent = `❌ API failed: ${apiData.error}. Switching to MANUAL mode.`;
loadingEl.style.color = 'var(--color-semantic-error)';
loadingEl.style.display = 'block';
} else {
loadingEl.textContent = "ℹ️ MANUAL mode active (copy the prompt above and fill in the fields below).";
loadingEl.style.color = 'var(--color-neutral-400)';
loadingEl.style.display = 'block';
}
}
if (editSection) editSection.style.display = 'flex';
if (confirmBtn) confirmBtn.style.display = 'block';
const prefix = (STATE.featureSpace || 'RAW').toLowerCase();
const key = `${prefix}_${dimIndex}`;
const currentLabel = STATE.dimensionLabels[key] || "";
const dictEntry = STATE.dimensionDictionary ? STATE.dimensionDictionary[key] : null;
const inputName = overlay.querySelector('#modal-input-name');
const inputRationale = overlay.querySelector('#modal-input-rationale');
const selectConfidence = overlay.querySelector('#modal-select-confidence');
if (inputName) inputName.value = currentLabel;
if (inputRationale && dictEntry) inputRationale.value = dictEntry.rationale || "";
if (selectConfidence && dictEntry) selectConfidence.value = dictEntry.confidence || "high";
setTimeout(() => {
if (inputName) { inputName.focus(); inputName.select(); }
}, 50);
}
} catch (err) {
console.error("Error loading naming evidence:", err);
if (loadingEl) {
loadingEl.textContent = `❌ Error: ${err.message}`;
loadingEl.style.color = 'var(--color-semantic-error)';
}
}
})();
}