// C4A-Script Tutorial App Controller class TutorialApp { constructor() { this.editor = null; this.currentScript = ''; this.currentJS = []; this.isEditingJS = false; this.tutorialMode = false; this.currentStep = 0; this.tutorialSteps = []; this.recordingManager = null; this.blocklyManager = null; this.init(); } init() { this.setupEditors(); this.setupButtons(); this.setupTabs(); this.setupTutorial(); this.checkFirstVisit(); // Initialize recording manager this.recordingManager = new RecordingManager(this); // Initialize Blockly manager this.blocklyManager = new BlocklyManager(this); } setupEditors() { // C4A Script Editor const c4aTextarea = document.getElementById('c4a-editor'); this.editor = CodeMirror.fromTextArea(c4aTextarea, { mode: 'javascript', // Use JS mode for now theme: 'material-darker', lineNumbers: true, lineWrapping: true, autoCloseBrackets: true, matchBrackets: true, readOnly: false, cursorBlinkRate: 530, inputStyle: 'contenteditable' // Changed from 'textarea' to prevent cursor issues }); // Save script on change this.editor.on('change', () => { this.currentScript = this.editor.getValue(); localStorage.setItem('c4a-script', this.currentScript); }); // Load saved script const saved = localStorage.getItem('c4a-script'); if (saved && !this.tutorialMode) { this.editor.setValue(saved); } // Ensure editor is properly sized and interactive setTimeout(() => { this.editor.refresh(); // Set cursor position instead of just focusing const doc = this.editor.getDoc(); doc.setCursor(doc.lineCount() - 1, 0); this.editor.focus(); }, 100); // Single unified click handler for focus const editorElement = this.editor.getWrapperElement(); editorElement.addEventListener('mousedown', (e) => { // Use mousedown instead of click for immediate response e.stopPropagation(); // Ensure editor gets focus on next tick setTimeout(() => { if (!this.editor.hasFocus()) { this.editor.focus(); } }, 0); }); } setupButtons() { // Run button document.getElementById('run-btn').addEventListener('click', () => { this.runScript(); }); // Clear button document.getElementById('clear-btn').addEventListener('click', () => { this.editor.setValue(''); this.clearConsole(); }); // Examples button document.getElementById('examples-btn').addEventListener('click', () => { this.showExamples(); }); // Tutorial button document.getElementById('tutorial-btn').addEventListener('click', () => { this.showIntroModal(); }); // Copy JS button document.getElementById('copy-js-btn').addEventListener('click', () => { this.copyJS(); }); // Edit JS button document.getElementById('edit-js-btn').addEventListener('click', () => { this.toggleJSEdit(); }); // Reset playground document.getElementById('reset-playground').addEventListener('click', () => { this.resetPlayground(); }); // Fullscreen document.getElementById('fullscreen-btn').addEventListener('click', () => { this.toggleFullscreen(); }); // Intro modal buttons document.getElementById('start-tutorial-btn').addEventListener('click', () => { this.hideIntroModal(); this.startTutorial(); }); document.getElementById('skip-tutorial-btn').addEventListener('click', () => { this.hideIntroModal(); }); // Tutorial navigation document.getElementById('tutorial-prev').addEventListener('click', () => { this.prevStep(); }); document.getElementById('tutorial-next').addEventListener('click', () => { this.nextStep(); }); document.getElementById('tutorial-exit').addEventListener('click', () => { this.exitTutorial(); }); } setupTabs() { const tabs = document.querySelectorAll('.tab'); tabs.forEach(tab => { tab.addEventListener('click', () => { const tabName = tab.getAttribute('data-tab'); this.switchTab(tabName); }); }); // Remove execution tab since we're removing it const progressTab = document.querySelector('[data-tab="progress"]'); if (progressTab) progressTab.remove(); } switchTab(tabName) { // Update tab buttons document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); // Update tab panes document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); document.getElementById(`${tabName}-tab`).classList.add('active'); } checkFirstVisit() { const hasVisited = localStorage.getItem('c4a-tutorial-visited'); if (!hasVisited) { setTimeout(() => this.showIntroModal(), 500); } } showIntroModal() { document.getElementById('tutorial-intro').classList.remove('hidden'); } hideIntroModal() { document.getElementById('tutorial-intro').classList.add('hidden'); localStorage.setItem('c4a-tutorial-visited', 'true'); } setupTutorial() { this.tutorialSteps = [ { title: "Welcome to C4A-Script!", description: "C4A-Script is a simple language for web automation. Let's start by handling popups that appear on websites.", script: "# Welcome to C4A-Script Tutorial!\n# Let's start by waiting for the page to load\n\nWAIT `body` 2", validate: () => true }, { title: "Handle Cookie Banner", description: "Check if cookie banner exists and accept it.", script: "# Wait for page and handle cookie banner\nWAIT `body` 2\n\n# Check if cookie banner exists, then click accept\nIF (EXISTS `.cookie-banner`) THEN CLICK `.accept`", validate: () => { const iframe = document.getElementById('playground-frame'); return !iframe.contentDocument?.querySelector('.cookie-banner'); } }, { title: "Handle Newsletter Popup", description: "Now let's handle the newsletter popup that appears after 3 seconds.", script: "# Wait for newsletter popup to appear\nWAIT 3\n\n# Close the newsletter popup\nIF (EXISTS `#newsletter-popup`) THEN CLICK `.close`", validate: () => { const iframe = document.getElementById('playground-frame'); return !iframe.contentDocument?.querySelector('#newsletter-popup'); } }, { title: "Start Interactive Elements", description: "Click the start button to reveal more interactive elements.", script: "# Click the start tutorial button\nCLICK `#start-tutorial`", validate: () => true }, { title: "Login Process", description: "Now let's complete the login form with email and password using the new SET command.", script: "# Login process\nCLICK `#login-btn`\nWAIT `.login-form` 2\n\n# Fill form fields using SET\nSET `#email` \"demo@example.com\"\nSET `#password` \"demo123\"\n\n# Submit the form\nCLICK `button[type=\"submit\"]`\nWAIT `.success` 2", validate: () => { const iframe = document.getElementById('playground-frame'); return iframe.contentDocument?.querySelector('.user-info')?.style.display === 'flex'; } }, { title: "Navigate and Scroll", description: "Navigate to the catalog and use scrolling to load more products.", script: "# Navigate to catalog\nCLICK `#catalog-link`\nWAIT `.product-grid` 3\n\n# Scroll to load more products\nSCROLL DOWN 500\nWAIT 1\nSCROLL DOWN 500\nWAIT 1\n\n# Apply a filter\nCLICK `.filter-group input[type=\"checkbox\"]`", validate: () => true }, { title: "Advanced - Procedures", description: "Create reusable command groups with PROC for common tasks.", script: "# Define a procedure to check login status\nPROC check_login\n IF (NOT EXISTS `.user-info`) THEN CLICK `#login-btn`\n IF (NOT EXISTS `.user-info`) THEN WAIT `.login-form` 2\n IF (NOT EXISTS `.user-info`) THEN SET `#email` \"demo@example.com\"\n IF (NOT EXISTS `.user-info`) THEN SET `#password` \"demo123\"\n IF (NOT EXISTS `.user-info`) THEN CLICK `button[type=\"submit\"]`\nENDPROC\n\n# Example: Navigate between sections\nCLICK `a[href=\"#tabs\"]`\nWAIT 1\nCLICK `a[href=\"#forms\"]`\nWAIT 1\n\n# If we get logged out, use our procedure\ncheck_login", validate: () => true }, { title: "More Commands", description: "Explore additional C4A commands with the Forms section.", script: "# First, let's fill the contact form with variables\n\n# Set variables\nSETVAR name = \"Alice Smith\"\nSETVAR msg = \"I'd like to know more about your Premium plan!\"\n\n# Fill contact form\nSET `#contact-name` $name\nSET `#contact-email` \"alice@example.com\"\nSET `#contact-message` $msg\n\n# Select dropdown option\nCLICK `#contact-subject`\nCLICK `option[value=\"support\"]`\nWAIT 0.5\n\n# Submit contact form\nCLICK `.btn-primary`\nWAIT 1\n\n# Now let's do the multi-step survey\nSCROLL DOWN 400\nWAIT 0.5\n\n# Step 1: Personal info\nSET `#full-name` \"Bob Johnson\"\nSET `#survey-email` \"bob@example.com\"\nCLICK `.next-step`\nWAIT 0.5\n\n# Step 2: Select interests (multi-select)\nCLICK `#interests`\nCLICK `option[value=\"technology\"]`\nCLICK `option[value=\"science\"]`\nCLICK `.next-step`\nWAIT 0.5\n\n# Step 3: Submit survey\nCLICK `#submit-survey`\n\n# Check results with JavaScript\nEVAL `console.log('Forms completed successfully!')`", validate: () => true }, { title: "Congratulations!", description: "You've mastered C4A-Script basics! You can now automate complex web interactions. Try the examples or create your own scripts.", script: "# 🎉 You've completed the tutorial!\n\n# You learned:\n# ✓ WAIT - Wait for elements or time\n# ✓ IF/THEN - Conditional actions\n# ✓ NOT - Negate conditions\n# ✓ CLICK - Click elements\n# ✓ SET - Set input field values\n# ✓ SETVAR - Create variables\n# ✓ SCROLL - Scroll the page\n# ✓ PROC - Create procedures\n# ✓ And much more!\n\n# Try the Examples button for more scripts\n# Happy automating with C4A-Script!", validate: () => true } ]; } startTutorial() { this.tutorialMode = true; this.currentStep = 0; document.getElementById('tutorial-nav').classList.remove('hidden'); document.querySelector('.app-container').classList.add('tutorial-active'); this.showStep(0); } exitTutorial() { this.tutorialMode = false; document.getElementById('tutorial-nav').classList.add('hidden'); document.querySelector('.app-container').classList.remove('tutorial-active'); } showStep(index) { const step = this.tutorialSteps[index]; if (!step) return; // Update navigation UI document.getElementById('tutorial-step-info').textContent = `Step ${index + 1} of ${this.tutorialSteps.length}`; document.getElementById('tutorial-title').textContent = step.title; // Update progress bar const progress = ((index + 1) / this.tutorialSteps.length) * 100; document.getElementById('tutorial-progress-fill').style.width = `${progress}%`; // Update buttons document.getElementById('tutorial-prev').disabled = index === 0; const nextBtn = document.getElementById('tutorial-next'); if (index === this.tutorialSteps.length - 1) { nextBtn.textContent = 'Finish'; } else { nextBtn.textContent = 'Next →'; } // Set script this.editor.setValue(step.script); // Update description in nav bar document.getElementById('tutorial-description').textContent = step.description; // Focus editor after setting content and ensure it's editable // Use requestAnimationFrame for better timing requestAnimationFrame(() => { this.editor.setOption('readOnly', false); this.editor.refresh(); // Place cursor at end first, then focus const doc = this.editor.getDoc(); const lastLine = doc.lineCount() - 1; const lastCh = doc.getLine(lastLine).length; doc.setCursor(lastLine, lastCh); this.editor.focus(); }); } // Removed showTooltip method - no longer needed nextStep() { if (this.currentStep < this.tutorialSteps.length - 1) { this.currentStep++; this.showStep(this.currentStep); } else { this.exitTutorial(); } } prevStep() { if (this.currentStep > 0) { this.currentStep--; this.showStep(this.currentStep); } } async runScript() { const script = this.editor.getValue(); if (!script.trim()) { this.addConsoleMessage('No script to run', 'error'); return; } this.clearConsole(); this.switchTab('console'); this.addConsoleMessage('Compiling C4A script...'); try { // If in JS edit mode, use the edited JS directly if (this.isEditingJS) { const jsCode = document.getElementById('js-output').textContent.split('\n').filter(line => line.trim()); await this.executeJS(jsCode); return; } // Compile C4A to JS const compiled = await this.compileScript(script); if (compiled.success) { this.currentJS = compiled.jsCode; this.displayJS(compiled.jsCode); this.addConsoleMessage(`✓ Compiled successfully (${compiled.jsCode.length} statements)`, 'success'); // Execute the JS await this.executeJS(compiled.jsCode); } else { // Show compilation error const error = compiled.error; this.addConsoleMessage(`✗ Compilation error at line ${error.line}:${error.column}`, 'error'); this.addConsoleMessage(` ${error.message}`, 'error'); if (error.suggestion) { this.addConsoleMessage(` 💡 ${error.suggestion}`, 'warning'); } } } catch (error) { this.addConsoleMessage(`Error: ${error.message}`, 'error'); } } async compileScript(script) { try { const response = await fetch('/api/compile', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ script }) }); if (!response.ok) { throw new Error('Compilation service unavailable'); } return await response.json(); } catch (error) { // Return error if compilation service is unavailable return { success: false, error: { line: 1, column: 1, message: "C4A compilation service unavailable. Please ensure the server is running.", suggestion: "Start the C4A server or check your connection" } }; } } displayJS(jsCode) { const formatted = jsCode.map((line, i) => `${(i + 1).toString().padStart(2, ' ')}. ${line}` ).join('\n'); document.getElementById('js-output').textContent = formatted; } async executeJS(jsCode) { this.addConsoleMessage('Executing JavaScript...'); // Send all code to iframe to execute at once await this.executeInIframe(jsCode); } async executeInIframe(jsCode) { const iframe = document.getElementById('playground-frame'); const iframeWindow = iframe.contentWindow; // Create a unique ID for this execution const executionId = 'exec_' + Date.now(); // Create the full script to execute in iframe const fullScript = ` (async () => { const results = []; try { ${jsCode.map((code, i) => ` try { ${code} results.push({ index: ${i}, success: true, code: ${JSON.stringify(code)} }); } catch (error) { results.push({ index: ${i}, success: false, error: error.message, code: ${JSON.stringify(code)} }); throw error; // Stop execution on first error } `).join('\n')} // Send success message window.parent.postMessage({ type: 'c4a-execution-complete', id: '${executionId}', success: true, results: results }, '*'); } catch (error) { // Send error message window.parent.postMessage({ type: 'c4a-execution-complete', id: '${executionId}', success: false, error: error.message, results: results }, '*'); } })(); `; // Wait for execution result return new Promise((resolve, reject) => { const messageHandler = (event) => { if (event.data.type === 'c4a-execution-complete' && event.data.id === executionId) { window.removeEventListener('message', messageHandler); // Log results if (event.data.results) { event.data.results.forEach(result => { if (result.success) { this.addConsoleMessage(`✓ ${result.code}`, 'success'); } else { this.addConsoleMessage(`✗ ${result.code}`, 'error'); this.addConsoleMessage(` Error: ${result.error}`, 'error'); } }); } if (event.data.success) { this.addConsoleMessage('Execution completed successfully', 'success'); resolve(); } else { this.addConsoleMessage('Execution stopped due to error', 'error'); resolve(); // Still resolve to not break the flow } } }; window.addEventListener('message', messageHandler); // Inject and execute the script const script = iframe.contentDocument.createElement('script'); script.textContent = fullScript; iframe.contentDocument.body.appendChild(script); script.remove(); // Timeout after 10 seconds setTimeout(() => { window.removeEventListener('message', messageHandler); this.addConsoleMessage('Execution timeout', 'warning'); resolve(); }, 10000); }); } highlightElement(element) { const originalBorder = element.style.border; const originalBackground = element.style.backgroundColor; element.style.border = '2px solid #0fbbaa'; element.style.backgroundColor = 'rgba(15, 187, 170, 0.1)'; setTimeout(() => { element.style.border = originalBorder; element.style.backgroundColor = originalBackground; }, 1000); } // Removed progress methods - everything goes to console now addConsoleMessage(message, type = 'text') { const consoleEl = document.getElementById('console-output'); const line = document.createElement('div'); line.className = 'console-line'; line.innerHTML = ` $ ${message} `; consoleEl.appendChild(line); // Scroll the console container (parent of console-output) const consoleContainer = consoleEl.parentElement; if (consoleContainer) { // Use requestAnimationFrame to ensure DOM has updated requestAnimationFrame(() => { consoleContainer.scrollTop = consoleContainer.scrollHeight; }); } } clearConsole() { document.getElementById('console-output').innerHTML = `
$ Ready to run C4A scripts...
`; } copyJS() { const text = this.currentJS.join('\n'); navigator.clipboard.writeText(text).then(() => { this.addConsoleMessage('JavaScript copied to clipboard', 'success'); }); } toggleJSEdit() { this.isEditingJS = !this.isEditingJS; const editBtn = document.getElementById('edit-js-btn'); const jsOutput = document.getElementById('js-output'); if (this.isEditingJS) { editBtn.innerHTML = '💾'; jsOutput.contentEditable = true; jsOutput.style.outline = '1px solid #0fbbaa'; this.addConsoleMessage('JS edit mode enabled - modify and run', 'warning'); } else { editBtn.innerHTML = 'âœī¸'; jsOutput.contentEditable = false; jsOutput.style.outline = 'none'; this.addConsoleMessage('JS edit mode disabled', 'text'); } } resetPlayground() { const iframe = document.getElementById('playground-frame'); iframe.src = iframe.src; this.addConsoleMessage('Playground reset', 'success'); } toggleFullscreen() { const playgroundPanel = document.querySelector('.playground-panel'); playgroundPanel.classList.toggle('fullscreen'); } showExamples() { const examples = [ { name: 'Quick Start - Handle Popups', script: `# Handle popups quickly\nWAIT \`body\` 2\nIF (EXISTS \`.cookie-banner\`) THEN CLICK \`.accept\`\nWAIT 3\nIF (EXISTS \`#newsletter-popup\`) THEN CLICK \`.close\`` }, { name: 'Complete Login Flow', script: `# Full login process\nCLICK \`#login-btn\`\nWAIT \`.login-form\` 2\n\n# Set credentials using the new SET command\nSET \`#email\` "demo@example.com"\nSET \`#password\` "demo123"\n\n# Submit\nCLICK \`button[type="submit"]\`\nWAIT \`.success\` 2` }, { name: 'Product Browsing', script: `# Browse products with filters\nCLICK \`#catalog-link\`\nWAIT \`.product-grid\` 3\n\n# Apply filters\nCLICK \`.filter-button\`\nWAIT 0.5\nCLICK \`input[value="electronics"]\`\n\n# Load more products\nREPEAT (SCROLL DOWN 800, 3)\nWAIT 1` }, { name: 'Advanced Form Wizard', script: `# Multi-step form with validation\nCLICK \`a[href="#forms"]\`\nWAIT \`#survey-form\` 2\n\n# Step 1: Personal Info\nSET \`#full-name\` "John Doe"\nSET \`#survey-email\` "john@example.com"\nCLICK \`.next-step\`\nWAIT 1\n\n# Step 2: Preferences\nCLICK \`#interests\`\nCLICK \`option[value="tech"]\`\nCLICK \`option[value="music"]\`\nCLICK \`.next-step\`\nWAIT 1\n\n# Step 3: Submit\nIF (EXISTS \`#comments\`) THEN SET \`#comments\` "Great experience!"\nCLICK \`#submit-survey\`\nWAIT \`.success-message\` 3` } ]; const currentIndex = parseInt(localStorage.getItem('exampleIndex') || '0'); const example = examples[currentIndex % examples.length]; this.editor.setValue(example.script); this.addConsoleMessage(`Loaded example: ${example.name}`, 'success'); localStorage.setItem('exampleIndex', ((currentIndex + 1) % examples.length).toString()); } wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } // Add animations CSS const style = document.createElement('style'); style.textContent = ` @keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } @keyframes fadeOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-10px); } } `; document.head.appendChild(style); // Recording Manager Class class RecordingManager { constructor(tutorialApp) { this.app = tutorialApp; this.isRecording = false; this.rawEvents = []; this.groupedEvents = []; this.startTime = 0; this.lastEventTime = 0; this.eventInjected = false; this.keyBuffer = []; this.keyBufferTimeout = null; this.scrollAccumulator = { direction: null, amount: 0, startTime: 0 }; this.processedEventIndices = new Set(); // Track which raw events have been processed this.init(); } init() { this.setupUI(); this.setupMessageHandler(); } setupUI() { // Record button const recordBtn = document.getElementById('record-btn'); recordBtn.addEventListener('click', () => this.toggleRecording()); // Timeline button const timelineBtn = document.getElementById('timeline-btn'); timelineBtn?.addEventListener('click', () => this.showTimeline()); // Back to editor button document.getElementById('back-to-editor')?.addEventListener('click', () => this.hideTimeline()); // Timeline controls document.getElementById('select-all-events')?.addEventListener('click', () => this.selectAllEvents()); document.getElementById('clear-events')?.addEventListener('click', () => this.clearEvents()); document.getElementById('generate-script')?.addEventListener('click', () => this.generateScript()); } setupMessageHandler() { window.addEventListener('message', (event) => { if (event.data.type === 'c4a-recording-event' && this.isRecording) { this.handleRecordedEvent(event.data.event); } }); } toggleRecording() { const recordBtn = document.getElementById('record-btn'); const timelineBtn = document.getElementById('timeline-btn'); if (!this.isRecording) { // Start recording this.isRecording = true; this.startTime = Date.now(); this.lastEventTime = this.startTime; this.rawEvents = []; this.groupedEvents = []; this.processedEventIndices = new Set(); this.keyBuffer = []; this.scrollAccumulator = { direction: null, amount: 0, startTime: 0 }; recordBtn.classList.add('recording'); recordBtn.innerHTML = '⏚Stop'; // Show timeline immediately when recording starts timelineBtn.classList.remove('hidden'); this.showTimeline(); this.injectEventCapture(); this.app.addConsoleMessage('🔴 Recording started...', 'info'); } else { // Stop recording this.isRecording = false; recordBtn.classList.remove('recording'); recordBtn.innerHTML = 'âēRecord'; this.removeEventCapture(); this.processEvents(); this.app.addConsoleMessage('âšī¸ Recording stopped', 'info'); } } showTimeline() { const editorView = document.getElementById('editor-view'); const timelineView = document.getElementById('timeline-view'); editorView.classList.add('hidden'); timelineView.classList.remove('hidden'); // Refresh CodeMirror when switching back this.editorNeedsRefresh = true; } hideTimeline() { const editorView = document.getElementById('editor-view'); const timelineView = document.getElementById('timeline-view'); timelineView.classList.add('hidden'); editorView.classList.remove('hidden'); // Refresh CodeMirror after switching if (this.editorNeedsRefresh) { setTimeout(() => this.app.editor.refresh(), 100); this.editorNeedsRefresh = false; } } injectEventCapture() { const iframe = document.getElementById('playground-frame'); const script = ` (function() { if (window.__c4aRecordingActive) return; window.__c4aRecordingActive = true; const captureEvent = (type, event) => { const data = { type: type, timestamp: Date.now(), targetTag: event.target.tagName, targetId: event.target.id, targetClass: event.target.className, targetSelector: generateSelector(event.target), targetType: event.target.type // For input elements }; // Add type-specific data 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; break; case 'input': case 'change': data.value = event.target.value; data.inputType = event.inputType; // For checkboxes and radio buttons, also capture checked state if (event.target.type === 'checkbox' || event.target.type === 'radio') { data.checked = event.target.checked; } // For select elements, capture selected text if (event.target.tagName === 'SELECT') { data.selectedText = event.target.options[event.target.selectedIndex]?.text || ''; } break; case 'scroll': case 'wheel': data.scrollTop = window.scrollY; data.scrollLeft = window.scrollX; data.deltaY = event.deltaY || 0; data.deltaX = event.deltaX || 0; break; case 'focus': case 'blur': data.value = event.target.value || ''; break; } window.parent.postMessage({ type: 'c4a-recording-event', event: data }, '*'); }; const generateSelector = (element) => { try { if (element.id) return '#' + element.id; if (element.className && typeof element.className === 'string') { const classes = element.className.trim().split(/\\s+/); if (classes.length > 0 && classes[0]) { return '.' + classes[0]; } } // Generate nth-child selector let path = []; let currentElement = element; while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) { let selector = currentElement.nodeName.toLowerCase(); if (currentElement.id) { selector = '#' + currentElement.id; path.unshift(selector); break; } else { let sibling = currentElement; let nth = 1; while (sibling.previousElementSibling) { sibling = sibling.previousElementSibling; if (sibling.nodeName === currentElement.nodeName) nth++; } if (nth > 1) selector += ':nth-child(' + nth + ')'; } path.unshift(selector); currentElement = currentElement.parentNode; } return path.join(' > ') || element.nodeName.toLowerCase(); } catch (e) { return element.nodeName.toLowerCase(); } }; // Store event handlers for cleanup window.__c4aEventHandlers = {}; // Attach event listeners const events = ['click', 'dblclick', 'contextmenu', 'keydown', 'keyup', 'input', 'change', 'scroll', 'wheel', 'focus', 'blur']; events.forEach(eventType => { const handler = (e) => captureEvent(eventType, e); window.__c4aEventHandlers[eventType] = handler; document.addEventListener(eventType, handler, true); }); // Cleanup function window.__c4aCleanupRecording = () => { events.forEach(eventType => { const handler = window.__c4aEventHandlers[eventType]; if (handler) { document.removeEventListener(eventType, handler, true); } }); delete window.__c4aRecordingActive; delete window.__c4aCleanupRecording; delete window.__c4aEventHandlers; }; })(); `; const scriptEl = iframe.contentDocument.createElement('script'); scriptEl.textContent = script; iframe.contentDocument.body.appendChild(scriptEl); scriptEl.remove(); this.eventInjected = true; } removeEventCapture() { if (!this.eventInjected) return; const iframe = document.getElementById('playground-frame'); iframe.contentWindow.eval('if (window.__c4aCleanupRecording) window.__c4aCleanupRecording();'); this.eventInjected = false; } handleRecordedEvent(event) { const now = Date.now(); const timeSinceStart = ((now - this.startTime) / 1000).toFixed(1); // Add time since last event event.timeSinceStart = timeSinceStart; event.timeSinceLast = now - this.lastEventTime; this.lastEventTime = now; this.rawEvents.push(event); // Real-time processing for immediate feedback if (event.type === 'keydown' && this.shouldGroupKeystrokes(event)) { this.keyBuffer.push(event); // Clear existing timeout if (this.keyBufferTimeout) { clearTimeout(this.keyBufferTimeout); } // Set timeout to flush buffer after 500ms of no typing this.keyBufferTimeout = setTimeout(() => { this.flushKeyBuffer(); this.updateTimeline(); }, 500); } else { // Handle change events for select, checkbox, radio if (event.type === 'change') { const tagName = event.targetTag?.toLowerCase(); // Only skip change events for text inputs (they're part of typing) if (tagName === 'input' && event.targetType !== 'checkbox' && event.targetType !== 'radio') { return; // Skip text input change events } // For select, checkbox, radio - process the change event if (tagName === 'select' || (tagName === 'input' && (event.targetType === 'checkbox' || event.targetType === 'radio'))) { // Flush any pending keystrokes first if (this.keyBuffer.length > 0) { this.flushKeyBuffer(); this.updateTimeline(); } // Create SET command for the value change const command = this.eventToCommand(event, this.rawEvents.length - 1); if (command) { this.groupedEvents.push(command); this.updateTimeline(); } return; } } // Skip input events - they're part of typing if (event.type === 'input') { return; } // Clear timeout if exists if (this.keyBufferTimeout) { clearTimeout(this.keyBufferTimeout); this.keyBufferTimeout = null; } // Flush key buffer only for significant events const shouldFlushBuffer = event.type === 'click' || event.type === 'dblclick' || event.type === 'contextmenu' || event.type === 'scroll' || event.type === 'wheel'; const hadKeyBuffer = this.keyBuffer.length > 0; if (shouldFlushBuffer && hadKeyBuffer) { this.flushKeyBuffer(); this.updateTimeline(); } // Process this event immediately if it's not a typing-related event if (event.type !== 'keydown' && event.type !== 'keyup' && event.type !== 'input' && event.type !== 'change' && event.type !== 'focus' && event.type !== 'blur') { const command = this.eventToCommand(event, this.rawEvents.length - 1); if (command) { // Check if it's a scroll event that should be accumulated if (command.type === 'SCROLL') { // Remove previous scroll events in the same direction this.groupedEvents = this.groupedEvents.filter(e => !(e.type === 'SCROLL' && e.direction === command.direction && parseFloat(e.time) > parseFloat(command.time) - 0.5) ); } this.groupedEvents.push(command); this.updateTimeline(); } } } } shouldGroupKeystrokes(event) { // Skip if no key if (!event.key) return false; // Group printable characters, space, and common typing keys return ( event.key.length === 1 || // Single characters event.key === ' ' || // Space event.key === 'Enter' || // Enter key event.key === 'Tab' || // Tab key event.key === 'Backspace' || // Backspace event.key === 'Delete' // Delete ); } flushKeyBuffer() { if (this.keyBuffer.length === 0) return; // Build the text, handling special keys const text = this.keyBuffer.map(e => { switch(e.key) { case ' ': return ' '; case 'Enter': return '\n'; case 'Tab': return '\t'; case 'Backspace': return ''; // Skip backspace in final text case 'Delete': return ''; // Skip delete in final text default: return e.key; } }).join(''); // Don't create empty TYPE commands if (text.length === 0) { this.keyBuffer = []; return; } const firstEvent = this.keyBuffer[0]; const lastEvent = this.keyBuffer[this.keyBuffer.length - 1]; // Mark all keystroke events as processed this.keyBuffer.forEach(event => { const index = this.rawEvents.indexOf(event); if (index !== -1) { this.processedEventIndices.add(index); } }); // Check if this should be a SET command instead of TYPE // Look for a click event just before the first keystroke const firstKeystrokeIndex = this.rawEvents.indexOf(firstEvent); let commandType = 'TYPE'; if (firstKeystrokeIndex > 0) { const prevEvent = this.rawEvents[firstKeystrokeIndex - 1]; if (prevEvent && prevEvent.type === 'click' && prevEvent.targetSelector === firstEvent.targetSelector) { // This looks like a SET pattern commandType = 'SET'; // Mark the click event as processed too this.processedEventIndices.add(firstKeystrokeIndex - 1); } } // Check if we already have a TYPE command for this exact text at this time // This prevents duplicates when the buffer is flushed multiple times const existingCommand = this.groupedEvents.find(cmd => cmd.type === commandType && cmd.value === text && cmd.time === firstEvent.timeSinceStart ); if (!existingCommand) { this.groupedEvents.push({ type: commandType, selector: firstEvent.targetSelector, value: text, time: firstEvent.timeSinceStart, duration: lastEvent.timestamp - firstEvent.timestamp, raw: [...this.keyBuffer] // Make a copy to avoid reference issues }); } this.keyBuffer = []; } processEvents() { // Clear any pending timeouts if (this.keyBufferTimeout) { clearTimeout(this.keyBufferTimeout); this.keyBufferTimeout = null; } // Flush any remaining buffers this.flushKeyBuffer(); // Don't reprocess events that were already grouped during recording // Just apply final optimizations this.optimizeEvents(); this.updateTimeline(); } eventToCommand(event, index) { // Skip already processed events if (this.processedEventIndices.has(index)) { return null; } // Skip events that should only be processed as grouped commands if (event.type === 'keydown' || event.type === 'keyup' || event.type === 'input' || event.type === 'focus' || event.type === 'blur') { return null; } // Allow change events for select, checkbox, radio if (event.type === 'change') { const tagName = event.targetTag?.toLowerCase(); if (tagName === 'select' || (tagName === 'input' && (event.targetType === 'checkbox' || event.targetType === 'radio'))) { // Process as SET command } else { return null; // Skip change events for text inputs } } switch (event.type) { case 'click': // Check if followed by input focus or change event const nextEvent = this.rawEvents[index + 1]; if (nextEvent && nextEvent.targetSelector === event.targetSelector) { if (nextEvent.type === 'focus' || nextEvent.type === 'change') { return null; // Skip, will be handled by SET or change event } } // Also check if this is a click on a select element if (event.targetTag?.toLowerCase() === 'select') { // Look ahead for a change event for (let i = index + 1; i < Math.min(index + 5, this.rawEvents.length); i++) { if (this.rawEvents[i].type === 'change' && this.rawEvents[i].targetSelector === event.targetSelector) { return null; // Skip click, change event will handle it } } } 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 'scroll': case 'wheel': // Accumulate scroll events if (event.deltaY !== 0) { const direction = event.deltaY > 0 ? 'DOWN' : 'UP'; const amount = Math.abs(event.deltaY); if (this.scrollAccumulator.direction === direction && event.timestamp - this.scrollAccumulator.startTime < 500) { this.scrollAccumulator.amount += amount; } else { this.scrollAccumulator = { direction, amount, startTime: event.timestamp }; } // Return accumulated scroll at end of sequence const nextEvent = this.rawEvents[index + 1]; if (!nextEvent || nextEvent.type !== 'scroll' || nextEvent.timestamp - event.timestamp > 500) { return { type: 'SCROLL', direction: this.scrollAccumulator.direction, amount: Math.round(this.scrollAccumulator.amount), time: event.timeSinceStart }; } } return null; // Input events are handled through keystroke grouping case 'input': return null; case 'change': // Handle select, checkbox, radio changes const tagName = event.targetTag?.toLowerCase(); if (tagName === 'select') { return { type: 'SET', selector: event.targetSelector, value: event.value, displayValue: event.selectedText || event.value, time: event.timeSinceStart }; } else if (tagName === 'input' && event.targetType === 'checkbox') { return { type: 'SET', selector: event.targetSelector, value: event.checked ? 'checked' : 'unchecked', time: event.timeSinceStart }; } else if (tagName === 'input' && event.targetType === 'radio') { return { type: 'SET', selector: event.targetSelector, value: 'checked', time: event.timeSinceStart }; } return null; } return null; } optimizeEvents() { const optimized = []; let lastTime = 0; this.groupedEvents.forEach((event, index) => { // Insert WAIT if pause > 1 second const currentTime = parseFloat(event.time); if (currentTime - lastTime > 1) { optimized.push({ type: 'WAIT', value: Math.round(currentTime - lastTime), time: lastTime.toFixed(1) }); } optimized.push(event); lastTime = currentTime; }); this.groupedEvents = optimized; } updateTimeline() { const timeline = document.getElementById('timeline-events'); timeline.innerHTML = ''; this.groupedEvents.forEach((event, index) => { const eventEl = this.createEventElement(event, index); timeline.appendChild(eventEl); }); } createEventElement(event, index) { const div = document.createElement('div'); div.className = 'timeline-event'; div.dataset.index = index; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'event-checkbox'; checkbox.checked = true; checkbox.addEventListener('change', () => { div.classList.toggle('selected', checkbox.checked); }); const time = document.createElement('span'); time.className = 'event-time'; time.textContent = event.time + 's'; const command = document.createElement('span'); command.className = 'event-command'; command.innerHTML = this.formatCommand(event); const editBtn = document.createElement('button'); editBtn.className = 'event-edit'; editBtn.textContent = 'Edit'; editBtn.addEventListener('click', () => this.editEvent(index)); div.appendChild(checkbox); div.appendChild(time); div.appendChild(command); div.appendChild(editBtn); // Initially selected div.classList.add('selected'); return div; } formatCommand(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}" (${event.value.length} chars)`; case 'SET': // Use displayValue if available (for select elements) const displayText = event.displayValue || event.value; return `SET \`${event.selector}\` "${displayText}"`; case 'SCROLL': return `SCROLL ${event.direction} ${event.amount}`; case 'WAIT': return `WAIT ${event.value}`; default: return `${event.type}`; } } editEvent(index) { const event = this.groupedEvents[index]; this.currentEditIndex = index; // Show modal const overlay = document.getElementById('event-editor-overlay'); const modal = document.getElementById('event-editor-modal'); overlay.classList.remove('hidden'); modal.classList.remove('hidden'); // Populate fields document.getElementById('edit-command-type').value = event.type; // Show/hide fields based on command type const selectorField = document.getElementById('edit-selector-field'); const valueField = document.getElementById('edit-value-field'); const directionField = document.getElementById('edit-direction-field'); selectorField.classList.add('hidden'); valueField.classList.add('hidden'); directionField.classList.add('hidden'); switch (event.type) { case 'CLICK': case 'DOUBLE_CLICK': case 'RIGHT_CLICK': selectorField.classList.remove('hidden'); document.getElementById('edit-selector').value = event.selector; break; case 'TYPE': valueField.classList.remove('hidden'); document.getElementById('edit-value').value = event.value; break; case 'SET': selectorField.classList.remove('hidden'); valueField.classList.remove('hidden'); document.getElementById('edit-selector').value = event.selector; document.getElementById('edit-value').value = event.value; break; case 'SCROLL': directionField.classList.remove('hidden'); valueField.classList.remove('hidden'); document.getElementById('edit-direction').value = event.direction; document.getElementById('edit-value').value = event.amount; break; case 'WAIT': valueField.classList.remove('hidden'); document.getElementById('edit-value').value = event.value; break; } // Setup event handlers this.setupEditModalHandlers(); } setupEditModalHandlers() { const overlay = document.getElementById('event-editor-overlay'); const modal = document.getElementById('event-editor-modal'); const cancelBtn = document.getElementById('edit-cancel'); const saveBtn = document.getElementById('edit-save'); const closeModal = () => { overlay.classList.add('hidden'); modal.classList.add('hidden'); }; const saveHandler = () => { const event = this.groupedEvents[this.currentEditIndex]; // Update event based on type switch (event.type) { case 'CLICK': case 'DOUBLE_CLICK': case 'RIGHT_CLICK': event.selector = document.getElementById('edit-selector').value; break; case 'TYPE': event.value = document.getElementById('edit-value').value; break; case 'SET': event.selector = document.getElementById('edit-selector').value; event.value = document.getElementById('edit-value').value; break; case 'SCROLL': event.direction = document.getElementById('edit-direction').value; event.amount = parseInt(document.getElementById('edit-value').value) || 0; break; case 'WAIT': event.value = parseInt(document.getElementById('edit-value').value) || 0; break; } // Update timeline this.updateTimeline(); closeModal(); }; // Clean up old handlers cancelBtn.replaceWith(cancelBtn.cloneNode(true)); saveBtn.replaceWith(saveBtn.cloneNode(true)); overlay.replaceWith(overlay.cloneNode(true)); // Add new handlers document.getElementById('edit-cancel').addEventListener('click', closeModal); document.getElementById('edit-save').addEventListener('click', saveHandler); document.getElementById('event-editor-overlay').addEventListener('click', closeModal); } selectAllEvents() { const checkboxes = document.querySelectorAll('.event-checkbox'); const events = document.querySelectorAll('.timeline-event'); checkboxes.forEach((cb, i) => { cb.checked = true; events[i].classList.add('selected'); }); } clearEvents() { this.groupedEvents = []; this.updateTimeline(); } generateScript() { const selectedEvents = []; const checkboxes = document.querySelectorAll('.event-checkbox'); checkboxes.forEach((cb, index) => { if (cb.checked && this.groupedEvents[index]) { selectedEvents.push(this.groupedEvents[index]); } }); if (selectedEvents.length === 0) { this.app.addConsoleMessage('No events selected', 'warning'); return; } const script = selectedEvents.map(event => this.eventToC4A(event)).join('\n'); // Set the script in the editor this.app.editor.setValue(script); this.app.addConsoleMessage(`Generated ${selectedEvents.length} commands`, 'success'); // Switch back to editor view this.hideTimeline(); } eventToC4A(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': return `SCROLL ${event.direction} ${event.amount}`; case 'WAIT': return `WAIT ${event.value}`; default: return `# Unknown: ${event.type}`; } } } // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.tutorialApp = new TutorialApp(); console.log('🎓 C4A-Script Tutorial initialized!'); });