class ScriptBuilder { constructor() { this.mode = 'recording'; this.isRecording = false; this.isPaused = false; this.rawEvents = []; this.groupedEvents = []; this.startTime = 0; this.lastEventTime = 0; this.toolbar = null; this.keyBuffer = []; this.keyBufferTimeout = null; this.scrollAccumulator = { direction: null, amount: 0, startTime: 0 }; this.processedEventIndices = new Set(); this.outputFormat = 'js'; // 'js' or 'c4a' this.timelineModal = null; this.recordingIndicator = null; } start() { // Don't start recording immediately, just show the toolbar this.createToolbar(); this.showStartScreen(); } showStartScreen() { const toolbarContent = this.toolbar.querySelector('.c4ai-toolbar-content'); toolbarContent.innerHTML = `
Script Builder Ready
Click "Start Recording" to begin capturing your actions. (Alpha - May contain bugs)
`; // Add event listeners document.getElementById('c4ai-start-recording').addEventListener('click', () => this.startRecording()); document.getElementById('c4ai-saved-flows').addEventListener('click', () => this.showSavedFlows()); } startRecording() { this.isRecording = true; this.startTime = Date.now(); this.lastEventTime = this.startTime; this.createRecordingIndicator(); this.injectEventCapture(); // Update toolbar to show recording controls const toolbarContent = this.toolbar.querySelector('.c4ai-toolbar-content'); toolbarContent.innerHTML = `
Actions: 0
Format:
Recording your actions... Click, type, and scroll to build your script.
`; // Re-add event listeners document.getElementById('c4ai-pause-recording').addEventListener('click', () => this.togglePause()); document.getElementById('c4ai-stop-generate').addEventListener('click', () => this.stopAndGenerate()); document.getElementById('c4ai-add-wait').addEventListener('click', () => this.showWaitDialog()); document.getElementById('c4ai-saved-flows').addEventListener('click', () => this.showSavedFlows()); document.getElementById('c4ai-output-format').addEventListener('change', (e) => { this.outputFormat = e.target.value; }); this.updateToolbar(); } stop() { this.isRecording = false; this.removeEventCapture(); this.toolbar?.remove(); this.recordingIndicator?.remove(); this.timelineModal?.remove(); this.rawEvents = []; this.groupedEvents = []; this.processedEventIndices.clear(); } createRecordingIndicator() { this.recordingIndicator = document.createElement('div'); this.recordingIndicator.className = 'c4ai-recording-indicator'; this.recordingIndicator.innerHTML = `
Recording `; document.body.appendChild(this.recordingIndicator); } createToolbar() { this.toolbar = document.createElement('div'); this.toolbar.className = 'c4ai-script-toolbar'; this.toolbar.innerHTML = `
Crawl4AI
Crawl4AI Script Builder (ALPHA)
`; document.body.appendChild(this.toolbar); // Add close button listener document.getElementById('c4ai-script-close').addEventListener('click', () => this.stop()); // Make toolbar draggable window.C4AI_Utils.makeDraggable(this.toolbar); } showStartScreen() { const content = document.getElementById('c4ai-toolbar-content'); content.innerHTML = `

Welcome to Script Builder

Record your actions to create automation scripts. Alpha version - may contain bugs

`; // Add event listeners document.getElementById('c4ai-start-recording').addEventListener('click', () => this.startRecording()); document.getElementById('c4ai-saved-flows').addEventListener('click', () => this.showSavedFlows()); } showRecordingUI() { const content = document.getElementById('c4ai-toolbar-content'); content.innerHTML = `
Actions: 0
Format:
Recording your actions... Click, type, and scroll to build your script.
`; // Re-add event listeners document.getElementById('c4ai-pause-recording').addEventListener('click', () => this.togglePause()); document.getElementById('c4ai-stop-generate').addEventListener('click', () => this.stopAndGenerate()); document.getElementById('c4ai-add-wait').addEventListener('click', () => this.showWaitDialog()); document.getElementById('c4ai-saved-flows').addEventListener('click', () => this.showSavedFlows()); document.getElementById('c4ai-output-format').addEventListener('change', (e) => { this.outputFormat = e.target.value; }); } togglePause() { this.isPaused = !this.isPaused; const pauseBtn = document.getElementById('c4ai-pause-recording'); const recordingIndicator = document.querySelector('.c4ai-recording-indicator'); if (this.isPaused) { pauseBtn.innerHTML = 'ā–¶ Resume'; pauseBtn.classList.add('c4ai-paused'); recordingIndicator.classList.add('c4ai-paused'); document.getElementById('c4ai-script-hint').textContent = 'Recording paused. Click Resume to continue.'; } else { pauseBtn.innerHTML = 'āø Pause'; pauseBtn.classList.remove('c4ai-paused'); recordingIndicator.classList.remove('c4ai-paused'); document.getElementById('c4ai-script-hint').textContent = 'Recording your actions... Click, type, and scroll to build your script.'; } } showWaitDialog() { const dialog = document.createElement('div'); dialog.className = 'c4ai-wait-dialog'; dialog.innerHTML = `

Add Wait Command

Click on an element to wait for

`; document.body.appendChild(dialog); // Handle radio button changes dialog.querySelectorAll('input[name="wait-type"]').forEach(radio => { radio.addEventListener('change', (e) => { const timeInput = document.getElementById('c4ai-wait-time-input'); const selectorInput = document.getElementById('c4ai-wait-selector-input'); if (e.target.value === 'time') { timeInput.classList.remove('c4ai-hidden'); selectorInput.classList.add('c4ai-hidden'); } else { timeInput.classList.add('c4ai-hidden'); selectorInput.classList.remove('c4ai-hidden'); this.waitForElementSelection(dialog); } }); }); // Handle buttons document.getElementById('c4ai-wait-cancel').addEventListener('click', () => { dialog.remove(); }); document.getElementById('c4ai-wait-add').addEventListener('click', () => { const waitType = dialog.querySelector('input[name="wait-type"]:checked').value; if (waitType === 'time') { const seconds = parseFloat(document.getElementById('c4ai-wait-seconds').value) || 2; this.addWaitCommand('time', seconds); dialog.remove(); } }); } waitForElementSelection(dialog) { this.isPaused = true; // Pause recording during element selection const highlightBox = document.createElement('div'); highlightBox.className = 'c4ai-highlight-box c4ai-wait-mode'; document.body.appendChild(highlightBox); const handleMouseMove = (e) => { const element = document.elementFromPoint(e.clientX, e.clientY); if (element && !this.isOurElement(element)) { this.highlightElement(element, highlightBox); } }; const handleClick = (e) => { const element = e.target; if (this.isOurElement(element)) return; e.preventDefault(); e.stopPropagation(); const selector = this.getElementSelector(element); this.addWaitCommand('selector', selector); // Cleanup document.removeEventListener('mousemove', handleMouseMove, true); document.removeEventListener('click', handleClick, true); highlightBox.remove(); dialog.remove(); this.isPaused = false; }; document.addEventListener('mousemove', handleMouseMove, true); document.addEventListener('click', handleClick, true); } addWaitCommand(type, value) { const waitEvent = { type: 'WAIT', waitType: type, value: value, time: (Date.now() - this.startTime) / 1000 }; this.groupedEvents.push(waitEvent); this.updateToolbar(); } highlightElement(element, highlightBox) { if (!highlightBox) { highlightBox = document.querySelector('.c4ai-highlight-box'); } const rect = element.getBoundingClientRect(); highlightBox.style.display = 'block'; highlightBox.style.top = `${rect.top + window.scrollY}px`; highlightBox.style.left = `${rect.left + window.scrollX}px`; highlightBox.style.width = `${rect.width}px`; highlightBox.style.height = `${rect.height}px`; } isOurElement(element) { return window.C4AI_Utils.isOurElement(element); } getElementSelector(element) { return window.C4AI_Utils.getElementSelector(element); } injectEventCapture() { // Use content script context instead of injecting inline script if (window.__c4aiScriptRecordingActive) return; window.__c4aiScriptRecordingActive = true; const captureEvent = (type, event) => { // Skip events from our own UI if (event.target.classList && (event.target.classList.contains('c4ai-script-toolbar') || event.target.closest('.c4ai-script-toolbar') || event.target.classList.contains('c4ai-wait-dialog') || event.target.closest('.c4ai-wait-dialog') || event.target.classList.contains('c4ai-timeline-modal') || event.target.closest('.c4ai-timeline-modal'))) { return; } const data = { type: type, timestamp: Date.now(), targetTag: event.target.tagName, targetId: event.target.id, targetClass: event.target.className, targetSelector: this.getElementSelector(event.target), targetType: event.target.type }; switch(type) { case 'click': case 'dblclick': case 'contextmenu': data.x = event.clientX; data.y = event.clientY; break; case 'keydown': case 'keyup': data.key = event.key; data.code = event.code; data.ctrlKey = event.ctrlKey; data.shiftKey = event.shiftKey; data.altKey = event.altKey; data.metaKey = event.metaKey; // Add meta key support break; case 'input': case 'change': // Check if it's a contenteditable element if (event.target.contentEditable === 'true' || event.target.getAttribute('contenteditable') === 'true') { data.value = event.target.textContent || event.target.innerText; data.isContentEditable = true; } else { data.value = event.target.value; } data.inputType = event.inputType; if (event.target.type === 'checkbox' || event.target.type === 'radio') { data.checked = event.target.checked; } if (event.target.tagName === 'SELECT') { data.selectedText = event.target.options[event.target.selectedIndex]?.text || ''; } break; case 'scroll': // Capture which element was scrolled if (event.target === window || event.target === document || event.target === document.body) { data.isWindowScroll = true; data.scrollTop = window.scrollY; data.scrollLeft = window.scrollX; } else { data.isWindowScroll = false; data.scrollTop = event.target.scrollTop; data.scrollLeft = event.target.scrollLeft; data.scrollHeight = event.target.scrollHeight; data.scrollWidth = event.target.scrollWidth; } break; case 'wheel': data.deltaY = event.deltaY || 0; data.deltaX = event.deltaX || 0; // Check if the target element is scrollable const isScrollable = (el) => { const hasScrollableContent = el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth; const overflowY = window.getComputedStyle(el).overflowY; const overflowX = window.getComputedStyle(el).overflowX; const canScroll = (overflowY !== 'visible' && overflowY !== 'hidden') || (overflowX !== 'visible' && overflowX !== 'hidden'); return hasScrollableContent && canScroll; }; // Find the actual scrollable element let scrollTarget = event.target; while (scrollTarget && scrollTarget !== document.body) { if (isScrollable(scrollTarget)) { break; } scrollTarget = scrollTarget.parentElement; } if (scrollTarget && scrollTarget !== document.body && isScrollable(scrollTarget)) { data.isWindowScroll = false; data.scrollTop = scrollTarget.scrollTop; data.scrollLeft = scrollTarget.scrollLeft; data.targetSelector = this.getElementSelector(scrollTarget); } else { data.isWindowScroll = true; data.scrollTop = window.scrollY; data.scrollLeft = window.scrollX; } break; } // Handle the event directly instead of using postMessage if (this.isRecording && !this.isPaused) { this.handleRecordedEvent(data); } }; // Store bound event handlers for later removal this.eventHandlers = { click: (e) => captureEvent('click', e), dblclick: (e) => captureEvent('dblclick', e), contextmenu: (e) => captureEvent('contextmenu', e), keydown: (e) => captureEvent('keydown', e), input: (e) => captureEvent('input', e), change: (e) => captureEvent('change', e), scroll: (e) => captureEvent('scroll', e) }; // Add event listeners Object.entries(this.eventHandlers).forEach(([eventType, handler]) => { document.addEventListener(eventType, handler, true); }); // Cleanup on page unload window.addEventListener('beforeunload', () => { this.removeEventCapture(); }); } removeEventCapture() { window.__c4aiScriptRecordingActive = false; // Remove event listeners if they exist if (this.eventHandlers) { Object.entries(this.eventHandlers).forEach(([eventType, handler]) => { document.removeEventListener(eventType, handler, true); }); this.eventHandlers = null; } } handleRecordedEvent(event) { // Skip events from our own UI if (event.targetClass && typeof event.targetClass === 'string' && event.targetClass.includes('c4ai-')) return; // Add time since start event.timeSinceStart = (event.timestamp - this.startTime) / 1000; // Store raw event this.rawEvents.push(event); // Process event based on type if (event.type === 'keydown') { // Handle keyboard shortcuts (with modifier keys) if (event.metaKey || event.ctrlKey || event.altKey) { // Flush any pending keystrokes if (this.keyBuffer.length > 0) { this.flushKeyBuffer(); } // Create keyboard shortcut command const command = this.eventToCommand(event); if (command) { this.groupedEvents.push(command); this.updateToolbar(); } } else if (this.shouldGroupKeystrokes(event)) { // Regular typing this.keyBuffer.push(event); if (this.keyBufferTimeout) { clearTimeout(this.keyBufferTimeout); } this.keyBufferTimeout = setTimeout(() => { this.flushKeyBuffer(); this.updateToolbar(); }, 500); } else if (event.key === 'Delete' || event.key === 'Backspace') { // Handle Delete and Backspace as individual commands when not part of typing if (this.keyBuffer.length > 0) { this.flushKeyBuffer(); } const command = this.eventToCommand(event); if (command) { this.groupedEvents.push(command); this.updateToolbar(); } } } else if (event.type === 'scroll' || event.type === 'wheel') { this.handleScrollEvent(event); } else if (event.type === 'click' || event.type === 'dblclick' || event.type === 'contextmenu') { // Flush any pending keystrokes before click if (this.keyBuffer.length > 0) { this.flushKeyBuffer(); } const command = this.eventToCommand(event); if (command) { this.groupedEvents.push(command); this.updateToolbar(); } } else if (event.type === 'change') { // Handle select, checkbox, radio changes const tagName = event.targetTag?.toLowerCase(); if (tagName === 'select' || (tagName === 'input' && (event.targetType === 'checkbox' || event.targetType === 'radio'))) { const command = this.eventToCommand(event); if (command) { this.groupedEvents.push(command); this.updateToolbar(); } } } } shouldGroupKeystrokes(event) { // Don't group if modifier keys are pressed (except shift for capitals) if (event.metaKey || event.ctrlKey || event.altKey) { return false; } return event.key && ( event.key.length === 1 || event.key === ' ' || event.key === 'Enter' || event.key === 'Tab' || event.key === 'Backspace' || event.key === 'Delete' ); } flushKeyBuffer() { if (this.keyBuffer.length === 0) return; const text = this.keyBuffer.map(e => { switch(e.key) { case ' ': return ' '; case 'Enter': return '\\n'; case 'Tab': return '\\t'; case 'Backspace': return ''; case 'Delete': return ''; default: return e.key; } }).join(''); if (text.length === 0) { this.keyBuffer = []; return; } const firstEvent = this.keyBuffer[0]; const lastEvent = this.keyBuffer[this.keyBuffer.length - 1]; // Check if this is a SET command pattern const firstKeystrokeIndex = this.rawEvents.indexOf(firstEvent); let commandType = 'TYPE'; if (firstKeystrokeIndex > 0) { const prevEvent = this.rawEvents[firstKeystrokeIndex - 1]; // Check for click before typing if (prevEvent && prevEvent.type === 'click' && prevEvent.targetSelector === firstEvent.targetSelector) { commandType = 'SET'; } // Check for Cmd+A or Ctrl+A (select all) before typing if (prevEvent && prevEvent.type === 'keydown' && prevEvent.key === 'a' && (prevEvent.metaKey || prevEvent.ctrlKey) && prevEvent.targetSelector === firstEvent.targetSelector) { commandType = 'SET'; } } // Also check within the last few events for select-all pattern if (commandType === 'TYPE' && firstKeystrokeIndex > 1) { // Look back up to 3 events for select-all for (let i = Math.max(0, firstKeystrokeIndex - 3); i < firstKeystrokeIndex; i++) { const event = this.rawEvents[i]; if (event && event.type === 'keydown' && event.key === 'a' && (event.metaKey || event.ctrlKey) && event.targetSelector === firstEvent.targetSelector) { commandType = 'SET'; break; } } } this.groupedEvents.push({ type: commandType, selector: firstEvent.targetSelector, value: text, time: firstEvent.timeSinceStart }); this.keyBuffer = []; } handleScrollEvent(event) { const direction = event.deltaY > 0 ? 'DOWN' : 'UP'; const amount = Math.abs(event.deltaY); // Remove previous scroll in same direction within 0.5s for the same element this.groupedEvents = this.groupedEvents.filter(e => !(e.type === 'SCROLL' && e.direction === direction && e.selector === (event.targetSelector || 'window') && parseFloat(e.time) > parseFloat(event.timeSinceStart) - 0.5) ); this.groupedEvents.push({ type: 'SCROLL', direction: direction, amount: amount, selector: event.targetSelector || 'window', isWindowScroll: event.isWindowScroll !== false, time: event.timeSinceStart }); this.updateToolbar(); } eventToCommand(event) { switch (event.type) { case 'click': return { type: 'CLICK', selector: event.targetSelector, time: event.timeSinceStart }; case 'dblclick': return { type: 'DOUBLE_CLICK', selector: event.targetSelector, time: event.timeSinceStart }; case 'contextmenu': return { type: 'RIGHT_CLICK', selector: event.targetSelector, time: event.timeSinceStart }; case 'keydown': // Handle keyboard shortcuts if (event.metaKey || event.ctrlKey || event.altKey) { return { type: 'KEYBOARD_SHORTCUT', key: event.key, code: event.code, metaKey: event.metaKey, ctrlKey: event.ctrlKey, altKey: event.altKey, shiftKey: event.shiftKey, time: event.timeSinceStart }; } else if (event.key === 'Delete' || event.key === 'Backspace') { // Handle Delete/Backspace as special keys return { type: 'KEY_PRESS', key: event.key, time: event.timeSinceStart }; } break; case 'change': if (event.targetTag === 'SELECT') { return { type: 'SET', selector: event.targetSelector, value: event.value, time: event.timeSinceStart }; } else if (event.targetType === 'checkbox' || event.targetType === 'radio') { return { type: 'CLICK', selector: event.targetSelector, time: event.timeSinceStart }; } break; } return null; } updateToolbar() { const actionCount = document.getElementById('c4ai-action-count'); if (actionCount) { actionCount.textContent = this.groupedEvents.length; } } stopAndGenerate() { // Flush any pending events if (this.keyBufferTimeout) { clearTimeout(this.keyBufferTimeout); this.flushKeyBuffer(); } this.isRecording = false; this.recordingIndicator?.remove(); this.showRecordingSummary(); } showRecordingSummary() { // Update toolbar to show summary const toolbarContent = this.toolbar.querySelector('.c4ai-toolbar-content'); toolbarContent.innerHTML = `
Recording Complete ${this.groupedEvents.length} actions
Recording stopped. You can replay, save, or generate code.
`; // Add event listeners document.getElementById('c4ai-replay')?.addEventListener('click', () => this.replayRecording()); document.getElementById('c4ai-save-flow')?.addEventListener('click', () => this.saveFlow()); document.getElementById('c4ai-show-timeline')?.addEventListener('click', () => this.showTimeline()); document.getElementById('c4ai-record-again')?.addEventListener('click', () => this.recordAgain()); } showTimeline() { this.timelineModal = document.createElement('div'); this.timelineModal.className = 'c4ai-timeline-modal'; this.timelineModal.innerHTML = `

Review Your Actions

${this.renderTimelineEvents()}
`; document.body.appendChild(this.timelineModal); // Event listeners document.getElementById('c4ai-close-timeline').addEventListener('click', () => { this.timelineModal.remove(); // Don't stop the toolbar, just close the modal }); document.getElementById('c4ai-select-all').addEventListener('click', () => { document.querySelectorAll('.c4ai-event-checkbox').forEach(cb => cb.checked = true); }); document.getElementById('c4ai-clear-all').addEventListener('click', () => { document.querySelectorAll('.c4ai-event-checkbox').forEach(cb => cb.checked = false); }); document.getElementById('c4ai-download-script').addEventListener('click', () => { this.generateAndDownload(); }); } renderTimelineEvents() { return this.groupedEvents.map((event, index) => { const detail = this.getEventDetail(event); return `
`; }).join(''); } getEventDetail(event) { switch (event.type) { case 'CLICK': case 'DOUBLE_CLICK': case 'RIGHT_CLICK': return event.selector; case 'TYPE': case 'SET': return `${event.selector} = "${event.value.substring(0, 30)}${event.value.length > 30 ? '...' : ''}"`; case 'SCROLL': if (event.isWindowScroll || event.selector === 'window') { return `${event.direction} ${event.amount}px`; } else { return `${event.selector} ${event.direction} ${event.amount}px`; } case 'WAIT': return event.waitType === 'time' ? `${event.value}s` : event.value; case 'KEYBOARD_SHORTCUT': const keys = []; if (event.metaKey) keys.push('Cmd'); if (event.ctrlKey) keys.push('Ctrl'); if (event.altKey) keys.push('Alt'); if (event.shiftKey) keys.push('Shift'); keys.push(event.key.toUpperCase()); return keys.join('+'); case 'KEY_PRESS': return event.key; default: return ''; } } generateAndDownload() { const format = document.getElementById('c4ai-final-format').value; const selectedEvents = []; document.querySelectorAll('.c4ai-event-checkbox').forEach((cb, index) => { if (cb.checked && this.groupedEvents[index]) { selectedEvents.push(this.groupedEvents[index]); } }); if (selectedEvents.length === 0) { alert('Please select at least one action'); return; } const code = this.generateCode(selectedEvents, format); chrome.runtime.sendMessage({ action: 'downloadScript', code: code, format: format, filename: `crawl4ai_script_${Date.now()}.py` }, (response) => { if (response && response.success) { this.timelineModal.remove(); // Don't stop the toolbar after download // Show success message in toolbar document.getElementById('c4ai-script-hint').textContent = 'āœ… Script downloaded successfully!'; } }); } generateCode(events, format) { let scriptCode; if (format === 'js') { scriptCode = this.generateJavaScript(events); } else { scriptCode = this.generateC4AScript(events); } return this.generatePythonTemplate(scriptCode, format); } generateJavaScript(events) { const commands = events.map(event => { switch (event.type) { case 'CLICK': return `document.querySelector('${event.selector}').click();`; case 'DOUBLE_CLICK': return `const el = document.querySelector('${event.selector}');\nconst evt = new MouseEvent('dblclick', {bubbles: true});\nel.dispatchEvent(evt);`; case 'RIGHT_CLICK': return `const el = document.querySelector('${event.selector}');\nconst evt = new MouseEvent('contextmenu', {bubbles: true});\nel.dispatchEvent(evt);`; case 'TYPE': return this.generateTypeCode(event, 'append'); case 'SET': return this.generateTypeCode(event, 'set'); case 'SCROLL': if (event.isWindowScroll || event.selector === 'window') { return `window.scrollBy(0, ${event.direction === 'DOWN' ? event.amount : -event.amount});`; } else { const scrollAmount = event.direction === 'DOWN' ? event.amount : -event.amount; return `// Scroll element const scrollEl = document.querySelector('${event.selector}'); if (scrollEl) { scrollEl.scrollTop += ${scrollAmount}; }`; } case 'WAIT': if (event.waitType === 'time') { return `await new Promise(resolve => setTimeout(resolve, ${event.value * 1000}));`; } else { return `// Wait for element: ${event.value}\nawait new Promise((resolve) => {\n const checkElement = setInterval(() => {\n if (document.querySelector('${event.value}')) {\n clearInterval(checkElement);\n resolve();\n }\n }, 100);\n setTimeout(() => { clearInterval(checkElement); resolve(); }, 5000);\n});`; } case 'KEYBOARD_SHORTCUT': const modifiers = []; if (event.metaKey) modifiers.push('metaKey: true'); if (event.ctrlKey) modifiers.push('ctrlKey: true'); if (event.altKey) modifiers.push('altKey: true'); if (event.shiftKey) modifiers.push('shiftKey: true'); return `// Keyboard shortcut: ${this.getEventDetail(event)}\ndocument.dispatchEvent(new KeyboardEvent('keydown', {key: '${event.key}', ${modifiers.join(', ')}}));`; case 'KEY_PRESS': return `// Press ${event.key} key\ndocument.dispatchEvent(new KeyboardEvent('keydown', {key: '${event.key}'}));`; default: return `// Unknown action: ${event.type}`; } }); return commands.join('\\n'); } generateTypeCode(event, mode) { const value = event.value.replace(/'/g, "\\'").replace(/\n/g, '\\n'); return `// ${mode === 'set' ? 'Set' : 'Type'} text const el = document.querySelector('${event.selector}'); el.focus(); el.click(); // Check if contenteditable if (el.contentEditable === 'true' || el.getAttribute('contenteditable') === 'true') { // Handle contenteditable element ${mode === 'set' ? 'el.textContent = "";' : ''} const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(el); range.collapse(${mode === 'set' ? 'true' : 'false'}); selection.removeAllRanges(); selection.addRange(range); // Type each character '${value}'.split('').forEach(char => { document.execCommand('insertText', false, char); el.dispatchEvent(new InputEvent('input', {bubbles: true, inputType: 'insertText', data: char})); }); } else if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') { // Handle input/textarea ${mode === 'set' ? `el.value = '${value}';` : `el.value += '${value}';`} el.dispatchEvent(new Event('input', {bubbles: true})); el.dispatchEvent(new Event('change', {bubbles: true})); } el.dispatchEvent(new Event('blur', {bubbles: true}));`; } generateC4AScript(events) { const commands = events.map(event => { switch (event.type) { case 'CLICK': return `CLICK \`${event.selector}\``; case 'DOUBLE_CLICK': return `DOUBLE_CLICK \`${event.selector}\``; case 'RIGHT_CLICK': return `RIGHT_CLICK \`${event.selector}\``; case 'TYPE': return `TYPE "${event.value}"`; case 'SET': return `SET \`${event.selector}\` "${event.value}"`; case 'SCROLL': if (event.isWindowScroll || event.selector === 'window') { return `SCROLL ${event.direction} ${event.amount}`; } else { return `SCROLL \`${event.selector}\` ${event.direction} ${event.amount}`; } case 'WAIT': if (event.waitType === 'time') { return `WAIT ${event.value}`; } else { return `WAIT \`${event.value}\` 5`; } case 'KEYBOARD_SHORTCUT': return `# Keyboard shortcut: ${this.getEventDetail(event)}\nKEY "${this.getEventDetail(event)}"`; case 'KEY_PRESS': return `KEY "${event.key}"`; default: return `# Unknown: ${event.type}`; } }); return commands.join('\\n'); } async replayRecording() { if (this.groupedEvents.length === 0) { alert('No actions to replay'); return; } // Create debugger window instead of simple overlay this.createDebuggerWindow(); } createReplayOverlay() { // Create visual indicator for current action this.replayIndicator = document.createElement('div'); this.replayIndicator.className = 'c4ai-replay-indicator'; this.replayIndicator.innerHTML = `
ā–¶ļø Replaying Actions
Preparing...
`; document.body.appendChild(this.replayIndicator); // Create highlight overlay for showing where actions happen this.replayHighlight = document.createElement('div'); this.replayHighlight.className = 'c4ai-replay-highlight'; document.body.appendChild(this.replayHighlight); // Add stop button listener document.getElementById('c4ai-replay-stop').addEventListener('click', () => { this.stopReplay(); }); } async executeReplaySequence() { const events = this.groupedEvents; const totalEvents = events.length; for (let i = 0; i < totalEvents; i++) { if (!this.isReplaying) break; this.replayIndex = i; const event = events[i]; const progress = ((i + 1) / totalEvents) * 100; // Update progress document.getElementById('c4ai-replay-progress').style.width = `${progress}%`; document.getElementById('c4ai-replay-status').textContent = `Action ${i + 1}/${totalEvents}: ${this.getReplayActionDescription(event)}`; // Execute the action with visual feedback await this.executeReplayAction(event); // Wait between actions (except for the last one) if (i < totalEvents - 1) { const nextEvent = events[i + 1]; const waitTime = this.calculateWaitTime(event, nextEvent); await this.wait(waitTime); } } // Replay complete this.stopReplay(true); } async executeReplayAction(event) { switch (event.type) { case 'CLICK': case 'DOUBLE_CLICK': case 'RIGHT_CLICK': await this.replayClickAction(event); break; case 'TYPE': case 'SET': await this.replayTypeAction(event); break; case 'SCROLL': await this.replayScrollAction(event); break; case 'WAIT': await this.replayWaitAction(event); break; case 'KEYBOARD_SHORTCUT': await this.replayKeyboardShortcut(event); break; case 'KEY_PRESS': await this.replayKeyPress(event); break; } } async replayClickAction(event) { const element = document.querySelector(event.selector); if (element) { // Highlight the element this.highlightReplayElement(element); // Show click animation this.showClickAnimation(element); // Wait a bit for visual effect await this.wait(300); } else { console.warn(`Element not found for selector: ${event.selector}`); } } async replayTypeAction(event) { const element = document.querySelector(event.selector); if (element) { // Highlight the input field this.highlightReplayElement(element); // Show typing animation const originalValue = element.value || ''; if (event.type === 'SET') { // Clear and set new value with animation element.value = ''; await this.wait(200); // Type character by character for (let i = 0; i < event.value.length; i++) { element.value = event.value.substring(0, i + 1); await this.wait(50); // Typing speed } } else { // Append text with typing animation const startLength = originalValue.length; for (let i = 0; i < event.value.length; i++) { element.value = originalValue + event.value.substring(0, i + 1); await this.wait(50); } } } } async replayScrollAction(event) { const scrollAmount = event.direction === 'DOWN' ? event.amount : -event.amount; // Animate scroll const startY = window.scrollY; const targetY = startY + scrollAmount; const duration = 500; // ms const startTime = Date.now(); const animateScroll = () => { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / duration, 1); const easeProgress = this.easeInOut(progress); window.scrollTo(0, startY + (targetY - startY) * easeProgress); if (progress < 1 && this.isReplaying) { requestAnimationFrame(animateScroll); } }; animateScroll(); await this.wait(duration); } async replayWaitAction(event) { if (event.waitType === 'time') { document.getElementById('c4ai-replay-status').textContent = `Waiting ${event.value} seconds...`; await this.wait(event.value * 1000); } else { // Wait for element document.getElementById('c4ai-replay-status').textContent = `Waiting for element: ${event.value}`; await this.waitForElement(event.value, 5000); // 5 second timeout } } async replayKeyboardShortcut(event) { const keys = []; if (event.metaKey) keys.push('Cmd'); if (event.ctrlKey) keys.push('Ctrl'); if (event.altKey) keys.push('Alt'); if (event.shiftKey) keys.push('Shift'); keys.push(event.key.toUpperCase()); // Show keyboard shortcut overlay this.showKeyboardOverlay(keys.join('+')); await this.wait(1000); } async replayKeyPress(event) { this.showKeyboardOverlay(event.key); await this.wait(500); } highlightReplayElement(element) { const rect = element.getBoundingClientRect(); this.replayHighlight.style.display = 'block'; this.replayHighlight.style.top = `${rect.top + window.scrollY}px`; this.replayHighlight.style.left = `${rect.left + window.scrollX}px`; this.replayHighlight.style.width = `${rect.width}px`; this.replayHighlight.style.height = `${rect.height}px`; // Fade out after a moment setTimeout(() => { this.replayHighlight.style.display = 'none'; }, 800); } showClickAnimation(element) { const rect = element.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const clickIndicator = document.createElement('div'); clickIndicator.className = 'c4ai-click-indicator'; clickIndicator.style.left = `${centerX}px`; clickIndicator.style.top = `${centerY}px`; document.body.appendChild(clickIndicator); // Remove after animation setTimeout(() => clickIndicator.remove(), 600); } showKeyboardOverlay(keys) { const overlay = document.createElement('div'); overlay.className = 'c4ai-keyboard-overlay'; overlay.textContent = keys; document.body.appendChild(overlay); // Fade in setTimeout(() => overlay.classList.add('visible'), 10); // Fade out and remove setTimeout(() => { overlay.classList.remove('visible'); setTimeout(() => overlay.remove(), 300); }, 700); } getReplayActionDescription(event) { switch (event.type) { case 'CLICK': return `Click on ${event.selector}`; case 'DOUBLE_CLICK': return `Double-click on ${event.selector}`; case 'RIGHT_CLICK': return `Right-click on ${event.selector}`; case 'TYPE': return `Type "${event.value.substring(0, 20)}..."`; case 'SET': return `Set value in ${event.selector}`; case 'SCROLL': if (event.isWindowScroll || event.selector === 'window') { return `Scroll ${event.direction} ${event.amount}px`; } else { return `Scroll ${event.selector} ${event.direction} ${event.amount}px`; } case 'WAIT': return event.waitType === 'time' ? `Wait ${event.value}s` : `Wait for ${event.value}`; case 'KEYBOARD_SHORTCUT': return `Press ${this.getEventDetail(event)}`; case 'KEY_PRESS': return `Press ${event.key}`; default: return event.type; } } calculateWaitTime(currentEvent, nextEvent) { // Use timing from recording if available if (nextEvent.time && currentEvent.time) { const timeDiff = (nextEvent.time - currentEvent.time) * 1000; // Convert to ms // Cap wait time between 100ms and 2000ms for better replay experience return Math.min(Math.max(timeDiff, 100), 2000); } return 500; // Default wait time } async wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async waitForElement(selector, timeout = 5000) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { const element = document.querySelector(selector); if (element) return element; await this.wait(100); } return null; } easeInOut(t) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; } stopReplay(completed = false) { this.isReplaying = false; // Remove replay UI this.replayIndicator?.remove(); this.replayHighlight?.remove(); // Update toolbar if (completed) { document.getElementById('c4ai-script-hint').textContent = 'āœ… Replay completed!'; } else { document.getElementById('c4ai-script-hint').textContent = 'ā¹ļø Replay stopped'; } // Re-enable replay button const replayBtn = document.getElementById('c4ai-replay'); if (replayBtn) { replayBtn.disabled = false; replayBtn.innerHTML = 'ā–¶ Replay'; } } createDebuggerWindow() { // Create debugger modal this.debuggerModal = document.createElement('div'); this.debuggerModal.className = 'c4ai-debugger-modal'; // Set initial position for dragging this.debuggerModal.style.position = 'fixed'; this.debuggerModal.style.top = '50px'; this.debuggerModal.style.right = '20px'; this.debuggerModal.innerHTML = `

Action Debugger (ALPHA)

Status: Ready
${this.renderDebuggerActions()}
`; document.body.appendChild(this.debuggerModal); // Initialize debugger state this.debuggerState = { currentIndex: 0, isRunning: false, isPaused: false, breakpoints: new Set(), editedEvents: [...this.groupedEvents] // Copy for editing }; // Create replay highlight element this.replayHighlight = document.createElement('div'); this.replayHighlight.className = 'c4ai-replay-highlight'; document.body.appendChild(this.replayHighlight); // Add event listeners this.attachDebuggerListeners(); // Make debugger draggable by the header makeDraggableByHeader(this.debuggerModal); } renderDebuggerActions() { const events = this.debuggerState ? this.debuggerState.editedEvents : this.groupedEvents; return events.map((event, index) => { const detail = this.getEventDetail(event); return `
${index + 1} āž¤
${event.type.replace(/_/g, ' ')}
${detail}
`; }).join(''); } attachDebuggerListeners() { // Control buttons document.getElementById('c4ai-debug-run').addEventListener('click', () => this.debugRun()); document.getElementById('c4ai-debug-step').addEventListener('click', () => this.debugStep()); document.getElementById('c4ai-debug-pause').addEventListener('click', () => this.debugPause()); document.getElementById('c4ai-debug-stop').addEventListener('click', () => this.debugStop()); document.getElementById('c4ai-debug-restart').addEventListener('click', () => this.debugRestart()); document.getElementById('c4ai-close-debugger').addEventListener('click', () => this.closeDebugger()); // Breakpoint checkboxes document.querySelectorAll('.c4ai-breakpoint-checkbox').forEach(cb => { cb.addEventListener('change', (e) => { const index = parseInt(e.target.dataset.index); if (e.target.checked) { this.debuggerState.breakpoints.add(index); } else { this.debuggerState.breakpoints.delete(index); } this.updateBreakpointVisual(index, e.target.checked); }); }); // Play buttons document.querySelectorAll('.c4ai-action-play').forEach(btn => { btn.addEventListener('click', (e) => { const index = parseInt(e.target.dataset.index); this.executeActionOnPage(this.debuggerState.editedEvents[index], index); }); }); // Delete buttons document.querySelectorAll('.c4ai-action-delete').forEach(btn => { btn.addEventListener('click', (e) => { const index = parseInt(e.target.dataset.index); this.deleteDebugAction(index); }); }); // Edit buttons document.querySelectorAll('.c4ai-action-edit').forEach(btn => { btn.addEventListener('click', (e) => { const index = parseInt(e.target.dataset.index); this.editDebugAction(index); }); }); // Inline editing document.querySelectorAll('.c4ai-action-detail[contenteditable]').forEach(elem => { elem.addEventListener('blur', (e) => { const index = parseInt(e.target.dataset.index); this.updateActionDetail(index, e.target.textContent); }); }); } async debugRun() { this.debuggerState.isRunning = true; this.debuggerState.isPaused = false; this.updateDebugControls(); this.updateDebugStatus('Running...'); const startIndex = this.debuggerState.currentIndex; const events = this.debuggerState.editedEvents; for (let i = startIndex; i < events.length; i++) { if (!this.debuggerState.isRunning || this.debuggerState.isPaused) break; // Update current action this.setCurrentAction(i); // Execute the action await this.executeDebugAction(events[i], i); // Check for breakpoint on next action if (i < events.length - 1 && this.debuggerState.breakpoints.has(i + 1)) { this.debuggerState.currentIndex = i + 1; this.debugPause(); this.updateDebugStatus(`Stopped at breakpoint ${i + 2}`); break; } this.debuggerState.currentIndex = i + 1; } if (this.debuggerState.currentIndex >= events.length) { this.debugStop(true); } } async debugStep() { if (this.debuggerState.currentIndex >= this.debuggerState.editedEvents.length) { this.updateDebugStatus('End of actions'); return; } this.updateDebugStatus('Stepping...'); this.setCurrentAction(this.debuggerState.currentIndex); await this.executeDebugAction( this.debuggerState.editedEvents[this.debuggerState.currentIndex], this.debuggerState.currentIndex ); this.debuggerState.currentIndex++; if (this.debuggerState.currentIndex >= this.debuggerState.editedEvents.length) { this.updateDebugStatus('Completed'); } else { this.updateDebugStatus('Ready'); } } debugPause() { this.debuggerState.isPaused = true; this.debuggerState.isRunning = false; this.updateDebugControls(); this.updateDebugStatus('Paused'); } debugStop(completed = false) { this.debuggerState.isRunning = false; this.debuggerState.isPaused = false; this.updateDebugControls(); if (completed) { this.updateDebugStatus('Completed'); } else { this.updateDebugStatus('Stopped'); } } debugRestart() { this.debuggerState.currentIndex = 0; this.debuggerState.isRunning = false; this.debuggerState.isPaused = false; this.setCurrentAction(0); this.updateDebugControls(); this.updateDebugStatus('Ready'); } closeDebugger() { this.debuggerModal?.remove(); this.replayHighlight?.remove(); this.debuggerState = null; } setCurrentAction(index) { // Remove previous current marker document.querySelectorAll('.c4ai-debug-action').forEach(elem => { elem.classList.remove('c4ai-current'); }); // Add current marker const currentElem = document.querySelector(`.c4ai-debug-action[data-index="${index}"]`); if (currentElem) { currentElem.classList.add('c4ai-current'); // Better scrolling into view const scrollContainer = document.querySelector('.c4ai-debugger-actions'); if (scrollContainer) { const containerRect = scrollContainer.getBoundingClientRect(); const elemRect = currentElem.getBoundingClientRect(); // Check if element is out of view if (elemRect.top < containerRect.top || elemRect.bottom > containerRect.bottom) { // Scroll to center the element const scrollTop = currentElem.offsetTop - scrollContainer.offsetTop - (scrollContainer.clientHeight / 2) + (currentElem.clientHeight / 2); scrollContainer.scrollTo({ top: scrollTop, behavior: 'smooth' }); } } } this.debuggerState.currentIndex = index; } updateDebugControls() { const runBtn = document.getElementById('c4ai-debug-run'); const stepBtn = document.getElementById('c4ai-debug-step'); const pauseBtn = document.getElementById('c4ai-debug-pause'); if (this.debuggerState.isRunning) { runBtn.disabled = true; stepBtn.disabled = true; pauseBtn.disabled = false; } else { runBtn.disabled = false; stepBtn.disabled = false; pauseBtn.disabled = true; } } updateDebugStatus(status) { document.getElementById('c4ai-debug-status').textContent = status; } updateBreakpointVisual(index, hasBreakpoint) { const actionElem = document.querySelector(`.c4ai-debug-action[data-index="${index}"]`); if (actionElem) { if (hasBreakpoint) { actionElem.classList.add('has-breakpoint'); } else { actionElem.classList.remove('has-breakpoint'); } } } deleteDebugAction(index) { if (confirm('Delete this action?')) { this.debuggerState.editedEvents.splice(index, 1); this.debuggerState.breakpoints = new Set( [...this.debuggerState.breakpoints] .filter(bp => bp !== index) .map(bp => bp > index ? bp - 1 : bp) ); // Re-render actions document.getElementById('c4ai-debugger-actions').innerHTML = this.renderDebuggerActions(); this.attachDebuggerListeners(); // Adjust current index if needed if (this.debuggerState.currentIndex > index) { this.debuggerState.currentIndex--; } this.setCurrentAction(Math.min(this.debuggerState.currentIndex, this.debuggerState.editedEvents.length - 1)); } } editDebugAction(index) { const event = this.debuggerState.editedEvents[index]; const dialog = document.createElement('div'); dialog.className = 'c4ai-edit-dialog'; dialog.innerHTML = `

Edit Action

`; document.body.appendChild(dialog); document.getElementById('c4ai-edit-save').addEventListener('click', () => { const newType = document.getElementById('c4ai-edit-type').value; const newValue = document.getElementById('c4ai-edit-value').value; // Update event based on type this.debuggerState.editedEvents[index] = { ...event, type: newType, selector: ['CLICK', 'TYPE', 'SET'].includes(newType) ? newValue : event.selector, value: ['TYPE', 'SET', 'WAIT'].includes(newType) ? newValue : event.value }; // Re-render document.getElementById('c4ai-debugger-actions').innerHTML = this.renderDebuggerActions(); this.attachDebuggerListeners(); dialog.remove(); }); document.getElementById('c4ai-edit-cancel').addEventListener('click', () => { dialog.remove(); }); } async executeDebugAction(event, index) { this.updateDebugStatus(`Executing: ${this.getEventDetail(event)}`); // Execute the action on the page await this.executeActionOnPage(event, index); // Add small delay between actions for visibility await this.wait(300); } async executeActionOnPage(event, index) { try { // Set current action for visual feedback this.setCurrentAction(index); switch (event.type) { case 'CLICK': case 'DOUBLE_CLICK': case 'RIGHT_CLICK': await this.executeClick(event); break; case 'TYPE': case 'SET': await this.executeType(event); break; case 'SCROLL': await this.executeScroll(event); break; case 'WAIT': await this.executeWait(event); break; case 'KEYBOARD_SHORTCUT': await this.executeKeyboardShortcut(event); break; case 'KEY_PRESS': await this.executeKeyPress(event); break; } this.updateDebugStatus(`Executed: ${this.getEventDetail(event)}`); } catch (error) { console.error('Error executing action:', error); this.updateDebugStatus(`Error: ${error.message}`); } } async executeClick(event) { const element = document.querySelector(event.selector); if (!element) { throw new Error(`Element not found: ${event.selector}`); } // Highlight element briefly this.highlightReplayElement(element); // Create and dispatch the appropriate mouse event const eventType = { 'CLICK': 'click', 'DOUBLE_CLICK': 'dblclick', 'RIGHT_CLICK': 'contextmenu' }[event.type]; const mouseEvent = new MouseEvent(eventType, { view: window, bubbles: true, cancelable: true, buttons: event.type === 'RIGHT_CLICK' ? 2 : 1 }); element.dispatchEvent(mouseEvent); // Also try clicking for better compatibility if (event.type === 'CLICK' && typeof element.click === 'function') { element.click(); } } async executeType(event) { const element = document.querySelector(event.selector); if (!element) { throw new Error(`Element not found: ${event.selector}`); } // Highlight element this.highlightReplayElement(element); // Make sure element is visible and focused element.scrollIntoView({ behavior: 'smooth', block: 'center' }); await this.wait(100); // Focus the element element.focus(); element.click(); // Some inputs need a click to properly focus // Check if it's a contenteditable element const isContentEditable = element.contentEditable === 'true' || element.getAttribute('contenteditable') === 'true' || element.closest('[contenteditable="true"]'); if (isContentEditable) { // Handle contenteditable elements await this.typeInContentEditable(element, event); } else if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') { // Handle regular input/textarea elements await this.typeInInput(element, event); } else { // Try contenteditable approach for other elements await this.typeInContentEditable(element, event); } } async typeInInput(element, event) { if (event.type === 'SET') { // Clear and set value element.value = ''; // Dispatch events element.dispatchEvent(new Event('focus', { bubbles: true })); element.dispatchEvent(new Event('input', { bubbles: true })); // Type character by character for realism for (let i = 0; i < event.value.length; i++) { element.value = event.value.substring(0, i + 1); // Dispatch multiple events for better compatibility element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true, inputType: 'insertText', data: event.value[i] })); await this.wait(30); } } else { // TYPE - append to existing value const startValue = element.value || ''; for (let i = 0; i < event.value.length; i++) { element.value = startValue + event.value.substring(0, i + 1); // Dispatch multiple events element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true, inputType: 'insertText', data: event.value[i] })); await this.wait(30); } } // Dispatch change and blur events element.dispatchEvent(new Event('change', { bubbles: true })); element.dispatchEvent(new Event('blur', { bubbles: true })); } async typeInContentEditable(element, event) { // Focus and prepare the element element.focus(); // Create a range and selection const selection = window.getSelection(); const range = document.createRange(); if (event.type === 'SET') { // Clear existing content element.textContent = ''; // Set cursor at the beginning range.selectNodeContents(element); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } else { // Move cursor to the end for TYPE range.selectNodeContents(element); range.collapse(false); selection.removeAllRanges(); selection.addRange(range); } // Type each character using keyboard events for (let i = 0; i < event.value.length; i++) { const char = event.value[i]; // Dispatch keydown const keydownEvent = new KeyboardEvent('keydown', { key: char, char: char, keyCode: char.charCodeAt(0), which: char.charCodeAt(0), bubbles: true, cancelable: true }); element.dispatchEvent(keydownEvent); // Insert the character if (!keydownEvent.defaultPrevented) { document.execCommand('insertText', false, char); } // Dispatch keyup const keyupEvent = new KeyboardEvent('keyup', { key: char, char: char, keyCode: char.charCodeAt(0), which: char.charCodeAt(0), bubbles: true, cancelable: true }); element.dispatchEvent(keyupEvent); // Dispatch input event element.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true, inputType: 'insertText', data: char })); await this.wait(30); } // Dispatch blur event element.dispatchEvent(new Event('blur', { bubbles: true })); } async executeScroll(event) { const scrollAmount = event.direction === 'DOWN' ? event.amount : -event.amount; const duration = 300; const startTime = Date.now(); if (event.isWindowScroll || event.selector === 'window') { // Window scroll const startY = window.scrollY; const targetY = startY + scrollAmount; const animateScroll = () => { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / duration, 1); const easeProgress = this.easeInOut(progress); window.scrollTo(0, startY + (targetY - startY) * easeProgress); if (progress < 1) { requestAnimationFrame(animateScroll); } }; animateScroll(); } else { // Element scroll const element = document.querySelector(event.selector); if (!element) { throw new Error(`Scrollable element not found: ${event.selector}`); } // Highlight the scrollable element this.highlightReplayElement(element); const startY = element.scrollTop; const targetY = startY + scrollAmount; const animateScroll = () => { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / duration, 1); const easeProgress = this.easeInOut(progress); element.scrollTop = startY + (targetY - startY) * easeProgress; if (progress < 1) { requestAnimationFrame(animateScroll); } }; animateScroll(); } await this.wait(duration); } async executeWait(event) { if (event.waitType === 'time') { await this.wait(event.value * 1000); } else { // Wait for element to appear const timeout = 5000; const startTime = Date.now(); while (Date.now() - startTime < timeout) { const element = document.querySelector(event.value); if (element) { this.highlightReplayElement(element); return; } await this.wait(100); } throw new Error(`Element not found after ${timeout}ms: ${event.value}`); } } async executeKeyboardShortcut(event) { // Show keyboard overlay this.showKeyboardOverlay(this.getEventDetail(event)); // Create and dispatch keyboard event const keyEvent = new KeyboardEvent('keydown', { key: event.key, code: event.code, ctrlKey: event.ctrlKey, metaKey: event.metaKey, altKey: event.altKey, shiftKey: event.shiftKey, bubbles: true, cancelable: true }); document.activeElement.dispatchEvent(keyEvent); // Some shortcuts need keyup as well const keyUpEvent = new KeyboardEvent('keyup', { key: event.key, code: event.code, ctrlKey: event.ctrlKey, metaKey: event.metaKey, altKey: event.altKey, shiftKey: event.shiftKey, bubbles: true, cancelable: true }); document.activeElement.dispatchEvent(keyUpEvent); await this.wait(500); } async executeKeyPress(event) { this.showKeyboardOverlay(event.key); const activeElement = document.activeElement; if (event.key === 'Delete' || event.key === 'Backspace') { // Handle delete/backspace on input elements if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA') { const start = activeElement.selectionStart; const end = activeElement.selectionEnd; if (start !== end) { // Delete selection activeElement.value = activeElement.value.substring(0, start) + activeElement.value.substring(end); activeElement.selectionStart = activeElement.selectionEnd = start; } else if (event.key === 'Backspace' && start > 0) { // Delete character before cursor activeElement.value = activeElement.value.substring(0, start - 1) + activeElement.value.substring(start); activeElement.selectionStart = activeElement.selectionEnd = start - 1; } else if (event.key === 'Delete' && start < activeElement.value.length) { // Delete character after cursor activeElement.value = activeElement.value.substring(0, start) + activeElement.value.substring(start + 1); } activeElement.dispatchEvent(new Event('input', { bubbles: true })); } } else { // Regular key press const keyEvent = new KeyboardEvent('keydown', { key: event.key, bubbles: true, cancelable: true }); activeElement.dispatchEvent(keyEvent); } await this.wait(300); } async saveFlow() { // Get flow name from user const flowName = prompt('Enter a name for this flow:'); if (!flowName || !flowName.trim()) { return; } // Get current domain const domain = window.location.hostname; // Create flow object const flow = { id: Date.now().toString(), name: flowName.trim(), domain: domain, url: window.location.href, events: this.groupedEvents, createdAt: new Date().toISOString(), outputFormat: this.outputFormat }; // Get existing flows for this domain const storageKey = `c4ai_flows_${domain}`; chrome.storage.local.get(storageKey, (result) => { const flows = result[storageKey] || []; flows.push(flow); // Save updated flows chrome.storage.local.set({ [storageKey]: flows }, () => { if (chrome.runtime.lastError) { alert('Failed to save flow: ' + chrome.runtime.lastError.message); } else { alert(`Flow "${flowName}" saved successfully!`); // Update hint text document.getElementById('c4ai-script-hint').textContent = `āœ… Flow "${flowName}" saved!`; } }); }); } showSavedFlows() { const domain = window.location.hostname; const storageKey = `c4ai_flows_${domain}`; chrome.storage.local.get(storageKey, (result) => { const flows = result[storageKey] || []; // Create modal const modal = document.createElement('div'); modal.className = 'c4ai-saved-flows-modal'; modal.innerHTML = `

Saved Flows for ${domain}

${flows.length === 0 ? '

No saved flows for this domain yet. Record and save your first flow!

' : `
${flows.map(flow => `

${flow.name}

${flow.events.length} actions • ${new Date(flow.createdAt).toLocaleDateString()}

`).join('')}
` }
`; document.body.appendChild(modal); // Add event listeners document.getElementById('c4ai-close-flows').addEventListener('click', () => { modal.remove(); }); // Load flow buttons modal.querySelectorAll('.c4ai-load-flow-btn').forEach(btn => { btn.addEventListener('click', (e) => { const flowId = e.target.closest('button').dataset.flowId; const flow = flows.find(f => f.id === flowId); if (flow) { this.loadFlow(flow); modal.remove(); } }); }); // Delete flow buttons modal.querySelectorAll('.c4ai-delete-flow-btn').forEach(btn => { btn.addEventListener('click', (e) => { const flowId = e.target.closest('button').dataset.flowId; if (confirm('Are you sure you want to delete this flow?')) { this.deleteFlow(flowId); modal.remove(); this.showSavedFlows(); // Refresh the modal } }); }); }); } loadFlow(flow) { // Stop any current recording this.isRecording = false; this.removeEventCapture(); // Load the flow's events this.groupedEvents = [...flow.events]; this.outputFormat = flow.outputFormat || 'js'; // Show the recording summary UI this.showRecordingSummary(); // Update hint document.getElementById('c4ai-script-hint').textContent = `Loaded flow: "${flow.name}"`; } deleteFlow(flowId) { const domain = window.location.hostname; const storageKey = `c4ai_flows_${domain}`; chrome.storage.local.get(storageKey, (result) => { const flows = result[storageKey] || []; const updatedFlows = flows.filter(f => f.id !== flowId); chrome.storage.local.set({ [storageKey]: updatedFlows }, () => { if (chrome.runtime.lastError) { alert('Failed to delete flow: ' + chrome.runtime.lastError.message); } }); }); } recordAgain() { // Show confirmation dialog if (!confirm('Are you sure you want to start a new recording? This will clear the current recording.')) { return; } // Clear current recording and start fresh this.groupedEvents = []; this.rawEvents = []; this.processedEventIndices.clear(); this.keyBuffer = []; // Don't create a new toolbar, just update the existing one this.isRecording = true; this.startTime = Date.now(); this.lastEventTime = this.startTime; // Reset toolbar content to recording state const toolbarContent = this.toolbar.querySelector('.c4ai-toolbar-content'); toolbarContent.innerHTML = `
Actions: 0
Format:
Recording your actions... Click, type, and scroll to build your script.
`; // Re-add event listeners document.getElementById('c4ai-pause-recording').addEventListener('click', () => this.togglePause()); document.getElementById('c4ai-stop-generate').addEventListener('click', () => this.stopAndGenerate()); document.getElementById('c4ai-add-wait').addEventListener('click', () => this.showWaitDialog()); document.getElementById('c4ai-saved-flows').addEventListener('click', () => this.showSavedFlows()); document.getElementById('c4ai-output-format').addEventListener('change', (e) => { this.outputFormat = e.target.value; }); this.createRecordingIndicator(); this.updateToolbar(); } generatePythonTemplate(scriptCode, format) { const currentUrl = window.location.href; const timestamp = new Date().toISOString(); if (format === 'js') { return `#!/usr/bin/env python3 """ Generated by Crawl4AI Chrome Extension - Script Builder (ALPHA) URL: ${currentUrl} Generated: ${timestamp} """ import asyncio from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig # JavaScript code to execute JS_SCRIPT = """ ${scriptCode} """ async def run_automation(): """Run the recorded automation script""" # Configure browser browser_config = BrowserConfig( headless=False, # Set to True for headless mode verbose=True ) # Configure crawler with JavaScript execution crawler_config = CrawlerRunConfig( js_code=JS_SCRIPT, wait_for="js:() => document.readyState === 'complete'", page_timeout=30000 # 30 seconds timeout ) async with AsyncWebCrawler(config=browser_config) as crawler: result = await crawler.arun( url="${currentUrl}", config=crawler_config ) if result.success: print("āœ… Automation completed successfully!") print(f"Final URL: {result.url}") # You can access the final HTML with result.html # Or extracted content with result.cleaned_html else: print("āŒ Automation failed:", result.error_message) if __name__ == "__main__": asyncio.run(run_automation()) `; } else { // C4A Script format return `#!/usr/bin/env python3 """ Generated by Crawl4AI Chrome Extension - Script Builder (ALPHA) URL: ${currentUrl} Generated: ${timestamp} """ import asyncio from pathlib import Path from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig from crawl4ai import JsonCssExtractionStrategy # C4A Script commands C4A_SCRIPT = """ ${scriptCode} """ # Save the C4A script for reference script_path = Path('automation_script.c4a') with open(script_path, 'w') as f: f.write(C4A_SCRIPT) print(f"šŸ’¾ C4A Script saved to: {script_path}") print("\\nšŸ“œ Generated C4A Script:") print(C4A_SCRIPT) # Note: To execute C4A scripts, you'll need to use the C4A Script compiler # Example: # from crawl4ai.script import C4ACompiler # compiler = C4ACompiler() # js_code = compiler.compile(C4A_SCRIPT) # # Then use js_code in CrawlerRunConfig as shown in the JavaScript example above print("\\nšŸ’” To execute this C4A script, compile it to JavaScript first!") `; } } } // Initialize let schemaBuilder = null; let scriptBuilder = null; // Listen for messages from popup chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { switch (message.action) { case 'startSchemaCapture': if (!schemaBuilder) { schemaBuilder = new SchemaBuilder(); } schemaBuilder.start(); sendResponse({ success: true }); break; case 'startScriptCapture': if (!scriptBuilder) { scriptBuilder = new ScriptBuilder(); } scriptBuilder.start(); sendResponse({ success: true }); break; case 'stopCapture': if (schemaBuilder) { schemaBuilder.stop(); schemaBuilder = null; } if (scriptBuilder) { scriptBuilder.stop(); scriptBuilder = null; } sendResponse({ success: true }); break; case 'generateCode': if (schemaBuilder) { const code = schemaBuilder.generateCode(); schemaBuilder.showCodeModal(code); } sendResponse({ success: true }); break; } return true; });