This commit introduces significant enhancements to the Crawl4AI ecosystem: Chrome Extension - Script Builder (Alpha): - Add recording functionality to capture user interactions (clicks, typing, scrolling) - Implement smart event grouping for cleaner script generation - Support export to both JavaScript and C4A script formats - Add timeline view for visualizing and editing recorded actions - Include wait commands (time-based and element-based) - Add saved flows functionality for reusing automation scripts - Update UI with consistent dark terminal theme (Dank Mono font, green/pink accents) - Release new extension versions: v1.1.0, v1.2.0, v1.2.1 LLM Context Builder Improvements: - Reorganize context files from llmtxt/ to llm.txt/ with better structure - Separate diagram templates from text content (diagrams/ and txt/ subdirectories) - Add comprehensive context files for all major Crawl4AI components - Improve file naming convention for better discoverability Documentation Updates: - Update apps index page to match main documentation theme - Standardize color scheme: "Available" tags use primary color (#50ffff) - Change "Coming Soon" tags to dark gray for better visual hierarchy - Add interactive two-column layout for extension landing page - Include code examples for both Schema Builder and Script Builder features Technical Improvements: - Enhance event capture mechanism with better element selection - Add support for contenteditable elements and complex form interactions - Implement proper scroll event handling for both window and element scrolling - Add meta key support for keyboard shortcuts - Improve selector generation for more reliable element targeting The Script Builder is released as Alpha, acknowledging potential bugs while providing early access to this powerful automation recording feature.
3392 lines
113 KiB
JavaScript
3392 lines
113 KiB
JavaScript
// Content script for Crawl4AI Assistant
|
|
class SchemaBuilder {
|
|
constructor() {
|
|
this.mode = null;
|
|
this.container = null;
|
|
this.fields = [];
|
|
this.overlay = null;
|
|
this.toolbar = null;
|
|
this.highlightBox = null;
|
|
this.selectedElements = new Set();
|
|
this.isPaused = false;
|
|
this.codeModal = null;
|
|
|
|
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
this.handleClick = this.handleClick.bind(this);
|
|
this.handleKeyPress = this.handleKeyPress.bind(this);
|
|
}
|
|
|
|
start() {
|
|
this.mode = 'container';
|
|
this.createOverlay();
|
|
this.createToolbar();
|
|
this.attachEventListeners();
|
|
this.updateToolbar();
|
|
}
|
|
|
|
stop() {
|
|
this.detachEventListeners();
|
|
this.overlay?.remove();
|
|
this.toolbar?.remove();
|
|
this.highlightBox?.remove();
|
|
this.removeAllHighlights();
|
|
this.mode = null;
|
|
this.container = null;
|
|
this.fields = [];
|
|
this.selectedElements.clear();
|
|
}
|
|
|
|
createOverlay() {
|
|
// Create highlight box
|
|
this.highlightBox = document.createElement('div');
|
|
this.highlightBox.className = 'c4ai-highlight-box';
|
|
document.body.appendChild(this.highlightBox);
|
|
}
|
|
|
|
createToolbar() {
|
|
this.toolbar = document.createElement('div');
|
|
this.toolbar.className = 'c4ai-toolbar';
|
|
this.toolbar.innerHTML = `
|
|
<div class="c4ai-toolbar-titlebar">
|
|
<div class="c4ai-titlebar-dots">
|
|
<button class="c4ai-dot c4ai-dot-close" id="c4ai-close"></button>
|
|
<button class="c4ai-dot c4ai-dot-minimize"></button>
|
|
<button class="c4ai-dot c4ai-dot-maximize"></button>
|
|
</div>
|
|
<img src="${chrome.runtime.getURL('icons/icon-16.png')}" class="c4ai-titlebar-icon" alt="Crawl4AI">
|
|
<div class="c4ai-titlebar-title">Crawl4AI Schema Builder</div>
|
|
</div>
|
|
<div class="c4ai-toolbar-content">
|
|
<div class="c4ai-toolbar-status">
|
|
<div class="c4ai-status-item">
|
|
<span class="c4ai-status-label">Mode:</span>
|
|
<span class="c4ai-status-value" id="c4ai-mode">Select Container</span>
|
|
</div>
|
|
<div class="c4ai-status-item">
|
|
<span class="c4ai-status-label">Container:</span>
|
|
<span class="c4ai-status-value" id="c4ai-container">Not selected</span>
|
|
</div>
|
|
</div>
|
|
<div class="c4ai-fields-list" id="c4ai-fields-list" style="display: none;">
|
|
<div class="c4ai-fields-header">Selected Fields:</div>
|
|
<ul class="c4ai-fields-items" id="c4ai-fields-items"></ul>
|
|
</div>
|
|
<div class="c4ai-toolbar-hint" id="c4ai-hint">
|
|
Click on a container element (e.g., product card, article, etc.)
|
|
</div>
|
|
<div class="c4ai-toolbar-actions">
|
|
<button id="c4ai-pause" class="c4ai-action-btn c4ai-pause-btn">
|
|
<span class="c4ai-pause-icon">⏸</span> Pause
|
|
</button>
|
|
<button id="c4ai-generate" class="c4ai-action-btn c4ai-generate-btn">
|
|
<span class="c4ai-generate-icon">⚡</span> Generate Code
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(this.toolbar);
|
|
|
|
// Add event listeners for toolbar buttons
|
|
document.getElementById('c4ai-pause').addEventListener('click', () => this.togglePause());
|
|
document.getElementById('c4ai-generate').addEventListener('click', () => this.stopAndGenerate());
|
|
document.getElementById('c4ai-close').addEventListener('click', () => this.stop());
|
|
|
|
// Make toolbar draggable
|
|
this.makeDraggable(this.toolbar);
|
|
}
|
|
|
|
attachEventListeners() {
|
|
document.addEventListener('mousemove', this.handleMouseMove, true);
|
|
document.addEventListener('click', this.handleClick, true);
|
|
document.addEventListener('keydown', this.handleKeyPress, true);
|
|
}
|
|
|
|
detachEventListeners() {
|
|
document.removeEventListener('mousemove', this.handleMouseMove, true);
|
|
document.removeEventListener('click', this.handleClick, true);
|
|
document.removeEventListener('keydown', this.handleKeyPress, true);
|
|
}
|
|
|
|
handleMouseMove(e) {
|
|
if (this.isPaused) return;
|
|
|
|
const element = document.elementFromPoint(e.clientX, e.clientY);
|
|
if (element && !this.isOurElement(element)) {
|
|
this.highlightElement(element);
|
|
}
|
|
}
|
|
|
|
handleClick(e) {
|
|
if (this.isPaused) return;
|
|
|
|
const element = e.target;
|
|
|
|
if (this.isOurElement(element)) {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
if (this.mode === 'container') {
|
|
this.selectContainer(element);
|
|
} else if (this.mode === 'field') {
|
|
this.selectField(element);
|
|
}
|
|
}
|
|
|
|
handleKeyPress(e) {
|
|
if (e.key === 'Escape') {
|
|
this.stop();
|
|
}
|
|
}
|
|
|
|
isOurElement(element) {
|
|
return element.classList.contains('c4ai-highlight-box') ||
|
|
element.classList.contains('c4ai-toolbar') ||
|
|
element.closest('.c4ai-toolbar') ||
|
|
element.closest('.c4ai-field-dialog') ||
|
|
element.closest('.c4ai-code-modal');
|
|
}
|
|
|
|
makeDraggable(element) {
|
|
let isDragging = false;
|
|
let startX, startY, initialX, initialY;
|
|
|
|
const titlebar = element.querySelector('.c4ai-toolbar-titlebar');
|
|
|
|
titlebar.addEventListener('mousedown', (e) => {
|
|
// Don't drag if clicking on buttons
|
|
if (e.target.classList.contains('c4ai-dot')) return;
|
|
|
|
isDragging = true;
|
|
startX = e.clientX;
|
|
startY = e.clientY;
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
initialX = rect.left;
|
|
initialY = rect.top;
|
|
|
|
element.style.transition = 'none';
|
|
titlebar.style.cursor = 'grabbing';
|
|
});
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!isDragging) return;
|
|
|
|
const deltaX = e.clientX - startX;
|
|
const deltaY = e.clientY - startY;
|
|
|
|
element.style.left = `${initialX + deltaX}px`;
|
|
element.style.top = `${initialY + deltaY}px`;
|
|
element.style.right = 'auto';
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
element.style.transition = '';
|
|
titlebar.style.cursor = 'grab';
|
|
}
|
|
});
|
|
}
|
|
|
|
makeDraggableByHeader(element) {
|
|
let isDragging = false;
|
|
let startX, startY, initialX, initialY;
|
|
|
|
const header = element.querySelector('.c4ai-debugger-header');
|
|
if (!header) return;
|
|
|
|
header.addEventListener('mousedown', (e) => {
|
|
// Don't drag if clicking on close button
|
|
if (e.target.id === 'c4ai-close-debugger' || e.target.closest('#c4ai-close-debugger')) return;
|
|
|
|
isDragging = true;
|
|
startX = e.clientX;
|
|
startY = e.clientY;
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
initialX = rect.left;
|
|
initialY = rect.top;
|
|
|
|
element.style.transition = 'none';
|
|
header.style.cursor = 'grabbing';
|
|
});
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!isDragging) return;
|
|
|
|
const deltaX = e.clientX - startX;
|
|
const deltaY = e.clientY - startY;
|
|
|
|
element.style.left = `${initialX + deltaX}px`;
|
|
element.style.top = `${initialY + deltaY}px`;
|
|
element.style.right = 'auto';
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
element.style.transition = '';
|
|
header.style.cursor = 'grab';
|
|
}
|
|
});
|
|
}
|
|
|
|
togglePause() {
|
|
this.isPaused = !this.isPaused;
|
|
const pauseBtn = document.getElementById('c4ai-pause');
|
|
if (this.isPaused) {
|
|
pauseBtn.innerHTML = '<span class="c4ai-play-icon">▶</span> Resume';
|
|
pauseBtn.classList.add('c4ai-paused');
|
|
this.highlightBox.style.display = 'none';
|
|
} else {
|
|
pauseBtn.innerHTML = '<span class="c4ai-pause-icon">⏸</span> Pause';
|
|
pauseBtn.classList.remove('c4ai-paused');
|
|
}
|
|
}
|
|
|
|
stopAndGenerate() {
|
|
if (!this.container || this.fields.length === 0) {
|
|
alert('Please select a container and at least one field before generating code.');
|
|
return;
|
|
}
|
|
|
|
const code = this.generateCode();
|
|
this.showCodeModal(code);
|
|
}
|
|
|
|
highlightElement(element) {
|
|
const rect = element.getBoundingClientRect();
|
|
this.highlightBox.style.cssText = `
|
|
left: ${rect.left + window.scrollX}px;
|
|
top: ${rect.top + window.scrollY}px;
|
|
width: ${rect.width}px;
|
|
height: ${rect.height}px;
|
|
display: block;
|
|
`;
|
|
|
|
if (this.mode === 'container') {
|
|
this.highlightBox.className = 'c4ai-highlight-box c4ai-container-mode';
|
|
} else {
|
|
this.highlightBox.className = 'c4ai-highlight-box c4ai-field-mode';
|
|
}
|
|
}
|
|
|
|
selectContainer(element) {
|
|
// Remove previous container highlight
|
|
if (this.container) {
|
|
this.container.element.classList.remove('c4ai-selected-container');
|
|
}
|
|
|
|
this.container = {
|
|
element: element,
|
|
html: element.outerHTML,
|
|
selector: this.generateSelector(element),
|
|
tagName: element.tagName.toLowerCase()
|
|
};
|
|
|
|
element.classList.add('c4ai-selected-container');
|
|
this.mode = 'field';
|
|
this.updateToolbar();
|
|
this.updateStats();
|
|
}
|
|
|
|
selectField(element) {
|
|
// Don't select the container itself
|
|
if (element === this.container.element) {
|
|
return;
|
|
}
|
|
|
|
// Check if already selected - if so, deselect it
|
|
if (this.selectedElements.has(element)) {
|
|
this.deselectField(element);
|
|
return;
|
|
}
|
|
|
|
// Must be inside the container
|
|
if (!this.container.element.contains(element)) {
|
|
return;
|
|
}
|
|
|
|
this.showFieldDialog(element);
|
|
}
|
|
|
|
deselectField(element) {
|
|
// Remove from fields array
|
|
this.fields = this.fields.filter(f => f.element !== element);
|
|
|
|
// Remove from selected elements set
|
|
this.selectedElements.delete(element);
|
|
|
|
// Remove visual selection
|
|
element.classList.remove('c4ai-selected-field');
|
|
|
|
// Update UI
|
|
this.updateToolbar();
|
|
this.updateStats();
|
|
}
|
|
|
|
showFieldDialog(element) {
|
|
const dialog = document.createElement('div');
|
|
dialog.className = 'c4ai-field-dialog';
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
dialog.style.cssText = `
|
|
left: ${rect.left + window.scrollX}px;
|
|
top: ${rect.bottom + window.scrollY + 10}px;
|
|
`;
|
|
|
|
dialog.innerHTML = `
|
|
<div class="c4ai-field-dialog-content">
|
|
<h4>Name this field:</h4>
|
|
<input type="text" id="c4ai-field-name" placeholder="e.g., title, price, description" autofocus>
|
|
<div class="c4ai-field-preview">
|
|
<strong>Content:</strong> ${element.textContent.trim().substring(0, 50)}...
|
|
</div>
|
|
<div class="c4ai-field-actions">
|
|
<button id="c4ai-field-save">Save</button>
|
|
<button id="c4ai-field-cancel">Cancel</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(dialog);
|
|
|
|
const input = dialog.querySelector('#c4ai-field-name');
|
|
const saveBtn = dialog.querySelector('#c4ai-field-save');
|
|
const cancelBtn = dialog.querySelector('#c4ai-field-cancel');
|
|
|
|
const save = () => {
|
|
const fieldName = input.value.trim();
|
|
if (fieldName) {
|
|
this.fields.push({
|
|
name: fieldName,
|
|
value: element.textContent.trim(),
|
|
element: element,
|
|
selector: this.generateSelector(element, this.container.element)
|
|
});
|
|
|
|
element.classList.add('c4ai-selected-field');
|
|
this.selectedElements.add(element);
|
|
this.updateToolbar();
|
|
this.updateStats();
|
|
}
|
|
dialog.remove();
|
|
};
|
|
|
|
const cancel = () => {
|
|
dialog.remove();
|
|
};
|
|
|
|
saveBtn.addEventListener('click', save);
|
|
cancelBtn.addEventListener('click', cancel);
|
|
input.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') save();
|
|
if (e.key === 'Escape') cancel();
|
|
});
|
|
|
|
input.focus();
|
|
}
|
|
|
|
generateSelector(element, context = document) {
|
|
// Try to generate a robust selector
|
|
if (element.id) {
|
|
return `#${CSS.escape(element.id)}`;
|
|
}
|
|
|
|
// Check for data attributes (most stable)
|
|
const dataAttrs = ['data-testid', 'data-id', 'data-test', 'data-cy'];
|
|
for (const attr of dataAttrs) {
|
|
const value = element.getAttribute(attr);
|
|
if (value) {
|
|
return `[${attr}="${value}"]`;
|
|
}
|
|
}
|
|
|
|
// Check for aria-label
|
|
if (element.getAttribute('aria-label')) {
|
|
return `[aria-label="${element.getAttribute('aria-label')}"]`;
|
|
}
|
|
|
|
// Try semantic HTML elements with text
|
|
const tagName = element.tagName.toLowerCase();
|
|
if (['button', 'a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
|
const text = element.textContent.trim();
|
|
if (text && text.length < 50) {
|
|
// Use tag name with partial text match
|
|
return `${tagName}`;
|
|
}
|
|
}
|
|
|
|
// Check for simple, non-utility classes
|
|
const classes = Array.from(element.classList)
|
|
.filter(c => !c.startsWith('c4ai-')) // Exclude our classes
|
|
.filter(c => !c.includes('[') && !c.includes('(') && !c.includes(':')) // Exclude utility classes
|
|
.filter(c => c.length < 30); // Exclude very long classes
|
|
|
|
if (classes.length > 0 && classes.length <= 3) {
|
|
const selector = classes.map(c => `.${CSS.escape(c)}`).join('');
|
|
try {
|
|
if (context.querySelectorAll(selector).length === 1) {
|
|
return selector;
|
|
}
|
|
} catch (e) {
|
|
// Invalid selector, continue
|
|
}
|
|
}
|
|
|
|
// Use nth-child with simple parent tag
|
|
const parent = element.parentElement;
|
|
if (parent && parent !== context) {
|
|
const siblings = Array.from(parent.children);
|
|
const index = siblings.indexOf(element) + 1;
|
|
// Just use parent tag name to avoid recursion
|
|
const parentTag = parent.tagName.toLowerCase();
|
|
return `${parentTag} > ${tagName}:nth-child(${index})`;
|
|
}
|
|
|
|
// Final fallback
|
|
return tagName;
|
|
}
|
|
|
|
updateToolbar() {
|
|
document.getElementById('c4ai-mode').textContent =
|
|
this.mode === 'container' ? 'Select Container' : 'Select Fields';
|
|
|
|
document.getElementById('c4ai-container').textContent =
|
|
this.container ? `${this.container.tagName} ✓` : 'Not selected';
|
|
|
|
// Update fields list
|
|
const fieldsList = document.getElementById('c4ai-fields-list');
|
|
const fieldsItems = document.getElementById('c4ai-fields-items');
|
|
|
|
if (this.fields.length > 0) {
|
|
fieldsList.style.display = 'block';
|
|
fieldsItems.innerHTML = this.fields.map(field => `
|
|
<li class="c4ai-field-item">
|
|
<span class="c4ai-field-name">${field.name}</span>
|
|
<span class="c4ai-field-value">${field.value.substring(0, 30)}${field.value.length > 30 ? '...' : ''}</span>
|
|
</li>
|
|
`).join('');
|
|
} else {
|
|
fieldsList.style.display = 'none';
|
|
}
|
|
|
|
const hint = document.getElementById('c4ai-hint');
|
|
if (this.mode === 'container') {
|
|
hint.textContent = 'Click on a container element (e.g., product card, article, etc.)';
|
|
} else if (this.fields.length === 0) {
|
|
hint.textContent = 'Click on fields inside the container to extract (title, price, etc.)';
|
|
} else {
|
|
hint.innerHTML = `Continue selecting fields or click <strong>Stop & Generate</strong> to finish.`;
|
|
}
|
|
}
|
|
|
|
updateStats() {
|
|
chrome.runtime.sendMessage({
|
|
action: 'updateStats',
|
|
stats: {
|
|
container: !!this.container,
|
|
fields: this.fields.length
|
|
}
|
|
});
|
|
}
|
|
|
|
removeAllHighlights() {
|
|
document.querySelectorAll('.c4ai-selected-container').forEach(el => {
|
|
el.classList.remove('c4ai-selected-container');
|
|
});
|
|
document.querySelectorAll('.c4ai-selected-field').forEach(el => {
|
|
el.classList.remove('c4ai-selected-field');
|
|
});
|
|
}
|
|
|
|
generateCode() {
|
|
const fieldDescriptions = this.fields.map(f =>
|
|
`- ${f.name} (example: "${f.value.substring(0, 50)}...")`
|
|
).join('\n');
|
|
|
|
return `#!/usr/bin/env python3
|
|
"""
|
|
Generated by Crawl4AI Chrome Extension
|
|
URL: ${window.location.href}
|
|
Generated: ${new Date().toISOString()}
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
from pathlib import Path
|
|
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig
|
|
from crawl4ai.extraction_strategy import JsonCssExtractionStrategy
|
|
|
|
# HTML snippet of the selected container element
|
|
HTML_SNIPPET = """
|
|
${this.container.html}
|
|
"""
|
|
|
|
# Extraction query based on your field selections
|
|
EXTRACTION_QUERY = """
|
|
Create a JSON CSS extraction schema to extract the following fields:
|
|
${fieldDescriptions}
|
|
|
|
The schema should handle multiple ${this.container.tagName} elements on the page.
|
|
Each item should be extracted as a separate object in the results array.
|
|
"""
|
|
|
|
async def generate_schema():
|
|
"""Generate extraction schema using LLM"""
|
|
print("🔧 Generating extraction schema...")
|
|
|
|
try:
|
|
# Generate the schema using Crawl4AI's built-in LLM integration
|
|
schema = JsonCssExtractionStrategy.generate_schema(
|
|
html=HTML_SNIPPET,
|
|
query=EXTRACTION_QUERY,
|
|
)
|
|
|
|
# Save the schema for reuse
|
|
schema_path = Path('generated_schema.json')
|
|
with open(schema_path, 'w') as f:
|
|
json.dump(schema, f, indent=2)
|
|
|
|
print("✅ Schema generated successfully!")
|
|
print(f"📄 Schema saved to: {schema_path}")
|
|
print("\\nGenerated schema:")
|
|
print(json.dumps(schema, indent=2))
|
|
|
|
return schema
|
|
|
|
except Exception as e:
|
|
print(f"❌ Error generating schema: {e}")
|
|
return None
|
|
|
|
async def test_extraction(url: str = "${window.location.href}"):
|
|
"""Test the generated schema on the actual webpage"""
|
|
print("\\n🧪 Testing extraction on live webpage...")
|
|
|
|
# Load the generated schema
|
|
try:
|
|
with open('generated_schema.json', 'r') as f:
|
|
schema = json.load(f)
|
|
except FileNotFoundError:
|
|
print("❌ Schema file not found. Run generate_schema() first.")
|
|
return
|
|
|
|
# Configure browser
|
|
browser_config = BrowserConfig(
|
|
headless=True,
|
|
verbose=False
|
|
)
|
|
|
|
# Configure extraction
|
|
crawler_config = CrawlerRunConfig(
|
|
extraction_strategy=JsonCssExtractionStrategy(schema=schema)
|
|
)
|
|
|
|
async with AsyncWebCrawler(config=browser_config) as crawler:
|
|
result = await crawler.arun(
|
|
url=url,
|
|
config=crawler_config
|
|
)
|
|
|
|
if result.success and result.extracted_content:
|
|
data = json.loads(result.extracted_content)
|
|
print(f"\\n✅ Successfully extracted {len(data)} items!")
|
|
|
|
# Save results
|
|
with open('extracted_data.json', 'w') as f:
|
|
json.dump(data, f, indent=2)
|
|
|
|
# Show sample results
|
|
print("\\n📊 Sample results (first 2 items):")
|
|
for i, item in enumerate(data[:2], 1):
|
|
print(f"\\nItem {i}:")
|
|
for key, value in item.items():
|
|
print(f" {key}: {value}")
|
|
else:
|
|
print("❌ Extraction failed:", result.error_message)
|
|
|
|
if __name__ == "__main__":
|
|
# Step 1: Generate the schema from HTML snippet
|
|
asyncio.run(generate_schema())
|
|
|
|
# Step 2: Test extraction on the live webpage
|
|
# Uncomment the line below to test extraction:
|
|
# asyncio.run(test_extraction())
|
|
|
|
print("\\n🎯 Next steps:")
|
|
print("1. Review the generated schema in 'generated_schema.json'")
|
|
print("2. Uncomment the test_extraction() line to test on the live site")
|
|
print("3. Use the schema in your Crawl4AI projects!")
|
|
`;
|
|
|
|
return code;
|
|
}
|
|
|
|
showCodeModal(code) {
|
|
// Create modal
|
|
this.codeModal = document.createElement('div');
|
|
this.codeModal.className = 'c4ai-code-modal';
|
|
this.codeModal.innerHTML = `
|
|
<div class="c4ai-code-modal-content">
|
|
<div class="c4ai-code-modal-header">
|
|
<h2>Generated Python Code</h2>
|
|
<button class="c4ai-close-modal" id="c4ai-close-modal">✕</button>
|
|
</div>
|
|
<div class="c4ai-code-modal-body">
|
|
<pre class="c4ai-code-block"><code class="language-python">${this.escapeHtml(code)}</code></pre>
|
|
</div>
|
|
<div class="c4ai-code-modal-footer">
|
|
<button class="c4ai-action-btn c4ai-cloud-btn" id="c4ai-run-cloud" disabled>
|
|
<span>☁️</span> Run on C4AI Cloud (Coming Soon)
|
|
</button>
|
|
<button class="c4ai-action-btn c4ai-download-btn" id="c4ai-download-code">
|
|
<span>⬇</span> Download Code
|
|
</button>
|
|
<button class="c4ai-action-btn c4ai-copy-btn" id="c4ai-copy-code">
|
|
<span>📋</span> Copy to Clipboard
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(this.codeModal);
|
|
|
|
// Add event listeners
|
|
document.getElementById('c4ai-close-modal').addEventListener('click', () => {
|
|
this.codeModal.remove();
|
|
this.codeModal = null;
|
|
// Don't stop the capture session
|
|
});
|
|
|
|
document.getElementById('c4ai-download-code').addEventListener('click', () => {
|
|
chrome.runtime.sendMessage({
|
|
action: 'downloadCode',
|
|
code: code,
|
|
filename: `crawl4ai_schema_${Date.now()}.py`
|
|
}, (response) => {
|
|
if (response && response.success) {
|
|
const btn = document.getElementById('c4ai-download-code');
|
|
const originalHTML = btn.innerHTML;
|
|
btn.innerHTML = '<span>✓</span> Downloaded!';
|
|
setTimeout(() => {
|
|
btn.innerHTML = originalHTML;
|
|
}, 2000);
|
|
} else {
|
|
console.error('Download failed:', response?.error);
|
|
alert('Download failed. Please check your browser settings.');
|
|
}
|
|
});
|
|
});
|
|
|
|
document.getElementById('c4ai-copy-code').addEventListener('click', () => {
|
|
navigator.clipboard.writeText(code).then(() => {
|
|
const btn = document.getElementById('c4ai-copy-code');
|
|
btn.innerHTML = '<span>✓</span> Copied!';
|
|
setTimeout(() => {
|
|
btn.innerHTML = '<span>📋</span> Copy to Clipboard';
|
|
}, 2000);
|
|
});
|
|
});
|
|
|
|
// Apply syntax highlighting if possible
|
|
this.applySyntaxHighlighting();
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
applySyntaxHighlighting() {
|
|
// Simple Python syntax highlighting - using a different approach
|
|
const codeElement = this.codeModal.querySelector('.language-python');
|
|
const code = codeElement.textContent;
|
|
|
|
// Split by lines to handle line-by-line
|
|
const lines = code.split('\n');
|
|
const highlightedLines = lines.map(line => {
|
|
let highlightedLine = this.escapeHtml(line);
|
|
|
|
// Skip if line is empty
|
|
if (!highlightedLine.trim()) return highlightedLine;
|
|
|
|
// Comments (lines starting with #)
|
|
if (highlightedLine.trim().startsWith('#')) {
|
|
return `<span class="c4ai-comment">${highlightedLine}</span>`;
|
|
}
|
|
|
|
// Triple quoted strings
|
|
if (highlightedLine.includes('"""')) {
|
|
highlightedLine = highlightedLine.replace(/(""".*?""")/g, '<span class="c4ai-string">$1</span>');
|
|
}
|
|
|
|
// Regular strings - single and double quotes
|
|
highlightedLine = highlightedLine.replace(/(["'])([^"']*)\1/g, '<span class="c4ai-string">$1$2$1</span>');
|
|
|
|
// Keywords - only highlight if not inside a string
|
|
const keywords = ['import', 'from', 'async', 'def', 'await', 'try', 'except', 'with', 'as', 'for', 'if', 'else', 'elif', 'return', 'print', 'open', 'and', 'or', 'not', 'in', 'is', 'class', 'self', 'None', 'True', 'False', '__name__', '__main__'];
|
|
|
|
keywords.forEach(keyword => {
|
|
// Use word boundaries and lookahead/lookbehind to ensure we're not in a string
|
|
const regex = new RegExp(`\\b(${keyword})\\b(?![^<]*</span>)`, 'g');
|
|
highlightedLine = highlightedLine.replace(regex, '<span class="c4ai-keyword">$1</span>');
|
|
});
|
|
|
|
// Functions (word followed by parenthesis)
|
|
highlightedLine = highlightedLine.replace(/\b([a-zA-Z_]\w*)\s*\(/g, '<span class="c4ai-function">$1</span>(');
|
|
|
|
return highlightedLine;
|
|
});
|
|
|
|
codeElement.innerHTML = highlightedLines.join('\n');
|
|
}
|
|
}
|
|
|
|
// Script Builder for recording user actions
|
|
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 = `
|
|
<div class="c4ai-toolbar-status">
|
|
<div class="c4ai-status-item">
|
|
<span class="c4ai-status-label">Script Builder</span>
|
|
<span class="c4ai-status-value">Ready</span>
|
|
</div>
|
|
</div>
|
|
<div class="c4ai-toolbar-hint" id="c4ai-script-hint">
|
|
Click "Start Recording" to begin capturing your actions. <span style="color: #ff3c74; font-size: 11px;">(Alpha - May contain bugs)</span>
|
|
</div>
|
|
<div class="c4ai-toolbar-actions c4ai-start-actions">
|
|
<button id="c4ai-start-recording" class="c4ai-action-btn c4ai-primary-btn">
|
|
<span>🔴</span> Start Recording
|
|
</button>
|
|
<button id="c4ai-saved-flows" class="c4ai-action-btn c4ai-secondary-btn">
|
|
<span>📂</span> Saved Flows
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<div class="c4ai-toolbar-status">
|
|
<div class="c4ai-status-item">
|
|
<span class="c4ai-status-label">Actions:</span>
|
|
<span class="c4ai-status-value" id="c4ai-action-count">0</span>
|
|
</div>
|
|
<div class="c4ai-status-item">
|
|
<span class="c4ai-status-label">Format:</span>
|
|
<select id="c4ai-output-format" class="c4ai-format-select">
|
|
<option value="js">JavaScript</option>
|
|
<option value="c4a">C4A Script</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="c4ai-toolbar-hint" id="c4ai-script-hint">
|
|
Recording your actions... Click, type, and scroll to build your script.
|
|
</div>
|
|
<div class="c4ai-toolbar-actions c4ai-recording-actions">
|
|
<button id="c4ai-add-wait" class="c4ai-action-btn c4ai-secondary-btn">
|
|
<span class="c4ai-wait-icon">⏱</span> Add Wait
|
|
</button>
|
|
<button id="c4ai-pause-recording" class="c4ai-action-btn c4ai-secondary-btn">
|
|
<span class="c4ai-pause-icon">⏸</span> Pause
|
|
</button>
|
|
<button id="c4ai-stop-generate" class="c4ai-action-btn c4ai-primary-btn">
|
|
<span class="c4ai-generate-icon">⚡</span> Stop & Generate
|
|
</button>
|
|
<button id="c4ai-saved-flows" class="c4ai-action-btn c4ai-secondary-btn">
|
|
<span>📂</span> Saved Flows
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<div class="c4ai-recording-dot"></div>
|
|
<span>Recording</span>
|
|
`;
|
|
document.body.appendChild(this.recordingIndicator);
|
|
}
|
|
|
|
createToolbar() {
|
|
this.toolbar = document.createElement('div');
|
|
this.toolbar.className = 'c4ai-script-toolbar';
|
|
this.toolbar.innerHTML = `
|
|
<div class="c4ai-toolbar-titlebar">
|
|
<div class="c4ai-titlebar-dots">
|
|
<button class="c4ai-dot c4ai-dot-close" id="c4ai-script-close"></button>
|
|
<button class="c4ai-dot c4ai-dot-minimize"></button>
|
|
<button class="c4ai-dot c4ai-dot-maximize"></button>
|
|
</div>
|
|
<img src="${chrome.runtime.getURL('icons/icon-16.png')}" class="c4ai-titlebar-icon" alt="Crawl4AI">
|
|
<div class="c4ai-titlebar-title">Crawl4AI Script Builder <span style="color: #ff3c74; font-size: 10px; margin-left: 8px;">(ALPHA)</span></div>
|
|
</div>
|
|
<div class="c4ai-toolbar-content" id="c4ai-toolbar-content">
|
|
<!-- Content will be dynamically updated -->
|
|
</div>
|
|
`;
|
|
document.body.appendChild(this.toolbar);
|
|
|
|
// Add close button listener
|
|
document.getElementById('c4ai-script-close').addEventListener('click', () => this.stop());
|
|
|
|
// Make toolbar draggable
|
|
this.makeDraggable(this.toolbar);
|
|
}
|
|
|
|
showStartScreen() {
|
|
const content = document.getElementById('c4ai-toolbar-content');
|
|
content.innerHTML = `
|
|
<div class="c4ai-start-screen">
|
|
<div class="c4ai-welcome-message">
|
|
<h3>Welcome to Script Builder</h3>
|
|
<p>Record your actions to create automation scripts. <span style="color: #ff3c74;">Alpha version - may contain bugs</span></p>
|
|
</div>
|
|
<div class="c4ai-start-actions">
|
|
<button id="c4ai-start-recording" class="c4ai-action-btn c4ai-primary-btn">
|
|
<span>🔴</span> Start Recording
|
|
</button>
|
|
<button id="c4ai-saved-flows" class="c4ai-action-btn c4ai-secondary-btn">
|
|
<span>📂</span> Saved Flows
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<div class="c4ai-toolbar-status">
|
|
<div class="c4ai-status-item">
|
|
<span class="c4ai-status-label">Actions:</span>
|
|
<span class="c4ai-status-value" id="c4ai-action-count">0</span>
|
|
</div>
|
|
<div class="c4ai-status-item">
|
|
<span class="c4ai-status-label">Format:</span>
|
|
<select id="c4ai-output-format" class="c4ai-format-select">
|
|
<option value="js">JavaScript</option>
|
|
<option value="c4a">C4A Script</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="c4ai-toolbar-hint" id="c4ai-script-hint">
|
|
Recording your actions... Click, type, and scroll to build your script.
|
|
</div>
|
|
<div class="c4ai-toolbar-actions">
|
|
<button id="c4ai-saved-flows" class="c4ai-action-btn c4ai-secondary-btn">
|
|
<span>📂</span> Saved Flows
|
|
</button>
|
|
<button id="c4ai-add-wait" class="c4ai-action-btn c4ai-secondary-btn">
|
|
<span class="c4ai-wait-icon">⏱</span> Add Wait
|
|
</button>
|
|
<button id="c4ai-pause-recording" class="c4ai-action-btn c4ai-pause-btn">
|
|
<span class="c4ai-pause-icon">⏸</span> Pause
|
|
</button>
|
|
<button id="c4ai-stop-generate" class="c4ai-action-btn c4ai-primary-btn">
|
|
<span class="c4ai-generate-icon">⚡</span> Stop & Generate
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
// 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;
|
|
});
|
|
}
|
|
|
|
makeDraggable(element) {
|
|
let isDragging = false;
|
|
let startX, startY, initialX, initialY;
|
|
|
|
const titlebar = element.querySelector('.c4ai-toolbar-titlebar');
|
|
|
|
titlebar.addEventListener('mousedown', (e) => {
|
|
if (e.target.classList.contains('c4ai-dot')) return;
|
|
|
|
isDragging = true;
|
|
startX = e.clientX;
|
|
startY = e.clientY;
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
initialX = rect.left;
|
|
initialY = rect.top;
|
|
|
|
element.style.transition = 'none';
|
|
titlebar.style.cursor = 'grabbing';
|
|
});
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!isDragging) return;
|
|
|
|
const deltaX = e.clientX - startX;
|
|
const deltaY = e.clientY - startY;
|
|
|
|
element.style.left = `${initialX + deltaX}px`;
|
|
element.style.top = `${initialY + deltaY}px`;
|
|
element.style.right = 'auto';
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
element.style.transition = '';
|
|
titlebar.style.cursor = 'grab';
|
|
}
|
|
});
|
|
}
|
|
|
|
makeDraggableByHeader(element) {
|
|
let isDragging = false;
|
|
let startX, startY, initialX, initialY;
|
|
|
|
const header = element.querySelector('.c4ai-debugger-header');
|
|
if (!header) return;
|
|
|
|
header.addEventListener('mousedown', (e) => {
|
|
// Don't drag if clicking on close button
|
|
if (e.target.id === 'c4ai-close-debugger' || e.target.closest('#c4ai-close-debugger')) return;
|
|
|
|
isDragging = true;
|
|
startX = e.clientX;
|
|
startY = e.clientY;
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
initialX = rect.left;
|
|
initialY = rect.top;
|
|
|
|
element.style.transition = 'none';
|
|
header.style.cursor = 'grabbing';
|
|
});
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!isDragging) return;
|
|
|
|
const deltaX = e.clientX - startX;
|
|
const deltaY = e.clientY - startY;
|
|
|
|
element.style.left = `${initialX + deltaX}px`;
|
|
element.style.top = `${initialY + deltaY}px`;
|
|
element.style.right = 'auto';
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
element.style.transition = '';
|
|
header.style.cursor = 'grab';
|
|
}
|
|
});
|
|
}
|
|
|
|
togglePause() {
|
|
this.isPaused = !this.isPaused;
|
|
const pauseBtn = document.getElementById('c4ai-pause-recording');
|
|
const recordingIndicator = document.querySelector('.c4ai-recording-indicator');
|
|
|
|
if (this.isPaused) {
|
|
pauseBtn.innerHTML = '<span class="c4ai-play-icon">▶</span> 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 = '<span class="c4ai-pause-icon">⏸</span> 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 = `
|
|
<div class="c4ai-wait-dialog-content">
|
|
<h4>Add Wait Command</h4>
|
|
<div class="c4ai-wait-options">
|
|
<label class="c4ai-wait-option">
|
|
<input type="radio" name="wait-type" value="time" checked>
|
|
<span>Wait for time (seconds)</span>
|
|
</label>
|
|
<label class="c4ai-wait-option">
|
|
<input type="radio" name="wait-type" value="selector">
|
|
<span>Wait for element</span>
|
|
</label>
|
|
</div>
|
|
<div class="c4ai-wait-input" id="c4ai-wait-time-input">
|
|
<input type="number" id="c4ai-wait-seconds" min="0.5" step="0.5" value="2" placeholder="Seconds">
|
|
</div>
|
|
<div class="c4ai-wait-input c4ai-hidden" id="c4ai-wait-selector-input">
|
|
<p>Click on an element to wait for</p>
|
|
</div>
|
|
<div class="c4ai-wait-actions">
|
|
<button id="c4ai-wait-cancel" class="c4ai-action-btn">Cancel</button>
|
|
<button id="c4ai-wait-add" class="c4ai-action-btn c4ai-primary">Add Wait</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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 element.classList.contains('c4ai-script-toolbar') ||
|
|
element.closest('.c4ai-script-toolbar') ||
|
|
element.classList.contains('c4ai-wait-dialog') ||
|
|
element.closest('.c4ai-wait-dialog') ||
|
|
element.classList.contains('c4ai-highlight-box') ||
|
|
element.classList.contains('c4ai-timeline-modal') ||
|
|
element.closest('.c4ai-timeline-modal');
|
|
}
|
|
|
|
getElementSelector(element) {
|
|
// Priority: ID > unique class > tag with position
|
|
if (element.id) {
|
|
return `#${element.id}`;
|
|
}
|
|
|
|
if (element.className && typeof element.className === 'string') {
|
|
const classes = element.className.split(' ').filter(c => c && !c.startsWith('c4ai-'));
|
|
if (classes.length > 0) {
|
|
const selector = `.${classes[0]}`;
|
|
if (document.querySelectorAll(selector).length === 1) {
|
|
return selector;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build a path selector
|
|
const path = [];
|
|
let current = element;
|
|
|
|
while (current && current !== document.body) {
|
|
const tagName = current.tagName.toLowerCase();
|
|
const parent = current.parentElement;
|
|
|
|
if (parent) {
|
|
const siblings = Array.from(parent.children);
|
|
const index = siblings.indexOf(current) + 1;
|
|
|
|
if (siblings.filter(s => s.tagName === current.tagName).length > 1) {
|
|
path.unshift(`${tagName}:nth-child(${index})`);
|
|
} else {
|
|
path.unshift(tagName);
|
|
}
|
|
} else {
|
|
path.unshift(tagName);
|
|
}
|
|
|
|
current = parent;
|
|
}
|
|
|
|
return path.join(' > ');
|
|
}
|
|
|
|
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 = `
|
|
<div class="c4ai-toolbar-status">
|
|
<div class="c4ai-status-item">
|
|
<span class="c4ai-status-label">Recording Complete</span>
|
|
<span class="c4ai-status-value">${this.groupedEvents.length} actions</span>
|
|
</div>
|
|
</div>
|
|
<div class="c4ai-toolbar-hint" id="c4ai-script-hint">
|
|
Recording stopped. You can replay, save, or generate code.
|
|
</div>
|
|
<div class="c4ai-toolbar-actions c4ai-summary-actions">
|
|
<button id="c4ai-replay" class="c4ai-action-btn c4ai-replay-btn">
|
|
<span>▶</span> Replay
|
|
</button>
|
|
<button id="c4ai-save-flow" class="c4ai-action-btn c4ai-save-btn">
|
|
<span>💾</span> Save Flow
|
|
</button>
|
|
<button id="c4ai-show-timeline" class="c4ai-action-btn c4ai-timeline-btn">
|
|
<span>📋</span> Review & Generate
|
|
</button>
|
|
<button id="c4ai-record-again" class="c4ai-action-btn c4ai-record-btn">
|
|
<span>🔄</span> Record Again
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<div class="c4ai-timeline-content">
|
|
<div class="c4ai-timeline-header">
|
|
<h2>Review Your Actions</h2>
|
|
<button class="c4ai-close-modal" id="c4ai-close-timeline">✕</button>
|
|
</div>
|
|
<div class="c4ai-timeline-body">
|
|
<div class="c4ai-timeline-controls">
|
|
<button class="c4ai-action-btn" id="c4ai-select-all">Select All</button>
|
|
<button class="c4ai-action-btn" id="c4ai-clear-all">Clear All</button>
|
|
</div>
|
|
<div class="c4ai-timeline-events" id="c4ai-timeline-events">
|
|
${this.renderTimelineEvents()}
|
|
</div>
|
|
</div>
|
|
<div class="c4ai-timeline-footer">
|
|
<select id="c4ai-final-format" class="c4ai-format-select">
|
|
<option value="js" ${this.outputFormat === 'js' ? 'selected' : ''}>JavaScript</option>
|
|
<option value="c4a" ${this.outputFormat === 'c4a' ? 'selected' : ''}>C4A Script</option>
|
|
</select>
|
|
<button class="c4ai-action-btn c4ai-download-btn" id="c4ai-download-script">
|
|
<span>⬇</span> Generate & Download
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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 `
|
|
<div class="c4ai-timeline-event">
|
|
<input type="checkbox" class="c4ai-event-checkbox" id="event-${index}" checked>
|
|
<label for="event-${index}" class="c4ai-event-label">
|
|
<span class="c4ai-event-time">${event.time.toFixed(1)}s</span>
|
|
<span class="c4ai-event-type">${event.type.replace(/_/g, ' ')}</span>
|
|
<span class="c4ai-event-detail" title="${detail.replace(/"/g, '"')}">${detail}</span>
|
|
</label>
|
|
</div>
|
|
`;
|
|
}).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 = `
|
|
<div class="c4ai-replay-content">
|
|
<div class="c4ai-replay-header">
|
|
<span class="c4ai-replay-icon">▶️</span>
|
|
<span class="c4ai-replay-title">Replaying Actions</span>
|
|
</div>
|
|
<div class="c4ai-replay-progress">
|
|
<div class="c4ai-replay-progress-bar" id="c4ai-replay-progress"></div>
|
|
</div>
|
|
<div class="c4ai-replay-status" id="c4ai-replay-status">
|
|
Preparing...
|
|
</div>
|
|
<button class="c4ai-replay-stop" id="c4ai-replay-stop">Stop Replay</button>
|
|
</div>
|
|
`;
|
|
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 = '<span>▶</span> 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 = `
|
|
<div class="c4ai-debugger-content">
|
|
<div class="c4ai-debugger-header">
|
|
<h2>Action Debugger <span style="color: #ff3c74; font-size: 12px;">(ALPHA)</span></h2>
|
|
<button class="c4ai-close-modal" id="c4ai-close-debugger">✕</button>
|
|
</div>
|
|
<div class="c4ai-debugger-body">
|
|
<div class="c4ai-debugger-controls">
|
|
<button class="c4ai-debug-btn c4ai-run-btn" id="c4ai-debug-run" title="Run to end or next breakpoint">
|
|
<span>▶</span> Run
|
|
</button>
|
|
<button class="c4ai-debug-btn c4ai-step-btn" id="c4ai-debug-step" title="Execute next action">
|
|
<span>⏭</span> Step
|
|
</button>
|
|
<button class="c4ai-debug-btn c4ai-pause-btn" id="c4ai-debug-pause" disabled title="Pause execution">
|
|
<span>⏸</span> Pause
|
|
</button>
|
|
<button class="c4ai-debug-btn c4ai-stop-btn" id="c4ai-debug-stop" title="Stop debugging">
|
|
<span>⏹</span> Stop
|
|
</button>
|
|
<button class="c4ai-debug-btn c4ai-restart-btn" id="c4ai-debug-restart" title="Restart from beginning">
|
|
<span>🔄</span> Restart
|
|
</button>
|
|
</div>
|
|
<div class="c4ai-debugger-status">
|
|
<span class="c4ai-debug-label">Status:</span>
|
|
<span class="c4ai-debug-value" id="c4ai-debug-status">Ready</span>
|
|
</div>
|
|
<div class="c4ai-debugger-actions" id="c4ai-debugger-actions">
|
|
${this.renderDebuggerActions()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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
|
|
this.makeDraggableByHeader(this.debuggerModal);
|
|
}
|
|
|
|
renderDebuggerActions() {
|
|
const events = this.debuggerState ? this.debuggerState.editedEvents : this.groupedEvents;
|
|
return events.map((event, index) => {
|
|
const detail = this.getEventDetail(event);
|
|
return `
|
|
<div class="c4ai-debug-action ${index === 0 ? 'c4ai-current' : ''}" data-index="${index}">
|
|
<div class="c4ai-debug-action-left">
|
|
<input type="checkbox" class="c4ai-breakpoint-checkbox" data-index="${index}" title="Toggle breakpoint">
|
|
<span class="c4ai-action-number">${index + 1}</span>
|
|
<span class="c4ai-action-indicator">➤</span>
|
|
</div>
|
|
<div class="c4ai-debug-action-content">
|
|
<div class="c4ai-action-type">${event.type.replace(/_/g, ' ')}</div>
|
|
<div class="c4ai-action-detail" contenteditable="true" data-index="${index}" data-field="detail">
|
|
${detail}
|
|
</div>
|
|
</div>
|
|
<div class="c4ai-debug-action-right">
|
|
<button class="c4ai-action-play" data-index="${index}" title="Execute this action">▶</button>
|
|
<button class="c4ai-action-edit" data-index="${index}" title="Edit action">✏️</button>
|
|
<button class="c4ai-action-delete" data-index="${index}" title="Delete action">🗑</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).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 = `
|
|
<div class="c4ai-edit-content">
|
|
<h3>Edit Action</h3>
|
|
<div class="c4ai-edit-field">
|
|
<label>Type:</label>
|
|
<select id="c4ai-edit-type">
|
|
<option value="CLICK" ${event.type === 'CLICK' ? 'selected' : ''}>Click</option>
|
|
<option value="TYPE" ${event.type === 'TYPE' ? 'selected' : ''}>Type</option>
|
|
<option value="SET" ${event.type === 'SET' ? 'selected' : ''}>Set</option>
|
|
<option value="SCROLL" ${event.type === 'SCROLL' ? 'selected' : ''}>Scroll</option>
|
|
<option value="WAIT" ${event.type === 'WAIT' ? 'selected' : ''}>Wait</option>
|
|
</select>
|
|
</div>
|
|
<div class="c4ai-edit-field">
|
|
<label>Selector/Value:</label>
|
|
<input type="text" id="c4ai-edit-value" value="${event.selector || event.value || ''}">
|
|
</div>
|
|
<div class="c4ai-edit-actions">
|
|
<button id="c4ai-edit-save">Save</button>
|
|
<button id="c4ai-edit-cancel">Cancel</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="c4ai-saved-flows-content">
|
|
<div class="c4ai-saved-flows-header">
|
|
<h2>Saved Flows for ${domain}</h2>
|
|
<button class="c4ai-close-modal" id="c4ai-close-flows">✕</button>
|
|
</div>
|
|
<div class="c4ai-saved-flows-body">
|
|
${flows.length === 0 ?
|
|
'<p class="c4ai-no-flows">No saved flows for this domain yet. Record and save your first flow!</p>' :
|
|
`<div class="c4ai-flows-list">
|
|
${flows.map(flow => `
|
|
<div class="c4ai-flow-item" data-flow-id="${flow.id}">
|
|
<div class="c4ai-flow-info">
|
|
<h3>${flow.name}</h3>
|
|
<p class="c4ai-flow-meta">
|
|
<span>${flow.events.length} actions</span>
|
|
<span>•</span>
|
|
<span>${new Date(flow.createdAt).toLocaleDateString()}</span>
|
|
</p>
|
|
</div>
|
|
<div class="c4ai-flow-actions">
|
|
<button class="c4ai-action-btn c4ai-load-flow-btn" data-flow-id="${flow.id}">
|
|
<span>▶</span> Load
|
|
</button>
|
|
<button class="c4ai-action-btn c4ai-delete-flow-btn" data-flow-id="${flow.id}">
|
|
<span>🗑</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>`
|
|
}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="c4ai-toolbar-status">
|
|
<div class="c4ai-status-item">
|
|
<span class="c4ai-status-label">Actions:</span>
|
|
<span class="c4ai-status-value" id="c4ai-action-count">0</span>
|
|
</div>
|
|
<div class="c4ai-status-item">
|
|
<span class="c4ai-status-label">Format:</span>
|
|
<select id="c4ai-output-format" class="c4ai-format-select">
|
|
<option value="js">JavaScript</option>
|
|
<option value="c4a">C4A Script</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="c4ai-toolbar-hint" id="c4ai-script-hint">
|
|
Recording your actions... Click, type, and scroll to build your script.
|
|
</div>
|
|
<div class="c4ai-toolbar-actions">
|
|
<button id="c4ai-saved-flows" class="c4ai-action-btn c4ai-flows-btn">
|
|
<span>📂</span> Saved Flows
|
|
</button>
|
|
<button id="c4ai-add-wait" class="c4ai-action-btn c4ai-wait-btn">
|
|
<span class="c4ai-wait-icon">⏱</span> Add Wait
|
|
</button>
|
|
<button id="c4ai-pause-recording" class="c4ai-action-btn c4ai-pause-btn">
|
|
<span class="c4ai-pause-icon">⏸</span> Pause
|
|
</button>
|
|
<button id="c4ai-stop-generate" class="c4ai-action-btn c4ai-generate-btn">
|
|
<span class="c4ai-generate-icon">⚡</span> Stop & Generate
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
// 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.extraction_strategy 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;
|
|
}); |