Files
kandev/apps/backend/internal/agentctl/server/api/scripts/inspector.js
T

312 lines
11 KiB
JavaScript

(function () {
'use strict';
var SOURCE = 'kandev-inspector';
var DRAG_THRESHOLD = 5;
var Z = 2147483640;
var active = false;
var mouseDownAt = null;
var dragRect = null;
var hoverEl = null;
var counter = 0;
var hoverOverlay = null;
var dragOverlay = null;
var popup = null;
var pendingAnnotation = null;
function send(type, payload) {
try {
window.parent.postMessage({ source: SOURCE, type: type, payload: payload }, '*');
} catch (e) {}
}
function getSelector(el) {
var parts = [];
var cur = el;
while (cur && cur.nodeType === 1 && parts.length < 5) {
var tag = cur.tagName.toLowerCase();
var id = cur.getAttribute && cur.getAttribute('id');
if (id) { parts.unshift(tag + '#' + CSS.escape(id)); break; }
var cls = '';
if (cur.classList && cur.classList.length) {
cls = Array.from(cur.classList).slice(0, 2)
.map(function (c) { return '.' + CSS.escape(c); }).join('');
}
var part = tag + cls;
var parent = cur.parentElement;
if (parent) {
var sibs = Array.from(parent.children).filter(function (c) { return c.tagName === cur.tagName; });
if (sibs.length > 1) part += ':nth-of-type(' + (sibs.indexOf(cur) + 1) + ')';
}
parts.unshift(part);
cur = parent;
}
return parts.join(' > ');
}
function captureElement(el) {
if (!el || el.nodeType !== 1) return null;
var cls = '';
if (el.className && typeof el.className === 'string') {
cls = el.className.trim().split(/\s+/).slice(0, 3).join(' ') || '';
}
return {
tag: el.tagName.toLowerCase(),
id: el.id || undefined,
classes: cls || undefined,
role: el.getAttribute('role') || undefined,
ariaLabel: el.getAttribute('aria-label') || undefined,
text: ((el.textContent || '').replace(/\s+/g, ' ').trim()).slice(0, 120) || undefined,
selector: getSelector(el),
};
}
function elementsInRect(rect) {
var out = [];
var all = document.querySelectorAll('body *');
for (var i = 0; i < all.length && out.length < 5; i++) {
var el = all[i];
if (el === hoverOverlay || el === dragOverlay || el === popup) continue;
if (popup && popup.contains(el)) continue;
var r = el.getBoundingClientRect();
if (r.width < 4 || r.height < 4) continue;
if (r.left >= rect.x && r.top >= rect.y
&& r.right <= rect.x + rect.w && r.bottom <= rect.y + rect.h) {
out.push(captureElement(el));
}
}
return out;
}
function makeOverlay(borderColor, bg) {
var d = document.createElement('div');
d.style.cssText = 'position:fixed;pointer-events:none;box-sizing:border-box;display:none;z-index:' + Z + ';'
+ 'border:2px solid ' + borderColor + ';background:' + bg + ';';
document.body.appendChild(d);
return d;
}
function ensureOverlays() {
if (!hoverOverlay) hoverOverlay = makeOverlay('#6366f1', 'rgba(99,102,241,0.10)');
if (!dragOverlay) dragOverlay = makeOverlay('#f59e0b', 'rgba(245,158,11,0.15)');
}
function positionOverlay(o, x, y, w, h) {
o.style.display = 'block';
o.style.left = x + 'px';
o.style.top = y + 'px';
o.style.width = w + 'px';
o.style.height = h + 'px';
}
function hideOverlay(o) { if (o) o.style.display = 'none'; }
function placeMarker(num, x, y) {
var m = document.createElement('div');
m.setAttribute('data-kandev-marker', String(num));
m.style.cssText = 'position:fixed;left:' + (x - 12) + 'px;top:' + (y - 12) + 'px;width:24px;height:24px;'
+ 'border-radius:50%;background:#6366f1;color:#fff;font:600 12px/24px system-ui,sans-serif;'
+ 'text-align:center;z-index:' + Z + ';pointer-events:none;box-shadow:0 1px 4px rgba(0,0,0,0.3);';
m.textContent = String(num);
document.body.appendChild(m);
}
function clearMarkers() {
var nodes = document.querySelectorAll('[data-kandev-marker]');
for (var i = 0; i < nodes.length; i++) nodes[i].parentNode.removeChild(nodes[i]);
counter = 0;
}
function closePopup() {
if (popup && popup.parentNode) popup.parentNode.removeChild(popup);
popup = null;
pendingAnnotation = null;
}
function currentPagePath() {
var path = location.pathname;
var prefix = window.__kandevProxyPrefix;
if (typeof prefix === 'string' && prefix) {
if (path === prefix || path.indexOf(prefix + '/') === 0) {
path = path.slice(prefix.length) || '/';
}
}
return path + location.search;
}
function openCommentPopup(anchorX, anchorY, onSubmit) {
var keep = pendingAnnotation;
closePopup();
pendingAnnotation = keep;
popup = document.createElement('div');
popup.style.cssText = 'position:fixed;left:' + anchorX + 'px;top:' + anchorY + 'px;z-index:' + (Z + 1) + ';'
+ 'background:#fff;border:1px solid #e5e7eb;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.15);'
+ 'padding:8px;width:280px;font-family:system-ui,sans-serif;';
var ta = document.createElement('textarea');
ta.placeholder = 'Describe the change (Enter to save, Esc to cancel)';
ta.style.cssText = 'width:100%;min-height:60px;border:1px solid #e5e7eb;border-radius:4px;padding:6px;'
+ 'font:13px system-ui,sans-serif;resize:vertical;outline:none;box-sizing:border-box;';
var row = document.createElement('div');
row.style.cssText = 'display:flex;gap:6px;justify-content:flex-end;margin-top:6px;';
var cancel = document.createElement('button');
cancel.textContent = 'Cancel';
cancel.style.cssText = 'padding:4px 10px;border:1px solid #e5e7eb;background:#fff;border-radius:4px;cursor:pointer;font:12px system-ui,sans-serif;';
var save = document.createElement('button');
save.textContent = 'Save';
save.style.cssText = 'padding:4px 10px;border:0;background:#6366f1;color:#fff;border-radius:4px;cursor:pointer;font:12px system-ui,sans-serif;';
cancel.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); closePopup(); });
save.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); onSubmit(ta.value.trim()); });
ta.addEventListener('keydown', function (e) {
e.stopPropagation();
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onSubmit(ta.value.trim()); }
if (e.key === 'Escape') { e.preventDefault(); closePopup(); }
});
popup.appendChild(ta);
row.appendChild(cancel); row.appendChild(save);
popup.appendChild(row);
document.body.appendChild(popup);
ta.focus();
}
function commitAnnotation(comment) {
if (!pendingAnnotation) return;
counter += 1;
var a = pendingAnnotation;
a.id = 'a-' + Date.now() + '-' + counter;
a.comment = comment || '';
placeMarker(counter, a.markerX, a.markerY);
// Note: we do NOT set `a.number` here. The React parent assigns numbers
// so they remain monotonic across iframe refreshes.
send('annotation-added', a);
pendingAnnotation = null;
closePopup();
}
function startPending(kind, rect, element, elements, markerX, markerY) {
pendingAnnotation = {
kind: kind,
rect: rect,
element: element || null,
elements: elements || null,
markerX: markerX,
markerY: markerY,
pagePath: currentPagePath(),
};
var popupX = Math.min(markerX + 14, window.innerWidth - 296);
var popupY = Math.min(markerY + 14, window.innerHeight - 140);
openCommentPopup(popupX, popupY, commitAnnotation);
}
function onMouseDown(e) {
if (popup && popup.contains(e.target)) return;
e.preventDefault();
e.stopPropagation();
mouseDownAt = { x: e.clientX, y: e.clientY };
dragRect = null;
}
function onMouseMove(e) {
if (popup && popup.contains(e.target)) return;
if (mouseDownAt) {
var dx = e.clientX - mouseDownAt.x;
var dy = e.clientY - mouseDownAt.y;
if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) {
dragRect = {
x: Math.min(mouseDownAt.x, e.clientX),
y: Math.min(mouseDownAt.y, e.clientY),
w: Math.abs(dx),
h: Math.abs(dy),
};
hideOverlay(hoverOverlay);
positionOverlay(dragOverlay, dragRect.x, dragRect.y, dragRect.w, dragRect.h);
}
return;
}
var el = e.target;
if (el === hoverOverlay || el === dragOverlay || (popup && popup.contains(el))) return;
hoverEl = el;
var r = el.getBoundingClientRect();
positionOverlay(hoverOverlay, r.left, r.top, r.width, r.height);
}
function onMouseUp(e) {
if (popup && popup.contains(e.target)) return;
var wasDragging = !!dragRect;
var rect = dragRect;
var down = mouseDownAt;
mouseDownAt = null;
dragRect = null;
hideOverlay(dragOverlay);
if (wasDragging && rect && rect.w > DRAG_THRESHOLD && rect.h > DRAG_THRESHOLD) {
var inside = elementsInRect(rect);
startPending('area', rect, null, inside, rect.x + rect.w, rect.y);
return;
}
if (down) {
var clickEl = document.elementFromPoint(down.x, down.y);
if (!clickEl || clickEl === hoverOverlay || clickEl === dragOverlay) return;
if (popup && popup.contains(clickEl)) return;
var el = captureElement(clickEl);
startPending('pin', null, el, null, down.x, down.y);
}
}
function onClickCapture(e) {
if (popup && popup.contains(e.target)) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
function onKeyDown(e) {
if (popup) return;
if (e.key === 'Escape') { e.preventDefault(); setActive(false); send('inspect-exited', {}); }
}
function setActive(isOn) {
if (isOn === active) return;
active = isOn;
if (isOn) {
ensureOverlays();
document.body.style.cursor = 'crosshair';
document.addEventListener('mousedown', onMouseDown, true);
document.addEventListener('mousemove', onMouseMove, true);
document.addEventListener('mouseup', onMouseUp, true);
document.addEventListener('click', onClickCapture, true);
document.addEventListener('keydown', onKeyDown, true);
} else {
document.removeEventListener('mousedown', onMouseDown, true);
document.removeEventListener('mousemove', onMouseMove, true);
document.removeEventListener('mouseup', onMouseUp, true);
document.removeEventListener('click', onClickCapture, true);
document.removeEventListener('keydown', onKeyDown, true);
document.body.style.cursor = '';
hideOverlay(hoverOverlay);
hideOverlay(dragOverlay);
closePopup();
}
}
function removeMarker(num) {
if (num == null) return;
var nodes = document.querySelectorAll('[data-kandev-marker="' + String(num) + '"]');
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].parentNode) nodes[i].parentNode.removeChild(nodes[i]);
}
}
window.addEventListener('message', function (e) {
if (!e.data || e.data.source !== SOURCE) return;
if (e.data.type === 'toggle-inspect') setActive(!!(e.data.payload && e.data.payload.active));
if (e.data.type === 'clear-annotations') clearMarkers();
if (e.data.type === 'remove-marker') removeMarker(e.data.payload && e.data.payload.number);
});
send('inspector-ready', {});
})();