chore: import upstream snapshot with attribution
@@ -0,0 +1,396 @@
|
||||
# C4A-Script Interactive Tutorial
|
||||
|
||||
A comprehensive web-based tutorial for learning and experimenting with C4A-Script - Crawl4AI's visual web automation language.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- Python 3.7+
|
||||
- Modern web browser (Chrome, Firefox, Safari, Edge)
|
||||
|
||||
### Running the Tutorial
|
||||
|
||||
1. **Clone and Navigate**
|
||||
```bash
|
||||
git clone https://github.com/unclecode/crawl4ai.git
|
||||
cd crawl4ai/docs/examples/c4a_script/tutorial/
|
||||
```
|
||||
|
||||
2. **Install Dependencies**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Launch the Server**
|
||||
```bash
|
||||
python server.py
|
||||
```
|
||||
|
||||
4. **Open in Browser**
|
||||
```
|
||||
http://localhost:8000
|
||||
```
|
||||
|
||||
**🌐 Try Online**: [Live Demo](https://docs.crawl4ai.com/c4a-script/demo)
|
||||
|
||||
### 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
|
||||
|
||||
### Core Features
|
||||
- **📝 Text Editor**: Write C4A-Script with syntax highlighting
|
||||
- **🧩 Visual Editor**: Build scripts using drag-and-drop Blockly interface
|
||||
- **🎬 Recording Mode**: Capture browser actions and auto-generate scripts
|
||||
- **⚡ Live Execution**: Run scripts in real-time with instant feedback
|
||||
- **📊 Timeline View**: Visualize and edit automation steps
|
||||
|
||||
## 📚 Tutorial Content
|
||||
|
||||
### Basic Commands
|
||||
- **Navigation**: `GO url`
|
||||
- **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
|
||||
|
||||
## 🏗️ Developer Guide
|
||||
|
||||
### Project Architecture
|
||||
|
||||
```
|
||||
tutorial/
|
||||
├── server.py # Flask application server
|
||||
├── assets/ # Tutorial-specific assets
|
||||
│ ├── app.js # Main application logic
|
||||
│ ├── c4a-blocks.js # Custom Blockly blocks
|
||||
│ ├── c4a-generator.js # Code generation
|
||||
│ ├── blockly-manager.js # Blockly integration
|
||||
│ └── styles.css # Main styling
|
||||
├── playground/ # Interactive demo environment
|
||||
│ ├── index.html # Demo web application
|
||||
│ ├── app.js # Demo app logic
|
||||
│ └── styles.css # Demo styling
|
||||
├── scripts/ # Example C4A scripts
|
||||
└── index.html # Main tutorial interface
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
#### 1. TutorialApp (`assets/app.js`)
|
||||
Main application controller managing:
|
||||
- Code editor integration (CodeMirror)
|
||||
- Script execution and browser preview
|
||||
- Tutorial navigation and lessons
|
||||
- State management and persistence
|
||||
|
||||
#### 2. BlocklyManager (`assets/blockly-manager.js`)
|
||||
Visual programming interface:
|
||||
- Custom C4A-Script block definitions
|
||||
- Bidirectional sync between visual blocks and text
|
||||
- Real-time code generation
|
||||
- Dark theme integration
|
||||
|
||||
#### 3. Recording System
|
||||
Powers the recording functionality:
|
||||
- Browser event capture
|
||||
- Smart event grouping and filtering
|
||||
- Automatic C4A-Script generation
|
||||
- Timeline visualization
|
||||
|
||||
### Customization
|
||||
|
||||
#### Adding New Commands
|
||||
1. **Define Block** (`assets/c4a-blocks.js`)
|
||||
2. **Add Generator** (`assets/c4a-generator.js`)
|
||||
3. **Update Parser** (`assets/blockly-manager.js`)
|
||||
|
||||
#### Themes and Styling
|
||||
- Main styles: `assets/styles.css`
|
||||
- Theme variables: CSS custom properties
|
||||
- Dark mode: Auto-applied based on system preference
|
||||
|
||||
### Configuration
|
||||
```python
|
||||
# server.py configuration
|
||||
PORT = 8000
|
||||
DEBUG = True
|
||||
THREADED = True
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
- `GET /` - Main tutorial interface
|
||||
- `GET /playground/` - Interactive demo environment
|
||||
- `POST /execute` - Script execution endpoint
|
||||
- `GET /examples/<script>` - Load example scripts
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Port Already in Use**
|
||||
```bash
|
||||
# Kill existing process
|
||||
lsof -ti:8000 | xargs kill -9
|
||||
# Or use different port
|
||||
python server.py --port 8001
|
||||
```
|
||||
|
||||
**Blockly Not Loading**
|
||||
- Check browser console for JavaScript errors
|
||||
- Verify all static files are served correctly
|
||||
- Ensure proper script loading order
|
||||
|
||||
**Recording Issues**
|
||||
- Verify iframe permissions
|
||||
- Check cross-origin communication
|
||||
- Ensure event listeners are attached
|
||||
|
||||
### Debug Mode
|
||||
Enable detailed logging by setting `DEBUG = True` in `assets/app.js`
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
- **[C4A-Script Documentation](../../md_v2/core/c4a-script.md)** - Complete language guide
|
||||
- **[API Reference](../../md_v2/api/c4a-script-reference.md)** - Detailed command documentation
|
||||
- **[Live Demo](https://docs.crawl4ai.com/c4a-script/demo)** - Try without installation
|
||||
- **[Example Scripts](../)** - More automation examples
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
### Bug Reports
|
||||
1. Check existing issues on GitHub
|
||||
2. Provide minimal reproduction steps
|
||||
3. Include browser and system information
|
||||
4. Add relevant console logs
|
||||
|
||||
### Feature Requests
|
||||
1. Fork the repository
|
||||
2. Create feature branch: `git checkout -b feature/my-feature`
|
||||
3. Test thoroughly with different browsers
|
||||
4. Update documentation
|
||||
5. Submit pull request
|
||||
|
||||
### Code Style
|
||||
- Use consistent indentation (2 spaces for JS, 4 for Python)
|
||||
- Add comments for complex logic
|
||||
- Follow existing naming conventions
|
||||
- Test with multiple browsers
|
||||
|
||||
---
|
||||
|
||||
**Happy Automating!** 🎉
|
||||
|
||||
Need help? Check our [documentation](https://docs.crawl4ai.com) or open an issue on [GitHub](https://github.com/unclecode/crawl4ai).
|
||||
@@ -0,0 +1,906 @@
|
||||
/* ================================================================
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
Recording Timeline Styles
|
||||
================================================================ */
|
||||
|
||||
.action-btn.record {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--error-color);
|
||||
}
|
||||
|
||||
.action-btn.record:hover {
|
||||
background: var(--error-color);
|
||||
border-color: var(--error-color);
|
||||
}
|
||||
|
||||
.action-btn.record.recording {
|
||||
background: var(--error-color);
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.action-btn.record.recording .icon {
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#editor-view,
|
||||
#timeline-view {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.recording-timeline {
|
||||
background: var(--bg-secondary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.timeline-header h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timeline-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.timeline-events {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.timeline-event {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 6px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timeline-event:hover {
|
||||
border-color: var(--border-hover);
|
||||
background: var(--code-bg);
|
||||
}
|
||||
|
||||
.timeline-event.selected {
|
||||
border-color: var(--primary-color);
|
||||
background: rgba(15, 187, 170, 0.1);
|
||||
}
|
||||
|
||||
.event-checkbox {
|
||||
margin-right: 10px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.event-time {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-right: 10px;
|
||||
font-family: 'Dank Mono', monospace;
|
||||
min-width: 45px;
|
||||
}
|
||||
|
||||
.event-command {
|
||||
flex: 1;
|
||||
font-family: 'Dank Mono', monospace;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.event-command .cmd-name {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.event-command .cmd-selector {
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
.event-command .cmd-value {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.event-command .cmd-detail {
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.event-edit {
|
||||
margin-left: 10px;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.event-edit:hover {
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Event Editor Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--modal-overlay);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.event-editor-modal {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
z-index: 1000;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.event-editor-modal h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--text-primary);
|
||||
font-family: 'Dank Mono', monospace;
|
||||
}
|
||||
|
||||
.editor-field {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.editor-field label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'Dank Mono', monospace;
|
||||
}
|
||||
|
||||
.editor-field input,
|
||||
.editor-field select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
border-radius: 4px;
|
||||
font-family: 'Dank Mono', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.editor-field input:focus,
|
||||
.editor-field select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Blockly Button */
|
||||
#blockly-btn .icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Hidden State */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
// Blockly Manager for C4A-Script
|
||||
// Handles Blockly workspace, code generation, and synchronization with text editor
|
||||
|
||||
class BlocklyManager {
|
||||
constructor(tutorialApp) {
|
||||
this.app = tutorialApp;
|
||||
this.workspace = null;
|
||||
this.isUpdating = false; // Prevent circular updates
|
||||
this.blocklyVisible = false;
|
||||
this.toolboxXml = this.generateToolbox();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupBlocklyContainer();
|
||||
this.initializeWorkspace();
|
||||
this.setupEventHandlers();
|
||||
this.setupSynchronization();
|
||||
}
|
||||
|
||||
setupBlocklyContainer() {
|
||||
// Create blockly container div
|
||||
const editorContainer = document.querySelector('.editor-container');
|
||||
const blocklyDiv = document.createElement('div');
|
||||
blocklyDiv.id = 'blockly-view';
|
||||
blocklyDiv.className = 'blockly-workspace hidden';
|
||||
blocklyDiv.style.height = '100%';
|
||||
blocklyDiv.style.width = '100%';
|
||||
editorContainer.appendChild(blocklyDiv);
|
||||
}
|
||||
|
||||
generateToolbox() {
|
||||
return `
|
||||
<xml id="toolbox" style="display: none">
|
||||
<category name="Navigation" colour="${BlockColors.NAVIGATION}">
|
||||
<block type="c4a_go"></block>
|
||||
<block type="c4a_reload"></block>
|
||||
<block type="c4a_back"></block>
|
||||
<block type="c4a_forward"></block>
|
||||
</category>
|
||||
|
||||
<category name="Wait" colour="${BlockColors.WAIT}">
|
||||
<block type="c4a_wait_time">
|
||||
<field name="SECONDS">3</field>
|
||||
</block>
|
||||
<block type="c4a_wait_selector">
|
||||
<field name="SELECTOR">#content</field>
|
||||
<field name="TIMEOUT">10</field>
|
||||
</block>
|
||||
<block type="c4a_wait_text">
|
||||
<field name="TEXT">Loading complete</field>
|
||||
<field name="TIMEOUT">5</field>
|
||||
</block>
|
||||
</category>
|
||||
|
||||
<category name="Mouse Actions" colour="${BlockColors.ACTIONS}">
|
||||
<block type="c4a_click">
|
||||
<field name="SELECTOR">button.submit</field>
|
||||
</block>
|
||||
<block type="c4a_click_xy"></block>
|
||||
<block type="c4a_double_click"></block>
|
||||
<block type="c4a_right_click"></block>
|
||||
<block type="c4a_move"></block>
|
||||
<block type="c4a_drag"></block>
|
||||
<block type="c4a_scroll">
|
||||
<field name="DIRECTION">DOWN</field>
|
||||
<field name="AMOUNT">500</field>
|
||||
</block>
|
||||
</category>
|
||||
|
||||
<category name="Keyboard" colour="${BlockColors.KEYBOARD}">
|
||||
<block type="c4a_type">
|
||||
<field name="TEXT">hello@example.com</field>
|
||||
</block>
|
||||
<block type="c4a_type_var">
|
||||
<field name="VAR">email</field>
|
||||
</block>
|
||||
<block type="c4a_clear"></block>
|
||||
<block type="c4a_set">
|
||||
<field name="SELECTOR">#email</field>
|
||||
<field name="VALUE">user@example.com</field>
|
||||
</block>
|
||||
<block type="c4a_press">
|
||||
<field name="KEY">Tab</field>
|
||||
</block>
|
||||
<block type="c4a_key_down">
|
||||
<field name="KEY">Shift</field>
|
||||
</block>
|
||||
<block type="c4a_key_up">
|
||||
<field name="KEY">Shift</field>
|
||||
</block>
|
||||
</category>
|
||||
|
||||
<category name="Control Flow" colour="${BlockColors.CONTROL}">
|
||||
<block type="c4a_if_exists">
|
||||
<field name="SELECTOR">.cookie-banner</field>
|
||||
</block>
|
||||
<block type="c4a_if_exists_else">
|
||||
<field name="SELECTOR">#user</field>
|
||||
</block>
|
||||
<block type="c4a_if_not_exists">
|
||||
<field name="SELECTOR">.modal</field>
|
||||
</block>
|
||||
<block type="c4a_if_js">
|
||||
<field name="CONDITION">window.innerWidth < 768</field>
|
||||
</block>
|
||||
<block type="c4a_repeat_times">
|
||||
<field name="TIMES">5</field>
|
||||
</block>
|
||||
<block type="c4a_repeat_while">
|
||||
<field name="CONDITION">document.querySelector('.load-more')</field>
|
||||
</block>
|
||||
</category>
|
||||
|
||||
<category name="Variables" colour="${BlockColors.VARIABLES}">
|
||||
<block type="c4a_setvar">
|
||||
<field name="NAME">username</field>
|
||||
<field name="VALUE">john@example.com</field>
|
||||
</block>
|
||||
<block type="c4a_eval">
|
||||
<field name="CODE">console.log('Hello')</field>
|
||||
</block>
|
||||
</category>
|
||||
|
||||
<category name="Procedures" colour="${BlockColors.PROCEDURES}">
|
||||
<block type="c4a_proc_def">
|
||||
<field name="NAME">login</field>
|
||||
</block>
|
||||
<block type="c4a_proc_call">
|
||||
<field name="NAME">login</field>
|
||||
</block>
|
||||
</category>
|
||||
|
||||
<category name="Comments" colour="#9E9E9E">
|
||||
<block type="c4a_comment">
|
||||
<field name="TEXT">Add comment here</field>
|
||||
</block>
|
||||
</category>
|
||||
</xml>`;
|
||||
}
|
||||
|
||||
initializeWorkspace() {
|
||||
const blocklyDiv = document.getElementById('blockly-view');
|
||||
|
||||
// Dark theme configuration
|
||||
const theme = Blockly.Theme.defineTheme('c4a-dark', {
|
||||
'base': Blockly.Themes.Classic,
|
||||
'componentStyles': {
|
||||
'workspaceBackgroundColour': '#0e0e10',
|
||||
'toolboxBackgroundColour': '#1a1a1b',
|
||||
'toolboxForegroundColour': '#e0e0e0',
|
||||
'flyoutBackgroundColour': '#1a1a1b',
|
||||
'flyoutForegroundColour': '#e0e0e0',
|
||||
'flyoutOpacity': 0.9,
|
||||
'scrollbarColour': '#2a2a2c',
|
||||
'scrollbarOpacity': 0.5,
|
||||
'insertionMarkerColour': '#0fbbaa',
|
||||
'insertionMarkerOpacity': 0.3,
|
||||
'markerColour': '#0fbbaa',
|
||||
'cursorColour': '#0fbbaa',
|
||||
'selectedGlowColour': '#0fbbaa',
|
||||
'selectedGlowOpacity': 0.4,
|
||||
'replacementGlowColour': '#0fbbaa',
|
||||
'replacementGlowOpacity': 0.5
|
||||
},
|
||||
'fontStyle': {
|
||||
'family': 'Dank Mono, Monaco, Consolas, monospace',
|
||||
'weight': 'normal',
|
||||
'size': 13
|
||||
}
|
||||
});
|
||||
|
||||
this.workspace = Blockly.inject(blocklyDiv, {
|
||||
toolbox: this.toolboxXml,
|
||||
theme: theme,
|
||||
grid: {
|
||||
spacing: 20,
|
||||
length: 3,
|
||||
colour: '#2a2a2c',
|
||||
snap: true
|
||||
},
|
||||
zoom: {
|
||||
controls: true,
|
||||
wheel: true,
|
||||
startScale: 1.0,
|
||||
maxScale: 3,
|
||||
minScale: 0.3,
|
||||
scaleSpeed: 1.2
|
||||
},
|
||||
trashcan: true,
|
||||
sounds: false,
|
||||
media: 'https://unpkg.com/blockly/media/'
|
||||
});
|
||||
|
||||
// Add workspace change listener
|
||||
this.workspace.addChangeListener((event) => {
|
||||
if (!this.isUpdating && event.type !== Blockly.Events.UI) {
|
||||
this.syncBlocksToCode();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupEventHandlers() {
|
||||
// Add blockly toggle button
|
||||
const headerActions = document.querySelector('.editor-panel .header-actions');
|
||||
const blocklyBtn = document.createElement('button');
|
||||
blocklyBtn.id = 'blockly-btn';
|
||||
blocklyBtn.className = 'action-btn';
|
||||
blocklyBtn.title = 'Toggle Blockly Mode';
|
||||
blocklyBtn.innerHTML = '<span class="icon">🧩</span>';
|
||||
|
||||
// Insert before the Run button
|
||||
const runBtn = document.getElementById('run-btn');
|
||||
headerActions.insertBefore(blocklyBtn, runBtn);
|
||||
|
||||
blocklyBtn.addEventListener('click', () => this.toggleBlocklyView());
|
||||
}
|
||||
|
||||
setupSynchronization() {
|
||||
// Listen to CodeMirror changes
|
||||
this.app.editor.on('change', (instance, changeObj) => {
|
||||
if (!this.isUpdating && this.blocklyVisible && changeObj.origin !== 'setValue') {
|
||||
this.syncCodeToBlocks();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleBlocklyView() {
|
||||
const editorView = document.getElementById('editor-view');
|
||||
const blocklyView = document.getElementById('blockly-view');
|
||||
const timelineView = document.getElementById('timeline-view');
|
||||
const blocklyBtn = document.getElementById('blockly-btn');
|
||||
|
||||
this.blocklyVisible = !this.blocklyVisible;
|
||||
|
||||
if (this.blocklyVisible) {
|
||||
// Show Blockly
|
||||
editorView.classList.add('hidden');
|
||||
timelineView.classList.add('hidden');
|
||||
blocklyView.classList.remove('hidden');
|
||||
blocklyBtn.classList.add('active');
|
||||
|
||||
// Resize workspace
|
||||
Blockly.svgResize(this.workspace);
|
||||
|
||||
// Sync current code to blocks
|
||||
this.syncCodeToBlocks();
|
||||
} else {
|
||||
// Show editor
|
||||
blocklyView.classList.add('hidden');
|
||||
editorView.classList.remove('hidden');
|
||||
blocklyBtn.classList.remove('active');
|
||||
|
||||
// Refresh CodeMirror
|
||||
setTimeout(() => this.app.editor.refresh(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
syncBlocksToCode() {
|
||||
if (this.isUpdating) return;
|
||||
|
||||
try {
|
||||
this.isUpdating = true;
|
||||
|
||||
// Generate C4A-Script from blocks using our custom generator
|
||||
if (typeof c4aGenerator !== 'undefined') {
|
||||
const code = c4aGenerator.workspaceToCode(this.workspace);
|
||||
|
||||
// Process the code to maintain proper formatting
|
||||
const lines = code.split('\n');
|
||||
const formattedLines = [];
|
||||
let lastWasComment = false;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
|
||||
const isComment = line.startsWith('#');
|
||||
|
||||
// Add blank line when transitioning between comments and commands
|
||||
if (formattedLines.length > 0 && lastWasComment !== isComment) {
|
||||
formattedLines.push('');
|
||||
}
|
||||
|
||||
formattedLines.push(line);
|
||||
lastWasComment = isComment;
|
||||
}
|
||||
|
||||
const cleanCode = formattedLines.join('\n');
|
||||
|
||||
// Update CodeMirror
|
||||
this.app.editor.setValue(cleanCode);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error syncing blocks to code:', error);
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
syncCodeToBlocks() {
|
||||
if (this.isUpdating) return;
|
||||
|
||||
try {
|
||||
this.isUpdating = true;
|
||||
|
||||
// Clear workspace
|
||||
this.workspace.clear();
|
||||
|
||||
// Parse C4A-Script and generate blocks
|
||||
const code = this.app.editor.getValue();
|
||||
const blocks = this.parseC4AToBlocks(code);
|
||||
|
||||
if (blocks) {
|
||||
Blockly.Xml.domToWorkspace(blocks, this.workspace);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error syncing code to blocks:', error);
|
||||
// Show error in console
|
||||
this.app.addConsoleMessage(`Blockly sync error: ${error.message}`, 'warning');
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
parseC4AToBlocks(code) {
|
||||
const lines = code.split('\n');
|
||||
const xml = document.createElement('xml');
|
||||
let yPos = 20;
|
||||
let previousBlock = null;
|
||||
let rootBlock = null;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
// Skip empty lines
|
||||
if (!line) continue;
|
||||
|
||||
// Handle comments
|
||||
if (line.startsWith('#')) {
|
||||
const commentBlock = this.parseLineToBlock(line, i, lines);
|
||||
if (commentBlock) {
|
||||
if (previousBlock) {
|
||||
// Connect to previous block
|
||||
const next = document.createElement('next');
|
||||
next.appendChild(commentBlock);
|
||||
previousBlock.appendChild(next);
|
||||
} else {
|
||||
// First block - set position
|
||||
commentBlock.setAttribute('x', 20);
|
||||
commentBlock.setAttribute('y', yPos);
|
||||
xml.appendChild(commentBlock);
|
||||
rootBlock = commentBlock;
|
||||
yPos += 60;
|
||||
}
|
||||
previousBlock = commentBlock;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const block = this.parseLineToBlock(line, i, lines);
|
||||
|
||||
if (block) {
|
||||
if (previousBlock) {
|
||||
// Connect to previous block using <next>
|
||||
const next = document.createElement('next');
|
||||
next.appendChild(block);
|
||||
previousBlock.appendChild(next);
|
||||
} else {
|
||||
// First block - set position
|
||||
block.setAttribute('x', 20);
|
||||
block.setAttribute('y', yPos);
|
||||
xml.appendChild(block);
|
||||
rootBlock = block;
|
||||
yPos += 60;
|
||||
}
|
||||
previousBlock = block;
|
||||
}
|
||||
}
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
parseLineToBlock(line, index, allLines) {
|
||||
// Navigation commands
|
||||
if (line.startsWith('GO ')) {
|
||||
const url = line.substring(3).trim();
|
||||
return this.createBlock('c4a_go', { 'URL': url });
|
||||
}
|
||||
if (line === 'RELOAD') {
|
||||
return this.createBlock('c4a_reload');
|
||||
}
|
||||
if (line === 'BACK') {
|
||||
return this.createBlock('c4a_back');
|
||||
}
|
||||
if (line === 'FORWARD') {
|
||||
return this.createBlock('c4a_forward');
|
||||
}
|
||||
|
||||
// Wait commands
|
||||
if (line.startsWith('WAIT ')) {
|
||||
const parts = line.substring(5).trim();
|
||||
|
||||
// Check if it's just a number (wait time)
|
||||
if (/^\d+(\.\d+)?$/.test(parts)) {
|
||||
return this.createBlock('c4a_wait_time', { 'SECONDS': parts });
|
||||
}
|
||||
|
||||
// Check for selector wait
|
||||
const selectorMatch = parts.match(/^`([^`]+)`\s+(\d+)$/);
|
||||
if (selectorMatch) {
|
||||
return this.createBlock('c4a_wait_selector', {
|
||||
'SELECTOR': selectorMatch[1],
|
||||
'TIMEOUT': selectorMatch[2]
|
||||
});
|
||||
}
|
||||
|
||||
// Check for text wait
|
||||
const textMatch = parts.match(/^"([^"]+)"\s+(\d+)$/);
|
||||
if (textMatch) {
|
||||
return this.createBlock('c4a_wait_text', {
|
||||
'TEXT': textMatch[1],
|
||||
'TIMEOUT': textMatch[2]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Click commands
|
||||
if (line.startsWith('CLICK ')) {
|
||||
const target = line.substring(6).trim();
|
||||
|
||||
// Check for coordinates
|
||||
const coordMatch = target.match(/^(\d+)\s+(\d+)$/);
|
||||
if (coordMatch) {
|
||||
return this.createBlock('c4a_click_xy', {
|
||||
'X': coordMatch[1],
|
||||
'Y': coordMatch[2]
|
||||
});
|
||||
}
|
||||
|
||||
// Selector click
|
||||
const selectorMatch = target.match(/^`([^`]+)`$/);
|
||||
if (selectorMatch) {
|
||||
return this.createBlock('c4a_click', {
|
||||
'SELECTOR': selectorMatch[1]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Other mouse actions
|
||||
if (line.startsWith('DOUBLE_CLICK ')) {
|
||||
const selector = line.substring(13).trim().match(/^`([^`]+)`$/);
|
||||
if (selector) {
|
||||
return this.createBlock('c4a_double_click', {
|
||||
'SELECTOR': selector[1]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (line.startsWith('RIGHT_CLICK ')) {
|
||||
const selector = line.substring(12).trim().match(/^`([^`]+)`$/);
|
||||
if (selector) {
|
||||
return this.createBlock('c4a_right_click', {
|
||||
'SELECTOR': selector[1]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll
|
||||
if (line.startsWith('SCROLL ')) {
|
||||
const match = line.match(/^SCROLL\s+(UP|DOWN|LEFT|RIGHT)(?:\s+(\d+))?$/);
|
||||
if (match) {
|
||||
return this.createBlock('c4a_scroll', {
|
||||
'DIRECTION': match[1],
|
||||
'AMOUNT': match[2] || '500'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Type commands
|
||||
if (line.startsWith('TYPE ')) {
|
||||
const content = line.substring(5).trim();
|
||||
|
||||
// Variable type
|
||||
if (content.startsWith('$')) {
|
||||
return this.createBlock('c4a_type_var', {
|
||||
'VAR': content.substring(1)
|
||||
});
|
||||
}
|
||||
|
||||
// Text type
|
||||
const textMatch = content.match(/^"([^"]*)"$/);
|
||||
if (textMatch) {
|
||||
return this.createBlock('c4a_type', {
|
||||
'TEXT': textMatch[1]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// SET command
|
||||
if (line.startsWith('SET ')) {
|
||||
const match = line.match(/^SET\s+`([^`]+)`\s+"([^"]*)"$/);
|
||||
if (match) {
|
||||
return this.createBlock('c4a_set', {
|
||||
'SELECTOR': match[1],
|
||||
'VALUE': match[2]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// CLEAR command
|
||||
if (line.startsWith('CLEAR ')) {
|
||||
const match = line.match(/^CLEAR\s+`([^`]+)`$/);
|
||||
if (match) {
|
||||
return this.createBlock('c4a_clear', {
|
||||
'SELECTOR': match[1]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// SETVAR command
|
||||
if (line.startsWith('SETVAR ')) {
|
||||
const match = line.match(/^SETVAR\s+(\w+)\s*=\s*"([^"]*)"$/);
|
||||
if (match) {
|
||||
return this.createBlock('c4a_setvar', {
|
||||
'NAME': match[1],
|
||||
'VALUE': match[2]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// IF commands (simplified - only single line)
|
||||
if (line.startsWith('IF ')) {
|
||||
// IF EXISTS
|
||||
const existsMatch = line.match(/^IF\s+\(EXISTS\s+`([^`]+)`\)\s+THEN\s+(.+?)(?:\s+ELSE\s+(.+))?$/);
|
||||
if (existsMatch) {
|
||||
if (existsMatch[3]) {
|
||||
// Has ELSE
|
||||
const block = this.createBlock('c4a_if_exists_else', {
|
||||
'SELECTOR': existsMatch[1]
|
||||
});
|
||||
// Parse then and else commands - simplified for now
|
||||
return block;
|
||||
} else {
|
||||
// No ELSE
|
||||
const block = this.createBlock('c4a_if_exists', {
|
||||
'SELECTOR': existsMatch[1]
|
||||
});
|
||||
return block;
|
||||
}
|
||||
}
|
||||
|
||||
// IF NOT EXISTS
|
||||
const notExistsMatch = line.match(/^IF\s+\(NOT\s+EXISTS\s+`([^`]+)`\)\s+THEN\s+(.+)$/);
|
||||
if (notExistsMatch) {
|
||||
const block = this.createBlock('c4a_if_not_exists', {
|
||||
'SELECTOR': notExistsMatch[1]
|
||||
});
|
||||
return block;
|
||||
}
|
||||
}
|
||||
|
||||
// Comments
|
||||
if (line.startsWith('#')) {
|
||||
return this.createBlock('c4a_comment', {
|
||||
'TEXT': line.substring(1).trim()
|
||||
});
|
||||
}
|
||||
|
||||
// If we can't parse it, return null
|
||||
return null;
|
||||
}
|
||||
|
||||
createBlock(type, fields = {}) {
|
||||
const block = document.createElement('block');
|
||||
block.setAttribute('type', type);
|
||||
|
||||
// Add fields
|
||||
for (const [name, value] of Object.entries(fields)) {
|
||||
const field = document.createElement('field');
|
||||
field.setAttribute('name', name);
|
||||
field.textContent = value;
|
||||
block.appendChild(field);
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/* Blockly Theme CSS for C4A-Script */
|
||||
|
||||
/* Blockly workspace container */
|
||||
.blockly-workspace {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
/* Blockly button active state */
|
||||
#blockly-btn.active {
|
||||
background: var(--primary-color);
|
||||
color: var(--bg-primary);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
#blockly-btn.active:hover {
|
||||
background: var(--primary-hover);
|
||||
border-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
/* Override Blockly's default styles for dark theme */
|
||||
.blocklyToolboxDiv {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
border-right: 1px solid var(--border-color) !important;
|
||||
}
|
||||
|
||||
.blocklyFlyout {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
}
|
||||
|
||||
.blocklyFlyoutBackground {
|
||||
fill: var(--bg-secondary) !important;
|
||||
}
|
||||
|
||||
.blocklyMainBackground {
|
||||
stroke: none !important;
|
||||
}
|
||||
|
||||
.blocklyTreeRow {
|
||||
color: var(--text-primary) !important;
|
||||
font-family: 'Dank Mono', monospace !important;
|
||||
padding: 4px 16px !important;
|
||||
margin: 2px 0 !important;
|
||||
}
|
||||
|
||||
.blocklyTreeRow:hover {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
}
|
||||
|
||||
.blocklyTreeSelected {
|
||||
background-color: var(--primary-dim) !important;
|
||||
}
|
||||
|
||||
.blocklyTreeLabel {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Blockly scrollbars */
|
||||
.blocklyScrollbarHorizontal,
|
||||
.blocklyScrollbarVertical {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.blocklyScrollbarHandle {
|
||||
fill: var(--border-color) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
.blocklyScrollbarHandle:hover {
|
||||
fill: var(--border-hover) !important;
|
||||
opacity: 0.8 !important;
|
||||
}
|
||||
|
||||
/* Blockly zoom controls */
|
||||
.blocklyZoom > image {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.blocklyZoom > image:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Blockly trash can */
|
||||
.blocklyTrash {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.blocklyTrash:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Blockly context menus */
|
||||
.blocklyContextMenu {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.blocklyMenuItem {
|
||||
color: var(--text-primary) !important;
|
||||
font-family: 'Dank Mono', monospace !important;
|
||||
}
|
||||
|
||||
.blocklyMenuItemDisabled {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.blocklyMenuItem:hover {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
}
|
||||
|
||||
/* Blockly text inputs */
|
||||
.blocklyHtmlInput {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
font-family: 'Dank Mono', monospace !important;
|
||||
font-size: 13px !important;
|
||||
padding: 4px 8px !important;
|
||||
}
|
||||
|
||||
.blocklyHtmlInput:focus {
|
||||
border-color: var(--primary-color) !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* Blockly dropdowns */
|
||||
.blocklyDropDownDiv {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.blocklyDropDownContent {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.blocklyDropDownDiv .goog-menuitem {
|
||||
color: var(--text-primary) !important;
|
||||
font-family: 'Dank Mono', monospace !important;
|
||||
padding: 4px 16px !important;
|
||||
}
|
||||
|
||||
.blocklyDropDownDiv .goog-menuitem-highlight,
|
||||
.blocklyDropDownDiv .goog-menuitem-hover {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
}
|
||||
|
||||
/* Custom block colors are defined in the block definitions */
|
||||
|
||||
/* Block text styling */
|
||||
.blocklyText {
|
||||
fill: #ffffff !important;
|
||||
font-family: 'Dank Mono', monospace !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.blocklyEditableText > .blocklyText {
|
||||
fill: #ffffff !important;
|
||||
}
|
||||
|
||||
.blocklyEditableText:hover > rect {
|
||||
stroke: var(--primary-color) !important;
|
||||
stroke-width: 2px !important;
|
||||
}
|
||||
|
||||
/* Improve visibility of connection highlights */
|
||||
.blocklyHighlightedConnectionPath {
|
||||
stroke: var(--primary-color) !important;
|
||||
stroke-width: 4px !important;
|
||||
}
|
||||
|
||||
.blocklyInsertionMarker > .blocklyPath {
|
||||
fill-opacity: 0.3 !important;
|
||||
stroke-opacity: 0.6 !important;
|
||||
}
|
||||
|
||||
/* Workspace grid pattern */
|
||||
.blocklyWorkspace > .blocklyBlockCanvas > .blocklyGridCanvas {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
/* Smooth transitions */
|
||||
.blocklyDraggable {
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
/* Field labels */
|
||||
.blocklyFieldLabel {
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
/* Comment blocks styling */
|
||||
.blocklyCommentText {
|
||||
font-style: italic !important;
|
||||
}
|
||||
|
||||
/* Make comment blocks slightly transparent */
|
||||
g[data-category="Comments"] .blocklyPath {
|
||||
fill-opacity: 0.8 !important;
|
||||
}
|
||||
|
||||
/* Better visibility for disabled blocks */
|
||||
.blocklyDisabled > .blocklyPath {
|
||||
fill-opacity: 0.3 !important;
|
||||
}
|
||||
|
||||
.blocklyDisabled > .blocklyText {
|
||||
fill-opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
/* Warning and error text */
|
||||
.blocklyWarningText,
|
||||
.blocklyErrorText {
|
||||
font-family: 'Dank Mono', monospace !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
/* Workspace scrollbar improvement for dark theme */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-hover);
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
// C4A-Script Blockly Block Definitions
|
||||
// This file defines all custom blocks for C4A-Script commands
|
||||
|
||||
// Color scheme for different block categories
|
||||
const BlockColors = {
|
||||
NAVIGATION: '#1E88E5', // Blue
|
||||
ACTIONS: '#43A047', // Green
|
||||
CONTROL: '#FB8C00', // Orange
|
||||
VARIABLES: '#8E24AA', // Purple
|
||||
WAIT: '#E53935', // Red
|
||||
KEYBOARD: '#00ACC1', // Cyan
|
||||
PROCEDURES: '#6A1B9A' // Deep Purple
|
||||
};
|
||||
|
||||
// Helper to create selector input with backticks
|
||||
Blockly.Blocks['c4a_selector_input'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("selector"), "SELECTOR")
|
||||
.appendField("`");
|
||||
this.setOutput(true, "Selector");
|
||||
this.setColour(BlockColors.ACTIONS);
|
||||
this.setTooltip("CSS selector for element");
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// NAVIGATION BLOCKS
|
||||
// ============================================
|
||||
|
||||
Blockly.Blocks['c4a_go'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("GO")
|
||||
.appendField(new Blockly.FieldTextInput("https://example.com"), "URL");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.NAVIGATION);
|
||||
this.setTooltip("Navigate to URL");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_reload'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("RELOAD");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.NAVIGATION);
|
||||
this.setTooltip("Reload current page");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_back'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("BACK");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.NAVIGATION);
|
||||
this.setTooltip("Go back in browser history");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_forward'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("FORWARD");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.NAVIGATION);
|
||||
this.setTooltip("Go forward in browser history");
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// WAIT BLOCKS
|
||||
// ============================================
|
||||
|
||||
Blockly.Blocks['c4a_wait_time'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("WAIT")
|
||||
.appendField(new Blockly.FieldNumber(1, 0), "SECONDS")
|
||||
.appendField("seconds");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.WAIT);
|
||||
this.setTooltip("Wait for specified seconds");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_wait_selector'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("WAIT for")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("selector"), "SELECTOR")
|
||||
.appendField("`")
|
||||
.appendField("max")
|
||||
.appendField(new Blockly.FieldNumber(10, 1), "TIMEOUT")
|
||||
.appendField("sec");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.WAIT);
|
||||
this.setTooltip("Wait for element to appear");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_wait_text'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("WAIT for text")
|
||||
.appendField(new Blockly.FieldTextInput("Loading complete"), "TEXT")
|
||||
.appendField("max")
|
||||
.appendField(new Blockly.FieldNumber(5, 1), "TIMEOUT")
|
||||
.appendField("sec");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.WAIT);
|
||||
this.setTooltip("Wait for text to appear on page");
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// MOUSE ACTION BLOCKS
|
||||
// ============================================
|
||||
|
||||
Blockly.Blocks['c4a_click'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("CLICK")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("button"), "SELECTOR")
|
||||
.appendField("`");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.ACTIONS);
|
||||
this.setTooltip("Click on element");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_click_xy'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("CLICK at")
|
||||
.appendField("X:")
|
||||
.appendField(new Blockly.FieldNumber(100, 0), "X")
|
||||
.appendField("Y:")
|
||||
.appendField(new Blockly.FieldNumber(100, 0), "Y");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.ACTIONS);
|
||||
this.setTooltip("Click at coordinates");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_double_click'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("DOUBLE_CLICK")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput(".item"), "SELECTOR")
|
||||
.appendField("`");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.ACTIONS);
|
||||
this.setTooltip("Double click on element");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_right_click'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("RIGHT_CLICK")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("#menu"), "SELECTOR")
|
||||
.appendField("`");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.ACTIONS);
|
||||
this.setTooltip("Right click on element");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_move'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("MOVE to")
|
||||
.appendField("X:")
|
||||
.appendField(new Blockly.FieldNumber(500, 0), "X")
|
||||
.appendField("Y:")
|
||||
.appendField(new Blockly.FieldNumber(300, 0), "Y");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.ACTIONS);
|
||||
this.setTooltip("Move mouse to position");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_drag'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("DRAG from")
|
||||
.appendField("X:")
|
||||
.appendField(new Blockly.FieldNumber(100, 0), "X1")
|
||||
.appendField("Y:")
|
||||
.appendField(new Blockly.FieldNumber(100, 0), "Y1");
|
||||
this.appendDummyInput()
|
||||
.appendField("to")
|
||||
.appendField("X:")
|
||||
.appendField(new Blockly.FieldNumber(500, 0), "X2")
|
||||
.appendField("Y:")
|
||||
.appendField(new Blockly.FieldNumber(300, 0), "Y2");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.ACTIONS);
|
||||
this.setTooltip("Drag from one point to another");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_scroll'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("SCROLL")
|
||||
.appendField(new Blockly.FieldDropdown([
|
||||
["DOWN", "DOWN"],
|
||||
["UP", "UP"],
|
||||
["LEFT", "LEFT"],
|
||||
["RIGHT", "RIGHT"]
|
||||
]), "DIRECTION")
|
||||
.appendField(new Blockly.FieldNumber(500, 0), "AMOUNT")
|
||||
.appendField("pixels");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.ACTIONS);
|
||||
this.setTooltip("Scroll in direction");
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// KEYBOARD BLOCKS
|
||||
// ============================================
|
||||
|
||||
Blockly.Blocks['c4a_type'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("TYPE")
|
||||
.appendField(new Blockly.FieldTextInput("text to type"), "TEXT");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.KEYBOARD);
|
||||
this.setTooltip("Type text");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_type_var'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("TYPE")
|
||||
.appendField("$")
|
||||
.appendField(new Blockly.FieldTextInput("variable"), "VAR");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.KEYBOARD);
|
||||
this.setTooltip("Type variable value");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_clear'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("CLEAR")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("input"), "SELECTOR")
|
||||
.appendField("`");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.KEYBOARD);
|
||||
this.setTooltip("Clear input field");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_set'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("SET")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("#input"), "SELECTOR")
|
||||
.appendField("`")
|
||||
.appendField("to")
|
||||
.appendField(new Blockly.FieldTextInput("value"), "VALUE");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.KEYBOARD);
|
||||
this.setTooltip("Set input field value");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_press'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("PRESS")
|
||||
.appendField(new Blockly.FieldDropdown([
|
||||
["Tab", "Tab"],
|
||||
["Enter", "Enter"],
|
||||
["Escape", "Escape"],
|
||||
["Space", "Space"],
|
||||
["ArrowUp", "ArrowUp"],
|
||||
["ArrowDown", "ArrowDown"],
|
||||
["ArrowLeft", "ArrowLeft"],
|
||||
["ArrowRight", "ArrowRight"],
|
||||
["Delete", "Delete"],
|
||||
["Backspace", "Backspace"]
|
||||
]), "KEY");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.KEYBOARD);
|
||||
this.setTooltip("Press and release key");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_key_down'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("KEY_DOWN")
|
||||
.appendField(new Blockly.FieldDropdown([
|
||||
["Shift", "Shift"],
|
||||
["Control", "Control"],
|
||||
["Alt", "Alt"],
|
||||
["Meta", "Meta"]
|
||||
]), "KEY");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.KEYBOARD);
|
||||
this.setTooltip("Hold key down");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_key_up'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("KEY_UP")
|
||||
.appendField(new Blockly.FieldDropdown([
|
||||
["Shift", "Shift"],
|
||||
["Control", "Control"],
|
||||
["Alt", "Alt"],
|
||||
["Meta", "Meta"]
|
||||
]), "KEY");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.KEYBOARD);
|
||||
this.setTooltip("Release key");
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// CONTROL FLOW BLOCKS
|
||||
// ============================================
|
||||
|
||||
Blockly.Blocks['c4a_if_exists'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("IF EXISTS")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput(".element"), "SELECTOR")
|
||||
.appendField("`")
|
||||
.appendField("THEN");
|
||||
this.appendStatementInput("THEN")
|
||||
.setCheck(null);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.CONTROL);
|
||||
this.setTooltip("If element exists, then do something");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_if_exists_else'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("IF EXISTS")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput(".element"), "SELECTOR")
|
||||
.appendField("`")
|
||||
.appendField("THEN");
|
||||
this.appendStatementInput("THEN")
|
||||
.setCheck(null);
|
||||
this.appendDummyInput()
|
||||
.appendField("ELSE");
|
||||
this.appendStatementInput("ELSE")
|
||||
.setCheck(null);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.CONTROL);
|
||||
this.setTooltip("If element exists, then do something, else do something else");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_if_not_exists'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("IF NOT EXISTS")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput(".element"), "SELECTOR")
|
||||
.appendField("`")
|
||||
.appendField("THEN");
|
||||
this.appendStatementInput("THEN")
|
||||
.setCheck(null);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.CONTROL);
|
||||
this.setTooltip("If element does not exist, then do something");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_if_js'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("IF")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("window.innerWidth < 768"), "CONDITION")
|
||||
.appendField("`")
|
||||
.appendField("THEN");
|
||||
this.appendStatementInput("THEN")
|
||||
.setCheck(null);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.CONTROL);
|
||||
this.setTooltip("If JavaScript condition is true");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_repeat_times'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("REPEAT")
|
||||
.appendField(new Blockly.FieldNumber(5, 1), "TIMES")
|
||||
.appendField("times");
|
||||
this.appendStatementInput("DO")
|
||||
.setCheck(null);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.CONTROL);
|
||||
this.setTooltip("Repeat commands N times");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_repeat_while'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("REPEAT WHILE")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("document.querySelector('.load-more')"), "CONDITION")
|
||||
.appendField("`");
|
||||
this.appendStatementInput("DO")
|
||||
.setCheck(null);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.CONTROL);
|
||||
this.setTooltip("Repeat while condition is true");
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// VARIABLE BLOCKS
|
||||
// ============================================
|
||||
|
||||
Blockly.Blocks['c4a_setvar'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("SETVAR")
|
||||
.appendField(new Blockly.FieldTextInput("username"), "NAME")
|
||||
.appendField("=")
|
||||
.appendField(new Blockly.FieldTextInput("value"), "VALUE");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.VARIABLES);
|
||||
this.setTooltip("Set variable value");
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// ADVANCED BLOCKS
|
||||
// ============================================
|
||||
|
||||
Blockly.Blocks['c4a_eval'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("EVAL")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("console.log('Hello')"), "CODE")
|
||||
.appendField("`");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.VARIABLES);
|
||||
this.setTooltip("Execute JavaScript code");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_comment'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("#")
|
||||
.appendField(new Blockly.FieldTextInput("Comment", null, {
|
||||
spellcheck: false,
|
||||
class: 'blocklyCommentText'
|
||||
}), "TEXT");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour("#616161");
|
||||
this.setTooltip("Add a comment");
|
||||
this.setStyle('comment_blocks');
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// PROCEDURE BLOCKS
|
||||
// ============================================
|
||||
|
||||
Blockly.Blocks['c4a_proc_def'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("PROC")
|
||||
.appendField(new Blockly.FieldTextInput("procedure_name"), "NAME");
|
||||
this.appendStatementInput("BODY")
|
||||
.setCheck(null);
|
||||
this.appendDummyInput()
|
||||
.appendField("ENDPROC");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.PROCEDURES);
|
||||
this.setTooltip("Define a procedure");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_proc_call'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("Call")
|
||||
.appendField(new Blockly.FieldTextInput("procedure_name"), "NAME");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.PROCEDURES);
|
||||
this.setTooltip("Call a procedure");
|
||||
}
|
||||
};
|
||||
|
||||
// Code generators have been moved to c4a-generator.js
|
||||
@@ -0,0 +1,261 @@
|
||||
// C4A-Script Code Generator for Blockly
|
||||
// Compatible with latest Blockly API
|
||||
|
||||
// Create a custom code generator for C4A-Script
|
||||
const c4aGenerator = new Blockly.Generator('C4A');
|
||||
|
||||
// Helper to get field value with proper escaping
|
||||
c4aGenerator.getFieldValue = function(block, fieldName) {
|
||||
return block.getFieldValue(fieldName);
|
||||
};
|
||||
|
||||
// Navigation generators
|
||||
c4aGenerator.forBlock['c4a_go'] = function(block, generator) {
|
||||
const url = generator.getFieldValue(block, 'URL');
|
||||
return `GO ${url}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_reload'] = function(block, generator) {
|
||||
return 'RELOAD\n';
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_back'] = function(block, generator) {
|
||||
return 'BACK\n';
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_forward'] = function(block, generator) {
|
||||
return 'FORWARD\n';
|
||||
};
|
||||
|
||||
// Wait generators
|
||||
c4aGenerator.forBlock['c4a_wait_time'] = function(block, generator) {
|
||||
const seconds = generator.getFieldValue(block, 'SECONDS');
|
||||
return `WAIT ${seconds}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_wait_selector'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
const timeout = generator.getFieldValue(block, 'TIMEOUT');
|
||||
return `WAIT \`${selector}\` ${timeout}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_wait_text'] = function(block, generator) {
|
||||
const text = generator.getFieldValue(block, 'TEXT');
|
||||
const timeout = generator.getFieldValue(block, 'TIMEOUT');
|
||||
return `WAIT "${text}" ${timeout}\n`;
|
||||
};
|
||||
|
||||
// Mouse action generators
|
||||
c4aGenerator.forBlock['c4a_click'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
return `CLICK \`${selector}\`\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_click_xy'] = function(block, generator) {
|
||||
const x = generator.getFieldValue(block, 'X');
|
||||
const y = generator.getFieldValue(block, 'Y');
|
||||
return `CLICK ${x} ${y}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_double_click'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
return `DOUBLE_CLICK \`${selector}\`\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_right_click'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
return `RIGHT_CLICK \`${selector}\`\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_move'] = function(block, generator) {
|
||||
const x = generator.getFieldValue(block, 'X');
|
||||
const y = generator.getFieldValue(block, 'Y');
|
||||
return `MOVE ${x} ${y}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_drag'] = function(block, generator) {
|
||||
const x1 = generator.getFieldValue(block, 'X1');
|
||||
const y1 = generator.getFieldValue(block, 'Y1');
|
||||
const x2 = generator.getFieldValue(block, 'X2');
|
||||
const y2 = generator.getFieldValue(block, 'Y2');
|
||||
return `DRAG ${x1} ${y1} ${x2} ${y2}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_scroll'] = function(block, generator) {
|
||||
const direction = generator.getFieldValue(block, 'DIRECTION');
|
||||
const amount = generator.getFieldValue(block, 'AMOUNT');
|
||||
return `SCROLL ${direction} ${amount}\n`;
|
||||
};
|
||||
|
||||
// Keyboard generators
|
||||
c4aGenerator.forBlock['c4a_type'] = function(block, generator) {
|
||||
const text = generator.getFieldValue(block, 'TEXT');
|
||||
return `TYPE "${text}"\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_type_var'] = function(block, generator) {
|
||||
const varName = generator.getFieldValue(block, 'VAR');
|
||||
return `TYPE $${varName}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_clear'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
return `CLEAR \`${selector}\`\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_set'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
const value = generator.getFieldValue(block, 'VALUE');
|
||||
return `SET \`${selector}\` "${value}"\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_press'] = function(block, generator) {
|
||||
const key = generator.getFieldValue(block, 'KEY');
|
||||
return `PRESS ${key}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_key_down'] = function(block, generator) {
|
||||
const key = generator.getFieldValue(block, 'KEY');
|
||||
return `KEY_DOWN ${key}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_key_up'] = function(block, generator) {
|
||||
const key = generator.getFieldValue(block, 'KEY');
|
||||
return `KEY_UP ${key}\n`;
|
||||
};
|
||||
|
||||
// Control flow generators
|
||||
c4aGenerator.forBlock['c4a_if_exists'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
const thenCode = generator.statementToCode(block, 'THEN').trim();
|
||||
|
||||
if (thenCode.includes('\n')) {
|
||||
// Multi-line then block
|
||||
const lines = thenCode.split('\n').filter(line => line.trim());
|
||||
return lines.map(line => `IF (EXISTS \`${selector}\`) THEN ${line}`).join('\n') + '\n';
|
||||
} else if (thenCode) {
|
||||
// Single line
|
||||
return `IF (EXISTS \`${selector}\`) THEN ${thenCode}\n`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_if_exists_else'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
const thenCode = generator.statementToCode(block, 'THEN').trim();
|
||||
const elseCode = generator.statementToCode(block, 'ELSE').trim();
|
||||
|
||||
// For simplicity, only handle single-line then/else
|
||||
const thenLine = thenCode.split('\n')[0];
|
||||
const elseLine = elseCode.split('\n')[0];
|
||||
|
||||
if (thenLine && elseLine) {
|
||||
return `IF (EXISTS \`${selector}\`) THEN ${thenLine} ELSE ${elseLine}\n`;
|
||||
} else if (thenLine) {
|
||||
return `IF (EXISTS \`${selector}\`) THEN ${thenLine}\n`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_if_not_exists'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
const thenCode = generator.statementToCode(block, 'THEN').trim();
|
||||
|
||||
if (thenCode.includes('\n')) {
|
||||
const lines = thenCode.split('\n').filter(line => line.trim());
|
||||
return lines.map(line => `IF (NOT EXISTS \`${selector}\`) THEN ${line}`).join('\n') + '\n';
|
||||
} else if (thenCode) {
|
||||
return `IF (NOT EXISTS \`${selector}\`) THEN ${thenCode}\n`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_if_js'] = function(block, generator) {
|
||||
const condition = generator.getFieldValue(block, 'CONDITION');
|
||||
const thenCode = generator.statementToCode(block, 'THEN').trim();
|
||||
|
||||
if (thenCode.includes('\n')) {
|
||||
const lines = thenCode.split('\n').filter(line => line.trim());
|
||||
return lines.map(line => `IF (\`${condition}\`) THEN ${line}`).join('\n') + '\n';
|
||||
} else if (thenCode) {
|
||||
return `IF (\`${condition}\`) THEN ${thenCode}\n`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_repeat_times'] = function(block, generator) {
|
||||
const times = generator.getFieldValue(block, 'TIMES');
|
||||
const doCode = generator.statementToCode(block, 'DO').trim();
|
||||
|
||||
if (doCode) {
|
||||
// Get first command for repeat
|
||||
const firstLine = doCode.split('\n')[0];
|
||||
return `REPEAT (${firstLine}, ${times})\n`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_repeat_while'] = function(block, generator) {
|
||||
const condition = generator.getFieldValue(block, 'CONDITION');
|
||||
const doCode = generator.statementToCode(block, 'DO').trim();
|
||||
|
||||
if (doCode) {
|
||||
// Get first command for repeat
|
||||
const firstLine = doCode.split('\n')[0];
|
||||
return `REPEAT (${firstLine}, \`${condition}\`)\n`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// Variable generators
|
||||
c4aGenerator.forBlock['c4a_setvar'] = function(block, generator) {
|
||||
const name = generator.getFieldValue(block, 'NAME');
|
||||
const value = generator.getFieldValue(block, 'VALUE');
|
||||
return `SETVAR ${name} = "${value}"\n`;
|
||||
};
|
||||
|
||||
// Advanced generators
|
||||
c4aGenerator.forBlock['c4a_eval'] = function(block, generator) {
|
||||
const code = generator.getFieldValue(block, 'CODE');
|
||||
return `EVAL \`${code}\`\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_comment'] = function(block, generator) {
|
||||
const text = generator.getFieldValue(block, 'TEXT');
|
||||
return `# ${text}\n`;
|
||||
};
|
||||
|
||||
// Procedure generators
|
||||
c4aGenerator.forBlock['c4a_proc_def'] = function(block, generator) {
|
||||
const name = generator.getFieldValue(block, 'NAME');
|
||||
const body = generator.statementToCode(block, 'BODY');
|
||||
return `PROC ${name}\n${body}ENDPROC\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_proc_call'] = function(block, generator) {
|
||||
const name = generator.getFieldValue(block, 'NAME');
|
||||
return `${name}\n`;
|
||||
};
|
||||
|
||||
// Override scrub_ to handle our custom format
|
||||
c4aGenerator.scrub_ = function(block, code, opt_thisOnly) {
|
||||
const nextBlock = block.nextConnection && block.nextConnection.targetBlock();
|
||||
let nextCode = '';
|
||||
|
||||
if (nextBlock) {
|
||||
if (!opt_thisOnly) {
|
||||
nextCode = c4aGenerator.blockToCode(nextBlock);
|
||||
|
||||
// Add blank line between comment and non-comment blocks
|
||||
const currentIsComment = block.type === 'c4a_comment';
|
||||
const nextIsComment = nextBlock.type === 'c4a_comment';
|
||||
|
||||
// Add blank line when transitioning from command to comment or vice versa
|
||||
if (currentIsComment !== nextIsComment && code.trim() && nextCode.trim()) {
|
||||
nextCode = '\n' + nextCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return code + nextCode;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Demo: Login Flow with Blockly
|
||||
# This script can be created visually using Blockly blocks
|
||||
|
||||
GO https://example.com/login
|
||||
WAIT `#login-form` 5
|
||||
|
||||
# Check if already logged in
|
||||
IF (EXISTS `.user-avatar`) THEN GO https://example.com/dashboard
|
||||
|
||||
# Fill login form
|
||||
CLICK `#email`
|
||||
TYPE "demo@example.com"
|
||||
CLICK `#password`
|
||||
TYPE "password123"
|
||||
|
||||
# Submit form
|
||||
CLICK `button[type="submit"]`
|
||||
WAIT `.dashboard` 10
|
||||
|
||||
# Success message
|
||||
EVAL `console.log('Login successful!')`
|
||||
@@ -0,0 +1,205 @@
|
||||
<!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="assets/blockly-theme.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/theme/material-darker.min.css">
|
||||
</head>
|
||||
<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>
|
||||
|
||||
<!-- Event Editor Modal -->
|
||||
<div id="event-editor-overlay" class="modal-overlay hidden"></div>
|
||||
<div id="event-editor-modal" class="event-editor-modal hidden">
|
||||
<h4>Edit Event</h4>
|
||||
<div class="editor-field">
|
||||
<label>Command Type</label>
|
||||
<select id="edit-command-type" disabled>
|
||||
<option value="CLICK">CLICK</option>
|
||||
<option value="DOUBLE_CLICK">DOUBLE_CLICK</option>
|
||||
<option value="RIGHT_CLICK">RIGHT_CLICK</option>
|
||||
<option value="TYPE">TYPE</option>
|
||||
<option value="SET">SET</option>
|
||||
<option value="SCROLL">SCROLL</option>
|
||||
<option value="WAIT">WAIT</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="edit-selector-field" class="editor-field">
|
||||
<label>Selector</label>
|
||||
<input type="text" id="edit-selector" placeholder=".class or #id">
|
||||
</div>
|
||||
<div id="edit-value-field" class="editor-field">
|
||||
<label>Value</label>
|
||||
<input type="text" id="edit-value" placeholder="Text or number">
|
||||
</div>
|
||||
<div id="edit-direction-field" class="editor-field hidden">
|
||||
<label>Direction</label>
|
||||
<select id="edit-direction">
|
||||
<option value="UP">UP</option>
|
||||
<option value="DOWN">DOWN</option>
|
||||
<option value="LEFT">LEFT</option>
|
||||
<option value="RIGHT">RIGHT</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="editor-actions">
|
||||
<button id="edit-cancel" class="mini-btn">Cancel</button>
|
||||
<button id="edit-save" class="mini-btn primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main App Layout -->
|
||||
<div class="app-container">
|
||||
<!-- 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>
|
||||
<button id="record-btn" class="action-btn record">
|
||||
<span class="icon">⏺</span>Record
|
||||
</button>
|
||||
<button id="timeline-btn" class="action-btn timeline hidden" title="View Timeline">
|
||||
<span class="icon">📊</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-container">
|
||||
<div id="editor-view" class="editor-wrapper">
|
||||
<textarea id="c4a-editor" placeholder="# Write your C4A script here..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Recording Timeline -->
|
||||
<div id="timeline-view" class="recording-timeline hidden">
|
||||
<div class="timeline-header">
|
||||
<h3>Recording Timeline</h3>
|
||||
<div class="timeline-actions">
|
||||
<button id="back-to-editor" class="mini-btn">← Back</button>
|
||||
<button id="select-all-events" class="mini-btn">Select All</button>
|
||||
<button id="clear-events" class="mini-btn">Clear</button>
|
||||
<button id="generate-script" class="mini-btn primary">Generate Script</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="timeline-events" class="timeline-events">
|
||||
<!-- Events will be added here dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom: Output Tabs -->
|
||||
<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>
|
||||
|
||||
<!-- Blockly -->
|
||||
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
|
||||
<script src="assets/c4a-blocks.js"></script>
|
||||
<script src="assets/c4a-generator.js"></script>
|
||||
<script src="assets/blockly-manager.js"></script>
|
||||
|
||||
<script src="assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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!');
|
||||
});
|
||||
@@ -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">×</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">×</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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
flask>=2.3.0
|
||||
flask-cors>=4.0.0
|
||||
@@ -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:8000/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', 8000))
|
||||
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)
|
||||
@@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Blockly Test</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #0e0e10;
|
||||
color: #e0e0e0;
|
||||
font-family: monospace;
|
||||
}
|
||||
#blocklyDiv {
|
||||
height: 600px;
|
||||
width: 100%;
|
||||
border: 1px solid #2a2a2c;
|
||||
}
|
||||
#output {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #1a1a1b;
|
||||
border: 1px solid #2a2a2c;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>C4A-Script Blockly Test</h1>
|
||||
<div id="blocklyDiv"></div>
|
||||
<div id="output">
|
||||
<h3>Generated C4A-Script:</h3>
|
||||
<pre id="code-output"></pre>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
|
||||
<script src="assets/c4a-blocks.js"></script>
|
||||
<script>
|
||||
// Simple test
|
||||
const workspace = Blockly.inject('blocklyDiv', {
|
||||
toolbox: `
|
||||
<xml>
|
||||
<category name="Test" colour="#1E88E5">
|
||||
<block type="c4a_go"></block>
|
||||
<block type="c4a_wait_time"></block>
|
||||
<block type="c4a_click"></block>
|
||||
</category>
|
||||
</xml>
|
||||
`,
|
||||
theme: Blockly.Theme.defineTheme('dark', {
|
||||
'base': Blockly.Themes.Classic,
|
||||
'componentStyles': {
|
||||
'workspaceBackgroundColour': '#0e0e10',
|
||||
'toolboxBackgroundColour': '#1a1a1b',
|
||||
'toolboxForegroundColour': '#e0e0e0',
|
||||
'flyoutBackgroundColour': '#1a1a1b',
|
||||
'flyoutForegroundColour': '#e0e0e0',
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
workspace.addChangeListener((event) => {
|
||||
const code = Blockly.JavaScript.workspaceToCode(workspace);
|
||||
document.getElementById('code-output').textContent = code;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,124 @@
|
||||
# Crawl4AI Chrome Extension
|
||||
|
||||
Visual extraction tools for Crawl4AI - Click to extract data and content from any webpage!
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- **Click2Crawl**: Click on elements to build data extraction schemas instantly
|
||||
- **Markdown Extraction**: Select elements and export as clean markdown
|
||||
- **Script Builder (Alpha)**: Record browser actions to create automation scripts
|
||||
- **Smart Element Selection**: Container and field selection with visual feedback
|
||||
- **Code Generation**: Generates complete Python code for Crawl4AI
|
||||
- **Beautiful Dark UI**: Consistent with Crawl4AI's design language
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### Method 1: Load Unpacked Extension (Recommended for Development)
|
||||
|
||||
1. Open Chrome and navigate to `chrome://extensions/`
|
||||
2. Enable "Developer mode" in the top right corner
|
||||
3. Click "Load unpacked"
|
||||
4. Select the `crawl4ai-assistant` folder
|
||||
5. The extension icon (🚀🤖) will appear in your toolbar
|
||||
|
||||
### Method 2: Generate Icons First
|
||||
|
||||
If you want proper icons:
|
||||
|
||||
1. Open `icons/generate_icons.html` in your browser
|
||||
2. Right-click each canvas and save as:
|
||||
- `icon-16.png`
|
||||
- `icon-48.png`
|
||||
- `icon-128.png`
|
||||
3. Then follow Method 1 above
|
||||
|
||||
## 🎯 How to Use
|
||||
|
||||
### Using Click2Crawl
|
||||
|
||||
1. **Navigate to any website** you want to extract data from
|
||||
2. **Click the Crawl4AI extension icon** in your toolbar
|
||||
3. **Click "Click2Crawl"** to start the capture mode
|
||||
4. **Select a container element**:
|
||||
- Hover over elements (they'll highlight in blue)
|
||||
- Click on a repeating container (e.g., product card, article block)
|
||||
5. **Select fields within the container**:
|
||||
- Elements will now highlight in green
|
||||
- Click on each piece of data you want to extract
|
||||
- Name each field (e.g., "title", "price", "description")
|
||||
6. **Test and Export**:
|
||||
- Click "Test Schema" to see extracted data instantly
|
||||
- Export as Python code, JSON schema, or markdown
|
||||
|
||||
### Running the Generated Code
|
||||
|
||||
The downloaded Python file contains:
|
||||
|
||||
```python
|
||||
# 1. The HTML snippet of your selected container
|
||||
HTML_SNIPPET = """..."""
|
||||
|
||||
# 2. The extraction query based on your selections
|
||||
EXTRACTION_QUERY = """..."""
|
||||
|
||||
# 3. Functions to generate and test the schema
|
||||
async def generate_schema():
|
||||
# Generates the extraction schema using LLM
|
||||
|
||||
async def test_extraction():
|
||||
# Tests the schema on the actual website
|
||||
```
|
||||
|
||||
To use it:
|
||||
|
||||
1. Install Crawl4AI: `pip install crawl4ai`
|
||||
2. Run the script: `python crawl4ai_schema_*.py`
|
||||
3. The script will generate a `generated_schema.json` file
|
||||
4. Use this schema in your Crawl4AI projects!
|
||||
|
||||
## 🎨 Visual Feedback
|
||||
|
||||
- **Blue dashed outline**: Container selection mode
|
||||
- **Green dashed outline**: Field selection mode
|
||||
- **Solid blue outline**: Selected container
|
||||
- **Solid green outline**: Selected fields
|
||||
- **Floating toolbar**: Shows current mode and selection status
|
||||
|
||||
## ⌨️ Keyboard Shortcuts
|
||||
|
||||
- **ESC**: Cancel current capture session
|
||||
|
||||
## 🔧 Technical Details
|
||||
|
||||
- Built with Manifest V3 for security and performance
|
||||
- Pure client-side - no data sent to external servers
|
||||
- Generates code that uses Crawl4AI's LLM integration
|
||||
- Smart selector generation prioritizes stable attributes
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Extension doesn't load
|
||||
- Make sure you're in Developer Mode
|
||||
- Check the console for any errors
|
||||
- Ensure all files are in the correct directories
|
||||
|
||||
### Can't select elements
|
||||
- Some websites may block extensions
|
||||
- Try refreshing the page
|
||||
- Make sure you clicked "Schema Builder" first
|
||||
|
||||
### Generated code doesn't work
|
||||
- Ensure you have Crawl4AI installed
|
||||
- Check that you have an LLM API key configured
|
||||
- Make sure the website structure hasn't changed
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
This extension is part of the Crawl4AI project. Contributions are welcome!
|
||||
|
||||
- Report issues: [GitHub Issues](https://github.com/unclecode/crawl4ai/issues)
|
||||
- Join discussion: [Discord](https://discord.gg/crawl4ai)
|
||||
|
||||
## 📄 License
|
||||
|
||||
Same as Crawl4AI - see main project for details.
|
||||
@@ -0,0 +1,39 @@
|
||||
// Service worker for Crawl4AI Assistant
|
||||
|
||||
// Handle messages from content script
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.action === 'downloadCode' || message.action === 'downloadScript') {
|
||||
try {
|
||||
// Create a data URL for the Python code
|
||||
const dataUrl = 'data:text/plain;charset=utf-8,' + encodeURIComponent(message.code);
|
||||
|
||||
// Download the file
|
||||
chrome.downloads.download({
|
||||
url: dataUrl,
|
||||
filename: message.filename || 'crawl4ai_schema.py',
|
||||
saveAs: true
|
||||
}, (downloadId) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error('Download failed:', chrome.runtime.lastError);
|
||||
sendResponse({ success: false, error: chrome.runtime.lastError.message });
|
||||
} else {
|
||||
console.log('Download started with ID:', downloadId);
|
||||
sendResponse({ success: true, downloadId: downloadId });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating download:', error);
|
||||
sendResponse({ success: false, error: error.message });
|
||||
}
|
||||
|
||||
return true; // Keep the message channel open for async response
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// Clean up on extension install/update
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
// Clear any stored state
|
||||
chrome.storage.local.clear();
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
// Main content script for Crawl4AI Assistant
|
||||
// Coordinates between Click2Crawl, ScriptBuilder, and MarkdownExtraction
|
||||
|
||||
let activeBuilder = null;
|
||||
|
||||
// Listen for messages from popup
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.action === 'startCapture') {
|
||||
if (activeBuilder) {
|
||||
console.log('Stopping existing capture session');
|
||||
activeBuilder.stop();
|
||||
activeBuilder = null;
|
||||
}
|
||||
|
||||
if (request.mode === 'schema') {
|
||||
console.log('Starting Click2Crawl');
|
||||
activeBuilder = new Click2Crawl();
|
||||
activeBuilder.start();
|
||||
} else if (request.mode === 'script') {
|
||||
console.log('Starting Script Builder');
|
||||
activeBuilder = new ScriptBuilder();
|
||||
activeBuilder.start();
|
||||
}
|
||||
|
||||
sendResponse({ success: true });
|
||||
} else if (request.action === 'stopCapture') {
|
||||
if (activeBuilder) {
|
||||
activeBuilder.stop();
|
||||
activeBuilder = null;
|
||||
}
|
||||
sendResponse({ success: true });
|
||||
} else if (request.action === 'startSchemaCapture') {
|
||||
if (activeBuilder) {
|
||||
activeBuilder.deactivate?.();
|
||||
activeBuilder = null;
|
||||
}
|
||||
console.log('Starting Click2Crawl');
|
||||
activeBuilder = new Click2Crawl();
|
||||
activeBuilder.start();
|
||||
sendResponse({ success: true });
|
||||
} else if (request.action === 'startScriptCapture') {
|
||||
if (activeBuilder) {
|
||||
activeBuilder.deactivate?.();
|
||||
activeBuilder = null;
|
||||
}
|
||||
console.log('Starting Script Builder');
|
||||
activeBuilder = new ScriptBuilder();
|
||||
activeBuilder.start();
|
||||
sendResponse({ success: true });
|
||||
} else if (request.action === 'startClick2Crawl') {
|
||||
if (activeBuilder) {
|
||||
activeBuilder.deactivate?.();
|
||||
activeBuilder = null;
|
||||
}
|
||||
console.log('Starting Markdown Extraction');
|
||||
activeBuilder = new MarkdownExtraction();
|
||||
sendResponse({ success: true });
|
||||
} else if (request.action === 'generateCode') {
|
||||
if (activeBuilder && activeBuilder.generateCode) {
|
||||
activeBuilder.generateCode();
|
||||
}
|
||||
sendResponse({ success: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (activeBuilder) {
|
||||
if (activeBuilder.deactivate) {
|
||||
activeBuilder.deactivate();
|
||||
} else if (activeBuilder.stop) {
|
||||
activeBuilder.stop();
|
||||
}
|
||||
activeBuilder = null;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Crawl4AI Assistant content script loaded');
|
||||
@@ -0,0 +1,623 @@
|
||||
class ContentAnalyzer {
|
||||
constructor() {
|
||||
this.patterns = {
|
||||
article: ['article', 'main', 'content', 'post', 'entry'],
|
||||
navigation: ['nav', 'menu', 'navigation', 'breadcrumb'],
|
||||
sidebar: ['sidebar', 'aside', 'widget'],
|
||||
header: ['header', 'masthead', 'banner'],
|
||||
footer: ['footer', 'copyright', 'contact'],
|
||||
list: ['list', 'items', 'results', 'products', 'cards'],
|
||||
table: ['table', 'grid', 'data'],
|
||||
media: ['gallery', 'carousel', 'slideshow', 'video', 'media']
|
||||
};
|
||||
}
|
||||
|
||||
async analyze(elements) {
|
||||
const analysis = {
|
||||
structure: await this.analyzeStructure(elements),
|
||||
contentType: this.identifyContentType(elements),
|
||||
hierarchy: this.buildHierarchy(elements),
|
||||
mediaAssets: this.collectMediaAssets(elements),
|
||||
textDensity: this.calculateTextDensity(elements),
|
||||
semanticRegions: this.identifySemanticRegions(elements),
|
||||
relationships: this.analyzeRelationships(elements),
|
||||
metadata: this.extractMetadata(elements)
|
||||
};
|
||||
|
||||
return analysis;
|
||||
}
|
||||
|
||||
analyzeStructure(elements) {
|
||||
const structure = {
|
||||
hasHeadings: false,
|
||||
hasLists: false,
|
||||
hasTables: false,
|
||||
hasMedia: false,
|
||||
hasCode: false,
|
||||
hasLinks: false,
|
||||
layout: 'linear', // linear, grid, mixed
|
||||
depth: 0,
|
||||
elementTypes: new Map()
|
||||
};
|
||||
|
||||
// Analyze each element
|
||||
for (const element of elements) {
|
||||
this.analyzeElementStructure(element, structure);
|
||||
}
|
||||
|
||||
// Determine layout type
|
||||
structure.layout = this.determineLayout(elements);
|
||||
|
||||
// Calculate max depth
|
||||
structure.depth = this.calculateMaxDepth(elements);
|
||||
|
||||
return structure;
|
||||
}
|
||||
|
||||
analyzeElementStructure(element, structure, visited = new Set()) {
|
||||
if (visited.has(element)) return;
|
||||
visited.add(element);
|
||||
|
||||
const tagName = element.tagName;
|
||||
|
||||
// Update element type count
|
||||
structure.elementTypes.set(
|
||||
tagName,
|
||||
(structure.elementTypes.get(tagName) || 0) + 1
|
||||
);
|
||||
|
||||
// Check for specific structures
|
||||
if (/^H[1-6]$/.test(tagName)) {
|
||||
structure.hasHeadings = true;
|
||||
} else if (['UL', 'OL', 'DL'].includes(tagName)) {
|
||||
structure.hasLists = true;
|
||||
} else if (tagName === 'TABLE') {
|
||||
structure.hasTables = true;
|
||||
} else if (['IMG', 'VIDEO', 'IFRAME', 'PICTURE'].includes(tagName)) {
|
||||
structure.hasMedia = true;
|
||||
} else if (['CODE', 'PRE'].includes(tagName)) {
|
||||
structure.hasCode = true;
|
||||
} else if (tagName === 'A') {
|
||||
structure.hasLinks = true;
|
||||
}
|
||||
|
||||
// Analyze children
|
||||
for (const child of element.children) {
|
||||
this.analyzeElementStructure(child, structure, visited);
|
||||
}
|
||||
}
|
||||
|
||||
identifyContentType(elements) {
|
||||
const scores = {
|
||||
article: 0,
|
||||
list: 0,
|
||||
table: 0,
|
||||
form: 0,
|
||||
media: 0,
|
||||
mixed: 0
|
||||
};
|
||||
|
||||
for (const element of elements) {
|
||||
// Score based on element types and classes
|
||||
const tagName = element.tagName;
|
||||
const className = element.className.toLowerCase();
|
||||
const id = element.id.toLowerCase();
|
||||
|
||||
// Check for article patterns
|
||||
if (tagName === 'ARTICLE' ||
|
||||
this.matchesPattern(className + ' ' + id, this.patterns.article)) {
|
||||
scores.article += 10;
|
||||
}
|
||||
|
||||
// Check for list patterns
|
||||
if (['UL', 'OL'].includes(tagName) ||
|
||||
this.matchesPattern(className, this.patterns.list)) {
|
||||
scores.list += 5;
|
||||
}
|
||||
|
||||
// Check for table
|
||||
if (tagName === 'TABLE') {
|
||||
scores.table += 10;
|
||||
}
|
||||
|
||||
// Check for form
|
||||
if (tagName === 'FORM' || element.querySelector('input, select, textarea')) {
|
||||
scores.form += 5;
|
||||
}
|
||||
|
||||
// Check for media gallery
|
||||
if (this.matchesPattern(className, this.patterns.media) ||
|
||||
element.querySelectorAll('img, video').length > 3) {
|
||||
scores.media += 5;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine primary content type
|
||||
const maxScore = Math.max(...Object.values(scores));
|
||||
if (maxScore === 0) return 'unknown';
|
||||
|
||||
for (const [type, score] of Object.entries(scores)) {
|
||||
if (score === maxScore) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return 'mixed';
|
||||
}
|
||||
|
||||
buildHierarchy(elements) {
|
||||
const hierarchy = {
|
||||
root: null,
|
||||
levels: [],
|
||||
headingStructure: []
|
||||
};
|
||||
|
||||
// Find common ancestor
|
||||
if (elements.length > 0) {
|
||||
hierarchy.root = this.findCommonAncestor(elements);
|
||||
}
|
||||
|
||||
// Build heading hierarchy
|
||||
const headings = [];
|
||||
for (const element of elements) {
|
||||
const foundHeadings = element.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
headings.push(...Array.from(foundHeadings));
|
||||
}
|
||||
|
||||
// Sort headings by document position
|
||||
headings.sort((a, b) => {
|
||||
const position = a.compareDocumentPosition(b);
|
||||
if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
|
||||
return -1;
|
||||
} else if (position & Node.DOCUMENT_POSITION_PRECEDING) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Build heading structure
|
||||
let currentLevel = 0;
|
||||
const stack = [];
|
||||
|
||||
for (const heading of headings) {
|
||||
const level = parseInt(heading.tagName.substring(1));
|
||||
const item = {
|
||||
level,
|
||||
text: heading.textContent.trim(),
|
||||
element: heading,
|
||||
children: []
|
||||
};
|
||||
|
||||
// Find parent in stack
|
||||
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
if (stack.length > 0) {
|
||||
stack[stack.length - 1].children.push(item);
|
||||
} else {
|
||||
hierarchy.headingStructure.push(item);
|
||||
}
|
||||
|
||||
stack.push(item);
|
||||
}
|
||||
|
||||
return hierarchy;
|
||||
}
|
||||
|
||||
collectMediaAssets(elements) {
|
||||
const media = {
|
||||
images: [],
|
||||
videos: [],
|
||||
iframes: [],
|
||||
audio: []
|
||||
};
|
||||
|
||||
for (const element of elements) {
|
||||
// Collect images
|
||||
const images = element.querySelectorAll('img');
|
||||
for (const img of images) {
|
||||
media.images.push({
|
||||
src: img.src,
|
||||
alt: img.alt,
|
||||
title: img.title,
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
element: img
|
||||
});
|
||||
}
|
||||
|
||||
// Collect videos
|
||||
const videos = element.querySelectorAll('video');
|
||||
for (const video of videos) {
|
||||
media.videos.push({
|
||||
src: video.src,
|
||||
poster: video.poster,
|
||||
width: video.width,
|
||||
height: video.height,
|
||||
element: video
|
||||
});
|
||||
}
|
||||
|
||||
// Collect iframes
|
||||
const iframes = element.querySelectorAll('iframe');
|
||||
for (const iframe of iframes) {
|
||||
media.iframes.push({
|
||||
src: iframe.src,
|
||||
width: iframe.width,
|
||||
height: iframe.height,
|
||||
title: iframe.title,
|
||||
element: iframe
|
||||
});
|
||||
}
|
||||
|
||||
// Collect audio
|
||||
const audios = element.querySelectorAll('audio');
|
||||
for (const audio of audios) {
|
||||
media.audio.push({
|
||||
src: audio.src,
|
||||
element: audio
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return media;
|
||||
}
|
||||
|
||||
calculateTextDensity(elements) {
|
||||
let totalText = 0;
|
||||
let totalElements = 0;
|
||||
let linkText = 0;
|
||||
let codeText = 0;
|
||||
|
||||
for (const element of elements) {
|
||||
const stats = this.getTextStats(element);
|
||||
totalText += stats.textLength;
|
||||
totalElements += stats.elementCount;
|
||||
linkText += stats.linkTextLength;
|
||||
codeText += stats.codeTextLength;
|
||||
}
|
||||
|
||||
return {
|
||||
textLength: totalText,
|
||||
elementCount: totalElements,
|
||||
averageTextPerElement: totalElements > 0 ? totalText / totalElements : 0,
|
||||
linkDensity: totalText > 0 ? linkText / totalText : 0,
|
||||
codeDensity: totalText > 0 ? codeText / totalText : 0
|
||||
};
|
||||
}
|
||||
|
||||
getTextStats(element, visited = new Set()) {
|
||||
if (visited.has(element)) {
|
||||
return { textLength: 0, elementCount: 0, linkTextLength: 0, codeTextLength: 0 };
|
||||
}
|
||||
visited.add(element);
|
||||
|
||||
let stats = {
|
||||
textLength: 0,
|
||||
elementCount: 1,
|
||||
linkTextLength: 0,
|
||||
codeTextLength: 0
|
||||
};
|
||||
|
||||
// Get direct text content
|
||||
for (const node of element.childNodes) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = node.textContent.trim();
|
||||
stats.textLength += text.length;
|
||||
|
||||
// Check if this text is within a link
|
||||
if (element.tagName === 'A') {
|
||||
stats.linkTextLength += text.length;
|
||||
}
|
||||
|
||||
// Check if this text is within code
|
||||
if (['CODE', 'PRE'].includes(element.tagName)) {
|
||||
stats.codeTextLength += text.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process children
|
||||
for (const child of element.children) {
|
||||
const childStats = this.getTextStats(child, visited);
|
||||
stats.textLength += childStats.textLength;
|
||||
stats.elementCount += childStats.elementCount;
|
||||
stats.linkTextLength += childStats.linkTextLength;
|
||||
stats.codeTextLength += childStats.codeTextLength;
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
identifySemanticRegions(elements) {
|
||||
const regions = {
|
||||
headers: [],
|
||||
navigation: [],
|
||||
main: [],
|
||||
sidebars: [],
|
||||
footers: [],
|
||||
articles: []
|
||||
};
|
||||
|
||||
for (const element of elements) {
|
||||
// Check element and its ancestors for semantic regions
|
||||
let current = element;
|
||||
while (current) {
|
||||
const tagName = current.tagName;
|
||||
const className = current.className.toLowerCase();
|
||||
const role = current.getAttribute('role');
|
||||
|
||||
// Check semantic HTML5 elements
|
||||
if (tagName === 'HEADER' || role === 'banner') {
|
||||
regions.headers.push(current);
|
||||
} else if (tagName === 'NAV' || role === 'navigation') {
|
||||
regions.navigation.push(current);
|
||||
} else if (tagName === 'MAIN' || role === 'main') {
|
||||
regions.main.push(current);
|
||||
} else if (tagName === 'ASIDE' || role === 'complementary') {
|
||||
regions.sidebars.push(current);
|
||||
} else if (tagName === 'FOOTER' || role === 'contentinfo') {
|
||||
regions.footers.push(current);
|
||||
} else if (tagName === 'ARTICLE' || role === 'article') {
|
||||
regions.articles.push(current);
|
||||
}
|
||||
|
||||
// Check class patterns
|
||||
if (this.matchesPattern(className, this.patterns.header)) {
|
||||
regions.headers.push(current);
|
||||
} else if (this.matchesPattern(className, this.patterns.navigation)) {
|
||||
regions.navigation.push(current);
|
||||
} else if (this.matchesPattern(className, this.patterns.sidebar)) {
|
||||
regions.sidebars.push(current);
|
||||
} else if (this.matchesPattern(className, this.patterns.footer)) {
|
||||
regions.footers.push(current);
|
||||
}
|
||||
|
||||
current = current.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate
|
||||
for (const key of Object.keys(regions)) {
|
||||
regions[key] = Array.from(new Set(regions[key]));
|
||||
}
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
analyzeRelationships(elements) {
|
||||
const relationships = {
|
||||
siblings: [],
|
||||
parents: [],
|
||||
children: [],
|
||||
relatedByClass: new Map(),
|
||||
relatedByStructure: []
|
||||
};
|
||||
|
||||
// Find sibling relationships
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
for (let j = i + 1; j < elements.length; j++) {
|
||||
if (elements[i].parentElement === elements[j].parentElement) {
|
||||
relationships.siblings.push([elements[i], elements[j]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find parent-child relationships
|
||||
for (const element of elements) {
|
||||
for (const other of elements) {
|
||||
if (element !== other) {
|
||||
if (element.contains(other)) {
|
||||
relationships.parents.push({ parent: element, child: other });
|
||||
} else if (other.contains(element)) {
|
||||
relationships.children.push({ parent: other, child: element });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group by similar classes
|
||||
for (const element of elements) {
|
||||
const classes = Array.from(element.classList);
|
||||
for (const className of classes) {
|
||||
if (!relationships.relatedByClass.has(className)) {
|
||||
relationships.relatedByClass.set(className, []);
|
||||
}
|
||||
relationships.relatedByClass.get(className).push(element);
|
||||
}
|
||||
}
|
||||
|
||||
// Find structurally similar elements
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
for (let j = i + 1; j < elements.length; j++) {
|
||||
if (this.areStructurallySimilar(elements[i], elements[j])) {
|
||||
relationships.relatedByStructure.push([elements[i], elements[j]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return relationships;
|
||||
}
|
||||
|
||||
areStructurallySimilar(element1, element2) {
|
||||
// Same tag name
|
||||
if (element1.tagName !== element2.tagName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Similar class structure
|
||||
const classes1 = Array.from(element1.classList).sort();
|
||||
const classes2 = Array.from(element2.classList).sort();
|
||||
|
||||
// At least 50% overlap in classes
|
||||
const intersection = classes1.filter(c => classes2.includes(c));
|
||||
const union = Array.from(new Set([...classes1, ...classes2]));
|
||||
|
||||
if (union.length > 0 && intersection.length / union.length >= 0.5) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Similar child structure
|
||||
if (element1.children.length === element2.children.length) {
|
||||
const childTags1 = Array.from(element1.children).map(c => c.tagName).sort();
|
||||
const childTags2 = Array.from(element2.children).map(c => c.tagName).sort();
|
||||
|
||||
if (JSON.stringify(childTags1) === JSON.stringify(childTags2)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
extractMetadata(elements) {
|
||||
const metadata = {
|
||||
title: null,
|
||||
description: null,
|
||||
author: null,
|
||||
date: null,
|
||||
tags: [],
|
||||
microdata: []
|
||||
};
|
||||
|
||||
for (const element of elements) {
|
||||
// Look for title
|
||||
const h1 = element.querySelector('h1');
|
||||
if (h1 && !metadata.title) {
|
||||
metadata.title = h1.textContent.trim();
|
||||
}
|
||||
|
||||
// Look for meta information
|
||||
const metaElements = element.querySelectorAll('[itemprop], [property], [name]');
|
||||
for (const meta of metaElements) {
|
||||
const prop = meta.getAttribute('itemprop') ||
|
||||
meta.getAttribute('property') ||
|
||||
meta.getAttribute('name');
|
||||
const content = meta.getAttribute('content') || meta.textContent.trim();
|
||||
|
||||
if (prop && content) {
|
||||
if (prop.includes('author')) {
|
||||
metadata.author = content;
|
||||
} else if (prop.includes('date') || prop.includes('time')) {
|
||||
metadata.date = content;
|
||||
} else if (prop.includes('description')) {
|
||||
metadata.description = content;
|
||||
} else if (prop.includes('tag') || prop.includes('keyword')) {
|
||||
metadata.tags.push(content);
|
||||
}
|
||||
|
||||
metadata.microdata.push({ property: prop, value: content });
|
||||
}
|
||||
}
|
||||
|
||||
// Look for time elements
|
||||
const timeElements = element.querySelectorAll('time');
|
||||
for (const time of timeElements) {
|
||||
if (!metadata.date && time.dateTime) {
|
||||
metadata.date = time.dateTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
determineLayout(elements) {
|
||||
// Check if elements form a grid
|
||||
const positions = elements.map(el => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return { x: rect.left, y: rect.top, width: rect.width, height: rect.height };
|
||||
});
|
||||
|
||||
// Check for grid layout (multiple elements on same row)
|
||||
const rows = new Map();
|
||||
for (const pos of positions) {
|
||||
const row = Math.round(pos.y / 10) * 10; // Round to nearest 10px
|
||||
if (!rows.has(row)) {
|
||||
rows.set(row, []);
|
||||
}
|
||||
rows.get(row).push(pos);
|
||||
}
|
||||
|
||||
// If multiple elements share rows, it's likely a grid
|
||||
const hasGrid = Array.from(rows.values()).some(row => row.length > 1);
|
||||
|
||||
if (hasGrid) {
|
||||
return 'grid';
|
||||
}
|
||||
|
||||
// Check for mixed layout (significant variation in widths)
|
||||
const widths = positions.map(p => p.width);
|
||||
const avgWidth = widths.reduce((a, b) => a + b, 0) / widths.length;
|
||||
const variance = widths.reduce((sum, w) => sum + Math.pow(w - avgWidth, 2), 0) / widths.length;
|
||||
const stdDev = Math.sqrt(variance);
|
||||
|
||||
if (stdDev / avgWidth > 0.3) {
|
||||
return 'mixed';
|
||||
}
|
||||
|
||||
return 'linear';
|
||||
}
|
||||
|
||||
calculateMaxDepth(elements) {
|
||||
let maxDepth = 0;
|
||||
|
||||
for (const element of elements) {
|
||||
const depth = this.getElementDepth(element);
|
||||
maxDepth = Math.max(maxDepth, depth);
|
||||
}
|
||||
|
||||
return maxDepth;
|
||||
}
|
||||
|
||||
getElementDepth(element, depth = 0) {
|
||||
if (element.children.length === 0) {
|
||||
return depth;
|
||||
}
|
||||
|
||||
let maxChildDepth = depth;
|
||||
for (const child of element.children) {
|
||||
const childDepth = this.getElementDepth(child, depth + 1);
|
||||
maxChildDepth = Math.max(maxChildDepth, childDepth);
|
||||
}
|
||||
|
||||
return maxChildDepth;
|
||||
}
|
||||
|
||||
findCommonAncestor(elements) {
|
||||
if (elements.length === 0) return null;
|
||||
if (elements.length === 1) return elements[0].parentElement;
|
||||
|
||||
// Start with the first element's ancestors
|
||||
let ancestor = elements[0];
|
||||
const ancestors = [];
|
||||
|
||||
while (ancestor) {
|
||||
ancestors.push(ancestor);
|
||||
ancestor = ancestor.parentElement;
|
||||
}
|
||||
|
||||
// Find the deepest common ancestor
|
||||
for (const ancestorCandidate of ancestors) {
|
||||
let isCommon = true;
|
||||
|
||||
for (const element of elements) {
|
||||
if (!ancestorCandidate.contains(element)) {
|
||||
isCommon = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isCommon) {
|
||||
return ancestorCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
return document.body;
|
||||
}
|
||||
|
||||
matchesPattern(text, patterns) {
|
||||
return patterns.some(pattern => text.includes(pattern));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,718 @@
|
||||
class MarkdownConverter {
|
||||
constructor() {
|
||||
// Conversion handlers for different element types
|
||||
this.converters = {
|
||||
'H1': async (el, ctx) => await this.convertHeading(el, 1, ctx),
|
||||
'H2': async (el, ctx) => await this.convertHeading(el, 2, ctx),
|
||||
'H3': async (el, ctx) => await this.convertHeading(el, 3, ctx),
|
||||
'H4': async (el, ctx) => await this.convertHeading(el, 4, ctx),
|
||||
'H5': async (el, ctx) => await this.convertHeading(el, 5, ctx),
|
||||
'H6': async (el, ctx) => await this.convertHeading(el, 6, ctx),
|
||||
'P': async (el, ctx) => await this.convertParagraph(el, ctx),
|
||||
'A': async (el, ctx) => await this.convertLink(el, ctx),
|
||||
'IMG': async (el, ctx) => await this.convertImage(el, ctx),
|
||||
'UL': async (el, ctx) => await this.convertList(el, 'ul', ctx),
|
||||
'OL': async (el, ctx) => await this.convertList(el, 'ol', ctx),
|
||||
'LI': async (el, ctx) => await this.convertListItem(el, ctx),
|
||||
'TABLE': async (el, ctx) => await this.convertTable(el, ctx),
|
||||
'BLOCKQUOTE': async (el, ctx) => await this.convertBlockquote(el, ctx),
|
||||
'PRE': async (el, ctx) => await this.convertPreformatted(el, ctx),
|
||||
'CODE': async (el, ctx) => await this.convertCode(el, ctx),
|
||||
'HR': async (el, ctx) => '\n---\n',
|
||||
'BR': async (el, ctx) => ' \n',
|
||||
'STRONG': async (el, ctx) => `**${await this.getTextContent(el, ctx)}**`,
|
||||
'B': async (el, ctx) => `**${await this.getTextContent(el, ctx)}**`,
|
||||
'EM': async (el, ctx) => `*${await this.getTextContent(el, ctx)}*`,
|
||||
'I': async (el, ctx) => `*${await this.getTextContent(el, ctx)}*`,
|
||||
'DEL': async (el, ctx) => `~~${await this.getTextContent(el, ctx)}~~`,
|
||||
'S': async (el, ctx) => `~~${await this.getTextContent(el, ctx)}~~`,
|
||||
'DIV': async (el, ctx) => await this.convertDiv(el, ctx),
|
||||
'SPAN': async (el, ctx) => await this.convertSpan(el, ctx),
|
||||
'ARTICLE': async (el, ctx) => await this.convertArticle(el, ctx),
|
||||
'SECTION': async (el, ctx) => await this.convertSection(el, ctx),
|
||||
'FIGURE': async (el, ctx) => await this.convertFigure(el, ctx),
|
||||
'FIGCAPTION': async (el, ctx) => await this.convertFigCaption(el, ctx),
|
||||
'VIDEO': async (el, ctx) => await this.convertVideo(el, ctx),
|
||||
'IFRAME': async (el, ctx) => await this.convertIframe(el, ctx),
|
||||
'DL': async (el, ctx) => await this.convertDefinitionList(el, ctx),
|
||||
'DT': async (el, ctx) => await this.convertDefinitionTerm(el, ctx),
|
||||
'DD': async (el, ctx) => await this.convertDefinitionDescription(el, ctx),
|
||||
'TR': async (el, ctx) => await this.convertTableRow(el, ctx)
|
||||
};
|
||||
|
||||
// Maintain context during conversion
|
||||
this.conversionContext = {
|
||||
listDepth: 0,
|
||||
inTable: false,
|
||||
inCode: false,
|
||||
preserveWhitespace: false,
|
||||
references: [],
|
||||
imageCount: 0,
|
||||
linkCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
async convert(elements, options = {}) {
|
||||
// Reset context
|
||||
this.resetContext();
|
||||
|
||||
// Apply options
|
||||
this.options = {
|
||||
includeImages: true,
|
||||
preserveTables: true,
|
||||
keepCodeFormatting: true,
|
||||
simplifyLayout: false,
|
||||
preserveLinks: true,
|
||||
...options
|
||||
};
|
||||
|
||||
// Convert elements
|
||||
const markdownParts = [];
|
||||
|
||||
for (const element of elements) {
|
||||
const markdown = await this.convertElement(element, this.conversionContext);
|
||||
if (markdown.trim()) {
|
||||
markdownParts.push(markdown);
|
||||
}
|
||||
}
|
||||
|
||||
// Join parts with appropriate spacing
|
||||
let result = markdownParts.join('\n\n');
|
||||
|
||||
// Add references if using reference-style links
|
||||
if (this.conversionContext.references.length > 0) {
|
||||
result += '\n\n' + this.generateReferences();
|
||||
}
|
||||
|
||||
// Post-process to clean up
|
||||
result = this.postProcess(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
resetContext() {
|
||||
this.conversionContext = {
|
||||
listDepth: 0,
|
||||
inTable: false,
|
||||
inCode: false,
|
||||
preserveWhitespace: false,
|
||||
references: [],
|
||||
imageCount: 0,
|
||||
linkCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
async convertElement(element, context) {
|
||||
// Skip hidden elements
|
||||
if (this.isHidden(element)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Skip script and style elements
|
||||
if (['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(element.tagName)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Get converter for this element type
|
||||
const converter = this.converters[element.tagName];
|
||||
|
||||
if (converter) {
|
||||
return await converter(element, context);
|
||||
} else {
|
||||
// For unknown elements, process children
|
||||
return await this.processChildren(element, context);
|
||||
}
|
||||
}
|
||||
|
||||
async processChildren(element, context) {
|
||||
const parts = [];
|
||||
|
||||
for (const child of element.childNodes) {
|
||||
if (child.nodeType === Node.TEXT_NODE) {
|
||||
const text = this.processTextNode(child, context);
|
||||
if (text) {
|
||||
parts.push(text);
|
||||
}
|
||||
} else if (child.nodeType === Node.ELEMENT_NODE) {
|
||||
const markdown = await this.convertElement(child, context);
|
||||
if (markdown) {
|
||||
parts.push(markdown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
processTextNode(node, context) {
|
||||
let text = node.textContent;
|
||||
|
||||
// Preserve whitespace in code blocks
|
||||
if (!context.preserveWhitespace && !context.inCode) {
|
||||
// Normalize whitespace
|
||||
text = text.replace(/\s+/g, ' ');
|
||||
|
||||
// Trim if at block boundaries
|
||||
if (this.isBlockBoundary(node.previousSibling)) {
|
||||
text = text.trimStart();
|
||||
}
|
||||
if (this.isBlockBoundary(node.nextSibling)) {
|
||||
text = text.trimEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// Escape markdown characters
|
||||
if (!context.inCode) {
|
||||
text = this.escapeMarkdown(text);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
isBlockBoundary(node) {
|
||||
if (!node || node.nodeType !== Node.ELEMENT_NODE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const blockElements = [
|
||||
'DIV', 'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
|
||||
'UL', 'OL', 'LI', 'BLOCKQUOTE', 'PRE', 'TABLE',
|
||||
'HR', 'ARTICLE', 'SECTION', 'HEADER', 'FOOTER',
|
||||
'NAV', 'ASIDE', 'MAIN'
|
||||
];
|
||||
|
||||
return blockElements.includes(node.tagName);
|
||||
}
|
||||
|
||||
escapeMarkdown(text) {
|
||||
// In text-only mode, don't escape characters
|
||||
if (this.options.textOnly) {
|
||||
return text;
|
||||
}
|
||||
|
||||
// Escape special markdown characters
|
||||
return text
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/\*/g, '\\*')
|
||||
.replace(/_/g, '\\_')
|
||||
.replace(/\[/g, '\\[')
|
||||
.replace(/\]/g, '\\]')
|
||||
.replace(/\(/g, '\\(')
|
||||
.replace(/\)/g, '\\)')
|
||||
.replace(/\#/g, '\\#')
|
||||
.replace(/\+/g, '\\+')
|
||||
.replace(/\-/g, '\\-')
|
||||
.replace(/\./g, '\\.')
|
||||
.replace(/\!/g, '\\!')
|
||||
.replace(/\|/g, '\\|');
|
||||
}
|
||||
|
||||
async convertHeading(element, level, context) {
|
||||
const text = await this.getTextContent(element, context);
|
||||
return '#'.repeat(level) + ' ' + text + '\n';
|
||||
}
|
||||
|
||||
async convertParagraph(element, context) {
|
||||
const content = await this.processChildren(element, context);
|
||||
return content.trim() ? content + '\n' : '';
|
||||
}
|
||||
|
||||
async convertLink(element, context) {
|
||||
if (!this.options.preserveLinks || this.options.textOnly) {
|
||||
return await this.getTextContent(element, context);
|
||||
}
|
||||
|
||||
const text = await this.getTextContent(element, context);
|
||||
const href = element.getAttribute('href');
|
||||
const title = element.getAttribute('title');
|
||||
|
||||
if (!href) {
|
||||
return text;
|
||||
}
|
||||
|
||||
// Convert relative URLs to absolute
|
||||
const absoluteUrl = this.makeAbsoluteUrl(href);
|
||||
|
||||
// Use reference-style links for cleaner markdown
|
||||
if (text && absoluteUrl) {
|
||||
if (title) {
|
||||
return `[${text}](${absoluteUrl} "${title}")`;
|
||||
} else {
|
||||
return `[${text}](${absoluteUrl})`;
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
async convertImage(element, context) {
|
||||
if (!this.options.includeImages || this.options.textOnly) {
|
||||
// In text-only mode, return alt text if available
|
||||
if (this.options.textOnly) {
|
||||
const alt = element.getAttribute('alt');
|
||||
return alt ? `[Image: ${alt}]` : '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const src = element.getAttribute('src');
|
||||
const alt = element.getAttribute('alt') || '';
|
||||
const title = element.getAttribute('title');
|
||||
|
||||
if (!src) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Convert relative URLs to absolute
|
||||
const absoluteUrl = this.makeAbsoluteUrl(src);
|
||||
|
||||
if (title) {
|
||||
return ``;
|
||||
} else {
|
||||
return ``;
|
||||
}
|
||||
}
|
||||
|
||||
async convertList(element, type, context) {
|
||||
const oldDepth = context.listDepth;
|
||||
context.listDepth++;
|
||||
|
||||
const items = [];
|
||||
for (const child of element.children) {
|
||||
if (child.tagName === 'LI') {
|
||||
const markdown = await this.convertListItem(child, { ...context, listType: type });
|
||||
if (markdown) {
|
||||
items.push(markdown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.listDepth = oldDepth;
|
||||
|
||||
return items.join('\n') + (context.listDepth === 0 ? '\n' : '');
|
||||
}
|
||||
|
||||
async convertListItem(element, context) {
|
||||
const indent = ' '.repeat(Math.max(0, context.listDepth - 1));
|
||||
const bullet = context.listType === 'ol' ? '1.' : '-';
|
||||
const content = (await this.processChildren(element, context)).trim();
|
||||
|
||||
return `${indent}${bullet} ${content}`;
|
||||
}
|
||||
|
||||
async convertTable(element, context) {
|
||||
if (!this.options.preserveTables || this.options.textOnly) {
|
||||
// Fallback to simple text representation
|
||||
return await this.convertTableToText(element, context);
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
const headerRows = [];
|
||||
let maxCols = 0;
|
||||
|
||||
// Process table rows
|
||||
for (const child of element.children) {
|
||||
if (child.tagName === 'THEAD') {
|
||||
for (const row of child.children) {
|
||||
if (row.tagName === 'TR') {
|
||||
const cells = await this.processTableRow(row, context);
|
||||
headerRows.push(cells);
|
||||
maxCols = Math.max(maxCols, cells.length);
|
||||
}
|
||||
}
|
||||
} else if (child.tagName === 'TBODY') {
|
||||
for (const row of child.children) {
|
||||
if (row.tagName === 'TR') {
|
||||
const cells = await this.processTableRow(row, context);
|
||||
rows.push(cells);
|
||||
maxCols = Math.max(maxCols, cells.length);
|
||||
}
|
||||
}
|
||||
} else if (child.tagName === 'TR') {
|
||||
const cells = await this.processTableRow(child, context);
|
||||
rows.push(cells);
|
||||
maxCols = Math.max(maxCols, cells.length);
|
||||
}
|
||||
}
|
||||
|
||||
// Build markdown table
|
||||
const markdownRows = [];
|
||||
|
||||
// Add headers
|
||||
if (headerRows.length > 0) {
|
||||
for (const headerRow of headerRows) {
|
||||
const paddedRow = this.padTableRow(headerRow, maxCols);
|
||||
markdownRows.push('| ' + paddedRow.join(' | ') + ' |');
|
||||
}
|
||||
|
||||
// Add separator
|
||||
const separator = Array(maxCols).fill('---');
|
||||
markdownRows.push('| ' + separator.join(' | ') + ' |');
|
||||
}
|
||||
|
||||
// Add body rows
|
||||
for (const row of rows) {
|
||||
const paddedRow = this.padTableRow(row, maxCols);
|
||||
markdownRows.push('| ' + paddedRow.join(' | ') + ' |');
|
||||
}
|
||||
|
||||
return markdownRows.join('\n') + '\n';
|
||||
}
|
||||
|
||||
async processTableRow(row, context) {
|
||||
const cells = [];
|
||||
|
||||
for (const cell of row.children) {
|
||||
if (cell.tagName === 'TD' || cell.tagName === 'TH') {
|
||||
const content = (await this.getTextContent(cell, context)).trim();
|
||||
cells.push(content);
|
||||
}
|
||||
}
|
||||
|
||||
return cells;
|
||||
}
|
||||
|
||||
async convertTableRow(element, context) {
|
||||
// Convert a single table row to markdown
|
||||
if (this.options.textOnly) {
|
||||
const cells = await this.processTableRow(element, context);
|
||||
return cells.join(' ');
|
||||
}
|
||||
|
||||
// For non-text-only mode, create a simple table representation
|
||||
const cells = await this.processTableRow(element, context);
|
||||
return '| ' + cells.join(' | ') + ' |';
|
||||
}
|
||||
|
||||
padTableRow(row, targetLength) {
|
||||
const padded = [...row];
|
||||
while (padded.length < targetLength) {
|
||||
padded.push('');
|
||||
}
|
||||
return padded;
|
||||
}
|
||||
|
||||
async convertTableToText(element, context) {
|
||||
// Convert table to clean text representation
|
||||
const lines = [];
|
||||
const rows = element.querySelectorAll('tr');
|
||||
|
||||
for (const row of rows) {
|
||||
const cells = row.querySelectorAll('td, th');
|
||||
const cellTexts = [];
|
||||
|
||||
for (const cell of cells) {
|
||||
const text = (await this.getTextContent(cell, context)).trim();
|
||||
if (text) {
|
||||
cellTexts.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
if (cellTexts.length > 0) {
|
||||
// Join cells with space, handling common patterns
|
||||
lines.push(cellTexts.join(' '));
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
async convertBlockquote(element, context) {
|
||||
const lines = (await this.processChildren(element, context)).trim().split('\n');
|
||||
return lines.map(line => '> ' + line).join('\n') + '\n';
|
||||
}
|
||||
|
||||
async convertPreformatted(element, context) {
|
||||
const oldInCode = context.inCode;
|
||||
const oldPreserveWhitespace = context.preserveWhitespace;
|
||||
|
||||
context.inCode = true;
|
||||
context.preserveWhitespace = true;
|
||||
|
||||
let content = '';
|
||||
let language = '';
|
||||
|
||||
// Check if this is a code block with language
|
||||
const codeElement = element.querySelector('code');
|
||||
if (codeElement) {
|
||||
// Try to detect language from class
|
||||
const className = codeElement.className;
|
||||
const langMatch = className.match(/language-(\w+)/);
|
||||
if (langMatch) {
|
||||
language = langMatch[1];
|
||||
}
|
||||
|
||||
content = codeElement.textContent;
|
||||
} else {
|
||||
content = element.textContent;
|
||||
}
|
||||
|
||||
context.inCode = oldInCode;
|
||||
context.preserveWhitespace = oldPreserveWhitespace;
|
||||
|
||||
// Use fenced code blocks
|
||||
return '```' + language + '\n' + content + '\n```\n';
|
||||
}
|
||||
|
||||
async convertCode(element, context) {
|
||||
if (element.parentElement && element.parentElement.tagName === 'PRE') {
|
||||
// Already handled by convertPreformatted
|
||||
return element.textContent;
|
||||
}
|
||||
|
||||
const content = element.textContent;
|
||||
return '`' + content + '`';
|
||||
}
|
||||
|
||||
async convertDiv(element, context) {
|
||||
// Check for special div types
|
||||
if (element.className.includes('code-block') ||
|
||||
element.className.includes('highlight')) {
|
||||
return await this.convertPreformatted(element, context);
|
||||
}
|
||||
|
||||
const content = await this.processChildren(element, context);
|
||||
return content.trim() ? content + '\n' : '';
|
||||
}
|
||||
|
||||
async convertSpan(element, context) {
|
||||
// Check for special span types
|
||||
if (element.className.includes('code') ||
|
||||
element.className.includes('inline-code')) {
|
||||
return this.convertCode(element, context);
|
||||
}
|
||||
|
||||
return await this.processChildren(element, context);
|
||||
}
|
||||
|
||||
async convertArticle(element, context) {
|
||||
const content = await this.processChildren(element, context);
|
||||
return content.trim() ? content + '\n' : '';
|
||||
}
|
||||
|
||||
async convertSection(element, context) {
|
||||
const content = await this.processChildren(element, context);
|
||||
return content.trim() ? content + '\n' : '';
|
||||
}
|
||||
|
||||
async convertFigure(element, context) {
|
||||
const content = await this.processChildren(element, context);
|
||||
return content.trim() ? content + '\n' : '';
|
||||
}
|
||||
|
||||
async convertFigCaption(element, context) {
|
||||
const caption = await this.getTextContent(element, context);
|
||||
return caption ? '\n*' + caption + '*\n' : '';
|
||||
}
|
||||
|
||||
async convertVideo(element, context) {
|
||||
const title = element.getAttribute('title') || 'Video';
|
||||
|
||||
if (this.options.textOnly) {
|
||||
return `[Video: ${title}]`;
|
||||
}
|
||||
|
||||
const src = element.getAttribute('src');
|
||||
const poster = element.getAttribute('poster');
|
||||
|
||||
if (!src) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Convert to markdown with poster image if available
|
||||
if (poster) {
|
||||
const absolutePoster = this.makeAbsoluteUrl(poster);
|
||||
const absoluteSrc = this.makeAbsoluteUrl(src);
|
||||
return `[](${absoluteSrc})`;
|
||||
} else {
|
||||
const absoluteSrc = this.makeAbsoluteUrl(src);
|
||||
return `[${title}](${absoluteSrc})`;
|
||||
}
|
||||
}
|
||||
|
||||
async convertIframe(element, context) {
|
||||
const title = element.getAttribute('title') || 'Embedded content';
|
||||
|
||||
if (this.options.textOnly) {
|
||||
const src = element.getAttribute('src') || '';
|
||||
if (src.includes('youtube.com') || src.includes('youtu.be')) {
|
||||
return `[Video: ${title}]`;
|
||||
} else if (src.includes('vimeo.com')) {
|
||||
return `[Video: ${title}]`;
|
||||
} else {
|
||||
return `[Embedded: ${title}]`;
|
||||
}
|
||||
}
|
||||
|
||||
const src = element.getAttribute('src');
|
||||
if (!src) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Check for common embeds
|
||||
if (src.includes('youtube.com') || src.includes('youtu.be')) {
|
||||
return `[▶️ ${title}](${src})`;
|
||||
} else if (src.includes('vimeo.com')) {
|
||||
return `[▶️ ${title}](${src})`;
|
||||
} else {
|
||||
return `[${title}](${src})`;
|
||||
}
|
||||
}
|
||||
|
||||
async convertDefinitionList(element, context) {
|
||||
return await this.processChildren(element, context) + '\n';
|
||||
}
|
||||
|
||||
async convertDefinitionTerm(element, context) {
|
||||
const term = await this.getTextContent(element, context);
|
||||
return '**' + term + '**\n';
|
||||
}
|
||||
|
||||
async convertDefinitionDescription(element, context) {
|
||||
const description = await this.processChildren(element, context);
|
||||
return ': ' + description + '\n';
|
||||
}
|
||||
|
||||
async getTextContent(element, context) {
|
||||
// Special handling for elements that might contain other markdown
|
||||
if (context.inCode) {
|
||||
return element.textContent;
|
||||
}
|
||||
|
||||
return await this.processChildren(element, context);
|
||||
}
|
||||
|
||||
makeAbsoluteUrl(url) {
|
||||
if (!url) return '';
|
||||
|
||||
try {
|
||||
// Check if already absolute
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Handle protocol-relative URLs
|
||||
if (url.startsWith('//')) {
|
||||
return window.location.protocol + url;
|
||||
}
|
||||
|
||||
// Convert relative to absolute
|
||||
const base = window.location.origin;
|
||||
const path = window.location.pathname;
|
||||
|
||||
if (url.startsWith('/')) {
|
||||
return base + url;
|
||||
} else {
|
||||
// Relative to current path
|
||||
const pathDir = path.substring(0, path.lastIndexOf('/') + 1);
|
||||
return base + pathDir + url;
|
||||
}
|
||||
} catch (e) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
isHidden(element) {
|
||||
const style = window.getComputedStyle(element);
|
||||
return style.display === 'none' ||
|
||||
style.visibility === 'hidden' ||
|
||||
style.opacity === '0';
|
||||
}
|
||||
|
||||
generateReferences() {
|
||||
return this.conversionContext.references
|
||||
.map((ref, index) => `[${index + 1}]: ${ref.url}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
postProcess(markdown) {
|
||||
// Apply text-only specific processing
|
||||
if (this.options.textOnly) {
|
||||
markdown = this.postProcessTextOnly(markdown);
|
||||
}
|
||||
|
||||
// Clean up excessive newlines
|
||||
markdown = markdown.replace(/\n{3,}/g, '\n\n');
|
||||
|
||||
// Clean up spaces before punctuation
|
||||
markdown = markdown.replace(/ +([.,;:!?])/g, '$1');
|
||||
|
||||
// Ensure proper spacing around headers
|
||||
markdown = markdown.replace(/\n(#{1,6} )/g, '\n\n$1');
|
||||
markdown = markdown.replace(/(#{1,6} .+)\n(?![\n#])/g, '$1\n\n');
|
||||
|
||||
// Clean up list spacing
|
||||
markdown = markdown.replace(/\n\n(-|\d+\.) /g, '\n$1 ');
|
||||
|
||||
// Trim final result
|
||||
return markdown.trim();
|
||||
}
|
||||
|
||||
postProcessTextOnly(markdown) {
|
||||
// Smart pattern recognition for common formats
|
||||
const lines = markdown.split('\n');
|
||||
const processedLines = [];
|
||||
let inMetadata = false;
|
||||
let currentItem = null;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) {
|
||||
processedLines.push('');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Detect numbered list items (common in HN, Reddit, etc.)
|
||||
const numberPattern = /^(\d+)\.\s*(.+)$/;
|
||||
const numberMatch = line.match(numberPattern);
|
||||
|
||||
if (numberMatch) {
|
||||
// Start of a new numbered item
|
||||
inMetadata = false;
|
||||
currentItem = numberMatch[1];
|
||||
const content = numberMatch[2];
|
||||
|
||||
// Check if content has domain in parentheses
|
||||
const domainPattern = /^(.+?)\s*\(([^)]+)\)\s*(.*)$/;
|
||||
const domainMatch = content.match(domainPattern);
|
||||
|
||||
if (domainMatch) {
|
||||
const [, title, domain, rest] = domainMatch;
|
||||
processedLines.push(`${currentItem}. **${title.trim()}** (${domain})`);
|
||||
if (rest.trim()) {
|
||||
processedLines.push(` ${rest.trim()}`);
|
||||
inMetadata = true;
|
||||
}
|
||||
} else {
|
||||
processedLines.push(`${currentItem}. **${content}**`);
|
||||
}
|
||||
} else if (line.match(/\b(points?|by|ago|hide|comments?)\b/i) && currentItem) {
|
||||
// This looks like metadata for the current item
|
||||
const cleanedLine = line
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\s*\|\s*/g, ' | ')
|
||||
.trim();
|
||||
processedLines.push(` ${cleanedLine}`);
|
||||
inMetadata = true;
|
||||
} else if (inMetadata && line.length < 100) {
|
||||
// Continue metadata if we're in metadata mode and line is short
|
||||
processedLines.push(` ${line}`);
|
||||
} else {
|
||||
// Regular content
|
||||
inMetadata = false;
|
||||
processedLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the output
|
||||
let result = processedLines.join('\n');
|
||||
|
||||
// Remove excessive blank lines
|
||||
result = result.replace(/\n{3,}/g, '\n\n');
|
||||
|
||||
// Ensure proper spacing after numbered items
|
||||
result = result.replace(/^(\d+\..+)$\n^(?!\s)/gm, '$1\n\n');
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
class MarkdownExtraction {
|
||||
constructor() {
|
||||
this.selectedElements = new Set();
|
||||
this.highlightBoxes = new Map();
|
||||
this.selectionMode = false;
|
||||
this.toolbar = null;
|
||||
this.markdownPreviewModal = null;
|
||||
this.selectionCounter = 0;
|
||||
this.markdownConverter = null;
|
||||
this.contentAnalyzer = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Initialize dependencies
|
||||
this.markdownConverter = new MarkdownConverter();
|
||||
this.contentAnalyzer = new ContentAnalyzer();
|
||||
|
||||
this.createToolbar();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
createToolbar() {
|
||||
// Create floating toolbar
|
||||
this.toolbar = document.createElement('div');
|
||||
this.toolbar.className = 'c4ai-c2c-toolbar';
|
||||
this.toolbar.innerHTML = `
|
||||
<div class="c4ai-toolbar-header">
|
||||
<div class="c4ai-toolbar-dots">
|
||||
<span class="c4ai-dot c4ai-dot-red"></span>
|
||||
<span class="c4ai-dot c4ai-dot-yellow"></span>
|
||||
<span class="c4ai-dot c4ai-dot-green"></span>
|
||||
</div>
|
||||
<span class="c4ai-toolbar-title">Markdown Extraction</span>
|
||||
<button class="c4ai-close-btn" title="Close">×</button>
|
||||
</div>
|
||||
<div class="c4ai-toolbar-content">
|
||||
<div class="c4ai-selection-info">
|
||||
<span class="c4ai-selection-count">0 elements selected</span>
|
||||
<button class="c4ai-clear-btn" title="Clear selection" disabled>Clear</button>
|
||||
</div>
|
||||
<div class="c4ai-toolbar-actions">
|
||||
<button class="c4ai-preview-btn" disabled>Preview Markdown</button>
|
||||
<button class="c4ai-copy-btn" disabled>Copy to Clipboard</button>
|
||||
</div>
|
||||
<div class="c4ai-toolbar-instructions">
|
||||
<p>💡 <strong>Ctrl/Cmd + Click</strong> to select multiple elements</p>
|
||||
<p>📝 Selected elements will be converted to clean markdown</p>
|
||||
<p>⌨️ Press <strong>ESC</strong> to exit</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(this.toolbar);
|
||||
makeDraggableByHeader(this.toolbar);
|
||||
|
||||
// Position toolbar
|
||||
this.toolbar.style.position = 'fixed';
|
||||
this.toolbar.style.top = '20px';
|
||||
this.toolbar.style.right = '20px';
|
||||
this.toolbar.style.zIndex = '999999';
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Close button
|
||||
this.toolbar.querySelector('.c4ai-close-btn').addEventListener('click', () => {
|
||||
this.deactivate();
|
||||
});
|
||||
|
||||
// Clear selection button
|
||||
this.toolbar.querySelector('.c4ai-clear-btn').addEventListener('click', () => {
|
||||
this.clearSelection();
|
||||
});
|
||||
|
||||
// Preview button
|
||||
this.toolbar.querySelector('.c4ai-preview-btn').addEventListener('click', () => {
|
||||
this.showPreview();
|
||||
});
|
||||
|
||||
// Copy button
|
||||
this.toolbar.querySelector('.c4ai-copy-btn').addEventListener('click', () => {
|
||||
this.copyToClipboard();
|
||||
});
|
||||
|
||||
// Document click handler for element selection
|
||||
this.documentClickHandler = (event) => this.handleElementClick(event);
|
||||
document.addEventListener('click', this.documentClickHandler, true);
|
||||
|
||||
// Prevent default link behavior during selection mode
|
||||
this.linkClickHandler = (event) => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', this.linkClickHandler, true);
|
||||
|
||||
// Hover effect
|
||||
this.documentHoverHandler = (event) => this.handleElementHover(event);
|
||||
document.addEventListener('mouseover', this.documentHoverHandler, true);
|
||||
|
||||
// Remove hover on mouseout
|
||||
this.documentMouseOutHandler = (event) => this.handleElementMouseOut(event);
|
||||
document.addEventListener('mouseout', this.documentMouseOutHandler, true);
|
||||
|
||||
// Keyboard shortcuts
|
||||
this.keyboardHandler = (event) => this.handleKeyboard(event);
|
||||
document.addEventListener('keydown', this.keyboardHandler);
|
||||
}
|
||||
|
||||
handleElementClick(event) {
|
||||
// Check if Ctrl/Cmd is pressed
|
||||
if (!event.ctrlKey && !event.metaKey) return;
|
||||
|
||||
// Prevent default behavior
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const element = event.target;
|
||||
|
||||
// Don't select our own UI elements
|
||||
if (element.closest('.c4ai-c2c-toolbar') ||
|
||||
element.closest('.c4ai-c2c-preview') ||
|
||||
element.closest('.c4ai-highlight-box')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle element selection
|
||||
if (this.selectedElements.has(element)) {
|
||||
this.deselectElement(element);
|
||||
} else {
|
||||
this.selectElement(element);
|
||||
}
|
||||
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
handleElementHover(event) {
|
||||
const element = event.target;
|
||||
|
||||
// Don't hover our own UI elements
|
||||
if (element.closest('.c4ai-c2c-toolbar') ||
|
||||
element.closest('.c4ai-c2c-preview') ||
|
||||
element.closest('.c4ai-highlight-box') ||
|
||||
element.hasAttribute('data-c4ai-badge')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add hover class
|
||||
element.classList.add('c4ai-hover-candidate');
|
||||
}
|
||||
|
||||
handleElementMouseOut(event) {
|
||||
const element = event.target;
|
||||
element.classList.remove('c4ai-hover-candidate');
|
||||
}
|
||||
|
||||
handleKeyboard(event) {
|
||||
// ESC to deactivate
|
||||
if (event.key === 'Escape') {
|
||||
this.deactivate();
|
||||
}
|
||||
// Ctrl/Cmd + A to select all visible elements
|
||||
else if ((event.ctrlKey || event.metaKey) && event.key === 'a') {
|
||||
event.preventDefault();
|
||||
// Select all visible text-containing elements
|
||||
const elements = document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, div, span, article, section');
|
||||
elements.forEach(el => {
|
||||
if (el.textContent.trim() && this.isVisible(el) && !this.selectedElements.has(el)) {
|
||||
this.selectElement(el);
|
||||
}
|
||||
});
|
||||
this.updateUI();
|
||||
}
|
||||
}
|
||||
|
||||
isVisible(element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(element);
|
||||
return rect.width > 0 &&
|
||||
rect.height > 0 &&
|
||||
style.display !== 'none' &&
|
||||
style.visibility !== 'hidden' &&
|
||||
style.opacity !== '0';
|
||||
}
|
||||
|
||||
selectElement(element) {
|
||||
this.selectedElements.add(element);
|
||||
|
||||
// Create highlight box
|
||||
const box = this.createHighlightBox(element);
|
||||
this.highlightBoxes.set(element, box);
|
||||
|
||||
// Add selected class
|
||||
element.classList.add('c4ai-selected');
|
||||
|
||||
this.selectionCounter++;
|
||||
}
|
||||
|
||||
deselectElement(element) {
|
||||
this.selectedElements.delete(element);
|
||||
|
||||
// Remove highlight box (badge)
|
||||
const badge = this.highlightBoxes.get(element);
|
||||
if (badge) {
|
||||
// Remove scroll/resize listeners
|
||||
if (badge._updatePosition) {
|
||||
window.removeEventListener('scroll', badge._updatePosition, true);
|
||||
window.removeEventListener('resize', badge._updatePosition);
|
||||
}
|
||||
badge.remove();
|
||||
this.highlightBoxes.delete(element);
|
||||
}
|
||||
|
||||
// Remove outline
|
||||
element.style.outline = '';
|
||||
element.style.outlineOffset = '';
|
||||
|
||||
// Remove attributes
|
||||
element.removeAttribute('data-c4ai-selection-order');
|
||||
element.classList.remove('c4ai-selected');
|
||||
|
||||
this.selectionCounter--;
|
||||
}
|
||||
|
||||
createHighlightBox(element) {
|
||||
// Add a data attribute to track selection order
|
||||
element.setAttribute('data-c4ai-selection-order', this.selectionCounter + 1);
|
||||
|
||||
// Add selection outline directly to the element
|
||||
element.style.outline = '2px solid #0fbbaa';
|
||||
element.style.outlineOffset = '2px';
|
||||
|
||||
// Create badge with fixed positioning
|
||||
const badge = document.createElement('div');
|
||||
badge.className = 'c4ai-selection-badge-fixed';
|
||||
badge.textContent = this.selectionCounter + 1;
|
||||
badge.setAttribute('data-c4ai-badge', 'true');
|
||||
badge.title = 'Click to deselect';
|
||||
|
||||
// Get element position and set badge position
|
||||
const rect = element.getBoundingClientRect();
|
||||
badge.style.cssText = `
|
||||
position: fixed !important;
|
||||
top: ${rect.top - 12}px !important;
|
||||
left: ${rect.left - 12}px !important;
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
background: #0fbbaa !important;
|
||||
color: #070708 !important;
|
||||
border-radius: 50% !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: bold !important;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
|
||||
z-index: 999998 !important;
|
||||
cursor: pointer !important;
|
||||
transition: all 0.2s ease !important;
|
||||
pointer-events: auto !important;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
line-height: 1 !important;
|
||||
text-align: center !important;
|
||||
text-decoration: none !important;
|
||||
box-sizing: border-box !important;
|
||||
`;
|
||||
|
||||
// Add hover styles dynamically
|
||||
badge.addEventListener('mouseenter', () => {
|
||||
badge.style.setProperty('background', '#ff3c74', 'important');
|
||||
badge.style.setProperty('transform', 'scale(1.1)', 'important');
|
||||
});
|
||||
|
||||
badge.addEventListener('mouseleave', () => {
|
||||
badge.style.setProperty('background', '#0fbbaa', 'important');
|
||||
badge.style.setProperty('transform', 'scale(1)', 'important');
|
||||
});
|
||||
|
||||
// Add click handler to badge for deselection
|
||||
badge.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.deselectElement(element);
|
||||
this.updateUI();
|
||||
});
|
||||
|
||||
// Add scroll listener to update position
|
||||
const updatePosition = () => {
|
||||
const newRect = element.getBoundingClientRect();
|
||||
badge.style.top = `${newRect.top - 12}px`;
|
||||
badge.style.left = `${newRect.left - 12}px`;
|
||||
};
|
||||
|
||||
// Store the update function so we can remove it later
|
||||
badge._updatePosition = updatePosition;
|
||||
window.addEventListener('scroll', updatePosition, true);
|
||||
window.addEventListener('resize', updatePosition);
|
||||
|
||||
document.body.appendChild(badge);
|
||||
|
||||
return badge;
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
// Clear all selections
|
||||
this.selectedElements.forEach(element => {
|
||||
// Remove badge
|
||||
const badge = this.highlightBoxes.get(element);
|
||||
if (badge) {
|
||||
// Remove scroll/resize listeners
|
||||
if (badge._updatePosition) {
|
||||
window.removeEventListener('scroll', badge._updatePosition, true);
|
||||
window.removeEventListener('resize', badge._updatePosition);
|
||||
}
|
||||
badge.remove();
|
||||
}
|
||||
|
||||
// Remove outline
|
||||
element.style.outline = '';
|
||||
element.style.outlineOffset = '';
|
||||
|
||||
// Remove attributes
|
||||
element.removeAttribute('data-c4ai-selection-order');
|
||||
element.classList.remove('c4ai-selected');
|
||||
});
|
||||
|
||||
this.selectedElements.clear();
|
||||
this.highlightBoxes.clear();
|
||||
this.selectionCounter = 0;
|
||||
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
const count = this.selectedElements.size;
|
||||
|
||||
// Update selection count
|
||||
this.toolbar.querySelector('.c4ai-selection-count').textContent =
|
||||
`${count} element${count !== 1 ? 's' : ''} selected`;
|
||||
|
||||
// Enable/disable buttons
|
||||
const hasSelection = count > 0;
|
||||
this.toolbar.querySelector('.c4ai-preview-btn').disabled = !hasSelection;
|
||||
this.toolbar.querySelector('.c4ai-copy-btn').disabled = !hasSelection;
|
||||
this.toolbar.querySelector('.c4ai-clear-btn').disabled = !hasSelection;
|
||||
}
|
||||
|
||||
async showPreview() {
|
||||
// Initialize markdown preview modal if not already done
|
||||
if (!this.markdownPreviewModal) {
|
||||
this.markdownPreviewModal = new MarkdownPreviewModal();
|
||||
}
|
||||
|
||||
// Show modal with callback to generate markdown
|
||||
this.markdownPreviewModal.show(async (options) => {
|
||||
return await this.generateMarkdown(options);
|
||||
});
|
||||
}
|
||||
|
||||
/* createPreviewPanel() {
|
||||
this.previewPanel = document.createElement('div');
|
||||
this.previewPanel.className = 'c4ai-c2c-preview';
|
||||
this.previewPanel.innerHTML = `
|
||||
<div class="c4ai-preview-header">
|
||||
<div class="c4ai-toolbar-dots">
|
||||
<span class="c4ai-dot c4ai-dot-red"></span>
|
||||
<span class="c4ai-dot c4ai-dot-yellow"></span>
|
||||
<span class="c4ai-dot c4ai-dot-green"></span>
|
||||
</div>
|
||||
<span class="c4ai-preview-title">Markdown Preview</span>
|
||||
<button class="c4ai-preview-close">×</button>
|
||||
</div>
|
||||
<div class="c4ai-preview-options">
|
||||
<label><input type="checkbox" name="textOnly"> 👁️ Visual Text Mode (As You See) TRY THIS!!!</label>
|
||||
<label><input type="checkbox" name="includeImages" checked> Include Images</label>
|
||||
<label><input type="checkbox" name="preserveTables" checked> Preserve Tables</label>
|
||||
<label><input type="checkbox" name="preserveLinks" checked> Preserve Links</label>
|
||||
<label><input type="checkbox" name="keepCodeFormatting" checked> Keep Code Formatting</label>
|
||||
<label><input type="checkbox" name="simplifyLayout"> Simplify Layout</label>
|
||||
<label><input type="checkbox" name="addSeparators" checked> Add Separators</label>
|
||||
<label><input type="checkbox" name="includeXPath"> Include XPath Headers</label>
|
||||
</div>
|
||||
<div class="c4ai-preview-content">
|
||||
<div class="c4ai-preview-tabs">
|
||||
<button class="c4ai-tab active" data-tab="preview">Preview</button>
|
||||
<button class="c4ai-tab" data-tab="markdown">Markdown</button>
|
||||
<button class="c4ai-wrap-toggle" title="Toggle word wrap">↔️ Wrap</button>
|
||||
</div>
|
||||
<div class="c4ai-preview-pane active" data-pane="preview"></div>
|
||||
<div class="c4ai-preview-pane" data-pane="markdown"></div>
|
||||
</div>
|
||||
<div class="c4ai-preview-actions">
|
||||
<button class="c4ai-download-btn">Download .md</button>
|
||||
<button class="c4ai-copy-markdown-btn">Copy Markdown</button>
|
||||
<button class="c4ai-cloud-btn" disabled>Send to Cloud (Coming Soon)</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(this.previewPanel);
|
||||
makeDraggableByHeader(this.previewPanel);
|
||||
|
||||
// Position preview panel
|
||||
this.previewPanel.style.position = 'fixed';
|
||||
this.previewPanel.style.top = '50%';
|
||||
this.previewPanel.style.left = '50%';
|
||||
this.previewPanel.style.transform = 'translate(-50%, -50%)';
|
||||
this.previewPanel.style.zIndex = '999999';
|
||||
|
||||
this.setupPreviewEventListeners();
|
||||
} */
|
||||
|
||||
/* setupPreviewEventListeners() {
|
||||
// Close button
|
||||
this.previewPanel.querySelector('.c4ai-preview-close').addEventListener('click', () => {
|
||||
this.previewPanel.style.display = 'none';
|
||||
});
|
||||
|
||||
// Tab switching
|
||||
this.previewPanel.querySelectorAll('.c4ai-tab').forEach(tab => {
|
||||
tab.addEventListener('click', (e) => {
|
||||
const tabName = e.target.dataset.tab;
|
||||
this.switchPreviewTab(tabName);
|
||||
});
|
||||
});
|
||||
|
||||
// Wrap toggle
|
||||
const wrapToggle = this.previewPanel.querySelector('.c4ai-wrap-toggle');
|
||||
wrapToggle.addEventListener('click', () => {
|
||||
const panes = this.previewPanel.querySelectorAll('.c4ai-preview-pane');
|
||||
panes.forEach(pane => {
|
||||
pane.classList.toggle('wrap');
|
||||
});
|
||||
wrapToggle.classList.toggle('active');
|
||||
});
|
||||
|
||||
// Options change
|
||||
this.previewPanel.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', async (e) => {
|
||||
this.options[e.target.name] = e.target.checked;
|
||||
|
||||
// If text-only is enabled, automatically disable certain options
|
||||
if (e.target.name === 'textOnly' && e.target.checked) {
|
||||
// Update UI checkboxes
|
||||
const preserveLinksCheckbox = this.previewPanel.querySelector('input[name="preserveLinks"]');
|
||||
if (preserveLinksCheckbox) {
|
||||
preserveLinksCheckbox.checked = false;
|
||||
preserveLinksCheckbox.disabled = true;
|
||||
}
|
||||
|
||||
// Optionally disable images in text-only mode
|
||||
const includeImagesCheckbox = this.previewPanel.querySelector('input[name="includeImages"]');
|
||||
if (includeImagesCheckbox) {
|
||||
includeImagesCheckbox.disabled = true;
|
||||
}
|
||||
} else if (e.target.name === 'textOnly' && !e.target.checked) {
|
||||
// Re-enable options when text-only is disabled
|
||||
const preserveLinksCheckbox = this.previewPanel.querySelector('input[name="preserveLinks"]');
|
||||
if (preserveLinksCheckbox) {
|
||||
preserveLinksCheckbox.disabled = false;
|
||||
}
|
||||
|
||||
const includeImagesCheckbox = this.previewPanel.querySelector('input[name="includeImages"]');
|
||||
if (includeImagesCheckbox) {
|
||||
includeImagesCheckbox.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
const markdown = await this.generateMarkdown();
|
||||
await this.updatePreviewContent(markdown);
|
||||
});
|
||||
});
|
||||
|
||||
// Action buttons
|
||||
this.previewPanel.querySelector('.c4ai-copy-markdown-btn').addEventListener('click', () => {
|
||||
this.copyToClipboard();
|
||||
});
|
||||
|
||||
this.previewPanel.querySelector('.c4ai-download-btn').addEventListener('click', () => {
|
||||
this.downloadMarkdown();
|
||||
});
|
||||
} */
|
||||
|
||||
/* switchPreviewTab(tabName) {
|
||||
// Update active tab
|
||||
this.previewPanel.querySelectorAll('.c4ai-tab').forEach(tab => {
|
||||
tab.classList.toggle('active', tab.dataset.tab === tabName);
|
||||
});
|
||||
|
||||
// Update active pane
|
||||
this.previewPanel.querySelectorAll('.c4ai-preview-pane').forEach(pane => {
|
||||
pane.classList.toggle('active', pane.dataset.pane === tabName);
|
||||
});
|
||||
} */
|
||||
|
||||
/* async updatePreviewContent(markdown) {
|
||||
// Update markdown pane
|
||||
const markdownPane = this.previewPanel.querySelector('[data-pane="markdown"]');
|
||||
markdownPane.innerHTML = `<pre><code>${this.escapeHtml(markdown)}</code></pre>`;
|
||||
|
||||
// Update preview pane using marked.js
|
||||
const previewPane = this.previewPanel.querySelector('[data-pane="preview"]');
|
||||
|
||||
// Configure marked options (marked.js is already loaded via manifest)
|
||||
if (window.marked) {
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
tables: true,
|
||||
headerIds: false,
|
||||
mangle: false
|
||||
});
|
||||
|
||||
// Render markdown to HTML
|
||||
const html = marked.parse(markdown);
|
||||
previewPane.innerHTML = `<div class="c4ai-markdown-preview">${html}</div>`;
|
||||
} else {
|
||||
// Fallback if marked.js is not available
|
||||
previewPane.innerHTML = `<div class="c4ai-markdown-preview"><pre>${this.escapeHtml(markdown)}</pre></div>`;
|
||||
}
|
||||
} */
|
||||
|
||||
|
||||
/* escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
} */
|
||||
|
||||
async generateMarkdown(options) {
|
||||
// Get selected elements as array
|
||||
const elements = Array.from(this.selectedElements);
|
||||
|
||||
// Sort elements by their selection order
|
||||
const sortedElements = elements.sort((a, b) => {
|
||||
const orderA = parseInt(a.getAttribute('data-c4ai-selection-order') || '0');
|
||||
const orderB = parseInt(b.getAttribute('data-c4ai-selection-order') || '0');
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
// Convert each element separately
|
||||
const markdownParts = [];
|
||||
|
||||
for (let i = 0; i < sortedElements.length; i++) {
|
||||
const element = sortedElements[i];
|
||||
|
||||
// Add XPath header if enabled
|
||||
if (options.includeXPath) {
|
||||
const xpath = this.getXPath(element);
|
||||
markdownParts.push(`### Element ${i + 1} - XPath: \`${xpath}\`\n`);
|
||||
}
|
||||
|
||||
// Check if element is part of a table structure that should be processed specially
|
||||
let elementsToConvert = [element];
|
||||
|
||||
// If text-only mode and element is a TR, process the entire table for better context
|
||||
if (options.textOnly && element.tagName === 'TR') {
|
||||
const table = element.closest('table');
|
||||
if (table && !sortedElements.includes(table)) {
|
||||
// Only include this table row, not the whole table
|
||||
elementsToConvert = [element];
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze and convert individual element
|
||||
const analysis = await this.contentAnalyzer.analyze(elementsToConvert);
|
||||
const markdown = await this.markdownConverter.convert(elementsToConvert, {
|
||||
...options,
|
||||
analysis
|
||||
});
|
||||
|
||||
// Trim the markdown before adding
|
||||
const trimmedMarkdown = markdown.trim();
|
||||
markdownParts.push(trimmedMarkdown);
|
||||
|
||||
// Add separator if enabled and not last element
|
||||
if (options.addSeparators && i < sortedElements.length - 1) {
|
||||
markdownParts.push('\n---\n');
|
||||
}
|
||||
}
|
||||
|
||||
return markdownParts.join('\n');
|
||||
}
|
||||
|
||||
getXPath(element) {
|
||||
if (element.id) {
|
||||
return `//*[@id="${element.id}"]`;
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
let current = element;
|
||||
|
||||
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
||||
let index = 0;
|
||||
let sibling = current.previousSibling;
|
||||
|
||||
while (sibling) {
|
||||
if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === current.nodeName) {
|
||||
index++;
|
||||
}
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
|
||||
const tagName = current.nodeName.toLowerCase();
|
||||
const part = index > 0 ? `${tagName}[${index + 1}]` : tagName;
|
||||
parts.unshift(part);
|
||||
|
||||
current = current.parentNode;
|
||||
}
|
||||
|
||||
return '/' + parts.join('/');
|
||||
}
|
||||
|
||||
sortElementsByPosition(elements) {
|
||||
return elements.sort((a, b) => {
|
||||
const position = a.compareDocumentPosition(b);
|
||||
if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
|
||||
return -1;
|
||||
} else if (position & Node.DOCUMENT_POSITION_PRECEDING) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
async copyToClipboard() {
|
||||
if (this.markdownPreviewModal) {
|
||||
await this.markdownPreviewModal.copyToClipboard();
|
||||
}
|
||||
}
|
||||
|
||||
async downloadMarkdown() {
|
||||
if (this.markdownPreviewModal) {
|
||||
await this.markdownPreviewModal.downloadMarkdown();
|
||||
}
|
||||
}
|
||||
|
||||
showNotification(message, type = 'success') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `c4ai-notification c4ai-notification-${type}`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Animate in
|
||||
setTimeout(() => notification.classList.add('show'), 10);
|
||||
|
||||
// Remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('show');
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
// Remove event listeners
|
||||
document.removeEventListener('click', this.documentClickHandler, true);
|
||||
document.removeEventListener('click', this.linkClickHandler, true);
|
||||
document.removeEventListener('mouseover', this.documentHoverHandler, true);
|
||||
document.removeEventListener('mouseout', this.documentMouseOutHandler, true);
|
||||
document.removeEventListener('keydown', this.keyboardHandler);
|
||||
|
||||
// Clear selections
|
||||
this.clearSelection();
|
||||
|
||||
// Remove UI elements
|
||||
if (this.toolbar) {
|
||||
this.toolbar.remove();
|
||||
this.toolbar = null;
|
||||
}
|
||||
|
||||
if (this.markdownPreviewModal) {
|
||||
this.markdownPreviewModal.destroy();
|
||||
this.markdownPreviewModal = null;
|
||||
}
|
||||
|
||||
// Remove hover styles
|
||||
document.querySelectorAll('.c4ai-hover-candidate').forEach(el => {
|
||||
el.classList.remove('c4ai-hover-candidate');
|
||||
});
|
||||
|
||||
// Notify background script (with error handling)
|
||||
try {
|
||||
if (chrome.runtime && chrome.runtime.sendMessage) {
|
||||
chrome.runtime.sendMessage({
|
||||
action: 'c2cDeactivated'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Extension context might be invalidated, ignore the error
|
||||
console.log('Markdown Extraction deactivated (extension context unavailable)');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
// Shared Markdown Preview Modal Component for Crawl4AI Assistant
|
||||
// Used by both SchemaBuilder and Click2CrawlBuilder
|
||||
|
||||
class MarkdownPreviewModal {
|
||||
constructor(options = {}) {
|
||||
this.modal = null;
|
||||
this.markdownOptions = {
|
||||
includeImages: true,
|
||||
preserveTables: true,
|
||||
keepCodeFormatting: true,
|
||||
simplifyLayout: false,
|
||||
preserveLinks: true,
|
||||
addSeparators: true,
|
||||
includeXPath: false,
|
||||
textOnly: false,
|
||||
...options
|
||||
};
|
||||
this.onGenerateMarkdown = null;
|
||||
this.currentMarkdown = '';
|
||||
}
|
||||
|
||||
show(generateMarkdownCallback) {
|
||||
this.onGenerateMarkdown = generateMarkdownCallback;
|
||||
|
||||
if (!this.modal) {
|
||||
this.createModal();
|
||||
}
|
||||
|
||||
// Generate initial markdown
|
||||
this.updateContent();
|
||||
this.modal.style.display = 'block';
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (this.modal) {
|
||||
this.modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
createModal() {
|
||||
this.modal = document.createElement('div');
|
||||
this.modal.className = 'c4ai-c2c-preview';
|
||||
this.modal.innerHTML = `
|
||||
<div class="c4ai-preview-header">
|
||||
<div class="c4ai-toolbar-dots">
|
||||
<span class="c4ai-dot c4ai-dot-red"></span>
|
||||
<span class="c4ai-dot c4ai-dot-yellow"></span>
|
||||
<span class="c4ai-dot c4ai-dot-green"></span>
|
||||
</div>
|
||||
<span class="c4ai-preview-title">Markdown Preview</span>
|
||||
<button class="c4ai-preview-close">×</button>
|
||||
</div>
|
||||
<div class="c4ai-preview-options">
|
||||
<label><input type="checkbox" name="textOnly"> 👁️ Visual Text Mode (As You See)</label>
|
||||
<label><input type="checkbox" name="includeImages" checked> Include Images</label>
|
||||
<label><input type="checkbox" name="preserveTables" checked> Preserve Tables</label>
|
||||
<label><input type="checkbox" name="preserveLinks" checked> Preserve Links</label>
|
||||
<label><input type="checkbox" name="keepCodeFormatting" checked> Keep Code Formatting</label>
|
||||
<label><input type="checkbox" name="simplifyLayout"> Simplify Layout</label>
|
||||
<label><input type="checkbox" name="addSeparators" checked> Add Separators</label>
|
||||
<label><input type="checkbox" name="includeXPath"> Include XPath Headers</label>
|
||||
</div>
|
||||
<div class="c4ai-preview-content">
|
||||
<div class="c4ai-preview-tabs">
|
||||
<button class="c4ai-tab active" data-tab="preview">Preview</button>
|
||||
<button class="c4ai-tab" data-tab="markdown">Markdown</button>
|
||||
<button class="c4ai-wrap-toggle" title="Toggle word wrap">↔️ Wrap</button>
|
||||
</div>
|
||||
<div class="c4ai-preview-pane active" data-pane="preview"></div>
|
||||
<div class="c4ai-preview-pane" data-pane="markdown"></div>
|
||||
</div>
|
||||
<div class="c4ai-preview-actions">
|
||||
<button class="c4ai-download-btn">Download .md</button>
|
||||
<button class="c4ai-copy-markdown-btn">Copy Markdown</button>
|
||||
<button class="c4ai-cloud-btn" disabled>Send to Cloud (Coming Soon)</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(this.modal);
|
||||
|
||||
// Make modal draggable
|
||||
if (window.C4AI_Utils && window.C4AI_Utils.makeDraggable) {
|
||||
window.C4AI_Utils.makeDraggable(this.modal);
|
||||
}
|
||||
|
||||
// Position preview modal
|
||||
this.modal.style.position = 'fixed';
|
||||
this.modal.style.top = '50%';
|
||||
this.modal.style.left = '50%';
|
||||
this.modal.style.transform = 'translate(-50%, -50%)';
|
||||
this.modal.style.zIndex = '999999';
|
||||
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Close button
|
||||
this.modal.querySelector('.c4ai-preview-close').addEventListener('click', () => {
|
||||
this.hide();
|
||||
});
|
||||
|
||||
// Tab switching
|
||||
this.modal.querySelectorAll('.c4ai-tab').forEach(tab => {
|
||||
tab.addEventListener('click', (e) => {
|
||||
const tabName = e.target.dataset.tab;
|
||||
this.switchTab(tabName);
|
||||
});
|
||||
});
|
||||
|
||||
// Wrap toggle
|
||||
const wrapToggle = this.modal.querySelector('.c4ai-wrap-toggle');
|
||||
wrapToggle.addEventListener('click', () => {
|
||||
const panes = this.modal.querySelectorAll('.c4ai-preview-pane');
|
||||
panes.forEach(pane => {
|
||||
pane.classList.toggle('wrap');
|
||||
});
|
||||
wrapToggle.classList.toggle('active');
|
||||
});
|
||||
|
||||
// Options change
|
||||
this.modal.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', async (e) => {
|
||||
this.markdownOptions[e.target.name] = e.target.checked;
|
||||
|
||||
// Handle text-only mode dependencies
|
||||
if (e.target.name === 'textOnly' && e.target.checked) {
|
||||
const preserveLinksCheckbox = this.modal.querySelector('input[name="preserveLinks"]');
|
||||
if (preserveLinksCheckbox) {
|
||||
preserveLinksCheckbox.checked = false;
|
||||
preserveLinksCheckbox.disabled = true;
|
||||
this.markdownOptions.preserveLinks = false;
|
||||
}
|
||||
|
||||
const includeImagesCheckbox = this.modal.querySelector('input[name="includeImages"]');
|
||||
if (includeImagesCheckbox) {
|
||||
includeImagesCheckbox.disabled = true;
|
||||
}
|
||||
} else if (e.target.name === 'textOnly' && !e.target.checked) {
|
||||
// Re-enable options when text-only is disabled
|
||||
const preserveLinksCheckbox = this.modal.querySelector('input[name="preserveLinks"]');
|
||||
if (preserveLinksCheckbox) {
|
||||
preserveLinksCheckbox.disabled = false;
|
||||
}
|
||||
|
||||
const includeImagesCheckbox = this.modal.querySelector('input[name="includeImages"]');
|
||||
if (includeImagesCheckbox) {
|
||||
includeImagesCheckbox.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Update markdown content
|
||||
await this.updateContent();
|
||||
});
|
||||
});
|
||||
|
||||
// Action buttons
|
||||
this.modal.querySelector('.c4ai-copy-markdown-btn').addEventListener('click', () => {
|
||||
this.copyToClipboard();
|
||||
});
|
||||
|
||||
this.modal.querySelector('.c4ai-download-btn').addEventListener('click', () => {
|
||||
this.downloadMarkdown();
|
||||
});
|
||||
}
|
||||
|
||||
switchTab(tabName) {
|
||||
// Update active tab
|
||||
this.modal.querySelectorAll('.c4ai-tab').forEach(tab => {
|
||||
tab.classList.toggle('active', tab.dataset.tab === tabName);
|
||||
});
|
||||
|
||||
// Update active pane
|
||||
this.modal.querySelectorAll('.c4ai-preview-pane').forEach(pane => {
|
||||
pane.classList.toggle('active', pane.dataset.pane === tabName);
|
||||
});
|
||||
}
|
||||
|
||||
async updateContent() {
|
||||
if (!this.onGenerateMarkdown) return;
|
||||
|
||||
try {
|
||||
// Generate markdown with current options
|
||||
this.currentMarkdown = await this.onGenerateMarkdown(this.markdownOptions);
|
||||
|
||||
// Update markdown pane
|
||||
const markdownPane = this.modal.querySelector('[data-pane="markdown"]');
|
||||
markdownPane.innerHTML = `<pre><code>${this.escapeHtml(this.currentMarkdown)}</code></pre>`;
|
||||
|
||||
// Update preview pane
|
||||
const previewPane = this.modal.querySelector('[data-pane="preview"]');
|
||||
|
||||
// Use marked.js if available
|
||||
if (window.marked) {
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
tables: true,
|
||||
headerIds: false,
|
||||
mangle: false
|
||||
});
|
||||
|
||||
const html = marked.parse(this.currentMarkdown);
|
||||
previewPane.innerHTML = `<div class="c4ai-markdown-preview">${html}</div>`;
|
||||
} else {
|
||||
// Fallback
|
||||
previewPane.innerHTML = `<div class="c4ai-markdown-preview"><pre>${this.escapeHtml(this.currentMarkdown)}</pre></div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating markdown:', error);
|
||||
this.showNotification('Error generating markdown', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async copyToClipboard() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.currentMarkdown);
|
||||
this.showNotification('Markdown copied to clipboard!');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
this.showNotification('Failed to copy. Please try again.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async downloadMarkdown() {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
||||
const filename = `crawl4ai-export-${timestamp}.md`;
|
||||
|
||||
// Create blob and download
|
||||
const blob = new Blob([this.currentMarkdown], { type: 'text/markdown' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
this.showNotification(`Downloaded ${filename}`);
|
||||
}
|
||||
|
||||
showNotification(message, type = 'success') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `c4ai-notification c4ai-notification-${type}`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Animate in
|
||||
setTimeout(() => notification.classList.add('show'), 10);
|
||||
|
||||
// Remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('show');
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// Get current options
|
||||
getOptions() {
|
||||
return { ...this.markdownOptions };
|
||||
}
|
||||
|
||||
// Update options programmatically
|
||||
setOptions(options) {
|
||||
this.markdownOptions = { ...this.markdownOptions, ...options };
|
||||
|
||||
// Update checkboxes to reflect new options
|
||||
Object.entries(options).forEach(([key, value]) => {
|
||||
const checkbox = this.modal?.querySelector(`input[name="${key}"]`);
|
||||
if (checkbox && typeof value === 'boolean') {
|
||||
checkbox.checked = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
destroy() {
|
||||
if (this.modal) {
|
||||
this.modal.remove();
|
||||
this.modal = null;
|
||||
}
|
||||
this.onGenerateMarkdown = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other scripts
|
||||
if (typeof window !== 'undefined') {
|
||||
window.MarkdownPreviewModal = MarkdownPreviewModal;
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// Shared utilities for Crawl4AI Chrome Extension
|
||||
|
||||
// Make element draggable by its titlebar
|
||||
function makeDraggable(element) {
|
||||
let isDragging = false;
|
||||
let startX, startY, initialX, initialY;
|
||||
|
||||
const titlebar = element.querySelector('.c4ai-toolbar-titlebar, .c4ai-titlebar');
|
||||
if (!titlebar) return;
|
||||
|
||||
titlebar.addEventListener('mousedown', (e) => {
|
||||
// Don't drag if clicking on buttons
|
||||
if (e.target.classList.contains('c4ai-dot') || e.target.closest('button')) return;
|
||||
|
||||
isDragging = true;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
initialX = rect.left;
|
||||
initialY = rect.top;
|
||||
|
||||
element.style.transition = 'none';
|
||||
titlebar.style.cursor = 'grabbing';
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const deltaX = e.clientX - startX;
|
||||
const deltaY = e.clientY - startY;
|
||||
|
||||
element.style.left = `${initialX + deltaX}px`;
|
||||
element.style.top = `${initialY + deltaY}px`;
|
||||
element.style.right = 'auto';
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
element.style.transition = '';
|
||||
titlebar.style.cursor = 'grab';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Make element draggable by a specific header element
|
||||
function makeDraggableByHeader(element) {
|
||||
let isDragging = false;
|
||||
let startX, startY, initialX, initialY;
|
||||
|
||||
const header = element.querySelector('.c4ai-debugger-header');
|
||||
if (!header) return;
|
||||
|
||||
header.addEventListener('mousedown', (e) => {
|
||||
// Don't drag if clicking on close button
|
||||
if (e.target.id === 'c4ai-close-debugger' || e.target.closest('#c4ai-close-debugger')) return;
|
||||
|
||||
isDragging = true;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
initialX = rect.left;
|
||||
initialY = rect.top;
|
||||
|
||||
element.style.transition = 'none';
|
||||
header.style.cursor = 'grabbing';
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const deltaX = e.clientX - startX;
|
||||
const deltaY = e.clientY - startY;
|
||||
|
||||
element.style.left = `${initialX + deltaX}px`;
|
||||
element.style.top = `${initialY + deltaY}px`;
|
||||
element.style.right = 'auto';
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
element.style.transition = '';
|
||||
header.style.cursor = 'grab';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Escape HTML for safe display
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Apply syntax highlighting to Python code
|
||||
function applySyntaxHighlighting(codeElement) {
|
||||
const code = codeElement.textContent;
|
||||
|
||||
// Split by lines to handle line-by-line
|
||||
const lines = code.split('\n');
|
||||
const highlightedLines = lines.map(line => {
|
||||
let highlightedLine = escapeHtml(line);
|
||||
|
||||
// Skip if line is empty
|
||||
if (!highlightedLine.trim()) return highlightedLine;
|
||||
|
||||
// Comments (lines starting with #)
|
||||
if (highlightedLine.trim().startsWith('#')) {
|
||||
return `<span class="c4ai-comment">${highlightedLine}</span>`;
|
||||
}
|
||||
|
||||
// Triple quoted strings
|
||||
if (highlightedLine.includes('"""')) {
|
||||
highlightedLine = highlightedLine.replace(/(""".*?""")/g, '<span class="c4ai-string">$1</span>');
|
||||
}
|
||||
|
||||
// Regular strings - single and double quotes
|
||||
highlightedLine = highlightedLine.replace(/(["'])([^"']*)\1/g, '<span class="c4ai-string">$1$2$1</span>');
|
||||
|
||||
// Keywords - only highlight if not inside a string
|
||||
const keywords = ['import', 'from', 'async', 'def', 'await', 'try', 'except', 'with', 'as', 'for', 'if', 'else', 'elif', 'return', 'print', 'open', 'and', 'or', 'not', 'in', 'is', 'class', 'self', 'None', 'True', 'False', '__name__', '__main__'];
|
||||
|
||||
keywords.forEach(keyword => {
|
||||
// Use word boundaries and lookahead/lookbehind to ensure we're not in a string
|
||||
const regex = new RegExp(`\\b(${keyword})\\b(?![^<]*</span>)`, 'g');
|
||||
highlightedLine = highlightedLine.replace(regex, '<span class="c4ai-keyword">$1</span>');
|
||||
});
|
||||
|
||||
// Functions (word followed by parenthesis)
|
||||
highlightedLine = highlightedLine.replace(/\b([a-zA-Z_]\w*)\s*\(/g, '<span class="c4ai-function">$1</span>(');
|
||||
|
||||
return highlightedLine;
|
||||
});
|
||||
|
||||
codeElement.innerHTML = highlightedLines.join('\n');
|
||||
}
|
||||
|
||||
// Apply syntax highlighting to JavaScript code
|
||||
function applySyntaxHighlightingJS(codeElement) {
|
||||
const code = codeElement.textContent;
|
||||
|
||||
// Split by lines to handle line-by-line
|
||||
const lines = code.split('\n');
|
||||
const highlightedLines = lines.map(line => {
|
||||
let highlightedLine = escapeHtml(line);
|
||||
|
||||
// Skip if line is empty
|
||||
if (!highlightedLine.trim()) return highlightedLine;
|
||||
|
||||
// Comments
|
||||
if (highlightedLine.trim().startsWith('//')) {
|
||||
return `<span class="c4ai-comment">${highlightedLine}</span>`;
|
||||
}
|
||||
|
||||
// Multi-line comments
|
||||
highlightedLine = highlightedLine.replace(/(\/\*.*?\*\/)/g, '<span class="c4ai-comment">$1</span>');
|
||||
|
||||
// Template literals
|
||||
highlightedLine = highlightedLine.replace(/(`[^`]*`)/g, '<span class="c4ai-string">$1</span>');
|
||||
|
||||
// Regular strings - single and double quotes
|
||||
highlightedLine = highlightedLine.replace(/(["'])([^"']*)\1/g, '<span class="c4ai-string">$1$2$1</span>');
|
||||
|
||||
// Keywords
|
||||
const keywords = ['const', 'let', 'var', 'function', 'async', 'await', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'return', 'try', 'catch', 'finally', 'throw', 'new', 'this', 'class', 'extends', 'import', 'export', 'default', 'from', 'null', 'undefined', 'true', 'false'];
|
||||
|
||||
keywords.forEach(keyword => {
|
||||
const regex = new RegExp(`\\b(${keyword})\\b(?![^<]*</span>)`, 'g');
|
||||
highlightedLine = highlightedLine.replace(regex, '<span class="c4ai-keyword">$1</span>');
|
||||
});
|
||||
|
||||
// Functions and methods
|
||||
highlightedLine = highlightedLine.replace(/\b([a-zA-Z_$][\w$]*)\s*\(/g, '<span class="c4ai-function">$1</span>(');
|
||||
|
||||
// Numbers
|
||||
highlightedLine = highlightedLine.replace(/\b(\d+)\b/g, '<span class="c4ai-number">$1</span>');
|
||||
|
||||
return highlightedLine;
|
||||
});
|
||||
|
||||
codeElement.innerHTML = highlightedLines.join('\n');
|
||||
}
|
||||
|
||||
// Get element selector
|
||||
function getElementSelector(element) {
|
||||
// Priority: ID > unique class > tag with position
|
||||
if (element.id) {
|
||||
return `#${element.id}`;
|
||||
}
|
||||
|
||||
if (element.className && typeof element.className === 'string') {
|
||||
const classes = element.className.split(' ').filter(c => c && !c.startsWith('c4ai-'));
|
||||
if (classes.length > 0) {
|
||||
const selector = `.${classes[0]}`;
|
||||
if (document.querySelectorAll(selector).length === 1) {
|
||||
return selector;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build a path selector
|
||||
const path = [];
|
||||
let current = element;
|
||||
|
||||
while (current && current !== document.body) {
|
||||
const tagName = current.tagName.toLowerCase();
|
||||
const parent = current.parentElement;
|
||||
|
||||
if (parent) {
|
||||
const siblings = Array.from(parent.children);
|
||||
const index = siblings.indexOf(current) + 1;
|
||||
|
||||
if (siblings.filter(s => s.tagName === current.tagName).length > 1) {
|
||||
path.unshift(`${tagName}:nth-child(${index})`);
|
||||
} else {
|
||||
path.unshift(tagName);
|
||||
}
|
||||
} else {
|
||||
path.unshift(tagName);
|
||||
}
|
||||
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return path.join(' > ');
|
||||
}
|
||||
|
||||
// Check if element is part of our extension UI
|
||||
function isOurElement(element) {
|
||||
return element.classList.contains('c4ai-highlight-box') ||
|
||||
element.classList.contains('c4ai-toolbar') ||
|
||||
element.closest('.c4ai-toolbar') ||
|
||||
element.classList.contains('c4ai-script-toolbar') ||
|
||||
element.closest('.c4ai-script-toolbar') ||
|
||||
element.closest('.c4ai-field-dialog') ||
|
||||
element.closest('.c4ai-code-modal') ||
|
||||
element.closest('.c4ai-wait-dialog') ||
|
||||
element.closest('.c4ai-timeline-modal');
|
||||
}
|
||||
|
||||
// Export utilities
|
||||
window.C4AI_Utils = {
|
||||
makeDraggable,
|
||||
makeDraggableByHeader,
|
||||
escapeHtml,
|
||||
applySyntaxHighlighting,
|
||||
applySyntaxHighlightingJS,
|
||||
getElementSelector,
|
||||
isOurElement
|
||||
};
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,974 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crawl4AI Assistant - Chrome Extension for Visual Web Scraping</title>
|
||||
<link rel="stylesheet" href="assistant.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="terminal-container">
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
<div class="logo-section">
|
||||
<img src="../../img/favicon-32x32.png" alt="Crawl4AI Logo" class="logo">
|
||||
<div>
|
||||
<h1>Crawl4AI Assistant</h1>
|
||||
<p class="tagline">Chrome Extension for Visual Web Scraping</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav-links">
|
||||
<a href="../../" class="nav-link">← Back to Docs</a>
|
||||
<a href="../" class="nav-link">All Apps</a>
|
||||
<a href="https://github.com/unclecode/crawl4ai" class="nav-link" target="_blank">GitHub</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- Video Section -->
|
||||
<section class="video-section">
|
||||
<div class="video-wrapper">
|
||||
<video autoplay loop muted playsinline class="demo-video">
|
||||
<source src="demo.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Cloud Announcement Banner -->
|
||||
<section class="cloud-banner-section">
|
||||
<div class="cloud-banner">
|
||||
<div class="cloud-banner-content">
|
||||
<div class="cloud-banner-text">
|
||||
<h3>You don't need Puppeteer. You need Crawl4AI Cloud.</h3>
|
||||
<p>One API call. JS-rendered. No browser cluster to maintain.</p>
|
||||
</div>
|
||||
<button class="cloud-banner-btn" id="joinWaitlistBanner">
|
||||
Get API Key →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Introduction -->
|
||||
<section class="intro-section">
|
||||
<div class="terminal-window">
|
||||
<div class="terminal-header">
|
||||
<span class="terminal-title">About Crawl4AI Assistant</span>
|
||||
</div>
|
||||
<div class="terminal-content">
|
||||
<p>Transform any website into structured data with just a few clicks! The Crawl4AI Assistant Chrome Extension provides three powerful tools for web scraping and data extraction.</p>
|
||||
|
||||
<div style="background: #0fbbaa; color: #070708; padding: 12px 16px; border-radius: 8px; margin: 16px 0; font-weight: 600;">
|
||||
🎉 NEW: Click2Crawl extracts data INSTANTLY without any LLM! Test your schema and see JSON results immediately in the browser!
|
||||
</div>
|
||||
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">🎯</span>
|
||||
<h3>Click2Crawl</h3>
|
||||
<p>Visual data extraction - click elements to build schemas instantly!</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">🔴</span>
|
||||
<h3>Script Builder <span style="color: #f380f5; font-size: 0.75rem;">(Alpha)</span></h3>
|
||||
<p>Record browser actions to create automation scripts</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">📝</span>
|
||||
<h3>Markdown Extraction <span style="color: #0fbbaa; font-size: 0.75rem;">(New!)</span></h3>
|
||||
<p>Convert any webpage content to clean markdown with Visual Text Mode</p>
|
||||
</div>
|
||||
<!-- <div class="feature-card">
|
||||
<span class="feature-icon">🐍</span>
|
||||
<h3>Python Code</h3>
|
||||
<p>Get production-ready Crawl4AI code instantly</p>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Quick Start -->
|
||||
<section class="quickstart-section">
|
||||
<h2>Quick Start</h2>
|
||||
<div class="terminal-window">
|
||||
<div class="terminal-header">
|
||||
<span class="terminal-title">Installation</span>
|
||||
</div>
|
||||
<div class="terminal-content">
|
||||
<div class="installation-steps">
|
||||
<div class="step">
|
||||
<span class="step-number">1</span>
|
||||
<div class="step-content">
|
||||
<h4>Download the Extension</h4>
|
||||
<p>Get the latest release from GitHub or use the button below</p>
|
||||
<a href="crawl4ai-assistant-v1.3.0.zip" class="download-button" download>
|
||||
<span class="button-icon">↓</span>
|
||||
Download Extension (v1.3.0)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<span class="step-number">2</span>
|
||||
<div class="step-content">
|
||||
<h4>Load in Chrome</h4>
|
||||
<p>Navigate to <code>chrome://extensions/</code> and enable Developer Mode</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<span class="step-number">3</span>
|
||||
<div class="step-content">
|
||||
<h4>Load Unpacked</h4>
|
||||
<p>Click "Load unpacked" and select the extracted extension folder</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Tools Section -->
|
||||
<section class="interactive-tools">
|
||||
<h2>Explore Our Tools</h2>
|
||||
|
||||
<div class="tools-container">
|
||||
<!-- Left Panel - Tool Selector -->
|
||||
<div class="tools-panel">
|
||||
<div class="tool-selector active" data-tool="click2crawl">
|
||||
<div class="tool-icon">🎯</div>
|
||||
<div class="tool-info">
|
||||
<h3>Click2Crawl</h3>
|
||||
<p>Visual data extraction</p>
|
||||
</div>
|
||||
<div class="tool-status">Available</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-selector" data-tool="script-builder">
|
||||
<div class="tool-icon">🔴</div>
|
||||
<div class="tool-info">
|
||||
<h3>Script Builder</h3>
|
||||
<p>Browser automation</p>
|
||||
</div>
|
||||
<div class="tool-status alpha">Alpha</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-selector" data-tool="markdown-extraction">
|
||||
<div class="tool-icon">📝</div>
|
||||
<div class="tool-info">
|
||||
<h3>Markdown Extraction</h3>
|
||||
<p>Content to markdown</p>
|
||||
</div>
|
||||
<div class="tool-status new">New!</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel - Tool Details -->
|
||||
<div class="tool-details">
|
||||
<!-- Click2Crawl Details -->
|
||||
<div class="tool-content active" id="click2crawl">
|
||||
<div class="tool-header">
|
||||
<h3>🎯 Click2Crawl</h3>
|
||||
<span class="tool-tagline">Click elements to build extraction schemas - No LLM needed!</span>
|
||||
</div>
|
||||
|
||||
<div class="tool-steps">
|
||||
<div class="step-item">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-content">
|
||||
<h4>Select Container</h4>
|
||||
<p>Click on any repeating element like product cards or articles. Use up/down navigation to fine-tune selection!</p>
|
||||
<div class="step-visual">
|
||||
<span class="highlight-green">■</span> Container highlighted in green
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-item">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-content">
|
||||
<h4>Click Fields to Extract</h4>
|
||||
<p>Click on data fields inside the container - choose text, links, images, or attributes</p>
|
||||
<div class="step-visual">
|
||||
<span class="highlight-pink">■</span> Fields highlighted in pink
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-item">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-content">
|
||||
<h4>Test & Extract Data Instantly!</h4>
|
||||
<p>🎉 Click "Test Schema" to see extracted JSON immediately - no LLM or coding required!</p>
|
||||
<div class="step-visual">
|
||||
<span class="highlight-accent">⚡</span> See extracted JSON immediately
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-features">
|
||||
<div class="feature-tag">🚀 Zero LLM dependency</div>
|
||||
<div class="feature-tag">📊 Instant JSON extraction</div>
|
||||
<div class="feature-tag">🎯 Visual element selection</div>
|
||||
<div class="feature-tag">🐍 Export Python code</div>
|
||||
<div class="feature-tag">✨ Live preview</div>
|
||||
<div class="feature-tag">📥 Download results</div>
|
||||
<div class="feature-tag">📝 Export to markdown</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Script Builder Details -->
|
||||
<div class="tool-content" id="script-builder">
|
||||
<div class="tool-header">
|
||||
<h3>🔴 Script Builder</h3>
|
||||
<span class="tool-tagline">Record actions, generate automation</span>
|
||||
</div>
|
||||
|
||||
<div class="tool-steps">
|
||||
<div class="step-item">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-content">
|
||||
<h4>Hit Record</h4>
|
||||
<p>Start capturing your browser interactions</p>
|
||||
<div class="step-visual">
|
||||
<span class="recording-dot">●</span> Recording indicator
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-item">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-content">
|
||||
<h4>Interact Naturally</h4>
|
||||
<p>Click, type, scroll - everything is captured</p>
|
||||
<div class="step-visual">
|
||||
<span class="action-icon">🖱️</span> <span class="action-icon">⌨️</span> <span class="action-icon">📜</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-item">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-content">
|
||||
<h4>Export Script</h4>
|
||||
<p>Get JavaScript for Crawl4AI's js_code parameter</p>
|
||||
<div class="step-visual">
|
||||
<span class="highlight-accent">📝</span> Automation ready
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-features">
|
||||
<div class="feature-tag">Smart action grouping</div>
|
||||
<div class="feature-tag">Wait detection</div>
|
||||
<div class="feature-tag">Keyboard shortcuts</div>
|
||||
<div class="feature-tag alpha-tag">Alpha version</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Markdown Extraction Details -->
|
||||
<div class="tool-content" id="markdown-extraction">
|
||||
<div class="tool-header">
|
||||
<h3>📝 Markdown Extraction</h3>
|
||||
<span class="tool-tagline">Convert webpage content to clean markdown "as you see"</span>
|
||||
</div>
|
||||
|
||||
<div class="tool-steps">
|
||||
<div class="step-item">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-content">
|
||||
<h4>Ctrl/Cmd + Click</h4>
|
||||
<p>Hold Ctrl/Cmd and click multiple elements you want to extract</p>
|
||||
<div class="step-visual">
|
||||
<span class="highlight-green">🔢</span> Numbered selection badges
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-item">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-content">
|
||||
<h4>Enable Visual Text Mode</h4>
|
||||
<p>Extract content "as you see" - clean text without complex HTML structures</p>
|
||||
<div class="step-visual">
|
||||
<span class="highlight-accent">👁️</span> Visual Text Mode (As You See)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-item">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-content">
|
||||
<h4>Export Clean Markdown</h4>
|
||||
<p>Get beautifully formatted markdown ready for documentation or LLMs</p>
|
||||
<div class="step-visual">
|
||||
<span class="highlight-pink">📄</span> Clean, readable output
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-features">
|
||||
<div class="feature-tag">Multi-select with Ctrl/Cmd</div>
|
||||
<div class="feature-tag">Visual Text Mode (As You See)</div>
|
||||
<div class="feature-tag">Clean markdown output</div>
|
||||
<div class="feature-tag">Export to Crawl4AI Cloud (soon)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Code Examples -->
|
||||
<section class="code-showcase">
|
||||
<h2>See the Generated Code & Extracted Data</h2>
|
||||
|
||||
<div class="code-tabs">
|
||||
<button class="code-tab active" data-example="schema">🎯 Click2Crawl</button>
|
||||
<button class="code-tab" data-example="script">🔴 Script Builder</button>
|
||||
<button class="code-tab" data-example="markdown">📝 Markdown Extraction</button>
|
||||
</div>
|
||||
|
||||
<div class="code-examples">
|
||||
<!-- Click2Crawl Code -->
|
||||
<div class="code-example active" id="code-schema">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
|
||||
<!-- Python Code -->
|
||||
<div class="terminal-window">
|
||||
<div class="terminal-header">
|
||||
<span class="terminal-title">click2crawl_extraction.py</span>
|
||||
<button class="copy-button" data-code="schema-python">Copy</button>
|
||||
</div>
|
||||
<div class="terminal-content">
|
||||
<pre><code><span class="comment">#!/usr/bin/env python3</span>
|
||||
<span class="comment">"""
|
||||
🎉 NO LLM NEEDED! Direct extraction with CSS selectors
|
||||
Generated by Crawl4AI Chrome Extension - Click2Crawl
|
||||
"""</span>
|
||||
|
||||
<span class="keyword">import</span> asyncio
|
||||
<span class="keyword">import</span> json
|
||||
<span class="keyword">from</span> crawl4ai <span class="keyword">import</span> AsyncWebCrawler, BrowserConfig, CrawlerRunConfig
|
||||
<span class="keyword">from</span> crawl4ai.extraction_strategy <span class="keyword">import</span> JsonCssExtractionStrategy
|
||||
|
||||
<span class="comment"># The EXACT schema from Click2Crawl - no guessing!</span>
|
||||
EXTRACTION_SCHEMA = {
|
||||
<span class="string">"name"</span>: <span class="string">"Product Catalog"</span>,
|
||||
<span class="string">"baseSelector"</span>: <span class="string">"div.product-card"</span>, <span class="comment"># The container you selected</span>
|
||||
<span class="string">"fields"</span>: [
|
||||
{
|
||||
<span class="string">"name"</span>: <span class="string">"title"</span>,
|
||||
<span class="string">"selector"</span>: <span class="string">"h3.product-title"</span>,
|
||||
<span class="string">"type"</span>: <span class="string">"text"</span>
|
||||
},
|
||||
{
|
||||
<span class="string">"name"</span>: <span class="string">"price"</span>,
|
||||
<span class="string">"selector"</span>: <span class="string">"span.price"</span>,
|
||||
<span class="string">"type"</span>: <span class="string">"text"</span>
|
||||
},
|
||||
{
|
||||
<span class="string">"name"</span>: <span class="string">"image"</span>,
|
||||
<span class="string">"selector"</span>: <span class="string">"img.product-img"</span>,
|
||||
<span class="string">"type"</span>: <span class="string">"attribute"</span>,
|
||||
<span class="string">"attribute"</span>: <span class="string">"src"</span>
|
||||
},
|
||||
{
|
||||
<span class="string">"name"</span>: <span class="string">"link"</span>,
|
||||
<span class="string">"selector"</span>: <span class="string">"a.product-link"</span>,
|
||||
<span class="string">"type"</span>: <span class="string">"attribute"</span>,
|
||||
<span class="string">"attribute"</span>: <span class="string">"href"</span>
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
<span class="keyword">async</span> <span class="keyword">def</span> <span class="function">extract_data</span>(url: str):
|
||||
<span class="comment"># Direct extraction - no LLM API calls!</span>
|
||||
extraction_strategy = JsonCssExtractionStrategy(schema=EXTRACTION_SCHEMA)
|
||||
|
||||
<span class="keyword">async</span> <span class="keyword">with</span> AsyncWebCrawler() <span class="keyword">as</span> crawler:
|
||||
result = <span class="keyword">await</span> crawler.arun(
|
||||
url=url,
|
||||
config=CrawlerRunConfig(extraction_strategy=extraction_strategy)
|
||||
)
|
||||
|
||||
<span class="keyword">if</span> result.success:
|
||||
data = json.loads(result.extracted_content)
|
||||
<span class="keyword">print</span>(<span class="string">f"✅ Extracted {len(data)} items instantly!"</span>)
|
||||
|
||||
<span class="comment"># Save to file</span>
|
||||
<span class="keyword">with</span> open(<span class="string">'products.json'</span>, <span class="string">'w'</span>) <span class="keyword">as</span> f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
<span class="keyword">return</span> data
|
||||
|
||||
<span class="comment"># Run extraction on any similar page!</span>
|
||||
data = asyncio.run(extract_data(<span class="string">"https://example.com/products"</span>))
|
||||
|
||||
<span class="comment"># 🎯 Result: Clean JSON data, no LLM costs, instant results!</span></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Extracted JSON Data -->
|
||||
<div class="terminal-window">
|
||||
<div class="terminal-header">
|
||||
<span class="terminal-title">extracted_data.json</span>
|
||||
<button class="copy-button" data-code="schema-json">Copy</button>
|
||||
</div>
|
||||
<div class="terminal-content">
|
||||
<pre><code><span class="comment">// 🎉 Instantly extracted from the page - no coding required!</span>
|
||||
[
|
||||
{
|
||||
<span class="string">"title"</span>: <span class="string">"Wireless Bluetooth Headphones"</span>,
|
||||
<span class="string">"price"</span>: <span class="string">"$79.99"</span>,
|
||||
<span class="string">"image"</span>: <span class="string">"https://example.com/images/headphones-bt-01.jpg"</span>,
|
||||
<span class="string">"link"</span>: <span class="string">"/products/wireless-bluetooth-headphones"</span>
|
||||
},
|
||||
{
|
||||
<span class="string">"title"</span>: <span class="string">"Smart Watch Pro 2024"</span>,
|
||||
<span class="string">"price"</span>: <span class="string">"$299.00"</span>,
|
||||
<span class="string">"image"</span>: <span class="string">"https://example.com/images/smartwatch-pro.jpg"</span>,
|
||||
<span class="string">"link"</span>: <span class="string">"/products/smart-watch-pro-2024"</span>
|
||||
},
|
||||
{
|
||||
<span class="string">"title"</span>: <span class="string">"4K Webcam for Streaming"</span>,
|
||||
<span class="string">"price"</span>: <span class="string">"$149.99"</span>,
|
||||
<span class="string">"image"</span>: <span class="string">"https://example.com/images/webcam-4k.jpg"</span>,
|
||||
<span class="string">"link"</span>: <span class="string">"/products/4k-webcam-streaming"</span>
|
||||
},
|
||||
{
|
||||
<span class="string">"title"</span>: <span class="string">"Mechanical Gaming Keyboard RGB"</span>,
|
||||
<span class="string">"price"</span>: <span class="string">"$129.99"</span>,
|
||||
<span class="string">"image"</span>: <span class="string">"https://example.com/images/keyboard-gaming.jpg"</span>,
|
||||
<span class="string">"link"</span>: <span class="string">"/products/mechanical-gaming-keyboard"</span>
|
||||
},
|
||||
{
|
||||
<span class="string">"title"</span>: <span class="string">"USB-C Hub 7-in-1"</span>,
|
||||
<span class="string">"price"</span>: <span class="string">"$45.99"</span>,
|
||||
<span class="string">"image"</span>: <span class="string">"https://example.com/images/usbc-hub.jpg"</span>,
|
||||
<span class="string">"link"</span>: <span class="string">"/products/usb-c-hub-7in1"</span>
|
||||
}
|
||||
]</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Script Builder Code -->
|
||||
<div class="code-example" id="code-script">
|
||||
<div class="terminal-window">
|
||||
<div class="terminal-header">
|
||||
<span class="terminal-title">automation_script.py</span>
|
||||
<button class="copy-button" data-code="script">Copy</button>
|
||||
</div>
|
||||
<div class="terminal-content">
|
||||
<pre><code><span class="keyword">import</span> asyncio
|
||||
<span class="keyword">from</span> crawl4ai <span class="keyword">import</span> AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
<span class="comment"># JavaScript generated from your recorded actions</span>
|
||||
js_script = <span class="string">"""
|
||||
// Search for products
|
||||
document.querySelector('button.search-toggle').click();
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
// Type search query
|
||||
const searchInput = document.querySelector('input#search');
|
||||
searchInput.value = 'wireless headphones';
|
||||
searchInput.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
|
||||
// Submit search
|
||||
searchInput.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
key: 'Enter', keyCode: 13, bubbles: true
|
||||
}));
|
||||
|
||||
// Wait for results
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
// Click first product
|
||||
document.querySelector('.product-item:first-child').click();
|
||||
|
||||
// Wait for product page
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
// Add to cart
|
||||
document.querySelector('button.add-to-cart').click();
|
||||
"""</span>
|
||||
|
||||
<span class="keyword">async</span> <span class="keyword">def</span> <span class="function">automate_shopping</span>():
|
||||
config = CrawlerRunConfig(
|
||||
js_code=js_script,
|
||||
wait_for=<span class="string">"css:.cart-confirmation"</span>,
|
||||
screenshot=<span class="keyword">True</span>
|
||||
)
|
||||
|
||||
<span class="keyword">async</span> <span class="keyword">with</span> AsyncWebCrawler() <span class="keyword">as</span> crawler:
|
||||
result = <span class="keyword">await</span> crawler.arun(
|
||||
url=<span class="string">"https://shop.example.com"</span>,
|
||||
config=config
|
||||
)
|
||||
<span class="keyword">print</span>(<span class="string">f"✓ Automation complete: {result.url}"</span>)
|
||||
<span class="keyword">return</span> result
|
||||
|
||||
asyncio.run(automate_shopping())</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Markdown Extraction Output -->
|
||||
<div class="code-example" id="code-markdown">
|
||||
<div class="terminal-window">
|
||||
<div class="terminal-header">
|
||||
<span class="terminal-title">extracted_content.md</span>
|
||||
<button class="copy-button" data-code="markdown">Copy</button>
|
||||
</div>
|
||||
<div class="terminal-content">
|
||||
<pre><code><span class="comment"># Extracted from Hacker News with Visual Text Mode 👁️</span>
|
||||
|
||||
<span class="string">1. **Show HN: I built a tool to find and reach out to YouTubers** (hellosimply.io)
|
||||
84 points by erickim 2 hours ago | hide | 31 comments
|
||||
|
||||
2. **The 24 Hour Restaurant** (logicmag.io)
|
||||
124 points by helsinkiandrew 5 hours ago | hide | 52 comments
|
||||
|
||||
3. **Building a Better Bloom Filter in Rust** (carlmastrangelo.com)
|
||||
89 points by carlmastrangelo 3 hours ago | hide | 27 comments
|
||||
|
||||
---
|
||||
|
||||
### Article: The 24 Hour Restaurant
|
||||
|
||||
In New York City, the 24-hour restaurant is becoming extinct. What we lose when we can no longer eat whenever we want.
|
||||
|
||||
When I first moved to New York, I loved that I could get a full meal at 3 AM. Not just pizza or fast food, but a proper sit-down dinner with table service and a menu that ran for pages. The city that never sleeps had restaurants that matched its rhythm.
|
||||
|
||||
Today, finding a 24-hour restaurant in Manhattan requires genuine effort. The pandemic accelerated a decline that was already underway, but the roots go deeper: rising rents, changing labor laws, and shifting cultural patterns have all contributed to the death of round-the-clock dining.
|
||||
|
||||
---
|
||||
|
||||
### Product Review: Framework Laptop 16
|
||||
|
||||
**Specifications:**
|
||||
- Display: 16" 2560×1600 165Hz
|
||||
- Processor: AMD Ryzen 7 7840HS
|
||||
- Memory: 32GB DDR5-5600
|
||||
- Storage: 2TB NVMe Gen4
|
||||
- Price: Starting at $1,399
|
||||
|
||||
**Pros:**
|
||||
- Fully modular and repairable
|
||||
- Excellent Linux support
|
||||
- Great keyboard and trackpad
|
||||
- Expansion card system
|
||||
|
||||
**Cons:**
|
||||
- Battery life could be better
|
||||
- Slightly heavier than competitors
|
||||
- Fan noise under load</span></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Crawl4AI Cloud Section -->
|
||||
<section class="cloud-section">
|
||||
<div class="cloud-announcement">
|
||||
<h2>Crawl4AI Cloud</h2>
|
||||
<p class="cloud-tagline">Your browser cluster without the cluster.</p>
|
||||
|
||||
<div class="cloud-features-preview">
|
||||
<div class="cloud-feature-item">
|
||||
⚡ POST /crawl
|
||||
</div>
|
||||
<div class="cloud-feature-item">
|
||||
🌐 JS-rendered pages
|
||||
</div>
|
||||
<div class="cloud-feature-item">
|
||||
📊 Schema extraction built-in
|
||||
</div>
|
||||
<div class="cloud-feature-item">
|
||||
💰 $0.001/page
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="cloud-cta-button" id="joinWaitlist">
|
||||
Get Early Access →
|
||||
</button>
|
||||
|
||||
<p class="cloud-hint">See it extract your own data. Right now.</p>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Signup Form -->
|
||||
<div class="signup-overlay" id="signupOverlay">
|
||||
<div class="signup-container" id="signupContainer">
|
||||
<button class="close-signup" id="closeSignup">×</button>
|
||||
|
||||
<div class="signup-content" id="signupForm">
|
||||
<h3>🚀 Join C4AI Cloud Waiting List</h3>
|
||||
<p>Be among the first to experience the future of web scraping</p>
|
||||
|
||||
<form id="waitlistForm" class="waitlist-form">
|
||||
<div class="form-field">
|
||||
<label for="userName">Your Name</label>
|
||||
<input type="text" id="userName" name="name" placeholder="John Doe" required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="userEmail">Email Address</label>
|
||||
<input type="email" id="userEmail" name="email" placeholder="john@example.com" required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="userCompany">Company (Optional)</label>
|
||||
<input type="text" id="userCompany" name="company" placeholder="Acme Inc.">
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="useCase">What will you use Crawl4AI Cloud for?</label>
|
||||
<select id="useCase" name="useCase">
|
||||
<option value="">Select use case...</option>
|
||||
<option value="price-monitoring">Price Monitoring</option>
|
||||
<option value="news-aggregation">News Aggregation</option>
|
||||
<option value="market-research">Market Research</option>
|
||||
<option value="ai-training">AI Training Data</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit-button">
|
||||
<span>🎯</span> Submit & Watch the Magic
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Crawling Animation -->
|
||||
<div class="crawl-animation" id="crawlAnimation" style="display: none;">
|
||||
<div class="terminal-window crawl-terminal">
|
||||
<div class="terminal-header">
|
||||
<span class="terminal-title">Crawl4AI Cloud Demo</span>
|
||||
</div>
|
||||
<div class="terminal-content">
|
||||
<pre id="crawlOutput" class="crawl-log"><code>$ crawl4ai cloud extract --url "signup-form" --auto-detect</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="extracted-preview" id="extractedPreview" style="display: none;">
|
||||
<h4>📊 Extracted Data</h4>
|
||||
<pre class="json-preview"><code id="jsonOutput"></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="success-message" id="successMessage" style="display: none;">
|
||||
<div class="success-icon">✅</div>
|
||||
<h3>Data Uploaded Successfully!</h3>
|
||||
<p>You're on the Crawl4AI Cloud waiting list!</p>
|
||||
<p>What you just witnessed:</p>
|
||||
<ul>
|
||||
<li>⚡ Real-time extraction of your form data</li>
|
||||
<li>🔄 Automatic schema detection</li>
|
||||
<li>📤 Instant cloud processing</li>
|
||||
<li>✨ No code required - just like that!</li>
|
||||
</ul>
|
||||
<p class="success-note">We'll notify you at <strong id="userEmailDisplay"></strong> when Crawl4AI Cloud launches!</p>
|
||||
<button class="continue-button" id="continueBtn">Continue Exploring</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Coming Soon Section -->
|
||||
<section class="coming-soon-section">
|
||||
<h2>More Features Coming Soon</h2>
|
||||
<div class="terminal-window">
|
||||
<div class="terminal-header">
|
||||
<span class="terminal-title">Roadmap</span>
|
||||
</div>
|
||||
<div class="terminal-content">
|
||||
<p class="intro-text">We're continuously expanding C4AI Assistant with powerful new features:</p>
|
||||
|
||||
<div class="coming-features">
|
||||
<div class="coming-feature">
|
||||
<div class="feature-header">
|
||||
<span class="feature-badge">Direct</span>
|
||||
<h3>Direct Data Download</h3>
|
||||
</div>
|
||||
<p>Skip the code generation entirely! Download extracted data directly from Click2Crawl as JSON or CSV files.</p>
|
||||
<div class="feature-preview">
|
||||
<code>📊 One-click download • No Python needed • Multiple export formats</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="coming-feature">
|
||||
<div class="feature-header">
|
||||
<span class="feature-badge">AI</span>
|
||||
<h3>Smart Field Detection</h3>
|
||||
</div>
|
||||
<p>AI-powered field detection for Click2Crawl that automatically suggests the most likely data fields on any page.</p>
|
||||
<div class="feature-preview">
|
||||
<code>🤖 Auto-detect fields • Smart naming • Pattern recognition</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stay-tuned">
|
||||
<p>🚀 Stay tuned for updates! Follow our <a href="https://github.com/unclecode/crawl4ai" target="_blank">GitHub</a> for the latest releases.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h4>Resources</h4>
|
||||
<ul>
|
||||
<li><a href="https://github.com/unclecode/crawl4ai">GitHub Repository</a></li>
|
||||
<li><a href="../../">Documentation</a></li>
|
||||
<li><a href="https://discord.gg/jP8KfhDhyN">Discord Community</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Connect</h4>
|
||||
<ul>
|
||||
<li><a href="https://twitter.com/unclecode">Twitter @unclecode</a></li>
|
||||
<li><a href="https://github.com/unclecode">GitHub @unclecode</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>Made with 🚀 by the Crawl4AI team</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Tool Selector Interaction
|
||||
document.querySelectorAll('.tool-selector').forEach(selector => {
|
||||
selector.addEventListener('click', function() {
|
||||
// Remove active class from all selectors
|
||||
document.querySelectorAll('.tool-selector').forEach(s => s.classList.remove('active'));
|
||||
document.querySelectorAll('.tool-content').forEach(c => c.classList.remove('active'));
|
||||
|
||||
// Add active class to clicked selector
|
||||
this.classList.add('active');
|
||||
|
||||
// Show corresponding content
|
||||
const toolId = this.getAttribute('data-tool');
|
||||
const contentElement = document.getElementById(toolId);
|
||||
if (contentElement) {
|
||||
contentElement.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Code Tab Interaction
|
||||
document.querySelectorAll('.code-tab').forEach(tab => {
|
||||
tab.addEventListener('click', function() {
|
||||
// Remove active class from all tabs
|
||||
document.querySelectorAll('.code-tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.code-example').forEach(e => e.classList.remove('active'));
|
||||
|
||||
// Add active class to clicked tab
|
||||
this.classList.add('active');
|
||||
|
||||
// Show corresponding code
|
||||
const exampleId = this.getAttribute('data-example');
|
||||
document.getElementById('code-' + exampleId).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Copy Button Functionality
|
||||
document.querySelectorAll('.copy-button').forEach(button => {
|
||||
button.addEventListener('click', async function() {
|
||||
const codeType = this.getAttribute('data-code');
|
||||
let codeText = '';
|
||||
|
||||
// Handle different code types
|
||||
if (codeType === 'schema-python') {
|
||||
const codeElement = document.querySelector('#code-schema .terminal-window:first-child pre code');
|
||||
codeText = codeElement.textContent;
|
||||
} else if (codeType === 'schema-json') {
|
||||
const codeElement = document.querySelector('#code-schema .terminal-window:last-child pre code');
|
||||
codeText = codeElement.textContent;
|
||||
} else {
|
||||
const codeElement = document.getElementById('code-' + codeType).querySelector('pre code');
|
||||
codeText = codeElement.textContent;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(codeText);
|
||||
this.textContent = 'Copied!';
|
||||
this.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
this.textContent = 'Copy';
|
||||
this.classList.remove('copied');
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy code:', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Crawl4AI Cloud Interactive Demo
|
||||
const joinWaitlistBtn = document.getElementById('joinWaitlist');
|
||||
const signupOverlay = document.getElementById('signupOverlay');
|
||||
const closeSignupBtn = document.getElementById('closeSignup');
|
||||
const waitlistForm = document.getElementById('waitlistForm');
|
||||
const signupForm = document.getElementById('signupForm');
|
||||
const crawlAnimation = document.getElementById('crawlAnimation');
|
||||
const crawlOutput = document.getElementById('crawlOutput');
|
||||
const extractedPreview = document.getElementById('extractedPreview');
|
||||
const jsonOutput = document.getElementById('jsonOutput');
|
||||
const successMessage = document.getElementById('successMessage');
|
||||
const continueBtn = document.getElementById('continueBtn');
|
||||
const userEmailDisplay = document.getElementById('userEmailDisplay');
|
||||
|
||||
// Open signup modal
|
||||
joinWaitlistBtn.addEventListener('click', () => {
|
||||
signupOverlay.classList.add('active');
|
||||
});
|
||||
|
||||
// Banner button
|
||||
const joinWaitlistBannerBtn = document.getElementById('joinWaitlistBanner');
|
||||
if (joinWaitlistBannerBtn) {
|
||||
joinWaitlistBannerBtn.addEventListener('click', () => {
|
||||
signupOverlay.classList.add('active');
|
||||
});
|
||||
}
|
||||
|
||||
// Close signup modal
|
||||
closeSignupBtn.addEventListener('click', () => {
|
||||
signupOverlay.classList.remove('active');
|
||||
});
|
||||
|
||||
// Close on overlay click
|
||||
signupOverlay.addEventListener('click', (e) => {
|
||||
if (e.target === signupOverlay) {
|
||||
signupOverlay.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Continue button
|
||||
if (continueBtn) {
|
||||
continueBtn.addEventListener('click', () => {
|
||||
signupOverlay.classList.remove('active');
|
||||
// Reset form for next time
|
||||
waitlistForm.reset();
|
||||
signupForm.style.display = 'block';
|
||||
crawlAnimation.style.display = 'none';
|
||||
extractedPreview.style.display = 'none';
|
||||
successMessage.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Form submission with crawling animation
|
||||
waitlistForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Get form data
|
||||
const formData = {
|
||||
name: document.getElementById('userName').value,
|
||||
email: document.getElementById('userEmail').value,
|
||||
company: document.getElementById('userCompany').value || 'Not specified',
|
||||
useCase: document.getElementById('useCase').value || 'General web scraping',
|
||||
timestamp: new Date().toISOString(),
|
||||
source: 'Crawl4AI Assistant Landing Page'
|
||||
};
|
||||
|
||||
// Update email display
|
||||
userEmailDisplay.textContent = formData.email;
|
||||
|
||||
// Hide form and show crawling animation
|
||||
signupForm.style.display = 'none';
|
||||
crawlAnimation.style.display = 'block';
|
||||
|
||||
// Clear previous output
|
||||
const codeElement = crawlOutput.querySelector('code');
|
||||
codeElement.innerHTML = '$ crawl4ai cloud extract --url "signup-form" --auto-detect\n\n';
|
||||
|
||||
// Simulate crawling process with proper C4AI log format
|
||||
const crawlSteps = [
|
||||
{
|
||||
log: '<span class="log-init">[INIT]....</span> → Crawl4AI Cloud 1.0.0',
|
||||
time: '0.12s'
|
||||
},
|
||||
{
|
||||
log: '<span class="log-fetch">[FETCH]...</span> ↓ https://crawl4ai.com/waitlist-form',
|
||||
time: '0.45s'
|
||||
},
|
||||
{
|
||||
log: '<span class="log-scrape">[SCRAPE]..</span> ◆ https://crawl4ai.com/waitlist-form',
|
||||
time: '0.28s'
|
||||
},
|
||||
{
|
||||
log: '<span class="log-extract">[EXTRACT].</span> ■ Extracting form data with auto-detect',
|
||||
time: '0.55s'
|
||||
},
|
||||
{
|
||||
log: '<span class="log-complete">[COMPLETE]</span> ● https://crawl4ai.com/waitlist-form',
|
||||
time: '1.40s'
|
||||
}
|
||||
];
|
||||
|
||||
let stepIndex = 0;
|
||||
const typeStep = async () => {
|
||||
if (stepIndex < crawlSteps.length) {
|
||||
const step = crawlSteps[stepIndex];
|
||||
codeElement.innerHTML += step.log + ' | <span class="log-success">✓</span> | <span class="log-time">⏱: ' + step.time + '</span>\n';
|
||||
stepIndex++;
|
||||
|
||||
// Scroll to bottom
|
||||
const terminal = crawlOutput.parentElement;
|
||||
terminal.scrollTop = terminal.scrollHeight;
|
||||
|
||||
setTimeout(typeStep, 600);
|
||||
} else {
|
||||
// Show extracted data
|
||||
setTimeout(() => {
|
||||
codeElement.innerHTML += '\n<span class="log-success">[UPLOAD]..</span> ↑ Uploading to Crawl4AI Cloud...';
|
||||
|
||||
setTimeout(() => {
|
||||
extractedPreview.style.display = 'block';
|
||||
jsonOutput.textContent = JSON.stringify(formData, null, 2);
|
||||
|
||||
// Add syntax highlighting
|
||||
jsonOutput.innerHTML = jsonOutput.textContent
|
||||
.replace(/"([^"]+)":/g, '<span class="string">"$1"</span>:')
|
||||
.replace(/: "([^"]+)"/g, ': <span class="string">"$1"</span>');
|
||||
|
||||
codeElement.innerHTML += ' | <span class="log-success">✓</span> | <span class="log-time">⏱: 0.23s</span>\n';
|
||||
codeElement.innerHTML += '\n<span class="log-success">[SUCCESS]</span> ✨ Data uploaded successfully!';
|
||||
|
||||
// Show success message after a delay
|
||||
setTimeout(() => {
|
||||
successMessage.style.display = 'block';
|
||||
|
||||
// Smooth scroll to bottom to show success message
|
||||
setTimeout(() => {
|
||||
const container = document.getElementById('signupContainer');
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 100);
|
||||
|
||||
// Actually submit to waiting list (you can implement this)
|
||||
console.log('Waitlist submission:', formData);
|
||||
}, 1500);
|
||||
}, 800);
|
||||
}, 600);
|
||||
}
|
||||
};
|
||||
|
||||
// Start the animation
|
||||
setTimeout(typeStep, 500);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Crawl4AI Assistant",
|
||||
"version": "1.3.0",
|
||||
"description": "Visual schema and script builder for Crawl4AI - Build extraction schemas and automation scripts by clicking and recording actions",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage",
|
||||
"downloads"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"action": {
|
||||
"default_popup": "popup/popup.html",
|
||||
"default_icon": {
|
||||
"16": "icons/icon-16.png",
|
||||
"48": "icons/icon-48.png",
|
||||
"128": "icons/icon-128.png"
|
||||
}
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": [
|
||||
"libs/marked.min.js",
|
||||
"content/shared/utils.js",
|
||||
"content/markdownPreviewModal.js",
|
||||
"content/click2crawl.js",
|
||||
"content/scriptBuilder.js",
|
||||
"content/contentAnalyzer.js",
|
||||
"content/markdownConverter.js",
|
||||
"content/markdownExtraction.js",
|
||||
"content/content.js"
|
||||
],
|
||||
"css": ["content/overlay.css"],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background/service-worker.js"
|
||||
},
|
||||
"icons": {
|
||||
"16": "icons/icon-16.png",
|
||||
"48": "icons/icon-48.png",
|
||||
"128": "icons/icon-128.png"
|
||||
},
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["icons/*", "assets/*"],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,330 @@
|
||||
/* Font Face Definitions */
|
||||
@font-face {
|
||||
font-family: 'Dank Mono';
|
||||
src: url('../assets/DankMono-Regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Dank Mono';
|
||||
src: url('../assets/DankMono-Bold.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Dank Mono';
|
||||
src: url('../assets/DankMono-Italic.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-primary: 'Dank Mono', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 380px;
|
||||
font-family: var(--font-primary);
|
||||
background: #0a0a0a;
|
||||
color: #e0e0e0;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.popup-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.header-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.github-stars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #999;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.github-stars:hover {
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
.github-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mode-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mode-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background: #1a1a1a;
|
||||
border: 2px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mode-button:hover:not(:disabled) {
|
||||
background: #252525;
|
||||
border-color: #4a9eff;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.mode-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mode-button .icon {
|
||||
font-size: 32px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #252525;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mode-button.schema .icon {
|
||||
background: #1e3a5f;
|
||||
}
|
||||
|
||||
.mode-button.script .icon {
|
||||
background: #3a1e5f;
|
||||
}
|
||||
|
||||
.mode-button.c2c .icon {
|
||||
background: #1e5f3a;
|
||||
}
|
||||
|
||||
.mode-info h3 {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mode-info p {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.active-session {
|
||||
background: #1a1a1a;
|
||||
border: 2px solid #4a9eff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.active-session.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.session-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #4a9eff;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.session-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
.session-stats {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #0a0a0a;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.session-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
flex: 1;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-button.primary {
|
||||
background: #4a9eff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.action-button.primary:hover:not(:disabled) {
|
||||
background: #3a8eef;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-button.primary:disabled {
|
||||
background: #2a4a7f;
|
||||
color: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-button.secondary {
|
||||
background: #2a2a2a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-button.secondary:hover {
|
||||
background: #3a3a3a;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
background: #1a1a1a;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.instructions h4 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.instructions ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.instructions li {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #ccc;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.social-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #999;
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.social-link:hover {
|
||||
color: #0fbbaa;
|
||||
background: #2a2a2a;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.social-link svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="popup-container">
|
||||
<header>
|
||||
<img src="icons/icon-48.png" class="logo" alt="Crawl4AI">
|
||||
<div class="header-content">
|
||||
<h1>Crawl4AI Assistant</h1>
|
||||
<div class="header-stats">
|
||||
<a href="https://github.com/unclecode/crawl4ai" target="_blank" class="github-stars">
|
||||
<svg class="github-icon" viewBox="0 0 16 16" width="16" height="16">
|
||||
<path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path>
|
||||
</svg>
|
||||
<span id="stars-count">Loading...</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="mode-selector">
|
||||
<button id="schema-mode" class="mode-button schema">
|
||||
<div class="icon">🎯</div>
|
||||
<div class="mode-info">
|
||||
<h3>Click2Crawl</h3>
|
||||
<p>Click elements to build extraction schemas</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button id="script-mode" class="mode-button script">
|
||||
<div class="icon">🔴</div>
|
||||
<div class="mode-info">
|
||||
<h3>Script Builder <span style="color: #ff3c74; font-size: 10px;">(Alpha)</span></h3>
|
||||
<p>Record actions to build automation scripts</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button id="c2c-mode" class="mode-button c2c">
|
||||
<div class="icon">📝</div>
|
||||
<div class="mode-info">
|
||||
<h3>Markdown Extraction</h3>
|
||||
<p>Select elements and convert to clean markdown</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="active-session" class="active-session hidden">
|
||||
<div class="session-header">
|
||||
<span class="status-dot"></span>
|
||||
<span class="session-title">Schema Capture Active</span>
|
||||
</div>
|
||||
<div class="session-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-label">Container:</span>
|
||||
<span id="container-status" class="stat-value">Not selected</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Fields:</span>
|
||||
<span id="fields-count" class="stat-value">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-actions">
|
||||
<button id="generate-code" class="action-button primary" disabled>
|
||||
<span>Generate Code</span>
|
||||
</button>
|
||||
<button id="stop-capture" class="action-button secondary">
|
||||
<span>Stop Capture</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="instructions" style="display: none;">
|
||||
<h4>How to use:</h4>
|
||||
<ol>
|
||||
<li>Click "Click2Crawl" to start</li>
|
||||
<li>Click on a container element (e.g., product card)</li>
|
||||
<li>Click individual fields inside and name them</li>
|
||||
<li>Generate Python code when done</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="social-links">
|
||||
<a href="https://docs.crawl4ai.com" target="_blank" class="social-link">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
</svg>
|
||||
<span>Docs</span>
|
||||
</a>
|
||||
<a href="https://twitter.com/unclecode" target="_blank" class="social-link">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||
<path fill="currentColor" d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||||
</svg>
|
||||
<span>@unclecode</span>
|
||||
</a>
|
||||
<a href="https://discord.gg/jP8KfhDhyN" target="_blank" class="social-link">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||
<path fill="currentColor" d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.09.09 0 00-.07.03c-.18.33-.39.76-.53 1.09a16.09 16.09 0 00-4.8 0c-.14-.34-.35-.76-.54-1.09-.01-.02-.04-.03-.07-.03-1.5.26-2.93.71-4.27 1.33-.01 0-.02.01-.03.02-2.72 4.07-3.47 8.03-3.1 11.95 0 .02.01.04.03.05 1.8 1.32 3.53 2.12 5.24 2.65.03.01.06 0 .07-.02.4-.55.76-1.13 1.07-1.74.02-.04 0-.08-.04-.09-.57-.22-1.11-.48-1.64-.78-.04-.02-.04-.08-.01-.11.11-.08.22-.17.33-.25.02-.02.05-.02.07-.01 3.44 1.57 7.15 1.57 10.55 0 .02-.01.05-.01.07.01.11.09.22.17.33.26.04.03.04.09-.01.11-.52.31-1.07.56-1.64.78-.04.01-.05.06-.04.09.32.61.68 1.19 1.07 1.74.03.01.06.02.09.01 1.72-.53 3.45-1.33 5.25-2.65.02-.01.03-.03.03-.05.44-4.53-.73-8.46-3.1-11.95-.01-.01-.02-.02-.04-.02zM8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12 0 1.17-.84 2.12-1.89 2.12zm6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12 0 1.17-.83 2.12-1.89 2.12z"/>
|
||||
</svg>
|
||||
<span>Discord</span>
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,146 @@
|
||||
// Popup script for Crawl4AI Assistant
|
||||
let activeMode = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Fetch GitHub stars
|
||||
fetchGitHubStars();
|
||||
|
||||
// Check current state
|
||||
chrome.storage.local.get(['captureMode', 'captureStats'], (data) => {
|
||||
if (data.captureMode) {
|
||||
activeMode = data.captureMode;
|
||||
showActiveSession(data.captureStats || {});
|
||||
}
|
||||
});
|
||||
|
||||
// Mode buttons
|
||||
document.getElementById('schema-mode').addEventListener('click', () => {
|
||||
startSchemaCapture();
|
||||
});
|
||||
|
||||
document.getElementById('script-mode').addEventListener('click', () => {
|
||||
startScriptCapture();
|
||||
});
|
||||
|
||||
document.getElementById('c2c-mode').addEventListener('click', () => {
|
||||
startClick2Crawl();
|
||||
});
|
||||
|
||||
// Session actions
|
||||
document.getElementById('generate-code').addEventListener('click', () => {
|
||||
generateCode();
|
||||
});
|
||||
|
||||
document.getElementById('stop-capture').addEventListener('click', () => {
|
||||
stopCapture();
|
||||
});
|
||||
});
|
||||
|
||||
async function fetchGitHubStars() {
|
||||
try {
|
||||
const response = await fetch('https://api.github.com/repos/unclecode/crawl4ai');
|
||||
const data = await response.json();
|
||||
const stars = data.stargazers_count;
|
||||
|
||||
// Format the number (e.g., 1.2k)
|
||||
let formattedStars;
|
||||
if (stars >= 1000) {
|
||||
formattedStars = (stars / 1000).toFixed(1) + 'k';
|
||||
} else {
|
||||
formattedStars = stars.toString();
|
||||
}
|
||||
|
||||
document.getElementById('stars-count').textContent = `⭐ ${formattedStars}`;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch GitHub stars:', error);
|
||||
document.getElementById('stars-count').textContent = '⭐ 2k+';
|
||||
}
|
||||
}
|
||||
|
||||
function startSchemaCapture() {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
chrome.tabs.sendMessage(tabs[0].id, {
|
||||
action: 'startSchemaCapture'
|
||||
}, (response) => {
|
||||
if (response && response.success) {
|
||||
// Close the popup to let user interact with the page
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function startScriptCapture() {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
chrome.tabs.sendMessage(tabs[0].id, {
|
||||
action: 'startScriptCapture'
|
||||
}, (response) => {
|
||||
if (response && response.success) {
|
||||
// Close the popup to let user interact with the page
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function startClick2Crawl() {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
chrome.tabs.sendMessage(tabs[0].id, {
|
||||
action: 'startClick2Crawl'
|
||||
}, (response) => {
|
||||
if (response && response.success) {
|
||||
// Close the popup to let user interact with the page
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showActiveSession(stats) {
|
||||
document.querySelector('.mode-selector').style.display = 'none';
|
||||
document.getElementById('active-session').classList.remove('hidden');
|
||||
|
||||
updateSessionStats(stats);
|
||||
}
|
||||
|
||||
function updateSessionStats(stats) {
|
||||
document.getElementById('container-status').textContent =
|
||||
stats.container ? 'Selected ✓' : 'Not selected';
|
||||
document.getElementById('fields-count').textContent = stats.fields || 0;
|
||||
|
||||
// Enable generate button if we have container and fields
|
||||
document.getElementById('generate-code').disabled =
|
||||
!stats.container || stats.fields === 0;
|
||||
}
|
||||
|
||||
function generateCode() {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
chrome.tabs.sendMessage(tabs[0].id, {
|
||||
action: 'generateCode'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function stopCapture() {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
chrome.tabs.sendMessage(tabs[0].id, {
|
||||
action: 'stopCapture'
|
||||
}, () => {
|
||||
// Reset UI
|
||||
document.querySelector('.mode-selector').style.display = 'flex';
|
||||
document.getElementById('active-session').classList.add('hidden');
|
||||
activeMode = null;
|
||||
|
||||
// Clear storage
|
||||
chrome.storage.local.remove(['captureMode', 'captureStats']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for stats updates from content script
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.action === 'updateStats') {
|
||||
updateSessionStats(message.stats);
|
||||
chrome.storage.local.set({ captureStats: message.stats });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,302 @@
|
||||
# 🚀 Crawl4AI Interactive Apps
|
||||
|
||||
Welcome to the Crawl4AI Apps Hub - your gateway to interactive tools and demos that make web scraping more intuitive and powerful.
|
||||
|
||||
<style>
|
||||
.apps-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.app-card {
|
||||
background: #3f3f44;
|
||||
border: 1px solid #3f3f44;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
|
||||
border-color: #50ffff;
|
||||
}
|
||||
|
||||
.app-card h3 {
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #e8e9ed;
|
||||
}
|
||||
|
||||
.app-status {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.status-available {
|
||||
background: #50ffff;
|
||||
color: #070708;
|
||||
}
|
||||
|
||||
.status-beta {
|
||||
background: #f59e0b;
|
||||
color: #070708;
|
||||
}
|
||||
|
||||
.status-coming-soon {
|
||||
background: #2a2a2a;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.app-description {
|
||||
margin: 1rem 0;
|
||||
line-height: 1.6;
|
||||
color: #a3abba;
|
||||
}
|
||||
|
||||
.app-features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.app-features li {
|
||||
padding-left: 1.5rem;
|
||||
position: relative;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #d5cec0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.app-features li:before {
|
||||
content: "▸";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #50ffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.app-action {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.app-btn {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #50ffff;
|
||||
color: #070708;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
font-family: dm, Monaco, monospace;
|
||||
}
|
||||
|
||||
.app-btn:hover {
|
||||
background: #09b5a5;
|
||||
transform: scale(1.05);
|
||||
color: #070708;
|
||||
}
|
||||
|
||||
.app-btn.disabled {
|
||||
background: #2a2a2a;
|
||||
color: #666;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.app-btn.disabled:hover {
|
||||
background: #2a2a2a;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.intro-section {
|
||||
background: #3f3f44;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 3rem;
|
||||
border: 1px solid #3f3f44;
|
||||
}
|
||||
|
||||
.intro-section h2 {
|
||||
margin-top: 0;
|
||||
color: #50ffff;
|
||||
}
|
||||
|
||||
.intro-section p {
|
||||
color: #d5cec0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="intro-section">
|
||||
<h2>🛠️ Interactive Tools for Modern Web Scraping</h2>
|
||||
<p>
|
||||
Our apps are designed to make Crawl4AI more accessible and powerful. Whether you're learning browser automation, designing extraction strategies, or building complex scrapers, these tools provide visual, interactive ways to work with Crawl4AI's features.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## 🎯 Available Apps
|
||||
|
||||
<div class="apps-container">
|
||||
|
||||
<div class="app-card">
|
||||
<span class="app-status status-available">Available</span>
|
||||
<h3>🎨 C4A-Script Interactive Editor</h3>
|
||||
<p class="app-description">
|
||||
A visual, block-based programming environment for creating browser automation scripts. Perfect for beginners and experts alike!
|
||||
</p>
|
||||
<ul class="app-features">
|
||||
<li>Drag-and-drop visual programming</li>
|
||||
<li>Real-time JavaScript generation</li>
|
||||
<li>Interactive tutorials</li>
|
||||
<li>Export to C4A-Script or JavaScript</li>
|
||||
<li>Live preview capabilities</li>
|
||||
</ul>
|
||||
<div class="app-action">
|
||||
<a href="c4a-script/" class="app-btn" target="_blank">Launch Editor →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<span class="app-status status-available">Available</span>
|
||||
<h3>🧠 LLM Context Builder</h3>
|
||||
<p class="app-description">
|
||||
Generate optimized context files for your favorite LLM when working with Crawl4AI. Get focused, relevant documentation based on your needs.
|
||||
</p>
|
||||
<ul class="app-features">
|
||||
<li>Modular context generation</li>
|
||||
<li>Memory, reasoning & examples perspectives</li>
|
||||
<li>Component-based selection</li>
|
||||
<li>Vibe coding preset</li>
|
||||
<li>Download custom contexts</li>
|
||||
</ul>
|
||||
<div class="app-action">
|
||||
<a href="llmtxt/" class="app-btn" target="_blank">Launch Builder →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<span class="app-status status-coming-soon">Coming Soon</span>
|
||||
<h3>🕸️ Web Scraping Playground</h3>
|
||||
<p class="app-description">
|
||||
Test your scraping strategies on real websites with instant feedback. See how different configurations affect your results.
|
||||
</p>
|
||||
<ul class="app-features">
|
||||
<li>Live website testing</li>
|
||||
<li>Side-by-side result comparison</li>
|
||||
<li>Performance metrics</li>
|
||||
<li>Export configurations</li>
|
||||
</ul>
|
||||
<div class="app-action">
|
||||
<a href="#" class="app-btn disabled">Coming Soon</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<span class="app-status status-available">Available</span>
|
||||
<h3>🔍 Crawl4AI Assistant (Chrome Extension)</h3>
|
||||
<p class="app-description">
|
||||
Visual schema builder Chrome extension - click on webpage elements to generate extraction schemas and Python code!
|
||||
</p>
|
||||
<ul class="app-features">
|
||||
<li>Visual element selection</li>
|
||||
<li>Container & field selection modes</li>
|
||||
<li>Smart selector generation</li>
|
||||
<li>Complete Python code generation</li>
|
||||
<li>One-click installation</li>
|
||||
</ul>
|
||||
<div class="app-action">
|
||||
<a href="crawl4ai-assistant/" class="app-btn">Install Extension →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<span class="app-status status-coming-soon">Coming Soon</span>
|
||||
<h3>🧪 Extraction Lab</h3>
|
||||
<p class="app-description">
|
||||
Experiment with different extraction strategies and see how they perform on your content. Compare LLM vs CSS vs XPath approaches.
|
||||
</p>
|
||||
<ul class="app-features">
|
||||
<li>Strategy comparison tools</li>
|
||||
<li>Performance benchmarks</li>
|
||||
<li>Cost estimation for LLM strategies</li>
|
||||
<li>Best practice recommendations</li>
|
||||
</ul>
|
||||
<div class="app-action">
|
||||
<a href="#" class="app-btn disabled">Coming Soon</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<span class="app-status status-coming-soon">Coming Soon</span>
|
||||
<h3>🤖 AI Prompt Designer</h3>
|
||||
<p class="app-description">
|
||||
Craft and test prompts for LLM-based extraction. See how different prompts affect extraction quality and costs.
|
||||
</p>
|
||||
<ul class="app-features">
|
||||
<li>Prompt templates library</li>
|
||||
<li>A/B testing interface</li>
|
||||
<li>Token usage calculator</li>
|
||||
<li>Quality metrics</li>
|
||||
</ul>
|
||||
<div class="app-action">
|
||||
<a href="#" class="app-btn disabled">Coming Soon</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<span class="app-status status-coming-soon">Coming Soon</span>
|
||||
<h3>📊 Crawl Monitor</h3>
|
||||
<p class="app-description">
|
||||
Real-time monitoring dashboard for your crawling operations. Track performance, debug issues, and optimize your scrapers.
|
||||
</p>
|
||||
<ul class="app-features">
|
||||
<li>Real-time crawl statistics</li>
|
||||
<li>Error tracking and debugging</li>
|
||||
<li>Resource usage monitoring</li>
|
||||
<li>Historical analytics</li>
|
||||
</ul>
|
||||
<div class="app-action">
|
||||
<a href="#" class="app-btn disabled">Coming Soon</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
## 🚀 Why Use These Apps?
|
||||
|
||||
### 🎯 **Accelerate Learning**
|
||||
Visual tools help you understand Crawl4AI's concepts faster than reading documentation alone.
|
||||
|
||||
### 💡 **Reduce Development Time**
|
||||
Generate working code instantly instead of writing everything from scratch.
|
||||
|
||||
### 🔍 **Improve Quality**
|
||||
Test and refine your approach before deploying to production.
|
||||
|
||||
### 🤝 **Community Driven**
|
||||
These tools are built based on user feedback. Have an idea? [Let us know](https://github.com/unclecode/crawl4ai/issues)!
|
||||
|
||||
## 📢 Stay Updated
|
||||
|
||||
Want to know when new apps are released?
|
||||
|
||||
- ⭐ [Star us on GitHub](https://github.com/unclecode/crawl4ai) to get notifications
|
||||
- 🐦 Follow [@unclecode](https://twitter.com/unclecode) for announcements
|
||||
- 💬 Join our [Discord community](https://discord.gg/crawl4ai) for early access
|
||||
|
||||
---
|
||||
|
||||
!!! tip "Developer Resources"
|
||||
Building your own tools with Crawl4AI? Check out our [API Reference](../api/async-webcrawler.md) and [Integration Guide](../advanced/advanced-features.md) for comprehensive documentation.
|
||||
@@ -0,0 +1,75 @@
|
||||
|
||||
O**Prompt for AI Coding Assistant: Create an Interactive LLM Context Builder Page**
|
||||
|
||||
**Objective:**
|
||||
|
||||
Your task is to create an interactive HTML webpage with JavaScript functionality that allows users to select and combine different `crawl4ai` LLM context files into a single downloadable Markdown (`.md`) file. This tool will empower users to craft tailored context for their AI assistants based on their specific needs.
|
||||
|
||||
**Core Functionality:**
|
||||
|
||||
1. **Display `crawl4ai` Components:** The page will list all available `crawl4ai` documentation components.
|
||||
2. **Select Context Types:** For each component, users can select which types of context they want to include:
|
||||
* Memory (API facts)
|
||||
* Reasoning (How-to/why)
|
||||
* Examples (Code snippets)
|
||||
(All should be selected by default for each initially selected component).
|
||||
3. **Special "Aggregate" Contexts:** Include options for special, pre-combined contexts:
|
||||
* "Vibe Coding" (a curated mix for general AI prompting)
|
||||
* "All Library Context" (a comprehensive aggregation of all memory, reasoning, and examples for the entire library).
|
||||
4. **Fetch and Concatenate:** When the user clicks a "Download Combined Context" button:
|
||||
* The JavaScript will fetch the content of all selected Markdown files from the server (from a predefined folder, e.g., `/llmtxt/`).
|
||||
* It will concatenate the content of these files into a single string.
|
||||
5. **Client-Side Download:** The concatenated content will be offered to the user as a download (e.g., `custom_crawl4ai_context.md`).
|
||||
|
||||
**Input/Assumptions:**
|
||||
|
||||
* **Context Files Location:** All individual context Markdown files are located on the server in a publicly accessible folder named `llmtxt/`.
|
||||
* **File Naming Convention:** Files follow the pattern: `crawl4ai_{{component_name}}_[memory|reasoning|examples]_content.llm.md`.
|
||||
* `{{component_name}}` can contain underscores (e.g., `deep_crawling`, `config_objects`).
|
||||
* The special contexts will have names like `crawl4ai_vibe_content.llm.md` and `crawl4ai_all_content.llm.md`.
|
||||
* **Component List:** You will be provided with a list of `crawl4ai` components. For this implementation, use the following list:
|
||||
* `core`
|
||||
* `config_objects`
|
||||
* `deep_crawling`
|
||||
* `deployment` (covers Installation & Docker Deployment)
|
||||
* `extraction` (covers Structured Data Extraction)
|
||||
* `markdown` (covers Markdown Generation Algorithm)
|
||||
* `pdf_processing`
|
||||
* *(No separate "Vibe Coding" or "All Library Context" in this list, as they are special top-level selections)*
|
||||
|
||||
**Detailed UI/UX Requirements:**
|
||||
|
||||
1. **Main Page Structure:**
|
||||
* **Header:** "Crawl4AI Interactive LLM Context Builder"
|
||||
* **Introduction:** Briefly explain the purpose of the tool (from the `USING_LLM_CONTEXTS.md` content you helped draft: "Supercharging Your AI Assistant...").
|
||||
* **Selection Area:**
|
||||
* **Special Aggregate Contexts (Radio Buttons or Prominent Checkboxes):**
|
||||
* [ ] "Vibe Coding Context" (`crawl4ai_vibe_content.llm.md`)
|
||||
* [ ] "All Library Context (Comprehensive)" (`crawl4ai_all_content.llm.md`)
|
||||
* *Behavior:* Selecting one of these might disable individual component selections (or vice-versa) to avoid redundancy, or simply add them to the list. Consider user experience here. A simple approach is that if an aggregate is selected, it's the *only* thing downloaded.
|
||||
* **Individual Component Selection (Table or List of Checkboxes):**
|
||||
* A section titled "Select Individual Components & Context Types:"
|
||||
* For each component in the provided list:
|
||||
* A master checkbox for the component itself (e.g., `[ ] Core Functionality`). Selected by default.
|
||||
* Nested checkboxes (indented or grouped) for context types, enabled only if the parent component is checked:
|
||||
* `[x] Memory (API Facts)`
|
||||
* `[x] Reasoning (How-to/Why)`
|
||||
* `[x] Examples (Code Snippets)`
|
||||
(These three sub-checkboxes should be selected by default if the parent component is selected).
|
||||
* **Action Button:**
|
||||
* A button: "Generate & Download Combined Context"
|
||||
* **Status/Feedback Area:** (Optional, but good UX)
|
||||
* Display messages like "Fetching files...", "Combining context...", "Download starting..." or error messages.
|
||||
|
||||
|
||||
**Final Output:**
|
||||
|
||||
* A single HTML file (e.g., `interactive_context_builder.html`).
|
||||
* Associated JavaScript code (can be inline within `<script>` tags or in a separate `.js` file).
|
||||
* Associated CSS code (can be inline within `<style>` tags or in a separate `.css` file).
|
||||
|
||||
This interactive tool will greatly enhance the user experience for `crawl4ai` developers looking to leverage your specialized LLM contexts. Please ensure the JavaScript is robust and provides good user feedback.
|
||||
|
||||
---
|
||||
|
||||
This prompt should give your AI coding assistant a very clear set of requirements and guidelines for building the interactive context builder. Remember to provide it with the list of components as mentioned in the "Input/Assumptions" section.
|
||||
@@ -0,0 +1,135 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crawl4AI LLM Context Builder</title>
|
||||
<link rel="stylesheet" href="llmtxt.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="terminal-container">
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
<div class="logo-section">
|
||||
<img src="../../img/favicon-32x32.png" alt="Crawl4AI Logo" class="logo">
|
||||
<div>
|
||||
<h1>Crawl4AI LLM Context Builder</h1>
|
||||
<p class="tagline">Multi-Dimensional Context for AI Assistants</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav-links">
|
||||
<a href="../../" class="nav-link">← Back to Docs</a>
|
||||
<a href="../" class="nav-link">All Apps</a>
|
||||
<a href="https://github.com/unclecode/crawl4ai" class="nav-link" target="_blank">GitHub</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<section class="intro">
|
||||
<div class="intro-header">
|
||||
<h2>🧠 A New Approach to LLM Context</h2>
|
||||
<p>
|
||||
Traditional <code>llm.txt</code> files often fail with complex libraries like Crawl4AI. They dump massive amounts of API documentation, causing <strong>information overload</strong> and <strong>lost focus</strong>. They provide the "what" but miss the crucial "how" and "why" that makes AI assistants truly helpful.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="intro-solution">
|
||||
<h3>💡 The Solution: Multi-Dimensional, Modular Contexts</h3>
|
||||
<p>
|
||||
Inspired by modular libraries like Lodash, I've redesigned how we provide context to AI assistants. Instead of one monolithic file, Crawl4AI's documentation is organized by <strong>components</strong> and <strong>perspectives</strong>.
|
||||
</p>
|
||||
|
||||
<div class="dimensions">
|
||||
<div class="dimension">
|
||||
<span class="badge memory">Memory</span>
|
||||
<h4>The "What"</h4>
|
||||
<p>Precise API facts, parameters, signatures, and configuration objects. Your unambiguous reference.</p>
|
||||
</div>
|
||||
<div class="dimension">
|
||||
<span class="badge reasoning">Reasoning</span>
|
||||
<h4>The "How" & "Why"</h4>
|
||||
<p>Design principles, best practices, trade-offs, and workflows. Teaches AI to think like an expert.</p>
|
||||
</div>
|
||||
<div class="dimension">
|
||||
<span class="badge examples">Examples</span>
|
||||
<h4>The "Show Me"</h4>
|
||||
<p>Runnable code snippets demonstrating patterns in action. Pure practical implementation.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="intro-benefits">
|
||||
<p>
|
||||
<strong>Why this matters:</strong> You can now give your AI assistant exactly what it needs - whether that's quick API lookups, help designing solutions, or seeing practical implementations. No more information overload, just focused, relevant context.
|
||||
</p>
|
||||
<p class="learn-more">
|
||||
<a href="/blog/articles/llm-context-revolution" class="learn-more-link" target="_parent">📖 Read the full story behind this approach →</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="builder">
|
||||
<div class="component-selector" id="component-selector">
|
||||
<h2>Select Components & Context Types</h2>
|
||||
<div class="select-all-controls">
|
||||
<button class="btn-small" id="select-all">Select All</button>
|
||||
<button class="btn-small" id="deselect-all">Deselect All</button>
|
||||
</div>
|
||||
|
||||
<div class="component-table-wrapper">
|
||||
<table class="component-selection-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="50"></th>
|
||||
<th>Component</th>
|
||||
<th class="clickable-header" data-type="memory">Memory<br><span class="header-subtitle">Full Content</span></th>
|
||||
<th class="clickable-header" data-type="reasoning">Reasoning<br><span class="header-subtitle">Diagrams</span></th>
|
||||
<th class="clickable-header" data-type="examples">Examples<br><span class="header-subtitle">Code</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="components-tbody">
|
||||
<!-- Components will be dynamically inserted here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-area">
|
||||
<div class="token-summary" id="token-summary">
|
||||
<span class="token-label">Estimated Tokens:</span>
|
||||
<span class="token-count" id="total-tokens">0</span>
|
||||
</div>
|
||||
<button class="download-btn" id="download-btn">
|
||||
<span class="icon">⬇</span> Generate & Download Context
|
||||
</button>
|
||||
<div class="status" id="status"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="reference-table">
|
||||
<h2>Available Context Files</h2>
|
||||
<div class="table-wrapper">
|
||||
<table class="context-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Component</th>
|
||||
<th>Memory</th>
|
||||
<th>Reasoning</th>
|
||||
<th>Examples</th>
|
||||
<th>Full</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="reference-table-body">
|
||||
<!-- Table rows will be dynamically inserted here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="llmtxt.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,603 @@
|
||||
/* Terminal Theme CSS for LLM Context Builder */
|
||||
|
||||
/* Font Face Definitions */
|
||||
@font-face {
|
||||
font-family: 'Dank Mono';
|
||||
src: url('../assets/DankMono-Regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Dank Mono';
|
||||
src: url('../assets/DankMono-Bold.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Dank Mono';
|
||||
src: url('../assets/DankMono-Italic.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background-color: #070708;
|
||||
--font-color: #e8e9ed;
|
||||
--primary-color: #50ffff;
|
||||
--primary-dimmed: #09b5a5;
|
||||
--secondary-color: #d5cec0;
|
||||
--tertiary-color: #a3abba;
|
||||
--accent-color: rgb(243, 128, 245);
|
||||
--error-color: #ff3c74;
|
||||
--code-bg-color: #3f3f44;
|
||||
--border-color: #3f3f44;
|
||||
--hover-bg: #1a1a1c;
|
||||
--success-color: #50ff50;
|
||||
--bg-secondary: #1a1a1a;
|
||||
--primary-green: #0fbbaa;
|
||||
--font-primary: 'Dank Mono', dm, Monaco, Courier New, monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--font-primary);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
background-color: var(--background-color);
|
||||
color: var(--font-color);
|
||||
}
|
||||
|
||||
/* Terminal Container */
|
||||
.terminal-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header - matching assistant layout */
|
||||
.header {
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 1.5rem 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(26, 26, 26, 0.95);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.logo-section h1 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--font-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 0.875rem;
|
||||
color: var(--tertiary-color);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: var(--tertiary-color);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--primary-green);
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.content {
|
||||
flex: 1;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Intro Section */
|
||||
.intro {
|
||||
background-color: var(--code-bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.intro-header h2 {
|
||||
color: var(--primary-color);
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.intro-header p {
|
||||
line-height: 1.6;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.intro-header code {
|
||||
background-color: var(--hover-bg);
|
||||
padding: 2px 6px;
|
||||
color: var(--primary-dimmed);
|
||||
}
|
||||
|
||||
.intro-solution {
|
||||
margin-top: 5px;
|
||||
padding-top: 25px;
|
||||
border-top: 1px dashed var(--border-color);
|
||||
}
|
||||
|
||||
.intro-solution h3 {
|
||||
color: var(--secondary-color);
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.dimensions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.dimension {
|
||||
background-color: var(--hover-bg);
|
||||
padding: 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dimension:hover {
|
||||
border-color: var(--primary-dimmed);
|
||||
}
|
||||
|
||||
.dimension h4 {
|
||||
color: var(--font-color);
|
||||
margin: 10px 0 8px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dimension p {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--tertiary-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.intro-benefits {
|
||||
margin-top: 0px;
|
||||
padding-top: 0x;
|
||||
border-top: 1px dashed var(--border-color);
|
||||
}
|
||||
|
||||
.intro-benefits strong {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.learn-more {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.learn-more-link {
|
||||
color: var(--primary-dimmed);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.learn-more-link:hover {
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.badge.memory {
|
||||
background-color: var(--primary-dimmed);
|
||||
color: var(--background-color);
|
||||
}
|
||||
|
||||
.badge.reasoning {
|
||||
background-color: var(--accent-color);
|
||||
color: var(--background-color);
|
||||
}
|
||||
|
||||
.badge.examples {
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--background-color);
|
||||
}
|
||||
|
||||
/* Builder Section */
|
||||
.builder {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.builder h2 {
|
||||
color: var(--primary-color);
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Preset Options */
|
||||
.preset-options {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.preset-option {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.preset-option input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.preset-card {
|
||||
border: 2px solid var(--border-color);
|
||||
padding: 20px;
|
||||
transition: all 0.2s ease;
|
||||
background-color: var(--code-bg-color);
|
||||
}
|
||||
|
||||
.preset-card h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--secondary-color);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.preset-card p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--tertiary-color);
|
||||
}
|
||||
|
||||
.preset-option input:checked + .preset-card {
|
||||
border-color: var(--primary-color);
|
||||
background-color: var(--hover-bg);
|
||||
}
|
||||
|
||||
.preset-card:hover {
|
||||
border-color: var(--primary-dimmed);
|
||||
}
|
||||
|
||||
/* Component Selector */
|
||||
.component-selector {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.select-all-controls {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
background-color: var(--code-bg-color);
|
||||
color: var(--font-color);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 5px 15px;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-small:hover {
|
||||
background-color: var(--primary-dimmed);
|
||||
color: var(--background-color);
|
||||
}
|
||||
|
||||
/* Component Selection Table */
|
||||
.component-table-wrapper {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.component-selection-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background-color: var(--code-bg-color);
|
||||
}
|
||||
|
||||
.component-selection-table th,
|
||||
.component-selection-table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.component-selection-table th {
|
||||
background-color: var(--hover-bg);
|
||||
color: var(--primary-color);
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 10px;
|
||||
color: var(--tertiary-color);
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.component-selection-table th.clickable-header {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.component-selection-table th.clickable-header:hover {
|
||||
background-color: var(--primary-dimmed);
|
||||
color: var(--background-color);
|
||||
}
|
||||
|
||||
.component-selection-table th.clickable-header[data-type="examples"] {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.component-selection-table th.clickable-header[data-type="examples"]:hover {
|
||||
background-color: var(--hover-bg);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.component-selection-table th:nth-child(3),
|
||||
.component-selection-table th:nth-child(4),
|
||||
.component-selection-table th:nth-child(5) {
|
||||
text-align: center;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.component-selection-table td {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.component-selection-table td:nth-child(3),
|
||||
.component-selection-table td:nth-child(4),
|
||||
.component-selection-table td:nth-child(5) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.component-selection-table tr:hover td {
|
||||
background-color: var(--hover-bg);
|
||||
}
|
||||
|
||||
.component-name {
|
||||
color: var(--primary-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Token display in table cells */
|
||||
.token-info {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--tertiary-color);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.component-selection-table input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.component-selection-table input[type="checkbox"]:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Disabled row state */
|
||||
.component-selection-table tr.disabled td:not(:first-child) {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Action Area */
|
||||
.action-area {
|
||||
text-align: center;
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
/* Token Summary */
|
||||
.token-summary {
|
||||
margin-bottom: 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.token-label {
|
||||
color: var(--tertiary-color);
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.token-count {
|
||||
color: var(--primary-color);
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.token-count::after {
|
||||
content: " est.";
|
||||
font-size: 12px;
|
||||
color: var(--tertiary-color);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
background-color: var(--primary-dimmed);
|
||||
color: var(--background-color);
|
||||
border: none;
|
||||
padding: 15px 40px;
|
||||
font-size: 16px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.download-btn:hover {
|
||||
background-color: var(--primary-color);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.download-btn .icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.status.loading {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.status.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
/* Reference Table */
|
||||
.reference-table {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
.reference-table h2 {
|
||||
color: var(--primary-color);
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.context-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background-color: var(--code-bg-color);
|
||||
}
|
||||
|
||||
.context-table th,
|
||||
.context-table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.context-table th {
|
||||
background-color: var(--hover-bg);
|
||||
color: var(--primary-color);
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.context-table td {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.context-table tr:hover td {
|
||||
background-color: var(--hover-bg);
|
||||
}
|
||||
|
||||
.file-link {
|
||||
color: var(--primary-dimmed);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.file-link:hover {
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
color: var(--tertiary-color);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.preset-options {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.components-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,580 @@
|
||||
// Crawl4AI LLM Context Builder JavaScript
|
||||
|
||||
// Component definitions - order matters
|
||||
const components = [
|
||||
{
|
||||
id: 'installation',
|
||||
name: 'Installation',
|
||||
description: 'Setup and installation options'
|
||||
},
|
||||
{
|
||||
id: 'simple_crawling',
|
||||
name: 'Simple Crawling',
|
||||
description: 'Basic web crawling operations'
|
||||
},
|
||||
{
|
||||
id: 'config_objects',
|
||||
name: 'Configuration Objects',
|
||||
description: 'Browser and crawler configuration'
|
||||
},
|
||||
{
|
||||
id: 'extraction-llm',
|
||||
name: 'Data Extraction Using LLM',
|
||||
description: 'Structured data extraction strategies using LLMs'
|
||||
},
|
||||
{
|
||||
id: 'extraction-no-llm',
|
||||
name: 'Data Extraction Without LLM',
|
||||
description: 'Structured data extraction strategies without LLMs'
|
||||
},
|
||||
{
|
||||
id: 'multi_urls_crawling',
|
||||
name: 'Multi URLs Crawling',
|
||||
description: 'Crawling multiple URLs efficiently'
|
||||
},
|
||||
{
|
||||
id: 'deep_crawling',
|
||||
name: 'Deep Crawling',
|
||||
description: 'Multi-page crawling strategies'
|
||||
},
|
||||
{
|
||||
id: 'docker',
|
||||
name: 'Docker',
|
||||
description: 'Docker deployment and configuration'
|
||||
},
|
||||
{
|
||||
id: 'cli',
|
||||
name: 'CLI',
|
||||
description: 'Command-line interface usage'
|
||||
},
|
||||
{
|
||||
id: 'http_based_crawler_strategy',
|
||||
name: 'HTTP-based Crawler',
|
||||
description: 'HTTP crawler strategy implementation'
|
||||
},
|
||||
{
|
||||
id: 'url_seeder',
|
||||
name: 'URL Seeder',
|
||||
description: 'URL seeding and discovery'
|
||||
},
|
||||
{
|
||||
id: 'deep_crawl_advanced_filters_scorers',
|
||||
name: 'Advanced Filters & Scorers',
|
||||
description: 'Deep crawl filtering and scoring'
|
||||
}
|
||||
];
|
||||
|
||||
// Context types
|
||||
const contextTypes = ['memory', 'reasoning', 'examples'];
|
||||
|
||||
// State management
|
||||
const state = {
|
||||
selectedComponents: new Set(),
|
||||
selectedContextTypes: new Map(),
|
||||
tokenCounts: new Map() // Store token counts for each file
|
||||
};
|
||||
|
||||
// Initialize the application
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
renderComponents();
|
||||
renderReferenceTable();
|
||||
setupActionHandlers();
|
||||
setupColumnHeaderHandlers();
|
||||
|
||||
// Initialize first component as selected with available context types
|
||||
const firstComponent = components[0];
|
||||
state.selectedComponents.add(firstComponent.id);
|
||||
state.selectedContextTypes.set(firstComponent.id, new Set(['memory', 'reasoning']));
|
||||
updateComponentUI();
|
||||
});
|
||||
|
||||
// Helper function to count tokens (words × 2.5)
|
||||
function estimateTokens(text) {
|
||||
if (!text) return 0;
|
||||
const words = text.trim().split(/\s+/).length;
|
||||
return Math.round(words * 2.5);
|
||||
}
|
||||
|
||||
// Update total token count display
|
||||
function updateTotalTokenCount() {
|
||||
let totalTokens = 0;
|
||||
|
||||
state.selectedComponents.forEach(compId => {
|
||||
const types = state.selectedContextTypes.get(compId);
|
||||
if (types) {
|
||||
types.forEach(type => {
|
||||
const key = `${compId}-${type}`;
|
||||
totalTokens += state.tokenCounts.get(key) || 0;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('total-tokens').textContent = totalTokens.toLocaleString();
|
||||
}
|
||||
|
||||
// Render component selection table
|
||||
function renderComponents() {
|
||||
const tbody = document.getElementById('components-tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
components.forEach(component => {
|
||||
const row = createComponentRow(component);
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
// Fetch token counts for all files
|
||||
fetchAllTokenCounts();
|
||||
}
|
||||
|
||||
// Create a component table row
|
||||
function createComponentRow(component) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.id = `component-${component.id}`;
|
||||
|
||||
// Component checkbox cell
|
||||
const checkboxCell = document.createElement('td');
|
||||
checkboxCell.innerHTML = `
|
||||
<input type="checkbox" id="check-${component.id}"
|
||||
data-component="${component.id}">
|
||||
`;
|
||||
tr.appendChild(checkboxCell);
|
||||
|
||||
// Component name cell
|
||||
const nameCell = document.createElement('td');
|
||||
nameCell.innerHTML = `<span class="component-name">${component.name}</span>`;
|
||||
tr.appendChild(nameCell);
|
||||
|
||||
// Context type cells
|
||||
contextTypes.forEach(type => {
|
||||
const td = document.createElement('td');
|
||||
const key = `${component.id}-${type}`;
|
||||
const tokenCount = state.tokenCounts.get(key) || 0;
|
||||
const isDisabled = type === 'examples' ? 'disabled' : '';
|
||||
|
||||
td.innerHTML = `
|
||||
<input type="checkbox" id="check-${component.id}-${type}"
|
||||
data-component="${component.id}" data-type="${type}"
|
||||
${isDisabled}>
|
||||
<span class="token-info" id="tokens-${component.id}-${type}">
|
||||
${tokenCount > 0 ? `${tokenCount.toLocaleString()} tokens` : ''}
|
||||
</span>
|
||||
`;
|
||||
tr.appendChild(td);
|
||||
});
|
||||
|
||||
// Add event listeners
|
||||
const mainCheckbox = tr.querySelector(`#check-${component.id}`);
|
||||
mainCheckbox.addEventListener('change', (e) => {
|
||||
handleComponentToggle(component.id, e.target.checked);
|
||||
});
|
||||
|
||||
// Add event listeners for context type checkboxes
|
||||
contextTypes.forEach(type => {
|
||||
const typeCheckbox = tr.querySelector(`#check-${component.id}-${type}`);
|
||||
if (!typeCheckbox.disabled) {
|
||||
typeCheckbox.addEventListener('change', (e) => {
|
||||
handleContextTypeToggle(component.id, type, e.target.checked);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
// Handle component checkbox toggle
|
||||
function handleComponentToggle(componentId, checked) {
|
||||
if (checked) {
|
||||
state.selectedComponents.add(componentId);
|
||||
// Select only available context types when component is selected
|
||||
if (!state.selectedContextTypes.has(componentId)) {
|
||||
state.selectedContextTypes.set(componentId, new Set(['memory', 'reasoning']));
|
||||
} else {
|
||||
// If component was already partially selected, select all available
|
||||
state.selectedContextTypes.set(componentId, new Set(['memory', 'reasoning']));
|
||||
}
|
||||
} else {
|
||||
state.selectedComponents.delete(componentId);
|
||||
state.selectedContextTypes.delete(componentId);
|
||||
}
|
||||
updateComponentUI();
|
||||
}
|
||||
|
||||
// Handle component selection based on context types
|
||||
function updateComponentSelection(componentId) {
|
||||
const types = state.selectedContextTypes.get(componentId) || new Set();
|
||||
if (types.size > 0) {
|
||||
state.selectedComponents.add(componentId);
|
||||
} else {
|
||||
state.selectedComponents.delete(componentId);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle context type checkbox toggle
|
||||
function handleContextTypeToggle(componentId, type, checked) {
|
||||
if (!state.selectedContextTypes.has(componentId)) {
|
||||
state.selectedContextTypes.set(componentId, new Set());
|
||||
}
|
||||
|
||||
const types = state.selectedContextTypes.get(componentId);
|
||||
if (checked) {
|
||||
types.add(type);
|
||||
} else {
|
||||
types.delete(type);
|
||||
}
|
||||
|
||||
updateComponentSelection(componentId);
|
||||
updateComponentUI();
|
||||
}
|
||||
|
||||
// Update UI to reflect current state
|
||||
function updateComponentUI() {
|
||||
components.forEach(component => {
|
||||
const row = document.getElementById(`component-${component.id}`);
|
||||
if (!row) return;
|
||||
|
||||
const mainCheckbox = row.querySelector(`#check-${component.id}`);
|
||||
const hasSelection = state.selectedComponents.has(component.id);
|
||||
const selectedTypes = state.selectedContextTypes.get(component.id) || new Set();
|
||||
|
||||
// Update main checkbox
|
||||
mainCheckbox.checked = hasSelection;
|
||||
|
||||
// Update row disabled state
|
||||
row.classList.toggle('disabled', !hasSelection);
|
||||
|
||||
// Update context type checkboxes
|
||||
contextTypes.forEach(type => {
|
||||
const typeCheckbox = row.querySelector(`#check-${component.id}-${type}`);
|
||||
typeCheckbox.checked = selectedTypes.has(type);
|
||||
});
|
||||
});
|
||||
|
||||
updateTotalTokenCount();
|
||||
}
|
||||
|
||||
// Fetch token counts for all files
|
||||
async function fetchAllTokenCounts() {
|
||||
const promises = [];
|
||||
|
||||
components.forEach(component => {
|
||||
contextTypes.forEach(type => {
|
||||
promises.push(fetchTokenCount(component.id, type));
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
updateComponentUI();
|
||||
renderReferenceTable(); // Update reference table with token counts
|
||||
}
|
||||
|
||||
// Fetch token count for a specific file
|
||||
async function fetchTokenCount(componentId, type) {
|
||||
const key = `${componentId}-${type}`;
|
||||
|
||||
try {
|
||||
const fileName = getFileName(componentId, type);
|
||||
const baseUrl = getBaseUrl(type);
|
||||
const response = await fetch(baseUrl + fileName);
|
||||
|
||||
if (response.ok) {
|
||||
const content = await response.text();
|
||||
const tokens = estimateTokens(content);
|
||||
state.tokenCounts.set(key, tokens);
|
||||
|
||||
// Update UI
|
||||
const tokenSpan = document.getElementById(`tokens-${componentId}-${type}`);
|
||||
if (tokenSpan) {
|
||||
tokenSpan.textContent = `${tokens.toLocaleString()} tokens`;
|
||||
}
|
||||
} else if (type === 'examples') {
|
||||
// Examples might not exist yet
|
||||
state.tokenCounts.set(key, 0);
|
||||
const tokenSpan = document.getElementById(`tokens-${componentId}-${type}`);
|
||||
if (tokenSpan) {
|
||||
tokenSpan.textContent = '';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to fetch token count for ${componentId}-${type}`);
|
||||
if (type === 'examples') {
|
||||
const tokenSpan = document.getElementById(`tokens-${componentId}-${type}`);
|
||||
if (tokenSpan) {
|
||||
tokenSpan.textContent = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get file name based on component and type
|
||||
function getFileName(componentId, type) {
|
||||
// For new structure, all files are just [componentId].txt
|
||||
return `${componentId}.txt`;
|
||||
}
|
||||
|
||||
// Get base URL based on context type
|
||||
function getBaseUrl(type) {
|
||||
// For MkDocs, we need to go up to the root level
|
||||
const basePrefix = window.location.pathname.includes('/apps/') ? '../../' : '/';
|
||||
|
||||
switch(type) {
|
||||
case 'memory':
|
||||
return basePrefix + 'assets/llm.txt/txt/';
|
||||
case 'reasoning':
|
||||
return basePrefix + 'assets/llm.txt/diagrams/';
|
||||
case 'examples':
|
||||
return basePrefix + 'assets/llm.txt/examples/'; // Will return 404 for now
|
||||
default:
|
||||
return basePrefix + 'assets/llm.txt/txt/';
|
||||
}
|
||||
}
|
||||
|
||||
// Setup action button handlers
|
||||
function setupActionHandlers() {
|
||||
// Select/Deselect all buttons
|
||||
document.getElementById('select-all').addEventListener('click', () => {
|
||||
components.forEach(comp => {
|
||||
state.selectedComponents.add(comp.id);
|
||||
state.selectedContextTypes.set(comp.id, new Set(['memory', 'reasoning']));
|
||||
});
|
||||
updateComponentUI();
|
||||
});
|
||||
|
||||
document.getElementById('deselect-all').addEventListener('click', () => {
|
||||
state.selectedComponents.clear();
|
||||
state.selectedContextTypes.clear();
|
||||
updateComponentUI();
|
||||
});
|
||||
|
||||
// Download button
|
||||
document.getElementById('download-btn').addEventListener('click', handleDownload);
|
||||
}
|
||||
|
||||
// Setup column header click handlers
|
||||
function setupColumnHeaderHandlers() {
|
||||
const headers = document.querySelectorAll('.clickable-header');
|
||||
headers.forEach(header => {
|
||||
header.addEventListener('click', () => {
|
||||
const type = header.getAttribute('data-type');
|
||||
toggleColumnSelection(type);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle all checkboxes in a column
|
||||
function toggleColumnSelection(type) {
|
||||
// Don't toggle examples column
|
||||
if (type === 'examples') return;
|
||||
|
||||
// Check if all are currently selected
|
||||
let allSelected = true;
|
||||
components.forEach(comp => {
|
||||
const types = state.selectedContextTypes.get(comp.id);
|
||||
if (!types || !types.has(type)) {
|
||||
allSelected = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle all
|
||||
components.forEach(comp => {
|
||||
if (!state.selectedContextTypes.has(comp.id)) {
|
||||
state.selectedContextTypes.set(comp.id, new Set());
|
||||
}
|
||||
|
||||
const types = state.selectedContextTypes.get(comp.id);
|
||||
if (allSelected) {
|
||||
types.delete(type);
|
||||
} else {
|
||||
types.add(type);
|
||||
}
|
||||
|
||||
updateComponentSelection(comp.id);
|
||||
});
|
||||
|
||||
updateComponentUI();
|
||||
}
|
||||
|
||||
// Handle download action
|
||||
async function handleDownload() {
|
||||
const statusEl = document.getElementById('status');
|
||||
statusEl.textContent = 'Preparing context files...';
|
||||
statusEl.className = 'status loading';
|
||||
|
||||
try {
|
||||
const files = getSelectedFiles();
|
||||
if (files.length === 0) {
|
||||
throw new Error('No files selected. Please select at least one component or preset.');
|
||||
}
|
||||
|
||||
statusEl.textContent = `Fetching ${files.length} files...`;
|
||||
|
||||
const contents = await fetchFiles(files);
|
||||
const combined = combineContents(contents);
|
||||
|
||||
downloadFile(combined, 'crawl4ai_custom_context.md');
|
||||
|
||||
statusEl.textContent = 'Download complete!';
|
||||
statusEl.className = 'status success';
|
||||
|
||||
setTimeout(() => {
|
||||
statusEl.textContent = '';
|
||||
statusEl.className = 'status';
|
||||
}, 3000);
|
||||
|
||||
} catch (error) {
|
||||
statusEl.textContent = `Error: ${error.message}`;
|
||||
statusEl.className = 'status error';
|
||||
}
|
||||
}
|
||||
|
||||
// Get list of selected files based on current state
|
||||
function getSelectedFiles() {
|
||||
const files = [];
|
||||
|
||||
// Build list of selected files with their context info
|
||||
state.selectedComponents.forEach(compId => {
|
||||
const types = state.selectedContextTypes.get(compId);
|
||||
if (types) {
|
||||
types.forEach(type => {
|
||||
files.push({
|
||||
componentId: compId,
|
||||
type: type,
|
||||
fileName: getFileName(compId, type),
|
||||
baseUrl: getBaseUrl(type)
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
// Fetch multiple files
|
||||
async function fetchFiles(fileInfos) {
|
||||
const promises = fileInfos.map(async (fileInfo) => {
|
||||
try {
|
||||
const response = await fetch(fileInfo.baseUrl + fileInfo.fileName);
|
||||
if (!response.ok) {
|
||||
if (fileInfo.type === 'examples') {
|
||||
return {
|
||||
fileInfo,
|
||||
content: `<!-- Examples for ${fileInfo.componentId} coming soon -->\n\nExamples are currently being developed for this component.`
|
||||
};
|
||||
}
|
||||
console.warn(`Failed to fetch ${fileInfo.fileName} from ${fileInfo.baseUrl + fileInfo.fileName}`);
|
||||
return { fileInfo, content: `<!-- Failed to load ${fileInfo.fileName} -->` };
|
||||
}
|
||||
const content = await response.text();
|
||||
return { fileInfo, content };
|
||||
} catch (error) {
|
||||
if (fileInfo.type === 'examples') {
|
||||
return {
|
||||
fileInfo,
|
||||
content: `<!-- Examples for ${fileInfo.componentId} coming soon -->\n\nExamples are currently being developed for this component.`
|
||||
};
|
||||
}
|
||||
console.warn(`Error fetching ${fileInfo.fileName}:`, error);
|
||||
return { fileInfo, content: `<!-- Error loading ${fileInfo.fileName} -->` };
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
// Combine file contents with headers
|
||||
function combineContents(fileContents) {
|
||||
// Calculate total tokens
|
||||
let totalTokens = 0;
|
||||
fileContents.forEach(({ content }) => {
|
||||
totalTokens += estimateTokens(content);
|
||||
});
|
||||
|
||||
const header = `# Crawl4AI Custom LLM Context
|
||||
Generated on: ${new Date().toISOString()}
|
||||
Total files: ${fileContents.length}
|
||||
Estimated tokens: ${totalTokens.toLocaleString()}
|
||||
|
||||
---
|
||||
|
||||
`;
|
||||
|
||||
const sections = fileContents.map(({ fileInfo, content }) => {
|
||||
const component = components.find(c => c.id === fileInfo.componentId);
|
||||
const componentName = component ? component.name : fileInfo.componentId;
|
||||
const contextType = getContextTypeName(fileInfo.type);
|
||||
const tokens = estimateTokens(content);
|
||||
|
||||
return `## ${componentName} - ${contextType}
|
||||
Component ID: ${fileInfo.componentId}
|
||||
Context Type: ${fileInfo.type}
|
||||
Estimated tokens: ${tokens.toLocaleString()}
|
||||
|
||||
${content}
|
||||
|
||||
---
|
||||
|
||||
`;
|
||||
});
|
||||
|
||||
return header + sections.join('\n');
|
||||
}
|
||||
|
||||
// Get display name for context type
|
||||
function getContextTypeName(type) {
|
||||
switch(type) {
|
||||
case 'memory': return 'Full Content';
|
||||
case 'reasoning': return 'Diagrams & Workflows';
|
||||
case 'examples': return 'Code Examples';
|
||||
default: return type;
|
||||
}
|
||||
}
|
||||
|
||||
// Download file to user's computer
|
||||
function downloadFile(content, fileName) {
|
||||
const blob = new Blob([content], { type: 'text/markdown' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Render reference table
|
||||
function renderReferenceTable() {
|
||||
const tbody = document.getElementById('reference-table-body');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Get base path for links
|
||||
const basePrefix = window.location.pathname.includes('/apps/') ? '../../' : '/';
|
||||
|
||||
components.forEach(component => {
|
||||
const row = document.createElement('tr');
|
||||
const memoryTokens = state.tokenCounts.get(`${component.id}-memory`) || 0;
|
||||
const reasoningTokens = state.tokenCounts.get(`${component.id}-reasoning`) || 0;
|
||||
const examplesTokens = state.tokenCounts.get(`${component.id}-examples`) || 0;
|
||||
|
||||
row.innerHTML = `
|
||||
<td><strong>${component.name}</strong></td>
|
||||
<td>
|
||||
<a href="${basePrefix}assets/llm.txt/txt/${component.id}.txt" class="file-link" target="_blank">Memory</a>
|
||||
${memoryTokens > 0 ? `<span class="file-size">${memoryTokens.toLocaleString()} tokens</span>` : ''}
|
||||
</td>
|
||||
<td>
|
||||
<a href="${basePrefix}assets/llm.txt/diagrams/${component.id}.txt" class="file-link" target="_blank">Reasoning</a>
|
||||
${reasoningTokens > 0 ? `<span class="file-size">${reasoningTokens.toLocaleString()} tokens</span>` : ''}
|
||||
</td>
|
||||
<td>
|
||||
${examplesTokens > 0
|
||||
? `<a href="${basePrefix}assets/llm.txt/examples/${component.id}.txt" class="file-link" target="_blank">Examples</a>
|
||||
<span class="file-size">${examplesTokens.toLocaleString()} tokens</span>`
|
||||
: '-'
|
||||
}
|
||||
</td>
|
||||
<td>-</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Supercharging Your AI Assistant: My Journey to Better LLM Contexts for `crawl4ai`
|
||||
|
||||
When I started diving deep into using AI coding assistants with my own libraries, particularly `crawl4ai`, I quickly realized that the common approach to providing context via a simple `llm.txt` or even a beefed-up `README.md` just wasn't cutting it. This document explains the problems I encountered and how I've tried to create a more effective system for `crawl4ai`, allowing you (and your AI assistant) to get precisely the right information.
|
||||
|
||||
## My Frustration with Standard `llm.txt` Files
|
||||
|
||||
My experience with generic `llm.txt` files for complex libraries like `crawl4ai` revealed several pain points:
|
||||
|
||||
1. **Information Overload & Lost Focus:** I found that when I threw a massive, monolithic context file at an LLM, it often struggled. The sheer volume of information seemed to dilute its focus. If I asked a specific question about a niche feature, the LLM might get sidetracked by more prominent but currently irrelevant parts of the library. It felt like trying to find a single sentence in a thousand-page novel – the information was *there*, but not always accessible or prioritized correctly by the AI.
|
||||
|
||||
2. **The "What" Without the "How" or "Why":** Most `llm.txt` files I encountered were essentially API dumps – a list of functions, classes, and parameters. This is the "what" of a library. But to truly use a library effectively, especially one as flexible as `crawl4ai`, you need the "how" (idiomatic usage patterns, best practices for common tasks) and the "why" (the design rationale behind certain features). Without this, I noticed my AI assistant would often generate syntactically correct but practically inefficient or non-idiomatic code. It was guessing the *intent* and the *best way* to use the library, and those guesses weren't always right.
|
||||
|
||||
3. **No Guidance on "Thinking" Like an Expert:** A static list of facts doesn't teach an LLM the *art* of using the library. It doesn't convey the trade-offs an experienced developer considers, the common pitfalls they've learned to avoid, or the clever ways to combine features to solve complex problems. I wanted my AI assistant to not just recall an API, but to help me *reason* about the best way to build a solution with `crawl4ai`.
|
||||
|
||||
## Inspiration: Selective Inclusion & Multi-Dimensional Understanding
|
||||
|
||||
I've always admired how libraries like Lodash or jQuery (in its modular days) allowed developers to pick and choose only the parts they needed, resulting in smaller, more focused bundles. This idea of modularity and selective inclusion resonated deeply with me as I thought about LLM context. Why force-feed an LLM the entire library's details when I'm only working on a specific component or task?
|
||||
|
||||
This led me to develop a new approach for `crawl4ai`: **multi-dimensional, modular contexts**.
|
||||
|
||||
Instead of one giant `llm.txt`, I've broken down the `crawl4ai` documentation into:
|
||||
|
||||
1. **Logical Components:** Context is organized around the major functional areas of the library (e.g., Core, Data Extraction, Deep Crawling, Markdown Generation, etc.). This allows you to select context relevant only to the task at hand.
|
||||
2. **Three Dimensions of Context for Each Component:**
|
||||
* **`_memory.md` (Foundational Memory):** This is the "what." It contains the precise, factual information about the component's public API, data structures, configuration objects, parameters, and method signatures. It's the detailed, unambiguous reference.
|
||||
* **`_reasoning.md` (Reasoning & Problem-Solving Framework):** This is the "how" and "why." It includes design principles, common task workflows with decision guides, best practices, anti-patterns, illustrative code examples solving real problems, and explanations of trade-offs. It aims to guide the LLM in "thinking" like an expert `crawl4ai` user.
|
||||
* **`_examples.md` (Practical Code Examples):** This is pure "show-me-the-code." It's a collection of runnable snippets demonstrating various ways to use the component's features and configurations, with minimal explanatory text. It’s for quickly seeing different patterns in action.
|
||||
|
||||
**The Goal:**
|
||||
My aim is to provide you with a flexible system. You can give your AI assistant:
|
||||
* Just the **memory** files for quick API lookups.
|
||||
* The **reasoning** files (perhaps with memory) for help designing solutions.
|
||||
* The **examples** files for seeing practical implementations.
|
||||
* A **combination** of these across one or more components tailored to your specific task.
|
||||
* Or, for broader understanding, special aggregate contexts like the "Vibe Coding" context or the "All Library Context."
|
||||
|
||||
By providing these structured, multi-faceted contexts, I hope to significantly improve the quality and relevance of the assistance you get when using AI to code with `crawl4ai`. The following sections will guide you on how to select and use these different context files.
|
||||