mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-23 21:06:50 +00:00
refector ui ux
This commit is contained in:
parent
3ef1cee4e8
commit
121fbf4092
11 changed files with 1730 additions and 26 deletions
|
|
@ -237,13 +237,37 @@ async function handleServerMessage(message) {
|
|||
});
|
||||
break;
|
||||
|
||||
case 'ACTION':
|
||||
case 'ACTION': {
|
||||
broadcastToPopup({
|
||||
type: 'ACTION',
|
||||
action: message.action,
|
||||
detail: message.detail,
|
||||
});
|
||||
const actionTabId = message.tabId || getDefaultTabId();
|
||||
const actionDetail = message.detail || message.action || '';
|
||||
// Forward summary text to overlay
|
||||
sendOverlayEvent(actionTabId, {
|
||||
type: 'OVERLAY_SUMMARY',
|
||||
text:
|
||||
actionDetail.length > 60
|
||||
? message.action || actionDetail.slice(0, 60)
|
||||
: actionDetail,
|
||||
});
|
||||
// Extract element ref from detail (e.g. "ref=e46, text=...") and resolve via CDP
|
||||
const refMatch = actionDetail.match(/ref=(e\d+)/);
|
||||
if (refMatch && actionTabId) {
|
||||
resolveElementRect(refMatch[1], actionTabId).then((rect) => {
|
||||
if (rect) {
|
||||
sendOverlayEvent(actionTabId, {
|
||||
type: 'OVERLAY_CURSOR_MOVE',
|
||||
x: rect.cx,
|
||||
y: rect.cy,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ACTION_COMPLETE':
|
||||
broadcastToPopup({
|
||||
|
|
@ -251,28 +275,63 @@ async function handleServerMessage(message) {
|
|||
success: message.success,
|
||||
result: message.result,
|
||||
});
|
||||
// Notify overlay that action is done
|
||||
sendOverlayEvent(message.tabId || getDefaultTabId(), {
|
||||
type: 'OVERLAY_AGENT_STEP',
|
||||
stepId: message.id || '',
|
||||
state: message.success ? 'done' : 'error',
|
||||
summary: message.success ? 'Done' : 'Action failed',
|
||||
});
|
||||
break;
|
||||
|
||||
case 'CDP_COMMAND': {
|
||||
// Execute CDP command via chrome.debugger, routed by tabId
|
||||
const targetTabId = message.tabId || getDefaultTabId();
|
||||
try {
|
||||
// Check if we should highlight before this action
|
||||
// Highlight before this action — prefer overlay content script, fallback to CDP
|
||||
if (message.highlight && message.highlight.selector) {
|
||||
await highlightElement(
|
||||
message.highlight.selector,
|
||||
message.highlight.duration || 1500,
|
||||
targetTabId
|
||||
);
|
||||
const overlayHighlighted = await sendOverlayEvent(targetTabId, {
|
||||
type: 'OVERLAY_HIGHLIGHT',
|
||||
selector: message.highlight.selector,
|
||||
duration: message.highlight.duration || 1500,
|
||||
});
|
||||
if (!overlayHighlighted) {
|
||||
// Fallback to CDP-injected highlight
|
||||
await highlightElement(
|
||||
message.highlight.selector,
|
||||
message.highlight.duration || 1500,
|
||||
targetTabId
|
||||
);
|
||||
}
|
||||
if (message.highlight.summary) {
|
||||
sendOverlayEvent(targetTabId, {
|
||||
type: 'OVERLAY_SUMMARY',
|
||||
text: message.highlight.summary,
|
||||
});
|
||||
}
|
||||
// Small delay to let user see the highlight
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const result = await executeCdpCommand(
|
||||
message.method,
|
||||
message.params || {},
|
||||
targetTabId
|
||||
);
|
||||
// Extract cursor position from CDP commands that have coordinates
|
||||
const params = message.params || {};
|
||||
const method = message.method || '';
|
||||
if (
|
||||
method === 'Input.dispatchMouseEvent' &&
|
||||
params.x != null &&
|
||||
params.y != null
|
||||
) {
|
||||
if (params.type === 'mousePressed') {
|
||||
sendOverlayEvent(targetTabId, {
|
||||
type: 'OVERLAY_CURSOR_MOVE',
|
||||
x: params.x,
|
||||
y: params.y,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 80));
|
||||
}
|
||||
}
|
||||
|
||||
const result = await executeCdpCommand(method, params, targetTabId);
|
||||
|
||||
// Send result back to server with tabId
|
||||
sendToServer({
|
||||
|
|
@ -357,20 +416,38 @@ async function handleServerMessage(message) {
|
|||
break;
|
||||
}
|
||||
|
||||
case 'TASK_COMPLETE':
|
||||
case 'TASK_COMPLETE': {
|
||||
broadcastToPopup({
|
||||
type: 'TASK_COMPLETE',
|
||||
result: message.result,
|
||||
});
|
||||
// Hide all overlay — agent session ended
|
||||
const completeTabId = message.tabId || getDefaultTabId();
|
||||
sendOverlayEvent(completeTabId, {
|
||||
type: 'OVERLAY_STATE',
|
||||
auroraVisible: false,
|
||||
cursorVisible: false,
|
||||
summaryText: '',
|
||||
});
|
||||
// Don't detach all tabs on task complete - let server manage tab lifecycle
|
||||
break;
|
||||
}
|
||||
|
||||
case 'TASK_ERROR':
|
||||
case 'TASK_ERROR': {
|
||||
broadcastToPopup({
|
||||
type: 'TASK_ERROR',
|
||||
error: message.error,
|
||||
});
|
||||
// Hide all overlay — agent session ended with error
|
||||
const errorTabId = message.tabId || getDefaultTabId();
|
||||
sendOverlayEvent(errorTabId, {
|
||||
type: 'OVERLAY_STATE',
|
||||
auroraVisible: false,
|
||||
cursorVisible: false,
|
||||
summaryText: '',
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'STREAM_TEXT':
|
||||
{
|
||||
|
|
@ -422,21 +499,35 @@ async function handleServerMessage(message) {
|
|||
break;
|
||||
|
||||
case 'HIGHLIGHT': {
|
||||
// Highlight an element on the page
|
||||
console.log('Received HIGHLIGHT message:', message);
|
||||
// Resolve element position via CDP, move cursor there, then highlight
|
||||
const hlTabId = message.tabId || getDefaultTabId();
|
||||
try {
|
||||
const highlightResult = await highlightElement(
|
||||
// Resolve element rect in page main world
|
||||
const rect = await resolveElementRect(message.selector, hlTabId);
|
||||
if (rect) {
|
||||
// Move overlay cursor to element center
|
||||
sendOverlayEvent(hlTabId, {
|
||||
type: 'OVERLAY_CURSOR_MOVE',
|
||||
x: rect.cx,
|
||||
y: rect.cy,
|
||||
});
|
||||
// Show highlight rect via overlay
|
||||
sendOverlayEvent(hlTabId, {
|
||||
type: 'OVERLAY_HIGHLIGHT_RECT',
|
||||
rect: rect,
|
||||
duration: message.duration || 2000,
|
||||
});
|
||||
}
|
||||
// Also run the existing CDP highlight (red ring)
|
||||
await highlightElement(
|
||||
message.selector,
|
||||
message.duration || 2000,
|
||||
hlTabId
|
||||
);
|
||||
console.log('Highlight completed:', highlightResult);
|
||||
sendToServer({
|
||||
type: 'HIGHLIGHT_RESULT',
|
||||
id: message.id,
|
||||
success: true,
|
||||
result: highlightResult,
|
||||
tabId: hlTabId,
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
@ -476,6 +567,78 @@ async function enableCdpDomains(tabId) {
|
|||
}
|
||||
}
|
||||
|
||||
// Resolve element ref to bounding rect via CDP (runs in page main world)
|
||||
// Returns { x, y, width, height, cx, cy } or null
|
||||
async function resolveElementRect(selector, tabId) {
|
||||
const targetTabId = tabId || getDefaultTabId();
|
||||
if (!targetTabId) return null;
|
||||
|
||||
const script = `
|
||||
(function() {
|
||||
const sel = ${JSON.stringify(selector)};
|
||||
let element = null;
|
||||
|
||||
// Method 1: ariaSnapshot
|
||||
if (typeof __ariaSnapshot !== 'undefined' && __ariaSnapshot.getElementByRef) {
|
||||
try { element = __ariaSnapshot.getElementByRef(sel, document.body); } catch(e) {}
|
||||
}
|
||||
|
||||
// Method 2: _ariaRef DOM walk
|
||||
if (!element) {
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false);
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
if (node._ariaRef && node._ariaRef.ref === sel) { element = node; break; }
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: data attributes
|
||||
if (!element) {
|
||||
const refNum = sel.replace(/^e/, '');
|
||||
const selectors = [
|
||||
'[data-ref="' + sel + '"]', '[data-ref="' + refNum + '"]',
|
||||
'[ref="' + sel + '"]', '[aria-ref="' + sel + '"]',
|
||||
'[data-camel-ref="' + sel + '"]', '[data-camel-ref="' + refNum + '"]'
|
||||
];
|
||||
for (const s of selectors) {
|
||||
try { element = document.querySelector(s); if (element) break; } catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 4: CSS selector
|
||||
if (!element && (sel.includes('[') || sel.includes('.') || sel.includes('#'))) {
|
||||
try { element = document.querySelector(sel); } catch(e) {}
|
||||
}
|
||||
|
||||
if (!element) return null;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
x: rect.left, y: rect.top, width: rect.width, height: rect.height,
|
||||
cx: rect.left + rect.width / 2, cy: rect.top + rect.height / 2
|
||||
};
|
||||
})();
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await executeCdpCommand(
|
||||
'Runtime.evaluate',
|
||||
{
|
||||
expression: script,
|
||||
returnByValue: true,
|
||||
},
|
||||
targetTabId
|
||||
);
|
||||
|
||||
if (result && result.result && result.result.value) {
|
||||
return result.result.value;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('resolveElementRect failed:', e.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Highlight element on page with animation
|
||||
async function highlightElement(selector, duration = 600, tabId = null) {
|
||||
const targetTabId = tabId || getDefaultTabId();
|
||||
|
|
@ -573,9 +736,9 @@ async function highlightElement(selector, duration = 600, tabId = null) {
|
|||
style.id = '__agent_highlight_styles__';
|
||||
style.textContent = \`
|
||||
@keyframes __agent_pulse__ {
|
||||
0% { box-shadow: 0 0 0 4px rgba(255, 68, 68, 1), 0 0 15px rgba(255, 68, 68, 0.7); }
|
||||
50% { box-shadow: 0 0 0 6px rgba(255, 68, 68, 0.7), 0 0 25px rgba(255, 68, 68, 0.5); }
|
||||
100% { box-shadow: 0 0 0 4px rgba(255, 68, 68, 1), 0 0 15px rgba(255, 68, 68, 0.7); }
|
||||
0% { box-shadow: 0 0 0 4px rgba(21, 93, 252, 1), 0 0 15px rgba(21, 93, 252, 0.7); }
|
||||
50% { box-shadow: 0 0 0 6px rgba(21, 93, 252, 0.7), 0 0 25px rgba(21, 93, 252, 0.5); }
|
||||
100% { box-shadow: 0 0 0 4px rgba(21, 93, 252, 1), 0 0 15px rgba(21, 93, 252, 0.7); }
|
||||
}
|
||||
@keyframes __agent_ripple__ {
|
||||
0% { transform: scale(0.8); opacity: 1; }
|
||||
|
|
@ -601,9 +764,9 @@ async function highlightElement(selector, duration = 600, tabId = null) {
|
|||
left: \${rect.left - 8}px;
|
||||
width: \${rect.width + 16}px;
|
||||
height: \${rect.height + 16}px;
|
||||
border: 4px solid #ff4444;
|
||||
border: 4px solid #155DFC;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 68, 68, 0.15);
|
||||
background: rgba(21, 93, 252, 0.15);
|
||||
pointer-events: none;
|
||||
z-index: 2147483647;
|
||||
animation: __agent_pulse__ 0.2s ease-in-out infinite;
|
||||
|
|
@ -620,7 +783,7 @@ async function highlightElement(selector, duration = 600, tabId = null) {
|
|||
left: \${rect.left + rect.width/2 - 25}px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 3px solid #ff4444;
|
||||
border: 3px solid #155DFC;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 2147483646;
|
||||
|
|
@ -841,6 +1004,19 @@ function broadcastToPopup(message) {
|
|||
});
|
||||
}
|
||||
|
||||
// Send overlay event to content script on a specific tab
|
||||
// Returns true if message was delivered, false if no content script listening
|
||||
async function sendOverlayEvent(tabId, event) {
|
||||
if (!tabId) return false;
|
||||
try {
|
||||
await chrome.tabs.sendMessage(tabId, event);
|
||||
return true;
|
||||
} catch (e) {
|
||||
// Content script not injected on this tab (restricted page, etc.)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for debugger events - forward from ALL attached tabs
|
||||
chrome.debugger.onEvent.addListener((source, method, params) => {
|
||||
if (attachedTabs.has(source.tabId)) {
|
||||
|
|
@ -1044,6 +1220,13 @@ async function executeTask(task, tabId, url) {
|
|||
message: 'Sending task to AI...',
|
||||
});
|
||||
|
||||
// Show aurora overlay — agent session started
|
||||
sendOverlayEvent(tabId, {
|
||||
type: 'OVERLAY_STATE',
|
||||
enabled: true,
|
||||
auroraVisible: true,
|
||||
});
|
||||
|
||||
sendToServer({
|
||||
type: 'START_TASK',
|
||||
task: task,
|
||||
|
|
|
|||
295
extensions/chrome_extension/content.js
Normal file
295
extensions/chrome_extension/content.js
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
// Content script — Shadow DOM overlay mount + orchestrator
|
||||
// Runs after all overlay/*.js modules are loaded by manifest
|
||||
|
||||
// Prevent double injection
|
||||
if (document.getElementById('eigent-agent-overlay')) {
|
||||
// already injected
|
||||
} else if (
|
||||
window.location.href.startsWith('chrome://') ||
|
||||
window.location.href.startsWith('chrome-extension://') ||
|
||||
window.location.href.startsWith('edge://') ||
|
||||
window.location.href.startsWith('about:')
|
||||
) {
|
||||
// skip restricted pages
|
||||
} else {
|
||||
console.log('[Eigent Cursor] Initializing content script...');
|
||||
|
||||
// Verify all modules loaded
|
||||
const modules = {
|
||||
OverlayStore,
|
||||
OverlayEvents,
|
||||
OverlayMotion,
|
||||
OverlayAurora,
|
||||
OverlayCursor,
|
||||
OverlaySummary,
|
||||
OverlayHighlight,
|
||||
};
|
||||
for (const [name, mod] of Object.entries(modules)) {
|
||||
if (!mod) {
|
||||
console.error(`[Eigent Cursor] Module ${name} not loaded!`);
|
||||
} else {
|
||||
console.log(`[Eigent Cursor] Module ${name} OK`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create overlay host — full viewport, no interaction blocking
|
||||
const host = document.createElement('div');
|
||||
host.id = 'eigent-agent-overlay';
|
||||
host.style.cssText =
|
||||
'position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 2147483646; pointer-events: none; overflow: visible;';
|
||||
document.documentElement.appendChild(host);
|
||||
|
||||
// Attach Shadow DOM for style isolation
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
|
||||
// Inject all overlay styles into shadow root
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.textContent = [
|
||||
OverlayAurora.getStyles(),
|
||||
OverlayCursor.getStyles(),
|
||||
OverlaySummary.getStyles(),
|
||||
OverlayHighlight.getStyles(),
|
||||
].join('\n');
|
||||
shadow.appendChild(styleEl);
|
||||
|
||||
console.log(
|
||||
'[Eigent Cursor] Styles injected, length:',
|
||||
styleEl.textContent.length
|
||||
);
|
||||
|
||||
// Detect reduced motion preference
|
||||
const reducedMotion = window.matchMedia(
|
||||
'(prefers-reduced-motion: reduce)'
|
||||
).matches;
|
||||
OverlayStore.update({ reducedMotion });
|
||||
window
|
||||
.matchMedia('(prefers-reduced-motion: reduce)')
|
||||
.addEventListener('change', (e) => {
|
||||
OverlayStore.update({ reducedMotion: e.matches });
|
||||
});
|
||||
|
||||
// Initialize all overlay layers (order matters for z-stacking)
|
||||
OverlayAurora.init(shadow);
|
||||
OverlayHighlight.init(shadow);
|
||||
OverlayCursor.init(shadow);
|
||||
OverlaySummary.init(shadow);
|
||||
|
||||
console.log('[Eigent Cursor] All layers initialized');
|
||||
console.log(
|
||||
'[Eigent Cursor] Shadow root children:',
|
||||
shadow.childNodes.length
|
||||
);
|
||||
console.log(
|
||||
'[Eigent Cursor] Host element in DOM:',
|
||||
!!document.getElementById('eigent-agent-overlay')
|
||||
);
|
||||
|
||||
// Start listening for messages from background
|
||||
OverlayEvents.listen();
|
||||
OverlayEvents.notifyReady();
|
||||
|
||||
// --- DOM Target Resolution (Milestone 2) ---
|
||||
const OverlayDomResolver = {
|
||||
resolve(target) {
|
||||
const element = OverlayHighlight.resolveElement(target);
|
||||
if (!element) {
|
||||
return { found: false, target };
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
// Scroll into view if off-screen
|
||||
if (
|
||||
rect.top < 0 ||
|
||||
rect.bottom > window.innerHeight ||
|
||||
rect.left < 0 ||
|
||||
rect.right > window.innerWidth
|
||||
) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center',
|
||||
});
|
||||
// Re-measure after scroll
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const newRect = element.getBoundingClientRect();
|
||||
resolve({
|
||||
found: true,
|
||||
target,
|
||||
rect: {
|
||||
x: newRect.left,
|
||||
y: newRect.top,
|
||||
width: newRect.width,
|
||||
height: newRect.height,
|
||||
},
|
||||
center: {
|
||||
x: newRect.left + newRect.width / 2,
|
||||
y: newRect.top + newRect.height / 2,
|
||||
},
|
||||
});
|
||||
}, 400);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
found: true,
|
||||
target,
|
||||
rect: {
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
},
|
||||
center: {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Expose resolver globally for events.js
|
||||
window.__eigentDomResolver = OverlayDomResolver;
|
||||
|
||||
// --- Scripted Demo Mode ---
|
||||
window.__eigentOverlayDemo = async function () {
|
||||
const store = OverlayStore;
|
||||
const motion = OverlayMotion;
|
||||
const cursor = OverlayCursor;
|
||||
const highlight = OverlayHighlight;
|
||||
|
||||
console.log('[Eigent Demo] Starting overlay demo...');
|
||||
|
||||
// Step 1: Show aurora (simulates agent session start)
|
||||
store.update({ aurora: { visible: true }, enabled: true });
|
||||
await sleep(800);
|
||||
|
||||
// Step 2: Show cursor at top-left area
|
||||
store.update({ cursor: { x: 100, y: 100, visible: true, state: 'idle' } });
|
||||
motion.setPosition(100, 100);
|
||||
await sleep(600);
|
||||
|
||||
// Step 3: Look for a search input or first input on page
|
||||
const searchInput =
|
||||
document.querySelector('input[type="search"]') ||
|
||||
document.querySelector('input[type="text"]') ||
|
||||
document.querySelector('input:not([type="hidden"])') ||
|
||||
document.querySelector('textarea');
|
||||
|
||||
if (searchInput) {
|
||||
const rect = searchInput.getBoundingClientRect();
|
||||
const targetX = rect.left + rect.width / 2;
|
||||
const targetY = rect.top + rect.height / 2;
|
||||
|
||||
// Move cursor to search input
|
||||
store.update({
|
||||
summary: { text: 'Looking for search field\u2026', visible: true },
|
||||
});
|
||||
await motion.moveCursor(targetX, targetY);
|
||||
await motion.settle(180);
|
||||
|
||||
// Highlight target
|
||||
highlight.highlightSelector(
|
||||
'input[type="search"], input[type="text"], input:not([type="hidden"]), textarea',
|
||||
2500
|
||||
);
|
||||
store.update({ cursor: { state: 'hovering' } });
|
||||
await sleep(800);
|
||||
|
||||
// Click
|
||||
store.update({
|
||||
summary: { text: 'Typing search query\u2026', visible: true },
|
||||
});
|
||||
cursor.animateClick();
|
||||
await sleep(1200);
|
||||
} else {
|
||||
// Fallback: move to center of page
|
||||
store.update({ summary: { text: 'Scanning page\u2026', visible: true } });
|
||||
await motion.moveCursor(window.innerWidth / 2, window.innerHeight / 3);
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
// Step 4: Find a button or link
|
||||
const button =
|
||||
document.querySelector('button:not([disabled])') ||
|
||||
document.querySelector('a[href]') ||
|
||||
document.querySelector('[role="button"]');
|
||||
|
||||
if (button) {
|
||||
const rect = button.getBoundingClientRect();
|
||||
const targetX = rect.left + rect.width / 2;
|
||||
const targetY = rect.top + rect.height / 2;
|
||||
|
||||
store.update({
|
||||
summary: {
|
||||
text:
|
||||
'Clicking ' + (button.textContent || 'button').trim().slice(0, 30),
|
||||
visible: true,
|
||||
},
|
||||
});
|
||||
await motion.moveCursor(targetX, targetY);
|
||||
await motion.settle(150);
|
||||
|
||||
highlight.highlightSelector(button.tagName.toLowerCase(), 2000);
|
||||
cursor.animateClick();
|
||||
await sleep(1500);
|
||||
}
|
||||
|
||||
// Step 5: Done
|
||||
store.update({
|
||||
summary: { text: 'Done', visible: true },
|
||||
cursor: { state: 'complete' },
|
||||
});
|
||||
await sleep(2000);
|
||||
|
||||
// Fade out everything (simulates agent session end)
|
||||
store.update({
|
||||
summary: { visible: false },
|
||||
cursor: { visible: false },
|
||||
highlight: null,
|
||||
aurora: { visible: false },
|
||||
});
|
||||
|
||||
console.log('[Eigent Demo] Demo complete.');
|
||||
};
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Listen for custom events from the MAIN world bridge (overlay/bridge.js)
|
||||
document.addEventListener('eigent-cursor-demo', () => {
|
||||
console.log('[Eigent Cursor] Demo event received from bridge');
|
||||
if (typeof window.__eigentOverlayDemo === 'function') {
|
||||
window.__eigentOverlayDemo();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('eigent-cursor-cmd', (e) => {
|
||||
const detail = e.detail || {};
|
||||
switch (detail.cmd) {
|
||||
case 'showCursor':
|
||||
OverlayStore.update({
|
||||
cursor: {
|
||||
x: detail.x || 200,
|
||||
y: detail.y || 200,
|
||||
visible: true,
|
||||
state: 'idle',
|
||||
},
|
||||
});
|
||||
OverlayMotion.setPosition(detail.x || 200, detail.y || 200);
|
||||
break;
|
||||
case 'hideCursor':
|
||||
OverlayStore.update({ cursor: { visible: false } });
|
||||
break;
|
||||
case 'summary':
|
||||
OverlayStore.update({
|
||||
summary: { text: detail.text || '', visible: !!detail.text },
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[Eigent Cursor] Content script loaded, overlay mounted.');
|
||||
} // end else block
|
||||
|
|
@ -19,6 +19,28 @@
|
|||
"action": {
|
||||
"default_title": "Open ADGM Co-work Agent"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": [
|
||||
"overlay/store.js",
|
||||
"overlay/events.js",
|
||||
"overlay/motion.js",
|
||||
"overlay/aurora.js",
|
||||
"overlay/cursor.js",
|
||||
"overlay/summary.js",
|
||||
"overlay/highlight.js",
|
||||
"content.js"
|
||||
],
|
||||
"run_at": "document_idle"
|
||||
},
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["overlay/bridge.js"],
|
||||
"run_at": "document_idle",
|
||||
"world": "MAIN"
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
|
|
|
|||
144
extensions/chrome_extension/overlay/aurora.js
Normal file
144
extensions/chrome_extension/overlay/aurora.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
// Aurora gradient corner effect — soft ambient glow at all 4 corners
|
||||
|
||||
const OverlayAurora = (() => {
|
||||
let container = null;
|
||||
let blobs = [];
|
||||
let reducedMotion = false;
|
||||
|
||||
// Each corner: a triangle of color that fades diagonally to transparent.
|
||||
// The opposite corner of the rectangle is fully transparent.
|
||||
const BLOB_CONFIGS = [
|
||||
{
|
||||
corner: 'top-left',
|
||||
css: 'top: 0; left: 0; width: 40vw; height: 50vh;',
|
||||
gradient: `linear-gradient(135deg, rgba(150,130,255,0.9) 0%, rgba(120,180,255,0.4) 25%, transparent 50%)`,
|
||||
delay: '0s',
|
||||
},
|
||||
{
|
||||
corner: 'top-right',
|
||||
css: 'top: 0; right: 0; width: 40vw; height: 50vh;',
|
||||
gradient: `linear-gradient(225deg, rgba(120,170,255,0.9) 0%, rgba(150,120,255,0.4) 25%, transparent 50%)`,
|
||||
delay: '-2s',
|
||||
},
|
||||
{
|
||||
corner: 'bottom-left',
|
||||
css: 'bottom: 0; left: 0; width: 40vw; height: 50vh;',
|
||||
gradient: `linear-gradient(45deg, rgba(120,190,255,0.9) 0%, rgba(155,130,255,0.4) 25%, transparent 50%)`,
|
||||
delay: '-4s',
|
||||
},
|
||||
{
|
||||
corner: 'bottom-right',
|
||||
css: 'bottom: 0; right: 0; width: 40vw; height: 50vh;',
|
||||
gradient: `linear-gradient(315deg, rgba(255,130,210,0.9) 0%, rgba(150,120,255,0.4) 25%, transparent 50%)`,
|
||||
delay: '-6s',
|
||||
},
|
||||
];
|
||||
|
||||
function getStyles() {
|
||||
return `
|
||||
.eigent-aurora-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 2147483646;
|
||||
opacity: 0;
|
||||
transition: opacity 0.8s ease;
|
||||
}
|
||||
|
||||
.eigent-aurora-container--visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.eigent-aurora-blob {
|
||||
position: absolute;
|
||||
will-change: opacity;
|
||||
animation: eigent-aurora-glow 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes eigent-aurora-glow {
|
||||
0%, 100% { opacity: var(--aurora-intensity); }
|
||||
50% { opacity: calc(var(--aurora-intensity) * 0.5); }
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function init(shadowRoot) {
|
||||
reducedMotion = window.matchMedia(
|
||||
'(prefers-reduced-motion: reduce)'
|
||||
).matches;
|
||||
window
|
||||
.matchMedia('(prefers-reduced-motion: reduce)')
|
||||
.addEventListener('change', (e) => {
|
||||
reducedMotion = e.matches;
|
||||
updateAnimation();
|
||||
});
|
||||
|
||||
container = document.createElement('div');
|
||||
container.className = 'eigent-aurora-container';
|
||||
|
||||
const state = OverlayStore.getState();
|
||||
const intensity = state.aurora.intensity || 0.7;
|
||||
|
||||
BLOB_CONFIGS.forEach((config) => {
|
||||
const blob = document.createElement('div');
|
||||
blob.className = 'eigent-aurora-blob';
|
||||
blob.style.cssText = `
|
||||
${config.css}
|
||||
background: ${config.gradient};
|
||||
--aurora-intensity: ${intensity};
|
||||
animation-delay: ${config.delay};
|
||||
${reducedMotion ? 'animation: none; opacity: ' + intensity + ';' : ''}
|
||||
`;
|
||||
blobs.push(blob);
|
||||
container.appendChild(blob);
|
||||
});
|
||||
|
||||
shadowRoot.appendChild(container);
|
||||
|
||||
OverlayStore.subscribe((state, changed) => {
|
||||
if (changed.aurora !== undefined || changed.enabled !== undefined) {
|
||||
updateVisibility();
|
||||
}
|
||||
});
|
||||
|
||||
updateVisibility();
|
||||
}
|
||||
|
||||
function updateVisibility() {
|
||||
if (!container) return;
|
||||
const state = OverlayStore.getState();
|
||||
const visible = state.enabled && state.aurora.visible;
|
||||
container.classList.toggle('eigent-aurora-container--visible', visible);
|
||||
|
||||
const intensity = state.aurora.intensity || 0.7;
|
||||
blobs.forEach((blob) => {
|
||||
blob.style.setProperty('--aurora-intensity', String(intensity));
|
||||
});
|
||||
}
|
||||
|
||||
function updateAnimation() {
|
||||
const state = OverlayStore.getState();
|
||||
const intensity = state.aurora.intensity || 0.7;
|
||||
blobs.forEach((blob) => {
|
||||
if (reducedMotion) {
|
||||
blob.style.animation = 'none';
|
||||
blob.style.opacity = String(intensity);
|
||||
} else {
|
||||
blob.style.animation = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (container) {
|
||||
container.remove();
|
||||
container = null;
|
||||
blobs = [];
|
||||
}
|
||||
}
|
||||
|
||||
return { init, getStyles, destroy };
|
||||
})();
|
||||
35
extensions/chrome_extension/overlay/bridge.js
Normal file
35
extensions/chrome_extension/overlay/bridge.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Bridge script — runs in the MAIN world (page context)
|
||||
// Exposes __eigentOverlayDemo to DevTools console by dispatching
|
||||
// a custom DOM event that the content script (isolated world) listens for.
|
||||
|
||||
window.__eigentOverlayDemo = function () {
|
||||
document.dispatchEvent(new CustomEvent('eigent-cursor-demo'));
|
||||
console.log('[Eigent] Demo triggered');
|
||||
};
|
||||
|
||||
window.__eigentOverlay = {
|
||||
demo: function () {
|
||||
document.dispatchEvent(new CustomEvent('eigent-cursor-demo'));
|
||||
},
|
||||
showCursor: function (x, y) {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('eigent-cursor-cmd', {
|
||||
detail: { cmd: 'showCursor', x: x, y: y },
|
||||
})
|
||||
);
|
||||
},
|
||||
hideCursor: function () {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('eigent-cursor-cmd', {
|
||||
detail: { cmd: 'hideCursor' },
|
||||
})
|
||||
);
|
||||
},
|
||||
summary: function (text) {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('eigent-cursor-cmd', {
|
||||
detail: { cmd: 'summary', text: text },
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
157
extensions/chrome_extension/overlay/cursor.js
Normal file
157
extensions/chrome_extension/overlay/cursor.js
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
// Agent cursor — custom SVG cursor with state-based animations
|
||||
|
||||
const OverlayCursor = (() => {
|
||||
let cursorEl = null;
|
||||
let shadowRoot = null;
|
||||
|
||||
// Custom cursor SVG from design
|
||||
const CURSOR_SVG = `
|
||||
<svg width="32" height="32" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_cursor)">
|
||||
<g filter="url(#filter0_cursor)">
|
||||
<path d="M43.1519 31.002C48.4436 28.1565 51.0894 26.7338 51.7589 25.021C52.3399 23.5348 52.1764 21.8606 51.3191 20.5147C50.3311 18.9638 47.4601 18.0794 41.7181 16.3106L20.0154 9.62537C15.1706 8.13297 12.7482 7.3868 11.1463 7.99175C9.75183 8.51835 8.66505 9.63962 8.18225 11.0498C7.62763 12.6699 8.44913 15.0678 10.0922 19.8636L17.37 41.1073C19.3909 47.0063 20.4014 49.956 22.0131 50.8935C23.4108 51.7065 25.1154 51.7935 26.5886 51.1268C28.2874 50.358 29.5926 47.5265 32.2034 41.8635L34.1321 37.6798C34.5476 36.7783 34.7554 36.3275 35.0399 35.934C35.2921 35.5845 35.5886 35.2695 35.9216 34.996C36.2971 34.688 36.7341 34.453 37.6084 33.983L43.1519 31.002Z" fill="#155DFC"/>
|
||||
<path d="M43.1519 31.002C48.4436 28.1565 51.0894 26.7338 51.7589 25.021C52.3399 23.5348 52.1764 21.8606 51.3191 20.5147C50.3311 18.9638 47.4601 18.0794 41.7181 16.3106L20.0154 9.62537C15.1706 8.13297 12.7482 7.3868 11.1463 7.99175C9.75183 8.51835 8.66505 9.63962 8.18225 11.0498C7.62763 12.6699 8.44913 15.0678 10.0922 19.8636L17.37 41.1073C19.3909 47.0063 20.4014 49.956 22.0131 50.8935C23.4108 51.7065 25.1154 51.7935 26.5886 51.1268C28.2874 50.358 29.5926 47.5265 32.2034 41.8635L34.1321 37.6798C34.5476 36.7783 34.7554 36.3275 35.0399 35.934C35.2921 35.5845 35.5886 35.2695 35.9216 34.996C36.2971 34.688 36.7341 34.453 37.6084 33.983L43.1519 31.002Z" stroke="white" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_cursor" x="0.509766" y="2.28125" width="59.0918" height="58.7903" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="2.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.113725 0 0 0 0 0.113725 0 0 0 0 0.113725 0 0 0 0.4 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_cursor"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_cursor" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_cursor">
|
||||
<rect width="60" height="60" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
function getStyles() {
|
||||
return `
|
||||
.eigent-cursor {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
pointer-events: none;
|
||||
z-index: 2147483647;
|
||||
will-change: transform, opacity;
|
||||
transition: opacity 0.2s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.eigent-cursor--visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.eigent-cursor--idle {
|
||||
animation: eigent-cursor-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.eigent-cursor--clicking {
|
||||
animation: eigent-cursor-click 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.eigent-cursor--thinking {
|
||||
animation: eigent-cursor-think 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes eigent-cursor-pulse {
|
||||
0%, 100% { transform: var(--cursor-translate) scale(1); }
|
||||
50% { transform: var(--cursor-translate) scale(1.08); }
|
||||
}
|
||||
|
||||
@keyframes eigent-cursor-click {
|
||||
0% { transform: var(--cursor-translate) scale(1); }
|
||||
40% { transform: var(--cursor-translate) scale(0.82); }
|
||||
100% { transform: var(--cursor-translate) scale(1); }
|
||||
}
|
||||
|
||||
@keyframes eigent-cursor-think {
|
||||
0% { transform: var(--cursor-translate) rotate(0deg); }
|
||||
25% { transform: var(--cursor-translate) rotate(6deg); }
|
||||
75% { transform: var(--cursor-translate) rotate(-6deg); }
|
||||
100% { transform: var(--cursor-translate) rotate(0deg); }
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function init(root) {
|
||||
shadowRoot = root;
|
||||
|
||||
cursorEl = document.createElement('div');
|
||||
cursorEl.className = 'eigent-cursor';
|
||||
cursorEl.innerHTML = CURSOR_SVG;
|
||||
|
||||
shadowRoot.appendChild(cursorEl);
|
||||
|
||||
OverlayStore.subscribe((state, changed) => {
|
||||
if (changed.cursor !== undefined || changed.enabled !== undefined) {
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!cursorEl) return;
|
||||
const { cursor, enabled, reducedMotion } = OverlayStore.getState();
|
||||
|
||||
const visible = enabled && cursor.visible;
|
||||
cursorEl.classList.toggle('eigent-cursor--visible', visible);
|
||||
|
||||
cursorEl.style.setProperty(
|
||||
'--cursor-translate',
|
||||
`translate3d(${cursor.x}px, ${cursor.y}px, 0)`
|
||||
);
|
||||
cursorEl.style.transform = `translate3d(${cursor.x}px, ${cursor.y}px, 0)`;
|
||||
|
||||
const states = [
|
||||
'idle',
|
||||
'moving',
|
||||
'hovering',
|
||||
'clicking',
|
||||
'thinking',
|
||||
'complete',
|
||||
'error',
|
||||
];
|
||||
for (const s of states) {
|
||||
cursorEl.classList.toggle(`eigent-cursor--${s}`, cursor.state === s);
|
||||
}
|
||||
|
||||
if (reducedMotion) {
|
||||
cursorEl.style.animation = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function animateClick() {
|
||||
OverlayStore.update({ cursor: { state: 'clicking' } });
|
||||
setTimeout(() => {
|
||||
OverlayStore.update({ cursor: { state: 'idle' } });
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function show() {
|
||||
OverlayStore.update({ cursor: { visible: true } });
|
||||
}
|
||||
|
||||
function hide() {
|
||||
OverlayStore.update({ cursor: { visible: false } });
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (cursorEl) {
|
||||
cursorEl.remove();
|
||||
cursorEl = null;
|
||||
}
|
||||
}
|
||||
|
||||
return { init, getStyles, animateClick, show, hide, destroy };
|
||||
})();
|
||||
213
extensions/chrome_extension/overlay/events.js
Normal file
213
extensions/chrome_extension/overlay/events.js
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
// Event types and message bridge between background ↔ content script
|
||||
|
||||
const OverlayEvents = (() => {
|
||||
// Message types: Background → Content
|
||||
const TYPES = {
|
||||
AGENT_STEP: 'OVERLAY_AGENT_STEP',
|
||||
CURSOR_MOVE: 'OVERLAY_CURSOR_MOVE',
|
||||
HIGHLIGHT: 'OVERLAY_HIGHLIGHT',
|
||||
SUMMARY: 'OVERLAY_SUMMARY',
|
||||
STATE: 'OVERLAY_STATE',
|
||||
RESOLVE_TARGET: 'OVERLAY_RESOLVE_TARGET',
|
||||
RESOLVE_AND_MOVE: 'OVERLAY_RESOLVE_AND_MOVE',
|
||||
// Content → Background
|
||||
READY: 'OVERLAY_READY',
|
||||
ELEMENT_RESOLVED: 'OVERLAY_ELEMENT_RESOLVED',
|
||||
};
|
||||
|
||||
function listen() {
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (!message || !message.type || !message.type.startsWith('OVERLAY_')) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (message.type) {
|
||||
case TYPES.AGENT_STEP:
|
||||
handleAgentStep(message);
|
||||
break;
|
||||
case 'OVERLAY_DEMO':
|
||||
if (typeof window.__eigentOverlayDemo === 'function') {
|
||||
window.__eigentOverlayDemo();
|
||||
}
|
||||
break;
|
||||
case TYPES.CURSOR_MOVE:
|
||||
console.log(
|
||||
'[Eigent Cursor] CURSOR_MOVE received:',
|
||||
message.x,
|
||||
message.y
|
||||
);
|
||||
OverlayStore.update({
|
||||
cursor: {
|
||||
x: message.x,
|
||||
y: message.y,
|
||||
state: 'moving',
|
||||
visible: true,
|
||||
},
|
||||
});
|
||||
if (typeof OverlayMotion !== 'undefined') {
|
||||
OverlayMotion.moveCursor(message.x, message.y, message.duration);
|
||||
}
|
||||
break;
|
||||
case TYPES.HIGHLIGHT:
|
||||
if (typeof OverlayHighlight !== 'undefined') {
|
||||
OverlayHighlight.highlightSelector(
|
||||
message.selector,
|
||||
message.duration
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'OVERLAY_HIGHLIGHT_RECT':
|
||||
// Highlight at a pre-resolved rect (from CDP resolution)
|
||||
if (typeof OverlayHighlight !== 'undefined' && message.rect) {
|
||||
OverlayHighlight.showRect(message.rect);
|
||||
if (message.duration > 0) {
|
||||
setTimeout(() => OverlayHighlight.hide(), message.duration);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case TYPES.SUMMARY:
|
||||
OverlayStore.update({
|
||||
summary: { text: message.text, visible: !!message.text },
|
||||
});
|
||||
break;
|
||||
case TYPES.STATE: {
|
||||
const stateUpdate = {};
|
||||
if (message.enabled !== undefined) {
|
||||
stateUpdate.enabled = message.enabled;
|
||||
}
|
||||
if (message.auroraVisible !== undefined) {
|
||||
stateUpdate.aurora = { visible: message.auroraVisible };
|
||||
}
|
||||
if (message.cursorVisible !== undefined) {
|
||||
stateUpdate.cursor = { visible: message.cursorVisible };
|
||||
}
|
||||
if (message.summaryText !== undefined) {
|
||||
stateUpdate.summary = {
|
||||
text: message.summaryText,
|
||||
visible: !!message.summaryText,
|
||||
};
|
||||
}
|
||||
OverlayStore.update(stateUpdate);
|
||||
break;
|
||||
}
|
||||
case TYPES.RESOLVE_AND_MOVE: {
|
||||
// Resolve element by ARIA ref and move cursor to its center
|
||||
const ref = message.ref;
|
||||
console.log('[Eigent Cursor] RESOLVE_AND_MOVE for ref:', ref);
|
||||
const el = resolveElementByRef(ref);
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const cx = rect.left + rect.width / 2;
|
||||
const cy = rect.top + rect.height / 2;
|
||||
console.log('[Eigent Cursor] Resolved ref', ref, 'to', cx, cy);
|
||||
OverlayStore.update({
|
||||
cursor: { visible: true, state: 'moving' },
|
||||
});
|
||||
if (typeof OverlayMotion !== 'undefined') {
|
||||
OverlayMotion.moveCursor(cx, cy);
|
||||
}
|
||||
} else {
|
||||
console.log('[Eigent Cursor] Could not resolve ref:', ref);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TYPES.RESOLVE_TARGET:
|
||||
if (typeof OverlayDomResolver !== 'undefined') {
|
||||
const result = OverlayDomResolver.resolve(message.target);
|
||||
sendResponse(result);
|
||||
}
|
||||
return true; // async response
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleAgentStep(msg) {
|
||||
if (msg.summary) {
|
||||
OverlayStore.update({ summary: { text: msg.summary, visible: true } });
|
||||
}
|
||||
if (msg.cursor) {
|
||||
OverlayStore.update({ cursor: { visible: true } });
|
||||
if (typeof OverlayMotion !== 'undefined') {
|
||||
OverlayMotion.moveCursor(msg.cursor.x, msg.cursor.y);
|
||||
}
|
||||
}
|
||||
if (msg.highlight) {
|
||||
OverlayStore.update({ highlight: msg.highlight });
|
||||
}
|
||||
if (msg.state === 'done' || msg.state === 'error') {
|
||||
setTimeout(() => {
|
||||
OverlayStore.update({
|
||||
cursor: { state: msg.state === 'done' ? 'complete' : 'error' },
|
||||
summary: { visible: false },
|
||||
highlight: null,
|
||||
});
|
||||
}, 1200);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve an element by ARIA ref (e.g. "e46") — same methods as the CDP highlight system
|
||||
function resolveElementByRef(ref) {
|
||||
if (!ref) return null;
|
||||
|
||||
// Method 1: __ariaSnapshot.getElementByRef
|
||||
if (
|
||||
typeof __ariaSnapshot !== 'undefined' &&
|
||||
__ariaSnapshot.getElementByRef
|
||||
) {
|
||||
try {
|
||||
const el = __ariaSnapshot.getElementByRef(ref, document.body);
|
||||
if (el) return el;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Method 2: Walk DOM for _ariaRef property
|
||||
const walker = document.createTreeWalker(
|
||||
document.body,
|
||||
NodeFilter.SHOW_ELEMENT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
let node;
|
||||
while ((node = walker.nextNode())) {
|
||||
if (node._ariaRef && node._ariaRef.ref === ref) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: data attributes
|
||||
const refNum = ref.replace(/^e/, '');
|
||||
const selectors = [
|
||||
`[data-ref="${ref}"]`,
|
||||
`[data-ref="${refNum}"]`,
|
||||
`[ref="${ref}"]`,
|
||||
`[aria-ref="${ref}"]`,
|
||||
`[data-camel-ref="${ref}"]`,
|
||||
`[data-camel-ref="${refNum}"]`,
|
||||
];
|
||||
for (const sel of selectors) {
|
||||
try {
|
||||
const el = document.querySelector(sel);
|
||||
if (el) return el;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Method 4: Try as CSS selector directly
|
||||
if (ref.includes('[') || ref.includes('.') || ref.includes('#')) {
|
||||
try {
|
||||
return document.querySelector(ref);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function sendToBackground(type, data = {}) {
|
||||
chrome.runtime.sendMessage({ type, ...data }).catch(() => {});
|
||||
}
|
||||
|
||||
function notifyReady() {
|
||||
sendToBackground(TYPES.READY);
|
||||
}
|
||||
|
||||
return { TYPES, listen, sendToBackground, notifyReady };
|
||||
})();
|
||||
264
extensions/chrome_extension/overlay/highlight.js
Normal file
264
extensions/chrome_extension/overlay/highlight.js
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
// Target element highlight — Atlas-style blue glow
|
||||
|
||||
const OverlayHighlight = (() => {
|
||||
let highlightEl = null;
|
||||
let shadowRoot = null;
|
||||
let scrollListener = null;
|
||||
let resizeListener = null;
|
||||
let currentTarget = null; // DOM element being tracked
|
||||
let updateFrame = null;
|
||||
|
||||
function getStyles() {
|
||||
return `
|
||||
.eigent-highlight {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
z-index: 2147483646;
|
||||
border: 2px solid rgba(120, 180, 255, 0.6);
|
||||
border-radius: 8px;
|
||||
background: rgba(120, 180, 255, 0.08);
|
||||
box-shadow: 0 0 12px rgba(120, 180, 255, 0.3), inset 0 0 8px rgba(120, 180, 255, 0.1);
|
||||
will-change: transform, opacity, width, height;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease, transform 0.15s ease, width 0.15s ease, height 0.15s ease;
|
||||
}
|
||||
|
||||
.eigent-highlight--visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.eigent-highlight--pulse {
|
||||
animation: eigent-highlight-pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes eigent-highlight-pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 12px rgba(120, 180, 255, 0.3), inset 0 0 8px rgba(120, 180, 255, 0.1);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(120, 180, 255, 0.5), inset 0 0 12px rgba(120, 180, 255, 0.15);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function init(root) {
|
||||
shadowRoot = root;
|
||||
|
||||
highlightEl = document.createElement('div');
|
||||
highlightEl.className = 'eigent-highlight';
|
||||
shadowRoot.appendChild(highlightEl);
|
||||
|
||||
// Track scroll and resize for position updates
|
||||
scrollListener = () => updatePosition();
|
||||
resizeListener = () => updatePosition();
|
||||
window.addEventListener('scroll', scrollListener, {
|
||||
passive: true,
|
||||
capture: true,
|
||||
});
|
||||
window.addEventListener('resize', resizeListener, { passive: true });
|
||||
|
||||
OverlayStore.subscribe((state, changed) => {
|
||||
if (changed.highlight !== undefined) {
|
||||
if (state.highlight) {
|
||||
showRect(state.highlight);
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
if (changed.enabled !== undefined && !state.enabled) {
|
||||
hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show highlight at a specific rect { x, y, width, height }
|
||||
function showRect(rect) {
|
||||
if (!highlightEl) return;
|
||||
const padding = 6;
|
||||
highlightEl.style.transform = `translate3d(${rect.x - padding}px, ${rect.y - padding}px, 0)`;
|
||||
highlightEl.style.width = `${rect.width + padding * 2}px`;
|
||||
highlightEl.style.height = `${rect.height + padding * 2}px`;
|
||||
highlightEl.classList.add(
|
||||
'eigent-highlight--visible',
|
||||
'eigent-highlight--pulse'
|
||||
);
|
||||
}
|
||||
|
||||
// Highlight a DOM element by selector — resolves and tracks it
|
||||
function highlightSelector(selector, duration = 2000) {
|
||||
const element = resolveElement(selector);
|
||||
if (!element) {
|
||||
console.warn('[Eigent Cursor] Highlight target not found:', selector);
|
||||
hide();
|
||||
return null;
|
||||
}
|
||||
|
||||
currentTarget = element;
|
||||
updatePosition();
|
||||
highlightEl.classList.add(
|
||||
'eigent-highlight--visible',
|
||||
'eigent-highlight--pulse'
|
||||
);
|
||||
|
||||
// Update store with rect
|
||||
const rect = element.getBoundingClientRect();
|
||||
OverlayStore.update({
|
||||
highlight: {
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
},
|
||||
});
|
||||
|
||||
// Auto-hide after duration
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
if (currentTarget === element) {
|
||||
hide();
|
||||
}
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
||||
}
|
||||
|
||||
// Resolve element from various descriptor types
|
||||
function resolveElement(descriptor) {
|
||||
if (typeof descriptor === 'string') {
|
||||
// Try CSS selector
|
||||
try {
|
||||
const el = document.querySelector(descriptor);
|
||||
if (el && isVisible(el)) return el;
|
||||
} catch (e) {}
|
||||
|
||||
// Try text content match
|
||||
return findByText(descriptor);
|
||||
}
|
||||
|
||||
if (typeof descriptor === 'object') {
|
||||
// Try selector first
|
||||
if (descriptor.selector) {
|
||||
try {
|
||||
const el = document.querySelector(descriptor.selector);
|
||||
if (el && isVisible(el)) return el;
|
||||
} catch (e) {}
|
||||
}
|
||||
// Try ARIA label
|
||||
if (descriptor.ariaLabel) {
|
||||
const el = document.querySelector(
|
||||
`[aria-label="${CSS.escape(descriptor.ariaLabel)}"]`
|
||||
);
|
||||
if (el && isVisible(el)) return el;
|
||||
}
|
||||
// Try role + text
|
||||
if (descriptor.role) {
|
||||
const candidates = document.querySelectorAll(
|
||||
`[role="${descriptor.role}"]`
|
||||
);
|
||||
for (const el of candidates) {
|
||||
if (
|
||||
descriptor.text &&
|
||||
el.textContent.includes(descriptor.text) &&
|
||||
isVisible(el)
|
||||
) {
|
||||
return el;
|
||||
}
|
||||
if (!descriptor.text && isVisible(el)) return el;
|
||||
}
|
||||
}
|
||||
// Try text
|
||||
if (descriptor.text) {
|
||||
return findByText(descriptor.text);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findByText(text) {
|
||||
const walker = document.createTreeWalker(
|
||||
document.body,
|
||||
NodeFilter.SHOW_ELEMENT
|
||||
);
|
||||
let node;
|
||||
while ((node = walker.nextNode())) {
|
||||
if (node.children.length === 0 || node.childNodes.length === 1) {
|
||||
const content = (node.textContent || '').trim();
|
||||
if (content === text || content.includes(text)) {
|
||||
if (isVisible(node)) return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
const style = getComputedStyle(el);
|
||||
if (
|
||||
style.display === 'none' ||
|
||||
style.visibility === 'hidden' ||
|
||||
style.opacity === '0'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const rect = el.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
}
|
||||
|
||||
function updatePosition() {
|
||||
if (!currentTarget || !highlightEl) return;
|
||||
if (updateFrame) cancelAnimationFrame(updateFrame);
|
||||
|
||||
updateFrame = requestAnimationFrame(() => {
|
||||
if (!currentTarget) return;
|
||||
const rect = currentTarget.getBoundingClientRect();
|
||||
if (rect.width === 0 && rect.height === 0) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
const padding = 6;
|
||||
highlightEl.style.transform = `translate3d(${rect.left - padding}px, ${rect.top - padding}px, 0)`;
|
||||
highlightEl.style.width = `${rect.width + padding * 2}px`;
|
||||
highlightEl.style.height = `${rect.height + padding * 2}px`;
|
||||
});
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (!highlightEl) return;
|
||||
highlightEl.classList.remove(
|
||||
'eigent-highlight--visible',
|
||||
'eigent-highlight--pulse'
|
||||
);
|
||||
currentTarget = null;
|
||||
// Only update store if highlight isn't already null (avoid re-entrant loop)
|
||||
if (OverlayStore.getState().highlight !== null) {
|
||||
OverlayStore.update({ highlight: null });
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (scrollListener)
|
||||
window.removeEventListener('scroll', scrollListener, { capture: true });
|
||||
if (resizeListener) window.removeEventListener('resize', resizeListener);
|
||||
if (updateFrame) cancelAnimationFrame(updateFrame);
|
||||
if (highlightEl) {
|
||||
highlightEl.remove();
|
||||
highlightEl = null;
|
||||
}
|
||||
currentTarget = null;
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
getStyles,
|
||||
highlightSelector,
|
||||
showRect,
|
||||
hide,
|
||||
resolveElement,
|
||||
destroy,
|
||||
};
|
||||
})();
|
||||
147
extensions/chrome_extension/overlay/motion.js
Normal file
147
extensions/chrome_extension/overlay/motion.js
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
// Animation utilities — spring easing, cursor motion, tab visibility
|
||||
|
||||
const OverlayMotion = (() => {
|
||||
let cursorAnimFrame = null;
|
||||
let cursorCurrentX = 0;
|
||||
let cursorCurrentY = 0;
|
||||
let tabVisible = true;
|
||||
|
||||
// Track tab visibility to pause animations
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
tabVisible = !document.hidden;
|
||||
});
|
||||
|
||||
// Easing functions
|
||||
function easeOutCubic(t) {
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
}
|
||||
|
||||
function easeOutQuart(t) {
|
||||
return 1 - Math.pow(1 - t, 4);
|
||||
}
|
||||
|
||||
// Spring approximation — overdamp for smooth settle
|
||||
function springEase(t) {
|
||||
const c4 = (2 * Math.PI) / 3;
|
||||
return t === 0
|
||||
? 0
|
||||
: t === 1
|
||||
? 1
|
||||
: Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
|
||||
}
|
||||
|
||||
// Compute duration based on distance (300ms min, 900ms max)
|
||||
function durationForDistance(x1, y1, x2, y2) {
|
||||
const dist = Math.hypot(x2 - x1, y2 - y1);
|
||||
return Math.min(900, Math.max(300, dist * 1.2));
|
||||
}
|
||||
|
||||
// Animate a value from → to using requestAnimationFrame
|
||||
function animate({
|
||||
from,
|
||||
to,
|
||||
duration,
|
||||
easing = easeOutCubic,
|
||||
onUpdate,
|
||||
onComplete,
|
||||
}) {
|
||||
const start = performance.now();
|
||||
let frame;
|
||||
|
||||
function tick(now) {
|
||||
const elapsed = now - start;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const easedProgress = easing(progress);
|
||||
|
||||
// Interpolate
|
||||
if (typeof from === 'number') {
|
||||
onUpdate(from + (to - from) * easedProgress);
|
||||
} else {
|
||||
// Object with x, y
|
||||
onUpdate({
|
||||
x: from.x + (to.x - from.x) * easedProgress,
|
||||
y: from.y + (to.y - from.y) * easedProgress,
|
||||
});
|
||||
}
|
||||
|
||||
if (progress < 1 && tabVisible) {
|
||||
frame = requestAnimationFrame(tick);
|
||||
} else if (progress >= 1) {
|
||||
if (onComplete) onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
frame = requestAnimationFrame(tick);
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}
|
||||
|
||||
// Move cursor to target position with smooth animation
|
||||
function moveCursor(targetX, targetY, customDuration) {
|
||||
if (cursorAnimFrame) {
|
||||
cancelAnimationFrame(cursorAnimFrame);
|
||||
cursorAnimFrame = null;
|
||||
}
|
||||
|
||||
const duration =
|
||||
customDuration ||
|
||||
durationForDistance(cursorCurrentX, cursorCurrentY, targetX, targetY);
|
||||
const startX = cursorCurrentX;
|
||||
const startY = cursorCurrentY;
|
||||
const startTime = performance.now();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
function tick(now) {
|
||||
const elapsed = now - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const eased = easeOutQuart(progress);
|
||||
|
||||
cursorCurrentX = startX + (targetX - startX) * eased;
|
||||
cursorCurrentY = startY + (targetY - startY) * eased;
|
||||
|
||||
// Update store (cursor module reads from store)
|
||||
OverlayStore.update({
|
||||
cursor: {
|
||||
x: cursorCurrentX,
|
||||
y: cursorCurrentY,
|
||||
state: progress < 1 ? 'moving' : 'idle',
|
||||
},
|
||||
});
|
||||
|
||||
if (progress < 1 && tabVisible) {
|
||||
cursorAnimFrame = requestAnimationFrame(tick);
|
||||
} else {
|
||||
cursorAnimFrame = null;
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
cursorAnimFrame = requestAnimationFrame(tick);
|
||||
});
|
||||
}
|
||||
|
||||
// Settle delay — pause before click action
|
||||
function settle(ms = 150) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function getCurrentPosition() {
|
||||
return { x: cursorCurrentX, y: cursorCurrentY };
|
||||
}
|
||||
|
||||
function setPosition(x, y) {
|
||||
cursorCurrentX = x;
|
||||
cursorCurrentY = y;
|
||||
}
|
||||
|
||||
return {
|
||||
animate,
|
||||
moveCursor,
|
||||
settle,
|
||||
easeOutCubic,
|
||||
easeOutQuart,
|
||||
springEase,
|
||||
getCurrentPosition,
|
||||
setPosition,
|
||||
durationForDistance,
|
||||
};
|
||||
})();
|
||||
70
extensions/chrome_extension/overlay/store.js
Normal file
70
extensions/chrome_extension/overlay/store.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// Overlay state store — simple pub/sub, no framework
|
||||
// Shared across all overlay modules via content script injection order
|
||||
|
||||
const OverlayStore = (() => {
|
||||
const state = {
|
||||
enabled: true,
|
||||
reducedMotion: false,
|
||||
aurora: { visible: false, intensity: 0.7 },
|
||||
cursor: { x: 0, y: 0, state: 'idle', visible: false },
|
||||
summary: { text: '', visible: false },
|
||||
highlight: null, // { x, y, width, height } or null
|
||||
};
|
||||
|
||||
const listeners = new Set();
|
||||
let notifying = false;
|
||||
let pendingUpdate = null;
|
||||
|
||||
function getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
function update(partial) {
|
||||
// Apply state changes immediately
|
||||
for (const key of Object.keys(partial)) {
|
||||
if (
|
||||
typeof partial[key] === 'object' &&
|
||||
partial[key] !== null &&
|
||||
!Array.isArray(partial[key])
|
||||
) {
|
||||
state[key] = { ...state[key], ...partial[key] };
|
||||
} else {
|
||||
state[key] = partial[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Re-entrancy guard: if a subscriber calls update(), batch it
|
||||
if (notifying) {
|
||||
pendingUpdate = pendingUpdate || {};
|
||||
Object.assign(pendingUpdate, partial);
|
||||
return;
|
||||
}
|
||||
|
||||
notifying = true;
|
||||
try {
|
||||
for (const fn of listeners) {
|
||||
try {
|
||||
fn(state, partial);
|
||||
} catch (e) {
|
||||
console.error('[Eigent Cursor] Store listener error:', e);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
notifying = false;
|
||||
}
|
||||
|
||||
// Flush any updates that were queued during notification
|
||||
if (pendingUpdate) {
|
||||
const queued = pendingUpdate;
|
||||
pendingUpdate = null;
|
||||
update(queued);
|
||||
}
|
||||
}
|
||||
|
||||
function subscribe(fn) {
|
||||
listeners.add(fn);
|
||||
return () => listeners.delete(fn);
|
||||
}
|
||||
|
||||
return { getState, update, subscribe };
|
||||
})();
|
||||
174
extensions/chrome_extension/overlay/summary.js
Normal file
174
extensions/chrome_extension/overlay/summary.js
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// Floating action summary bubble — attached near cursor
|
||||
|
||||
const OverlaySummary = (() => {
|
||||
let bubbleEl = null;
|
||||
let currentText = '';
|
||||
let followTimer = null;
|
||||
let bubbleX = 0;
|
||||
let bubbleY = 0;
|
||||
|
||||
const OFFSET_X = 20; // px right of cursor
|
||||
const OFFSET_Y = 28; // px below cursor
|
||||
const MAX_CHARS = 64;
|
||||
const FOLLOW_LAG = 0; // no lag, follow cursor immediately
|
||||
|
||||
function getStyles() {
|
||||
return `
|
||||
.eigent-summary {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
z-index: 2147483647;
|
||||
will-change: transform, opacity;
|
||||
max-width: 320px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font: 13px/1.3 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
color: #fff;
|
||||
background: #155DFC;
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border-radius: 9999px;
|
||||
padding: 8px 18px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
opacity: 0;
|
||||
transition: opacity 0.18s ease;
|
||||
}
|
||||
|
||||
.eigent-summary--visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.eigent-summary--entering {
|
||||
transform: translate3d(var(--summary-x), var(--summary-y), 0) translateY(4px);
|
||||
}
|
||||
|
||||
.eigent-summary--active {
|
||||
transform: translate3d(var(--summary-x), var(--summary-y), 0);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function init(shadowRoot) {
|
||||
bubbleEl = document.createElement('div');
|
||||
bubbleEl.className = 'eigent-summary';
|
||||
shadowRoot.appendChild(bubbleEl);
|
||||
|
||||
OverlayStore.subscribe((state, changed) => {
|
||||
if (changed.summary !== undefined) {
|
||||
updateText(state.summary);
|
||||
}
|
||||
if (changed.cursor !== undefined) {
|
||||
scheduleFollow(state.cursor);
|
||||
}
|
||||
if (changed.enabled !== undefined && !state.enabled) {
|
||||
hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateText(summary) {
|
||||
if (!bubbleEl) return;
|
||||
|
||||
if (!summary.visible || !summary.text) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const text =
|
||||
summary.text.length > MAX_CHARS
|
||||
? summary.text.slice(0, MAX_CHARS - 1) + '\u2026'
|
||||
: summary.text;
|
||||
|
||||
if (text !== currentText) {
|
||||
// Fade out, swap, fade in
|
||||
bubbleEl.classList.remove('eigent-summary--active');
|
||||
bubbleEl.classList.add('eigent-summary--entering');
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
bubbleEl.textContent = text;
|
||||
currentText = text;
|
||||
// Position at center-top if cursor not visible
|
||||
const { cursor } = OverlayStore.getState();
|
||||
if (!cursor.visible) {
|
||||
positionCenterTop();
|
||||
}
|
||||
bubbleEl.classList.add('eigent-summary--visible');
|
||||
requestAnimationFrame(() => {
|
||||
bubbleEl.classList.remove('eigent-summary--entering');
|
||||
bubbleEl.classList.add('eigent-summary--active');
|
||||
});
|
||||
},
|
||||
currentText ? 120 : 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleFollow(cursor) {
|
||||
if (cursor.visible) {
|
||||
// Cursor is active (clicking action) — follow it
|
||||
setPosition(cursor.x + OFFSET_X, cursor.y + OFFSET_Y);
|
||||
} else {
|
||||
// No cursor (non-click action) — center top of screen, 10vh from top
|
||||
positionCenterTop();
|
||||
}
|
||||
}
|
||||
|
||||
function positionCenterTop() {
|
||||
if (!bubbleEl) return;
|
||||
const vw = window.innerWidth;
|
||||
const rect = bubbleEl.getBoundingClientRect();
|
||||
const w = rect.width || 200;
|
||||
const x = (vw - w) / 2;
|
||||
const y = window.innerHeight * 0.1; // 10vh from top
|
||||
bubbleX = x;
|
||||
bubbleY = y;
|
||||
bubbleEl.style.setProperty('--summary-x', `${bubbleX}px`);
|
||||
bubbleEl.style.setProperty('--summary-y', `${bubbleY}px`);
|
||||
bubbleEl.style.transform = `translate3d(${bubbleX}px, ${bubbleY}px, 0)`;
|
||||
}
|
||||
|
||||
function setPosition(x, y) {
|
||||
if (!bubbleEl) return;
|
||||
bubbleX = x;
|
||||
bubbleY = y;
|
||||
|
||||
// Clamp to viewport
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
const rect = bubbleEl.getBoundingClientRect();
|
||||
const w = rect.width || 200;
|
||||
const h = rect.height || 30;
|
||||
|
||||
if (bubbleX + w > vw - 8) bubbleX = vw - w - 8;
|
||||
if (bubbleY + h > vh - 8) bubbleY = vh - h - 8;
|
||||
if (bubbleX < 8) bubbleX = 8;
|
||||
if (bubbleY < 8) bubbleY = 8;
|
||||
|
||||
bubbleEl.style.setProperty('--summary-x', `${bubbleX}px`);
|
||||
bubbleEl.style.setProperty('--summary-y', `${bubbleY}px`);
|
||||
bubbleEl.style.transform = `translate3d(${bubbleX}px, ${bubbleY}px, 0)`;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (!bubbleEl) return;
|
||||
bubbleEl.classList.remove(
|
||||
'eigent-summary--visible',
|
||||
'eigent-summary--active'
|
||||
);
|
||||
currentText = '';
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (followTimer) clearTimeout(followTimer);
|
||||
if (bubbleEl) {
|
||||
bubbleEl.remove();
|
||||
bubbleEl = null;
|
||||
}
|
||||
}
|
||||
|
||||
return { init, getStyles, hide, destroy };
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue