Spaces:
Sleeping
Sleeping
| // 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)'; | |
| } | |
| } | |
| })(); | |
| } | |