llm-semantic-visualizer / src /ui /modals /CustomModal.js
hbauzan's picture
Release v4.17.7 — sync from GitHub main
819a47f
Raw
History Blame Contribute Delete
5.99 kB
// Shared modal overlay utility (extracted from Sidebar.js in H4 4.0).
// Builds a single `#app-modal-overlay` over the app, stopping propagation so
// inputs/clicks inside the modal never reach the 3D controls.
/** @typedef {import('../types.js').ModalOptions} ModalOptions */
/**
* Show a custom modal overlay. Replaces any existing overlay.
* @param {ModalOptions} options
* @returns {void}
*/
export function showCustomModal({ title, contentHTML, onConfirm, onCancel, confirmText = "Confirm", cancelText = "Cancel", isAlert = false }) {
// Clean old modal overlays
const oldOverlay = document.getElementById('app-modal-overlay');
if (oldOverlay) oldOverlay.remove();
const overlay = document.createElement('div');
overlay.id = 'app-modal-overlay';
overlay.className = 'modal-overlay';
document.body.appendChild(overlay);
overlay.innerHTML = `
<div class="modal-content app-panel">
<div class="modal-header">
<span class="modal-title">${title}</span>
${isAlert ? '<button id="modal-close-x" class="btn btn-secondary btn-square" style="width:24px; height:24px; min-height:24px; padding:0; font-size:10px; border-radius:50%;">✕</button>' : ''}
</div>
<div class="modal-body">
${contentHTML}
</div>
<div class="modal-actions">
${!isAlert ? `<button id="modal-cancel-btn" class="btn btn-secondary">${cancelText}</button>` : ''}
<button id="modal-confirm-btn" class="btn btn-primary">${confirmText}</button>
</div>
</div>
`;
// Prevent propagation so inputs/clicks inside modal don't affect 3D controls
['mousedown', 'click', 'keydown', 'keyup'].forEach(evt => {
overlay.addEventListener(evt, (e) => e.stopPropagation());
});
const closeOverlay = () => {
overlay.classList.remove('active');
setTimeout(() => overlay.remove(), 300);
};
const confirmBtn = overlay.querySelector('#modal-confirm-btn');
if (confirmBtn) {
confirmBtn.onclick = () => {
if (onConfirm) onConfirm(overlay);
closeOverlay();
};
}
const cancelBtn = overlay.querySelector('#modal-cancel-btn');
if (cancelBtn) {
cancelBtn.onclick = () => {
if (onCancel) onCancel();
closeOverlay();
};
}
const closeX = overlay.querySelector('#modal-close-x');
if (closeX) {
closeX.onclick = () => {
if (onCancel) onCancel();
closeOverlay();
};
}
// Force reflow and activate transition
overlay.offsetHeight;
overlay.classList.add('active');
}
/**
* Open the bookmark naming modal and lock/unlock PointerLock camera flight controls.
* @param {{ docId: string, chunkIndex: number, dimensionIndex: number, value: number }} coordinates
* @param {function(string): void} callback
* @param {object} [controls]
* @returns {void}
*/
export function showBookmarkNamingModal(coordinates, callback, controls = null) {
if (controls && controls.isLocked) {
controls.unlock();
}
const valStr = typeof coordinates.value === 'number' ? coordinates.value.toFixed(4) : coordinates.value;
const contentHTML = `
<div style="display:flex; flex-direction:column; gap: var(--spacing-sm); width: 100%; max-width: 420px; font-family: var(--font-sans);">
<p style="font-size: var(--font-size-small); color: var(--color-neutral-300); margin: 0; line-height: 1.5;">
Bookmark: Document <strong>${coordinates.docId}</strong> · Chunk <strong>${coordinates.chunkIndex}</strong> · Dimension <strong>${coordinates.dimensionIndex}</strong> (Value: <strong>${valStr}</strong>)
</p>
<div style="display:flex; flex-direction:column; gap: 4px; margin-top: var(--spacing-xs);">
<label for="bookmark-name-input" style="font-size: var(--font-size-caption); color: var(--color-neutral-400);">Bookmark name:</label>
<input id="bookmark-name-input" class="input-field" type="text" placeholder="e.g. Attention peak" style="width: 100%; border: 1px solid oklch(from var(--color-neutral-50) l c h / 0.1); background: oklch(from var(--color-neutral-950) l c h / 0.5); color: var(--color-neutral-50); padding: var(--spacing-xs); border-radius: 4px;">
</div>
</div>
`;
showCustomModal({
title: "New Bookmark",
contentHTML: contentHTML,
confirmText: "Save",
cancelText: "Cancel",
onConfirm: () => {
// Handled by custom validation below
},
onCancel: () => {
if (controls) {
controls.lock();
}
}
});
// Custom validation: override confirmBtn.onclick to prevent closing if input is empty
const overlay = document.getElementById('app-modal-overlay');
if (overlay) {
const confirmBtn = overlay.querySelector('#modal-confirm-btn');
const input = overlay.querySelector('#bookmark-name-input');
if (confirmBtn && input) {
confirmBtn.onclick = () => {
const name = input.value.trim();
if (!name) {
input.style.borderColor = 'var(--color-semantic-error)';
input.focus();
return; // Prevent closing
}
callback(name);
// Destroy the modal
overlay.classList.remove('active');
setTimeout(() => overlay.remove(), 300);
// Restore flight controls
if (controls) {
controls.lock();
}
};
}
// Focus the input field automatically
setTimeout(() => {
if (input) {
input.focus();
input.select();
}
}, 50);
}
}