UNPKG

donobu

Version:

Create browser automations with an LLM agent and replay them as Playwright scripts.

463 lines (394 loc) 13.4 kB
(() => { /** * Wait until document.body is available, then call `init()`. */ function waitForBody(init) { if (document.body) { init(); } else { document.addEventListener('DOMContentLoaded', init, { once: true }); } } /** * Build an SVG icon via DOM APIs (to avoid TrustedHTML issues). */ function buildSvgIcon(opts) { const ns = 'http://www.w3.org/2000/svg'; const svg = document.createElementNS(ns, 'svg'); for (const [attr, val] of Object.entries(opts.svgAttrs || {})) { svg.setAttribute(attr, val); } // Add child elements (circle, polygon, line, etc.) for (const childSpec of opts.children || []) { const child = document.createElementNS(ns, childSpec.tag); for (const [attr, val] of Object.entries(childSpec.attrs || {})) { child.setAttribute(attr, val); } svg.appendChild(child); } return svg; } /** * Main setup logic for the control panel. */ function createControlPanel() { // Only create if not already in DOM. const existingHost = document.getElementById('donobu-control-panel'); if (existingHost) return existingHost; // Host element const hostEl = document.createElement('div'); hostEl.id = 'donobu-control-panel'; document.body.appendChild(hostEl); // Shadow root const shadow = hostEl.attachShadow({ mode: 'open' }); // Global styles const styleEl = document.createElement('style'); styleEl.textContent = ` :host { all: initial; position: fixed; top: 150px; right: 20px; z-index: 2147483647; width: 300px; } #panel-content, #panel-content * { box-sizing: border-box; margin: 0; padding: 0; font-family: Helvetica, Arial, sans-serif; font-size: 14px; color: #fff !important; } #donobu-control-panel-title-text { font-weight: 600 !important; margin-left: 10px; } #panel-content input[type="text"] { border: 1px solid #ddd !important; background-color: #fff !important; color: #000 !important; } #donobu-play-icon, #donobu-pause-icon, #donobu-end-icon { fill: none !important; stroke: currentColor !important; stroke-width: 2 !important; stroke-linecap: round !important; stroke-linejoin: round !important; } #donobu-control-panel-display-area { color: #fff !important; margin-top: 10px; } #panel-content { background-color: rgba(6,8,7,0.9); border: 3px solid rgba(251,188,5, 0.9); border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.8); transition: height 0.3s ease; cursor: move; width: 100%; overflow: hidden; } #donobu-control-panel-expandable-content { display: none; padding: 15px; max-height: 300px; overflow-y: auto; } .navbar { display: flex; align-items: center; justify-content: space-between; padding: 15px; font-weight: 600; background-color: rgba(6,8,7,0.9); border-bottom: 1px solid rgba(251,188,5, 0.5); } .icon-btn { margin-right: 8px; display: flex; cursor: pointer; opacity: 0.5; } #donobu-instructions-input { width: 100%; margin-bottom: 10px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .icon-container { display: flex; align-items: center; margin-right: 5px; } `; shadow.appendChild(styleEl); // Main panel container const panelContent = document.createElement('div'); panelContent.id = 'panel-content'; // Navbar const navbar = document.createElement('div'); navbar.className = 'navbar'; // Title container const titleContainer = document.createElement('div'); Object.assign(titleContainer.style, { display: 'flex', alignItems: 'center', }); // Icon container const iconContainer = document.createElement('div'); iconContainer.className = 'icon-container'; // Build the actual SVG DOM nodes: const playIcon = buildSvgIcon({ svgAttrs: { id: 'donobu-play-icon', width: '24', height: '24', viewBox: '0 0 24 24', }, children: [ { tag: 'circle', attrs: { cx: '12', cy: '12', r: '10' } }, { tag: 'polygon', attrs: { points: '10 8 16 12 10 16 10 8' } }, ], }); const pauseIcon = buildSvgIcon({ svgAttrs: { id: 'donobu-pause-icon', width: '24', height: '24', viewBox: '0 0 24 24', }, children: [ { tag: 'circle', attrs: { cx: '12', cy: '12', r: '10' } }, { tag: 'line', attrs: { x1: '10', y1: '15', x2: '10', y2: '9' } }, { tag: 'line', attrs: { x1: '14', y1: '15', x2: '14', y2: '9' } }, ], }); const endIcon = buildSvgIcon({ svgAttrs: { id: 'donobu-end-icon', width: '24', height: '24', viewBox: '0 0 24 24', }, children: [ { tag: 'circle', attrs: { cx: '12', cy: '12', r: '10' } }, { tag: 'rect', attrs: { x: '9', y: '9', width: '6', height: '6' } }, ], }); // Helper to create an icon button function createIconBtn(id, svgEl) { const btn = document.createElement('span'); btn.id = id; btn.className = 'icon-btn'; btn.appendChild(svgEl); return btn; } // Create the 3 icon buttons const playButton = createIconBtn('donobu-play', playIcon); const pauseButton = createIconBtn('donobu-pause', pauseIcon); const endButton = createIconBtn('donobu-end', endIcon); iconContainer.appendChild(playButton); iconContainer.appendChild(pauseButton); iconContainer.appendChild(endButton); // Title text const titleText = document.createElement('span'); titleText.id = 'donobu-control-panel-title-text'; titleText.textContent = 'Loading...'; // Assemble navbar titleContainer.appendChild(iconContainer); titleContainer.appendChild(titleText); navbar.appendChild(titleContainer); // Expandable content const expandableContent = document.createElement('div'); expandableContent.id = 'donobu-control-panel-expandable-content'; const instructionsInput = document.createElement('input'); instructionsInput.type = 'text'; instructionsInput.id = 'donobu-instructions-input'; instructionsInput.placeholder = 'Additional instructions...'; const displayArea = document.createElement('div'); displayArea.id = 'donobu-control-panel-display-area'; displayArea.textContent = 'Popup: Monitoring...'; expandableContent.appendChild(instructionsInput); expandableContent.appendChild(displayArea); // Attach everything panelContent.appendChild(navbar); panelContent.appendChild(expandableContent); shadow.appendChild(panelContent); // Track panel flow state for the UI hostEl.donobuFlowState = hostEl.donobuFlowState || 'LOADING'; // We also track "transitioning" states internally let isTransitioningToPause = false; let isTransitioningToEnd = false; function setButtonState(btn, active) { btn.style.opacity = active ? '1' : '0.5'; btn.style.cursor = active ? 'pointer' : 'default'; } function updateButtonStates() { if (!document.contains(hostEl)) { // If forcibly removed, re-append (optional). document.body.appendChild(hostEl); } const currentState = hostEl.donobuFlowState; if (isTransitioningToEnd) { titleText.textContent = 'Ending...'; setButtonState(playButton, false); setButtonState(pauseButton, false); setButtonState(endButton, false); return; } switch (currentState) { case 'PAUSED': titleText.textContent = 'Paused'; setButtonState(playButton, true); setButtonState(pauseButton, false); setButtonState(endButton, true); isTransitioningToPause = false; break; case 'QUERYING_LLM_FOR_NEXT_ACTION': titleText.textContent = 'Thinking...'; setButtonState(playButton, false); setButtonState(pauseButton, true); setButtonState(endButton, true); break; case 'WAITING_ON_USER_FOR_NEXT_ACTION': titleText.textContent = 'Waiting on user...'; setButtonState(playButton, false); setButtonState(pauseButton, false); setButtonState(endButton, true); break; case 'RUNNING_ACTION': // Do not set the titleText.textContent for this case since the // backend overwrites this value when running an action. setButtonState(playButton, false); setButtonState(pauseButton, true); setButtonState(endButton, true); break; case 'LOADING': titleText.textContent = 'Loading...'; setButtonState(playButton, false); setButtonState(pauseButton, true); setButtonState(endButton, true); break; case 'SUCCESS': titleText.textContent = 'Success!'; setButtonState(playButton, false); setButtonState(pauseButton, false); setButtonState(endButton, false); break; default: // "Running" or any other custom state if (isTransitioningToPause) { titleText.textContent = 'Pausing...'; setButtonState(playButton, false); setButtonState(pauseButton, false); setButtonState(endButton, true); } else { titleText.textContent = currentState; setButtonState(playButton, false); setButtonState(pauseButton, true); setButtonState(endButton, true); } break; } } // Interval for state updates const stateInterval = setInterval(updateButtonStates, 500); // --- Button event listeners --- // PLAY button: signals user wants to resume from paused playButton.addEventListener('click', () => { if (hostEl.donobuFlowState === 'PAUSED' && !isTransitioningToEnd) { // UI states hostEl.donobuFlowState = 'RUNNING'; isTransitioningToPause = false; // Expose next state so external script can pick it up window.donobuNextState = 'RESUMING'; updateButtonStates(); } }); // PAUSE button: signals user wants to pause pauseButton.addEventListener('click', () => { if (hostEl.donobuFlowState !== 'PAUSED' && !isTransitioningToEnd) { hostEl.donobuFlowState = 'PAUSED'; isTransitioningToPause = true; // Expose next state window.donobuNextState = 'PAUSED'; updateButtonStates(); } }); // END button: signals user wants to stop / end the flow endButton.addEventListener('click', () => { if (!isTransitioningToEnd) { hostEl.donobuFlowState = 'SUCCESS'; isTransitioningToEnd = true; // Expose next state window.donobuNextState = 'SUCCESS'; updateButtonStates(); } }); // --- Drag-and-drop logic --- let isDragging = false; let offsetX = 0; let offsetY = 0; function onDocumentMouseMove(e) { if (isDragging) { hostEl.style.left = `${e.clientX - offsetX}px`; hostEl.style.top = `${e.clientY - offsetY}px`; hostEl.style.right = 'auto'; } } function onDocumentMouseUp() { isDragging = false; panelContent.style.cursor = 'move'; } // Start dragging if user clicks navbar area shadow.addEventListener('mousedown', (e) => { const target = e.target; const isNavbar = target.classList?.contains('navbar') || target.id === 'donobu-control-panel-title-text' || target.parentElement?.classList?.contains('navbar'); if (isNavbar) { const rect = hostEl.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; isDragging = true; panelContent.style.cursor = 'grabbing'; e.preventDefault(); } }); document.addEventListener('mousemove', onDocumentMouseMove); document.addEventListener('mouseup', onDocumentMouseUp); // Watch for attempts to remove the panel const observer = new MutationObserver(() => { if (!document.contains(hostEl)) { document.body.appendChild(hostEl); } }); observer.observe(document.documentElement, { childList: true, subtree: true, }); // Clean up on page unload window.addEventListener('unload', () => { clearInterval(stateInterval); document.removeEventListener('mousemove', onDocumentMouseMove); document.removeEventListener('mouseup', onDocumentMouseUp); observer.disconnect(); }); return hostEl; } // Ensure body is ready before creation waitForBody(() => { try { createControlPanel(); } catch (err) { console.error('Control panel initialization error:', err); } }); })();