Compare commits
No commits in common. "e0504f37030ffd8c4a33188df8daf5b83edc52e9" and "497d0dd18c42a71cd3e2d9a329b98abe5ac5afb4" have entirely different histories.
e0504f3703
...
497d0dd18c
26 changed files with 3381 additions and 4678 deletions
|
|
@ -1,56 +1,23 @@
|
|||
name: BotUI CI
|
||||
name: GBCI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "botui/**"
|
||||
- "botlib/**"
|
||||
- ".github/workflows/botui.yaml"
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "botui/**"
|
||||
- "botlib/**"
|
||||
- ".github/workflows/botui.yaml"
|
||||
|
||||
env:
|
||||
CARGO_BUILD_JOBS: 8
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: gbo
|
||||
|
||||
steps:
|
||||
- name: Disable SSL verification
|
||||
- name: Disable SSL verification (temporary)
|
||||
run: git config --global http.sslVerify false
|
||||
|
||||
- name: Checkout BotUI Code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: botui
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Workspace
|
||||
run: |
|
||||
git clone --depth 1 --branch main https://alm.pragmatismo.com.br/GeneralBots/gb.git workspace
|
||||
cd workspace
|
||||
git submodule update --init --depth 1 botlib
|
||||
|
||||
# Remove all members except botui and botlib from workspace
|
||||
sed -i '/"botapp",/d' Cargo.toml
|
||||
sed -i '/"botdevice",/d' Cargo.toml
|
||||
sed -i '/"bottest",/d' Cargo.toml
|
||||
sed -i '/"botserver",/d' Cargo.toml
|
||||
sed -i '/"botbook",/d' Cargo.toml
|
||||
sed -i '/"botmodels",/d' Cargo.toml
|
||||
sed -i '/"botplugin",/d' Cargo.toml
|
||||
sed -i '/"bottemplates",/d' Cargo.toml
|
||||
|
||||
cd ..
|
||||
rm -rf workspace/botui
|
||||
mv botui workspace/botui
|
||||
- name: Clone botlib dependency
|
||||
run: git clone --depth 1 https://github.com/GeneralBots/botlib.git ../botlib
|
||||
|
||||
- name: Cache Cargo registry
|
||||
uses: actions/cache@v4
|
||||
|
|
@ -58,54 +25,34 @@ jobs:
|
|||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cache/sccache
|
||||
workspace/target
|
||||
key: ${{ runner.os }}-cargo-v2-debug-ui-${{ hashFiles('**/Cargo.lock') }}
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-botui-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-v2-debug-ui-
|
||||
${{ runner.os }}-cargo-v2-debug-
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libpq-dev libssl-dev liblzma-dev pkg-config
|
||||
${{ runner.os }}-cargo-botui-
|
||||
|
||||
- name: Install Rust
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install sccache
|
||||
run: |
|
||||
wget https://github.com/mozilla/sccache/releases/download/v0.8.2/sccache-v0.8.2-x86_64-unknown-linux-musl.tar.gz
|
||||
tar xzf sccache-v0.8.2-x86_64-unknown-linux-musl.tar.gz
|
||||
mv sccache-v0.8.2-x86_64-unknown-linux-musl/sccache $HOME/.cargo/bin/sccache
|
||||
chmod +x $HOME/.cargo/bin/sccache
|
||||
echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
|
||||
$HOME/.cargo/bin/sccache --start-server || true
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
echo "/root/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Setup environment
|
||||
run: sudo cp /opt/gbo/bin/system/.env . 2>/dev/null || true
|
||||
|
||||
- name: Build BotUI
|
||||
working-directory: workspace
|
||||
run: |
|
||||
cargo build -p botui --features embed-ui -j 8 2>&1 | tee /tmp/build.log
|
||||
ls -lh target/debug/botui
|
||||
sccache --show-stats || true
|
||||
sudo cp /opt/gbo/bin/system/botui.env .env
|
||||
|
||||
- name: Save build log
|
||||
if: always()
|
||||
run: |
|
||||
sudo mkdir -p /opt/gbo/logs
|
||||
sudo cp /tmp/build.log /opt/gbo/logs/botui-$(date +%Y%m%d-%H%M%S).log || true
|
||||
- name: Build Linux x86_64
|
||||
run: /root/.cargo/bin/cargo build --locked --release
|
||||
|
||||
- name: Deploy
|
||||
working-directory: workspace
|
||||
- name: Prepare release artifacts
|
||||
run: |
|
||||
lxc exec bot:pragmatismo-system -- systemctl stop ui || true
|
||||
|
||||
sudo cp target/debug/botui /opt/gbo/bin/system/
|
||||
sudo chmod +x /opt/gbo/bin/system/botui
|
||||
|
||||
lxc exec bot:pragmatismo-system -- systemctl start ui || true
|
||||
sudo mkdir -p /opt/gbo/releases/botui/linux-x86_64
|
||||
sudo cp ./target/release/botui /opt/gbo/releases/botui/linux-x86_64/ || true
|
||||
sudo chmod -R 755 /opt/gbo/releases/botui/
|
||||
|
||||
- name: Deploy and restart local service
|
||||
run: |
|
||||
lxc exec bot:pragmatismo-system -- systemctl stop botui
|
||||
|
||||
sudo cp ./target/release/botui /opt/gbo/bin/botui
|
||||
sudo chmod +x /opt/gbo/bin/botui
|
||||
|
||||
lxc exec bot:pragmatismo-system -- systemctl start botui
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ workspace = true
|
|||
features = ["http-client"]
|
||||
|
||||
[features]
|
||||
default = ["ui-server", "chat", "drive", "tasks", "admin"]
|
||||
default = ["ui-server", "chat", "drive", "tasks"]
|
||||
ui-server = []
|
||||
embed-ui = ["rust-embed"]
|
||||
|
||||
|
|
|
|||
278
PROMPT.md
Normal file
278
PROMPT.md
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
# BotUI Development Guide
|
||||
|
||||
**Version:** 6.2.0
|
||||
**Purpose:** Web UI server for General Bots (Axum + HTMX + CSS)
|
||||
|
||||
---
|
||||
|
||||
## ZERO TOLERANCE POLICY
|
||||
|
||||
**EVERY SINGLE WARNING MUST BE FIXED. NO EXCEPTIONS.**
|
||||
|
||||
---
|
||||
|
||||
## ❌ ABSOLUTE PROHIBITIONS
|
||||
|
||||
```
|
||||
❌ NEVER use #![allow()] or #[allow()] in source code
|
||||
❌ NEVER use _ prefix for unused variables - DELETE or USE them
|
||||
❌ NEVER use .unwrap() - use ? or proper error handling
|
||||
❌ NEVER use .expect() - use ? or proper error handling
|
||||
❌ NEVER use panic!() or unreachable!()
|
||||
❌ NEVER use todo!() or unimplemented!()
|
||||
❌ NEVER leave unused imports or dead code
|
||||
❌ NEVER add comments - code must be self-documenting
|
||||
❌ NEVER use CDN links - all assets must be local
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ ARCHITECTURE
|
||||
|
||||
### Dual Modes
|
||||
|
||||
| Mode | Command | Description |
|
||||
|------|---------|-------------|
|
||||
| Web | `cargo run` | Axum server on port 3000 |
|
||||
| Desktop | `cargo tauri dev` | Tauri native window |
|
||||
|
||||
### Code Organization
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.rs # Entry point - mode detection
|
||||
├── lib.rs # Feature-gated module exports
|
||||
├── http_client.rs # HTTP wrapper for botserver
|
||||
├── ui_server/
|
||||
│ └── mod.rs # Axum router + UI serving
|
||||
├── desktop/
|
||||
│ ├── mod.rs # Desktop module organization
|
||||
│ ├── drive.rs # File operations via Tauri
|
||||
│ └── tray.rs # System tray
|
||||
└── shared/
|
||||
└── state.rs # Shared application state
|
||||
|
||||
ui/
|
||||
├── suite/ # Main UI (HTML/CSS/JS)
|
||||
│ ├── js/vendor/ # Local JS libraries
|
||||
│ └── css/ # Stylesheets
|
||||
└── minimal/ # Minimal chat UI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 HTMX-FIRST FRONTEND
|
||||
|
||||
### Core Principle
|
||||
- **Use HTMX** to minimize JavaScript
|
||||
- **Server returns HTML fragments**, not JSON
|
||||
- **Delegate ALL logic** to Rust server
|
||||
|
||||
### HTMX Usage
|
||||
|
||||
| Use Case | Solution |
|
||||
|----------|----------|
|
||||
| Data fetching | `hx-get`, `hx-post` |
|
||||
| Form submission | `hx-post`, `hx-put` |
|
||||
| Real-time updates | `hx-ext="ws"` |
|
||||
| Content swapping | `hx-target`, `hx-swap` |
|
||||
| Polling | `hx-trigger="every 5s"` |
|
||||
| Loading states | `hx-indicator` |
|
||||
|
||||
### When JS is Required
|
||||
|
||||
| Use Case | Why JS Required |
|
||||
|----------|-----------------|
|
||||
| Modal show/hide | DOM manipulation |
|
||||
| Toast notifications | Dynamic element creation |
|
||||
| Clipboard operations | `navigator.clipboard` API |
|
||||
| Keyboard shortcuts | `keydown` event handling |
|
||||
| Complex animations | GSAP or custom |
|
||||
|
||||
---
|
||||
|
||||
## 📦 LOCAL ASSETS ONLY - NO CDN
|
||||
|
||||
```
|
||||
ui/suite/js/vendor/
|
||||
├── htmx.min.js
|
||||
├── htmx-ws.js
|
||||
├── marked.min.js
|
||||
├── gsap.min.js
|
||||
└── livekit-client.umd.min.js
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- ✅ CORRECT -->
|
||||
<script src="js/vendor/htmx.min.js"></script>
|
||||
|
||||
<!-- ❌ WRONG -->
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 OFFICIAL ICONS - MANDATORY
|
||||
|
||||
**NEVER generate icons with LLM. Use official SVG icons:**
|
||||
|
||||
```
|
||||
ui/suite/assets/icons/
|
||||
├── gb-logo.svg # Main GB logo
|
||||
├── gb-bot.svg # Bot/assistant
|
||||
├── gb-analytics.svg # Analytics
|
||||
├── gb-calendar.svg # Calendar
|
||||
├── gb-chat.svg # Chat
|
||||
├── gb-drive.svg # File storage
|
||||
├── gb-mail.svg # Email
|
||||
├── gb-meet.svg # Video meetings
|
||||
├── gb-tasks.svg # Task management
|
||||
└── ...
|
||||
```
|
||||
|
||||
All icons use `stroke="currentColor"` for CSS theming.
|
||||
|
||||
---
|
||||
|
||||
## 🔒 SECURITY ARCHITECTURE
|
||||
|
||||
### Centralized Auth Engine
|
||||
|
||||
All authentication is handled by `security-bootstrap.js` which MUST be loaded immediately after HTMX:
|
||||
|
||||
```html
|
||||
<head>
|
||||
<!-- 1. HTMX first -->
|
||||
<script src="js/vendor/htmx.min.js"></script>
|
||||
<script src="js/vendor/htmx-ws.js"></script>
|
||||
|
||||
<!-- 2. Security bootstrap immediately after -->
|
||||
<script src="js/security-bootstrap.js"></script>
|
||||
|
||||
<!-- 3. Other scripts -->
|
||||
<script src="js/api-client.js"></script>
|
||||
</head>
|
||||
```
|
||||
|
||||
### DO NOT Duplicate Auth Logic
|
||||
|
||||
```javascript
|
||||
// ❌ WRONG - Don't add auth headers manually
|
||||
fetch("/api/data", {
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
});
|
||||
|
||||
// ✅ CORRECT - Let security-bootstrap.js handle it
|
||||
fetch("/api/data");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 DESIGN SYSTEM
|
||||
|
||||
### Layout Standards
|
||||
|
||||
```css
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-panel {
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
```
|
||||
|
||||
### Theme Variables Required
|
||||
|
||||
```css
|
||||
[data-theme="your-theme"] {
|
||||
--bg: #0a0a0a;
|
||||
--surface: #161616;
|
||||
--surface-hover: #1e1e1e;
|
||||
--border: #2a2a2a;
|
||||
--text: #ffffff;
|
||||
--text-secondary: #888888;
|
||||
--primary: #c5f82a;
|
||||
--success: #22c55e;
|
||||
--warning: #f59e0b;
|
||||
--error: #ef4444;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ CODE PATTERNS
|
||||
|
||||
### Error Handling
|
||||
|
||||
```rust
|
||||
// ❌ WRONG
|
||||
let value = something.unwrap();
|
||||
|
||||
// ✅ CORRECT
|
||||
let value = something?;
|
||||
let value = something.ok_or_else(|| Error::NotFound)?;
|
||||
```
|
||||
|
||||
### Self Usage
|
||||
|
||||
```rust
|
||||
impl MyStruct {
|
||||
fn new() -> Self { Self { } } // ✅ Not MyStruct
|
||||
}
|
||||
```
|
||||
|
||||
### Format Strings
|
||||
|
||||
```rust
|
||||
format!("Hello {name}") // ✅ Not format!("{}", name)
|
||||
```
|
||||
|
||||
### Derive Eq with PartialEq
|
||||
|
||||
```rust
|
||||
#[derive(PartialEq, Eq)] // ✅ Always both
|
||||
struct MyStruct { }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 KEY DEPENDENCIES
|
||||
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| axum | 0.7.5 | Web framework |
|
||||
| reqwest | 0.12 | HTTP client |
|
||||
| tokio | 1.41 | Async runtime |
|
||||
| askama | 0.12 | HTML Templates |
|
||||
|
||||
---
|
||||
|
||||
## 🔑 REMEMBER
|
||||
|
||||
- **ZERO WARNINGS** - Every clippy warning must be fixed
|
||||
- **NO ALLOW IN CODE** - Never use #[allow()] in source files
|
||||
- **NO DEAD CODE** - Delete unused code
|
||||
- **NO UNWRAP/EXPECT** - Use ? operator
|
||||
- **HTMX first** - Minimize JS, delegate to server
|
||||
- **Local assets** - No CDN, all vendor files local
|
||||
- **No business logic** - All logic in botserver
|
||||
- **HTML responses** - Server returns fragments, not JSON
|
||||
- **Version 6.2.0** - do not change without approval
|
||||
317
README.md
317
README.md
|
|
@ -1,314 +1,39 @@
|
|||
# BotUI - General Bots Web Interface
|
||||
|
||||
**Version:** 6.2.0
|
||||
**Purpose:** Web UI server for General Bots (Axum + HTMX + CSS)
|
||||
# General Bots Desktop
|
||||
|
||||
---
|
||||
An AI-powered desktop automation tool that records and plays back user interactions useful for legacy systems and common desktop tasks. The BotDesktop automation tool fills a critical gap in the enterprise automation landscape by addressing legacy systems and desktop applications that lack modern APIs or integration capabilities. While botserver excels at creating conversational bots for modern channels like web, mobile and messaging platforms, many organizations still rely heavily on traditional desktop applications, mainframe systems, and custom internal tools that can only be accessed through their user interface. BotDesktop's ability to record and replay user interactions provides a practical bridge between these legacy systems and modern automation needs.
|
||||
|
||||
## Overview
|
||||
|
||||
BotUI is a modern web interface for General Bots, built with Rust, Axum, and HTMX. It provides a clean, responsive interface for interacting with the General Bots platform, featuring real-time updates via WebSocket connections and a minimalist JavaScript approach powered by HTMX.
|
||||

|
||||
|
||||
The interface supports multiple features including chat, file management, tasks, calendar, analytics, and more - all served through a fast, efficient Rust backend with a focus on server-rendered HTML and minimal client-side JavaScript.
|
||||
|
||||
For comprehensive documentation, see **[docs.pragmatismo.com.br](https://docs.pragmatismo.com.br)** or the **[BotBook](./botbook)** for detailed guides and API references.
|
||||
The tool's AI-powered approach to desktop automation represents a significant advancement over traditional robotic process automation (RPA) tools. By leveraging machine learning to understand screen elements and user interactions, BotDesktop can adapt to minor UI changes and variations that would break conventional scripted automation. This resilience is particularly valuable in enterprise environments where applications receive regular updates or where slight variations exist between different versions or installations of the same software. The AI component also simplifies the creation of automation scripts - instead of requiring complex programming, users can simply demonstrate the desired actions which BotDesktop observes and learns to replicate.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
From an integration perspective, BotDesktop complements botserver by enabling end-to-end automation scenarios that span both modern and legacy systems. For example, a bot created in botserver could collect information from users through a modern chat interface, then use BotDesktop to input that data into a legacy desktop application that lacks API access. This hybrid approach allows organizations to modernize their user interactions while still leveraging their existing IT investments. Additionally, BotDesktop can automate routine desktop tasks like file management, data entry, and application monitoring that fall outside the scope of conversational bot interactions.
|
||||
|
||||
|
||||
The combined toolset of botserver and BotDesktop provides organizations with comprehensive automation capabilities across their entire technology stack. While botserver handles the modern, API-driven interactions with users across multiple channels, BotDesktop extends automation capabilities to the desktop environment where many critical business processes still reside. This dual approach allows organizations to progressively modernize their systems while maintaining operational efficiency through automation of both new and legacy components. The result is a more flexible and complete automation solution that can adapt to various technical environments and business needs.
|
||||
## Setup
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
# Development mode - starts Axum server on port 3000
|
||||
cargo run
|
||||
|
||||
# Desktop mode (Tauri) - starts native window
|
||||
cargo tauri dev
|
||||
npm install
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
2. Create a .env file with your Azure OpenAI credentials
|
||||
|
||||
- `BOTUI_PORT` - Server port (default: 3000)
|
||||
|
||||
---
|
||||
|
||||
## ZERO TOLERANCE POLICY
|
||||
|
||||
**EVERY SINGLE WARNING MUST BE FIXED. NO EXCEPTIONS.**
|
||||
|
||||
---
|
||||
|
||||
## ❌ ABSOLUTE PROHIBITIONS
|
||||
|
||||
```
|
||||
❌ NEVER use #![allow()] or #[allow()] in source code
|
||||
❌ NEVER use _ prefix for unused variables - DELETE or USE them
|
||||
❌ NEVER use .unwrap() - use ? or proper error handling
|
||||
❌ NEVER use .expect() - use ? or proper error handling
|
||||
❌ NEVER use panic!() or unreachable!()
|
||||
❌ NEVER use todo!() or unimplemented!()
|
||||
❌ NEVER leave unused imports or dead code
|
||||
❌ NEVER add comments - code must be self-documenting
|
||||
❌ NEVER use CDN links - all assets must be local
|
||||
3. Development:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ ARCHITECTURE
|
||||
|
||||
### Dual Modes
|
||||
|
||||
| Mode | Command | Description |
|
||||
|------|---------|-------------|
|
||||
| Web | `cargo run` | Axum server on port 3000 |
|
||||
| Desktop | `cargo tauri dev` | Tauri native window |
|
||||
|
||||
### Code Organization
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.rs # Entry point - mode detection
|
||||
├── lib.rs # Feature-gated module exports
|
||||
├── http_client.rs # HTTP wrapper for botserver
|
||||
├── ui_server/
|
||||
│ └── mod.rs # Axum router + UI serving
|
||||
├── desktop/
|
||||
│ ├── mod.rs # Desktop module organization
|
||||
│ ├── drive.rs # File operations via Tauri
|
||||
│ └── tray.rs # System tray
|
||||
└── shared/
|
||||
└── state.rs # Shared application state
|
||||
|
||||
ui/
|
||||
├── suite/ # Main UI (HTML/CSS/JS)
|
||||
│ ├── js/vendor/ # Local JS libraries
|
||||
│ └── css/ # Stylesheets
|
||||
└── minimal/ # Minimal chat UI
|
||||
4. Build:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 HTMX-FIRST FRONTEND
|
||||
|
||||
### Core Principle
|
||||
- **Use HTMX** to minimize JavaScript
|
||||
- **Server returns HTML fragments**, not JSON
|
||||
- **Delegate ALL logic** to Rust server
|
||||
|
||||
### HTMX Usage
|
||||
|
||||
| Use Case | Solution |
|
||||
|----------|----------|
|
||||
| Data fetching | `hx-get`, `hx-post` |
|
||||
| Form submission | `hx-post`, `hx-put` |
|
||||
| Real-time updates | `hx-ext="ws"` |
|
||||
| Content swapping | `hx-target`, `hx-swap` |
|
||||
| Polling | `hx-trigger="every 5s"` |
|
||||
| Loading states | `hx-indicator` |
|
||||
|
||||
### When JS is Required
|
||||
|
||||
| Use Case | Why JS Required |
|
||||
|----------|-----------------|
|
||||
| Modal show/hide | DOM manipulation |
|
||||
| Toast notifications | Dynamic element creation |
|
||||
| Clipboard operations | `navigator.clipboard` API |
|
||||
| Keyboard shortcuts | `keydown` event handling |
|
||||
| Complex animations | GSAP or custom |
|
||||
|
||||
---
|
||||
|
||||
## 📦 LOCAL ASSETS ONLY - NO CDN
|
||||
|
||||
## Testing
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
ui/suite/js/vendor/
|
||||
├── htmx.min.js
|
||||
├── htmx-ws.js
|
||||
├── marked.min.js
|
||||
├── gsap.min.js
|
||||
└── livekit-client.umd.min.js
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- ✅ CORRECT -->
|
||||
<script src="js/vendor/htmx.min.js"></script>
|
||||
|
||||
<!-- ❌ WRONG -->
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 OFFICIAL ICONS - MANDATORY
|
||||
|
||||
**NEVER generate icons with LLM. Use official SVG icons:**
|
||||
|
||||
```
|
||||
ui/suite/assets/icons/
|
||||
├── gb-logo.svg # Main GB logo
|
||||
├── gb-bot.svg # Bot/assistant
|
||||
├── gb-analytics.svg # Analytics
|
||||
├── gb-calendar.svg # Calendar
|
||||
├── gb-chat.svg # Chat
|
||||
├── gb-drive.svg # File storage
|
||||
├── gb-mail.svg # Email
|
||||
├── gb-meet.svg # Video meetings
|
||||
├── gb-tasks.svg # Task management
|
||||
└── ...
|
||||
```
|
||||
|
||||
All icons use `stroke="currentColor"` for CSS theming.
|
||||
|
||||
---
|
||||
|
||||
## 🔒 SECURITY ARCHITECTURE
|
||||
|
||||
### Centralized Auth Engine
|
||||
|
||||
All authentication is handled by `security-bootstrap.js` which MUST be loaded immediately after HTMX:
|
||||
|
||||
```html
|
||||
<head>
|
||||
<!-- 1. HTMX first -->
|
||||
<script src="js/vendor/htmx.min.js"></script>
|
||||
<script src="js/vendor/htmx-ws.js"></script>
|
||||
|
||||
<!-- 2. Security bootstrap immediately after -->
|
||||
<script src="js/security-bootstrap.js"></script>
|
||||
|
||||
<!-- 3. Other scripts -->
|
||||
<script src="js/api-client.js"></script>
|
||||
</head>
|
||||
```
|
||||
|
||||
### DO NOT Duplicate Auth Logic
|
||||
|
||||
```javascript
|
||||
// ❌ WRONG - Don't add auth headers manually
|
||||
fetch("/api/data", {
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
});
|
||||
|
||||
// ✅ CORRECT - Let security-bootstrap.js handle it
|
||||
fetch("/api/data");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 DESIGN SYSTEM
|
||||
|
||||
### Layout Standards
|
||||
|
||||
```css
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-panel {
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
```
|
||||
|
||||
### Theme Variables Required
|
||||
|
||||
```css
|
||||
[data-theme="your-theme"] {
|
||||
--bg: #0a0a0a;
|
||||
--surface: #161616;
|
||||
--surface-hover: #1e1e1e;
|
||||
--border: #2a2a2a;
|
||||
--text: #ffffff;
|
||||
--text-secondary: #888888;
|
||||
--primary: #c5f82a;
|
||||
--success: #22c55e;
|
||||
--warning: #f59e0b;
|
||||
--error: #ef4444;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ CODE PATTERNS
|
||||
|
||||
### Error Handling
|
||||
|
||||
```rust
|
||||
// ❌ WRONG
|
||||
let value = something.unwrap();
|
||||
|
||||
// ✅ CORRECT
|
||||
let value = something?;
|
||||
let value = something.ok_or_else(|| Error::NotFound)?;
|
||||
```
|
||||
|
||||
### Self Usage
|
||||
|
||||
```rust
|
||||
impl MyStruct {
|
||||
fn new() -> Self { Self { } } // ✅ Not MyStruct
|
||||
}
|
||||
```
|
||||
|
||||
### Format Strings
|
||||
|
||||
```rust
|
||||
format!("Hello {name}") // ✅ Not format!("{}", name)
|
||||
```
|
||||
|
||||
### Derive Eq with PartialEq
|
||||
|
||||
```rust
|
||||
#[derive(PartialEq, Eq)] // ✅ Always both
|
||||
struct MyStruct { }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 KEY DEPENDENCIES
|
||||
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| axum | 0.7.5 | Web framework |
|
||||
| reqwest | 0.12 | HTTP client |
|
||||
| tokio | 1.41 | Async runtime |
|
||||
| askama | 0.12 | HTML Templates |
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For complete documentation, guides, and API references:
|
||||
|
||||
- **[docs.pragmatismo.com.br](https://docs.pragmatismo.com.br)** - Full online documentation
|
||||
- **[BotBook](./botbook)** - Local comprehensive guide
|
||||
- **[General Bots Repository](https://github.com/GeneralBots/BotServer)** - Main project repository
|
||||
|
||||
---
|
||||
|
||||
## 🔑 REMEMBER
|
||||
|
||||
- **ZERO WARNINGS** - Every clippy warning must be fixed
|
||||
- **NO ALLOW IN CODE** - Never use #[allow()] in source files
|
||||
- **NO DEAD CODE** - Delete unused code
|
||||
- **NO UNWRAP/EXPECT** - Use ? operator
|
||||
- **HTMX first** - Minimize JS, delegate to server
|
||||
- **Local assets** - No CDN, all vendor files local
|
||||
- **No business logic** - All logic in botserver
|
||||
- **HTML responses** - Server returns fragments, not JSON
|
||||
- **Version 6.2.0** - do not change without approval
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
use log::info;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ use axum::{
|
|||
http::{Request, StatusCode},
|
||||
response::{Html, IntoResponse, Response},
|
||||
routing::{any, get},
|
||||
Json, Router,
|
||||
Router,
|
||||
};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use log::{debug, error, info, warn};
|
||||
use log::{debug, error, info};
|
||||
#[cfg(feature = "embed-ui")]
|
||||
use rust_embed::RustEmbed;
|
||||
use serde::Deserialize;
|
||||
|
|
@ -20,6 +20,7 @@ use tokio_tungstenite::{
|
|||
connect_async_tls_with_config, tungstenite,
|
||||
tungstenite::protocol::Message as TungsteniteMessage,
|
||||
};
|
||||
#[cfg(not(feature = "embed-ui"))]
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
|
||||
#[cfg(feature = "embed-ui")]
|
||||
|
|
@ -129,98 +130,8 @@ const ROOT_FILES: &[&str] = &[
|
|||
"single.gbui",
|
||||
];
|
||||
|
||||
pub async fn index(OriginalUri(uri): OriginalUri) -> Response {
|
||||
let path = uri.path();
|
||||
|
||||
// Check if path contains static asset directories - serve them directly
|
||||
let path_lower = path.to_lowercase();
|
||||
if path_lower.contains("/js/")
|
||||
|| path_lower.contains("/css/")
|
||||
|| path_lower.contains("/vendor/")
|
||||
|| path_lower.contains("/assets/")
|
||||
|| path_lower.contains("/public/")
|
||||
|| path_lower.contains("/partials/")
|
||||
|| path_lower.ends_with(".js")
|
||||
|| path_lower.ends_with(".css")
|
||||
|| path_lower.ends_with(".png")
|
||||
|| path_lower.ends_with(".jpg")
|
||||
|| path_lower.ends_with(".jpeg")
|
||||
|| path_lower.ends_with(".gif")
|
||||
|| path_lower.ends_with(".svg")
|
||||
|| path_lower.ends_with(".ico")
|
||||
|| path_lower.ends_with(".woff")
|
||||
|| path_lower.ends_with(".woff2")
|
||||
|| path_lower.ends_with(".ttf")
|
||||
|| path_lower.ends_with(".eot")
|
||||
|| path_lower.ends_with(".mp4")
|
||||
|| path_lower.ends_with(".webm")
|
||||
|| path_lower.ends_with(".mp3")
|
||||
|| path_lower.ends_with(".wav")
|
||||
{
|
||||
// Remove bot name prefix if present (e.g., /edu/suite/js/file.js -> suite/js/file.js)
|
||||
let path_parts: Vec<&str> = path.split('/').collect();
|
||||
let fs_path = if path_parts.len() > 1 {
|
||||
let mut start_idx = 1;
|
||||
let known_dirs = ["suite", "js", "css", "vendor", "assets", "public", "partials", "settings", "auth", "about", "drive", "chat", "tasks", "admin", "mail", "calendar", "meet", "docs", "sheet", "slides", "paper", "research", "sources", "learn", "analytics", "dashboards", "monitoring", "people", "crm", "tickets", "billing", "products", "video", "player", "canvas", "social", "project", "goals", "workspace", "designer"];
|
||||
|
||||
if path_parts.len() > start_idx && !known_dirs.contains(&path_parts[start_idx]) {
|
||||
start_idx += 1;
|
||||
}
|
||||
|
||||
path_parts[start_idx..].join("/")
|
||||
} else {
|
||||
path.to_string()
|
||||
};
|
||||
|
||||
let full_path = get_ui_root().join(&fs_path);
|
||||
|
||||
debug!("index: Serving static file: {} -> {:?} (fs_path: {})", path, full_path, fs_path);
|
||||
|
||||
#[cfg(feature = "embed-ui")]
|
||||
{
|
||||
let asset_path = fs_path.trim_start_matches('/');
|
||||
if let Some(content) = Assets::get(asset_path) {
|
||||
let mime = mime_guess::from_path(asset_path).first_or_octet_stream();
|
||||
return ([(axum::http::header::CONTENT_TYPE, mime.as_ref())], content.data).into_response();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "embed-ui"))]
|
||||
{
|
||||
if let Ok(bytes) = tokio::fs::read(&full_path).await {
|
||||
let mime = mime_guess::from_path(&full_path).first_or_octet_stream();
|
||||
return (StatusCode::OK, [("content-type", mime.as_ref())], bytes).into_response();
|
||||
}
|
||||
}
|
||||
|
||||
warn!("index: Static file not found: {} -> {:?}", path, full_path);
|
||||
return StatusCode::NOT_FOUND.into_response();
|
||||
}
|
||||
|
||||
let path_parts: Vec<&str> = path.split('/').collect();
|
||||
let bot_name = path_parts
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|part| {
|
||||
!part.is_empty()
|
||||
&& **part != "chat"
|
||||
&& **part != "app"
|
||||
&& **part != "ws"
|
||||
&& **part != "ui"
|
||||
&& **part != "api"
|
||||
&& **part != "auth"
|
||||
&& **part != "suite"
|
||||
&& !part.ends_with(".js")
|
||||
&& !part.ends_with(".css")
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
info!(
|
||||
"index: Extracted bot_name: {:?} from path: {}",
|
||||
bot_name,
|
||||
path
|
||||
);
|
||||
serve_suite(bot_name).await.into_response()
|
||||
pub async fn index() -> impl IntoResponse {
|
||||
serve_suite().await
|
||||
}
|
||||
|
||||
pub fn get_ui_root() -> PathBuf {
|
||||
|
|
@ -285,7 +196,7 @@ pub async fn serve_minimal() -> impl IntoResponse {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn serve_suite(bot_name: Option<String>) -> impl IntoResponse {
|
||||
pub async fn serve_suite() -> impl IntoResponse {
|
||||
let raw_html_res = {
|
||||
#[cfg(feature = "embed-ui")]
|
||||
{
|
||||
|
|
@ -324,32 +235,6 @@ pub async fn serve_suite(bot_name: Option<String>) -> impl IntoResponse {
|
|||
#[allow(unused_mut)] // Mutable required for feature-gated blocks
|
||||
let mut html = raw_html;
|
||||
|
||||
// Inject base tag and bot_name into the page
|
||||
if let Some(head_end) = html.find("</head>") {
|
||||
// Set base href to include bot context if present (e.g., /edu/)
|
||||
let base_href = if let Some(ref name) = bot_name {
|
||||
format!("/{}/", name)
|
||||
} else {
|
||||
"/".to_string()
|
||||
};
|
||||
let base_tag = format!(r#"<base href="{}">"#, base_href);
|
||||
html.insert_str(head_end, &base_tag);
|
||||
|
||||
if let Some(name) = bot_name {
|
||||
info!("serve_suite: Injecting bot_name '{}' into page with base href='{}'", name, base_href);
|
||||
let bot_script = format!(
|
||||
r#"<script>window.__INITIAL_BOT_NAME__ = "{}";</script>"#,
|
||||
&name
|
||||
);
|
||||
html.insert_str(head_end + base_tag.len(), &bot_script);
|
||||
info!("serve_suite: Successfully injected base tag and bot_name script");
|
||||
} else {
|
||||
info!("serve_suite: Successfully injected base tag (no bot_name)");
|
||||
}
|
||||
} else {
|
||||
error!("serve_suite: Failed to find </head> tag to inject content");
|
||||
}
|
||||
|
||||
// Core Apps
|
||||
#[cfg(not(feature = "chat"))]
|
||||
{
|
||||
|
|
@ -567,26 +452,14 @@ async fn health(State(state): State<AppState>) -> (StatusCode, axum::Json<serde_
|
|||
}
|
||||
}
|
||||
|
||||
async fn api_health(State(state): State<AppState>) -> (StatusCode, axum::Json<serde_json::Value>) {
|
||||
if state.health_check().await {
|
||||
(
|
||||
StatusCode::OK,
|
||||
axum::Json(serde_json::json!({
|
||||
"status": "ok",
|
||||
"botserver": "healthy",
|
||||
"version": env!("CARGO_PKG_VERSION")
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
axum::Json(serde_json::json!({
|
||||
"status": "error",
|
||||
"botserver": "unhealthy",
|
||||
"version": env!("CARGO_PKG_VERSION")
|
||||
})),
|
||||
)
|
||||
}
|
||||
async fn api_health() -> (StatusCode, axum::Json<serde_json::Value>) {
|
||||
(
|
||||
StatusCode::OK,
|
||||
axum::Json(serde_json::json!({
|
||||
"status": "ok",
|
||||
"version": env!("CARGO_PKG_VERSION")
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
fn extract_app_context(headers: &axum::http::HeaderMap, path: &str) -> Option<String> {
|
||||
|
|
@ -715,7 +588,6 @@ async fn build_proxy_response(resp: reqwest::Response) -> Response<Body> {
|
|||
fn create_api_router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/health", get(api_health))
|
||||
.route("/client-error", axum::routing::post(handle_client_error))
|
||||
.fallback(any(proxy_api))
|
||||
}
|
||||
|
||||
|
|
@ -723,36 +595,6 @@ fn create_api_router() -> Router<AppState> {
|
|||
struct WsQuery {
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
bot_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ClientError {
|
||||
message: String,
|
||||
stack: Option<String>,
|
||||
source: String,
|
||||
url: String,
|
||||
user_agent: String,
|
||||
timestamp: String,
|
||||
}
|
||||
|
||||
async fn handle_client_error(Json(error): Json<ClientError>) -> impl IntoResponse {
|
||||
warn!(
|
||||
"CLIENT:{}: {} at {} ({}) - {}",
|
||||
error.source.to_uppercase(),
|
||||
error.message,
|
||||
error.url,
|
||||
error.timestamp,
|
||||
error.user_agent
|
||||
);
|
||||
|
||||
if let Some(stack) = &error.stack {
|
||||
if !stack.is_empty() {
|
||||
warn!("CLIENT:STACK: {}", stack);
|
||||
}
|
||||
}
|
||||
|
||||
StatusCode::OK
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
|
|
@ -763,31 +605,9 @@ struct OptionalWsQuery {
|
|||
async fn ws_proxy(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<AppState>,
|
||||
OriginalUri(uri): OriginalUri,
|
||||
Query(params): Query<WsQuery>,
|
||||
) -> impl IntoResponse {
|
||||
// Extract bot_name from URL path (e.g., /edu, /chat/edu)
|
||||
let path_parts: Vec<&str> = uri.path().split('/').collect();
|
||||
let bot_name = params
|
||||
.bot_name
|
||||
.filter(|name| name != "ws" && !name.is_empty())
|
||||
.or_else(|| {
|
||||
// Try to extract from path like /edu or /app/edu
|
||||
path_parts
|
||||
.iter()
|
||||
.find(|part| {
|
||||
!part.is_empty() && **part != "chat" && **part != "app" && **part != "ws"
|
||||
})
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.unwrap_or_else(|| "default".to_string());
|
||||
|
||||
let params_with_bot = WsQuery {
|
||||
bot_name: Some(bot_name),
|
||||
..params
|
||||
};
|
||||
|
||||
ws.on_upgrade(move |socket| handle_ws_proxy(socket, state, params_with_bot))
|
||||
ws.on_upgrade(move |socket| handle_ws_proxy(socket, state, params))
|
||||
}
|
||||
|
||||
async fn ws_task_progress_proxy(
|
||||
|
|
@ -941,15 +761,14 @@ async fn handle_task_progress_ws_proxy(
|
|||
#[allow(clippy::too_many_lines)]
|
||||
async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQuery) {
|
||||
let backend_url = format!(
|
||||
"{}/ws?session_id={}&user_id={}&bot_name={}",
|
||||
"{}/ws?session_id={}&user_id={}",
|
||||
state
|
||||
.client
|
||||
.base_url()
|
||||
.replace("https://", "wss://")
|
||||
.replace("http://", "ws://"),
|
||||
params.session_id,
|
||||
params.user_id,
|
||||
params.bot_name.unwrap_or_else(|| "default".to_string())
|
||||
params.user_id
|
||||
);
|
||||
|
||||
info!("Proxying WebSocket to: {backend_url}");
|
||||
|
|
@ -1156,14 +975,11 @@ fn add_static_routes(router: Router<AppState>, _suite_path: &Path) -> Router<App
|
|||
#[cfg(not(feature = "embed-ui"))]
|
||||
{
|
||||
let mut r = router;
|
||||
// Serve suite directories at BOTH root level and /suite/{dir} path
|
||||
// This allows HTML files to reference js/vendor/file.js directly
|
||||
for dir in SUITE_DIRS {
|
||||
let path = _suite_path.join(dir);
|
||||
info!("Adding route for /{} -> {:?}", dir, path);
|
||||
r = r.nest_service(&format!("/{dir}"), ServeDir::new(path.clone()));
|
||||
info!("Adding route for /suite/{} -> {:?}", dir, path);
|
||||
r = r.nest_service(&format!("/suite/{dir}"), ServeDir::new(path.clone()));
|
||||
r = r
|
||||
.nest_service(&format!("/suite/{dir}"), ServeDir::new(path.clone()))
|
||||
.nest_service(&format!("/{dir}"), ServeDir::new(path));
|
||||
}
|
||||
|
||||
for file in ROOT_FILES {
|
||||
|
|
@ -1186,14 +1002,12 @@ pub fn configure_router() -> Router {
|
|||
.nest("/ui", create_ui_router())
|
||||
.nest("/ws", create_ws_router())
|
||||
.nest("/apps", create_apps_router())
|
||||
.route("/", get(index))
|
||||
.route("/minimal", get(serve_minimal))
|
||||
.route("/suite", get(serve_suite))
|
||||
.route("/favicon.ico", get(serve_favicon));
|
||||
|
||||
router = add_static_routes(router, &suite_path);
|
||||
|
||||
router
|
||||
.route("/", get(index))
|
||||
.route("/minimal", get(serve_minimal))
|
||||
.route("/suite", get(serve_suite))
|
||||
.fallback(get(index))
|
||||
.with_state(state)
|
||||
router.fallback(get(index)).with_state(state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Login - General Bots</title>
|
||||
<script src="/suite/js/vendor/htmx.min.js"></script>
|
||||
<script src="/suite/js/vendor/htmx-json-enc.js"></script>
|
||||
<script src="/js/vendor/htmx.min.js"></script>
|
||||
<script src="/js/vendor/htmx-json-enc.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #3b82f6;
|
||||
|
|
@ -1264,18 +1264,12 @@
|
|||
|
||||
// Successful login - redirect
|
||||
if (response.redirect || response.success) {
|
||||
// Check for redirect parameter in URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const redirectUrl = urlParams.get('redirect') || response.redirect;
|
||||
window.location.href = redirectUrl ? redirectUrl : window.location.origin + "/#chat";
|
||||
window.location.href = response.redirect || "/";
|
||||
}
|
||||
} catch (e) {
|
||||
// If response is not JSON, check for redirect header
|
||||
if (event.detail.xhr.status === 200) {
|
||||
// Check for redirect parameter in URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const redirectUrl = urlParams.get('redirect');
|
||||
window.location.href = redirectUrl ? redirectUrl : window.location.origin + "/#chat";
|
||||
window.location.href = "/";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,6 @@
|
|||
<script src="/js/vendor/htmx-ws.js"></script>
|
||||
<script src="/js/vendor/htmx-json-enc.js"></script>
|
||||
|
||||
<!-- ERROR REPORTER - Captures JS errors and sends to server log -->
|
||||
<script src="/js/error-reporter.js"></script>
|
||||
|
||||
<!-- i18n -->
|
||||
<script src="/js/i18n.js"></script>
|
||||
|
||||
|
|
@ -119,7 +116,7 @@
|
|||
href="#tasks"
|
||||
class="app-item"
|
||||
role="menuitem"
|
||||
hx-get="/suite/tasks/autotask.html"
|
||||
hx-get="/tasks/tasks.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="true"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<link rel="stylesheet" href="/suite/calendar/calendar.css" />
|
||||
<link rel="stylesheet" href="calendar/calendar.css" />
|
||||
|
||||
<!-- Calendar - Event Management -->
|
||||
<div class="calendar-container" id="calendar-app">
|
||||
|
|
|
|||
|
|
@ -110,97 +110,97 @@
|
|||
|
||||
/* Messages Area */
|
||||
#messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--accent, #3b82f6) var(--surface, #1a1a24);
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--accent, #3b82f6) var(--surface, #1a1a24);
|
||||
}
|
||||
|
||||
/* Custom scrollbar for markers */
|
||||
#messages::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
#messages::-webkit-scrollbar-track {
|
||||
background: var(--surface, #1a1a24);
|
||||
border-radius: 3px;
|
||||
background: var(--surface, #1a1a24);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#messages::-webkit-scrollbar-thumb {
|
||||
background: var(--accent, #3b82f6);
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--surface, #1a1a24);
|
||||
background: var(--accent, #3b82f6);
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--surface, #1a1a24);
|
||||
}
|
||||
|
||||
#messages::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-hover, #2563eb);
|
||||
background: var(--accent-hover, #2563eb);
|
||||
}
|
||||
|
||||
/* Scrollbar markers container */
|
||||
.scrollbar-markers {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 2px;
|
||||
width: 8px;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 2px;
|
||||
width: 8px;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.scrollbar-marker {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--accent, #3b82f6);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.5);
|
||||
z-index: 11;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--accent, #3b82f6);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.5);
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.scrollbar-marker:hover {
|
||||
transform: scale(1.5);
|
||||
background: var(--accent-hover, #2563eb);
|
||||
box-shadow: 0 0 8px var(--accent, #3b82f6);
|
||||
transform: scale(1.5);
|
||||
background: var(--accent-hover, #2563eb);
|
||||
box-shadow: 0 0 8px var(--accent, #3b82f6);
|
||||
}
|
||||
|
||||
.scrollbar-marker.user-marker {
|
||||
background: var(--accent, #3b82f6);
|
||||
background: var(--accent, #3b82f6);
|
||||
}
|
||||
|
||||
.scrollbar-marker.bot-marker {
|
||||
background: var(--success, #22c55e);
|
||||
background: var(--success, #22c55e);
|
||||
}
|
||||
|
||||
.scrollbar-marker-tooltip {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
transform: translateY(-50%);
|
||||
background: var(--surface, #1a1a24);
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text, #ffffff);
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: none;
|
||||
z-index: 12;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
transform: translateY(-50%);
|
||||
background: var(--surface, #1a1a24);
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text, #ffffff);
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: none;
|
||||
z-index: 12;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.scrollbar-marker:hover .scrollbar-marker-tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Message Styles */
|
||||
|
|
@ -399,28 +399,28 @@ footer {
|
|||
}
|
||||
|
||||
.mention-results {
|
||||
overflow-y: auto;
|
||||
max-height: 250px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--accent, #3b82f6) transparent;
|
||||
overflow-y: auto;
|
||||
max-height: 250px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--accent, #3b82f6) transparent;
|
||||
}
|
||||
|
||||
.mention-results::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.mention-results::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.mention-results::-webkit-scrollbar-thumb {
|
||||
background: var(--accent, #3b82f6);
|
||||
border-radius: 2px;
|
||||
background: var(--accent, #3b82f6);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.mention-results::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-hover, #2563eb);
|
||||
background: var(--accent-hover, #2563eb);
|
||||
}
|
||||
|
||||
.mention-item {
|
||||
|
|
@ -574,13 +574,6 @@ footer {
|
|||
gap: 8px;
|
||||
}
|
||||
|
||||
.entity-card-btm {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.entity-card-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
|
|
@ -650,26 +643,12 @@ form.input-container {
|
|||
font-size: 15px;
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
caret-color: var(--accent, #3b82f6);
|
||||
}
|
||||
|
||||
#messageInput:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@keyframes cursor-blink {
|
||||
0%, 50% {
|
||||
caret-color: var(--accent, #3b82f6);
|
||||
}
|
||||
51%, 100% {
|
||||
caret-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
#messageInput:focus {
|
||||
animation: cursor-blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
#messageInput::placeholder {
|
||||
color: #888888;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<link rel="stylesheet" href="/suite/chat/chat.css" />
|
||||
<link rel="stylesheet" href="chat/chat.css" />
|
||||
|
||||
<div class="chat-layout" id="chat-app">
|
||||
<main id="messages"></main>
|
||||
|
|
@ -28,7 +28,6 @@
|
|||
id="voiceBtn"
|
||||
title="Voice"
|
||||
data-i18n-title="chat-voice"
|
||||
style="display: none"
|
||||
>
|
||||
🎤
|
||||
</button>
|
||||
|
|
@ -82,7 +81,7 @@
|
|||
|
||||
var WS_BASE_URL =
|
||||
window.location.protocol === "https:" ? "wss://" : "ws://";
|
||||
var WS_URL = WS_BASE_URL + window.location.host + "/ws/chat";
|
||||
var WS_URL = WS_BASE_URL + window.location.host;
|
||||
|
||||
var MessageType = {
|
||||
EXTERNAL: 0,
|
||||
|
|
@ -149,7 +148,6 @@
|
|||
var currentSessionId = null;
|
||||
var currentUserId = null;
|
||||
var currentBotId = "default";
|
||||
var currentBotName = "default";
|
||||
var isStreaming = false;
|
||||
var streamingMessageId = null;
|
||||
var currentStreamingContent = "";
|
||||
|
|
@ -735,12 +733,10 @@
|
|||
|
||||
var url =
|
||||
WS_URL +
|
||||
"?session_id=" +
|
||||
"/ws?session_id=" +
|
||||
currentSessionId +
|
||||
"&user_id=" +
|
||||
currentUserId +
|
||||
"&bot_name=" +
|
||||
currentBotName;
|
||||
currentUserId;
|
||||
ws = new WebSocket(url);
|
||||
|
||||
ws.onopen = function () {
|
||||
|
|
@ -751,43 +747,9 @@
|
|||
ws.onmessage = function (event) {
|
||||
try {
|
||||
var data = JSON.parse(event.data);
|
||||
console.log("Chat WebSocket received:", data);
|
||||
|
||||
// Ignore connection confirmation
|
||||
if (data.type === "connected") return;
|
||||
|
||||
// Ignore system events (theme changes, etc)
|
||||
if (data.event) {
|
||||
console.log(
|
||||
"System event received, ignoring:",
|
||||
data.event,
|
||||
data,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if content contains theme change events (JSON strings)
|
||||
if (data.content && typeof data.content === "string") {
|
||||
try {
|
||||
var contentObj = JSON.parse(data.content);
|
||||
if (contentObj.event === "change_theme") {
|
||||
console.log(
|
||||
"Theme change event in content, ignoring:",
|
||||
contentObj,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// Content is not JSON, continue processing
|
||||
}
|
||||
}
|
||||
|
||||
// Only process bot responses
|
||||
if (data.message_type === MessageType.BOT_RESPONSE) {
|
||||
console.log("Processing bot response:", data);
|
||||
processMessage(data);
|
||||
} else {
|
||||
console.log("Ignoring non-bot message:", data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("WS message error:", e);
|
||||
|
|
@ -808,12 +770,7 @@
|
|||
}
|
||||
|
||||
function initChat() {
|
||||
// Just proceed with chat initialization - no auth check
|
||||
proceedWithChatInit();
|
||||
}
|
||||
|
||||
function proceedWithChatInit() {
|
||||
var botName = window.__INITIAL_BOT_NAME__ || "default";
|
||||
var botName = "default";
|
||||
fetch("/api/auth?bot_name=" + encodeURIComponent(botName))
|
||||
.then(function (response) {
|
||||
return response.json();
|
||||
|
|
@ -822,19 +779,17 @@
|
|||
currentUserId = auth.user_id;
|
||||
currentSessionId = auth.session_id;
|
||||
currentBotId = auth.bot_id || "default";
|
||||
currentBotName = botName;
|
||||
console.log("Auth:", {
|
||||
currentUserId: currentUserId,
|
||||
currentSessionId: currentSessionId,
|
||||
currentBotId: currentBotId,
|
||||
currentBotName: currentBotName,
|
||||
});
|
||||
connectWebSocket();
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.error("Auth failed:", e);
|
||||
notify("Failed to connect to chat server", "error");
|
||||
setTimeout(proceedWithChatInit, 3000);
|
||||
setTimeout(initChat, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -156,34 +156,34 @@
|
|||
<!-- Application initialization -->
|
||||
<script>
|
||||
// Initialize application
|
||||
(function initApp() {
|
||||
"use strict";
|
||||
(function initApp() {
|
||||
"use strict";
|
||||
|
||||
// Initialize ThemeManager
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
console.log("🚀 Initializing General Bots Desktop...");
|
||||
// Initialize ThemeManager
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
console.log("🚀 Initializing General Bots Desktop...");
|
||||
|
||||
// Initialize theme system
|
||||
if (window.ThemeManager) {
|
||||
ThemeManager.init();
|
||||
console.log("✓ Theme Manager initialized");
|
||||
} else {
|
||||
console.warn("⚠ ThemeManager not found");
|
||||
}
|
||||
|
||||
// Initialize apps menu
|
||||
initAppsMenu();
|
||||
|
||||
// Hide loading overlay after initialization
|
||||
setTimeout(() => {
|
||||
const loadingOverlay =
|
||||
document.getElementById("loadingOverlay");
|
||||
if (loadingOverlay) {
|
||||
loadingOverlay.classList.add("hidden");
|
||||
console.log("✓ Application ready");
|
||||
// Initialize theme system
|
||||
if (window.ThemeManager) {
|
||||
ThemeManager.init();
|
||||
console.log("✓ Theme Manager initialized");
|
||||
} else {
|
||||
console.warn("⚠ ThemeManager not found");
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Initialize apps menu
|
||||
initAppsMenu();
|
||||
|
||||
// Hide loading overlay after initialization
|
||||
setTimeout(() => {
|
||||
const loadingOverlay =
|
||||
document.getElementById("loadingOverlay");
|
||||
if (loadingOverlay) {
|
||||
loadingOverlay.classList.add("hidden");
|
||||
console.log("✓ Application ready");
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Apps menu functionality
|
||||
function initAppsMenu() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!-- Drive - File Management v1.0 -->
|
||||
<link rel="stylesheet" href="/suite/drive/drive.css" />
|
||||
<link rel="stylesheet" href="drive/drive.css" />
|
||||
|
||||
<div class="drive-container" id="drive-app">
|
||||
<!-- Sidebar -->
|
||||
|
|
@ -348,4 +348,4 @@
|
|||
<!-- Context Menu (dynamically populated by JS) -->
|
||||
<div id="context-menu" class="context-menu hidden"></div>
|
||||
|
||||
<script src="/suite/drive/drive.js"></script>
|
||||
<script src="drive/drive.js"></script>
|
||||
|
|
|
|||
|
|
@ -324,7 +324,7 @@
|
|||
<a
|
||||
href="#tasks"
|
||||
class="app-card"
|
||||
hx-get="/suite/tasks/autotask.html"
|
||||
hx-get="/suite/tasks/tasks.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#tasks"
|
||||
>
|
||||
|
|
|
|||
3743
ui/suite/index.html
3743
ui/suite/index.html
File diff suppressed because it is too large
Load diff
|
|
@ -351,8 +351,8 @@
|
|||
this.clearAuth();
|
||||
this.emit("tokenExpired");
|
||||
|
||||
const currentPath = window.location.pathname + window.location.hash;
|
||||
if (!window.location.pathname.startsWith("/auth/")) {
|
||||
const currentPath = window.location.pathname;
|
||||
if (!currentPath.startsWith("/auth/")) {
|
||||
window.location.href =
|
||||
"/auth/login.html?expired=1&redirect=" +
|
||||
encodeURIComponent(currentPath);
|
||||
|
|
|
|||
|
|
@ -47,15 +47,12 @@ function applyProductConfig(config) {
|
|||
// Check if we have compiled_features info to filter even further
|
||||
// This ensures we don't show apps that are enabled in config but not compiled in binary
|
||||
if (config.compiled_features && Array.isArray(config.compiled_features)) {
|
||||
const compiledSet = new Set(
|
||||
config.compiled_features.map((f) => f.toLowerCase()),
|
||||
);
|
||||
effectiveApps = effectiveApps.filter(
|
||||
(app) =>
|
||||
compiledSet.has(app.toLowerCase()) ||
|
||||
app.toLowerCase() === "settings" ||
|
||||
app.toLowerCase() === "auth" ||
|
||||
app.toLowerCase() === "admin", // Admin usually contains settings which is always there
|
||||
const compiledSet = new Set(config.compiled_features.map(f => f.toLowerCase()));
|
||||
effectiveApps = effectiveApps.filter(app =>
|
||||
compiledSet.has(app.toLowerCase()) ||
|
||||
app.toLowerCase() === 'settings' ||
|
||||
app.toLowerCase() === 'auth' ||
|
||||
app.toLowerCase() === 'admin' // Admin usually contains settings which is always there
|
||||
);
|
||||
|
||||
// Also call a helper to hide UI elements for non-compiled features explicitly
|
||||
|
|
@ -64,33 +61,6 @@ function applyProductConfig(config) {
|
|||
}
|
||||
|
||||
filterAppsByConfig(effectiveApps);
|
||||
|
||||
// Check if there are any visible apps after filtering
|
||||
const hasVisibleApps = effectiveApps.length > 0;
|
||||
|
||||
// Hide apps menu button if menu launcher is disabled or if there are no apps to show
|
||||
if (config.menu_launcher_enabled === false || !hasVisibleApps) {
|
||||
const appsButton = document.getElementById("appsButton");
|
||||
if (appsButton) {
|
||||
appsButton.style.display = "none";
|
||||
}
|
||||
const appsMenuContainer = document.querySelector(".apps-menu-container");
|
||||
if (appsMenuContainer) {
|
||||
appsMenuContainer.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide omnibox if search is disabled
|
||||
if (config.search_enabled === false) {
|
||||
const omnibox = document.getElementById("omnibox");
|
||||
if (omnibox) {
|
||||
omnibox.style.display = "none";
|
||||
}
|
||||
const headerCenter = document.querySelector(".header-center");
|
||||
if (headerCenter) {
|
||||
headerCenter.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Apply custom logo
|
||||
|
|
@ -125,25 +95,21 @@ function applyProductConfig(config) {
|
|||
// Hide UI elements that require features not compiled in the binary
|
||||
function hideNonCompiledUI(compiledSet) {
|
||||
// Hide elements with data-feature attribute that aren't in compiled set
|
||||
document.querySelectorAll("[data-feature]").forEach((el) => {
|
||||
const feature = el.getAttribute("data-feature").toLowerCase();
|
||||
document.querySelectorAll('[data-feature]').forEach(el => {
|
||||
const feature = el.getAttribute('data-feature').toLowerCase();
|
||||
// Allow settings/admin as they are usually core
|
||||
if (
|
||||
!compiledSet.has(feature) &&
|
||||
feature !== "settings" &&
|
||||
feature !== "admin"
|
||||
) {
|
||||
el.style.display = "none";
|
||||
el.classList.add("hidden-uncompiled");
|
||||
if (!compiledSet.has(feature) && feature !== 'settings' && feature !== 'admin') {
|
||||
el.style.display = 'none';
|
||||
el.classList.add('hidden-uncompiled');
|
||||
}
|
||||
});
|
||||
|
||||
// Also look for specific sections that might map to features
|
||||
// e.g. .feature-mail, .feature-meet classes
|
||||
compiledSet.forEach((feature) => {
|
||||
// This loop defines what IS available.
|
||||
compiledSet.forEach(feature => {
|
||||
// This loop defines what IS available.
|
||||
// Logic should be inverse: find all feature- classes and hide if not in set
|
||||
// But scanning all classes is expensive.
|
||||
// But scanning all classes is expensive.
|
||||
// Better to rely on data-feature or explicit app hiding which filterAppsByConfig does.
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,135 +0,0 @@
|
|||
(function() {
|
||||
'use strict';
|
||||
|
||||
const MAX_ERRORS = 50;
|
||||
const REPORT_ENDPOINT = '/api/client-errors';
|
||||
let errorQueue = [];
|
||||
let isReporting = false;
|
||||
|
||||
function formatError(error, context = {}) {
|
||||
return {
|
||||
type: error.name || 'Error',
|
||||
message: error.message || String(error),
|
||||
stack: error.stack,
|
||||
url: window.location.href,
|
||||
userAgent: navigator.userAgent,
|
||||
timestamp: new Date().toISOString(),
|
||||
context: context
|
||||
};
|
||||
}
|
||||
|
||||
async function reportErrors() {
|
||||
if (isReporting || errorQueue.length === 0) return;
|
||||
|
||||
isReporting = true;
|
||||
const errorsToReport = errorQueue.splice(0, MAX_ERRORS);
|
||||
errorQueue = [];
|
||||
|
||||
try {
|
||||
const response = await fetch(REPORT_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ errors: errorsToReport })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ErrorReporter] Failed to send errors:', response.status);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[ErrorReporter] Failed to send errors:', e.message);
|
||||
errorQueue.unshift(...errorsToReport);
|
||||
} finally {
|
||||
isReporting = false;
|
||||
|
||||
if (errorQueue.length > 0) {
|
||||
setTimeout(reportErrors, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function queueError(errorData) {
|
||||
errorQueue.push(errorData);
|
||||
if (errorQueue.length >= 10) {
|
||||
reportErrors();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('error', (event) => {
|
||||
const errorData = formatError(event.error || new Error(event.message), {
|
||||
filename: event.filename,
|
||||
lineno: event.lineno,
|
||||
colno: event.colno
|
||||
});
|
||||
queueError(errorData);
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
const errorData = formatError(event.reason || new Error(String(event.reason)), {
|
||||
type: 'unhandledRejection'
|
||||
});
|
||||
queueError(errorData);
|
||||
});
|
||||
|
||||
window.ErrorReporter = {
|
||||
report: function(error, context) {
|
||||
queueError(formatError(error, context));
|
||||
},
|
||||
flush: function() {
|
||||
reportErrors();
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === 'complete') {
|
||||
setTimeout(reportErrors, 1000);
|
||||
} else {
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(reportErrors, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[ErrorReporter] Client-side error reporting initialized');
|
||||
|
||||
window.NavigationLogger = {
|
||||
log: function(from, to, method) {
|
||||
const navEvent = {
|
||||
type: 'navigation',
|
||||
from: from,
|
||||
to: to,
|
||||
method: method,
|
||||
url: window.location.href,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
queueError({
|
||||
name: 'Navigation',
|
||||
message: `${method}: ${from} -> ${to}`,
|
||||
stack: undefined
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.body.addEventListener('click', function(e) {
|
||||
const target = e.target.closest('[data-section]');
|
||||
if (target) {
|
||||
const section = target.getAttribute('data-section');
|
||||
const currentHash = window.location.hash.slice(1) || '';
|
||||
if (section !== currentHash) {
|
||||
setTimeout(() => {
|
||||
window.NavigationLogger.log(currentHash || 'home', section, 'click');
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
window.addEventListener('hashchange', function(e) {
|
||||
const oldURL = new URL(e.oldURL);
|
||||
const newURL = new URL(e.newURL);
|
||||
const fromHash = oldURL.hash.slice(1) || '';
|
||||
const toHash = newURL.hash.slice(1) || '';
|
||||
window.NavigationLogger.log(fromHash || 'home', toHash, 'hashchange');
|
||||
});
|
||||
|
||||
console.log('[NavigationLogger] Navigation tracking initialized');
|
||||
})();
|
||||
|
|
@ -203,12 +203,7 @@
|
|||
|
||||
// Handle WebSocket messages
|
||||
function handleWebSocketMessage(message) {
|
||||
const messageType = message.type || message.event;
|
||||
|
||||
// Debug logging
|
||||
console.log("handleWebSocketMessage called with:", { messageType, message });
|
||||
|
||||
switch (messageType) {
|
||||
switch (message.type) {
|
||||
case "message":
|
||||
appendMessage(message);
|
||||
break;
|
||||
|
|
@ -221,28 +216,8 @@
|
|||
case "suggestion":
|
||||
addSuggestion(message.text);
|
||||
break;
|
||||
case "change_theme":
|
||||
console.log("Processing change_theme event, not appending to chat");
|
||||
if (message.data) {
|
||||
ThemeManager.setThemeFromServer(message.data);
|
||||
|
||||
if (message.data.color1 || message.data.color2) {
|
||||
const root = document.documentElement;
|
||||
if (message.data.color1)
|
||||
root.style.setProperty("--color1", message.data.color1);
|
||||
if (message.data.color2)
|
||||
root.style.setProperty("--color2", message.data.color2);
|
||||
}
|
||||
}
|
||||
return; // Don't append theme events to chat
|
||||
default:
|
||||
// Only append unknown message types to chat if they have text content
|
||||
if (message.text || message.content) {
|
||||
console.log("Unknown message type, treating as chat message:", messageType);
|
||||
appendMessage(message);
|
||||
} else {
|
||||
console.log("Unknown message type:", messageType, message);
|
||||
}
|
||||
console.log("Unknown message type:", message.type);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -295,12 +295,6 @@
|
|||
});
|
||||
|
||||
window.addEventListener("gb:auth:expired", function (event) {
|
||||
// Check if current bot is public - if so, skip redirect
|
||||
if (window.__BOT_IS_PUBLIC__ === true) {
|
||||
console.log("[GBSecurity] Bot is public, skipping auth redirect");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"[GBSecurity] Auth expired, clearing tokens and redirecting",
|
||||
);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1343,5 +1343,5 @@
|
|||
</div>
|
||||
</dialog>
|
||||
|
||||
<link rel="stylesheet" href="/suite/mail/mail.css" />
|
||||
<script src="/suite/mail/mail.js"></script>
|
||||
<link rel="stylesheet" href="mail/mail.css" />
|
||||
<script src="mail/mail.js"></script>
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
<div class="navigation-menu" id="navigation-menu">
|
||||
<nav class="nav-menu-content">
|
||||
<a href="/auth/login"
|
||||
class="nav-menu-item nav-sign-in"
|
||||
hx-get="/auth/login"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="window.location.href='/login'">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path>
|
||||
<polyline points="10 17 15 12 10 7"></polyline>
|
||||
<line x1="15" y1="12" x2="3" y2="12"></line>
|
||||
</svg>
|
||||
<span>Sign in</span>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.navigation-menu {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
|
||||
min-width: 200px;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
animation: slideIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.nav-menu-content {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.nav-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
transition: all 0.15s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-sign-in {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.nav-sign-in:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
.nav-sign-in svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<link rel="stylesheet" href="/suite/tasks/autotask.css" />
|
||||
<link rel="stylesheet" href="tasks/autotask.css" />
|
||||
|
||||
<div class="autotask-container" data-theme="sentient">
|
||||
<!-- Top Navigation Bar -->
|
||||
|
|
@ -478,6 +478,6 @@ Examples:
|
|||
<!-- Toast Container -->
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
|
||||
<link rel="stylesheet" href="/suite/tasks/progress-panel.css" />
|
||||
<script src="/suite/tasks/progress-panel.js"></script>
|
||||
<script src="/suite/tasks/autotask.js"></script>
|
||||
<link rel="stylesheet" href="tasks/progress-panel.css" />
|
||||
<script src="tasks/progress-panel.js"></script>
|
||||
<script src="tasks/autotask.js"></script>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,304 @@
|
|||
<div class="modal-footer">
|
||||
<link rel="stylesheet" href="tasks/tasks.css" />
|
||||
|
||||
<!-- =============================================================================
|
||||
TASKS APP - Autonomous Task Management
|
||||
Respects Theme Manager - No hardcoded theme
|
||||
============================================================================= -->
|
||||
|
||||
<div class="tasks-app">
|
||||
<!-- Hidden element to load stats on page load -->
|
||||
<div
|
||||
hx-get="/api/ui/tasks/stats"
|
||||
hx-trigger="load, taskCreated from:body"
|
||||
hx-swap="innerHTML"
|
||||
style="display: none"
|
||||
></div>
|
||||
|
||||
<!-- Status Filter Pills Row -->
|
||||
<div class="status-filter-row">
|
||||
<button
|
||||
class="filter-pill"
|
||||
data-filter="complete"
|
||||
hx-get="/api/ui/tasks?filter=complete"
|
||||
hx-target="#task-list"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<span class="pill-icon">✓</span>
|
||||
<span class="pill-label" data-i18n="tasks-completed">Complete</span>
|
||||
<span class="pill-count" id="count-complete">-</span>
|
||||
</button>
|
||||
<button
|
||||
class="filter-pill active"
|
||||
data-filter="all"
|
||||
hx-get="/api/ui/tasks?filter=all"
|
||||
hx-target="#task-list"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<span class="pill-icon">📋</span>
|
||||
<span class="pill-label" data-i18n="tasks-all">All Tasks</span>
|
||||
<span class="pill-count" id="count-all">-</span>
|
||||
</button>
|
||||
<button
|
||||
class="filter-pill"
|
||||
data-filter="active"
|
||||
hx-get="/api/ui/tasks?filter=active"
|
||||
hx-target="#task-list"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<span class="pill-icon">⚡</span>
|
||||
<span class="pill-label" data-i18n="tasks-active"
|
||||
>Active Intents</span
|
||||
>
|
||||
<span class="pill-count" id="count-active">-</span>
|
||||
</button>
|
||||
<button
|
||||
class="filter-pill"
|
||||
data-filter="awaiting"
|
||||
hx-get="/api/ui/tasks?filter=awaiting"
|
||||
hx-target="#task-list"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<span class="pill-icon">⏳</span>
|
||||
<span class="pill-label" data-i18n="tasks-awaiting"
|
||||
>Awaiting Decision</span
|
||||
>
|
||||
<span class="pill-count" id="count-awaiting">-</span>
|
||||
</button>
|
||||
<button
|
||||
class="filter-pill"
|
||||
data-filter="paused"
|
||||
hx-get="/api/ui/tasks?filter=paused"
|
||||
hx-target="#task-list"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<span class="pill-icon">⏸</span>
|
||||
<span class="pill-label" data-i18n="tasks-paused">Paused</span>
|
||||
<span class="pill-count" id="count-paused">-</span>
|
||||
</button>
|
||||
<button
|
||||
class="filter-pill"
|
||||
data-filter="blocked"
|
||||
hx-get="/api/ui/tasks?filter=blocked"
|
||||
hx-target="#task-list"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<span class="pill-icon">⚠</span>
|
||||
<span class="pill-label" data-i18n="tasks-blocked"
|
||||
>Blocked/Issues</span
|
||||
>
|
||||
<span class="pill-count" id="count-blocked">-</span>
|
||||
</button>
|
||||
<div class="time-saved-badge">
|
||||
<span class="time-label" data-i18n="tasks-time-saved"
|
||||
>Active Time Saved:</span
|
||||
>
|
||||
<span class="time-value" id="time-saved-value">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Intent Input -->
|
||||
<div class="quick-intent-bar">
|
||||
<div class="intent-input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
id="quick-intent-input"
|
||||
name="intent"
|
||||
class="quick-intent-input"
|
||||
placeholder="What would you like to do? e.g., 'create a CRM app' or 'remind me to call John tomorrow'"
|
||||
data-i18n-placeholder="tasks-input-placeholder"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button
|
||||
id="quick-intent-btn"
|
||||
class="btn-create-run"
|
||||
hx-post="/api/ui/autotask/create"
|
||||
hx-ext="json-enc"
|
||||
hx-include="#quick-intent-input"
|
||||
hx-target="#intent-result-hidden"
|
||||
hx-swap="none"
|
||||
hx-indicator="#intent-spinner"
|
||||
hx-timeout="300000"
|
||||
>
|
||||
<span class="btn-text">Create & Run</span>
|
||||
<span class="spinner" id="intent-spinner"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="intent-result" class="intent-result"></div>
|
||||
<div id="intent-result-hidden" style="display: none"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main Two-Column Layout with Splitter -->
|
||||
<main class="tasks-main">
|
||||
<!-- Left Panel: Task Cards List -->
|
||||
<section class="tasks-list-panel">
|
||||
<div
|
||||
class="tasks-list-scroll"
|
||||
id="task-list"
|
||||
hx-get="/api/ui/tasks?filter=all"
|
||||
hx-trigger="load, taskCreated from:body throttle:2s"
|
||||
hx-swap="innerHTML transition:false"
|
||||
>
|
||||
<!-- Loading state - replaced by HTMX -->
|
||||
<div class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Loading tasks...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Splitter -->
|
||||
<div class="tasks-splitter" id="tasks-splitter"></div>
|
||||
|
||||
<!-- Right Panel: Task Detail -->
|
||||
<aside class="task-detail-panel" id="task-detail-panel">
|
||||
<!-- Detail content loaded dynamically -->
|
||||
<div class="detail-empty" id="detail-empty">
|
||||
<div class="empty-icon">📋</div>
|
||||
<h3 class="empty-title">Select a task</h3>
|
||||
<p class="empty-description">
|
||||
Click on a task from the list to view details
|
||||
</p>
|
||||
<div class="empty-info">
|
||||
<p>
|
||||
<strong>Bot Database:</strong> All apps share the same
|
||||
database tables.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Shared Resources:</strong> Schedulers, tools,
|
||||
and monitors work across all apps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic detail content -->
|
||||
<div
|
||||
id="task-detail-content"
|
||||
style="display: none"
|
||||
hx-get=""
|
||||
hx-trigger="taskSelected from:body"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<!-- Loaded via HTMX when task selected -->
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Floating Progress Panel - Shows live task generation progress -->
|
||||
<div
|
||||
class="floating-progress-panel"
|
||||
id="floating-progress"
|
||||
style="display: none"
|
||||
>
|
||||
<div class="floating-progress-header">
|
||||
<div class="floating-progress-title">
|
||||
<span class="progress-dot"></span>
|
||||
<span id="floating-task-name">Processing...</span>
|
||||
</div>
|
||||
<div class="floating-progress-actions">
|
||||
<button class="btn-minimize" onclick="minimizeFloatingProgress()">
|
||||
—
|
||||
</button>
|
||||
<button class="btn-close-float" onclick="closeFloatingProgress()">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="floating-progress-body">
|
||||
<div class="floating-progress-bar">
|
||||
<div
|
||||
class="floating-progress-fill"
|
||||
id="floating-progress-fill"
|
||||
style="width: 0%"
|
||||
></div>
|
||||
</div>
|
||||
<div class="floating-progress-info">
|
||||
<span id="floating-progress-step">Starting...</span>
|
||||
<span id="floating-progress-percent">0%</span>
|
||||
</div>
|
||||
<div class="floating-progress-log" id="floating-progress-log">
|
||||
<!-- Live log entries appear here -->
|
||||
</div>
|
||||
<div class="floating-llm-terminal" id="floating-llm-terminal">
|
||||
<!-- LLM streaming output appears here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
|
||||
<!-- New Intent Modal -->
|
||||
<div class="modal" id="new-intent-modal" style="display: none">
|
||||
<div class="modal-backdrop" onclick="closeNewIntentModal()"></div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Create New Intent</h3>
|
||||
<button class="btn-close" onclick="closeNewIntentModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="new-intent-form">
|
||||
<div class="form-group">
|
||||
<label for="intent-text"
|
||||
>What would you like to accomplish?</label
|
||||
>
|
||||
<textarea
|
||||
id="intent-text"
|
||||
name="intent"
|
||||
rows="4"
|
||||
placeholder="Describe what you want to create or automate..."
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="intent-priority">Priority</label>
|
||||
<select id="intent-priority" name="priority">
|
||||
<option value="low">Low</option>
|
||||
<option value="medium" selected>Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="urgent">Urgent</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="intent-mode">Execution Mode</label>
|
||||
<select id="intent-mode" name="mode">
|
||||
<option value="auto">Automatic</option>
|
||||
<option value="supervised">Supervised</option>
|
||||
<option value="manual">Manual Approval</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeNewIntentModal()">
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn-primary" onclick="submitNewIntent()">
|
||||
Create & Run
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Decision Modal -->
|
||||
<div class="modal" id="decision-modal" style="display: none">
|
||||
<div class="modal-backdrop" onclick="closeDecisionModal()"></div>
|
||||
<div class="modal-content modal-lg">
|
||||
<div class="modal-header">
|
||||
<h3>Make Decision</h3>
|
||||
<button class="btn-close" onclick="closeDecisionModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="decision-question" id="decision-question">
|
||||
<h4>Decision Required</h4>
|
||||
<p>Loading decision details...</p>
|
||||
</div>
|
||||
<div class="decision-options" id="decision-options">
|
||||
<!-- Options loaded dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeDecisionModal()">
|
||||
Cancel
|
||||
</button>
|
||||
|
|
@ -12,4 +312,4 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/suite/tasks/tasks.js"></script>
|
||||
<link rel="stylesheet" href="tasks/tasks.css" />
|
||||
|
|
|
|||
|
|
@ -213,10 +213,8 @@ function setupIntentInputHandlers() {
|
|||
}
|
||||
|
||||
// Task polling for async task creation
|
||||
if (typeof activePollingTaskId === "undefined") {
|
||||
var activePollingTaskId = null;
|
||||
var pollingInterval = null;
|
||||
}
|
||||
let activePollingTaskId = null;
|
||||
let pollingInterval = null;
|
||||
|
||||
function startTaskPolling(taskId) {
|
||||
// Stop any existing polling
|
||||
|
|
@ -631,9 +629,7 @@ function handleWebSocketMessage(data) {
|
|||
}
|
||||
|
||||
// Store pending manifest updates for tasks whose elements aren't loaded yet
|
||||
if (typeof pendingManifestUpdates === "undefined") {
|
||||
var pendingManifestUpdates = new Map();
|
||||
}
|
||||
const pendingManifestUpdates = new Map();
|
||||
|
||||
function renderManifestProgress(
|
||||
taskId,
|
||||
|
|
@ -2763,9 +2759,8 @@ function formatTime(seconds) {
|
|||
// GLOBAL STYLES FOR TOAST ANIMATIONS
|
||||
// =============================================================================
|
||||
|
||||
if (typeof taskStyleElement === "undefined") {
|
||||
var taskStyleElement = document.createElement("style");
|
||||
taskStyleElement.textContent = `
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
|
@ -2810,8 +2805,7 @@ if (typeof taskStyleElement === "undefined") {
|
|||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(taskStyleElement);
|
||||
}
|
||||
document.head.appendChild(style);
|
||||
|
||||
// =============================================================================
|
||||
// GOALS, PENDING INFO, SCHEDULERS, MONITORS
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue