fix CI.
This commit is contained in:
parent
5c3284e25f
commit
11ea023eec
19 changed files with 3048 additions and 38 deletions
BIN
.forgejo/workflows/.botserver.yaml.swp
Normal file
BIN
.forgejo/workflows/.botserver.yaml.swp
Normal file
Binary file not shown.
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
CARGO_TARGET_DIR: /opt/gbo/work/target
|
||||
RUSTC_WRAPPER: ""
|
||||
PATH: /home/gbuser/.cargo/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/sbin:/bin
|
||||
STAGE_SYSTEM_HOST: "10.0.3.10"
|
||||
STAGE_SYSTEM_HOST: "system"
|
||||
SYSTEM_USER: gbuser
|
||||
|
||||
steps:
|
||||
|
|
@ -29,14 +29,12 @@ jobs:
|
|||
git clean -fd
|
||||
git pull
|
||||
git submodule update --init --recursive
|
||||
mkdir -p /opt/gbo/work/target
|
||||
mkdir -p /opt/gbo/bin
|
||||
|
||||
- name: Build BotServer and BotUI
|
||||
run: |
|
||||
cd /opt/gbo/work/generalbots
|
||||
CARGO_BUILD_JOBS=4 cargo build -p botserver --bin botserver
|
||||
CARGO_BUILD_JOBS=4 cargo build -p botui --bin botui
|
||||
CARGO_BUILD_JOBS=6 RUST_WRAPPER=/usr/local/bin/sccache cargo build -p botserver --bin botserver
|
||||
CARGO_BUILD_JOBS=6 RUST_WRAPPER=/usr/local/bin/sccache cargo build -p botui --bin botui
|
||||
|
||||
- name: Deploy to Stage
|
||||
run: |
|
||||
|
|
@ -44,26 +42,22 @@ jobs:
|
|||
|
||||
# Copy both binaries to stage system container
|
||||
scp -i /home/gbuser/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
|
||||
/opt/gbo/work/target/debug/botserver \
|
||||
/opt/gbo/work/generalbots/target/debug/botserver \
|
||||
${SYSTEM_USER}@${STAGE_SYSTEM_HOST}:/opt/gbo/bin/botserver-new
|
||||
|
||||
scp -i /home/gbuser/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
|
||||
/opt/gbo/work/target/debug/botui \
|
||||
/opt/gbo/work/generalbots/target/debug/botui \
|
||||
${SYSTEM_USER}@${STAGE_SYSTEM_HOST}:/opt/gbo/bin/botui-new
|
||||
|
||||
# Restart services on stage
|
||||
ssh -i /home/gbuser/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
|
||||
${SYSTEM_USER}@${STAGE_SYSTEM_HOST} "\
|
||||
|
||||
sudo systemctl stop botserver && sudo systemctl stop botui
|
||||
sudo killall botui -9 & sudo killall botserver -9
|
||||
sudo mv /opt/gbo/bin/botserver-new /opt/gbo/bin/botserver && \
|
||||
sudo mv /opt/gbo/bin/botui-new /opt/gbo/bin/botui && \
|
||||
sudo chmod +x /opt/gbo/bin/botserver /opt/gbo/bin/botui && \
|
||||
sudo systemctl restart botserver && \
|
||||
sudo systemctl restart ui"
|
||||
sudo systemctl start botserver && \
|
||||
sudo systemctl start ui"
|
||||
|
||||
sleep 10
|
||||
|
||||
# Health checks
|
||||
ssh -i /home/gbuser/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
|
||||
${SYSTEM_USER}@${STAGE_SYSTEM_HOST} "\
|
||||
curl -sf http://localhost:8080/health && echo '✅ BotServer OK' || echo '❌ BotServer FAILED'; \
|
||||
curl -sf http://localhost:3000/ && echo '✅ BotUI OK' || echo '❌ BotUI FAILED'"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ The `.gbkb` package contains your bot's domain knowledge - documents that the AI
|
|||
|
||||
When you place documents in a `.gbkb` folder, the system automatically:
|
||||
|
||||
1. **Extracts text** from your files (PDF, DOCX, TXT, MD, HTML, CSV)
|
||||
1. **Extracts text** from your files (PDF, DOCX, XLSX, PPTX, EPUB, TXT, MD, HTML, CSV, JSON, YAML, TOML, and more)
|
||||
2. **Creates searchable indexes** using vector embeddings
|
||||
3. **Enables semantic search** so users can ask questions naturally
|
||||
|
||||
|
|
@ -45,13 +45,20 @@ CLEAR KB "policies"
|
|||
|
||||
## Supported File Types
|
||||
|
||||
| Format | Extensions |
|
||||
|--------|------------|
|
||||
| PDF | `.pdf` |
|
||||
| Word | `.docx`, `.doc` |
|
||||
| Text | `.txt`, `.md` |
|
||||
| Web | `.html` |
|
||||
| Data | `.csv`, `.json` |
|
||||
| Category | Formats | Extensions |
|
||||
|----------|---------|------------|
|
||||
| **Documents** | PDF, Word | `.pdf`, `.docx`, `.doc` |
|
||||
| **Spreadsheets** | Excel, OpenDocument | `.xlsx`, `.xls`, `.ods` |
|
||||
| **Presentations** | PowerPoint, OpenDocument | `.pptx`, `.ppt`, `.odp` |
|
||||
| **E-books** | EPUB, OpenDocument Text | `.epub`, `.odt` |
|
||||
| **Text** | Plain, Markdown, reStructuredText, AsciiDoc | `.txt`, `.md`, `.rst`, `.adoc` |
|
||||
| **Web** | HTML | `.html` |
|
||||
| **Data** | CSV, JSON, JSONL, TSV | `.csv`, `.json`, `.jsonl`, `.tsv` |
|
||||
| **Config** | YAML, TOML, INI, Properties | `.yaml`, `.yml`, `.toml`, `.ini`, `.conf`, `.cfg`, `.env`, `.properties` |
|
||||
| **Code** | Python, Rust, JS/TS, Shell, SQL, GraphQL, Proto | `.py`, `.rs`, `.js`, `.ts`, `.sh`, `.sql`, `.graphql`, `.proto` |
|
||||
| **Style** | CSS, SVG | `.css`, `.svg` |
|
||||
| **Calendar** | iCalendar, vCard, Email | `.ics`, `.vcf`, `.eml` |
|
||||
| **Logs** | Log files | `.log` |
|
||||
|
||||
## Key Points
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ Document → Extract Text → Chunk → Embed → Store in Qdrant
|
|||
|
||||
| Stage | Description |
|
||||
|-------|-------------|
|
||||
| **Extract** | Pull text from PDF, DOCX, HTML, MD, TXT, CSV |
|
||||
| **Extract** | Pull text from PDF, DOCX, DOC, XLSX, XLS, ODS, PPTX, PPT, ODP, EPUB, ODT, HTML, MD, TXT, CSV, JSON, YAML, TOML, and more |
|
||||
| **Chunk** | Split into ~500 token segments with 50 token overlap |
|
||||
| **Embed** | Generate vectors using BGE model |
|
||||
| **Store** | Save to Qdrant with metadata |
|
||||
|
|
@ -28,10 +28,20 @@ Document → Extract Text → Chunk → Embed → Store in Qdrant
|
|||
| Format | Notes |
|
||||
|--------|-------|
|
||||
| PDF | Full text extraction, OCR for scanned docs |
|
||||
| DOCX | Microsoft Word documents |
|
||||
| TXT/MD | Plain text and Markdown |
|
||||
| DOCX/DOC | Microsoft Word documents |
|
||||
| XLSX/XLS/ODS | Spreadsheets (Excel, OpenDocument) — each row indexed |
|
||||
| PPTX/PPT/ODP | Presentations (PowerPoint, OpenDocument) — slide text extracted |
|
||||
| EPUB/ODT | E-books and OpenDocument text |
|
||||
| TXT/MD/RST/ADOC | Plain text, Markdown, reStructuredText, AsciiDoc |
|
||||
| HTML | Web pages (text only) |
|
||||
| CSV/JSON | Structured data |
|
||||
| CSV/TSV | Tabular data — each row indexed separately |
|
||||
| JSON/JSONL | Structured data |
|
||||
| YAML/TOML/INI | Configuration files |
|
||||
| PY/RS/JS/TS/SH/SQL | Source code files |
|
||||
| CSS/SVG | Style and vector graphics |
|
||||
| ICS/VCF/EML | Calendar, contacts, email |
|
||||
| LOG | Log files |
|
||||
| Any `text/*` MIME | Catch-all for any text-based format (max 100MB) |
|
||||
|
||||
## Website Indexing
|
||||
|
||||
|
|
|
|||
|
|
@ -33,11 +33,11 @@ work/
|
|||
|
||||
### KB Loading Process
|
||||
|
||||
When you load a knowledge base, the system goes through several stages to make your documents searchable. First, the system scans the specified `.gbkb` folder to identify all documents. Then it processes each file by extracting text from PDFs, Word documents, text files, markdown, CSV files, and other supported formats. The extracted text is split into chunks of approximately 1000 characters with overlap between chunks to preserve context at boundaries. Each chunk is then converted into a vector representation using an embedding model. These vectors are stored in the vector database with metadata about their source, enabling fast similarity search. Once this process completes, the knowledge base is ready to answer semantic queries.
|
||||
When you load a knowledge base, the system goes through several stages to make your documents searchable. First, the system scans the specified `.gbkb` folder to identify all documents. Then it processes each file by extracting text using format-specific parsers — PDF, DOCX, DOC, XLSX, XLS, ODS, PPTX, PPT, ODP, EPUB, ODT, TXT, MD, RST, AsciiDoc, HTML, CSV, TSV, JSON, JSONL, YAML, TOML, source code, and any other text-based format. The extracted text is split into chunks of approximately 1000 characters with overlap between chunks to preserve context at boundaries. Each chunk is then converted into a vector representation using an embedding model. These vectors are stored in the vector database with metadata about their source, enabling fast similarity search. Once this process completes, the knowledge base is ready to answer semantic queries.
|
||||
|
||||
### Supported File Types
|
||||
|
||||
The system supports a variety of document formats. PDF files receive full text extraction using the pdf-extract library. Microsoft Word documents in both DOCX and DOC formats are supported. Plain text files and markdown documents are processed directly. CSV files treat each row as a separate searchable entry. HTML files have their text content extracted while ignoring markup. JSON files are parsed and their structured data becomes searchable.
|
||||
The system supports a wide variety of document and text formats. PDF files receive full text extraction using the pdf-extract library. Microsoft Word documents in both DOCX and DOC formats are supported. Spreadsheets in XLSX, XLS, and OpenDocument ODS formats have each row indexed as a separate searchable entry. Presentations in PPTX, PPT, and ODP formats have slide text extracted. E-books in EPUB and OpenDocument ODT formats are fully processed. Plain text, Markdown, reStructuredText, and AsciiDoc files are processed directly. CSV and TSV files treat each row as a separate searchable entry. HTML files have their text content extracted while ignoring markup. JSON and JSONL files are parsed and their structured data becomes searchable. YAML and TOML configuration files are indexed as text. Source code files (Python, Rust, JavaScript, TypeScript, Shell, SQL, GraphQL, Proto) are indexed as plain text. CSS and SVG files are supported. Calendar (ICS), contact (VCF), and email (EML) files are indexed. Log files are supported. Any file with a `text/*` MIME type is automatically indexed as a catch-all, up to 100MB per file.
|
||||
|
||||
### USE KB Keyword
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ The Knowledge Base (KB) system enables semantic search and document retrieval fo
|
|||
|---------|-------------|
|
||||
| **Storage** | S3-compatible drive + PostgreSQL metadata + Qdrant vectors |
|
||||
| **Search** | Hybrid (semantic + keyword) with optional reranking |
|
||||
| **Formats** | PDF, DOCX, TXT, MD, HTML, CSV, JSON |
|
||||
| **Formats** | PDF, DOCX, DOC, XLSX, XLS, ODS, PPTX, PPT, ODP, EPUB, ODT, TXT, MD, HTML, CSV, JSON, YAML, TOML, and 30+ text-based formats |
|
||||
| **Integration** | Automatic context injection into LLM responses |
|
||||
|
||||
## Basic Usage
|
||||
|
|
|
|||
|
|
@ -73,7 +73,20 @@ bot.gbkb/
|
|||
|
||||
## Supported File Types
|
||||
|
||||
PDF, DOCX, TXT, MD, HTML, CSV, JSON
|
||||
| Category | Formats | Extensions |
|
||||
|----------|---------|------------|
|
||||
| Documents | PDF, Word | `.pdf`, `.docx`, `.doc` |
|
||||
| Spreadsheets | Excel, OpenDocument | `.xlsx`, `.xls`, `.ods` |
|
||||
| Presentations | PowerPoint, OpenDocument | `.pptx`, `.ppt`, `.odp` |
|
||||
| E-books | EPUB, OpenDocument Text | `.epub`, `.odt` |
|
||||
| Text | Plain, Markdown, reStructuredText, AsciiDoc | `.txt`, `.md`, `.rst`, `.adoc` |
|
||||
| Web | HTML | `.html` |
|
||||
| Data | CSV, JSON, JSONL, TSV | `.csv`, `.json`, `.jsonl`, `.tsv` |
|
||||
| Config | YAML, TOML, INI, Properties | `.yaml`, `.yml`, `.toml`, `.ini`, `.conf`, `.cfg`, `.env`, `.properties` |
|
||||
| Code | Python, Rust, JS/TS, Shell, SQL, GraphQL, Proto | `.py`, `.rs`, `.js`, `.ts`, `.sh`, `.sql`, `.graphql`, `.proto` |
|
||||
| Style | CSS, SVG | `.css`, `.svg` |
|
||||
| Calendar | iCalendar, vCard, Email | `.ics`, `.vcf`, `.eml` |
|
||||
| Logs | Log files | `.log` |
|
||||
|
||||
## Performance
|
||||
|
||||
|
|
|
|||
|
|
@ -50,11 +50,25 @@ A **Knowledge Base (KB)** is a collection of documents that your bot uses to ans
|
|||
|--------|-----------|----------|
|
||||
| **PDF** | `.pdf` | Manuals, reports, official documents |
|
||||
| **Word** | `.docx`, `.doc` | Policies, procedures, articles |
|
||||
| **Excel** | `.xlsx`, `.xls` | FAQs, structured data, tabular content |
|
||||
| **OpenDocument Spreadsheet** | `.ods` | Open-source spreadsheet data |
|
||||
| **PowerPoint** | `.pptx`, `.ppt` | Training materials, slide decks |
|
||||
| **OpenDocument Presentation** | `.odp` | Open-source presentations |
|
||||
| **E-book** | `.epub` | Books, long-form guides |
|
||||
| **OpenDocument Text** | `.odt` | Open-source word processing |
|
||||
| **Text** | `.txt` | Simple content, FAQs |
|
||||
| **Markdown** | `.md` | Technical documentation |
|
||||
| **Excel** | `.xlsx`, `.xls` | FAQs, structured data |
|
||||
| **PowerPoint** | `.pptx` | Training materials |
|
||||
| **reStructuredText** | `.rst` | Python/docs project documentation |
|
||||
| **AsciiDoc** | `.adoc` | Technical publishing |
|
||||
| **HTML** | `.html` | Web content |
|
||||
| **CSV / TSV** | `.csv`, `.tsv` | Tabular data, exports |
|
||||
| **JSON / JSONL** | `.json`, `.jsonl` | Structured data, API responses |
|
||||
| **YAML** | `.yaml`, `.yml` | Configuration, structured content |
|
||||
| **TOML** | `.toml` | Configuration files |
|
||||
| **Source Code** | `.py`, `.rs`, `.js`, `.ts`, `.sh`, `.sql`, `.graphql`, `.proto` | Code documentation, technical references |
|
||||
| **CSS / SVG** | `.css`, `.svg` | Style references, vector graphics text |
|
||||
| **Calendar / Contacts / Email** | `.ics`, `.vcf`, `.eml` | Scheduling, address books, message archives |
|
||||
| **Config / Log** | `.ini`, `.conf`, `.cfg`, `.env`, `.properties`, `.log` | System configuration, diagnostics |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -475,10 +489,12 @@ HEAR question
|
|||
|
||||
| File Type | Max Size |
|
||||
|-----------|----------|
|
||||
| PDF | 50 MB |
|
||||
| Word | 25 MB |
|
||||
| Excel | 25 MB |
|
||||
| Text/MD | 10 MB |
|
||||
| PDF | 100 MB |
|
||||
| Word / EPUB / ODT | 100 MB |
|
||||
| Excel / ODS | 100 MB |
|
||||
| PowerPoint / ODP | 100 MB |
|
||||
| Text / MD / Code / Config | 100 MB |
|
||||
| Any text-based format | 100 MB |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
67
botui/ui/suite/designer/designer-canvas.js
Normal file
67
botui/ui/suite/designer/designer-canvas.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
function initCanvasInteraction() {
|
||||
const canvas = document.getElementById('canvas');
|
||||
const container = document.getElementById('canvas-container');
|
||||
|
||||
if (!canvas || !container) {
|
||||
console.warn('initCanvasInteraction: canvas or canvas-container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
let isPanning = false;
|
||||
let panStart = { x: 0, y: 0 };
|
||||
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
if (e.button === 1 || (e.button === 0 && e.target === canvas)) {
|
||||
isPanning = true;
|
||||
panStart = { x: e.clientX - state.pan.x, y: e.clientY - state.pan.y };
|
||||
canvas.style.cursor = 'grabbing';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (isPanning) {
|
||||
state.pan.x = e.clientX - panStart.x;
|
||||
state.pan.y = e.clientY - panStart.y;
|
||||
updateCanvasTransform();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isPanning = false;
|
||||
canvas.style.cursor = 'default';
|
||||
});
|
||||
|
||||
container.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
||||
const newZoom = Math.min(Math.max(state.zoom + delta, 0.25), 2);
|
||||
state.zoom = newZoom;
|
||||
updateCanvasTransform();
|
||||
updateZoomDisplay();
|
||||
});
|
||||
}
|
||||
|
||||
function updateCanvasTransform() {
|
||||
const inner = document.getElementById('canvas-inner');
|
||||
inner.style.transform = `translate(${state.pan.x}px, ${state.pan.y}px) scale(${state.zoom})`;
|
||||
}
|
||||
|
||||
function updateZoomDisplay() {
|
||||
document.getElementById('zoom-value').textContent = Math.round(state.zoom * 100) + '%';
|
||||
}
|
||||
|
||||
function snapToGrid(value, gridSize = 20) {
|
||||
return Math.round(value / gridSize) * gridSize;
|
||||
}
|
||||
|
||||
function zoomIn() {
|
||||
state.zoom = Math.min(state.zoom + 0.1, 2);
|
||||
updateCanvasTransform();
|
||||
updateZoomDisplay();
|
||||
}
|
||||
|
||||
function zoomOut() {
|
||||
state.zoom = Math.max(state.zoom - 0.1, 0.25);
|
||||
updateCanvasTransform();
|
||||
updateZoomDisplay();
|
||||
}
|
||||
70
botui/ui/suite/designer/designer-connections.js
Normal file
70
botui/ui/suite/designer/designer-connections.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
function startConnection(nodeId, portType) {
|
||||
state.isConnecting = true;
|
||||
state.connectionStart = { nodeId, portType };
|
||||
}
|
||||
|
||||
function endConnection(nodeId, portType) {
|
||||
if (!state.isConnecting || !state.connectionStart) return;
|
||||
if (state.connectionStart.nodeId === nodeId) {
|
||||
state.isConnecting = false;
|
||||
state.connectionStart = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.connectionStart.portType === 'input' || portType !== 'input') {
|
||||
state.isConnecting = false;
|
||||
state.connectionStart = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const connection = {
|
||||
from: state.connectionStart.nodeId,
|
||||
fromPort: state.connectionStart.portType,
|
||||
to: nodeId,
|
||||
toPort: portType
|
||||
};
|
||||
|
||||
state.connections.push(connection);
|
||||
state.isConnecting = false;
|
||||
state.connectionStart = null;
|
||||
updateConnections();
|
||||
saveToHistory();
|
||||
}
|
||||
|
||||
function updateConnections() {
|
||||
const svg = document.getElementById('connections-svg');
|
||||
let paths = '';
|
||||
|
||||
state.connections.forEach((conn, index) => {
|
||||
const fromEl = document.getElementById(conn.from);
|
||||
const toEl = document.getElementById(conn.to);
|
||||
if (!fromEl || !toEl) return;
|
||||
|
||||
const fromPort = fromEl.querySelector(`[data-port="${conn.fromPort}"]`);
|
||||
const toPort = toEl.querySelector(`[data-port="${conn.toPort}"]`);
|
||||
if (!fromPort || !toPort) return;
|
||||
|
||||
const fromRect = fromPort.getBoundingClientRect();
|
||||
const toRect = toPort.getBoundingClientRect();
|
||||
const canvasRect = document.getElementById('canvas-inner').getBoundingClientRect();
|
||||
|
||||
const x1 = (fromRect.left + fromRect.width / 2 - canvasRect.left) / state.zoom;
|
||||
const y1 = (fromRect.top + fromRect.height / 2 - canvasRect.top) / state.zoom;
|
||||
const x2 = (toRect.left + toRect.width / 2 - canvasRect.left) / state.zoom;
|
||||
const y2 = (toRect.top + toRect.height / 2 - canvasRect.top) / state.zoom;
|
||||
|
||||
const midX = (x1 + x2) / 2;
|
||||
const d = `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`;
|
||||
|
||||
paths += `<path class="connection" d="${d}" data-index="${index}"/>`;
|
||||
});
|
||||
|
||||
svg.innerHTML = `
|
||||
<defs>
|
||||
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="var(--primary)"/>
|
||||
</marker>
|
||||
</defs>
|
||||
${paths}
|
||||
`;
|
||||
}
|
||||
38
botui/ui/suite/designer/designer-history.js
Normal file
38
botui/ui/suite/designer/designer-history.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
function saveToHistory() {
|
||||
const snapshot = {
|
||||
nodes: Array.from(state.nodes.entries()).map(([id, node]) => ({...node})),
|
||||
connections: [...state.connections]
|
||||
};
|
||||
state.history = state.history.slice(0, state.historyIndex + 1);
|
||||
state.history.push(snapshot);
|
||||
state.historyIndex = state.history.length - 1;
|
||||
}
|
||||
|
||||
function undo() {
|
||||
if (state.historyIndex > 0) {
|
||||
state.historyIndex--;
|
||||
restoreSnapshot(state.history[state.historyIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
function redo() {
|
||||
if (state.historyIndex < state.history.length - 1) {
|
||||
state.historyIndex++;
|
||||
restoreSnapshot(state.history[state.historyIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
function restoreSnapshot(snapshot) {
|
||||
document.getElementById('canvas-inner').innerHTML = '';
|
||||
state.nodes.clear();
|
||||
state.connections = [];
|
||||
|
||||
snapshot.nodes.forEach(node => {
|
||||
state.nodes.set(node.id, {...node});
|
||||
renderNode(node);
|
||||
});
|
||||
|
||||
state.connections = [...snapshot.connections];
|
||||
updateConnections();
|
||||
updateStatusBar();
|
||||
}
|
||||
377
botui/ui/suite/designer/designer-io.js
Normal file
377
botui/ui/suite/designer/designer-io.js
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
function showModal(id) {
|
||||
const modal = document.getElementById(id);
|
||||
if (modal) {
|
||||
modal.classList.add('visible');
|
||||
if (id === 'open-modal') {
|
||||
htmx.trigger('#file-list-content', 'load');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hideModal(id) {
|
||||
const modal = document.getElementById(id);
|
||||
if (modal) {
|
||||
modal.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
function saveDesign() {
|
||||
const nodesData = Array.from(state.nodes.values());
|
||||
document.getElementById('nodes-data').value = JSON.stringify(nodesData);
|
||||
document.getElementById('connections-data').value = JSON.stringify(state.connections);
|
||||
|
||||
if (state.driveSource) {
|
||||
saveToDrive();
|
||||
} else {
|
||||
htmx.ajax('POST', '/api/designer/save', {
|
||||
source: document.getElementById('designer-data'),
|
||||
target: '#status-message'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function saveToDrive() {
|
||||
const basCode = generateBasCode();
|
||||
const { bucket, path } = state.driveSource;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/files/write', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ bucket, path, content: basCode })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const statusEl = document.querySelector('.status-item span');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `Saved: ${path.split('/').pop()}`;
|
||||
}
|
||||
} else {
|
||||
const err = await response.json();
|
||||
alert(`Save failed: ${err.error || 'Unknown error'}`);
|
||||
}
|
||||
} catch (e) {
|
||||
alert(`Save failed: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function generateBasCode() {
|
||||
let basCode = "' Generated by General Bots Designer\n";
|
||||
basCode += "' " + new Date().toISOString() + "\n\n";
|
||||
|
||||
const sortedNodes = Array.from(state.nodes.values()).sort((a, b) => {
|
||||
if (Math.abs(a.y - b.y) < 30) return a.x - b.x;
|
||||
return a.y - b.y;
|
||||
});
|
||||
|
||||
sortedNodes.forEach(node => {
|
||||
switch (node.type) {
|
||||
case 'TALK':
|
||||
basCode += `TALK "${node.fields.message || ''}"\n`;
|
||||
break;
|
||||
case 'HEAR':
|
||||
basCode += `HEAR ${node.fields.variable || 'input'} AS ${node.fields.type || 'string'}\n`;
|
||||
break;
|
||||
case 'SET':
|
||||
basCode += `SET ${node.fields.variable || 'x'} = ${node.fields.expression || '0'}\n`;
|
||||
break;
|
||||
case 'IF':
|
||||
basCode += `IF ${node.fields.condition || 'true'} THEN\n`;
|
||||
break;
|
||||
case 'FOR':
|
||||
basCode += `FOR EACH ${node.fields.variable || 'item'} IN ${node.fields.collection || 'items'}\n`;
|
||||
break;
|
||||
case 'CALL':
|
||||
basCode += `CALL ${node.fields.procedure || 'sub'}(${node.fields.arguments || ''})\n`;
|
||||
break;
|
||||
case 'SEND MAIL':
|
||||
basCode += `SEND MAIL TO "${node.fields.to || ''}" SUBJECT "${node.fields.subject || ''}" BODY "${node.fields.body || ''}"\n`;
|
||||
break;
|
||||
case 'GET':
|
||||
basCode += `GET ${node.fields.url || 'url'} TO ${node.fields.variable || 'result'}\n`;
|
||||
break;
|
||||
case 'POST':
|
||||
basCode += `POST ${node.fields.url || 'url'} WITH ${node.fields.body || '{}'} TO ${node.fields.variable || 'result'}\n`;
|
||||
break;
|
||||
case 'SAVE':
|
||||
basCode += `SAVE ${node.fields.data || 'data'} TO "${node.fields.filename || 'file.txt'}"\n`;
|
||||
break;
|
||||
case 'WAIT':
|
||||
basCode += `WAIT ${node.fields.duration || '1000'}\n`;
|
||||
break;
|
||||
case 'SET BOT MEMORY':
|
||||
basCode += `SET BOT MEMORY "${node.fields.key || 'key'}", ${node.fields.value || '""'}\n`;
|
||||
break;
|
||||
case 'GET BOT MEMORY':
|
||||
basCode += `GET BOT MEMORY "${node.fields.key || 'key'}" TO ${node.fields.variable || 'value'}\n`;
|
||||
break;
|
||||
case 'SET USER MEMORY':
|
||||
basCode += `SET USER MEMORY "${node.fields.key || 'key'}", ${node.fields.value || '""'}\n`;
|
||||
break;
|
||||
case 'GET USER MEMORY':
|
||||
basCode += `GET USER MEMORY "${node.fields.key || 'key'}" TO ${node.fields.variable || 'value'}\n`;
|
||||
break;
|
||||
case 'SWITCH':
|
||||
basCode += `SWITCH ${node.fields.expression || 'value'}\n`;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return basCode;
|
||||
}
|
||||
|
||||
function exportToBas() {
|
||||
let basCode = "' Generated by General Bots Designer\n";
|
||||
basCode += "' " + new Date().toISOString() + "\n\n";
|
||||
|
||||
const sortedNodes = Array.from(state.nodes.values()).sort((a, b) => {
|
||||
if (Math.abs(a.y - b.y) < 30) return a.x - b.x;
|
||||
return a.y - b.y;
|
||||
});
|
||||
|
||||
sortedNodes.forEach(node => {
|
||||
const template = nodeTemplates[node.type];
|
||||
switch (node.type) {
|
||||
case 'TALK':
|
||||
basCode += `TALK "${node.fields.message}"\n`;
|
||||
break;
|
||||
case 'HEAR':
|
||||
basCode += `HEAR ${node.fields.variable} AS ${node.fields.type}\n`;
|
||||
break;
|
||||
case 'SET':
|
||||
basCode += `SET ${node.fields.variable} = ${node.fields.expression}\n`;
|
||||
break;
|
||||
case 'IF':
|
||||
basCode += `IF ${node.fields.condition} THEN\n`;
|
||||
break;
|
||||
case 'FOR':
|
||||
basCode += `FOR EACH ${node.fields.variable} IN ${node.fields.collection}\n`;
|
||||
break;
|
||||
case 'CALL':
|
||||
basCode += `CALL ${node.fields.procedure}(${node.fields.arguments})\n`;
|
||||
break;
|
||||
case 'SEND MAIL':
|
||||
basCode += `SEND MAIL TO "${node.fields.to}" SUBJECT "${node.fields.subject}" BODY "${node.fields.body}"\n`;
|
||||
break;
|
||||
case 'GET':
|
||||
basCode += `GET ${node.fields.url} TO ${node.fields.variable}\n`;
|
||||
break;
|
||||
case 'POST':
|
||||
basCode += `POST ${node.fields.url} WITH ${node.fields.body} TO ${node.fields.variable}\n`;
|
||||
break;
|
||||
case 'SAVE':
|
||||
basCode += `SAVE ${node.fields.data} TO "${node.fields.filename}"\n`;
|
||||
break;
|
||||
case 'WAIT':
|
||||
basCode += `WAIT ${node.fields.duration}\n`;
|
||||
break;
|
||||
case 'SET BOT MEMORY':
|
||||
basCode += `SET BOT MEMORY "${node.fields.key}", ${node.fields.value}\n`;
|
||||
break;
|
||||
case 'GET BOT MEMORY':
|
||||
basCode += `GET BOT MEMORY "${node.fields.key}" AS ${node.fields.variable}\n`;
|
||||
break;
|
||||
case 'SET USER MEMORY':
|
||||
basCode += `SET USER MEMORY "${node.fields.key}", ${node.fields.value}\n`;
|
||||
break;
|
||||
case 'GET USER MEMORY':
|
||||
basCode += `GET USER MEMORY "${node.fields.key}" AS ${node.fields.variable}\n`;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const blob = new Blob([basCode], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = (document.getElementById('current-filename').value || 'dialog') + '.bas';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function newDesign() {
|
||||
if (state.nodes.size > 0) {
|
||||
if (!confirm('Clear current design? Unsaved changes will be lost.')) return;
|
||||
}
|
||||
document.getElementById('canvas-inner').innerHTML = '';
|
||||
state.nodes.clear();
|
||||
state.connections = [];
|
||||
state.selectedNode = null;
|
||||
state.history = [];
|
||||
state.historyIndex = -1;
|
||||
state.nextNodeId = 1;
|
||||
document.getElementById('current-filename').value = '';
|
||||
document.getElementById('file-name').textContent = 'Untitled';
|
||||
updateConnections();
|
||||
updatePropertiesPanel();
|
||||
updateStatusBar();
|
||||
}
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const fileItem = e.target.closest('.file-item');
|
||||
if (fileItem) {
|
||||
document.querySelectorAll('.file-item').forEach(f => f.classList.remove('selected'));
|
||||
fileItem.classList.add('selected');
|
||||
document.getElementById('selected-file').value = fileItem.dataset.path;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadFromUrlParams() {
|
||||
let bucket = null;
|
||||
let path = null;
|
||||
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
bucket = queryParams.get('bucket');
|
||||
path = queryParams.get('path');
|
||||
|
||||
if (!bucket || !path) {
|
||||
const hash = window.location.hash;
|
||||
const hashQueryIndex = hash.indexOf('?');
|
||||
if (hashQueryIndex !== -1) {
|
||||
const hashParams = new URLSearchParams(hash.substring(hashQueryIndex + 1));
|
||||
bucket = bucket || hashParams.get('bucket');
|
||||
path = path || hashParams.get('path');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('loadFromUrlParams called:', { bucket, path, hash: window.location.hash, search: window.location.search });
|
||||
|
||||
if (bucket && path) {
|
||||
const fileName = path.split('/').pop() || 'dialog.bas';
|
||||
document.getElementById('current-filename').value = path;
|
||||
document.getElementById('selected-file').value = path;
|
||||
|
||||
state.driveSource = { bucket, path };
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/files/read', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ bucket, path })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load file: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const content = data.content || '';
|
||||
console.log('Loaded .bas content:', content.substring(0, 200) + '...');
|
||||
|
||||
parseBasicCodeToNodes(content);
|
||||
updateStatusBar();
|
||||
|
||||
const statusEl = document.querySelector('.status-item span');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `Loaded: ${fileName}`;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load .bas file:', err);
|
||||
alert(`Failed to load file: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseBasicCodeToNodes(content) {
|
||||
console.log('parseBasicCodeToNodes called');
|
||||
state.nodes.clear();
|
||||
state.connections = [];
|
||||
state.nextNodeId = 1;
|
||||
|
||||
const lines = content.split('\n');
|
||||
let yPos = 100;
|
||||
let nodeCount = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("'")) continue;
|
||||
|
||||
const upper = trimmed.toUpperCase();
|
||||
let nodeType = null;
|
||||
let fields = {};
|
||||
|
||||
if (upper.startsWith('TALK ')) {
|
||||
nodeType = 'TALK';
|
||||
const match = trimmed.match(/TALK\s+"([^"]*)"/i) || trimmed.match(/TALK\s+(.+)/i);
|
||||
fields.message = match ? match[1] : '';
|
||||
} else if (upper.startsWith('HEAR ')) {
|
||||
nodeType = 'HEAR';
|
||||
const match = trimmed.match(/HEAR\s+(\w+)(?:\s+AS\s+(\w+))?/i);
|
||||
fields.variable = match ? match[1] : 'input';
|
||||
fields.type = match && match[2] ? match[2] : 'string';
|
||||
} else if (upper.startsWith('SET ') || upper.includes(' = ')) {
|
||||
nodeType = 'SET';
|
||||
const match = trimmed.match(/(?:SET\s+)?(\w+)\s*=\s*(.+)/i);
|
||||
fields.variable = match ? match[1] : 'x';
|
||||
fields.expression = match ? match[2] : '0';
|
||||
} else if (upper.startsWith('IF ')) {
|
||||
nodeType = 'IF';
|
||||
const match = trimmed.match(/IF\s+(.+?)\s+THEN/i);
|
||||
fields.condition = match ? match[1] : 'true';
|
||||
} else if (upper.startsWith('FOR ')) {
|
||||
nodeType = 'FOR';
|
||||
const match = trimmed.match(/FOR\s+(?:EACH\s+)?(\w+)\s+IN\s+(.+)/i);
|
||||
fields.variable = match ? match[1] : 'item';
|
||||
fields.collection = match ? match[2] : 'items';
|
||||
} else if (upper.startsWith('CALL ')) {
|
||||
nodeType = 'CALL';
|
||||
const match = trimmed.match(/CALL\s+(\w+)\s*\(([^)]*)\)/i);
|
||||
fields.procedure = match ? match[1] : 'sub';
|
||||
fields.arguments = match ? match[2] : '';
|
||||
} else if (upper.startsWith('WAIT ')) {
|
||||
nodeType = 'WAIT';
|
||||
const match = trimmed.match(/WAIT\s+(\d+)/i);
|
||||
fields.duration = match ? match[1] : '1000';
|
||||
} else if (upper.startsWith('GET ')) {
|
||||
nodeType = 'GET';
|
||||
const match = trimmed.match(/GET\s+(.+?)\s+TO\s+(\w+)/i);
|
||||
fields.url = match ? match[1] : '';
|
||||
fields.variable = match ? match[2] : 'result';
|
||||
} else if (upper.startsWith('PARAM ')) {
|
||||
nodeType = 'HEAR';
|
||||
const match = trimmed.match(/PARAM\s+(\w+)\s+AS\s+(\w+)/i);
|
||||
fields.variable = match ? match[1] : 'param';
|
||||
fields.type = match ? match[2] : 'string';
|
||||
}
|
||||
|
||||
if (nodeType && nodeTemplates[nodeType]) {
|
||||
const node = createNode(nodeType, 400, yPos);
|
||||
if (node) {
|
||||
Object.assign(node.fields, fields);
|
||||
|
||||
const nodeEl = document.getElementById(node.id);
|
||||
if (nodeEl) {
|
||||
nodeEl.querySelectorAll('.node-field-input, .node-field-select, textarea').forEach(input => {
|
||||
const fieldName = input.dataset.field || input.name;
|
||||
if (fields[fieldName] !== undefined) {
|
||||
input.value = fields[fieldName];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
yPos += 100;
|
||||
nodeCount++;
|
||||
console.log('Created node:', nodeType, fields);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Parsed ${nodeCount} nodes from BASIC code`);
|
||||
updateStatusBar();
|
||||
saveToHistory();
|
||||
}
|
||||
|
||||
function initializeCanvas() {
|
||||
console.log('initializeCanvas called');
|
||||
const canvasLoaded = document.querySelector('.canvas-loaded');
|
||||
if (!canvasLoaded) {
|
||||
console.log('No canvas-loaded element found');
|
||||
return;
|
||||
}
|
||||
|
||||
const content = canvasLoaded.dataset.content || '';
|
||||
console.log('Canvas content from server:', content.substring(0, 100));
|
||||
|
||||
canvasLoaded.remove();
|
||||
parseBasicCodeToNodes(content);
|
||||
}
|
||||
85
botui/ui/suite/designer/designer-keyboard.js
Normal file
85
botui/ui/suite/designer/designer-keyboard.js
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
function initKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
switch (e.key) {
|
||||
case 's':
|
||||
e.preventDefault();
|
||||
saveDesign();
|
||||
break;
|
||||
case 'o':
|
||||
e.preventDefault();
|
||||
showModal('open-modal');
|
||||
break;
|
||||
case 'z':
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) redo();
|
||||
else undo();
|
||||
break;
|
||||
case 'y':
|
||||
e.preventDefault();
|
||||
redo();
|
||||
break;
|
||||
case 'c':
|
||||
if (state.selectedNode) {
|
||||
e.preventDefault();
|
||||
state.clipboard = {...state.nodes.get(state.selectedNode)};
|
||||
}
|
||||
break;
|
||||
case 'v':
|
||||
if (state.clipboard) {
|
||||
e.preventDefault();
|
||||
const newNode = {...state.clipboard};
|
||||
newNode.id = 'node-' + state.nextNodeId++;
|
||||
newNode.x += 40;
|
||||
newNode.y += 40;
|
||||
state.nodes.set(newNode.id, newNode);
|
||||
renderNode(newNode);
|
||||
selectNode(newNode.id);
|
||||
saveToHistory();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'Delete' && state.selectedNode) {
|
||||
deleteSelectedNode();
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
deselectAll();
|
||||
hideContextMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initContextMenu() {
|
||||
const canvas = document.getElementById('canvas');
|
||||
const contextMenu = document.getElementById('context-menu');
|
||||
|
||||
if (!canvas || !contextMenu) {
|
||||
console.warn('initContextMenu: canvas or context-menu not found');
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
const nodeEl = e.target.closest('.node');
|
||||
if (nodeEl) {
|
||||
selectNode(nodeEl.id);
|
||||
}
|
||||
contextMenu.style.left = e.clientX + 'px';
|
||||
contextMenu.style.top = e.clientY + 'px';
|
||||
contextMenu.classList.add('visible');
|
||||
});
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
hideContextMenu();
|
||||
});
|
||||
}
|
||||
|
||||
function hideContextMenu() {
|
||||
const menu = document.getElementById('context-menu');
|
||||
if (menu) {
|
||||
menu.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
118
botui/ui/suite/designer/designer-magic.js
Normal file
118
botui/ui/suite/designer/designer-magic.js
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
function showMagicPanel() {
|
||||
const panel = document.getElementById('magic-panel');
|
||||
panel.classList.add('visible');
|
||||
analyzeMagicSuggestions();
|
||||
}
|
||||
|
||||
function hideMagicPanel() {
|
||||
const panel = document.getElementById('magic-panel');
|
||||
if (panel) {
|
||||
panel.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
async function analyzeMagicSuggestions() {
|
||||
const content = document.getElementById('magic-content');
|
||||
content.innerHTML = '<div class="magic-loading"><div class="spinner"></div><p>Analyzing your dialog...</p></div>';
|
||||
|
||||
const nodes = Array.from(state.nodes.values());
|
||||
const dialogData = {
|
||||
nodes: nodes.map(n => ({ type: n.type, fields: n.fields })),
|
||||
connections: state.connections.length,
|
||||
filename: document.getElementById('current-filename').value || 'untitled'
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/designer/magic', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(dialogData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const suggestions = await response.json();
|
||||
renderMagicSuggestions(suggestions);
|
||||
} else {
|
||||
renderFallbackSuggestions(dialogData);
|
||||
}
|
||||
} catch (e) {
|
||||
renderFallbackSuggestions(dialogData);
|
||||
}
|
||||
}
|
||||
|
||||
function renderFallbackSuggestions(dialogData) {
|
||||
const suggestions = [];
|
||||
const nodes = dialogData.nodes;
|
||||
|
||||
if (!nodes.some(n => n.type === 'HEAR')) {
|
||||
suggestions.push({
|
||||
type: 'ux',
|
||||
title: 'Add User Input',
|
||||
description: 'Your dialog has no HEAR nodes. Consider adding user input to make it interactive.'
|
||||
});
|
||||
}
|
||||
|
||||
if (nodes.filter(n => n.type === 'TALK').length > 5) {
|
||||
suggestions.push({
|
||||
type: 'ux',
|
||||
title: 'Break Up Long Responses',
|
||||
description: 'You have many TALK nodes. Consider grouping related messages or using a menu.'
|
||||
});
|
||||
}
|
||||
|
||||
if (!nodes.some(n => n.type === 'IF' || n.type === 'SWITCH')) {
|
||||
suggestions.push({
|
||||
type: 'feature',
|
||||
title: 'Add Decision Logic',
|
||||
description: 'Add IF or SWITCH nodes to handle different user responses dynamically.'
|
||||
});
|
||||
}
|
||||
|
||||
if (dialogData.connections < nodes.length - 1 && nodes.length > 1) {
|
||||
suggestions.push({
|
||||
type: 'perf',
|
||||
title: 'Check Connections',
|
||||
description: 'Some nodes may not be connected. Ensure all nodes flow properly.'
|
||||
});
|
||||
}
|
||||
|
||||
suggestions.push({
|
||||
type: 'a11y',
|
||||
title: 'Use Clear Language',
|
||||
description: 'Keep messages short and clear. Avoid jargon for better accessibility.'
|
||||
});
|
||||
|
||||
renderMagicSuggestions(suggestions);
|
||||
}
|
||||
|
||||
function renderMagicSuggestions(suggestions) {
|
||||
const content = document.getElementById('magic-content');
|
||||
if (!suggestions || suggestions.length === 0) {
|
||||
content.innerHTML = '<p style="text-align:center;color:var(--text-secondary, #94a3b8);padding:40px;">Your dialog looks great! No suggestions at this time.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const icons = {
|
||||
ux: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
|
||||
perf: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>',
|
||||
a11y: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
|
||||
feature: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"/></svg>'
|
||||
};
|
||||
|
||||
content.innerHTML = suggestions.map(s => `
|
||||
<div class="magic-suggestion">
|
||||
<div class="magic-suggestion-header">
|
||||
<div class="magic-suggestion-icon ${s.type}">${icons[s.type] || icons.feature}</div>
|
||||
<span class="magic-suggestion-title">${s.title}</span>
|
||||
</div>
|
||||
<p class="magic-suggestion-desc">${s.description}</p>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.key === 'm') {
|
||||
e.preventDefault();
|
||||
showMagicPanel();
|
||||
}
|
||||
});
|
||||
230
botui/ui/suite/designer/designer-nodes.js
Normal file
230
botui/ui/suite/designer/designer-nodes.js
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
function createNode(type, x, y) {
|
||||
const template = nodeTemplates[type];
|
||||
if (!template) {
|
||||
console.warn('No template found for node type:', type);
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = 'node-' + state.nextNodeId++;
|
||||
const node = {
|
||||
id,
|
||||
type,
|
||||
x,
|
||||
y,
|
||||
fields: {}
|
||||
};
|
||||
|
||||
template.fields.forEach(field => {
|
||||
node.fields[field.name] = field.default;
|
||||
});
|
||||
|
||||
state.nodes.set(id, node);
|
||||
renderNode(node);
|
||||
saveToHistory();
|
||||
updateStatusBar();
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
function renderNode(node) {
|
||||
const template = nodeTemplates[node.type];
|
||||
const typeClass = node.type.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
let fieldsHtml = '';
|
||||
template.fields.forEach(field => {
|
||||
const value = node.fields[field.name] || '';
|
||||
if (field.type === 'textarea') {
|
||||
fieldsHtml += `
|
||||
<div class="node-field">
|
||||
<label class="node-field-label">${field.label}</label>
|
||||
<textarea class="node-field-input" data-field="${field.name}" rows="2">${value}</textarea>
|
||||
</div>
|
||||
`;
|
||||
} else if (field.type === 'select') {
|
||||
const options = field.options.map(opt =>
|
||||
`<option value="${opt}" ${opt === value ? 'selected' : ''}>${opt}</option>`
|
||||
).join('');
|
||||
fieldsHtml += `
|
||||
<div class="node-field">
|
||||
<label class="node-field-label">${field.label}</label>
|
||||
<select class="node-field-select" data-field="${field.name}">${options}</select>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
fieldsHtml += `
|
||||
<div class="node-field">
|
||||
<label class="node-field-label">${field.label}</label>
|
||||
<input type="text" class="node-field-input" data-field="${field.name}" value="${value}">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
let portsHtml = '';
|
||||
if (template.hasInput) {
|
||||
portsHtml += `<div class="node-port input" data-port="input"></div>`;
|
||||
}
|
||||
if (template.hasOutput) {
|
||||
portsHtml += `<div class="node-port output" data-port="output"></div>`;
|
||||
}
|
||||
if (template.hasOutputTrue) {
|
||||
portsHtml += `<div class="node-port output-true" data-port="true" title="True"></div>`;
|
||||
}
|
||||
if (template.hasOutputFalse) {
|
||||
portsHtml += `<div class="node-port output-false" data-port="false" title="False"></div>`;
|
||||
}
|
||||
|
||||
const nodeEl = document.createElement('div');
|
||||
nodeEl.className = 'node';
|
||||
nodeEl.id = node.id;
|
||||
nodeEl.style.left = node.x + 'px';
|
||||
nodeEl.style.top = node.y + 'px';
|
||||
nodeEl.innerHTML = `
|
||||
<div class="node-header ${typeClass}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||
${getNodeIcon(node.type)}
|
||||
</svg>
|
||||
${node.type}
|
||||
</div>
|
||||
<div class="node-body">
|
||||
${fieldsHtml}
|
||||
</div>
|
||||
${portsHtml}
|
||||
`;
|
||||
|
||||
nodeEl.addEventListener('mousedown', (e) => {
|
||||
if (e.target.classList.contains('node-port')) return;
|
||||
selectNode(node.id);
|
||||
startNodeDrag(e, node);
|
||||
});
|
||||
|
||||
nodeEl.querySelectorAll('.node-field-input, .node-field-select').forEach(input => {
|
||||
input.addEventListener('change', (e) => {
|
||||
node.fields[e.target.dataset.field] = e.target.value;
|
||||
saveToHistory();
|
||||
});
|
||||
});
|
||||
|
||||
nodeEl.querySelectorAll('.node-port').forEach(port => {
|
||||
port.addEventListener('mousedown', (e) => {
|
||||
e.stopPropagation();
|
||||
startConnection(node.id, port.dataset.port);
|
||||
});
|
||||
port.addEventListener('mouseup', (e) => {
|
||||
e.stopPropagation();
|
||||
endConnection(node.id, port.dataset.port);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('canvas-inner').appendChild(nodeEl);
|
||||
}
|
||||
|
||||
function getNodeIcon(type) {
|
||||
const icons = {
|
||||
'TALK': '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
|
||||
'HEAR': '<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/>',
|
||||
'SET': '<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>',
|
||||
'IF': '<path d="M16 3h5v5"/><path d="M8 3H3v5"/><path d="M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3"/><path d="m15 9 6-6"/>',
|
||||
'FOR': '<polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/>',
|
||||
'SWITCH': '<path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/>',
|
||||
'CALL': '<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.362 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.338 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/>',
|
||||
'SEND MAIL': '<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/>',
|
||||
'GET': '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>',
|
||||
'POST': '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>',
|
||||
'SAVE': '<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/>',
|
||||
'WAIT': '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
|
||||
'SET BOT MEMORY': '<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/>',
|
||||
'GET BOT MEMORY': '<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="7 10 12 15 17 10"/>',
|
||||
'SET USER MEMORY': '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
|
||||
'GET USER MEMORY': '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>'
|
||||
};
|
||||
return icons[type] || '<circle cx="12" cy="12" r="10"/>';
|
||||
}
|
||||
|
||||
function startNodeDrag(e, node) {
|
||||
state.isDragging = true;
|
||||
const nodeEl = document.getElementById(node.id);
|
||||
const startX = e.clientX;
|
||||
const startY = e.clientY;
|
||||
const origX = node.x;
|
||||
const origY = node.y;
|
||||
|
||||
function onMove(e) {
|
||||
const dx = (e.clientX - startX) / state.zoom;
|
||||
const dy = (e.clientY - startY) / state.zoom;
|
||||
node.x = snapToGrid(origX + dx);
|
||||
node.y = snapToGrid(origY + dy);
|
||||
nodeEl.style.left = node.x + 'px';
|
||||
nodeEl.style.top = node.y + 'px';
|
||||
updateConnections();
|
||||
}
|
||||
|
||||
function onUp() {
|
||||
state.isDragging = false;
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
saveToHistory();
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
}
|
||||
|
||||
function selectNode(id) {
|
||||
if (state.selectedNode) {
|
||||
const prevEl = document.getElementById(state.selectedNode);
|
||||
if (prevEl) prevEl.classList.remove('selected');
|
||||
}
|
||||
state.selectedNode = id;
|
||||
const nodeEl = document.getElementById(id);
|
||||
if (nodeEl) nodeEl.classList.add('selected');
|
||||
updatePropertiesPanel();
|
||||
}
|
||||
|
||||
function deselectAll() {
|
||||
if (state.selectedNode) {
|
||||
const el = document.getElementById(state.selectedNode);
|
||||
if (el) el.classList.remove('selected');
|
||||
}
|
||||
state.selectedNode = null;
|
||||
state.selectedConnection = null;
|
||||
updatePropertiesPanel();
|
||||
}
|
||||
|
||||
function deleteSelectedNode() {
|
||||
if (!state.selectedNode) return;
|
||||
const nodeEl = document.getElementById(state.selectedNode);
|
||||
if (nodeEl) nodeEl.remove();
|
||||
|
||||
state.connections = state.connections.filter(
|
||||
conn => conn.from !== state.selectedNode && conn.to !== state.selectedNode
|
||||
);
|
||||
|
||||
state.nodes.delete(state.selectedNode);
|
||||
state.selectedNode = null;
|
||||
updateConnections();
|
||||
updatePropertiesPanel();
|
||||
updateStatusBar();
|
||||
saveToHistory();
|
||||
}
|
||||
|
||||
function duplicateNode() {
|
||||
if (!state.selectedNode) return;
|
||||
const node = state.nodes.get(state.selectedNode);
|
||||
if (!node) return;
|
||||
|
||||
const newNode = {...node, fields: {...node.fields}};
|
||||
newNode.id = 'node-' + state.nextNodeId++;
|
||||
newNode.x += 40;
|
||||
newNode.y += 40;
|
||||
state.nodes.set(newNode.id, newNode);
|
||||
renderNode(newNode);
|
||||
selectNode(newNode.id);
|
||||
saveToHistory();
|
||||
hideContextMenu();
|
||||
}
|
||||
|
||||
function updateStatusBar() {
|
||||
document.getElementById('node-count').textContent = state.nodes.size + ' nodes';
|
||||
document.getElementById('connection-count').textContent = state.connections.length + ' connections';
|
||||
}
|
||||
67
botui/ui/suite/designer/designer-properties.js
Normal file
67
botui/ui/suite/designer/designer-properties.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
function updatePropertiesPanel() {
|
||||
const content = document.getElementById('properties-content');
|
||||
const empty = document.getElementById('properties-empty');
|
||||
|
||||
if (!state.selectedNode) {
|
||||
content.style.display = 'none';
|
||||
empty.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
const node = state.nodes.get(state.selectedNode);
|
||||
if (!node) return;
|
||||
|
||||
const template = nodeTemplates[node.type];
|
||||
empty.style.display = 'none';
|
||||
content.style.display = 'block';
|
||||
|
||||
let html = `
|
||||
<div class="property-group">
|
||||
<div class="property-group-title">Node Info</div>
|
||||
<div class="property-field">
|
||||
<label class="property-label">Type</label>
|
||||
<input type="text" class="property-input" value="${node.type}" readonly>
|
||||
</div>
|
||||
<div class="property-field">
|
||||
<label class="property-label">ID</label>
|
||||
<input type="text" class="property-input" value="${node.id}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="property-group">
|
||||
<div class="property-group-title">Properties</div>
|
||||
`;
|
||||
|
||||
template.fields.forEach(field => {
|
||||
const value = node.fields[field.name] || '';
|
||||
if (field.type === 'textarea') {
|
||||
html += `
|
||||
<div class="property-field">
|
||||
<label class="property-label">${field.label}</label>
|
||||
<textarea class="property-textarea" data-node="${node.id}" data-field="${field.name}">${value}</textarea>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
html += `
|
||||
<div class="property-field">
|
||||
<label class="property-label">${field.label}</label>
|
||||
<input type="text" class="property-input" data-node="${node.id}" data-field="${field.name}" value="${value}">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
content.innerHTML = html;
|
||||
|
||||
content.querySelectorAll('.property-input:not([readonly]), .property-textarea').forEach(input => {
|
||||
input.addEventListener('change', (e) => {
|
||||
const n = state.nodes.get(e.target.dataset.node);
|
||||
if (n) {
|
||||
n.fields[e.target.dataset.field] = e.target.value;
|
||||
const nodeInput = document.querySelector(`#${n.id} [data-field="${e.target.dataset.field}"]`);
|
||||
if (nodeInput) nodeInput.value = e.target.value;
|
||||
saveToHistory();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
148
botui/ui/suite/designer/designer-state.js
Normal file
148
botui/ui/suite/designer/designer-state.js
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
const state = {
|
||||
nodes: new Map(),
|
||||
connections: [],
|
||||
selectedNode: null,
|
||||
selectedConnection: null,
|
||||
isDragging: false,
|
||||
isConnecting: false,
|
||||
connectionStart: null,
|
||||
zoom: 1,
|
||||
pan: { x: 0, y: 0 },
|
||||
history: [],
|
||||
historyIndex: -1,
|
||||
clipboard: null,
|
||||
nextNodeId: 1,
|
||||
driveSource: null
|
||||
};
|
||||
|
||||
const nodeTemplates = {
|
||||
'TALK': {
|
||||
fields: [
|
||||
{ name: 'message', label: 'Message', type: 'textarea', default: 'Hello!' }
|
||||
],
|
||||
hasInput: true,
|
||||
hasOutput: true
|
||||
},
|
||||
'HEAR': {
|
||||
fields: [
|
||||
{ name: 'variable', label: 'Variable', type: 'text', default: 'response' },
|
||||
{ name: 'type', label: 'Type', type: 'select', options: ['STRING', 'NUMBER', 'DATE', 'EMAIL', 'PHONE'], default: 'STRING' }
|
||||
],
|
||||
hasInput: true,
|
||||
hasOutput: true
|
||||
},
|
||||
'SET': {
|
||||
fields: [
|
||||
{ name: 'variable', label: 'Variable', type: 'text', default: 'value' },
|
||||
{ name: 'expression', label: 'Expression', type: 'text', default: '' }
|
||||
],
|
||||
hasInput: true,
|
||||
hasOutput: true
|
||||
},
|
||||
'IF': {
|
||||
fields: [
|
||||
{ name: 'condition', label: 'Condition', type: 'text', default: 'value = 1' }
|
||||
],
|
||||
hasInput: true,
|
||||
hasOutput: false,
|
||||
hasOutputTrue: true,
|
||||
hasOutputFalse: true
|
||||
},
|
||||
'FOR': {
|
||||
fields: [
|
||||
{ name: 'variable', label: 'Item Variable', type: 'text', default: 'item' },
|
||||
{ name: 'collection', label: 'Collection', type: 'text', default: 'items' }
|
||||
],
|
||||
hasInput: true,
|
||||
hasOutput: true,
|
||||
hasLoopOutput: true
|
||||
},
|
||||
'SWITCH': {
|
||||
fields: [
|
||||
{ name: 'expression', label: 'Expression', type: 'text', default: 'value' }
|
||||
],
|
||||
hasInput: true,
|
||||
hasOutput: true
|
||||
},
|
||||
'CALL': {
|
||||
fields: [
|
||||
{ name: 'procedure', label: 'Procedure', type: 'text', default: '' },
|
||||
{ name: 'arguments', label: 'Arguments', type: 'text', default: '' }
|
||||
],
|
||||
hasInput: true,
|
||||
hasOutput: true
|
||||
},
|
||||
'SEND MAIL': {
|
||||
fields: [
|
||||
{ name: 'to', label: 'To', type: 'text', default: '' },
|
||||
{ name: 'subject', label: 'Subject', type: 'text', default: '' },
|
||||
{ name: 'body', label: 'Body', type: 'textarea', default: '' }
|
||||
],
|
||||
hasInput: true,
|
||||
hasOutput: true
|
||||
},
|
||||
'GET': {
|
||||
fields: [
|
||||
{ name: 'url', label: 'URL', type: 'text', default: '' },
|
||||
{ name: 'variable', label: 'Result Variable', type: 'text', default: 'result' }
|
||||
],
|
||||
hasInput: true,
|
||||
hasOutput: true
|
||||
},
|
||||
'POST': {
|
||||
fields: [
|
||||
{ name: 'url', label: 'URL', type: 'text', default: '' },
|
||||
{ name: 'body', label: 'Body', type: 'textarea', default: '' },
|
||||
{ name: 'variable', label: 'Result Variable', type: 'text', default: 'result' }
|
||||
],
|
||||
hasInput: true,
|
||||
hasOutput: true
|
||||
},
|
||||
'SAVE': {
|
||||
fields: [
|
||||
{ name: 'filename', label: 'Filename', type: 'text', default: 'data.csv' },
|
||||
{ name: 'data', label: 'Data', type: 'text', default: '' }
|
||||
],
|
||||
hasInput: true,
|
||||
hasOutput: true
|
||||
},
|
||||
'WAIT': {
|
||||
fields: [
|
||||
{ name: 'duration', label: 'Duration (seconds)', type: 'text', default: '5' }
|
||||
],
|
||||
hasInput: true,
|
||||
hasOutput: true
|
||||
},
|
||||
'SET BOT MEMORY': {
|
||||
fields: [
|
||||
{ name: 'key', label: 'Key', type: 'text', default: '' },
|
||||
{ name: 'value', label: 'Value', type: 'text', default: '' }
|
||||
],
|
||||
hasInput: true,
|
||||
hasOutput: true
|
||||
},
|
||||
'GET BOT MEMORY': {
|
||||
fields: [
|
||||
{ name: 'key', label: 'Key', type: 'text', default: '' },
|
||||
{ name: 'variable', label: 'Variable', type: 'text', default: 'value' }
|
||||
],
|
||||
hasInput: true,
|
||||
hasOutput: true
|
||||
},
|
||||
'SET USER MEMORY': {
|
||||
fields: [
|
||||
{ name: 'key', label: 'Key', type: 'text', default: '' },
|
||||
{ name: 'value', label: 'Value', type: 'text', default: '' }
|
||||
],
|
||||
hasInput: true,
|
||||
hasOutput: true
|
||||
},
|
||||
'GET USER MEMORY': {
|
||||
fields: [
|
||||
{ name: 'key', label: 'Key', type: 'text', default: '' },
|
||||
{ name: 'variable', label: 'Variable', type: 'text', default: 'value' }
|
||||
],
|
||||
hasInput: true,
|
||||
hasOutput: true
|
||||
}
|
||||
};
|
||||
877
botui/ui/suite/designer/designer.css
Normal file
877
botui/ui/suite/designer/designer.css
Normal file
|
|
@ -0,0 +1,877 @@
|
|||
/* Designer uses global theme variables with node-specific colors */
|
||||
.designer-container {
|
||||
--node-talk: #89b4fa;
|
||||
--node-hear: #a6e3a1;
|
||||
--node-set: #f9e2af;
|
||||
--node-if: #cba6f7;
|
||||
--node-for: #f38ba8;
|
||||
--node-call: #94e2d5;
|
||||
--node-send: #fab387;
|
||||
--grid-size: 20px;
|
||||
}
|
||||
|
||||
.designer-container {
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr 280px;
|
||||
grid-template-rows: 48px 1fr 32px;
|
||||
height: calc(100vh - 64px);
|
||||
background: var(--bg, #0f172a);
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.designer-header {
|
||||
grid-column: 1 / -1;
|
||||
background: var(--surface, #1e293b);
|
||||
border-bottom: 1px solid var(--border, #334155);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.designer-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--primary, #3b82f6);
|
||||
}
|
||||
|
||||
.designer-logo svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.designer-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
background: var(--surface-hover, #334155);
|
||||
border: 1px solid var(--border, #334155);
|
||||
color: var(--text, #f8fafc);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.toolbar-btn.magic {
|
||||
background: linear-gradient(135deg, var(--primary, #a855f7), var(--primary, #3b82f6));
|
||||
border-color: var(--primary, #a855f7);
|
||||
}
|
||||
|
||||
.toolbar-btn.magic:hover {
|
||||
background: linear-gradient(135deg, var(--primary, #3b82f6), var(--primary, #a855f7));
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.magic-panel {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 60px;
|
||||
width: 380px;
|
||||
max-height: 500px;
|
||||
background: var(--surface, #1e293b);
|
||||
border: 1px solid var(--border, #334155);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.magic-panel.visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.magic-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--border, #334155);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: linear-gradient(135deg, rgba(203,166,247,0.1), rgba(137,180,250,0.1));
|
||||
}
|
||||
|
||||
.magic-header svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--primary, #a855f7);
|
||||
}
|
||||
|
||||
.magic-header h3 {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.magic-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary, #94a3b8);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.magic-close:hover {
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.magic-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.magic-loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--text-secondary, #94a3b8);
|
||||
}
|
||||
|
||||
.magic-loading .spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid var(--border, #334155);
|
||||
border-top-color: var(--primary, #a855f7);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.magic-suggestion {
|
||||
background: var(--surface-hover, #334155);
|
||||
border: 1px solid var(--border, #334155);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.magic-suggestion-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.magic-suggestion-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.magic-suggestion-icon.ux { background: rgba(166,227,161,0.2); color: var(--success, #22c55e); }
|
||||
.magic-suggestion-icon.perf { background: rgba(249,226,175,0.2); color: var(--warning, #f59e0b); }
|
||||
.magic-suggestion-icon.a11y { background: rgba(137,180,250,0.2); color: var(--primary, #3b82f6); }
|
||||
.magic-suggestion-icon.feature { background: rgba(203,166,247,0.2); color: var(--primary, #a855f7); }
|
||||
|
||||
.magic-suggestion-title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.magic-suggestion-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary, #94a3b8);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.magic-suggestion-apply {
|
||||
margin-top: 10px;
|
||||
background: var(--primary, #a855f7);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.magic-suggestion-apply:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
background: var(--border, #334155);
|
||||
}
|
||||
|
||||
.toolbar-btn.primary {
|
||||
background: var(--primary, #3b82f6);
|
||||
color: var(--bg, #0f172a);
|
||||
border-color: var(--primary, #3b82f6);
|
||||
}
|
||||
|
||||
.toolbar-btn.primary:hover {
|
||||
background: var(--primary-hover, #2563eb);
|
||||
}
|
||||
|
||||
.toolbar-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.toolbar-separator {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: var(--border, #334155);
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
/* Toolbox Panel */
|
||||
.toolbox-panel {
|
||||
background: var(--surface, #1e293b);
|
||||
border-right: 1px solid var(--border, #334155);
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.toolbox-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.toolbox-section-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary, #94a3b8);
|
||||
margin-bottom: 8px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.toolbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: grab;
|
||||
transition: all 0.15s;
|
||||
margin-bottom: 4px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.toolbox-item:hover {
|
||||
background: var(--surface-hover, #334155);
|
||||
}
|
||||
|
||||
.toolbox-item:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.toolbox-item.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.toolbox-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--bg, #0f172a);
|
||||
}
|
||||
|
||||
.node-item-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.toolbox-icon.talk { background: var(--node-talk); }
|
||||
.toolbox-icon.hear { background: var(--node-hear); }
|
||||
.toolbox-icon.set { background: var(--node-set); }
|
||||
.toolbox-icon.if { background: var(--node-if); }
|
||||
.toolbox-icon.for { background: var(--node-for); }
|
||||
.toolbox-icon.call { background: var(--node-call); }
|
||||
.toolbox-icon.send { background: var(--node-send); }
|
||||
.toolbox-icon.get { background: var(--info, #06b6d4); }
|
||||
.toolbox-icon.wait { background: var(--warning, #f59e0b); }
|
||||
.toolbox-icon.switch { background: var(--primary, #3b82f6); }
|
||||
|
||||
.toolbox-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.toolbox-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.toolbox-desc {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary, #94a3b8);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Canvas Area */
|
||||
.canvas-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--bg, #0f172a);
|
||||
}
|
||||
|
||||
.canvas-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(var(--border, #334155) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--border, #334155) 1px, transparent 1px);
|
||||
background-size: var(--grid-size) var(--grid-size);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.canvas-inner {
|
||||
position: relative;
|
||||
width: 3000px;
|
||||
height: 2000px;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* Nodes */
|
||||
.node {
|
||||
position: absolute;
|
||||
min-width: 180px;
|
||||
max-width: 280px;
|
||||
background: var(--surface, #1e293b);
|
||||
border: 2px solid var(--border, #334155);
|
||||
border-radius: 8px;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
transition: box-shadow 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.node:hover {
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.node.selected {
|
||||
border-color: var(--primary, #3b82f6);
|
||||
box-shadow: 0 0 0 3px rgba(137, 180, 250, 0.3);
|
||||
}
|
||||
|
||||
.node-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.node-header.talk { background: var(--node-talk); color: var(--bg, #0f172a); }
|
||||
.node-header.hear { background: var(--node-hear); color: var(--bg, #0f172a); }
|
||||
.node-header.set { background: var(--node-set); color: var(--bg, #0f172a); }
|
||||
.node-header.if { background: var(--node-if); color: var(--bg, #0f172a); }
|
||||
.node-header.for { background: var(--node-for); color: var(--bg, #0f172a); }
|
||||
.node-header.call { background: var(--node-call); color: var(--bg, #0f172a); }
|
||||
.node-header.send { background: var(--node-send); color: var(--bg, #0f172a); }
|
||||
.node-header.get { background: var(--info, #06b6d4); color: var(--bg, #0f172a); }
|
||||
.node-header.wait { background: var(--warning, #f59e0b); color: var(--bg, #0f172a); }
|
||||
.node-header.switch { background: var(--primary, #3b82f6); color: white; }
|
||||
|
||||
.node-header svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.node-body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.node-field {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.node-field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.node-field-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary, #94a3b8);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.node-field-input {
|
||||
width: 100%;
|
||||
background: var(--surface-hover, #334155);
|
||||
border: 1px solid var(--border, #334155);
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 12px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.node-field-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary, #3b82f6);
|
||||
}
|
||||
|
||||
.node-field-select {
|
||||
width: 100%;
|
||||
background: var(--surface-hover, #334155);
|
||||
border: 1px solid var(--border, #334155);
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Connection Ports */
|
||||
.node-port {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: var(--surface-hover, #334155);
|
||||
border: 2px solid var(--border, #334155);
|
||||
border-radius: 50%;
|
||||
cursor: crosshair;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.node-port:hover {
|
||||
background: var(--primary, #3b82f6);
|
||||
border-color: var(--primary, #3b82f6);
|
||||
transform: scale(1.3);
|
||||
}
|
||||
|
||||
.node-port.input {
|
||||
left: -6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.node-port.output {
|
||||
right: -6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.node-port.output-true {
|
||||
right: -6px;
|
||||
top: 35%;
|
||||
background: var(--success, #22c55e);
|
||||
border-color: var(--success, #22c55e);
|
||||
}
|
||||
|
||||
.node-port.output-false {
|
||||
right: -6px;
|
||||
top: 65%;
|
||||
background: var(--error, #ef4444);
|
||||
border-color: var(--error, #ef4444);
|
||||
}
|
||||
|
||||
/* Connections SVG */
|
||||
.connections-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.connection {
|
||||
fill: none;
|
||||
stroke: var(--border, #334155);
|
||||
stroke-width: 2;
|
||||
pointer-events: stroke;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.connection:hover {
|
||||
stroke: var(--primary, #3b82f6);
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.connection.drawing {
|
||||
stroke: var(--primary, #3b82f6);
|
||||
stroke-dasharray: 5, 5;
|
||||
}
|
||||
|
||||
/* Properties Panel */
|
||||
.properties-panel {
|
||||
background: var(--surface, #1e293b);
|
||||
border-left: 1px solid var(--border, #334155);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.properties-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border, #334155);
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.properties-header svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--primary, #3b82f6);
|
||||
}
|
||||
|
||||
.properties-empty {
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
color: var(--text-secondary, #94a3b8);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.properties-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.property-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.property-group-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--text-secondary, #94a3b8);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.property-field {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.property-label {
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.property-input {
|
||||
width: 100%;
|
||||
background: var(--surface-hover, #334155);
|
||||
border: 1px solid var(--border, #334155);
|
||||
border-radius: 4px;
|
||||
padding: 8px 10px;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.property-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary, #3b82f6);
|
||||
}
|
||||
|
||||
.property-textarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.property-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.property-checkbox input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--primary, #3b82f6);
|
||||
}
|
||||
|
||||
/* Status Bar */
|
||||
.status-bar {
|
||||
grid-column: 1 / -1;
|
||||
background: var(--surface, #1e293b);
|
||||
border-top: 1px solid var(--border, #334155);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary, #94a3b8);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.status-item svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--text-secondary, #94a3b8);
|
||||
}
|
||||
|
||||
.status-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.zoom-btn {
|
||||
background: var(--surface-hover, #334155);
|
||||
border: 1px solid var(--border, #334155);
|
||||
color: var(--text, #f8fafc);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.zoom-btn:hover {
|
||||
background: var(--border, #334155);
|
||||
}
|
||||
|
||||
.zoom-value {
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Context Menu */
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
background: var(--surface, #1e293b);
|
||||
border: 1px solid var(--border, #334155);
|
||||
border-radius: 8px;
|
||||
padding: 8px 0;
|
||||
min-width: 160px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.context-menu.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
background: var(--surface-hover, #334155);
|
||||
}
|
||||
|
||||
.context-menu-item.danger {
|
||||
color: var(--error, #ef4444);
|
||||
}
|
||||
|
||||
.context-menu-separator {
|
||||
height: 1px;
|
||||
background: var(--border, #334155);
|
||||
margin: 4px 8px;
|
||||
}
|
||||
|
||||
/* File Browser Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-overlay.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--surface, #1e293b);
|
||||
border: 1px solid var(--border, #334155);
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border, #334155);
|
||||
}
|
||||
|
||||
.magic-panel-header {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border, #334155);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary, #94a3b8);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid var(--border, #334155);
|
||||
}
|
||||
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background: var(--surface-hover, #334155);
|
||||
}
|
||||
|
||||
.file-item.selected {
|
||||
background: var(--primary, #3b82f6);
|
||||
color: var(--bg, #0f172a);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-path {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary, #94a3b8);
|
||||
}
|
||||
|
||||
.file-item.selected .file-path {
|
||||
color: var(--surface-hover, #334155);
|
||||
}
|
||||
|
||||
/* HTMX indicators */
|
||||
.htmx-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.htmx-request .htmx-indicator {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.htmx-request.htmx-indicator {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid var(--border, #334155);
|
||||
border-top-color: var(--primary, #3b82f6);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
/* Minimap */
|
||||
.minimap {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
width: 150px;
|
||||
height: 100px;
|
||||
background: var(--surface, #1e293b);
|
||||
border: 1px solid var(--border, #334155);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.minimap-viewport {
|
||||
position: absolute;
|
||||
border: 1px solid var(--primary, #3b82f6);
|
||||
background: rgba(137, 180, 250, 0.1);
|
||||
}
|
||||
|
||||
.minimap-node {
|
||||
position: absolute;
|
||||
background: var(--primary, #3b82f6);
|
||||
border-radius: 1px;
|
||||
}
|
||||
893
botui/ui/suite/tools/security.css
Normal file
893
botui/ui/suite/tools/security.css
Normal file
|
|
@ -0,0 +1,893 @@
|
|||
.security-container {
|
||||
padding: 24px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.security-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.security-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.security-back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--secondary-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.security-back-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.security-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.security-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.security-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 24px;
|
||||
background: var(--secondary-bg);
|
||||
padding: 4px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.security-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.security-tab:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.security-tab.active {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.security-tab svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.security-tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.security-tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.compliance-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.security-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--secondary-bg);
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.security-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.security-btn-primary {
|
||||
background: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.security-btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
border-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
.security-btn-success {
|
||||
background: #10b981;
|
||||
border-color: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.security-btn-success:hover {
|
||||
background: #059669;
|
||||
border-color: #059669;
|
||||
}
|
||||
|
||||
.security-btn-danger {
|
||||
background: #ef4444;
|
||||
border-color: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.security-btn-danger:hover {
|
||||
background: #dc2626;
|
||||
border-color: #dc2626;
|
||||
}
|
||||
|
||||
.security-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.security-stat-card {
|
||||
background: var(--secondary-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.security-stat-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.security-stat-card.critical::before {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.security-stat-card.high::before {
|
||||
background: #ea580c;
|
||||
}
|
||||
|
||||
.security-stat-card.medium::before {
|
||||
background: #d97706;
|
||||
}
|
||||
|
||||
.security-stat-card.low::before {
|
||||
background: #65a30d;
|
||||
}
|
||||
|
||||
.security-stat-card.info::before {
|
||||
background: #0891b2;
|
||||
}
|
||||
|
||||
.security-stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.security-stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.security-stat-card.critical .security-stat-value {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.security-stat-card.high .security-stat-value {
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
.security-stat-card.medium .security-stat-value {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.security-stat-card.low .security-stat-value {
|
||||
color: #65a30d;
|
||||
}
|
||||
|
||||
.security-stat-card.info .security-stat-value {
|
||||
color: #0891b2;
|
||||
}
|
||||
|
||||
.security-stat-desc {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.security-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
padding: 12px 16px;
|
||||
background: var(--secondary-bg);
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.security-filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.security-filter-label {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.security-filter-select {
|
||||
padding: 8px 12px;
|
||||
background: var(--primary-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.security-filter-input {
|
||||
padding: 8px 12px;
|
||||
background: var(--primary-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.security-filter-input::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.security-filter-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.security-filter-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.security-results {
|
||||
background: var(--secondary-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.security-results-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.security-results-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.security-results-count {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.security-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.security-table th {
|
||||
text-align: left;
|
||||
padding: 12px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
background: var(--primary-bg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.security-table td {
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.security-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.security-table tr:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.security-severity {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.security-severity.critical {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.security-severity.high {
|
||||
background: rgba(234, 88, 12, 0.1);
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
.security-severity.medium {
|
||||
background: rgba(217, 119, 6, 0.1);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.security-severity.low {
|
||||
background: rgba(101, 163, 13, 0.1);
|
||||
color: #65a30d;
|
||||
}
|
||||
|
||||
.security-severity.info {
|
||||
background: rgba(8, 145, 178, 0.1);
|
||||
color: #0891b2;
|
||||
}
|
||||
|
||||
.security-severity-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.security-issue-type {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.security-issue-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.security-issue-icon.password {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.security-issue-icon.security {
|
||||
background: rgba(234, 88, 12, 0.1);
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
.security-issue-icon.deprecated {
|
||||
background: rgba(217, 119, 6, 0.1);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.security-issue-icon.code {
|
||||
background: rgba(101, 163, 13, 0.1);
|
||||
color: #65a30d;
|
||||
}
|
||||
|
||||
.security-issue-icon.config {
|
||||
background: rgba(8, 145, 178, 0.1);
|
||||
color: #0891b2;
|
||||
}
|
||||
|
||||
.security-issue-title {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.security-issue-category {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.security-file-path {
|
||||
font-family: "SF Mono", "Monaco", monospace;
|
||||
font-size: 13px;
|
||||
color: var(--accent-color);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.security-file-line {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.security-description {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.security-code-snippet {
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--primary-bg);
|
||||
border-radius: 6px;
|
||||
font-family: "SF Mono", "Monaco", monospace;
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.security-action-btn {
|
||||
padding: 6px 14px;
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.security-action-btn:hover {
|
||||
background: var(--accent-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.protection-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.protection-card {
|
||||
background: var(--secondary-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.protection-card:hover {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.protection-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
background: var(--primary-bg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.protection-card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.protection-card-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.protection-card-icon.lynis {
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.protection-card-icon.rkhunter {
|
||||
background: linear-gradient(135deg, #ef4444, #b91c1c);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.protection-card-icon.chkrootkit {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.protection-card-icon.suricata {
|
||||
background: linear-gradient(135deg, #8b5cf6, #6d28d9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.protection-card-icon.lmd {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.protection-card-icon.clamav {
|
||||
background: linear-gradient(135deg, #ec4899, #be185d);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.protection-card-info h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.protection-card-info p {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.protection-card-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.protection-card-status.running {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.protection-card-status.stopped {
|
||||
background: rgba(107, 114, 128, 0.1);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.protection-card-status.not-installed {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.protection-card-status.updating {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.protection-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.protection-status-dot.running {
|
||||
animation: pulse-green 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-green {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.protection-card-body {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.protection-card-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.protection-meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.protection-meta-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.protection-meta-value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.protection-meta-value.success {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.protection-meta-value.warning {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.protection-meta-value.error {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.protection-card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.protection-card-actions .security-btn {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
justify-content: center;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.protection-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 20px;
|
||||
background: var(--primary-bg);
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.protection-footer-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.protection-icon-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--secondary-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.protection-icon-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.protection-icon-btn.danger:hover {
|
||||
color: #ef4444;
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.protection-auto-update {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.protection-toggle {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
background: var(--border-color);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.protection-toggle.active {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.protection-toggle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.protection-toggle.active::after {
|
||||
left: 18px;
|
||||
}
|
||||
|
||||
.report-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.report-modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.report-modal-content {
|
||||
background: var(--secondary-bg);
|
||||
border-radius: 16px;
|
||||
width: 90%;
|
||||
max-width: 900px;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.report-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.report-modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.report-modal-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.report-modal-close:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.report-modal-body {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.report-content {
|
||||
font-family: "SF Mono", "Monaco", monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
white-space: pre-wrap;
|
||||
background: var(--primary-bg);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.security-stats {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.protection-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.security-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.security-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.security-filters {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.security-filter-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.security-table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.security-tabs {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.protection-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue