Add C4A-Script support and documentation

- Generate OneShot js code geenrator
- Introduced a new C4A-Script tutorial example for login flow using Blockly.
- Updated index.html to include Blockly theme and event editor modal for script editing.
- Created a test HTML file for testing Blockly integration.
- Added comprehensive C4A-Script API reference documentation covering commands, syntax, and examples.
- Developed core documentation for C4A-Script, detailing its features, commands, and real-world examples.
- Updated mkdocs.yml to include new C4A-Script documentation in navigation.
This commit is contained in:
UncleCode
2025-06-07 23:07:19 +08:00
parent ca03acbc82
commit 08a2cdae53
46 changed files with 6914 additions and 326 deletions

View File

@@ -1,17 +1,37 @@
# C4A-Script Interactive Tutorial
Welcome to the C4A-Script Interactive Tutorial! This hands-on tutorial teaches you how to write web automation scripts using C4A-Script, a domain-specific language for Crawl4AI.
A comprehensive web-based tutorial for learning and experimenting with C4A-Script - Crawl4AI's visual web automation language.
## 🚀 Quick Start
### 1. Start the Tutorial Server
### Prerequisites
- Python 3.7+
- Modern web browser (Chrome, Firefox, Safari, Edge)
```bash
cd docs/examples/c4a_script/tutorial
python server.py
```
### Running the Tutorial
Then open your browser to: http://localhost:8080
1. **Clone and Navigate**
```bash
git clone https://github.com/unclecode/crawl4ai.git
cd crawl4ai/docs/examples/c4a_script/tutorial/
```
2. **Install Dependencies**
```bash
pip install flask
```
3. **Launch the Server**
```bash
python server.py
```
4. **Open in Browser**
```
http://localhost:8080
```
**🌐 Try Online**: [Live Demo](https://docs.crawl4ai.com/c4a-script/demo)
### 2. Try Your First Script
@@ -23,7 +43,16 @@ IF (EXISTS `.cookie-banner`) THEN CLICK `.accept`
CLICK `#start-tutorial`
```
## 📚 What You'll Learn
## 🎯 What You'll Learn
### Core Features
- **📝 Text Editor**: Write C4A-Script with syntax highlighting
- **🧩 Visual Editor**: Build scripts using drag-and-drop Blockly interface
- **🎬 Recording Mode**: Capture browser actions and auto-generate scripts
- **⚡ Live Execution**: Run scripts in real-time with instant feedback
- **📊 Timeline View**: Visualize and edit automation steps
## 📚 Tutorial Content
### Basic Commands
- **Navigation**: `GO url`
@@ -237,10 +266,131 @@ Check the `scripts/` folder for complete examples:
- `04-multi-step-form.c4a` - Complex forms
- `05-complex-workflow.c4a` - Full automation
## 🏗️ Developer Guide
### Project Architecture
```
tutorial/
├── server.py # Flask application server
├── assets/ # Tutorial-specific assets
│ ├── app.js # Main application logic
│ ├── c4a-blocks.js # Custom Blockly blocks
│ ├── c4a-generator.js # Code generation
│ ├── blockly-manager.js # Blockly integration
│ └── styles.css # Main styling
├── playground/ # Interactive demo environment
│ ├── index.html # Demo web application
│ ├── app.js # Demo app logic
│ └── styles.css # Demo styling
├── scripts/ # Example C4A scripts
└── index.html # Main tutorial interface
```
### Key Components
#### 1. TutorialApp (`assets/app.js`)
Main application controller managing:
- Code editor integration (CodeMirror)
- Script execution and browser preview
- Tutorial navigation and lessons
- State management and persistence
#### 2. BlocklyManager (`assets/blockly-manager.js`)
Visual programming interface:
- Custom C4A-Script block definitions
- Bidirectional sync between visual blocks and text
- Real-time code generation
- Dark theme integration
#### 3. Recording System
Powers the recording functionality:
- Browser event capture
- Smart event grouping and filtering
- Automatic C4A-Script generation
- Timeline visualization
### Customization
#### Adding New Commands
1. **Define Block** (`assets/c4a-blocks.js`)
2. **Add Generator** (`assets/c4a-generator.js`)
3. **Update Parser** (`assets/blockly-manager.js`)
#### Themes and Styling
- Main styles: `assets/styles.css`
- Theme variables: CSS custom properties
- Dark mode: Auto-applied based on system preference
### Configuration
```python
# server.py configuration
PORT = 8080
DEBUG = True
THREADED = True
```
### API Endpoints
- `GET /` - Main tutorial interface
- `GET /playground/` - Interactive demo environment
- `POST /execute` - Script execution endpoint
- `GET /examples/<script>` - Load example scripts
## 🔧 Troubleshooting
### Common Issues
**Port Already in Use**
```bash
# Kill existing process
lsof -ti:8080 | xargs kill -9
# Or use different port
python server.py --port 8081
```
**Blockly Not Loading**
- Check browser console for JavaScript errors
- Verify all static files are served correctly
- Ensure proper script loading order
**Recording Issues**
- Verify iframe permissions
- Check cross-origin communication
- Ensure event listeners are attached
### Debug Mode
Enable detailed logging by setting `DEBUG = True` in `assets/app.js`
## 📚 Additional Resources
- **[C4A-Script Documentation](../../md_v2/core/c4a-script.md)** - Complete language guide
- **[API Reference](../../md_v2/api/c4a-script-reference.md)** - Detailed command documentation
- **[Live Demo](https://docs.crawl4ai.com/c4a-script/demo)** - Try without installation
- **[Example Scripts](../)** - More automation examples
## 🤝 Contributing
Found a bug or have a suggestion? Please open an issue on GitHub!
### Bug Reports
1. Check existing issues on GitHub
2. Provide minimal reproduction steps
3. Include browser and system information
4. Add relevant console logs
### Feature Requests
1. Fork the repository
2. Create feature branch: `git checkout -b feature/my-feature`
3. Test thoroughly with different browsers
4. Update documentation
5. Submit pull request
### Code Style
- Use consistent indentation (2 spaces for JS, 4 for Python)
- Add comments for complex logic
- Follow existing naming conventions
- Test with multiple browsers
---
Happy automating with C4A-Script! 🎉
**Happy Automating!** 🎉
Need help? Check our [documentation](https://docs.crawl4ai.com) or open an issue on [GitHub](https://github.com/unclecode/crawl4ai).

View File

@@ -664,4 +664,243 @@ body {
.output-section {
height: 200px;
}
}
/* ================================================================
Recording Timeline Styles
================================================================ */
.action-btn.record {
background: var(--bg-tertiary);
border-color: var(--error-color);
}
.action-btn.record:hover {
background: var(--error-color);
border-color: var(--error-color);
}
.action-btn.record.recording {
background: var(--error-color);
animation: pulse 1.5s infinite;
}
.action-btn.record.recording .icon {
animation: blink 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.editor-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
#editor-view,
#timeline-view {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.recording-timeline {
background: var(--bg-secondary);
display: flex;
flex-direction: column;
height: 100%;
}
.timeline-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
border-bottom: 1px solid var(--border-color);
background: var(--bg-tertiary);
}
.timeline-header h3 {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.timeline-actions {
display: flex;
gap: 8px;
}
.timeline-events {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.timeline-event {
display: flex;
align-items: center;
padding: 8px 10px;
margin-bottom: 6px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 4px;
transition: all 0.2s;
cursor: pointer;
}
.timeline-event:hover {
border-color: var(--border-hover);
background: var(--code-bg);
}
.timeline-event.selected {
border-color: var(--primary-color);
background: rgba(15, 187, 170, 0.1);
}
.event-checkbox {
margin-right: 10px;
width: 16px;
height: 16px;
cursor: pointer;
}
.event-time {
font-size: 11px;
color: var(--text-muted);
margin-right: 10px;
font-family: 'Dank Mono', monospace;
min-width: 45px;
}
.event-command {
flex: 1;
font-family: 'Dank Mono', monospace;
font-size: 13px;
color: var(--text-primary);
}
.event-command .cmd-name {
color: var(--primary-color);
font-weight: 600;
}
.event-command .cmd-selector {
color: var(--info-color);
}
.event-command .cmd-value {
color: var(--warning-color);
}
.event-command .cmd-detail {
color: var(--text-secondary);
font-size: 11px;
margin-left: 5px;
}
.event-edit {
margin-left: 10px;
padding: 2px 8px;
font-size: 11px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
color: var(--text-secondary);
cursor: pointer;
border-radius: 3px;
transition: all 0.2s;
}
.event-edit:hover {
border-color: var(--primary-color);
color: var(--primary-color);
}
/* Event Editor Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--modal-overlay);
z-index: 999;
}
.event-editor-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
z-index: 1000;
min-width: 400px;
}
.event-editor-modal h4 {
margin: 0 0 15px 0;
color: var(--text-primary);
font-family: 'Dank Mono', monospace;
}
.editor-field {
margin-bottom: 15px;
}
.editor-field label {
display: block;
margin-bottom: 5px;
font-size: 12px;
color: var(--text-secondary);
font-family: 'Dank Mono', monospace;
}
.editor-field input,
.editor-field select {
width: 100%;
padding: 8px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-primary);
border-radius: 4px;
font-family: 'Dank Mono', monospace;
font-size: 13px;
}
.editor-field input:focus,
.editor-field select:focus {
outline: none;
border-color: var(--primary-color);
}
.editor-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
/* Blockly Button */
#blockly-btn .icon {
font-size: 16px;
}
/* Hidden State */
.hidden {
display: none !important;
}

View File

@@ -8,6 +8,8 @@ class TutorialApp {
this.tutorialMode = false;
this.currentStep = 0;
this.tutorialSteps = [];
this.recordingManager = null;
this.blocklyManager = null;
this.init();
}
@@ -18,6 +20,12 @@ class TutorialApp {
this.setupTabs();
this.setupTutorial();
this.checkFirstVisit();
// Initialize recording manager
this.recordingManager = new RecordingManager(this);
// Initialize Blockly manager
this.blocklyManager = new BlocklyManager(this);
}
setupEditors() {
@@ -618,6 +626,858 @@ style.textContent = `
`;
document.head.appendChild(style);
// Recording Manager Class
class RecordingManager {
constructor(tutorialApp) {
this.app = tutorialApp;
this.isRecording = false;
this.rawEvents = [];
this.groupedEvents = [];
this.startTime = 0;
this.lastEventTime = 0;
this.eventInjected = false;
this.keyBuffer = [];
this.keyBufferTimeout = null;
this.scrollAccumulator = { direction: null, amount: 0, startTime: 0 };
this.processedEventIndices = new Set(); // Track which raw events have been processed
this.init();
}
init() {
this.setupUI();
this.setupMessageHandler();
}
setupUI() {
// Record button
const recordBtn = document.getElementById('record-btn');
recordBtn.addEventListener('click', () => this.toggleRecording());
// Timeline button
const timelineBtn = document.getElementById('timeline-btn');
timelineBtn?.addEventListener('click', () => this.showTimeline());
// Back to editor button
document.getElementById('back-to-editor')?.addEventListener('click', () => this.hideTimeline());
// Timeline controls
document.getElementById('select-all-events')?.addEventListener('click', () => this.selectAllEvents());
document.getElementById('clear-events')?.addEventListener('click', () => this.clearEvents());
document.getElementById('generate-script')?.addEventListener('click', () => this.generateScript());
}
setupMessageHandler() {
window.addEventListener('message', (event) => {
if (event.data.type === 'c4a-recording-event' && this.isRecording) {
this.handleRecordedEvent(event.data.event);
}
});
}
toggleRecording() {
const recordBtn = document.getElementById('record-btn');
const timelineBtn = document.getElementById('timeline-btn');
if (!this.isRecording) {
// Start recording
this.isRecording = true;
this.startTime = Date.now();
this.lastEventTime = this.startTime;
this.rawEvents = [];
this.groupedEvents = [];
this.processedEventIndices = new Set();
this.keyBuffer = [];
this.scrollAccumulator = { direction: null, amount: 0, startTime: 0 };
recordBtn.classList.add('recording');
recordBtn.innerHTML = '<span class="icon">⏹</span>Stop';
// Show timeline immediately when recording starts
timelineBtn.classList.remove('hidden');
this.showTimeline();
this.injectEventCapture();
this.app.addConsoleMessage('🔴 Recording started...', 'info');
} else {
// Stop recording
this.isRecording = false;
recordBtn.classList.remove('recording');
recordBtn.innerHTML = '<span class="icon">⏺</span>Record';
this.removeEventCapture();
this.processEvents();
this.app.addConsoleMessage('⏹️ Recording stopped', 'info');
}
}
showTimeline() {
const editorView = document.getElementById('editor-view');
const timelineView = document.getElementById('timeline-view');
editorView.classList.add('hidden');
timelineView.classList.remove('hidden');
// Refresh CodeMirror when switching back
this.editorNeedsRefresh = true;
}
hideTimeline() {
const editorView = document.getElementById('editor-view');
const timelineView = document.getElementById('timeline-view');
timelineView.classList.add('hidden');
editorView.classList.remove('hidden');
// Refresh CodeMirror after switching
if (this.editorNeedsRefresh) {
setTimeout(() => this.app.editor.refresh(), 100);
this.editorNeedsRefresh = false;
}
}
injectEventCapture() {
const iframe = document.getElementById('playground-frame');
const script = `
(function() {
if (window.__c4aRecordingActive) return;
window.__c4aRecordingActive = true;
const captureEvent = (type, event) => {
const data = {
type: type,
timestamp: Date.now(),
targetTag: event.target.tagName,
targetId: event.target.id,
targetClass: event.target.className,
targetSelector: generateSelector(event.target),
targetType: event.target.type // For input elements
};
// Add type-specific data
switch(type) {
case 'click':
case 'dblclick':
case 'contextmenu':
data.x = event.clientX;
data.y = event.clientY;
break;
case 'keydown':
case 'keyup':
data.key = event.key;
data.code = event.code;
data.ctrlKey = event.ctrlKey;
data.shiftKey = event.shiftKey;
data.altKey = event.altKey;
break;
case 'input':
case 'change':
data.value = event.target.value;
data.inputType = event.inputType;
// For checkboxes and radio buttons, also capture checked state
if (event.target.type === 'checkbox' || event.target.type === 'radio') {
data.checked = event.target.checked;
}
// For select elements, capture selected text
if (event.target.tagName === 'SELECT') {
data.selectedText = event.target.options[event.target.selectedIndex]?.text || '';
}
break;
case 'scroll':
case 'wheel':
data.scrollTop = window.scrollY;
data.scrollLeft = window.scrollX;
data.deltaY = event.deltaY || 0;
data.deltaX = event.deltaX || 0;
break;
case 'focus':
case 'blur':
data.value = event.target.value || '';
break;
}
window.parent.postMessage({
type: 'c4a-recording-event',
event: data
}, '*');
};
const generateSelector = (element) => {
try {
if (element.id) return '#' + element.id;
if (element.className && typeof element.className === 'string') {
const classes = element.className.trim().split(/\\s+/);
if (classes.length > 0 && classes[0]) {
return '.' + classes[0];
}
}
// Generate nth-child selector
let path = [];
let currentElement = element;
while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {
let selector = currentElement.nodeName.toLowerCase();
if (currentElement.id) {
selector = '#' + currentElement.id;
path.unshift(selector);
break;
} else {
let sibling = currentElement;
let nth = 1;
while (sibling.previousElementSibling) {
sibling = sibling.previousElementSibling;
if (sibling.nodeName === currentElement.nodeName) nth++;
}
if (nth > 1) selector += ':nth-child(' + nth + ')';
}
path.unshift(selector);
currentElement = currentElement.parentNode;
}
return path.join(' > ') || element.nodeName.toLowerCase();
} catch (e) {
return element.nodeName.toLowerCase();
}
};
// Store event handlers for cleanup
window.__c4aEventHandlers = {};
// Attach event listeners
const events = ['click', 'dblclick', 'contextmenu', 'keydown', 'keyup',
'input', 'change', 'scroll', 'wheel', 'focus', 'blur'];
events.forEach(eventType => {
const handler = (e) => captureEvent(eventType, e);
window.__c4aEventHandlers[eventType] = handler;
document.addEventListener(eventType, handler, true);
});
// Cleanup function
window.__c4aCleanupRecording = () => {
events.forEach(eventType => {
const handler = window.__c4aEventHandlers[eventType];
if (handler) {
document.removeEventListener(eventType, handler, true);
}
});
delete window.__c4aRecordingActive;
delete window.__c4aCleanupRecording;
delete window.__c4aEventHandlers;
};
})();
`;
const scriptEl = iframe.contentDocument.createElement('script');
scriptEl.textContent = script;
iframe.contentDocument.body.appendChild(scriptEl);
scriptEl.remove();
this.eventInjected = true;
}
removeEventCapture() {
if (!this.eventInjected) return;
const iframe = document.getElementById('playground-frame');
iframe.contentWindow.eval('if (window.__c4aCleanupRecording) window.__c4aCleanupRecording();');
this.eventInjected = false;
}
handleRecordedEvent(event) {
const now = Date.now();
const timeSinceStart = ((now - this.startTime) / 1000).toFixed(1);
// Add time since last event
event.timeSinceStart = timeSinceStart;
event.timeSinceLast = now - this.lastEventTime;
this.lastEventTime = now;
this.rawEvents.push(event);
// Real-time processing for immediate feedback
if (event.type === 'keydown' && this.shouldGroupKeystrokes(event)) {
this.keyBuffer.push(event);
// Clear existing timeout
if (this.keyBufferTimeout) {
clearTimeout(this.keyBufferTimeout);
}
// Set timeout to flush buffer after 500ms of no typing
this.keyBufferTimeout = setTimeout(() => {
this.flushKeyBuffer();
this.updateTimeline();
}, 500);
} else {
// Handle change events for select, checkbox, radio
if (event.type === 'change') {
const tagName = event.targetTag?.toLowerCase();
// Only skip change events for text inputs (they're part of typing)
if (tagName === 'input' &&
event.targetType !== 'checkbox' &&
event.targetType !== 'radio') {
return; // Skip text input change events
}
// For select, checkbox, radio - process the change event
if (tagName === 'select' ||
(tagName === 'input' && (event.targetType === 'checkbox' || event.targetType === 'radio'))) {
// Flush any pending keystrokes first
if (this.keyBuffer.length > 0) {
this.flushKeyBuffer();
this.updateTimeline();
}
// Create SET command for the value change
const command = this.eventToCommand(event, this.rawEvents.length - 1);
if (command) {
this.groupedEvents.push(command);
this.updateTimeline();
}
return;
}
}
// Skip input events - they're part of typing
if (event.type === 'input') {
return;
}
// Clear timeout if exists
if (this.keyBufferTimeout) {
clearTimeout(this.keyBufferTimeout);
this.keyBufferTimeout = null;
}
// Flush key buffer only for significant events
const shouldFlushBuffer = event.type === 'click' ||
event.type === 'dblclick' ||
event.type === 'contextmenu' ||
event.type === 'scroll' ||
event.type === 'wheel';
const hadKeyBuffer = this.keyBuffer.length > 0;
if (shouldFlushBuffer && hadKeyBuffer) {
this.flushKeyBuffer();
this.updateTimeline();
}
// Process this event immediately if it's not a typing-related event
if (event.type !== 'keydown' && event.type !== 'keyup' &&
event.type !== 'input' && event.type !== 'change' &&
event.type !== 'focus' && event.type !== 'blur') {
const command = this.eventToCommand(event, this.rawEvents.length - 1);
if (command) {
// Check if it's a scroll event that should be accumulated
if (command.type === 'SCROLL') {
// Remove previous scroll events in the same direction
this.groupedEvents = this.groupedEvents.filter(e =>
!(e.type === 'SCROLL' && e.direction === command.direction &&
parseFloat(e.time) > parseFloat(command.time) - 0.5)
);
}
this.groupedEvents.push(command);
this.updateTimeline();
}
}
}
}
shouldGroupKeystrokes(event) {
// Skip if no key
if (!event.key) return false;
// Group printable characters, space, and common typing keys
return (
event.key.length === 1 || // Single characters
event.key === ' ' || // Space
event.key === 'Enter' || // Enter key
event.key === 'Tab' || // Tab key
event.key === 'Backspace' || // Backspace
event.key === 'Delete' // Delete
);
}
flushKeyBuffer() {
if (this.keyBuffer.length === 0) return;
// Build the text, handling special keys
const text = this.keyBuffer.map(e => {
switch(e.key) {
case ' ': return ' ';
case 'Enter': return '\n';
case 'Tab': return '\t';
case 'Backspace': return ''; // Skip backspace in final text
case 'Delete': return ''; // Skip delete in final text
default: return e.key;
}
}).join('');
// Don't create empty TYPE commands
if (text.length === 0) {
this.keyBuffer = [];
return;
}
const firstEvent = this.keyBuffer[0];
const lastEvent = this.keyBuffer[this.keyBuffer.length - 1];
// Mark all keystroke events as processed
this.keyBuffer.forEach(event => {
const index = this.rawEvents.indexOf(event);
if (index !== -1) {
this.processedEventIndices.add(index);
}
});
// Check if this should be a SET command instead of TYPE
// Look for a click event just before the first keystroke
const firstKeystrokeIndex = this.rawEvents.indexOf(firstEvent);
let commandType = 'TYPE';
if (firstKeystrokeIndex > 0) {
const prevEvent = this.rawEvents[firstKeystrokeIndex - 1];
if (prevEvent && prevEvent.type === 'click' &&
prevEvent.targetSelector === firstEvent.targetSelector) {
// This looks like a SET pattern
commandType = 'SET';
// Mark the click event as processed too
this.processedEventIndices.add(firstKeystrokeIndex - 1);
}
}
// Check if we already have a TYPE command for this exact text at this time
// This prevents duplicates when the buffer is flushed multiple times
const existingCommand = this.groupedEvents.find(cmd =>
cmd.type === commandType &&
cmd.value === text &&
cmd.time === firstEvent.timeSinceStart
);
if (!existingCommand) {
this.groupedEvents.push({
type: commandType,
selector: firstEvent.targetSelector,
value: text,
time: firstEvent.timeSinceStart,
duration: lastEvent.timestamp - firstEvent.timestamp,
raw: [...this.keyBuffer] // Make a copy to avoid reference issues
});
}
this.keyBuffer = [];
}
processEvents() {
// Clear any pending timeouts
if (this.keyBufferTimeout) {
clearTimeout(this.keyBufferTimeout);
this.keyBufferTimeout = null;
}
// Flush any remaining buffers
this.flushKeyBuffer();
// Don't reprocess events that were already grouped during recording
// Just apply final optimizations
this.optimizeEvents();
this.updateTimeline();
}
eventToCommand(event, index) {
// Skip already processed events
if (this.processedEventIndices.has(index)) {
return null;
}
// Skip events that should only be processed as grouped commands
if (event.type === 'keydown' || event.type === 'keyup' ||
event.type === 'input' || event.type === 'focus' || event.type === 'blur') {
return null;
}
// Allow change events for select, checkbox, radio
if (event.type === 'change') {
const tagName = event.targetTag?.toLowerCase();
if (tagName === 'select' ||
(tagName === 'input' && (event.targetType === 'checkbox' || event.targetType === 'radio'))) {
// Process as SET command
} else {
return null; // Skip change events for text inputs
}
}
switch (event.type) {
case 'click':
// Check if followed by input focus or change event
const nextEvent = this.rawEvents[index + 1];
if (nextEvent && nextEvent.targetSelector === event.targetSelector) {
if (nextEvent.type === 'focus' || nextEvent.type === 'change') {
return null; // Skip, will be handled by SET or change event
}
}
// Also check if this is a click on a select element
if (event.targetTag?.toLowerCase() === 'select') {
// Look ahead for a change event
for (let i = index + 1; i < Math.min(index + 5, this.rawEvents.length); i++) {
if (this.rawEvents[i].type === 'change' &&
this.rawEvents[i].targetSelector === event.targetSelector) {
return null; // Skip click, change event will handle it
}
}
}
return {
type: 'CLICK',
selector: event.targetSelector,
time: event.timeSinceStart
};
case 'dblclick':
return {
type: 'DOUBLE_CLICK',
selector: event.targetSelector,
time: event.timeSinceStart
};
case 'contextmenu':
return {
type: 'RIGHT_CLICK',
selector: event.targetSelector,
time: event.timeSinceStart
};
case 'scroll':
case 'wheel':
// Accumulate scroll events
if (event.deltaY !== 0) {
const direction = event.deltaY > 0 ? 'DOWN' : 'UP';
const amount = Math.abs(event.deltaY);
if (this.scrollAccumulator.direction === direction &&
event.timestamp - this.scrollAccumulator.startTime < 500) {
this.scrollAccumulator.amount += amount;
} else {
this.scrollAccumulator = { direction, amount, startTime: event.timestamp };
}
// Return accumulated scroll at end of sequence
const nextEvent = this.rawEvents[index + 1];
if (!nextEvent || nextEvent.type !== 'scroll' ||
nextEvent.timestamp - event.timestamp > 500) {
return {
type: 'SCROLL',
direction: this.scrollAccumulator.direction,
amount: Math.round(this.scrollAccumulator.amount),
time: event.timeSinceStart
};
}
}
return null;
// Input events are handled through keystroke grouping
case 'input':
return null;
case 'change':
// Handle select, checkbox, radio changes
const tagName = event.targetTag?.toLowerCase();
if (tagName === 'select') {
return {
type: 'SET',
selector: event.targetSelector,
value: event.value,
displayValue: event.selectedText || event.value,
time: event.timeSinceStart
};
} else if (tagName === 'input' && event.targetType === 'checkbox') {
return {
type: 'SET',
selector: event.targetSelector,
value: event.checked ? 'checked' : 'unchecked',
time: event.timeSinceStart
};
} else if (tagName === 'input' && event.targetType === 'radio') {
return {
type: 'SET',
selector: event.targetSelector,
value: 'checked',
time: event.timeSinceStart
};
}
return null;
}
return null;
}
optimizeEvents() {
const optimized = [];
let lastTime = 0;
this.groupedEvents.forEach((event, index) => {
// Insert WAIT if pause > 1 second
const currentTime = parseFloat(event.time);
if (currentTime - lastTime > 1) {
optimized.push({
type: 'WAIT',
value: Math.round(currentTime - lastTime),
time: lastTime.toFixed(1)
});
}
optimized.push(event);
lastTime = currentTime;
});
this.groupedEvents = optimized;
}
updateTimeline() {
const timeline = document.getElementById('timeline-events');
timeline.innerHTML = '';
this.groupedEvents.forEach((event, index) => {
const eventEl = this.createEventElement(event, index);
timeline.appendChild(eventEl);
});
}
createEventElement(event, index) {
const div = document.createElement('div');
div.className = 'timeline-event';
div.dataset.index = index;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'event-checkbox';
checkbox.checked = true;
checkbox.addEventListener('change', () => {
div.classList.toggle('selected', checkbox.checked);
});
const time = document.createElement('span');
time.className = 'event-time';
time.textContent = event.time + 's';
const command = document.createElement('span');
command.className = 'event-command';
command.innerHTML = this.formatCommand(event);
const editBtn = document.createElement('button');
editBtn.className = 'event-edit';
editBtn.textContent = 'Edit';
editBtn.addEventListener('click', () => this.editEvent(index));
div.appendChild(checkbox);
div.appendChild(time);
div.appendChild(command);
div.appendChild(editBtn);
// Initially selected
div.classList.add('selected');
return div;
}
formatCommand(event) {
switch (event.type) {
case 'CLICK':
return `<span class="cmd-name">CLICK</span> <span class="cmd-selector">\`${event.selector}\`</span>`;
case 'DOUBLE_CLICK':
return `<span class="cmd-name">DOUBLE_CLICK</span> <span class="cmd-selector">\`${event.selector}\`</span>`;
case 'RIGHT_CLICK':
return `<span class="cmd-name">RIGHT_CLICK</span> <span class="cmd-selector">\`${event.selector}\`</span>`;
case 'TYPE':
return `<span class="cmd-name">TYPE</span> <span class="cmd-value">"${event.value}"</span> <span class="cmd-detail">(${event.value.length} chars)</span>`;
case 'SET':
// Use displayValue if available (for select elements)
const displayText = event.displayValue || event.value;
return `<span class="cmd-name">SET</span> <span class="cmd-selector">\`${event.selector}\`</span> <span class="cmd-value">"${displayText}"</span>`;
case 'SCROLL':
return `<span class="cmd-name">SCROLL</span> <span class="cmd-value">${event.direction} ${event.amount}</span>`;
case 'WAIT':
return `<span class="cmd-name">WAIT</span> <span class="cmd-value">${event.value}</span>`;
default:
return `<span class="cmd-name">${event.type}</span>`;
}
}
editEvent(index) {
const event = this.groupedEvents[index];
this.currentEditIndex = index;
// Show modal
const overlay = document.getElementById('event-editor-overlay');
const modal = document.getElementById('event-editor-modal');
overlay.classList.remove('hidden');
modal.classList.remove('hidden');
// Populate fields
document.getElementById('edit-command-type').value = event.type;
// Show/hide fields based on command type
const selectorField = document.getElementById('edit-selector-field');
const valueField = document.getElementById('edit-value-field');
const directionField = document.getElementById('edit-direction-field');
selectorField.classList.add('hidden');
valueField.classList.add('hidden');
directionField.classList.add('hidden');
switch (event.type) {
case 'CLICK':
case 'DOUBLE_CLICK':
case 'RIGHT_CLICK':
selectorField.classList.remove('hidden');
document.getElementById('edit-selector').value = event.selector;
break;
case 'TYPE':
valueField.classList.remove('hidden');
document.getElementById('edit-value').value = event.value;
break;
case 'SET':
selectorField.classList.remove('hidden');
valueField.classList.remove('hidden');
document.getElementById('edit-selector').value = event.selector;
document.getElementById('edit-value').value = event.value;
break;
case 'SCROLL':
directionField.classList.remove('hidden');
valueField.classList.remove('hidden');
document.getElementById('edit-direction').value = event.direction;
document.getElementById('edit-value').value = event.amount;
break;
case 'WAIT':
valueField.classList.remove('hidden');
document.getElementById('edit-value').value = event.value;
break;
}
// Setup event handlers
this.setupEditModalHandlers();
}
setupEditModalHandlers() {
const overlay = document.getElementById('event-editor-overlay');
const modal = document.getElementById('event-editor-modal');
const cancelBtn = document.getElementById('edit-cancel');
const saveBtn = document.getElementById('edit-save');
const closeModal = () => {
overlay.classList.add('hidden');
modal.classList.add('hidden');
};
const saveHandler = () => {
const event = this.groupedEvents[this.currentEditIndex];
// Update event based on type
switch (event.type) {
case 'CLICK':
case 'DOUBLE_CLICK':
case 'RIGHT_CLICK':
event.selector = document.getElementById('edit-selector').value;
break;
case 'TYPE':
event.value = document.getElementById('edit-value').value;
break;
case 'SET':
event.selector = document.getElementById('edit-selector').value;
event.value = document.getElementById('edit-value').value;
break;
case 'SCROLL':
event.direction = document.getElementById('edit-direction').value;
event.amount = parseInt(document.getElementById('edit-value').value) || 0;
break;
case 'WAIT':
event.value = parseInt(document.getElementById('edit-value').value) || 0;
break;
}
// Update timeline
this.updateTimeline();
closeModal();
};
// Clean up old handlers
cancelBtn.replaceWith(cancelBtn.cloneNode(true));
saveBtn.replaceWith(saveBtn.cloneNode(true));
overlay.replaceWith(overlay.cloneNode(true));
// Add new handlers
document.getElementById('edit-cancel').addEventListener('click', closeModal);
document.getElementById('edit-save').addEventListener('click', saveHandler);
document.getElementById('event-editor-overlay').addEventListener('click', closeModal);
}
selectAllEvents() {
const checkboxes = document.querySelectorAll('.event-checkbox');
const events = document.querySelectorAll('.timeline-event');
checkboxes.forEach((cb, i) => {
cb.checked = true;
events[i].classList.add('selected');
});
}
clearEvents() {
this.groupedEvents = [];
this.updateTimeline();
}
generateScript() {
const selectedEvents = [];
const checkboxes = document.querySelectorAll('.event-checkbox');
checkboxes.forEach((cb, index) => {
if (cb.checked && this.groupedEvents[index]) {
selectedEvents.push(this.groupedEvents[index]);
}
});
if (selectedEvents.length === 0) {
this.app.addConsoleMessage('No events selected', 'warning');
return;
}
const script = selectedEvents.map(event => this.eventToC4A(event)).join('\n');
// Set the script in the editor
this.app.editor.setValue(script);
this.app.addConsoleMessage(`Generated ${selectedEvents.length} commands`, 'success');
// Switch back to editor view
this.hideTimeline();
}
eventToC4A(event) {
switch (event.type) {
case 'CLICK':
return `CLICK \`${event.selector}\``;
case 'DOUBLE_CLICK':
return `DOUBLE_CLICK \`${event.selector}\``;
case 'RIGHT_CLICK':
return `RIGHT_CLICK \`${event.selector}\``;
case 'TYPE':
return `TYPE "${event.value}"`;
case 'SET':
return `SET \`${event.selector}\` "${event.value}"`;
case 'SCROLL':
return `SCROLL ${event.direction} ${event.amount}`;
case 'WAIT':
return `WAIT ${event.value}`;
default:
return `# Unknown: ${event.type}`;
}
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.tutorialApp = new TutorialApp();

View File

@@ -0,0 +1,591 @@
// Blockly Manager for C4A-Script
// Handles Blockly workspace, code generation, and synchronization with text editor
class BlocklyManager {
constructor(tutorialApp) {
this.app = tutorialApp;
this.workspace = null;
this.isUpdating = false; // Prevent circular updates
this.blocklyVisible = false;
this.toolboxXml = this.generateToolbox();
this.init();
}
init() {
this.setupBlocklyContainer();
this.initializeWorkspace();
this.setupEventHandlers();
this.setupSynchronization();
}
setupBlocklyContainer() {
// Create blockly container div
const editorContainer = document.querySelector('.editor-container');
const blocklyDiv = document.createElement('div');
blocklyDiv.id = 'blockly-view';
blocklyDiv.className = 'blockly-workspace hidden';
blocklyDiv.style.height = '100%';
blocklyDiv.style.width = '100%';
editorContainer.appendChild(blocklyDiv);
}
generateToolbox() {
return `
<xml id="toolbox" style="display: none">
<category name="Navigation" colour="${BlockColors.NAVIGATION}">
<block type="c4a_go"></block>
<block type="c4a_reload"></block>
<block type="c4a_back"></block>
<block type="c4a_forward"></block>
</category>
<category name="Wait" colour="${BlockColors.WAIT}">
<block type="c4a_wait_time">
<field name="SECONDS">3</field>
</block>
<block type="c4a_wait_selector">
<field name="SELECTOR">#content</field>
<field name="TIMEOUT">10</field>
</block>
<block type="c4a_wait_text">
<field name="TEXT">Loading complete</field>
<field name="TIMEOUT">5</field>
</block>
</category>
<category name="Mouse Actions" colour="${BlockColors.ACTIONS}">
<block type="c4a_click">
<field name="SELECTOR">button.submit</field>
</block>
<block type="c4a_click_xy"></block>
<block type="c4a_double_click"></block>
<block type="c4a_right_click"></block>
<block type="c4a_move"></block>
<block type="c4a_drag"></block>
<block type="c4a_scroll">
<field name="DIRECTION">DOWN</field>
<field name="AMOUNT">500</field>
</block>
</category>
<category name="Keyboard" colour="${BlockColors.KEYBOARD}">
<block type="c4a_type">
<field name="TEXT">hello@example.com</field>
</block>
<block type="c4a_type_var">
<field name="VAR">email</field>
</block>
<block type="c4a_clear"></block>
<block type="c4a_set">
<field name="SELECTOR">#email</field>
<field name="VALUE">user@example.com</field>
</block>
<block type="c4a_press">
<field name="KEY">Tab</field>
</block>
<block type="c4a_key_down">
<field name="KEY">Shift</field>
</block>
<block type="c4a_key_up">
<field name="KEY">Shift</field>
</block>
</category>
<category name="Control Flow" colour="${BlockColors.CONTROL}">
<block type="c4a_if_exists">
<field name="SELECTOR">.cookie-banner</field>
</block>
<block type="c4a_if_exists_else">
<field name="SELECTOR">#user</field>
</block>
<block type="c4a_if_not_exists">
<field name="SELECTOR">.modal</field>
</block>
<block type="c4a_if_js">
<field name="CONDITION">window.innerWidth < 768</field>
</block>
<block type="c4a_repeat_times">
<field name="TIMES">5</field>
</block>
<block type="c4a_repeat_while">
<field name="CONDITION">document.querySelector('.load-more')</field>
</block>
</category>
<category name="Variables" colour="${BlockColors.VARIABLES}">
<block type="c4a_setvar">
<field name="NAME">username</field>
<field name="VALUE">john@example.com</field>
</block>
<block type="c4a_eval">
<field name="CODE">console.log('Hello')</field>
</block>
</category>
<category name="Procedures" colour="${BlockColors.PROCEDURES}">
<block type="c4a_proc_def">
<field name="NAME">login</field>
</block>
<block type="c4a_proc_call">
<field name="NAME">login</field>
</block>
</category>
<category name="Comments" colour="#9E9E9E">
<block type="c4a_comment">
<field name="TEXT">Add comment here</field>
</block>
</category>
</xml>`;
}
initializeWorkspace() {
const blocklyDiv = document.getElementById('blockly-view');
// Dark theme configuration
const theme = Blockly.Theme.defineTheme('c4a-dark', {
'base': Blockly.Themes.Classic,
'componentStyles': {
'workspaceBackgroundColour': '#0e0e10',
'toolboxBackgroundColour': '#1a1a1b',
'toolboxForegroundColour': '#e0e0e0',
'flyoutBackgroundColour': '#1a1a1b',
'flyoutForegroundColour': '#e0e0e0',
'flyoutOpacity': 0.9,
'scrollbarColour': '#2a2a2c',
'scrollbarOpacity': 0.5,
'insertionMarkerColour': '#0fbbaa',
'insertionMarkerOpacity': 0.3,
'markerColour': '#0fbbaa',
'cursorColour': '#0fbbaa',
'selectedGlowColour': '#0fbbaa',
'selectedGlowOpacity': 0.4,
'replacementGlowColour': '#0fbbaa',
'replacementGlowOpacity': 0.5
},
'fontStyle': {
'family': 'Dank Mono, Monaco, Consolas, monospace',
'weight': 'normal',
'size': 13
}
});
this.workspace = Blockly.inject(blocklyDiv, {
toolbox: this.toolboxXml,
theme: theme,
grid: {
spacing: 20,
length: 3,
colour: '#2a2a2c',
snap: true
},
zoom: {
controls: true,
wheel: true,
startScale: 1.0,
maxScale: 3,
minScale: 0.3,
scaleSpeed: 1.2
},
trashcan: true,
sounds: false,
media: 'https://unpkg.com/blockly/media/'
});
// Add workspace change listener
this.workspace.addChangeListener((event) => {
if (!this.isUpdating && event.type !== Blockly.Events.UI) {
this.syncBlocksToCode();
}
});
}
setupEventHandlers() {
// Add blockly toggle button
const headerActions = document.querySelector('.editor-panel .header-actions');
const blocklyBtn = document.createElement('button');
blocklyBtn.id = 'blockly-btn';
blocklyBtn.className = 'action-btn';
blocklyBtn.title = 'Toggle Blockly Mode';
blocklyBtn.innerHTML = '<span class="icon">🧩</span>';
// Insert before the Run button
const runBtn = document.getElementById('run-btn');
headerActions.insertBefore(blocklyBtn, runBtn);
blocklyBtn.addEventListener('click', () => this.toggleBlocklyView());
}
setupSynchronization() {
// Listen to CodeMirror changes
this.app.editor.on('change', (instance, changeObj) => {
if (!this.isUpdating && this.blocklyVisible && changeObj.origin !== 'setValue') {
this.syncCodeToBlocks();
}
});
}
toggleBlocklyView() {
const editorView = document.getElementById('editor-view');
const blocklyView = document.getElementById('blockly-view');
const timelineView = document.getElementById('timeline-view');
const blocklyBtn = document.getElementById('blockly-btn');
this.blocklyVisible = !this.blocklyVisible;
if (this.blocklyVisible) {
// Show Blockly
editorView.classList.add('hidden');
timelineView.classList.add('hidden');
blocklyView.classList.remove('hidden');
blocklyBtn.classList.add('active');
// Resize workspace
Blockly.svgResize(this.workspace);
// Sync current code to blocks
this.syncCodeToBlocks();
} else {
// Show editor
blocklyView.classList.add('hidden');
editorView.classList.remove('hidden');
blocklyBtn.classList.remove('active');
// Refresh CodeMirror
setTimeout(() => this.app.editor.refresh(), 100);
}
}
syncBlocksToCode() {
if (this.isUpdating) return;
try {
this.isUpdating = true;
// Generate C4A-Script from blocks using our custom generator
if (typeof c4aGenerator !== 'undefined') {
const code = c4aGenerator.workspaceToCode(this.workspace);
// Process the code to maintain proper formatting
const lines = code.split('\n');
const formattedLines = [];
let lastWasComment = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) continue;
const isComment = line.startsWith('#');
// Add blank line when transitioning between comments and commands
if (formattedLines.length > 0 && lastWasComment !== isComment) {
formattedLines.push('');
}
formattedLines.push(line);
lastWasComment = isComment;
}
const cleanCode = formattedLines.join('\n');
// Update CodeMirror
this.app.editor.setValue(cleanCode);
}
} catch (error) {
console.error('Error syncing blocks to code:', error);
} finally {
this.isUpdating = false;
}
}
syncCodeToBlocks() {
if (this.isUpdating) return;
try {
this.isUpdating = true;
// Clear workspace
this.workspace.clear();
// Parse C4A-Script and generate blocks
const code = this.app.editor.getValue();
const blocks = this.parseC4AToBlocks(code);
if (blocks) {
Blockly.Xml.domToWorkspace(blocks, this.workspace);
}
} catch (error) {
console.error('Error syncing code to blocks:', error);
// Show error in console
this.app.addConsoleMessage(`Blockly sync error: ${error.message}`, 'warning');
} finally {
this.isUpdating = false;
}
}
parseC4AToBlocks(code) {
const lines = code.split('\n');
const xml = document.createElement('xml');
let yPos = 20;
let previousBlock = null;
let rootBlock = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Skip empty lines
if (!line) continue;
// Handle comments
if (line.startsWith('#')) {
const commentBlock = this.parseLineToBlock(line, i, lines);
if (commentBlock) {
if (previousBlock) {
// Connect to previous block
const next = document.createElement('next');
next.appendChild(commentBlock);
previousBlock.appendChild(next);
} else {
// First block - set position
commentBlock.setAttribute('x', 20);
commentBlock.setAttribute('y', yPos);
xml.appendChild(commentBlock);
rootBlock = commentBlock;
yPos += 60;
}
previousBlock = commentBlock;
}
continue;
}
const block = this.parseLineToBlock(line, i, lines);
if (block) {
if (previousBlock) {
// Connect to previous block using <next>
const next = document.createElement('next');
next.appendChild(block);
previousBlock.appendChild(next);
} else {
// First block - set position
block.setAttribute('x', 20);
block.setAttribute('y', yPos);
xml.appendChild(block);
rootBlock = block;
yPos += 60;
}
previousBlock = block;
}
}
return xml;
}
parseLineToBlock(line, index, allLines) {
// Navigation commands
if (line.startsWith('GO ')) {
const url = line.substring(3).trim();
return this.createBlock('c4a_go', { 'URL': url });
}
if (line === 'RELOAD') {
return this.createBlock('c4a_reload');
}
if (line === 'BACK') {
return this.createBlock('c4a_back');
}
if (line === 'FORWARD') {
return this.createBlock('c4a_forward');
}
// Wait commands
if (line.startsWith('WAIT ')) {
const parts = line.substring(5).trim();
// Check if it's just a number (wait time)
if (/^\d+(\.\d+)?$/.test(parts)) {
return this.createBlock('c4a_wait_time', { 'SECONDS': parts });
}
// Check for selector wait
const selectorMatch = parts.match(/^`([^`]+)`\s+(\d+)$/);
if (selectorMatch) {
return this.createBlock('c4a_wait_selector', {
'SELECTOR': selectorMatch[1],
'TIMEOUT': selectorMatch[2]
});
}
// Check for text wait
const textMatch = parts.match(/^"([^"]+)"\s+(\d+)$/);
if (textMatch) {
return this.createBlock('c4a_wait_text', {
'TEXT': textMatch[1],
'TIMEOUT': textMatch[2]
});
}
}
// Click commands
if (line.startsWith('CLICK ')) {
const target = line.substring(6).trim();
// Check for coordinates
const coordMatch = target.match(/^(\d+)\s+(\d+)$/);
if (coordMatch) {
return this.createBlock('c4a_click_xy', {
'X': coordMatch[1],
'Y': coordMatch[2]
});
}
// Selector click
const selectorMatch = target.match(/^`([^`]+)`$/);
if (selectorMatch) {
return this.createBlock('c4a_click', {
'SELECTOR': selectorMatch[1]
});
}
}
// Other mouse actions
if (line.startsWith('DOUBLE_CLICK ')) {
const selector = line.substring(13).trim().match(/^`([^`]+)`$/);
if (selector) {
return this.createBlock('c4a_double_click', {
'SELECTOR': selector[1]
});
}
}
if (line.startsWith('RIGHT_CLICK ')) {
const selector = line.substring(12).trim().match(/^`([^`]+)`$/);
if (selector) {
return this.createBlock('c4a_right_click', {
'SELECTOR': selector[1]
});
}
}
// Scroll
if (line.startsWith('SCROLL ')) {
const match = line.match(/^SCROLL\s+(UP|DOWN|LEFT|RIGHT)(?:\s+(\d+))?$/);
if (match) {
return this.createBlock('c4a_scroll', {
'DIRECTION': match[1],
'AMOUNT': match[2] || '500'
});
}
}
// Type commands
if (line.startsWith('TYPE ')) {
const content = line.substring(5).trim();
// Variable type
if (content.startsWith('$')) {
return this.createBlock('c4a_type_var', {
'VAR': content.substring(1)
});
}
// Text type
const textMatch = content.match(/^"([^"]*)"$/);
if (textMatch) {
return this.createBlock('c4a_type', {
'TEXT': textMatch[1]
});
}
}
// SET command
if (line.startsWith('SET ')) {
const match = line.match(/^SET\s+`([^`]+)`\s+"([^"]*)"$/);
if (match) {
return this.createBlock('c4a_set', {
'SELECTOR': match[1],
'VALUE': match[2]
});
}
}
// CLEAR command
if (line.startsWith('CLEAR ')) {
const match = line.match(/^CLEAR\s+`([^`]+)`$/);
if (match) {
return this.createBlock('c4a_clear', {
'SELECTOR': match[1]
});
}
}
// SETVAR command
if (line.startsWith('SETVAR ')) {
const match = line.match(/^SETVAR\s+(\w+)\s*=\s*"([^"]*)"$/);
if (match) {
return this.createBlock('c4a_setvar', {
'NAME': match[1],
'VALUE': match[2]
});
}
}
// IF commands (simplified - only single line)
if (line.startsWith('IF ')) {
// IF EXISTS
const existsMatch = line.match(/^IF\s+\(EXISTS\s+`([^`]+)`\)\s+THEN\s+(.+?)(?:\s+ELSE\s+(.+))?$/);
if (existsMatch) {
if (existsMatch[3]) {
// Has ELSE
const block = this.createBlock('c4a_if_exists_else', {
'SELECTOR': existsMatch[1]
});
// Parse then and else commands - simplified for now
return block;
} else {
// No ELSE
const block = this.createBlock('c4a_if_exists', {
'SELECTOR': existsMatch[1]
});
return block;
}
}
// IF NOT EXISTS
const notExistsMatch = line.match(/^IF\s+\(NOT\s+EXISTS\s+`([^`]+)`\)\s+THEN\s+(.+)$/);
if (notExistsMatch) {
const block = this.createBlock('c4a_if_not_exists', {
'SELECTOR': notExistsMatch[1]
});
return block;
}
}
// Comments
if (line.startsWith('#')) {
return this.createBlock('c4a_comment', {
'TEXT': line.substring(1).trim()
});
}
// If we can't parse it, return null
return null;
}
createBlock(type, fields = {}) {
const block = document.createElement('block');
block.setAttribute('type', type);
// Add fields
for (const [name, value] of Object.entries(fields)) {
const field = document.createElement('field');
field.setAttribute('name', name);
field.textContent = value;
block.appendChild(field);
}
return block;
}
}

View File

@@ -0,0 +1,238 @@
/* Blockly Theme CSS for C4A-Script */
/* Blockly workspace container */
.blockly-workspace {
position: relative;
width: 100%;
height: 100%;
background: var(--bg-primary);
}
/* Blockly button active state */
#blockly-btn.active {
background: var(--primary-color);
color: var(--bg-primary);
border-color: var(--primary-color);
}
#blockly-btn.active:hover {
background: var(--primary-hover);
border-color: var(--primary-hover);
}
/* Override Blockly's default styles for dark theme */
.blocklyToolboxDiv {
background-color: var(--bg-tertiary) !important;
border-right: 1px solid var(--border-color) !important;
}
.blocklyFlyout {
background-color: var(--bg-secondary) !important;
}
.blocklyFlyoutBackground {
fill: var(--bg-secondary) !important;
}
.blocklyMainBackground {
stroke: none !important;
}
.blocklyTreeRow {
color: var(--text-primary) !important;
font-family: 'Dank Mono', monospace !important;
padding: 4px 16px !important;
margin: 2px 0 !important;
}
.blocklyTreeRow:hover {
background-color: var(--bg-secondary) !important;
}
.blocklyTreeSelected {
background-color: var(--primary-dim) !important;
}
.blocklyTreeLabel {
cursor: pointer;
}
/* Blockly scrollbars */
.blocklyScrollbarHorizontal,
.blocklyScrollbarVertical {
background-color: transparent !important;
}
.blocklyScrollbarHandle {
fill: var(--border-color) !important;
opacity: 0.5 !important;
}
.blocklyScrollbarHandle:hover {
fill: var(--border-hover) !important;
opacity: 0.8 !important;
}
/* Blockly zoom controls */
.blocklyZoom > image {
opacity: 0.6;
}
.blocklyZoom > image:hover {
opacity: 1;
}
/* Blockly trash can */
.blocklyTrash {
opacity: 0.6;
}
.blocklyTrash:hover {
opacity: 1;
}
/* Blockly context menus */
.blocklyContextMenu {
background-color: var(--bg-tertiary) !important;
border: 1px solid var(--border-color) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
}
.blocklyMenuItem {
color: var(--text-primary) !important;
font-family: 'Dank Mono', monospace !important;
}
.blocklyMenuItemDisabled {
color: var(--text-muted) !important;
}
.blocklyMenuItem:hover {
background-color: var(--bg-secondary) !important;
}
/* Blockly text inputs */
.blocklyHtmlInput {
background-color: var(--bg-tertiary) !important;
color: var(--text-primary) !important;
border: 1px solid var(--border-color) !important;
font-family: 'Dank Mono', monospace !important;
font-size: 13px !important;
padding: 4px 8px !important;
}
.blocklyHtmlInput:focus {
border-color: var(--primary-color) !important;
outline: none !important;
}
/* Blockly dropdowns */
.blocklyDropDownDiv {
background-color: var(--bg-tertiary) !important;
border: 1px solid var(--border-color) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
}
.blocklyDropDownContent {
color: var(--text-primary) !important;
}
.blocklyDropDownDiv .goog-menuitem {
color: var(--text-primary) !important;
font-family: 'Dank Mono', monospace !important;
padding: 4px 16px !important;
}
.blocklyDropDownDiv .goog-menuitem-highlight,
.blocklyDropDownDiv .goog-menuitem-hover {
background-color: var(--bg-secondary) !important;
}
/* Custom block colors are defined in the block definitions */
/* Block text styling */
.blocklyText {
fill: #ffffff !important;
font-family: 'Dank Mono', monospace !important;
font-size: 13px !important;
}
.blocklyEditableText > .blocklyText {
fill: #ffffff !important;
}
.blocklyEditableText:hover > rect {
stroke: var(--primary-color) !important;
stroke-width: 2px !important;
}
/* Improve visibility of connection highlights */
.blocklyHighlightedConnectionPath {
stroke: var(--primary-color) !important;
stroke-width: 4px !important;
}
.blocklyInsertionMarker > .blocklyPath {
fill-opacity: 0.3 !important;
stroke-opacity: 0.6 !important;
}
/* Workspace grid pattern */
.blocklyWorkspace > .blocklyBlockCanvas > .blocklyGridCanvas {
opacity: 0.1;
}
/* Smooth transitions */
.blocklyDraggable {
transition: transform 0.1s ease;
}
/* Field labels */
.blocklyFieldLabel {
font-weight: normal !important;
}
/* Comment blocks styling */
.blocklyCommentText {
font-style: italic !important;
}
/* Make comment blocks slightly transparent */
g[data-category="Comments"] .blocklyPath {
fill-opacity: 0.8 !important;
}
/* Better visibility for disabled blocks */
.blocklyDisabled > .blocklyPath {
fill-opacity: 0.3 !important;
}
.blocklyDisabled > .blocklyText {
fill-opacity: 0.5 !important;
}
/* Warning and error text */
.blocklyWarningText,
.blocklyErrorText {
font-family: 'Dank Mono', monospace !important;
font-size: 12px !important;
}
/* Workspace scrollbar improvement for dark theme */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--border-hover);
}

View File

@@ -0,0 +1,549 @@
// C4A-Script Blockly Block Definitions
// This file defines all custom blocks for C4A-Script commands
// Color scheme for different block categories
const BlockColors = {
NAVIGATION: '#1E88E5', // Blue
ACTIONS: '#43A047', // Green
CONTROL: '#FB8C00', // Orange
VARIABLES: '#8E24AA', // Purple
WAIT: '#E53935', // Red
KEYBOARD: '#00ACC1', // Cyan
PROCEDURES: '#6A1B9A' // Deep Purple
};
// Helper to create selector input with backticks
Blockly.Blocks['c4a_selector_input'] = {
init: function() {
this.appendDummyInput()
.appendField("`")
.appendField(new Blockly.FieldTextInput("selector"), "SELECTOR")
.appendField("`");
this.setOutput(true, "Selector");
this.setColour(BlockColors.ACTIONS);
this.setTooltip("CSS selector for element");
}
};
// ============================================
// NAVIGATION BLOCKS
// ============================================
Blockly.Blocks['c4a_go'] = {
init: function() {
this.appendDummyInput()
.appendField("GO")
.appendField(new Blockly.FieldTextInput("https://example.com"), "URL");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.NAVIGATION);
this.setTooltip("Navigate to URL");
}
};
Blockly.Blocks['c4a_reload'] = {
init: function() {
this.appendDummyInput()
.appendField("RELOAD");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.NAVIGATION);
this.setTooltip("Reload current page");
}
};
Blockly.Blocks['c4a_back'] = {
init: function() {
this.appendDummyInput()
.appendField("BACK");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.NAVIGATION);
this.setTooltip("Go back in browser history");
}
};
Blockly.Blocks['c4a_forward'] = {
init: function() {
this.appendDummyInput()
.appendField("FORWARD");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.NAVIGATION);
this.setTooltip("Go forward in browser history");
}
};
// ============================================
// WAIT BLOCKS
// ============================================
Blockly.Blocks['c4a_wait_time'] = {
init: function() {
this.appendDummyInput()
.appendField("WAIT")
.appendField(new Blockly.FieldNumber(1, 0), "SECONDS")
.appendField("seconds");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.WAIT);
this.setTooltip("Wait for specified seconds");
}
};
Blockly.Blocks['c4a_wait_selector'] = {
init: function() {
this.appendDummyInput()
.appendField("WAIT for")
.appendField("`")
.appendField(new Blockly.FieldTextInput("selector"), "SELECTOR")
.appendField("`")
.appendField("max")
.appendField(new Blockly.FieldNumber(10, 1), "TIMEOUT")
.appendField("sec");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.WAIT);
this.setTooltip("Wait for element to appear");
}
};
Blockly.Blocks['c4a_wait_text'] = {
init: function() {
this.appendDummyInput()
.appendField("WAIT for text")
.appendField(new Blockly.FieldTextInput("Loading complete"), "TEXT")
.appendField("max")
.appendField(new Blockly.FieldNumber(5, 1), "TIMEOUT")
.appendField("sec");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.WAIT);
this.setTooltip("Wait for text to appear on page");
}
};
// ============================================
// MOUSE ACTION BLOCKS
// ============================================
Blockly.Blocks['c4a_click'] = {
init: function() {
this.appendDummyInput()
.appendField("CLICK")
.appendField("`")
.appendField(new Blockly.FieldTextInput("button"), "SELECTOR")
.appendField("`");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.ACTIONS);
this.setTooltip("Click on element");
}
};
Blockly.Blocks['c4a_click_xy'] = {
init: function() {
this.appendDummyInput()
.appendField("CLICK at")
.appendField("X:")
.appendField(new Blockly.FieldNumber(100, 0), "X")
.appendField("Y:")
.appendField(new Blockly.FieldNumber(100, 0), "Y");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.ACTIONS);
this.setTooltip("Click at coordinates");
}
};
Blockly.Blocks['c4a_double_click'] = {
init: function() {
this.appendDummyInput()
.appendField("DOUBLE_CLICK")
.appendField("`")
.appendField(new Blockly.FieldTextInput(".item"), "SELECTOR")
.appendField("`");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.ACTIONS);
this.setTooltip("Double click on element");
}
};
Blockly.Blocks['c4a_right_click'] = {
init: function() {
this.appendDummyInput()
.appendField("RIGHT_CLICK")
.appendField("`")
.appendField(new Blockly.FieldTextInput("#menu"), "SELECTOR")
.appendField("`");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.ACTIONS);
this.setTooltip("Right click on element");
}
};
Blockly.Blocks['c4a_move'] = {
init: function() {
this.appendDummyInput()
.appendField("MOVE to")
.appendField("X:")
.appendField(new Blockly.FieldNumber(500, 0), "X")
.appendField("Y:")
.appendField(new Blockly.FieldNumber(300, 0), "Y");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.ACTIONS);
this.setTooltip("Move mouse to position");
}
};
Blockly.Blocks['c4a_drag'] = {
init: function() {
this.appendDummyInput()
.appendField("DRAG from")
.appendField("X:")
.appendField(new Blockly.FieldNumber(100, 0), "X1")
.appendField("Y:")
.appendField(new Blockly.FieldNumber(100, 0), "Y1");
this.appendDummyInput()
.appendField("to")
.appendField("X:")
.appendField(new Blockly.FieldNumber(500, 0), "X2")
.appendField("Y:")
.appendField(new Blockly.FieldNumber(300, 0), "Y2");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.ACTIONS);
this.setTooltip("Drag from one point to another");
}
};
Blockly.Blocks['c4a_scroll'] = {
init: function() {
this.appendDummyInput()
.appendField("SCROLL")
.appendField(new Blockly.FieldDropdown([
["DOWN", "DOWN"],
["UP", "UP"],
["LEFT", "LEFT"],
["RIGHT", "RIGHT"]
]), "DIRECTION")
.appendField(new Blockly.FieldNumber(500, 0), "AMOUNT")
.appendField("pixels");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.ACTIONS);
this.setTooltip("Scroll in direction");
}
};
// ============================================
// KEYBOARD BLOCKS
// ============================================
Blockly.Blocks['c4a_type'] = {
init: function() {
this.appendDummyInput()
.appendField("TYPE")
.appendField(new Blockly.FieldTextInput("text to type"), "TEXT");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.KEYBOARD);
this.setTooltip("Type text");
}
};
Blockly.Blocks['c4a_type_var'] = {
init: function() {
this.appendDummyInput()
.appendField("TYPE")
.appendField("$")
.appendField(new Blockly.FieldTextInput("variable"), "VAR");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.KEYBOARD);
this.setTooltip("Type variable value");
}
};
Blockly.Blocks['c4a_clear'] = {
init: function() {
this.appendDummyInput()
.appendField("CLEAR")
.appendField("`")
.appendField(new Blockly.FieldTextInput("input"), "SELECTOR")
.appendField("`");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.KEYBOARD);
this.setTooltip("Clear input field");
}
};
Blockly.Blocks['c4a_set'] = {
init: function() {
this.appendDummyInput()
.appendField("SET")
.appendField("`")
.appendField(new Blockly.FieldTextInput("#input"), "SELECTOR")
.appendField("`")
.appendField("to")
.appendField(new Blockly.FieldTextInput("value"), "VALUE");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.KEYBOARD);
this.setTooltip("Set input field value");
}
};
Blockly.Blocks['c4a_press'] = {
init: function() {
this.appendDummyInput()
.appendField("PRESS")
.appendField(new Blockly.FieldDropdown([
["Tab", "Tab"],
["Enter", "Enter"],
["Escape", "Escape"],
["Space", "Space"],
["ArrowUp", "ArrowUp"],
["ArrowDown", "ArrowDown"],
["ArrowLeft", "ArrowLeft"],
["ArrowRight", "ArrowRight"],
["Delete", "Delete"],
["Backspace", "Backspace"]
]), "KEY");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.KEYBOARD);
this.setTooltip("Press and release key");
}
};
Blockly.Blocks['c4a_key_down'] = {
init: function() {
this.appendDummyInput()
.appendField("KEY_DOWN")
.appendField(new Blockly.FieldDropdown([
["Shift", "Shift"],
["Control", "Control"],
["Alt", "Alt"],
["Meta", "Meta"]
]), "KEY");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.KEYBOARD);
this.setTooltip("Hold key down");
}
};
Blockly.Blocks['c4a_key_up'] = {
init: function() {
this.appendDummyInput()
.appendField("KEY_UP")
.appendField(new Blockly.FieldDropdown([
["Shift", "Shift"],
["Control", "Control"],
["Alt", "Alt"],
["Meta", "Meta"]
]), "KEY");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.KEYBOARD);
this.setTooltip("Release key");
}
};
// ============================================
// CONTROL FLOW BLOCKS
// ============================================
Blockly.Blocks['c4a_if_exists'] = {
init: function() {
this.appendDummyInput()
.appendField("IF EXISTS")
.appendField("`")
.appendField(new Blockly.FieldTextInput(".element"), "SELECTOR")
.appendField("`")
.appendField("THEN");
this.appendStatementInput("THEN")
.setCheck(null);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.CONTROL);
this.setTooltip("If element exists, then do something");
}
};
Blockly.Blocks['c4a_if_exists_else'] = {
init: function() {
this.appendDummyInput()
.appendField("IF EXISTS")
.appendField("`")
.appendField(new Blockly.FieldTextInput(".element"), "SELECTOR")
.appendField("`")
.appendField("THEN");
this.appendStatementInput("THEN")
.setCheck(null);
this.appendDummyInput()
.appendField("ELSE");
this.appendStatementInput("ELSE")
.setCheck(null);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.CONTROL);
this.setTooltip("If element exists, then do something, else do something else");
}
};
Blockly.Blocks['c4a_if_not_exists'] = {
init: function() {
this.appendDummyInput()
.appendField("IF NOT EXISTS")
.appendField("`")
.appendField(new Blockly.FieldTextInput(".element"), "SELECTOR")
.appendField("`")
.appendField("THEN");
this.appendStatementInput("THEN")
.setCheck(null);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.CONTROL);
this.setTooltip("If element does not exist, then do something");
}
};
Blockly.Blocks['c4a_if_js'] = {
init: function() {
this.appendDummyInput()
.appendField("IF")
.appendField("`")
.appendField(new Blockly.FieldTextInput("window.innerWidth < 768"), "CONDITION")
.appendField("`")
.appendField("THEN");
this.appendStatementInput("THEN")
.setCheck(null);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.CONTROL);
this.setTooltip("If JavaScript condition is true");
}
};
Blockly.Blocks['c4a_repeat_times'] = {
init: function() {
this.appendDummyInput()
.appendField("REPEAT")
.appendField(new Blockly.FieldNumber(5, 1), "TIMES")
.appendField("times");
this.appendStatementInput("DO")
.setCheck(null);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.CONTROL);
this.setTooltip("Repeat commands N times");
}
};
Blockly.Blocks['c4a_repeat_while'] = {
init: function() {
this.appendDummyInput()
.appendField("REPEAT WHILE")
.appendField("`")
.appendField(new Blockly.FieldTextInput("document.querySelector('.load-more')"), "CONDITION")
.appendField("`");
this.appendStatementInput("DO")
.setCheck(null);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.CONTROL);
this.setTooltip("Repeat while condition is true");
}
};
// ============================================
// VARIABLE BLOCKS
// ============================================
Blockly.Blocks['c4a_setvar'] = {
init: function() {
this.appendDummyInput()
.appendField("SETVAR")
.appendField(new Blockly.FieldTextInput("username"), "NAME")
.appendField("=")
.appendField(new Blockly.FieldTextInput("value"), "VALUE");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.VARIABLES);
this.setTooltip("Set variable value");
}
};
// ============================================
// ADVANCED BLOCKS
// ============================================
Blockly.Blocks['c4a_eval'] = {
init: function() {
this.appendDummyInput()
.appendField("EVAL")
.appendField("`")
.appendField(new Blockly.FieldTextInput("console.log('Hello')"), "CODE")
.appendField("`");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.VARIABLES);
this.setTooltip("Execute JavaScript code");
}
};
Blockly.Blocks['c4a_comment'] = {
init: function() {
this.appendDummyInput()
.appendField("#")
.appendField(new Blockly.FieldTextInput("Comment", null, {
spellcheck: false,
class: 'blocklyCommentText'
}), "TEXT");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour("#616161");
this.setTooltip("Add a comment");
this.setStyle('comment_blocks');
}
};
// ============================================
// PROCEDURE BLOCKS
// ============================================
Blockly.Blocks['c4a_proc_def'] = {
init: function() {
this.appendDummyInput()
.appendField("PROC")
.appendField(new Blockly.FieldTextInput("procedure_name"), "NAME");
this.appendStatementInput("BODY")
.setCheck(null);
this.appendDummyInput()
.appendField("ENDPROC");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.PROCEDURES);
this.setTooltip("Define a procedure");
}
};
Blockly.Blocks['c4a_proc_call'] = {
init: function() {
this.appendDummyInput()
.appendField("Call")
.appendField(new Blockly.FieldTextInput("procedure_name"), "NAME");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(BlockColors.PROCEDURES);
this.setTooltip("Call a procedure");
}
};
// Code generators have been moved to c4a-generator.js

View File

@@ -0,0 +1,261 @@
// C4A-Script Code Generator for Blockly
// Compatible with latest Blockly API
// Create a custom code generator for C4A-Script
const c4aGenerator = new Blockly.Generator('C4A');
// Helper to get field value with proper escaping
c4aGenerator.getFieldValue = function(block, fieldName) {
return block.getFieldValue(fieldName);
};
// Navigation generators
c4aGenerator.forBlock['c4a_go'] = function(block, generator) {
const url = generator.getFieldValue(block, 'URL');
return `GO ${url}\n`;
};
c4aGenerator.forBlock['c4a_reload'] = function(block, generator) {
return 'RELOAD\n';
};
c4aGenerator.forBlock['c4a_back'] = function(block, generator) {
return 'BACK\n';
};
c4aGenerator.forBlock['c4a_forward'] = function(block, generator) {
return 'FORWARD\n';
};
// Wait generators
c4aGenerator.forBlock['c4a_wait_time'] = function(block, generator) {
const seconds = generator.getFieldValue(block, 'SECONDS');
return `WAIT ${seconds}\n`;
};
c4aGenerator.forBlock['c4a_wait_selector'] = function(block, generator) {
const selector = generator.getFieldValue(block, 'SELECTOR');
const timeout = generator.getFieldValue(block, 'TIMEOUT');
return `WAIT \`${selector}\` ${timeout}\n`;
};
c4aGenerator.forBlock['c4a_wait_text'] = function(block, generator) {
const text = generator.getFieldValue(block, 'TEXT');
const timeout = generator.getFieldValue(block, 'TIMEOUT');
return `WAIT "${text}" ${timeout}\n`;
};
// Mouse action generators
c4aGenerator.forBlock['c4a_click'] = function(block, generator) {
const selector = generator.getFieldValue(block, 'SELECTOR');
return `CLICK \`${selector}\`\n`;
};
c4aGenerator.forBlock['c4a_click_xy'] = function(block, generator) {
const x = generator.getFieldValue(block, 'X');
const y = generator.getFieldValue(block, 'Y');
return `CLICK ${x} ${y}\n`;
};
c4aGenerator.forBlock['c4a_double_click'] = function(block, generator) {
const selector = generator.getFieldValue(block, 'SELECTOR');
return `DOUBLE_CLICK \`${selector}\`\n`;
};
c4aGenerator.forBlock['c4a_right_click'] = function(block, generator) {
const selector = generator.getFieldValue(block, 'SELECTOR');
return `RIGHT_CLICK \`${selector}\`\n`;
};
c4aGenerator.forBlock['c4a_move'] = function(block, generator) {
const x = generator.getFieldValue(block, 'X');
const y = generator.getFieldValue(block, 'Y');
return `MOVE ${x} ${y}\n`;
};
c4aGenerator.forBlock['c4a_drag'] = function(block, generator) {
const x1 = generator.getFieldValue(block, 'X1');
const y1 = generator.getFieldValue(block, 'Y1');
const x2 = generator.getFieldValue(block, 'X2');
const y2 = generator.getFieldValue(block, 'Y2');
return `DRAG ${x1} ${y1} ${x2} ${y2}\n`;
};
c4aGenerator.forBlock['c4a_scroll'] = function(block, generator) {
const direction = generator.getFieldValue(block, 'DIRECTION');
const amount = generator.getFieldValue(block, 'AMOUNT');
return `SCROLL ${direction} ${amount}\n`;
};
// Keyboard generators
c4aGenerator.forBlock['c4a_type'] = function(block, generator) {
const text = generator.getFieldValue(block, 'TEXT');
return `TYPE "${text}"\n`;
};
c4aGenerator.forBlock['c4a_type_var'] = function(block, generator) {
const varName = generator.getFieldValue(block, 'VAR');
return `TYPE $${varName}\n`;
};
c4aGenerator.forBlock['c4a_clear'] = function(block, generator) {
const selector = generator.getFieldValue(block, 'SELECTOR');
return `CLEAR \`${selector}\`\n`;
};
c4aGenerator.forBlock['c4a_set'] = function(block, generator) {
const selector = generator.getFieldValue(block, 'SELECTOR');
const value = generator.getFieldValue(block, 'VALUE');
return `SET \`${selector}\` "${value}"\n`;
};
c4aGenerator.forBlock['c4a_press'] = function(block, generator) {
const key = generator.getFieldValue(block, 'KEY');
return `PRESS ${key}\n`;
};
c4aGenerator.forBlock['c4a_key_down'] = function(block, generator) {
const key = generator.getFieldValue(block, 'KEY');
return `KEY_DOWN ${key}\n`;
};
c4aGenerator.forBlock['c4a_key_up'] = function(block, generator) {
const key = generator.getFieldValue(block, 'KEY');
return `KEY_UP ${key}\n`;
};
// Control flow generators
c4aGenerator.forBlock['c4a_if_exists'] = function(block, generator) {
const selector = generator.getFieldValue(block, 'SELECTOR');
const thenCode = generator.statementToCode(block, 'THEN').trim();
if (thenCode.includes('\n')) {
// Multi-line then block
const lines = thenCode.split('\n').filter(line => line.trim());
return lines.map(line => `IF (EXISTS \`${selector}\`) THEN ${line}`).join('\n') + '\n';
} else if (thenCode) {
// Single line
return `IF (EXISTS \`${selector}\`) THEN ${thenCode}\n`;
}
return '';
};
c4aGenerator.forBlock['c4a_if_exists_else'] = function(block, generator) {
const selector = generator.getFieldValue(block, 'SELECTOR');
const thenCode = generator.statementToCode(block, 'THEN').trim();
const elseCode = generator.statementToCode(block, 'ELSE').trim();
// For simplicity, only handle single-line then/else
const thenLine = thenCode.split('\n')[0];
const elseLine = elseCode.split('\n')[0];
if (thenLine && elseLine) {
return `IF (EXISTS \`${selector}\`) THEN ${thenLine} ELSE ${elseLine}\n`;
} else if (thenLine) {
return `IF (EXISTS \`${selector}\`) THEN ${thenLine}\n`;
}
return '';
};
c4aGenerator.forBlock['c4a_if_not_exists'] = function(block, generator) {
const selector = generator.getFieldValue(block, 'SELECTOR');
const thenCode = generator.statementToCode(block, 'THEN').trim();
if (thenCode.includes('\n')) {
const lines = thenCode.split('\n').filter(line => line.trim());
return lines.map(line => `IF (NOT EXISTS \`${selector}\`) THEN ${line}`).join('\n') + '\n';
} else if (thenCode) {
return `IF (NOT EXISTS \`${selector}\`) THEN ${thenCode}\n`;
}
return '';
};
c4aGenerator.forBlock['c4a_if_js'] = function(block, generator) {
const condition = generator.getFieldValue(block, 'CONDITION');
const thenCode = generator.statementToCode(block, 'THEN').trim();
if (thenCode.includes('\n')) {
const lines = thenCode.split('\n').filter(line => line.trim());
return lines.map(line => `IF (\`${condition}\`) THEN ${line}`).join('\n') + '\n';
} else if (thenCode) {
return `IF (\`${condition}\`) THEN ${thenCode}\n`;
}
return '';
};
c4aGenerator.forBlock['c4a_repeat_times'] = function(block, generator) {
const times = generator.getFieldValue(block, 'TIMES');
const doCode = generator.statementToCode(block, 'DO').trim();
if (doCode) {
// Get first command for repeat
const firstLine = doCode.split('\n')[0];
return `REPEAT (${firstLine}, ${times})\n`;
}
return '';
};
c4aGenerator.forBlock['c4a_repeat_while'] = function(block, generator) {
const condition = generator.getFieldValue(block, 'CONDITION');
const doCode = generator.statementToCode(block, 'DO').trim();
if (doCode) {
// Get first command for repeat
const firstLine = doCode.split('\n')[0];
return `REPEAT (${firstLine}, \`${condition}\`)\n`;
}
return '';
};
// Variable generators
c4aGenerator.forBlock['c4a_setvar'] = function(block, generator) {
const name = generator.getFieldValue(block, 'NAME');
const value = generator.getFieldValue(block, 'VALUE');
return `SETVAR ${name} = "${value}"\n`;
};
// Advanced generators
c4aGenerator.forBlock['c4a_eval'] = function(block, generator) {
const code = generator.getFieldValue(block, 'CODE');
return `EVAL \`${code}\`\n`;
};
c4aGenerator.forBlock['c4a_comment'] = function(block, generator) {
const text = generator.getFieldValue(block, 'TEXT');
return `# ${text}\n`;
};
// Procedure generators
c4aGenerator.forBlock['c4a_proc_def'] = function(block, generator) {
const name = generator.getFieldValue(block, 'NAME');
const body = generator.statementToCode(block, 'BODY');
return `PROC ${name}\n${body}ENDPROC\n`;
};
c4aGenerator.forBlock['c4a_proc_call'] = function(block, generator) {
const name = generator.getFieldValue(block, 'NAME');
return `${name}\n`;
};
// Override scrub_ to handle our custom format
c4aGenerator.scrub_ = function(block, code, opt_thisOnly) {
const nextBlock = block.nextConnection && block.nextConnection.targetBlock();
let nextCode = '';
if (nextBlock) {
if (!opt_thisOnly) {
nextCode = c4aGenerator.blockToCode(nextBlock);
// Add blank line between comment and non-comment blocks
const currentIsComment = block.type === 'c4a_comment';
const nextIsComment = nextBlock.type === 'c4a_comment';
// Add blank line when transitioning from command to comment or vice versa
if (currentIsComment !== nextIsComment && code.trim() && nextCode.trim()) {
nextCode = '\n' + nextCode;
}
}
}
return code + nextCode;
};

View File

@@ -0,0 +1,21 @@
# Demo: Login Flow with Blockly
# This script can be created visually using Blockly blocks
GO https://example.com/login
WAIT `#login-form` 5
# Check if already logged in
IF (EXISTS `.user-avatar`) THEN GO https://example.com/dashboard
# Fill login form
CLICK `#email`
TYPE "demo@example.com"
CLICK `#password`
TYPE "password123"
# Submit form
CLICK `button[type="submit"]`
WAIT `.dashboard` 10
# Success message
EVAL `console.log('Login successful!')`

View File

@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>C4A-Script Interactive Tutorial | Crawl4AI</title>
<link rel="stylesheet" href="assets/app.css">
<link rel="stylesheet" href="assets/blockly-theme.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/theme/material-darker.min.css">
</head>
@@ -25,6 +26,45 @@
</div>
</div>
</div>
<!-- Event Editor Modal -->
<div id="event-editor-overlay" class="modal-overlay hidden"></div>
<div id="event-editor-modal" class="event-editor-modal hidden">
<h4>Edit Event</h4>
<div class="editor-field">
<label>Command Type</label>
<select id="edit-command-type" disabled>
<option value="CLICK">CLICK</option>
<option value="DOUBLE_CLICK">DOUBLE_CLICK</option>
<option value="RIGHT_CLICK">RIGHT_CLICK</option>
<option value="TYPE">TYPE</option>
<option value="SET">SET</option>
<option value="SCROLL">SCROLL</option>
<option value="WAIT">WAIT</option>
</select>
</div>
<div id="edit-selector-field" class="editor-field">
<label>Selector</label>
<input type="text" id="edit-selector" placeholder=".class or #id">
</div>
<div id="edit-value-field" class="editor-field">
<label>Value</label>
<input type="text" id="edit-value" placeholder="Text or number">
</div>
<div id="edit-direction-field" class="editor-field hidden">
<label>Direction</label>
<select id="edit-direction">
<option value="UP">UP</option>
<option value="DOWN">DOWN</option>
<option value="LEFT">LEFT</option>
<option value="RIGHT">RIGHT</option>
</select>
</div>
<div class="editor-actions">
<button id="edit-cancel" class="mini-btn">Cancel</button>
<button id="edit-save" class="mini-btn primary">Save</button>
</div>
</div>
<!-- Main App Layout -->
<div class="app-container">
@@ -45,11 +85,35 @@
<button id="run-btn" class="action-btn primary">
<span class="icon"></span>Run
</button>
<button id="record-btn" class="action-btn record">
<span class="icon"></span>Record
</button>
<button id="timeline-btn" class="action-btn timeline hidden" title="View Timeline">
<span class="icon">📊</span>
</button>
</div>
</div>
<div class="editor-wrapper">
<textarea id="c4a-editor" placeholder="# Write your C4A script here..."></textarea>
<div class="editor-container">
<div id="editor-view" class="editor-wrapper">
<textarea id="c4a-editor" placeholder="# Write your C4A script here..."></textarea>
</div>
<!-- Recording Timeline -->
<div id="timeline-view" class="recording-timeline hidden">
<div class="timeline-header">
<h3>Recording Timeline</h3>
<div class="timeline-actions">
<button id="back-to-editor" class="mini-btn">← Back</button>
<button id="select-all-events" class="mini-btn">Select All</button>
<button id="clear-events" class="mini-btn">Clear</button>
<button id="generate-script" class="mini-btn primary">Generate Script</button>
</div>
</div>
<div id="timeline-events" class="timeline-events">
<!-- Events will be added here dynamically -->
</div>
</div>
</div>
<!-- Bottom: Output Tabs -->
@@ -129,6 +193,13 @@
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/mode/javascript/javascript.min.js"></script>
<!-- Blockly -->
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="assets/c4a-blocks.js"></script>
<script src="assets/c4a-generator.js"></script>
<script src="assets/blockly-manager.js"></script>
<script src="assets/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blockly Test</title>
<style>
body {
margin: 0;
padding: 20px;
background: #0e0e10;
color: #e0e0e0;
font-family: monospace;
}
#blocklyDiv {
height: 600px;
width: 100%;
border: 1px solid #2a2a2c;
}
#output {
margin-top: 20px;
padding: 15px;
background: #1a1a1b;
border: 1px solid #2a2a2c;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>C4A-Script Blockly Test</h1>
<div id="blocklyDiv"></div>
<div id="output">
<h3>Generated C4A-Script:</h3>
<pre id="code-output"></pre>
</div>
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="assets/c4a-blocks.js"></script>
<script>
// Simple test
const workspace = Blockly.inject('blocklyDiv', {
toolbox: `
<xml>
<category name="Test" colour="#1E88E5">
<block type="c4a_go"></block>
<block type="c4a_wait_time"></block>
<block type="c4a_click"></block>
</category>
</xml>
`,
theme: Blockly.Theme.defineTheme('dark', {
'base': Blockly.Themes.Classic,
'componentStyles': {
'workspaceBackgroundColour': '#0e0e10',
'toolboxBackgroundColour': '#1a1a1b',
'toolboxForegroundColour': '#e0e0e0',
'flyoutBackgroundColour': '#1a1a1b',
'flyoutForegroundColour': '#e0e0e0',
}
})
});
workspace.addChangeListener((event) => {
const code = Blockly.JavaScript.workspaceToCode(workspace);
document.getElementById('code-output').textContent = code;
});
</script>
</body>
</html>