Add some new commands for the Crawl4ai script transpiler and creating an interactive tutorial that allows users to go through multiple steps and apply the syntax to automate the page. Fixed some issues and add several new commands for setting input values, variables, clearing input fields, and more.

This commit is contained in:
UncleCode
2025-06-06 23:03:26 +08:00
parent 3f6f2e998c
commit ca03acbc82
17 changed files with 4368 additions and 27 deletions

View File

@@ -1,3 +1,9 @@
{
"permissions": {
"allow": [
"Bash(cd:*)",
"Bash(python3:*)"
]
},
"enableAllProjectMcpServers": false
}

View File

@@ -133,8 +133,8 @@ start : line*
?line : command | proc_def | include | comment
command : wait | nav | click_cmd | double_click | right_click | move | drag | scroll
| type | press | key_down | key_up
| eval_cmd | set_var | proc_call | if_cmd | repeat_cmd
| type | clear | set_input | press | key_down | key_up
| eval_cmd | setvar | proc_call | if_cmd | repeat_cmd
wait : "WAIT" (ESCAPED_STRING|BACKTICK_STRING|NUMBER) NUMBER? -> wait_cmd
nav : "GO" URL -> go
@@ -151,12 +151,14 @@ drag : "DRAG" coords coords -> drag
scroll : "SCROLL" DIR NUMBER? -> scroll
type : "TYPE" (ESCAPED_STRING | NAME) -> type
clear : "CLEAR" BACKTICK_STRING -> clear
set_input : "SET" BACKTICK_STRING (ESCAPED_STRING | BACKTICK_STRING | NAME) -> set_input
press : "PRESS" WORD -> press
key_down : "KEY_DOWN" WORD -> key_down
key_up : "KEY_UP" WORD -> key_up
eval_cmd : "EVAL" BACKTICK_STRING -> eval_cmd
set_var : "SET" NAME "=" value -> set_var
setvar : "SETVAR" NAME "=" value -> setvar
proc_call : NAME -> proc_call
proc_def : "PROC" NAME line* "ENDPROC" -> proc_def
include : "USE" ESCAPED_STRING -> include
@@ -165,14 +167,15 @@ comment : /#.*/ -> comment
if_cmd : "IF" "(" condition ")" "THEN" command ("ELSE" command)? -> if_cmd
repeat_cmd : "REPEAT" "(" command "," repeat_count ")" -> repeat_cmd
condition : exists_cond | js_cond
condition : not_cond | exists_cond | js_cond
not_cond : "NOT" condition -> not_cond
exists_cond : "EXISTS" BACKTICK_STRING -> exists_cond
js_cond : BACKTICK_STRING -> js_cond
repeat_count : NUMBER | BACKTICK_STRING
coords : NUMBER NUMBER
value : ESCAPED_STRING | NUMBER
value : ESCAPED_STRING | BACKTICK_STRING | NUMBER
DIR : /(UP|DOWN|LEFT|RIGHT)/i
REST : /[^\n]+/
@@ -270,13 +273,15 @@ class ASTBuilder(Transformer):
# KEYS
def type(self,tok): return Cmd("TYPE",[self._strip(str(tok))])
def clear(self,sel): return Cmd("CLEAR",[self._strip(str(sel))])
def set_input(self,sel,val): return Cmd("SET",[self._strip(str(sel)), self._strip(str(val))])
def press(self,w): return Cmd("PRESS",[str(w)])
def key_down(self,w): return Cmd("KEYDOWN",[str(w)])
def key_up(self,w): return Cmd("KEYUP",[str(w)])
# FLOW
def eval_cmd(self,txt): return Cmd("EVAL",[self._strip(str(txt))])
def set_var(self,n,v):
def setvar(self,n,v):
# v might be a Token or a Tree, extract value properly
if hasattr(v, 'value'):
value = v.value
@@ -284,7 +289,7 @@ class ASTBuilder(Transformer):
value = v.children[0].value
else:
value = str(v)
return Cmd("SET",[str(n), self._strip(value)])
return Cmd("SETVAR",[str(n), self._strip(value)])
def proc_call(self,n): return Cmd("CALL",[str(n)])
def proc_def(self,n,*body): return Proc(str(n),[b for b in body if isinstance(b,Cmd)])
def include(self,p): return Cmd("INCLUDE",[self._strip(p)])
@@ -297,6 +302,9 @@ class ASTBuilder(Transformer):
def condition(self, cond):
return cond
def not_cond(self, cond):
return ("NOT", cond)
def exists_cond(self, selector):
return ("EXISTS", self._strip(str(selector)))
@@ -366,19 +374,22 @@ class Compiler:
out=[]
for c in ir:
if isinstance(c,Cmd):
if c.op=="SET": self.vars[c.args[0].lstrip('$')]=c.args[1]
if c.op=="SETVAR":
# Store variable
self.vars[c.args[0].lstrip('$')]=c.args[1]
else:
if c.op in("TYPE","EVAL"): c.args=[sub(a) for a in c.args]
# Apply variable substitution to commands that use them
if c.op in("TYPE","EVAL","SET"): c.args=[sub(a) for a in c.args]
out.append(c)
return out
# JS emitter
def _emit_js(self, cmd: Cmd) -> str:
op, a = cmd.op, cmd.args
if op == "GO": return f"location.href = '{a[0]}';"
if op == "RELOAD": return "location.reload();"
if op == "BACK": return "history.back();"
if op == "FORWARD": return "history.forward();"
if op == "GO": return f"window.location.href = '{a[0]}';"
if op == "RELOAD": return "window.location.reload();"
if op == "BACK": return "window.history.back();"
if op == "FORWARD": return "window.history.forward();"
if op == "WAIT":
arg, kind = a[0]
@@ -411,13 +422,24 @@ class Compiler:
# click-style helpers
def _js_click(sel, evt="click", button=0, detail=1):
sel = sel.replace("'", "\\'")
return f"document.querySelector('{sel}')?.dispatchEvent(new MouseEvent('{evt}',{{bubbles:true,button:{button},detail:{detail}}}));"
return textwrap.dedent(f"""
(()=>{{
const el=document.querySelector('{sel}');
if(el){{
el.focus&&el.focus();
el.dispatchEvent(new MouseEvent('{evt}',{{bubbles:true,button:{button},detail:{detail}}}));
}}
}})();
""").strip()
def _js_click_xy(x, y, evt="click", button=0, detail=1):
return textwrap.dedent(f"""
(()=>{{
const el=document.elementFromPoint({x},{y});
el&&el.dispatchEvent(new MouseEvent('{evt}',{{bubbles:true,button:{button},detail:{detail}}}));
if(el){{
el.focus&&el.focus();
el.dispatchEvent(new MouseEvent('{evt}',{{bubbles:true,button:{button},detail:{detail}}}));
}}
}})();
""").strip()
@@ -463,23 +485,57 @@ class Compiler:
}})();
""").strip()
if op == "CLEAR":
sel = a[0].replace("'", "\\'")
return textwrap.dedent(f"""
(()=>{{
const el=document.querySelector('{sel}');
if(el && 'value' in el){{
el.value = '';
el.dispatchEvent(new Event('input',{{bubbles:true}}));
el.dispatchEvent(new Event('change',{{bubbles:true}}));
}}
}})();
""").strip()
if op == "SET" and len(a) == 2:
# This is SET for input fields (SET `#field` "value")
sel = a[0].replace("'", "\\'")
val = a[1].replace("'", "\\'")
return textwrap.dedent(f"""
(()=>{{
const el=document.querySelector('{sel}');
if(el && 'value' in el){{
el.value = '';
el.focus&&el.focus();
el.value = '{val}';
el.dispatchEvent(new Event('input',{{bubbles:true}}));
el.dispatchEvent(new Event('change',{{bubbles:true}}));
}}
}})();
""").strip()
if op in ("PRESS","KEYDOWN","KEYUP"):
key = a[0]
evs = {"PRESS":("keydown","keyup"),"KEYDOWN":("keydown",),"KEYUP":("keyup",)}[op]
return ";".join([f"document.dispatchEvent(new KeyboardEvent('{e}',{{key:'{key}',bubbles:true}}))" for e in evs]) + ";"
if op == "EVAL":
return a[0]
return textwrap.dedent(f"""
(()=>{{
try {{
{a[0]};
}} catch (e) {{
console.error('C4A-Script EVAL error:', e);
}}
}})();
""").strip()
if op == "IF":
condition, then_cmd, else_cmd = a
cond_type, cond_value = condition
# Generate condition JavaScript
if cond_type == "EXISTS":
js_condition = f"!!document.querySelector('{cond_value}')"
else: # JS condition
js_condition = cond_value
js_condition = self._emit_condition(condition)
# Generate commands - handle both regular commands and procedure calls
then_js = self._handle_cmd_or_proc(then_cmd)
@@ -531,6 +587,19 @@ class Compiler:
raise ValueError(f"Unhandled op {op}")
def _emit_condition(self, condition):
"""Convert a condition tuple to JavaScript"""
cond_type = condition[0]
if cond_type == "EXISTS":
return f"!!document.querySelector('{condition[1]}')"
elif cond_type == "NOT":
# Recursively handle the negated condition
inner_condition = self._emit_condition(condition[1])
return f"!({inner_condition})"
else: # JS condition
return condition[1]
def _handle_cmd_or_proc(self, cmd):
"""Handle a command that might be a regular command or a procedure call"""
if not cmd:
@@ -596,15 +665,13 @@ def compile_lines(lines: List[str], *, root: Union[pathlib.Path, None] = None) -
DEMO = """
# quick sanity demo
PROC login
CLICK `input[name="username"]`
TYPE $user
PRESS Tab
TYPE $pass
SET `input[name="username"]` $user
SET `input[name="password"]` $pass
CLICK `button.submit`
ENDPROC
SET user = "tom@crawl4ai.com"
SET pass = "hunter2"
SETVAR user = "tom@crawl4ai.com"
SETVAR pass = "hunter2"
GO https://example.com/login
WAIT `input[name="username"]` 10

View File

@@ -0,0 +1,246 @@
# 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.
## 🚀 Quick Start
### 1. Start the Tutorial Server
```bash
cd docs/examples/c4a_script/tutorial
python server.py
```
Then open your browser to: http://localhost:8080
### 2. Try Your First Script
```c4a
# Basic interaction
GO playground/
WAIT `body` 2
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept`
CLICK `#start-tutorial`
```
## 📚 What You'll Learn
### Basic Commands
- **Navigation**: `GO url`
- **Waiting**: `WAIT selector timeout` or `WAIT seconds`
- **Clicking**: `CLICK selector`
- **Typing**: `TYPE "text"`
- **Scrolling**: `SCROLL DOWN/UP amount`
### Control Flow
- **Conditionals**: `IF (condition) THEN action`
- **Loops**: `REPEAT (action, condition)`
- **Procedures**: Define reusable command sequences
### Advanced Features
- **JavaScript evaluation**: `EVAL code`
- **Variables**: `SET name = "value"`
- **Complex selectors**: CSS selectors in backticks
## 🎮 Interactive Playground Features
The tutorial includes a fully interactive web app with:
### 1. **Authentication System**
- Login form with validation
- Session management
- Protected content
### 2. **Dynamic Content**
- Infinite scroll products
- Pagination controls
- Load more buttons
### 3. **Complex Forms**
- Multi-step wizards
- Dynamic field visibility
- Form validation
### 4. **Interactive Elements**
- Tabs and accordions
- Modals and popups
- Expandable content
### 5. **Data Tables**
- Sortable columns
- Search functionality
- Export options
## 🛠️ Tutorial Features
### Live Code Editor
- Syntax highlighting
- Real-time compilation
- Error messages with suggestions
### JavaScript Output Viewer
- See generated JavaScript code
- Edit and test JS directly
- Understand the compilation
### Visual Execution
- Step-by-step progress
- Element highlighting
- Console output
### Example Scripts
Load pre-written examples demonstrating:
- Cookie banner handling
- Login workflows
- Infinite scroll automation
- Multi-step form completion
- Complex interaction sequences
## 📖 Tutorial Sections
### 1. Getting Started
Learn basic commands and syntax:
```c4a
GO https://example.com
WAIT `.content` 5
CLICK `.button`
```
### 2. Handling Dynamic Content
Master waiting strategies and conditionals:
```c4a
IF (EXISTS `.popup`) THEN CLICK `.close`
WAIT `.results` 10
```
### 3. Form Automation
Fill and submit forms:
```c4a
CLICK `#email`
TYPE "user@example.com"
CLICK `button[type="submit"]`
```
### 4. Advanced Workflows
Build complex automation flows:
```c4a
PROC login
CLICK `#username`
TYPE $username
CLICK `#password`
TYPE $password
CLICK `#login-btn`
ENDPROC
SET username = "demo"
SET password = "pass123"
login
```
## 🎯 Practice Challenges
### Challenge 1: Cookie & Popups
Handle the cookie banner and newsletter popup that appear on page load.
### Challenge 2: Complete Login
Successfully log into the application using the demo credentials.
### Challenge 3: Load All Products
Use infinite scroll to load all 100 products in the catalog.
### Challenge 4: Multi-step Survey
Complete the entire multi-step survey form.
### Challenge 5: Full Workflow
Create a script that logs in, browses products, and exports data.
## 💡 Tips & Tricks
### 1. Use Specific Selectors
```c4a
# Good - specific
CLICK `button.submit-order`
# Bad - too generic
CLICK `button`
```
### 2. Always Handle Popups
```c4a
# Check for common popups
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept`
IF (EXISTS `.newsletter-modal`) THEN CLICK `.close`
```
### 3. Add Appropriate Waits
```c4a
# Wait for elements before interacting
WAIT `.form` 5
CLICK `#submit`
```
### 4. Use Procedures for Reusability
```c4a
PROC handle_popups
IF (EXISTS `.popup`) THEN CLICK `.close`
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept`
ENDPROC
# Use anywhere
handle_popups
```
## 🔧 Troubleshooting
### Common Issues
1. **"Element not found"**
- Add a WAIT before clicking
- Check selector specificity
- Verify element exists with IF
2. **"Timeout waiting for selector"**
- Increase timeout value
- Check if element is dynamically loaded
- Verify selector is correct
3. **"Missing THEN keyword"**
- All IF statements need THEN
- Format: `IF (condition) THEN action`
## 🚀 Using with Crawl4AI
Once you've mastered C4A-Script in the tutorial, use it with Crawl4AI:
```python
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
config = CrawlerRunConfig(
url="https://example.com",
c4a_script="""
WAIT `.content` 5
IF (EXISTS `.load-more`) THEN CLICK `.load-more`
WAIT `.new-content` 3
"""
)
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(config=config)
```
## 📝 Example Scripts
Check the `scripts/` folder for complete examples:
- `01-basic-interaction.c4a` - Getting started
- `02-login-flow.c4a` - Authentication
- `03-infinite-scroll.c4a` - Dynamic content
- `04-multi-step-form.c4a` - Complex forms
- `05-complex-workflow.c4a` - Full automation
## 🤝 Contributing
Found a bug or have a suggestion? Please open an issue on GitHub!
---
Happy automating with C4A-Script! 🎉

View File

@@ -0,0 +1,667 @@
/* ================================================================
C4A-Script Tutorial - App Layout CSS
Terminal theme with Dank Mono font
================================================================ */
/* CSS Variables */
:root {
--bg-primary: #070708;
--bg-secondary: #0e0e10;
--bg-tertiary: #1a1a1b;
--border-color: #2a2a2c;
--border-hover: #3a3a3c;
--text-primary: #e0e0e0;
--text-secondary: #8b8b8d;
--text-muted: #606065;
--primary-color: #0fbbaa;
--primary-hover: #0da89a;
--primary-dim: #0a8577;
--error-color: #ff5555;
--warning-color: #ffb86c;
--success-color: #50fa7b;
--info-color: #8be9fd;
--code-bg: #1e1e20;
--modal-overlay: rgba(0, 0, 0, 0.8);
}
/* Base Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Fonts */
@font-face {
font-family: 'Dank Mono';
src: url('DankMono-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Dank Mono';
src: url('DankMono-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Dank Mono';
src: url('DankMono-Italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
}
/* Body & App Container */
body {
font-family: 'Dank Mono', 'Monaco', 'Consolas', monospace;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 14px;
line-height: 1.6;
overflow: hidden;
}
.app-container {
display: flex;
height: 100vh;
width: 100vw;
overflow: hidden;
}
/* Panels */
.editor-panel,
.playground-panel {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.editor-panel {
flex: 1;
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
min-width: 400px;
}
.playground-panel {
flex: 1;
background: var(--bg-primary);
min-width: 400px;
}
/* Panel Headers */
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.panel-header h2 {
font-size: 16px;
font-weight: 600;
color: var(--primary-color);
margin: 0;
}
.header-actions {
display: flex;
gap: 8px;
}
/* Action Buttons */
.action-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--bg-secondary);
color: var(--text-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
font-family: inherit;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
border-color: var(--border-hover);
}
.action-btn.primary {
background: var(--primary-color);
color: var(--bg-primary);
border-color: var(--primary-color);
}
.action-btn.primary:hover {
background: var(--primary-hover);
border-color: var(--primary-hover);
}
.action-btn .icon {
font-size: 16px;
}
/* Editor Wrapper */
.editor-wrapper {
flex: 1;
display: flex;
overflow: hidden;
position: relative;
z-index: 1; /* Ensure it's above any potential overlays */
}
.editor-wrapper .CodeMirror {
flex: 1;
height: 100%;
font-family: 'Dank Mono', monospace;
font-size: 14px;
line-height: 1.5;
}
/* Ensure CodeMirror is interactive */
.CodeMirror {
background: var(--bg-primary) !important;
}
.CodeMirror-scroll {
overflow: auto !important;
}
/* Make cursor more visible */
.CodeMirror-cursor {
border-left: 2px solid var(--primary-color) !important;
border-left-width: 2px !important;
opacity: 1 !important;
visibility: visible !important;
}
/* Ensure cursor is visible when focused */
.CodeMirror-focused .CodeMirror-cursor {
visibility: visible !important;
}
/* Fix for CodeMirror in flex container */
.CodeMirror-sizer {
min-height: auto !important;
}
/* Remove aggressive pointer-events override */
.CodeMirror-code {
cursor: text;
}
.editor-wrapper textarea {
display: none;
}
/* Output Section (Bottom of Editor) */
.output-section {
height: 250px;
border-top: 1px solid var(--border-color);
display: flex;
flex-direction: column;
flex-shrink: 0;
}
/* Tabs */
.tabs {
display: flex;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.tab {
padding: 8px 20px;
background: transparent;
color: var(--text-secondary);
border: none;
border-bottom: 2px solid transparent;
font-family: inherit;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.tab:hover {
color: var(--text-primary);
background: var(--bg-secondary);
}
.tab.active {
color: var(--primary-color);
border-bottom-color: var(--primary-color);
}
/* Tab Content */
.tab-content {
flex: 1;
overflow: hidden;
}
.tab-pane {
display: none;
height: 100%;
overflow-y: auto;
}
.tab-pane.active {
display: block;
}
/* Console */
.console {
padding: 12px;
background: var(--bg-primary);
font-size: 13px;
min-height: 100%;
}
.console-line {
margin-bottom: 8px;
display: flex;
align-items: flex-start;
gap: 8px;
}
.console-prompt {
color: var(--primary-color);
flex-shrink: 0;
}
.console-text {
color: var(--text-primary);
}
.console-error {
color: var(--error-color);
}
.console-warning {
color: var(--warning-color);
}
.console-success {
color: var(--success-color);
}
/* JavaScript Output */
.js-output-header {
display: flex;
justify-content: flex-end;
padding: 8px 12px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
}
.js-actions {
display: flex;
gap: 8px;
}
.mini-btn {
padding: 4px 8px;
background: var(--bg-secondary);
color: var(--text-secondary);
border: 1px solid var(--border-color);
border-radius: 3px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.mini-btn:hover {
background: var(--bg-primary);
color: var(--text-primary);
}
.js-output {
padding: 12px;
background: var(--code-bg);
color: var(--text-primary);
font-family: 'Dank Mono', monospace;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
margin: 0;
min-height: calc(100% - 44px);
}
/* Execution Progress */
.execution-progress {
padding: 12px;
background: var(--bg-primary);
}
.progress-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-size: 13px;
}
.progress-icon {
color: var(--text-muted);
}
.progress-item.active .progress-icon {
color: var(--info-color);
animation: pulse 1s infinite;
}
.progress-item.completed .progress-icon {
color: var(--success-color);
}
.progress-item.error .progress-icon {
color: var(--error-color);
}
/* Playground */
.playground-wrapper {
flex: 1;
overflow: hidden;
}
#playground-frame {
width: 100%;
height: 100%;
border: none;
background: var(--bg-secondary);
}
/* Tutorial Intro Modal */
.tutorial-intro-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--modal-overlay);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
transition: opacity 0.3s;
}
.tutorial-intro-modal.hidden {
display: none;
}
.intro-content {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 32px;
max-width: 500px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.6);
}
.intro-content h2 {
color: var(--primary-color);
margin-bottom: 16px;
font-size: 24px;
}
.intro-content p {
color: var(--text-primary);
margin-bottom: 16px;
line-height: 1.6;
}
.intro-content ul {
list-style: none;
margin-bottom: 24px;
}
.intro-content li {
color: var(--text-secondary);
margin-bottom: 8px;
padding-left: 20px;
position: relative;
}
.intro-content li:before {
content: "▸";
position: absolute;
left: 0;
color: var(--primary-color);
}
.intro-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.intro-btn {
padding: 10px 24px;
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
font-family: inherit;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.intro-btn:hover {
background: var(--bg-primary);
border-color: var(--border-hover);
}
.intro-btn.primary {
background: var(--primary-color);
color: var(--bg-primary);
border-color: var(--primary-color);
}
.intro-btn.primary:hover {
background: var(--primary-hover);
border-color: var(--primary-hover);
}
/* Tutorial Navigation Bar */
.tutorial-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--primary-color);
z-index: 1000;
transition: transform 0.3s;
}
.tutorial-nav.hidden {
transform: translateY(-100%);
}
.tutorial-nav-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
}
.tutorial-left {
flex: 1;
}
.tutorial-step-title {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 8px;
}
.tutorial-step-title span:first-child {
color: var(--text-secondary);
font-size: 12px;
text-transform: uppercase;
}
.tutorial-step-title span:last-child {
color: var(--primary-color);
font-weight: 600;
font-size: 16px;
}
.tutorial-description {
color: var(--text-primary);
margin: 0;
font-size: 14px;
max-width: 600px;
}
.tutorial-right {
display: flex;
align-items: center;
}
.tutorial-progress-bar {
height: 3px;
background: var(--bg-secondary);
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
.tutorial-progress-bar .progress-fill {
height: 100%;
background: var(--primary-color);
transition: width 0.3s;
}
/* Adjust app container when tutorial is active */
.app-container.tutorial-active {
padding-top: 80px;
}
.tutorial-controls {
display: flex;
gap: 12px;
}
.nav-btn {
padding: 8px 16px;
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
font-family: inherit;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.nav-btn:hover:not(:disabled) {
background: var(--bg-primary);
border-color: var(--border-hover);
}
.nav-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.nav-btn.primary {
background: var(--primary-color);
color: var(--bg-primary);
border-color: var(--primary-color);
}
.nav-btn.primary:hover {
background: var(--primary-hover);
border-color: var(--primary-hover);
}
.exit-btn {
width: 32px;
height: 32px;
background: transparent;
color: var(--text-secondary);
border: none;
font-size: 20px;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
margin-left: 16px;
}
.exit-btn:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
/* Fullscreen Mode */
.playground-panel.fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1500;
}
/* Animations */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Scrollbar Styling */
::-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);
}
/* Responsive */
@media (max-width: 768px) {
.app-container {
flex-direction: column;
}
.editor-panel,
.playground-panel {
min-width: auto;
width: 100%;
}
.editor-panel {
border-right: none;
border-bottom: 1px solid var(--border-color);
}
.output-section {
height: 200px;
}
}

View File

@@ -0,0 +1,625 @@
// C4A-Script Tutorial App Controller
class TutorialApp {
constructor() {
this.editor = null;
this.currentScript = '';
this.currentJS = [];
this.isEditingJS = false;
this.tutorialMode = false;
this.currentStep = 0;
this.tutorialSteps = [];
this.init();
}
init() {
this.setupEditors();
this.setupButtons();
this.setupTabs();
this.setupTutorial();
this.checkFirstVisit();
}
setupEditors() {
// C4A Script Editor
const c4aTextarea = document.getElementById('c4a-editor');
this.editor = CodeMirror.fromTextArea(c4aTextarea, {
mode: 'javascript', // Use JS mode for now
theme: 'material-darker',
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets: true,
readOnly: false,
cursorBlinkRate: 530,
inputStyle: 'contenteditable' // Changed from 'textarea' to prevent cursor issues
});
// Save script on change
this.editor.on('change', () => {
this.currentScript = this.editor.getValue();
localStorage.setItem('c4a-script', this.currentScript);
});
// Load saved script
const saved = localStorage.getItem('c4a-script');
if (saved && !this.tutorialMode) {
this.editor.setValue(saved);
}
// Ensure editor is properly sized and interactive
setTimeout(() => {
this.editor.refresh();
// Set cursor position instead of just focusing
const doc = this.editor.getDoc();
doc.setCursor(doc.lineCount() - 1, 0);
this.editor.focus();
}, 100);
// Single unified click handler for focus
const editorElement = this.editor.getWrapperElement();
editorElement.addEventListener('mousedown', (e) => {
// Use mousedown instead of click for immediate response
e.stopPropagation();
// Ensure editor gets focus on next tick
setTimeout(() => {
if (!this.editor.hasFocus()) {
this.editor.focus();
}
}, 0);
});
}
setupButtons() {
// Run button
document.getElementById('run-btn').addEventListener('click', () => {
this.runScript();
});
// Clear button
document.getElementById('clear-btn').addEventListener('click', () => {
this.editor.setValue('');
this.clearConsole();
});
// Examples button
document.getElementById('examples-btn').addEventListener('click', () => {
this.showExamples();
});
// Tutorial button
document.getElementById('tutorial-btn').addEventListener('click', () => {
this.showIntroModal();
});
// Copy JS button
document.getElementById('copy-js-btn').addEventListener('click', () => {
this.copyJS();
});
// Edit JS button
document.getElementById('edit-js-btn').addEventListener('click', () => {
this.toggleJSEdit();
});
// Reset playground
document.getElementById('reset-playground').addEventListener('click', () => {
this.resetPlayground();
});
// Fullscreen
document.getElementById('fullscreen-btn').addEventListener('click', () => {
this.toggleFullscreen();
});
// Intro modal buttons
document.getElementById('start-tutorial-btn').addEventListener('click', () => {
this.hideIntroModal();
this.startTutorial();
});
document.getElementById('skip-tutorial-btn').addEventListener('click', () => {
this.hideIntroModal();
});
// Tutorial navigation
document.getElementById('tutorial-prev').addEventListener('click', () => {
this.prevStep();
});
document.getElementById('tutorial-next').addEventListener('click', () => {
this.nextStep();
});
document.getElementById('tutorial-exit').addEventListener('click', () => {
this.exitTutorial();
});
}
setupTabs() {
const tabs = document.querySelectorAll('.tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const tabName = tab.getAttribute('data-tab');
this.switchTab(tabName);
});
});
// Remove execution tab since we're removing it
const progressTab = document.querySelector('[data-tab="progress"]');
if (progressTab) progressTab.remove();
}
switchTab(tabName) {
// Update tab buttons
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
// Update tab panes
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
document.getElementById(`${tabName}-tab`).classList.add('active');
}
checkFirstVisit() {
const hasVisited = localStorage.getItem('c4a-tutorial-visited');
if (!hasVisited) {
setTimeout(() => this.showIntroModal(), 500);
}
}
showIntroModal() {
document.getElementById('tutorial-intro').classList.remove('hidden');
}
hideIntroModal() {
document.getElementById('tutorial-intro').classList.add('hidden');
localStorage.setItem('c4a-tutorial-visited', 'true');
}
setupTutorial() {
this.tutorialSteps = [
{
title: "Welcome to C4A-Script!",
description: "C4A-Script is a simple language for web automation. Let's start by handling popups that appear on websites.",
script: "# Welcome to C4A-Script Tutorial!\n# Let's start by waiting for the page to load\n\nWAIT `body` 2",
validate: () => true
},
{
title: "Handle Cookie Banner",
description: "Check if cookie banner exists and accept it.",
script: "# Wait for page and handle cookie banner\nWAIT `body` 2\n\n# Check if cookie banner exists, then click accept\nIF (EXISTS `.cookie-banner`) THEN CLICK `.accept`",
validate: () => {
const iframe = document.getElementById('playground-frame');
return !iframe.contentDocument?.querySelector('.cookie-banner');
}
},
{
title: "Handle Newsletter Popup",
description: "Now let's handle the newsletter popup that appears after 3 seconds.",
script: "# Wait for newsletter popup to appear\nWAIT 3\n\n# Close the newsletter popup\nIF (EXISTS `#newsletter-popup`) THEN CLICK `.close`",
validate: () => {
const iframe = document.getElementById('playground-frame');
return !iframe.contentDocument?.querySelector('#newsletter-popup');
}
},
{
title: "Start Interactive Elements",
description: "Click the start button to reveal more interactive elements.",
script: "# Click the start tutorial button\nCLICK `#start-tutorial`",
validate: () => true
},
{
title: "Login Process",
description: "Now let's complete the login form with email and password using the new SET command.",
script: "# Login process\nCLICK `#login-btn`\nWAIT `.login-form` 2\n\n# Fill form fields using SET\nSET `#email` \"demo@example.com\"\nSET `#password` \"demo123\"\n\n# Submit the form\nCLICK `button[type=\"submit\"]`\nWAIT `.success` 2",
validate: () => {
const iframe = document.getElementById('playground-frame');
return iframe.contentDocument?.querySelector('.user-info')?.style.display === 'flex';
}
},
{
title: "Navigate and Scroll",
description: "Navigate to the catalog and use scrolling to load more products.",
script: "# Navigate to catalog\nCLICK `#catalog-link`\nWAIT `.product-grid` 3\n\n# Scroll to load more products\nSCROLL DOWN 500\nWAIT 1\nSCROLL DOWN 500\nWAIT 1\n\n# Apply a filter\nCLICK `.filter-group input[type=\"checkbox\"]`",
validate: () => true
},
{
title: "Advanced - Procedures",
description: "Create reusable command groups with PROC for common tasks.",
script: "# Define a procedure to check login status\nPROC check_login\n IF (NOT EXISTS `.user-info`) THEN CLICK `#login-btn`\n IF (NOT EXISTS `.user-info`) THEN WAIT `.login-form` 2\n IF (NOT EXISTS `.user-info`) THEN SET `#email` \"demo@example.com\"\n IF (NOT EXISTS `.user-info`) THEN SET `#password` \"demo123\"\n IF (NOT EXISTS `.user-info`) THEN CLICK `button[type=\"submit\"]`\nENDPROC\n\n# Example: Navigate between sections\nCLICK `a[href=\"#tabs\"]`\nWAIT 1\nCLICK `a[href=\"#forms\"]`\nWAIT 1\n\n# If we get logged out, use our procedure\ncheck_login",
validate: () => true
},
{
title: "More Commands",
description: "Explore additional C4A commands with the Forms section.",
script: "# First, let's fill the contact form with variables\n\n# Set variables\nSETVAR name = \"Alice Smith\"\nSETVAR msg = \"I'd like to know more about your Premium plan!\"\n\n# Fill contact form\nSET `#contact-name` $name\nSET `#contact-email` \"alice@example.com\"\nSET `#contact-message` $msg\n\n# Select dropdown option\nCLICK `#contact-subject`\nCLICK `option[value=\"support\"]`\nWAIT 0.5\n\n# Submit contact form\nCLICK `.btn-primary`\nWAIT 1\n\n# Now let's do the multi-step survey\nSCROLL DOWN 400\nWAIT 0.5\n\n# Step 1: Personal info\nSET `#full-name` \"Bob Johnson\"\nSET `#survey-email` \"bob@example.com\"\nCLICK `.next-step`\nWAIT 0.5\n\n# Step 2: Select interests (multi-select)\nCLICK `#interests`\nCLICK `option[value=\"technology\"]`\nCLICK `option[value=\"science\"]`\nCLICK `.next-step`\nWAIT 0.5\n\n# Step 3: Submit survey\nCLICK `#submit-survey`\n\n# Check results with JavaScript\nEVAL `console.log('Forms completed successfully!')`",
validate: () => true
},
{
title: "Congratulations!",
description: "You've mastered C4A-Script basics! You can now automate complex web interactions. Try the examples or create your own scripts.",
script: "# 🎉 You've completed the tutorial!\n\n# You learned:\n# ✓ WAIT - Wait for elements or time\n# ✓ IF/THEN - Conditional actions\n# ✓ NOT - Negate conditions\n# ✓ CLICK - Click elements\n# ✓ SET - Set input field values\n# ✓ SETVAR - Create variables\n# ✓ SCROLL - Scroll the page\n# ✓ PROC - Create procedures\n# ✓ And much more!\n\n# Try the Examples button for more scripts\n# Happy automating with C4A-Script!",
validate: () => true
}
];
}
startTutorial() {
this.tutorialMode = true;
this.currentStep = 0;
document.getElementById('tutorial-nav').classList.remove('hidden');
document.querySelector('.app-container').classList.add('tutorial-active');
this.showStep(0);
}
exitTutorial() {
this.tutorialMode = false;
document.getElementById('tutorial-nav').classList.add('hidden');
document.querySelector('.app-container').classList.remove('tutorial-active');
}
showStep(index) {
const step = this.tutorialSteps[index];
if (!step) return;
// Update navigation UI
document.getElementById('tutorial-step-info').textContent = `Step ${index + 1} of ${this.tutorialSteps.length}`;
document.getElementById('tutorial-title').textContent = step.title;
// Update progress bar
const progress = ((index + 1) / this.tutorialSteps.length) * 100;
document.getElementById('tutorial-progress-fill').style.width = `${progress}%`;
// Update buttons
document.getElementById('tutorial-prev').disabled = index === 0;
const nextBtn = document.getElementById('tutorial-next');
if (index === this.tutorialSteps.length - 1) {
nextBtn.textContent = 'Finish';
} else {
nextBtn.textContent = 'Next →';
}
// Set script
this.editor.setValue(step.script);
// Update description in nav bar
document.getElementById('tutorial-description').textContent = step.description;
// Focus editor after setting content and ensure it's editable
// Use requestAnimationFrame for better timing
requestAnimationFrame(() => {
this.editor.setOption('readOnly', false);
this.editor.refresh();
// Place cursor at end first, then focus
const doc = this.editor.getDoc();
const lastLine = doc.lineCount() - 1;
const lastCh = doc.getLine(lastLine).length;
doc.setCursor(lastLine, lastCh);
this.editor.focus();
});
}
// Removed showTooltip method - no longer needed
nextStep() {
if (this.currentStep < this.tutorialSteps.length - 1) {
this.currentStep++;
this.showStep(this.currentStep);
} else {
this.exitTutorial();
}
}
prevStep() {
if (this.currentStep > 0) {
this.currentStep--;
this.showStep(this.currentStep);
}
}
async runScript() {
const script = this.editor.getValue();
if (!script.trim()) {
this.addConsoleMessage('No script to run', 'error');
return;
}
this.clearConsole();
this.switchTab('console');
this.addConsoleMessage('Compiling C4A script...');
try {
// If in JS edit mode, use the edited JS directly
if (this.isEditingJS) {
const jsCode = document.getElementById('js-output').textContent.split('\n').filter(line => line.trim());
await this.executeJS(jsCode);
return;
}
// Compile C4A to JS
const compiled = await this.compileScript(script);
if (compiled.success) {
this.currentJS = compiled.jsCode;
this.displayJS(compiled.jsCode);
this.addConsoleMessage(`✓ Compiled successfully (${compiled.jsCode.length} statements)`, 'success');
// Execute the JS
await this.executeJS(compiled.jsCode);
} else {
// Show compilation error
const error = compiled.error;
this.addConsoleMessage(`✗ Compilation error at line ${error.line}:${error.column}`, 'error');
this.addConsoleMessage(` ${error.message}`, 'error');
if (error.suggestion) {
this.addConsoleMessage(` 💡 ${error.suggestion}`, 'warning');
}
}
} catch (error) {
this.addConsoleMessage(`Error: ${error.message}`, 'error');
}
}
async compileScript(script) {
try {
const response = await fetch('/api/compile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ script })
});
if (!response.ok) {
throw new Error('Compilation service unavailable');
}
return await response.json();
} catch (error) {
// Return error if compilation service is unavailable
return {
success: false,
error: {
line: 1,
column: 1,
message: "C4A compilation service unavailable. Please ensure the server is running.",
suggestion: "Start the C4A server or check your connection"
}
};
}
}
displayJS(jsCode) {
const formatted = jsCode.map((line, i) =>
`${(i + 1).toString().padStart(2, ' ')}. ${line}`
).join('\n');
document.getElementById('js-output').textContent = formatted;
}
async executeJS(jsCode) {
this.addConsoleMessage('Executing JavaScript...');
// Send all code to iframe to execute at once
await this.executeInIframe(jsCode);
}
async executeInIframe(jsCode) {
const iframe = document.getElementById('playground-frame');
const iframeWindow = iframe.contentWindow;
// Create a unique ID for this execution
const executionId = 'exec_' + Date.now();
// Create the full script to execute in iframe
const fullScript = `
(async () => {
const results = [];
try {
${jsCode.map((code, i) => `
try {
${code}
results.push({ index: ${i}, success: true, code: ${JSON.stringify(code)} });
} catch (error) {
results.push({ index: ${i}, success: false, error: error.message, code: ${JSON.stringify(code)} });
throw error; // Stop execution on first error
}
`).join('\n')}
// Send success message
window.parent.postMessage({
type: 'c4a-execution-complete',
id: '${executionId}',
success: true,
results: results
}, '*');
} catch (error) {
// Send error message
window.parent.postMessage({
type: 'c4a-execution-complete',
id: '${executionId}',
success: false,
error: error.message,
results: results
}, '*');
}
})();
`;
// Wait for execution result
return new Promise((resolve, reject) => {
const messageHandler = (event) => {
if (event.data.type === 'c4a-execution-complete' && event.data.id === executionId) {
window.removeEventListener('message', messageHandler);
// Log results
if (event.data.results) {
event.data.results.forEach(result => {
if (result.success) {
this.addConsoleMessage(`${result.code}`, 'success');
} else {
this.addConsoleMessage(`${result.code}`, 'error');
this.addConsoleMessage(` Error: ${result.error}`, 'error');
}
});
}
if (event.data.success) {
this.addConsoleMessage('Execution completed successfully', 'success');
resolve();
} else {
this.addConsoleMessage('Execution stopped due to error', 'error');
resolve(); // Still resolve to not break the flow
}
}
};
window.addEventListener('message', messageHandler);
// Inject and execute the script
const script = iframe.contentDocument.createElement('script');
script.textContent = fullScript;
iframe.contentDocument.body.appendChild(script);
script.remove();
// Timeout after 10 seconds
setTimeout(() => {
window.removeEventListener('message', messageHandler);
this.addConsoleMessage('Execution timeout', 'warning');
resolve();
}, 10000);
});
}
highlightElement(element) {
const originalBorder = element.style.border;
const originalBackground = element.style.backgroundColor;
element.style.border = '2px solid #0fbbaa';
element.style.backgroundColor = 'rgba(15, 187, 170, 0.1)';
setTimeout(() => {
element.style.border = originalBorder;
element.style.backgroundColor = originalBackground;
}, 1000);
}
// Removed progress methods - everything goes to console now
addConsoleMessage(message, type = 'text') {
const consoleEl = document.getElementById('console-output');
const line = document.createElement('div');
line.className = 'console-line';
line.innerHTML = `
<span class="console-prompt">$</span>
<span class="console-${type}">${message}</span>
`;
consoleEl.appendChild(line);
// Scroll the console container (parent of console-output)
const consoleContainer = consoleEl.parentElement;
if (consoleContainer) {
// Use requestAnimationFrame to ensure DOM has updated
requestAnimationFrame(() => {
consoleContainer.scrollTop = consoleContainer.scrollHeight;
});
}
}
clearConsole() {
document.getElementById('console-output').innerHTML = `
<div class="console-line">
<span class="console-prompt">$</span>
<span class="console-text">Ready to run C4A scripts...</span>
</div>
`;
}
copyJS() {
const text = this.currentJS.join('\n');
navigator.clipboard.writeText(text).then(() => {
this.addConsoleMessage('JavaScript copied to clipboard', 'success');
});
}
toggleJSEdit() {
this.isEditingJS = !this.isEditingJS;
const editBtn = document.getElementById('edit-js-btn');
const jsOutput = document.getElementById('js-output');
if (this.isEditingJS) {
editBtn.innerHTML = '<span>💾</span>';
jsOutput.contentEditable = true;
jsOutput.style.outline = '1px solid #0fbbaa';
this.addConsoleMessage('JS edit mode enabled - modify and run', 'warning');
} else {
editBtn.innerHTML = '<span>✏️</span>';
jsOutput.contentEditable = false;
jsOutput.style.outline = 'none';
this.addConsoleMessage('JS edit mode disabled', 'text');
}
}
resetPlayground() {
const iframe = document.getElementById('playground-frame');
iframe.src = iframe.src;
this.addConsoleMessage('Playground reset', 'success');
}
toggleFullscreen() {
const playgroundPanel = document.querySelector('.playground-panel');
playgroundPanel.classList.toggle('fullscreen');
}
showExamples() {
const examples = [
{
name: 'Quick Start - Handle Popups',
script: `# Handle popups quickly\nWAIT \`body\` 2\nIF (EXISTS \`.cookie-banner\`) THEN CLICK \`.accept\`\nWAIT 3\nIF (EXISTS \`#newsletter-popup\`) THEN CLICK \`.close\``
},
{
name: 'Complete Login Flow',
script: `# Full login process\nCLICK \`#login-btn\`\nWAIT \`.login-form\` 2\n\n# Set credentials using the new SET command\nSET \`#email\` "demo@example.com"\nSET \`#password\` "demo123"\n\n# Submit\nCLICK \`button[type="submit"]\`\nWAIT \`.success\` 2`
},
{
name: 'Product Browsing',
script: `# Browse products with filters\nCLICK \`#catalog-link\`\nWAIT \`.product-grid\` 3\n\n# Apply filters\nCLICK \`.filter-button\`\nWAIT 0.5\nCLICK \`input[value="electronics"]\`\n\n# Load more products\nREPEAT (SCROLL DOWN 800, 3)\nWAIT 1`
},
{
name: 'Advanced Form Wizard',
script: `# Multi-step form with validation\nCLICK \`a[href="#forms"]\`\nWAIT \`#survey-form\` 2\n\n# Step 1: Personal Info\nSET \`#full-name\` "John Doe"\nSET \`#survey-email\` "john@example.com"\nCLICK \`.next-step\`\nWAIT 1\n\n# Step 2: Preferences\nCLICK \`#interests\`\nCLICK \`option[value="tech"]\`\nCLICK \`option[value="music"]\`\nCLICK \`.next-step\`\nWAIT 1\n\n# Step 3: Submit\nIF (EXISTS \`#comments\`) THEN SET \`#comments\` "Great experience!"\nCLICK \`#submit-survey\`\nWAIT \`.success-message\` 3`
}
];
const currentIndex = parseInt(localStorage.getItem('exampleIndex') || '0');
const example = examples[currentIndex % examples.length];
this.editor.setValue(example.script);
this.addConsoleMessage(`Loaded example: ${example.name}`, 'success');
localStorage.setItem('exampleIndex', ((currentIndex + 1) % examples.length).toString());
}
wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Add animations CSS
const style = document.createElement('style');
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeOut {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(-10px); }
}
`;
document.head.appendChild(style);
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.tutorialApp = new TutorialApp();
console.log('🎓 C4A-Script Tutorial initialized!');
});

View File

@@ -0,0 +1,531 @@
/* DankMono Font Faces */
@font-face {
font-family: 'DankMono';
src: url('DankMono-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'DankMono';
src: url('DankMono-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'DankMono';
src: url('DankMono-Italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
}
/* Root Variables - Matching docs theme */
:root {
--global-font-size: 14px;
--global-code-font-size: 13px;
--global-line-height: 1.5em;
--global-space: 10px;
--font-stack: DankMono, Monaco, Courier New, monospace;
--mono-font-stack: DankMono, Monaco, Courier New, monospace;
--background-color: #070708;
--font-color: #e8e9ed;
--invert-font-color: #222225;
--secondary-color: #d5cec0;
--tertiary-color: #a3abba;
--primary-color: #0fbbaa;
--error-color: #ff3c74;
--progress-bar-background: #3f3f44;
--progress-bar-fill: #09b5a5;
--code-bg-color: #3f3f44;
--block-background-color: #202020;
--header-height: 55px;
}
/* Base Styles */
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: var(--font-stack);
font-size: var(--global-font-size);
line-height: var(--global-line-height);
color: var(--font-color);
background-color: var(--background-color);
}
/* Terminal Framework */
.terminal {
min-height: 100vh;
}
.container {
width: 100%;
margin: 0 auto;
}
/* Header */
.header-container {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--header-height);
background-color: var(--background-color);
border-bottom: 1px solid var(--progress-bar-background);
z-index: 1000;
padding: 0 calc(var(--global-space) * 2);
}
.terminal-nav {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
}
.terminal-logo h1 {
margin: 0;
font-size: 1.2em;
color: var(--primary-color);
font-weight: 400;
}
.terminal-menu ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
gap: 2em;
}
.terminal-menu a {
color: var(--secondary-color);
text-decoration: none;
transition: color 0.2s;
}
.terminal-menu a:hover,
.terminal-menu a.active {
color: var(--primary-color);
}
/* Main Container */
.main-container {
padding-top: calc(var(--header-height) + 2em);
padding-left: 2em;
padding-right: 2em;
max-width: 1400px;
margin: 0 auto;
}
/* Tutorial Grid */
.tutorial-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2em;
align-items: start;
}
/* Terminal Cards */
.terminal-card {
background-color: var(--block-background-color);
border: 1px solid var(--progress-bar-background);
margin-bottom: 1.5em;
}
.terminal-card header {
background-color: var(--progress-bar-background);
padding: 0.8em 1em;
font-weight: 700;
color: var(--font-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.terminal-card > div {
padding: 1.5em;
}
/* Editor Section */
.editor-controls {
display: flex;
gap: 0.5em;
}
.editor-container {
height: 300px;
overflow: hidden;
}
#c4a-editor {
width: 100%;
height: 100%;
font-family: var(--mono-font-stack);
font-size: var(--global-code-font-size);
background-color: var(--code-bg-color);
color: var(--font-color);
border: none;
padding: 1em;
resize: none;
}
/* JS Output */
.js-output-container {
max-height: 300px;
overflow-y: auto;
}
.js-output-container pre {
margin: 0;
padding: 1em;
background-color: var(--code-bg-color);
}
.js-output-container code {
font-family: var(--mono-font-stack);
font-size: var(--global-code-font-size);
color: var(--font-color);
white-space: pre-wrap;
}
/* Console Output */
.console-output {
font-family: var(--mono-font-stack);
font-size: var(--global-code-font-size);
max-height: 200px;
overflow-y: auto;
padding: 1em;
}
.console-line {
margin-bottom: 0.5em;
}
.console-prompt {
color: var(--primary-color);
margin-right: 0.5em;
}
.console-text {
color: var(--font-color);
}
.console-error {
color: var(--error-color);
}
.console-success {
color: var(--primary-color);
}
/* Playground */
.playground-container {
height: 600px;
background-color: #fff;
border: 1px solid var(--progress-bar-background);
}
#playground-frame {
width: 100%;
height: 100%;
border: none;
}
/* Execution Progress */
.execution-progress {
padding: 1em;
}
.progress-item {
display: flex;
align-items: center;
gap: 0.8em;
margin-bottom: 0.8em;
color: var(--secondary-color);
}
.progress-item.active {
color: var(--primary-color);
}
.progress-item.completed {
color: var(--tertiary-color);
}
.progress-item.error {
color: var(--error-color);
}
.progress-icon {
font-size: 1.2em;
}
/* Buttons */
.btn {
background-color: var(--primary-color);
color: var(--background-color);
border: none;
padding: 0.5em 1em;
font-family: var(--font-stack);
font-size: 0.9em;
cursor: pointer;
transition: all 0.2s;
}
.btn:hover {
background-color: var(--progress-bar-fill);
}
.btn-sm {
padding: 0.3em 0.8em;
font-size: 0.85em;
}
.btn-ghost {
background-color: transparent;
color: var(--secondary-color);
border: 1px solid var(--progress-bar-background);
}
.btn-ghost:hover {
background-color: var(--progress-bar-background);
color: var(--font-color);
}
/* Scrollbars */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--block-background-color);
}
::-webkit-scrollbar-thumb {
background: var(--progress-bar-background);
}
::-webkit-scrollbar-thumb:hover {
background: var(--secondary-color);
}
/* CodeMirror Theme Override */
.CodeMirror {
font-family: var(--mono-font-stack) !important;
font-size: var(--global-code-font-size) !important;
background-color: var(--code-bg-color) !important;
color: var(--font-color) !important;
height: 100% !important;
}
.CodeMirror-gutters {
background-color: var(--progress-bar-background) !important;
border-right: 1px solid var(--progress-bar-background) !important;
}
/* Responsive */
@media (max-width: 1200px) {
.tutorial-grid {
grid-template-columns: 1fr;
}
.playground-section {
order: -1;
}
}
/* Links */
a {
color: var(--primary-color);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Lists */
ul, ol {
padding-left: 2em;
}
li {
margin-bottom: 0.5em;
}
/* Code */
code {
background-color: var(--code-bg-color);
padding: 0.2em 0.4em;
font-family: var(--mono-font-stack);
font-size: 0.9em;
}
/* Headings */
h1, h2, h3, h4, h5, h6 {
font-weight: 700;
margin-top: 1.5em;
margin-bottom: 0.8em;
}
h3 {
color: var(--primary-color);
font-size: 1.1em;
}
/* Tutorial Panel */
.tutorial-panel {
position: absolute;
top: 60px;
right: 20px;
width: 380px;
background: #1a1a1b;
border: 1px solid #2a2a2c;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
z-index: 1000;
transition: all 0.3s ease;
}
.tutorial-panel.hidden {
display: none;
}
.tutorial-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #2a2a2c;
}
.tutorial-header h3 {
margin: 0;
color: #0fbbaa;
font-size: 18px;
}
.close-btn {
background: none;
border: none;
color: #8b8b8d;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.close-btn:hover {
background: #2a2a2c;
color: #e0e0e0;
}
.tutorial-content {
padding: 20px;
}
.tutorial-content p {
margin: 0 0 16px 0;
color: #e0e0e0;
line-height: 1.6;
}
.tutorial-progress {
margin-top: 16px;
}
.tutorial-progress span {
display: block;
margin-bottom: 8px;
color: #8b8b8d;
font-size: 12px;
text-transform: uppercase;
}
.progress-bar {
height: 4px;
background: #2a2a2c;
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #0fbbaa;
transition: width 0.3s ease;
}
.tutorial-actions {
display: flex;
gap: 12px;
padding: 0 20px 20px;
}
.tutorial-btn {
flex: 1;
padding: 10px 16px;
background: #2a2a2c;
color: #e0e0e0;
border: 1px solid #3a3a3c;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.tutorial-btn:hover:not(:disabled) {
background: #3a3a3c;
transform: translateY(-1px);
}
.tutorial-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.tutorial-btn.primary {
background: #0fbbaa;
color: #070708;
border-color: #0fbbaa;
}
.tutorial-btn.primary:hover {
background: #0da89a;
border-color: #0da89a;
}
/* Tutorial Highlights */
.tutorial-highlight {
position: relative;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(15, 187, 170, 0.4);
}
50% {
box-shadow: 0 0 0 10px rgba(15, 187, 170, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(15, 187, 170, 0);
}
}
.editor-card {
position: relative;
}

View File

@@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<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="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>
<body>
<!-- Tutorial Intro Modal -->
<div id="tutorial-intro" class="tutorial-intro-modal">
<div class="intro-content">
<h2>Welcome to C4A-Script Tutorial!</h2>
<p>C4A-Script is a simple language for web automation. This interactive tutorial will teach you:</p>
<ul>
<li>How to handle popups and banners</li>
<li>Form filling and navigation</li>
<li>Advanced automation techniques</li>
</ul>
<div class="intro-actions">
<button id="start-tutorial-btn" class="intro-btn primary">Start Tutorial</button>
<button id="skip-tutorial-btn" class="intro-btn">Skip</button>
</div>
</div>
</div>
<!-- Main App Layout -->
<div class="app-container">
<!-- Left Panel: Editor -->
<div class="editor-panel">
<div class="panel-header">
<h2>C4A-Script Editor</h2>
<div class="header-actions">
<button id="tutorial-btn" class="action-btn" title="Tutorial">
<span class="icon">📚</span>
</button>
<button id="examples-btn" class="action-btn" title="Examples">
<span class="icon">📋</span>
</button>
<button id="clear-btn" class="action-btn" title="Clear">
<span class="icon">🗑</span>
</button>
<button id="run-btn" class="action-btn primary">
<span class="icon"></span>Run
</button>
</div>
</div>
<div class="editor-wrapper">
<textarea id="c4a-editor" placeholder="# Write your C4A script here..."></textarea>
</div>
<!-- Bottom: Output Tabs -->
<div class="output-section">
<div class="tabs">
<button class="tab active" data-tab="console">Console</button>
<button class="tab" data-tab="javascript">Generated JS</button>
</div>
<div class="tab-content">
<div id="console-tab" class="tab-pane active">
<div id="console-output" class="console">
<div class="console-line">
<span class="console-prompt">$</span>
<span class="console-text">Ready to run C4A scripts...</span>
</div>
</div>
</div>
<div id="javascript-tab" class="tab-pane">
<div class="js-output-header">
<div class="js-actions">
<button id="copy-js-btn" class="mini-btn" title="Copy">
<span>📋</span>
</button>
<button id="edit-js-btn" class="mini-btn" title="Edit">
<span>✏️</span>
</button>
</div>
</div>
<pre id="js-output" class="js-output">// JavaScript will appear here...</pre>
</div>
</div>
</div>
</div>
<!-- Right Panel: Playground -->
<div class="playground-panel">
<div class="panel-header">
<h2>Playground</h2>
<div class="header-actions">
<button id="reset-playground" class="action-btn" title="Reset">
<span class="icon">🔄</span>
</button>
<button id="fullscreen-btn" class="action-btn" title="Fullscreen">
<span class="icon"></span>
</button>
</div>
</div>
<div class="playground-wrapper">
<iframe id="playground-frame" src="playground/" title="Playground"></iframe>
</div>
</div>
</div>
<!-- Tutorial Navigation Bar -->
<div id="tutorial-nav" class="tutorial-nav hidden">
<div class="tutorial-nav-content">
<div class="tutorial-left">
<div class="tutorial-step-title">
<span id="tutorial-step-info">Step 1 of 9</span>
<span id="tutorial-title">Welcome</span>
</div>
<p id="tutorial-description" class="tutorial-description">Let's start by waiting for the page to load.</p>
</div>
<div class="tutorial-right">
<div class="tutorial-controls">
<button id="tutorial-prev" class="nav-btn" disabled>← Previous</button>
<button id="tutorial-next" class="nav-btn primary">Next →</button>
</div>
<button id="tutorial-exit" class="exit-btn" title="Exit Tutorial">×</button>
</div>
</div>
<div class="tutorial-progress-bar">
<div id="tutorial-progress-fill" class="progress-fill"></div>
</div>
</div>
<!-- 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>
<script src="assets/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,604 @@
// Playground App JavaScript
class PlaygroundApp {
constructor() {
this.isLoggedIn = false;
this.currentSection = 'home';
this.productsLoaded = 0;
this.maxProducts = 100;
this.tableRowsLoaded = 10;
this.inspectorMode = false;
this.tooltip = null;
this.init();
}
init() {
this.setupCookieBanner();
this.setupNewsletterPopup();
this.setupNavigation();
this.setupAuth();
this.setupProductCatalog();
this.setupForms();
this.setupTabs();
this.setupDataTable();
this.setupInspector();
this.loadInitialData();
}
// Cookie Banner
setupCookieBanner() {
const banner = document.getElementById('cookie-banner');
const acceptBtn = banner.querySelector('.accept');
const declineBtn = banner.querySelector('.decline');
acceptBtn.addEventListener('click', () => {
banner.style.display = 'none';
console.log('✅ Cookies accepted');
});
declineBtn.addEventListener('click', () => {
banner.style.display = 'none';
console.log('❌ Cookies declined');
});
}
// Newsletter Popup
setupNewsletterPopup() {
const popup = document.getElementById('newsletter-popup');
const closeBtn = popup.querySelector('.close');
const subscribeBtn = popup.querySelector('.subscribe');
// Show popup after 3 seconds
setTimeout(() => {
popup.style.display = 'flex';
}, 3000);
closeBtn.addEventListener('click', () => {
popup.style.display = 'none';
});
subscribeBtn.addEventListener('click', () => {
const email = popup.querySelector('input').value;
if (email) {
console.log(`📧 Subscribed: ${email}`);
popup.style.display = 'none';
}
});
// Close on outside click
popup.addEventListener('click', (e) => {
if (e.target === popup) {
popup.style.display = 'none';
}
});
}
// Navigation
setupNavigation() {
const navLinks = document.querySelectorAll('.nav-link');
const sections = document.querySelectorAll('.section');
navLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const targetId = link.getAttribute('href').substring(1);
// Update active states
navLinks.forEach(l => l.classList.remove('active'));
link.classList.add('active');
// Show target section
sections.forEach(s => s.classList.remove('active'));
const targetSection = document.getElementById(targetId);
if (targetSection) {
targetSection.classList.add('active');
this.currentSection = targetId;
// Load content for specific sections
this.loadSectionContent(targetId);
}
});
});
// Start tutorial button
const startBtn = document.getElementById('start-tutorial');
if (startBtn) {
startBtn.addEventListener('click', () => {
console.log('🚀 Tutorial started!');
alert('Tutorial started! Check the console for progress.');
});
}
}
// Authentication
setupAuth() {
const loginBtn = document.getElementById('login-btn');
const logoutBtn = document.getElementById('logout-btn');
const loginModal = document.getElementById('login-modal');
const loginForm = document.getElementById('login-form');
const closeBtn = loginModal.querySelector('.close');
loginBtn.addEventListener('click', () => {
loginModal.style.display = 'flex';
});
closeBtn.addEventListener('click', () => {
loginModal.style.display = 'none';
});
loginForm.addEventListener('submit', (e) => {
e.preventDefault();
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
const rememberMe = document.getElementById('remember-me').checked;
const messageEl = document.getElementById('login-message');
// Simple validation
if (email === 'demo@example.com' && password === 'demo123') {
this.isLoggedIn = true;
messageEl.textContent = '✅ Login successful!';
messageEl.className = 'form-message success';
setTimeout(() => {
loginModal.style.display = 'none';
document.getElementById('login-btn').style.display = 'none';
document.getElementById('user-info').style.display = 'flex';
document.getElementById('username-display').textContent = 'Demo User';
console.log(`✅ Logged in${rememberMe ? ' (remembered)' : ''}`);
}, 1000);
} else {
messageEl.textContent = '❌ Invalid credentials. Try demo@example.com / demo123';
messageEl.className = 'form-message error';
}
});
logoutBtn.addEventListener('click', () => {
this.isLoggedIn = false;
document.getElementById('login-btn').style.display = 'block';
document.getElementById('user-info').style.display = 'none';
console.log('👋 Logged out');
});
// Close modal on outside click
loginModal.addEventListener('click', (e) => {
if (e.target === loginModal) {
loginModal.style.display = 'none';
}
});
}
// Product Catalog
setupProductCatalog() {
// View toggle
const infiniteBtn = document.getElementById('infinite-scroll-btn');
const paginationBtn = document.getElementById('pagination-btn');
const infiniteView = document.getElementById('infinite-scroll-view');
const paginationView = document.getElementById('pagination-view');
infiniteBtn.addEventListener('click', () => {
infiniteBtn.classList.add('active');
paginationBtn.classList.remove('active');
infiniteView.style.display = 'block';
paginationView.style.display = 'none';
this.setupInfiniteScroll();
});
paginationBtn.addEventListener('click', () => {
paginationBtn.classList.add('active');
infiniteBtn.classList.remove('active');
paginationView.style.display = 'block';
infiniteView.style.display = 'none';
});
// Load more button
const loadMoreBtn = paginationView.querySelector('.load-more');
loadMoreBtn.addEventListener('click', () => {
this.loadMoreProducts();
});
// Collapsible filters
const collapsibles = document.querySelectorAll('.collapsible');
collapsibles.forEach(header => {
header.addEventListener('click', () => {
const content = header.nextElementSibling;
const toggle = header.querySelector('.toggle');
content.style.display = content.style.display === 'none' ? 'block' : 'none';
toggle.textContent = content.style.display === 'none' ? '▶' : '▼';
});
});
}
setupInfiniteScroll() {
const container = document.querySelector('.products-container');
const loadingIndicator = document.getElementById('loading-indicator');
container.addEventListener('scroll', () => {
if (container.scrollTop + container.clientHeight >= container.scrollHeight - 100) {
if (this.productsLoaded < this.maxProducts) {
loadingIndicator.style.display = 'block';
setTimeout(() => {
this.loadMoreProducts();
loadingIndicator.style.display = 'none';
}, 1000);
}
}
});
}
loadMoreProducts() {
const grid = document.getElementById('product-grid');
const batch = 10;
for (let i = 0; i < batch && this.productsLoaded < this.maxProducts; i++) {
const product = this.createProductCard(this.productsLoaded + 1);
grid.appendChild(product);
this.productsLoaded++;
}
console.log(`📦 Loaded ${batch} more products. Total: ${this.productsLoaded}`);
}
createProductCard(id) {
const card = document.createElement('div');
card.className = 'product-card';
card.innerHTML = `
<div class="product-image">📦</div>
<div class="product-name">Product ${id}</div>
<div class="product-price">$${(Math.random() * 100 + 10).toFixed(2)}</div>
<button class="btn btn-sm">Quick View</button>
`;
// Quick view functionality
const quickViewBtn = card.querySelector('button');
quickViewBtn.addEventListener('click', () => {
alert(`Quick view for Product ${id}`);
});
return card;
}
// Forms
setupForms() {
// Contact Form
const contactForm = document.getElementById('contact-form');
const subjectSelect = document.getElementById('contact-subject');
const departmentGroup = document.getElementById('department-group');
const departmentSelect = document.getElementById('department');
subjectSelect.addEventListener('change', () => {
if (subjectSelect.value === 'support') {
departmentGroup.style.display = 'block';
departmentSelect.innerHTML = `
<option value="">Select department</option>
<option value="technical">Technical Support</option>
<option value="billing">Billing Support</option>
<option value="general">General Support</option>
`;
} else {
departmentGroup.style.display = 'none';
}
});
contactForm.addEventListener('submit', (e) => {
e.preventDefault();
const messageDisplay = document.getElementById('contact-message-display');
messageDisplay.textContent = '✅ Message sent successfully!';
messageDisplay.className = 'form-message success';
console.log('📧 Contact form submitted');
});
// Multi-step Form
const surveyForm = document.getElementById('survey-form');
const steps = surveyForm.querySelectorAll('.form-step');
const progressFill = document.getElementById('progress-fill');
let currentStep = 1;
surveyForm.addEventListener('click', (e) => {
if (e.target.classList.contains('next-step')) {
if (currentStep < 3) {
steps[currentStep - 1].style.display = 'none';
currentStep++;
steps[currentStep - 1].style.display = 'block';
progressFill.style.width = `${(currentStep / 3) * 100}%`;
}
} else if (e.target.classList.contains('prev-step')) {
if (currentStep > 1) {
steps[currentStep - 1].style.display = 'none';
currentStep--;
steps[currentStep - 1].style.display = 'block';
progressFill.style.width = `${(currentStep / 3) * 100}%`;
}
}
});
surveyForm.addEventListener('submit', (e) => {
e.preventDefault();
document.getElementById('survey-success').style.display = 'block';
console.log('📋 Survey submitted successfully!');
});
}
// Tabs
setupTabs() {
const tabBtns = document.querySelectorAll('.tab-btn');
const tabPanes = document.querySelectorAll('.tab-pane');
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const targetTab = btn.getAttribute('data-tab');
// Update active states
tabBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Show target pane
tabPanes.forEach(pane => {
pane.style.display = pane.id === targetTab ? 'block' : 'none';
});
});
});
// Show more functionality
const showMoreBtn = document.querySelector('.show-more');
const hiddenText = document.querySelector('.hidden-text');
if (showMoreBtn) {
showMoreBtn.addEventListener('click', () => {
if (hiddenText.style.display === 'none') {
hiddenText.style.display = 'block';
showMoreBtn.textContent = 'Show Less';
} else {
hiddenText.style.display = 'none';
showMoreBtn.textContent = 'Show More';
}
});
}
// Load comments
const loadCommentsBtn = document.querySelector('.load-comments');
const commentsSection = document.querySelector('.comments-section');
if (loadCommentsBtn) {
loadCommentsBtn.addEventListener('click', () => {
commentsSection.style.display = 'block';
commentsSection.innerHTML = `
<div class="comment">
<div class="comment-author">John Doe</div>
<div class="comment-text">Great product! Highly recommended.</div>
</div>
<div class="comment">
<div class="comment-author">Jane Smith</div>
<div class="comment-text">Excellent quality and fast shipping.</div>
</div>
`;
loadCommentsBtn.style.display = 'none';
console.log('💬 Comments loaded');
});
}
}
// Data Table
setupDataTable() {
const loadMoreBtn = document.querySelector('.load-more-rows');
const searchInput = document.querySelector('.search-input');
const exportBtn = document.getElementById('export-btn');
const sortableHeaders = document.querySelectorAll('.sortable');
// Load more rows
loadMoreBtn.addEventListener('click', () => {
this.loadMoreTableRows();
});
// Search functionality
searchInput.addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase();
const rows = document.querySelectorAll('#table-body tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(searchTerm) ? '' : 'none';
});
});
// Export functionality
exportBtn.addEventListener('click', () => {
console.log('📊 Exporting table data...');
alert('Table data exported! (Check console)');
});
// Sorting
sortableHeaders.forEach(header => {
header.addEventListener('click', () => {
console.log(`🔄 Sorting by ${header.getAttribute('data-sort')}`);
});
});
}
loadMoreTableRows() {
const tbody = document.getElementById('table-body');
const batch = 10;
for (let i = 0; i < batch; i++) {
const row = document.createElement('tr');
const id = this.tableRowsLoaded + i + 1;
row.innerHTML = `
<td>User ${id}</td>
<td>user${id}@example.com</td>
<td>${new Date().toLocaleDateString()}</td>
<td><button class="btn btn-sm">Edit</button></td>
`;
tbody.appendChild(row);
}
this.tableRowsLoaded += batch;
console.log(`📄 Loaded ${batch} more rows. Total: ${this.tableRowsLoaded}`);
}
// Load initial data
loadInitialData() {
// Load initial products
this.loadMoreProducts();
// Load initial table rows
this.loadMoreTableRows();
}
// Load content when navigating to sections
loadSectionContent(sectionId) {
switch(sectionId) {
case 'catalog':
// Ensure products are loaded in catalog
if (this.productsLoaded === 0) {
this.loadMoreProducts();
}
break;
case 'data-tables':
// Ensure table rows are loaded
if (this.tableRowsLoaded === 0) {
this.loadMoreTableRows();
}
break;
case 'forms':
// Forms are already set up
break;
case 'tabs':
// Tabs content is static
break;
}
}
// Inspector Mode
setupInspector() {
const inspectorBtn = document.getElementById('inspector-btn');
// Create tooltip element
this.tooltip = document.createElement('div');
this.tooltip.className = 'inspector-tooltip';
this.tooltip.style.cssText = `
position: fixed;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
font-family: monospace;
pointer-events: none;
z-index: 10000;
display: none;
max-width: 300px;
`;
document.body.appendChild(this.tooltip);
inspectorBtn.addEventListener('click', () => {
this.toggleInspector();
});
// Add mouse event listeners
document.addEventListener('mousemove', this.handleMouseMove.bind(this));
document.addEventListener('mouseout', this.handleMouseOut.bind(this));
}
toggleInspector() {
this.inspectorMode = !this.inspectorMode;
const inspectorBtn = document.getElementById('inspector-btn');
if (this.inspectorMode) {
inspectorBtn.classList.add('active');
inspectorBtn.style.background = '#0fbbaa';
document.body.style.cursor = 'crosshair';
} else {
inspectorBtn.classList.remove('active');
inspectorBtn.style.background = '';
document.body.style.cursor = '';
this.tooltip.style.display = 'none';
this.removeHighlight();
}
}
handleMouseMove(e) {
if (!this.inspectorMode) return;
const element = e.target;
if (element === this.tooltip) return;
// Highlight element
this.highlightElement(element);
// Show tooltip with element info
const info = this.getElementInfo(element);
this.tooltip.innerHTML = info;
this.tooltip.style.display = 'block';
// Position tooltip
const x = e.clientX + 15;
const y = e.clientY + 15;
// Adjust position if tooltip would go off screen
const rect = this.tooltip.getBoundingClientRect();
const adjustedX = x + rect.width > window.innerWidth ? x - rect.width - 30 : x;
const adjustedY = y + rect.height > window.innerHeight ? y - rect.height - 30 : y;
this.tooltip.style.left = adjustedX + 'px';
this.tooltip.style.top = adjustedY + 'px';
}
handleMouseOut(e) {
if (!this.inspectorMode) return;
if (e.target === document.body) {
this.removeHighlight();
this.tooltip.style.display = 'none';
}
}
highlightElement(element) {
this.removeHighlight();
element.style.outline = '2px solid #0fbbaa';
element.style.outlineOffset = '1px';
element.setAttribute('data-inspector-highlighted', 'true');
}
removeHighlight() {
const highlighted = document.querySelector('[data-inspector-highlighted]');
if (highlighted) {
highlighted.style.outline = '';
highlighted.style.outlineOffset = '';
highlighted.removeAttribute('data-inspector-highlighted');
}
}
getElementInfo(element) {
const tagName = element.tagName.toLowerCase();
const id = element.id ? `#${element.id}` : '';
const classes = element.className ?
`.${element.className.split(' ').filter(c => c).join('.')}` : '';
let selector = tagName;
if (id) {
selector = id;
} else if (classes) {
selector = `${tagName}${classes}`;
}
// Build info HTML
let info = `<strong>${selector}</strong>`;
// Add additional attributes
const attrs = [];
if (element.name) attrs.push(`name="${element.name}"`);
if (element.type) attrs.push(`type="${element.type}"`);
if (element.href) attrs.push(`href="${element.href}"`);
if (element.value && element.tagName === 'INPUT') attrs.push(`value="${element.value}"`);
if (attrs.length > 0) {
info += `<br><span style="color: #888;">${attrs.join(' ')}</span>`;
}
return info;
}
}
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.playgroundApp = new PlaygroundApp();
console.log('🎮 Playground app initialized!');
});

View File

@@ -0,0 +1,328 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>C4A-Script Playground</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- Cookie Banner -->
<div class="cookie-banner" id="cookie-banner">
<div class="cookie-content">
<p>🍪 We use cookies to enhance your experience. By continuing, you agree to our cookie policy.</p>
<div class="cookie-actions">
<button class="btn accept">Accept All</button>
<button class="btn btn-secondary decline">Decline</button>
</div>
</div>
</div>
<!-- Newsletter Popup (appears after 3 seconds) -->
<div class="modal" id="newsletter-popup" style="display: none;">
<div class="modal-content">
<span class="close">&times;</span>
<h2>📬 Subscribe to Our Newsletter</h2>
<p>Get the latest updates on web automation!</p>
<input type="email" placeholder="Enter your email" class="input">
<button class="btn subscribe">Subscribe</button>
</div>
</div>
<!-- Header -->
<header class="site-header">
<nav class="nav-menu">
<a href="#home" class="nav-link active">Home</a>
<a href="#catalog" class="nav-link" id="catalog-link">Products</a>
<a href="#forms" class="nav-link">Forms</a>
<a href="#data-tables" class="nav-link">Data Tables</a>
<div class="dropdown">
<a href="#" class="nav-link dropdown-toggle">More ▼</a>
<div class="dropdown-content">
<a href="#tabs">Tabs Demo</a>
<a href="#accordion">FAQ</a>
<a href="#gallery">Gallery</a>
</div>
</div>
</nav>
<div class="auth-section">
<button class="btn btn-sm" id="inspector-btn" title="Toggle Inspector">🔍</button>
<button class="btn btn-sm" id="login-btn">Login</button>
<div class="user-info" id="user-info" style="display: none;">
<span class="user-avatar">👤</span>
<span class="welcome-message">Welcome, <span id="username-display">User</span>!</span>
<button class="btn btn-sm btn-secondary" id="logout-btn">Logout</button>
</div>
</div>
</header>
<!-- Main Content -->
<main class="main-content">
<!-- Home Section -->
<section id="home" class="section active">
<h1>Welcome to C4A-Script Playground</h1>
<p>This is an interactive demo for testing C4A-Script commands. Each section contains different challenges for web automation.</p>
<button class="btn btn-primary" id="start-tutorial">Start Tutorial</button>
<div class="feature-grid">
<div class="feature-card">
<h3>🔐 Authentication</h3>
<p>Test login forms and user sessions</p>
</div>
<div class="feature-card">
<h3>📜 Dynamic Content</h3>
<p>Infinite scroll and pagination</p>
</div>
<div class="feature-card">
<h3>📝 Forms</h3>
<p>Complex form interactions</p>
</div>
<div class="feature-card">
<h3>📊 Data Tables</h3>
<p>Sortable and filterable data</p>
</div>
</div>
</section>
<!-- Login Modal -->
<div class="modal" id="login-modal" style="display: none;">
<div class="modal-content login-form">
<span class="close">&times;</span>
<h2>Login</h2>
<form id="login-form">
<div class="form-group">
<label>Email</label>
<input type="email" id="email" class="input" placeholder="demo@example.com">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" id="password" class="input" placeholder="demo123">
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="remember-me">
Remember me
</label>
</div>
<button type="submit" class="btn btn-primary">Login</button>
<div class="form-message" id="login-message"></div>
</form>
</div>
</div>
<!-- Product Catalog Section -->
<section id="catalog" class="section">
<h1>Product Catalog</h1>
<div class="view-toggle">
<button class="btn btn-sm active" id="infinite-scroll-btn">Infinite Scroll</button>
<button class="btn btn-sm" id="pagination-btn">Pagination</button>
</div>
<!-- Filters Sidebar -->
<div class="catalog-layout">
<aside class="filters-sidebar">
<h3>Filters</h3>
<div class="filter-group">
<h4 class="collapsible">Category <span class="toggle"></span></h4>
<div class="filter-content">
<label><input type="checkbox"> Electronics</label>
<label><input type="checkbox"> Clothing</label>
<label><input type="checkbox"> Books</label>
</div>
</div>
<div class="filter-group">
<h4 class="collapsible">Price Range <span class="toggle"></span></h4>
<div class="filter-content">
<input type="range" min="0" max="1000" value="500">
<span>$0 - $500</span>
</div>
</div>
</aside>
<!-- Products Grid -->
<div class="products-container">
<div class="product-grid" id="product-grid">
<!-- Products will be loaded here -->
</div>
<!-- Infinite Scroll View -->
<div id="infinite-scroll-view" class="view-mode">
<div class="loading-indicator" id="loading-indicator" style="display: none;">
<div class="spinner"></div>
<p>Loading more products...</p>
</div>
</div>
<!-- Pagination View -->
<div id="pagination-view" class="view-mode" style="display: none;">
<button class="btn load-more">Load More</button>
<div class="pagination">
<button class="page-btn">1</button>
<button class="page-btn">2</button>
<button class="page-btn">3</button>
</div>
</div>
</div>
</div>
</section>
<!-- Forms Section -->
<section id="forms" class="section">
<h1>Form Examples</h1>
<!-- Contact Form -->
<div class="form-card">
<h2>Contact Form</h2>
<form id="contact-form">
<div class="form-group">
<label>Name</label>
<input type="text" class="input" id="contact-name">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" class="input" id="contact-email">
</div>
<div class="form-group">
<label>Subject</label>
<select class="input" id="contact-subject">
<option value="">Select a subject</option>
<option value="support">Support</option>
<option value="sales">Sales</option>
<option value="feedback">Feedback</option>
</select>
</div>
<div class="form-group" id="department-group" style="display: none;">
<label>Department</label>
<select class="input" id="department">
<option value="">Select department</option>
</select>
</div>
<div class="form-group">
<label>Message</label>
<textarea class="input" id="contact-message" rows="4"></textarea>
</div>
<button type="submit" class="btn btn-primary">Send Message</button>
<div class="form-message" id="contact-message-display"></div>
</form>
</div>
<!-- Multi-step Form -->
<div class="form-card">
<h2>Multi-step Survey</h2>
<div class="progress-bar">
<div class="progress-fill" id="progress-fill" style="width: 33%"></div>
</div>
<form id="survey-form">
<!-- Step 1 -->
<div class="form-step active" data-step="1">
<h3>Step 1: Basic Information</h3>
<div class="form-group">
<label>Full Name</label>
<input type="text" class="input" id="full-name">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" class="input" id="survey-email">
</div>
<button type="button" class="btn next-step">Next</button>
</div>
<!-- Step 2 -->
<div class="form-step" data-step="2" style="display: none;">
<h3>Step 2: Preferences</h3>
<div class="form-group">
<label>Interests (select multiple)</label>
<select multiple class="input" id="interests">
<option value="tech">Technology</option>
<option value="sports">Sports</option>
<option value="music">Music</option>
<option value="travel">Travel</option>
</select>
</div>
<button type="button" class="btn prev-step">Previous</button>
<button type="button" class="btn next-step">Next</button>
</div>
<!-- Step 3 -->
<div class="form-step" data-step="3" style="display: none;">
<h3>Step 3: Confirmation</h3>
<p>Please review your information and submit.</p>
<button type="button" class="btn prev-step">Previous</button>
<button type="submit" class="btn btn-primary" id="submit-survey">Submit Survey</button>
</div>
</form>
<div class="form-message success-message" id="survey-success" style="display: none;">
✅ Survey submitted successfully!
</div>
</div>
</section>
<!-- Tabs Section -->
<section id="tabs" class="section">
<h1>Tabs Demo</h1>
<div class="tabs-container">
<div class="tabs-header">
<button class="tab-btn active" data-tab="description">Description</button>
<button class="tab-btn" data-tab="reviews">Reviews</button>
<button class="tab-btn" data-tab="specs">Specifications</button>
</div>
<div class="tabs-content">
<div class="tab-pane active" id="description">
<h3>Product Description</h3>
<p>This is a detailed description of the product...</p>
<div class="expandable-text">
<p class="text-preview">Lorem ipsum dolor sit amet, consectetur adipiscing elit...</p>
<button class="btn btn-sm show-more">Show More</button>
<div class="hidden-text" style="display: none;">
<p>This is the hidden text that appears when you click "Show More". It contains additional details about the product that weren't visible initially.</p>
</div>
</div>
</div>
<div class="tab-pane" id="reviews" style="display: none;">
<h3>Customer Reviews</h3>
<button class="btn btn-sm load-comments">Load Comments</button>
<div class="comments-section" style="display: none;">
<!-- Comments will be loaded here -->
</div>
</div>
<div class="tab-pane" id="specs" style="display: none;">
<h3>Technical Specifications</h3>
<table class="specs-table">
<tr><td>Model</td><td>XYZ-2000</td></tr>
<tr><td>Weight</td><td>2.5 kg</td></tr>
<tr><td>Dimensions</td><td>30 x 20 x 10 cm</td></tr>
</table>
</div>
</div>
</div>
</section>
<!-- Data Tables Section -->
<section id="data-tables" class="section">
<h1>Data Tables</h1>
<div class="table-controls">
<input type="text" class="input search-input" placeholder="Search...">
<button class="btn btn-sm" id="export-btn">Export</button>
</div>
<table class="data-table" id="data-table">
<thead>
<tr>
<th class="sortable" data-sort="name">Name ↕</th>
<th class="sortable" data-sort="email">Email ↕</th>
<th class="sortable" data-sort="date">Date ↕</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="table-body">
<!-- Table rows will be loaded here -->
</tbody>
</table>
<button class="btn load-more-rows">Load More Rows</button>
</section>
</main>
<script src="app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,627 @@
/* Playground Styles - Modern Web App Theme */
:root {
--primary-color: #0fbbaa;
--secondary-color: #3f3f44;
--background-color: #ffffff;
--text-color: #333333;
--border-color: #e0e0e0;
--error-color: #ff3c74;
--success-color: #0fbbaa;
--warning-color: #ffa500;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 16px;
line-height: 1.6;
color: var(--text-color);
background-color: var(--background-color);
}
/* Cookie Banner */
.cookie-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #2c3e50;
color: white;
padding: 1rem;
z-index: 1000;
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
}
.cookie-content {
max-width: 1200px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
}
.cookie-actions {
display: flex;
gap: 0.5rem;
}
/* Header */
.site-header {
background-color: #fff;
border-bottom: 1px solid var(--border-color);
padding: 1rem 2rem;
position: sticky;
top: 0;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-menu {
display: flex;
gap: 2rem;
align-items: center;
}
.nav-link {
text-decoration: none;
color: var(--text-color);
font-weight: 500;
transition: color 0.2s;
}
.nav-link:hover,
.nav-link.active {
color: var(--primary-color);
}
/* Dropdown */
.dropdown {
position: relative;
}
.dropdown-content {
display: none;
position: absolute;
background-color: white;
min-width: 160px;
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
z-index: 1;
border-radius: 4px;
top: 100%;
margin-top: 0.5rem;
}
.dropdown:hover .dropdown-content {
display: block;
}
.dropdown-content a {
color: var(--text-color);
padding: 0.75rem 1rem;
text-decoration: none;
display: block;
}
.dropdown-content a:hover {
background-color: #f5f5f5;
}
/* Auth Section */
.auth-section {
display: flex;
align-items: center;
gap: 1rem;
}
.user-info {
display: flex;
align-items: center;
gap: 0.5rem;
}
.user-avatar {
font-size: 1.5rem;
}
/* Main Content */
.main-content {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.section {
display: none;
}
.section.active {
display: block;
}
/* Buttons */
.btn {
background-color: var(--primary-color);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: all 0.2s;
}
.btn:hover {
background-color: #0aa599;
transform: translateY(-1px);
}
.btn-sm {
padding: 0.25rem 0.75rem;
font-size: 0.875rem;
}
.btn-secondary {
background-color: var(--secondary-color);
}
.btn-secondary:hover {
background-color: #333;
}
.btn-primary {
background-color: var(--primary-color);
}
/* Feature Grid */
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.feature-card {
background-color: #f8f9fa;
padding: 1.5rem;
border-radius: 8px;
text-align: center;
transition: transform 0.2s;
}
.feature-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.feature-card h3 {
margin-top: 0;
}
/* Modal */
.modal {
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background-color: white;
padding: 2rem;
border-radius: 8px;
max-width: 500px;
width: 90%;
position: relative;
animation: modalFadeIn 0.3s;
}
@keyframes modalFadeIn {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
.close {
position: absolute;
right: 1rem;
top: 1rem;
font-size: 1.5rem;
cursor: pointer;
color: #999;
}
.close:hover {
color: #333;
}
/* Forms */
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.input {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 1rem;
}
.input:focus {
outline: none;
border-color: var(--primary-color);
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
}
.form-message {
margin-top: 1rem;
padding: 0.75rem;
border-radius: 4px;
display: none;
}
.form-message.error {
background-color: #ffe6e6;
color: var(--error-color);
display: block;
}
.form-message.success {
background-color: #e6fff6;
color: var(--success-color);
display: block;
}
/* Product Catalog */
.view-toggle {
margin-bottom: 1rem;
}
.catalog-layout {
display: grid;
grid-template-columns: 250px 1fr;
gap: 2rem;
}
.filters-sidebar {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 8px;
}
.filter-group {
margin-bottom: 1.5rem;
}
.collapsible {
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.filter-content {
margin-top: 0.5rem;
}
.filter-content label {
display: block;
margin-bottom: 0.5rem;
}
/* Product Grid */
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1.5rem;
}
.product-card {
background-color: white;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
text-align: center;
transition: transform 0.2s;
}
.product-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.product-image {
width: 100%;
height: 150px;
background-color: #f0f0f0;
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
}
.product-name {
font-weight: 600;
margin-bottom: 0.5rem;
}
.product-price {
color: var(--primary-color);
font-size: 1.2rem;
font-weight: 700;
}
/* Loading Indicator */
.loading-indicator {
text-align: center;
padding: 2rem;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid var(--primary-color);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Pagination */
.pagination {
display: flex;
gap: 0.5rem;
justify-content: center;
margin-top: 2rem;
}
.page-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--border-color);
background-color: white;
cursor: pointer;
border-radius: 4px;
}
.page-btn:hover,
.page-btn.active {
background-color: var(--primary-color);
color: white;
}
/* Multi-step Form */
.progress-bar {
width: 100%;
height: 8px;
background-color: #e0e0e0;
border-radius: 4px;
margin-bottom: 2rem;
}
.progress-fill {
height: 100%;
background-color: var(--primary-color);
border-radius: 4px;
transition: width 0.3s;
}
.form-step {
display: none;
}
.form-step.active {
display: block;
}
/* Tabs */
.tabs-container {
margin-top: 2rem;
}
.tabs-header {
display: flex;
border-bottom: 2px solid var(--border-color);
}
.tab-btn {
background: none;
border: none;
padding: 1rem 2rem;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
color: var(--text-color);
position: relative;
}
.tab-btn:hover {
color: var(--primary-color);
}
.tab-btn.active {
color: var(--primary-color);
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background-color: var(--primary-color);
}
.tabs-content {
padding: 2rem 0;
}
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
}
/* Expandable Text */
.expandable-text {
margin-top: 1rem;
}
.text-preview {
margin-bottom: 0.5rem;
}
.show-more {
margin-top: 0.5rem;
}
/* Comments Section */
.comments-section {
margin-top: 1rem;
}
.comment {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
}
.comment-author {
font-weight: 600;
margin-bottom: 0.5rem;
}
/* Data Table */
.table-controls {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.search-input {
flex: 1;
max-width: 300px;
}
.data-table {
width: 100%;
border-collapse: collapse;
background-color: white;
}
.data-table th,
.data-table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.data-table th {
background-color: #f8f9fa;
font-weight: 600;
}
.sortable {
cursor: pointer;
}
.sortable:hover {
color: var(--primary-color);
}
/* Form Cards */
.form-card {
background-color: white;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 2rem;
margin-bottom: 2rem;
}
.form-card h2 {
margin-top: 0;
}
/* Success Message */
.success-message {
background-color: #e6fff6;
color: var(--success-color);
padding: 1rem;
border-radius: 4px;
text-align: center;
font-weight: 500;
}
/* Load More Button */
.load-more,
.load-more-rows {
display: block;
margin: 2rem auto;
}
/* Responsive */
@media (max-width: 768px) {
.catalog-layout {
grid-template-columns: 1fr;
}
.feature-grid {
grid-template-columns: 1fr;
}
.nav-menu {
flex-wrap: wrap;
gap: 1rem;
}
.cookie-content {
flex-direction: column;
text-align: center;
}
}
/* Inspector Mode */
#inspector-btn.active {
background: var(--primary-color) !important;
color: var(--bg-primary) !important;
}
.inspector-tooltip {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
}

View File

@@ -0,0 +1,2 @@
flask>=2.3.0
flask-cors>=4.0.0

View File

@@ -0,0 +1,18 @@
# Basic Page Interaction
# This script demonstrates basic C4A commands
# Navigate to the playground
GO http://127.0.0.1:8080/playground/
# Wait for page to load
WAIT `body` 2
# Handle cookie banner if present
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept`
# Close newsletter popup if it appears
WAIT 3
IF (EXISTS `.newsletter-popup`) THEN CLICK `.close`
# Click the start tutorial button
CLICK `#start-tutorial`

View File

@@ -0,0 +1,27 @@
# Complete Login Flow
# Demonstrates form interaction and authentication
# Click login button
CLICK `#login-btn`
# Wait for login modal
WAIT `.login-form` 3
# Fill in credentials
CLICK `#email`
TYPE "demo@example.com"
CLICK `#password`
TYPE "demo123"
# Check remember me
IF (EXISTS `#remember-me`) THEN CLICK `#remember-me`
# Submit form
CLICK `button[type="submit"]`
# Wait for success
WAIT `.welcome-message` 5
# Verify login succeeded
IF (EXISTS `.user-info`) THEN EVAL `console.log('✅ Login successful!')`

View File

@@ -0,0 +1,32 @@
# Infinite Scroll Product Loading
# Load all products using scroll automation
# Navigate to catalog
CLICK `#catalog-link`
WAIT `.product-grid` 3
# Switch to infinite scroll mode
CLICK `#infinite-scroll-btn`
# Define scroll procedure
PROC load_more_products
# Get current product count
EVAL `window.initialCount = document.querySelectorAll('.product-card').length`
# Scroll down
SCROLL DOWN 1000
WAIT 2
# Check if more products loaded
EVAL `
const newCount = document.querySelectorAll('.product-card').length;
console.log('Products loaded: ' + newCount);
window.moreLoaded = newCount > window.initialCount;
`
ENDPROC
# Load products until no more
REPEAT (load_more_products, `window.moreLoaded !== false`)
# Final count
EVAL `console.log('✅ Total products: ' + document.querySelectorAll('.product-card').length)`

View File

@@ -0,0 +1,41 @@
# Multi-step Form Wizard
# Complete a complex form with multiple steps
# Navigate to forms section
CLICK `a[href="#forms"]`
WAIT `#survey-form` 2
# Step 1: Basic Information
CLICK `#full-name`
TYPE "John Doe"
CLICK `#survey-email`
TYPE "john.doe@example.com"
# Go to next step
CLICK `.next-step`
WAIT 1
# Step 2: Select Interests
# Select multiple options
CLICK `#interests`
CLICK `option[value="tech"]`
CLICK `option[value="music"]`
CLICK `option[value="travel"]`
# Continue to final step
CLICK `.next-step`
WAIT 1
# Step 3: Review and Submit
# Verify we're on the last step
IF (EXISTS `#submit-survey`) THEN EVAL `console.log('📋 On final step')`
# Submit the form
CLICK `#submit-survey`
# Wait for success message
WAIT `.success-message` 5
# Verify submission
IF (EXISTS `.success-message`) THEN EVAL `console.log('✅ Survey submitted successfully!')`

View File

@@ -0,0 +1,82 @@
# Complete E-commerce Workflow
# Login, browse products, and interact with various elements
# Define reusable procedures
PROC handle_popups
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept`
IF (EXISTS `.newsletter-popup`) THEN CLICK `.close`
ENDPROC
PROC login_user
CLICK `#login-btn`
WAIT `.login-form` 2
CLICK `#email`
TYPE "demo@example.com"
CLICK `#password`
TYPE "demo123"
CLICK `button[type="submit"]`
WAIT `.welcome-message` 5
ENDPROC
PROC browse_products
# Go to catalog
CLICK `#catalog-link`
WAIT `.product-grid` 3
# Apply filters
CLICK `.collapsible`
WAIT 0.5
CLICK `input[type="checkbox"]`
# Load some products
SCROLL DOWN 500
WAIT 1
SCROLL DOWN 500
WAIT 1
ENDPROC
# Main workflow
GO http://127.0.0.1:8080/playground/
WAIT `body` 2
# Handle initial popups
handle_popups
# Login if not already
IF (NOT EXISTS `.user-info`) THEN login_user
# Browse products
browse_products
# Navigate to tabs demo
CLICK `a[href="#tabs"]`
WAIT `.tabs-container` 2
# Interact with tabs
CLICK `button[data-tab="reviews"]`
WAIT 1
# Load comments
IF (EXISTS `.load-comments`) THEN CLICK `.load-comments`
WAIT `.comments-section` 2
# Check specifications
CLICK `button[data-tab="specs"]`
WAIT 1
# Final navigation to data tables
CLICK `a[href="#data"]`
WAIT `.data-table` 2
# Search in table
CLICK `.search-input`
TYPE "User"
# Load more rows
CLICK `.load-more-rows`
WAIT 1
# Export data
CLICK `#export-btn`
EVAL `console.log('✅ Workflow completed successfully!')`

View File

@@ -0,0 +1,304 @@
#!/usr/bin/env python3
"""
C4A-Script Tutorial Server
Serves the tutorial app and provides C4A compilation API
"""
import sys
import os
from pathlib import Path
from flask import Flask, render_template_string, request, jsonify, send_from_directory
from flask_cors import CORS
# Add parent directories to path to import crawl4ai
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent.parent))
try:
from crawl4ai.script import compile as c4a_compile
C4A_AVAILABLE = True
except ImportError:
print("⚠️ C4A compiler not available. Using mock compiler.")
C4A_AVAILABLE = False
app = Flask(__name__)
CORS(app)
# Serve static files
@app.route('/')
def index():
return send_from_directory('.', 'index.html')
@app.route('/assets/<path:path>')
def serve_assets(path):
return send_from_directory('assets', path)
@app.route('/playground/')
def playground():
return send_from_directory('playground', 'index.html')
@app.route('/playground/<path:path>')
def serve_playground(path):
return send_from_directory('playground', path)
# API endpoint for C4A compilation
@app.route('/api/compile', methods=['POST'])
def compile_endpoint():
try:
data = request.get_json()
script = data.get('script', '')
if not script:
return jsonify({
'success': False,
'error': {
'line': 1,
'column': 1,
'message': 'No script provided',
'suggestion': 'Write some C4A commands'
}
})
if C4A_AVAILABLE:
# Use real C4A compiler
result = c4a_compile(script)
if result.success:
return jsonify({
'success': True,
'jsCode': result.js_code,
'metadata': {
'lineCount': len(result.js_code),
'sourceLines': len(script.split('\n'))
}
})
else:
error = result.first_error
return jsonify({
'success': False,
'error': {
'line': error.line,
'column': error.column,
'message': error.message,
'suggestion': error.suggestions[0].message if error.suggestions else None,
'code': error.code,
'sourceLine': error.source_line
}
})
else:
# Use mock compiler for demo
result = mock_compile(script)
return jsonify(result)
except Exception as e:
return jsonify({
'success': False,
'error': {
'line': 1,
'column': 1,
'message': f'Server error: {str(e)}',
'suggestion': 'Check server logs'
}
}), 500
def mock_compile(script):
"""Simple mock compiler for demo when C4A is not available"""
lines = [line for line in script.split('\n') if line.strip() and not line.strip().startswith('#')]
js_code = []
for i, line in enumerate(lines):
line = line.strip()
try:
if line.startswith('GO '):
url = line[3:].strip()
# Handle relative URLs
if not url.startswith(('http://', 'https://')):
url = '/' + url.lstrip('/')
js_code.append(f"await page.goto('{url}');")
elif line.startswith('WAIT '):
parts = line[5:].strip().split(' ')
if parts[0].startswith('`'):
selector = parts[0].strip('`')
timeout = parts[1] if len(parts) > 1 else '5'
js_code.append(f"await page.waitForSelector('{selector}', {{ timeout: {timeout}000 }});")
else:
seconds = parts[0]
js_code.append(f"await page.waitForTimeout({seconds}000);")
elif line.startswith('CLICK '):
selector = line[6:].strip().strip('`')
js_code.append(f"await page.click('{selector}');")
elif line.startswith('TYPE '):
text = line[5:].strip().strip('"')
js_code.append(f"await page.keyboard.type('{text}');")
elif line.startswith('SCROLL '):
parts = line[7:].strip().split(' ')
direction = parts[0]
amount = parts[1] if len(parts) > 1 else '500'
if direction == 'DOWN':
js_code.append(f"await page.evaluate(() => window.scrollBy(0, {amount}));")
elif direction == 'UP':
js_code.append(f"await page.evaluate(() => window.scrollBy(0, -{amount}));")
elif line.startswith('IF '):
if 'THEN' not in line:
return {
'success': False,
'error': {
'line': i + 1,
'column': len(line),
'message': "Missing 'THEN' keyword after IF condition",
'suggestion': "Add 'THEN' after the condition",
'sourceLine': line
}
}
condition = line[3:line.index('THEN')].strip()
action = line[line.index('THEN') + 4:].strip()
if 'EXISTS' in condition:
selector_match = condition.split('`')
if len(selector_match) >= 2:
selector = selector_match[1]
action_selector = action.split('`')[1] if '`' in action else ''
js_code.append(
f"if (await page.$$('{selector}').length > 0) {{ "
f"await page.click('{action_selector}'); }}"
)
elif line.startswith('PRESS '):
key = line[6:].strip()
js_code.append(f"await page.keyboard.press('{key}');")
else:
# Unknown command
return {
'success': False,
'error': {
'line': i + 1,
'column': 1,
'message': f"Unknown command: {line.split()[0]}",
'suggestion': "Check command syntax",
'sourceLine': line
}
}
except Exception as e:
return {
'success': False,
'error': {
'line': i + 1,
'column': 1,
'message': f"Failed to parse: {str(e)}",
'suggestion': "Check syntax",
'sourceLine': line
}
}
return {
'success': True,
'jsCode': js_code,
'metadata': {
'lineCount': len(js_code),
'sourceLines': len(lines)
}
}
# Example scripts endpoint
@app.route('/api/examples')
def get_examples():
examples = [
{
'id': 'cookie-banner',
'name': 'Handle Cookie Banner',
'description': 'Accept cookies and close newsletter popup',
'script': '''# Handle cookie banner and newsletter
GO http://127.0.0.1:8080/playground/
WAIT `body` 2
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept`
IF (EXISTS `.newsletter-popup`) THEN CLICK `.close`'''
},
{
'id': 'login',
'name': 'Login Flow',
'description': 'Complete login with credentials',
'script': '''# Login to the site
CLICK `#login-btn`
WAIT `.login-form` 2
CLICK `#email`
TYPE "demo@example.com"
CLICK `#password`
TYPE "demo123"
IF (EXISTS `#remember-me`) THEN CLICK `#remember-me`
CLICK `button[type="submit"]`
WAIT `.welcome-message` 5'''
},
{
'id': 'infinite-scroll',
'name': 'Infinite Scroll',
'description': 'Load products with scrolling',
'script': '''# Navigate to catalog and scroll
CLICK `#catalog-link`
WAIT `.product-grid` 3
# Scroll multiple times to load products
SCROLL DOWN 1000
WAIT 1
SCROLL DOWN 1000
WAIT 1
SCROLL DOWN 1000'''
},
{
'id': 'form-wizard',
'name': 'Multi-step Form',
'description': 'Complete a multi-step survey',
'script': '''# Navigate to forms
CLICK `a[href="#forms"]`
WAIT `#survey-form` 2
# Step 1: Basic info
CLICK `#full-name`
TYPE "John Doe"
CLICK `#survey-email`
TYPE "john@example.com"
CLICK `.next-step`
WAIT 1
# Step 2: Preferences
CLICK `#interests`
CLICK `option[value="tech"]`
CLICK `option[value="music"]`
CLICK `.next-step`
WAIT 1
# Step 3: Submit
CLICK `#submit-survey`
WAIT `.success-message` 5'''
}
]
return jsonify(examples)
if __name__ == '__main__':
port = int(os.environ.get('PORT', 8080))
print(f"""
╔══════════════════════════════════════════════════════════╗
║ C4A-Script Interactive Tutorial Server ║
╠══════════════════════════════════════════════════════════╣
║ ║
║ Server running at: http://localhost:{port:<6}
║ ║
║ Features: ║
║ • C4A-Script compilation API ║
║ • Interactive playground ║
║ • Real-time execution visualization ║
║ ║
║ C4A Compiler: {'✓ Available' if C4A_AVAILABLE else '✗ Using mock compiler':<30}
║ ║
╚══════════════════════════════════════════════════════════╝
""")
app.run(host='0.0.0.0', port=port, debug=True)