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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
paths:
|
|
||||||
- "botui/**"
|
|
||||||
- "botlib/**"
|
|
||||||
- ".github/workflows/botui.yaml"
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
paths:
|
|
||||||
- "botui/**"
|
|
||||||
- "botlib/**"
|
|
||||||
- ".github/workflows/botui.yaml"
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_BUILD_JOBS: 8
|
|
||||||
CARGO_INCREMENTAL: 0
|
|
||||||
CARGO_NET_RETRY: 10
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: gbo
|
runs-on: gbo
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Disable SSL verification
|
- name: Disable SSL verification (temporary)
|
||||||
run: git config --global http.sslVerify false
|
run: git config --global http.sslVerify false
|
||||||
|
|
||||||
- name: Checkout BotUI Code
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
path: botui
|
|
||||||
|
|
||||||
- name: Setup Workspace
|
- name: Clone botlib dependency
|
||||||
run: |
|
run: git clone --depth 1 https://github.com/GeneralBots/botlib.git ../botlib
|
||||||
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: Cache Cargo registry
|
- name: Cache Cargo registry
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
|
|
@ -58,54 +25,34 @@ jobs:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo/registry
|
~/.cargo/registry
|
||||||
~/.cargo/git
|
~/.cargo/git
|
||||||
~/.cache/sccache
|
target
|
||||||
workspace/target
|
key: ${{ runner.os }}-cargo-botui-${{ hashFiles('**/Cargo.lock') }}
|
||||||
key: ${{ runner.os }}-cargo-v2-debug-ui-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-cargo-v2-debug-ui-
|
${{ runner.os }}-cargo-botui-
|
||||||
${{ 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
|
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
run: |
|
run: |
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
echo "/root/.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
|
|
||||||
|
|
||||||
- name: Setup environment
|
- name: Setup environment
|
||||||
run: sudo cp /opt/gbo/bin/system/.env . 2>/dev/null || true
|
|
||||||
|
|
||||||
- name: Build BotUI
|
|
||||||
working-directory: workspace
|
|
||||||
run: |
|
run: |
|
||||||
cargo build -p botui --features embed-ui -j 8 2>&1 | tee /tmp/build.log
|
sudo cp /opt/gbo/bin/system/botui.env .env
|
||||||
ls -lh target/debug/botui
|
|
||||||
sccache --show-stats || true
|
|
||||||
|
|
||||||
- name: Save build log
|
- name: Build Linux x86_64
|
||||||
if: always()
|
run: /root/.cargo/bin/cargo build --locked --release
|
||||||
|
|
||||||
|
- name: Prepare release artifacts
|
||||||
run: |
|
run: |
|
||||||
sudo mkdir -p /opt/gbo/logs
|
sudo mkdir -p /opt/gbo/releases/botui/linux-x86_64
|
||||||
sudo cp /tmp/build.log /opt/gbo/logs/botui-$(date +%Y%m%d-%H%M%S).log || true
|
sudo cp ./target/release/botui /opt/gbo/releases/botui/linux-x86_64/ || true
|
||||||
|
sudo chmod -R 755 /opt/gbo/releases/botui/
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy and restart local service
|
||||||
working-directory: workspace
|
|
||||||
run: |
|
run: |
|
||||||
lxc exec bot:pragmatismo-system -- systemctl stop ui || true
|
lxc exec bot:pragmatismo-system -- systemctl stop botui
|
||||||
|
|
||||||
sudo cp target/debug/botui /opt/gbo/bin/system/
|
sudo cp ./target/release/botui /opt/gbo/bin/botui
|
||||||
sudo chmod +x /opt/gbo/bin/system/botui
|
sudo chmod +x /opt/gbo/bin/botui
|
||||||
|
|
||||||
lxc exec bot:pragmatismo-system -- systemctl start ui || true
|
lxc exec bot:pragmatismo-system -- systemctl start botui
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ workspace = true
|
||||||
features = ["http-client"]
|
features = ["http-client"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["ui-server", "chat", "drive", "tasks", "admin"]
|
default = ["ui-server", "chat", "drive", "tasks"]
|
||||||
ui-server = []
|
ui-server = []
|
||||||
embed-ui = ["rust-embed"]
|
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
|
# General Bots Desktop
|
||||||
**Purpose:** Web UI server for General Bots (Axum + HTMX + CSS)
|
|
||||||
|
|
||||||
---
|
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
|
```bash
|
||||||
# Development mode - starts Axum server on port 3000
|
npm install
|
||||||
cargo run
|
|
||||||
|
|
||||||
# Desktop mode (Tauri) - starts native window
|
|
||||||
cargo tauri dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
2. Create a .env file with your Azure OpenAI credentials
|
||||||
|
|
||||||
- `BOTUI_PORT` - Server port (default: 3000)
|
3. Development:
|
||||||
|
```bash
|
||||||
---
|
npm run dev
|
||||||
|
|
||||||
## 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
4. Build:
|
||||||
|
```bash
|
||||||
## 🏗️ ARCHITECTURE
|
npm run build
|
||||||
|
|
||||||
### 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Testing
|
||||||
|
```bash
|
||||||
## 🎨 HTMX-FIRST FRONTEND
|
npm test
|
||||||
|
|
||||||
### 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 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 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 log::info;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ use axum::{
|
||||||
http::{Request, StatusCode},
|
http::{Request, StatusCode},
|
||||||
response::{Html, IntoResponse, Response},
|
response::{Html, IntoResponse, Response},
|
||||||
routing::{any, get},
|
routing::{any, get},
|
||||||
Json, Router,
|
Router,
|
||||||
};
|
};
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info};
|
||||||
#[cfg(feature = "embed-ui")]
|
#[cfg(feature = "embed-ui")]
|
||||||
use rust_embed::RustEmbed;
|
use rust_embed::RustEmbed;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
@ -20,6 +20,7 @@ use tokio_tungstenite::{
|
||||||
connect_async_tls_with_config, tungstenite,
|
connect_async_tls_with_config, tungstenite,
|
||||||
tungstenite::protocol::Message as TungsteniteMessage,
|
tungstenite::protocol::Message as TungsteniteMessage,
|
||||||
};
|
};
|
||||||
|
#[cfg(not(feature = "embed-ui"))]
|
||||||
use tower_http::services::{ServeDir, ServeFile};
|
use tower_http::services::{ServeDir, ServeFile};
|
||||||
|
|
||||||
#[cfg(feature = "embed-ui")]
|
#[cfg(feature = "embed-ui")]
|
||||||
|
|
@ -129,98 +130,8 @@ const ROOT_FILES: &[&str] = &[
|
||||||
"single.gbui",
|
"single.gbui",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub async fn index(OriginalUri(uri): OriginalUri) -> Response {
|
pub async fn index() -> impl IntoResponse {
|
||||||
let path = uri.path();
|
serve_suite().await
|
||||||
|
|
||||||
// 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 fn get_ui_root() -> PathBuf {
|
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 = {
|
let raw_html_res = {
|
||||||
#[cfg(feature = "embed-ui")]
|
#[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
|
#[allow(unused_mut)] // Mutable required for feature-gated blocks
|
||||||
let mut html = raw_html;
|
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
|
// Core Apps
|
||||||
#[cfg(not(feature = "chat"))]
|
#[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>) {
|
async fn api_health() -> (StatusCode, axum::Json<serde_json::Value>) {
|
||||||
if state.health_check().await {
|
|
||||||
(
|
(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
axum::Json(serde_json::json!({
|
axum::Json(serde_json::json!({
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"botserver": "healthy",
|
|
||||||
"version": env!("CARGO_PKG_VERSION")
|
"version": env!("CARGO_PKG_VERSION")
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
(
|
|
||||||
StatusCode::SERVICE_UNAVAILABLE,
|
|
||||||
axum::Json(serde_json::json!({
|
|
||||||
"status": "error",
|
|
||||||
"botserver": "unhealthy",
|
|
||||||
"version": env!("CARGO_PKG_VERSION")
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_app_context(headers: &axum::http::HeaderMap, path: &str) -> Option<String> {
|
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> {
|
fn create_api_router() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/health", get(api_health))
|
.route("/health", get(api_health))
|
||||||
.route("/client-error", axum::routing::post(handle_client_error))
|
|
||||||
.fallback(any(proxy_api))
|
.fallback(any(proxy_api))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -723,36 +595,6 @@ fn create_api_router() -> Router<AppState> {
|
||||||
struct WsQuery {
|
struct WsQuery {
|
||||||
session_id: String,
|
session_id: String,
|
||||||
user_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)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
|
@ -763,31 +605,9 @@ struct OptionalWsQuery {
|
||||||
async fn ws_proxy(
|
async fn ws_proxy(
|
||||||
ws: WebSocketUpgrade,
|
ws: WebSocketUpgrade,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
OriginalUri(uri): OriginalUri,
|
|
||||||
Query(params): Query<WsQuery>,
|
Query(params): Query<WsQuery>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Extract bot_name from URL path (e.g., /edu, /chat/edu)
|
ws.on_upgrade(move |socket| handle_ws_proxy(socket, state, params))
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ws_task_progress_proxy(
|
async fn ws_task_progress_proxy(
|
||||||
|
|
@ -941,15 +761,14 @@ async fn handle_task_progress_ws_proxy(
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQuery) {
|
async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQuery) {
|
||||||
let backend_url = format!(
|
let backend_url = format!(
|
||||||
"{}/ws?session_id={}&user_id={}&bot_name={}",
|
"{}/ws?session_id={}&user_id={}",
|
||||||
state
|
state
|
||||||
.client
|
.client
|
||||||
.base_url()
|
.base_url()
|
||||||
.replace("https://", "wss://")
|
.replace("https://", "wss://")
|
||||||
.replace("http://", "ws://"),
|
.replace("http://", "ws://"),
|
||||||
params.session_id,
|
params.session_id,
|
||||||
params.user_id,
|
params.user_id
|
||||||
params.bot_name.unwrap_or_else(|| "default".to_string())
|
|
||||||
);
|
);
|
||||||
|
|
||||||
info!("Proxying WebSocket to: {backend_url}");
|
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"))]
|
#[cfg(not(feature = "embed-ui"))]
|
||||||
{
|
{
|
||||||
let mut r = router;
|
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 {
|
for dir in SUITE_DIRS {
|
||||||
let path = _suite_path.join(dir);
|
let path = _suite_path.join(dir);
|
||||||
info!("Adding route for /{} -> {:?}", dir, path);
|
r = r
|
||||||
r = r.nest_service(&format!("/{dir}"), ServeDir::new(path.clone()));
|
.nest_service(&format!("/suite/{dir}"), ServeDir::new(path.clone()))
|
||||||
info!("Adding route for /suite/{} -> {:?}", dir, path);
|
.nest_service(&format!("/{dir}"), ServeDir::new(path));
|
||||||
r = r.nest_service(&format!("/suite/{dir}"), ServeDir::new(path.clone()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for file in ROOT_FILES {
|
for file in ROOT_FILES {
|
||||||
|
|
@ -1186,14 +1002,12 @@ pub fn configure_router() -> Router {
|
||||||
.nest("/ui", create_ui_router())
|
.nest("/ui", create_ui_router())
|
||||||
.nest("/ws", create_ws_router())
|
.nest("/ws", create_ws_router())
|
||||||
.nest("/apps", create_apps_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));
|
.route("/favicon.ico", get(serve_favicon));
|
||||||
|
|
||||||
router = add_static_routes(router, &suite_path);
|
router = add_static_routes(router, &suite_path);
|
||||||
|
|
||||||
router
|
router.fallback(get(index)).with_state(state)
|
||||||
.route("/", get(index))
|
|
||||||
.route("/minimal", get(serve_minimal))
|
|
||||||
.route("/suite", get(serve_suite))
|
|
||||||
.fallback(get(index))
|
|
||||||
.with_state(state)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Login - General Bots</title>
|
<title>Login - General Bots</title>
|
||||||
<script src="/suite/js/vendor/htmx.min.js"></script>
|
<script src="/js/vendor/htmx.min.js"></script>
|
||||||
<script src="/suite/js/vendor/htmx-json-enc.js"></script>
|
<script src="/js/vendor/htmx-json-enc.js"></script>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary: #3b82f6;
|
--primary: #3b82f6;
|
||||||
|
|
@ -1264,18 +1264,12 @@
|
||||||
|
|
||||||
// Successful login - redirect
|
// Successful login - redirect
|
||||||
if (response.redirect || response.success) {
|
if (response.redirect || response.success) {
|
||||||
// Check for redirect parameter in URL
|
window.location.href = response.redirect || "/";
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const redirectUrl = urlParams.get('redirect') || response.redirect;
|
|
||||||
window.location.href = redirectUrl ? redirectUrl : window.location.origin + "/#chat";
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If response is not JSON, check for redirect header
|
// If response is not JSON, check for redirect header
|
||||||
if (event.detail.xhr.status === 200) {
|
if (event.detail.xhr.status === 200) {
|
||||||
// Check for redirect parameter in URL
|
window.location.href = "/";
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const redirectUrl = urlParams.get('redirect');
|
|
||||||
window.location.href = redirectUrl ? redirectUrl : window.location.origin + "/#chat";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,6 @@
|
||||||
<script src="/js/vendor/htmx-ws.js"></script>
|
<script src="/js/vendor/htmx-ws.js"></script>
|
||||||
<script src="/js/vendor/htmx-json-enc.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 -->
|
<!-- i18n -->
|
||||||
<script src="/js/i18n.js"></script>
|
<script src="/js/i18n.js"></script>
|
||||||
|
|
||||||
|
|
@ -119,7 +116,7 @@
|
||||||
href="#tasks"
|
href="#tasks"
|
||||||
class="app-item"
|
class="app-item"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
hx-get="/suite/tasks/autotask.html"
|
hx-get="/tasks/tasks.html"
|
||||||
hx-target="#main-content"
|
hx-target="#main-content"
|
||||||
hx-push-url="true"
|
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 -->
|
<!-- Calendar - Event Management -->
|
||||||
<div class="calendar-container" id="calendar-app">
|
<div class="calendar-container" id="calendar-app">
|
||||||
|
|
|
||||||
|
|
@ -110,97 +110,97 @@
|
||||||
|
|
||||||
/* Messages Area */
|
/* Messages Area */
|
||||||
#messages {
|
#messages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: var(--accent, #3b82f6) var(--surface, #1a1a24);
|
scrollbar-color: var(--accent, #3b82f6) var(--surface, #1a1a24);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar for markers */
|
/* Custom scrollbar for markers */
|
||||||
#messages::-webkit-scrollbar {
|
#messages::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#messages::-webkit-scrollbar-track {
|
#messages::-webkit-scrollbar-track {
|
||||||
background: var(--surface, #1a1a24);
|
background: var(--surface, #1a1a24);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#messages::-webkit-scrollbar-thumb {
|
#messages::-webkit-scrollbar-thumb {
|
||||||
background: var(--accent, #3b82f6);
|
background: var(--accent, #3b82f6);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: 1px solid var(--surface, #1a1a24);
|
border: 1px solid var(--surface, #1a1a24);
|
||||||
}
|
}
|
||||||
|
|
||||||
#messages::-webkit-scrollbar-thumb:hover {
|
#messages::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--accent-hover, #2563eb);
|
background: var(--accent-hover, #2563eb);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scrollbar markers container */
|
/* Scrollbar markers container */
|
||||||
.scrollbar-markers {
|
.scrollbar-markers {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 2px;
|
right: 2px;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-marker {
|
.scrollbar-marker {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
background: var(--accent, #3b82f6);
|
background: var(--accent, #3b82f6);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 0 4px rgba(0, 0, 0, 0.5);
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-marker:hover {
|
.scrollbar-marker:hover {
|
||||||
transform: scale(1.5);
|
transform: scale(1.5);
|
||||||
background: var(--accent-hover, #2563eb);
|
background: var(--accent-hover, #2563eb);
|
||||||
box-shadow: 0 0 8px var(--accent, #3b82f6);
|
box-shadow: 0 0 8px var(--accent, #3b82f6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-marker.user-marker {
|
.scrollbar-marker.user-marker {
|
||||||
background: var(--accent, #3b82f6);
|
background: var(--accent, #3b82f6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-marker.bot-marker {
|
.scrollbar-marker.bot-marker {
|
||||||
background: var(--success, #22c55e);
|
background: var(--success, #22c55e);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-marker-tooltip {
|
.scrollbar-marker-tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
background: var(--surface, #1a1a24);
|
background: var(--surface, #1a1a24);
|
||||||
border: 1px solid var(--border, #2a2a2a);
|
border: 1px solid var(--border, #2a2a2a);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text, #ffffff);
|
color: var(--text, #ffffff);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 12;
|
z-index: 12;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-marker:hover .scrollbar-marker-tooltip {
|
.scrollbar-marker:hover .scrollbar-marker-tooltip {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Message Styles */
|
/* Message Styles */
|
||||||
|
|
@ -399,28 +399,28 @@ footer {
|
||||||
}
|
}
|
||||||
|
|
||||||
.mention-results {
|
.mention-results {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 250px;
|
max-height: 250px;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: var(--accent, #3b82f6) transparent;
|
scrollbar-color: var(--accent, #3b82f6) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mention-results::-webkit-scrollbar {
|
.mention-results::-webkit-scrollbar {
|
||||||
width: 4px;
|
width: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mention-results::-webkit-scrollbar-track {
|
.mention-results::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mention-results::-webkit-scrollbar-thumb {
|
.mention-results::-webkit-scrollbar-thumb {
|
||||||
background: var(--accent, #3b82f6);
|
background: var(--accent, #3b82f6);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mention-results::-webkit-scrollbar-thumb:hover {
|
.mention-results::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--accent-hover, #2563eb);
|
background: var(--accent-hover, #2563eb);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mention-item {
|
.mention-item {
|
||||||
|
|
@ -574,13 +574,6 @@ footer {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-card-btm {
|
|
||||||
margin-top: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entity-card-btn {
|
.entity-card-btn {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
@ -650,26 +643,12 @@ form.input-container {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
caret-color: var(--accent, #3b82f6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#messageInput:focus {
|
#messageInput:focus {
|
||||||
outline: none;
|
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 {
|
#messageInput::placeholder {
|
||||||
color: #888888;
|
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">
|
<div class="chat-layout" id="chat-app">
|
||||||
<main id="messages"></main>
|
<main id="messages"></main>
|
||||||
|
|
@ -28,7 +28,6 @@
|
||||||
id="voiceBtn"
|
id="voiceBtn"
|
||||||
title="Voice"
|
title="Voice"
|
||||||
data-i18n-title="chat-voice"
|
data-i18n-title="chat-voice"
|
||||||
style="display: none"
|
|
||||||
>
|
>
|
||||||
🎤
|
🎤
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -82,7 +81,7 @@
|
||||||
|
|
||||||
var WS_BASE_URL =
|
var WS_BASE_URL =
|
||||||
window.location.protocol === "https:" ? "wss://" : "ws://";
|
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 = {
|
var MessageType = {
|
||||||
EXTERNAL: 0,
|
EXTERNAL: 0,
|
||||||
|
|
@ -149,7 +148,6 @@
|
||||||
var currentSessionId = null;
|
var currentSessionId = null;
|
||||||
var currentUserId = null;
|
var currentUserId = null;
|
||||||
var currentBotId = "default";
|
var currentBotId = "default";
|
||||||
var currentBotName = "default";
|
|
||||||
var isStreaming = false;
|
var isStreaming = false;
|
||||||
var streamingMessageId = null;
|
var streamingMessageId = null;
|
||||||
var currentStreamingContent = "";
|
var currentStreamingContent = "";
|
||||||
|
|
@ -735,12 +733,10 @@
|
||||||
|
|
||||||
var url =
|
var url =
|
||||||
WS_URL +
|
WS_URL +
|
||||||
"?session_id=" +
|
"/ws?session_id=" +
|
||||||
currentSessionId +
|
currentSessionId +
|
||||||
"&user_id=" +
|
"&user_id=" +
|
||||||
currentUserId +
|
currentUserId;
|
||||||
"&bot_name=" +
|
|
||||||
currentBotName;
|
|
||||||
ws = new WebSocket(url);
|
ws = new WebSocket(url);
|
||||||
|
|
||||||
ws.onopen = function () {
|
ws.onopen = function () {
|
||||||
|
|
@ -751,43 +747,9 @@
|
||||||
ws.onmessage = function (event) {
|
ws.onmessage = function (event) {
|
||||||
try {
|
try {
|
||||||
var data = JSON.parse(event.data);
|
var data = JSON.parse(event.data);
|
||||||
console.log("Chat WebSocket received:", data);
|
|
||||||
|
|
||||||
// Ignore connection confirmation
|
|
||||||
if (data.type === "connected") return;
|
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) {
|
if (data.message_type === MessageType.BOT_RESPONSE) {
|
||||||
console.log("Processing bot response:", data);
|
|
||||||
processMessage(data);
|
processMessage(data);
|
||||||
} else {
|
|
||||||
console.log("Ignoring non-bot message:", data);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("WS message error:", e);
|
console.error("WS message error:", e);
|
||||||
|
|
@ -808,12 +770,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function initChat() {
|
function initChat() {
|
||||||
// Just proceed with chat initialization - no auth check
|
var botName = "default";
|
||||||
proceedWithChatInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
function proceedWithChatInit() {
|
|
||||||
var botName = window.__INITIAL_BOT_NAME__ || "default";
|
|
||||||
fetch("/api/auth?bot_name=" + encodeURIComponent(botName))
|
fetch("/api/auth?bot_name=" + encodeURIComponent(botName))
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
return response.json();
|
return response.json();
|
||||||
|
|
@ -822,19 +779,17 @@
|
||||||
currentUserId = auth.user_id;
|
currentUserId = auth.user_id;
|
||||||
currentSessionId = auth.session_id;
|
currentSessionId = auth.session_id;
|
||||||
currentBotId = auth.bot_id || "default";
|
currentBotId = auth.bot_id || "default";
|
||||||
currentBotName = botName;
|
|
||||||
console.log("Auth:", {
|
console.log("Auth:", {
|
||||||
currentUserId: currentUserId,
|
currentUserId: currentUserId,
|
||||||
currentSessionId: currentSessionId,
|
currentSessionId: currentSessionId,
|
||||||
currentBotId: currentBotId,
|
currentBotId: currentBotId,
|
||||||
currentBotName: currentBotName,
|
|
||||||
});
|
});
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
})
|
})
|
||||||
.catch(function (e) {
|
.catch(function (e) {
|
||||||
console.error("Auth failed:", e);
|
console.error("Auth failed:", e);
|
||||||
notify("Failed to connect to chat server", "error");
|
notify("Failed to connect to chat server", "error");
|
||||||
setTimeout(proceedWithChatInit, 3000);
|
setTimeout(initChat, 3000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!-- Drive - File Management v1.0 -->
|
<!-- 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">
|
<div class="drive-container" id="drive-app">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
|
|
@ -348,4 +348,4 @@
|
||||||
<!-- Context Menu (dynamically populated by JS) -->
|
<!-- Context Menu (dynamically populated by JS) -->
|
||||||
<div id="context-menu" class="context-menu hidden"></div>
|
<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
|
<a
|
||||||
href="#tasks"
|
href="#tasks"
|
||||||
class="app-card"
|
class="app-card"
|
||||||
hx-get="/suite/tasks/autotask.html"
|
hx-get="/suite/tasks/tasks.html"
|
||||||
hx-target="#main-content"
|
hx-target="#main-content"
|
||||||
hx-push-url="/#tasks"
|
hx-push-url="/#tasks"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
1871
ui/suite/index.html
1871
ui/suite/index.html
File diff suppressed because it is too large
Load diff
|
|
@ -351,8 +351,8 @@
|
||||||
this.clearAuth();
|
this.clearAuth();
|
||||||
this.emit("tokenExpired");
|
this.emit("tokenExpired");
|
||||||
|
|
||||||
const currentPath = window.location.pathname + window.location.hash;
|
const currentPath = window.location.pathname;
|
||||||
if (!window.location.pathname.startsWith("/auth/")) {
|
if (!currentPath.startsWith("/auth/")) {
|
||||||
window.location.href =
|
window.location.href =
|
||||||
"/auth/login.html?expired=1&redirect=" +
|
"/auth/login.html?expired=1&redirect=" +
|
||||||
encodeURIComponent(currentPath);
|
encodeURIComponent(currentPath);
|
||||||
|
|
|
||||||
|
|
@ -47,15 +47,12 @@ function applyProductConfig(config) {
|
||||||
// Check if we have compiled_features info to filter even further
|
// 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
|
// 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)) {
|
if (config.compiled_features && Array.isArray(config.compiled_features)) {
|
||||||
const compiledSet = new Set(
|
const compiledSet = new Set(config.compiled_features.map(f => f.toLowerCase()));
|
||||||
config.compiled_features.map((f) => f.toLowerCase()),
|
effectiveApps = effectiveApps.filter(app =>
|
||||||
);
|
|
||||||
effectiveApps = effectiveApps.filter(
|
|
||||||
(app) =>
|
|
||||||
compiledSet.has(app.toLowerCase()) ||
|
compiledSet.has(app.toLowerCase()) ||
|
||||||
app.toLowerCase() === "settings" ||
|
app.toLowerCase() === 'settings' ||
|
||||||
app.toLowerCase() === "auth" ||
|
app.toLowerCase() === 'auth' ||
|
||||||
app.toLowerCase() === "admin", // Admin usually contains settings which is always there
|
app.toLowerCase() === 'admin' // Admin usually contains settings which is always there
|
||||||
);
|
);
|
||||||
|
|
||||||
// Also call a helper to hide UI elements for non-compiled features explicitly
|
// Also call a helper to hide UI elements for non-compiled features explicitly
|
||||||
|
|
@ -64,33 +61,6 @@ function applyProductConfig(config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
filterAppsByConfig(effectiveApps);
|
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
|
// Apply custom logo
|
||||||
|
|
@ -125,22 +95,18 @@ function applyProductConfig(config) {
|
||||||
// Hide UI elements that require features not compiled in the binary
|
// Hide UI elements that require features not compiled in the binary
|
||||||
function hideNonCompiledUI(compiledSet) {
|
function hideNonCompiledUI(compiledSet) {
|
||||||
// Hide elements with data-feature attribute that aren't in compiled set
|
// Hide elements with data-feature attribute that aren't in compiled set
|
||||||
document.querySelectorAll("[data-feature]").forEach((el) => {
|
document.querySelectorAll('[data-feature]').forEach(el => {
|
||||||
const feature = el.getAttribute("data-feature").toLowerCase();
|
const feature = el.getAttribute('data-feature').toLowerCase();
|
||||||
// Allow settings/admin as they are usually core
|
// Allow settings/admin as they are usually core
|
||||||
if (
|
if (!compiledSet.has(feature) && feature !== 'settings' && feature !== 'admin') {
|
||||||
!compiledSet.has(feature) &&
|
el.style.display = 'none';
|
||||||
feature !== "settings" &&
|
el.classList.add('hidden-uncompiled');
|
||||||
feature !== "admin"
|
|
||||||
) {
|
|
||||||
el.style.display = "none";
|
|
||||||
el.classList.add("hidden-uncompiled");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also look for specific sections that might map to features
|
// Also look for specific sections that might map to features
|
||||||
// e.g. .feature-mail, .feature-meet classes
|
// e.g. .feature-mail, .feature-meet classes
|
||||||
compiledSet.forEach((feature) => {
|
compiledSet.forEach(feature => {
|
||||||
// This loop defines what IS available.
|
// This loop defines what IS available.
|
||||||
// Logic should be inverse: find all feature- classes and hide if not in set
|
// 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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
// Handle WebSocket messages
|
||||||
function handleWebSocketMessage(message) {
|
function handleWebSocketMessage(message) {
|
||||||
const messageType = message.type || message.event;
|
switch (message.type) {
|
||||||
|
|
||||||
// Debug logging
|
|
||||||
console.log("handleWebSocketMessage called with:", { messageType, message });
|
|
||||||
|
|
||||||
switch (messageType) {
|
|
||||||
case "message":
|
case "message":
|
||||||
appendMessage(message);
|
appendMessage(message);
|
||||||
break;
|
break;
|
||||||
|
|
@ -221,28 +216,8 @@
|
||||||
case "suggestion":
|
case "suggestion":
|
||||||
addSuggestion(message.text);
|
addSuggestion(message.text);
|
||||||
break;
|
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:
|
default:
|
||||||
// Only append unknown message types to chat if they have text content
|
console.log("Unknown message type:", message.type);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -295,12 +295,6 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("gb:auth:expired", function (event) {
|
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(
|
console.log(
|
||||||
"[GBSecurity] Auth expired, clearing tokens and redirecting",
|
"[GBSecurity] Auth expired, clearing tokens and redirecting",
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@ window.Suite = {
|
||||||
description: "",
|
description: "",
|
||||||
actions: [],
|
actions: [],
|
||||||
searchable: true,
|
searchable: true,
|
||||||
...config,
|
...config
|
||||||
});
|
});
|
||||||
|
|
||||||
// Trigger UI update if Omnibox is initialized
|
// Trigger UI update if Omnibox is initialized
|
||||||
if (typeof Omnibox !== "undefined" && Omnibox.isActive) {
|
if (typeof Omnibox !== 'undefined' && Omnibox.isActive) {
|
||||||
Omnibox.updateActions();
|
Omnibox.updateActions();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -36,7 +36,7 @@ window.Suite = {
|
||||||
getContextActions(contextId) {
|
getContextActions(contextId) {
|
||||||
const app = this.apps.get(contextId);
|
const app = this.apps.get(contextId);
|
||||||
return app ? app.actions : null;
|
return app ? app.actions : null;
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
@ -55,9 +55,13 @@ const Omnibox = {
|
||||||
this.backdrop = document.getElementById("omniboxBackdrop");
|
this.backdrop = document.getElementById("omniboxBackdrop");
|
||||||
this.results = document.getElementById("omniboxResults");
|
this.results = document.getElementById("omniboxResults");
|
||||||
this.chat = document.getElementById("omniboxChat");
|
this.chat = document.getElementById("omniboxChat");
|
||||||
this.chatMessages = document.getElementById("omniboxChatMessages");
|
this.chatMessages = document.getElementById(
|
||||||
this.chatInput = document.getElementById("omniboxChatInput");
|
"omniboxChatMessages",
|
||||||
this.modeToggle = document.getElementById("omniboxModeToggle");
|
);
|
||||||
|
this.chatInput =
|
||||||
|
document.getElementById("omniboxChatInput");
|
||||||
|
this.modeToggle =
|
||||||
|
document.getElementById("omniboxModeToggle");
|
||||||
|
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
},
|
},
|
||||||
|
|
@ -73,7 +77,9 @@ const Omnibox = {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Keyboard navigation
|
// Keyboard navigation
|
||||||
this.input.addEventListener("keydown", (e) => this.handleKeydown(e));
|
this.input.addEventListener("keydown", (e) =>
|
||||||
|
this.handleKeydown(e),
|
||||||
|
);
|
||||||
this.chatInput?.addEventListener("keydown", (e) => {
|
this.chatInput?.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -88,8 +94,12 @@ const Omnibox = {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Action buttons
|
// Action buttons
|
||||||
document.querySelectorAll(".omnibox-action").forEach((btn) => {
|
document
|
||||||
btn.addEventListener("click", () => this.handleAction(btn));
|
.querySelectorAll(".omnibox-action")
|
||||||
|
.forEach((btn) => {
|
||||||
|
btn.addEventListener("click", () =>
|
||||||
|
this.handleAction(btn),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send button
|
// Send button
|
||||||
|
|
@ -105,7 +115,9 @@ const Omnibox = {
|
||||||
// Expand button
|
// Expand button
|
||||||
document
|
document
|
||||||
.getElementById("omniboxExpandBtn")
|
.getElementById("omniboxExpandBtn")
|
||||||
?.addEventListener("click", () => this.expandToFullChat());
|
?.addEventListener("click", () =>
|
||||||
|
this.expandToFullChat(),
|
||||||
|
);
|
||||||
|
|
||||||
// Global shortcut (Cmd+K / Ctrl+K)
|
// Global shortcut (Cmd+K / Ctrl+K)
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
|
|
@ -147,11 +159,17 @@ const Omnibox = {
|
||||||
|
|
||||||
if (e.key === "ArrowDown") {
|
if (e.key === "ArrowDown") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.selectedIndex = Math.min(this.selectedIndex + 1, actions.length - 1);
|
this.selectedIndex = Math.min(
|
||||||
|
this.selectedIndex + 1,
|
||||||
|
actions.length - 1,
|
||||||
|
);
|
||||||
this.updateSelection(actions);
|
this.updateSelection(actions);
|
||||||
} else if (e.key === "ArrowUp") {
|
} else if (e.key === "ArrowUp") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
this.selectedIndex = Math.max(
|
||||||
|
this.selectedIndex - 1,
|
||||||
|
0,
|
||||||
|
);
|
||||||
this.updateSelection(actions);
|
this.updateSelection(actions);
|
||||||
} else if (e.key === "Enter") {
|
} else if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -167,13 +185,17 @@ const Omnibox = {
|
||||||
|
|
||||||
updateSelection(actions) {
|
updateSelection(actions) {
|
||||||
actions.forEach((a, i) => {
|
actions.forEach((a, i) => {
|
||||||
a.classList.toggle("selected", i === this.selectedIndex);
|
a.classList.toggle(
|
||||||
|
"selected",
|
||||||
|
i === this.selectedIndex,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updateActions() {
|
updateActions() {
|
||||||
const currentApp = this.getCurrentApp();
|
const currentApp = this.getCurrentApp();
|
||||||
const actionsContainer = document.getElementById("omniboxActions");
|
const actionsContainer =
|
||||||
|
document.getElementById("omniboxActions");
|
||||||
|
|
||||||
const contextActions = {
|
const contextActions = {
|
||||||
chat: [
|
chat: [
|
||||||
|
|
@ -340,8 +362,12 @@ const Omnibox = {
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
// Rebind events
|
// Rebind events
|
||||||
actionsContainer.querySelectorAll(".omnibox-action").forEach((btn) => {
|
actionsContainer
|
||||||
btn.addEventListener("click", () => this.handleAction(btn));
|
.querySelectorAll(".omnibox-action")
|
||||||
|
.forEach((btn) => {
|
||||||
|
btn.addEventListener("click", () =>
|
||||||
|
this.handleAction(btn),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.selectedIndex = 0;
|
this.selectedIndex = 0;
|
||||||
|
|
@ -372,7 +398,9 @@ const Omnibox = {
|
||||||
|
|
||||||
navigateTo(target) {
|
navigateTo(target) {
|
||||||
this.close();
|
this.close();
|
||||||
const link = document.querySelector(`a[data-section="${target}"]`);
|
const link = document.querySelector(
|
||||||
|
`a[data-section="${target}"]`,
|
||||||
|
);
|
||||||
if (link) {
|
if (link) {
|
||||||
link.click();
|
link.click();
|
||||||
}
|
}
|
||||||
|
|
@ -408,24 +436,32 @@ const Omnibox = {
|
||||||
},
|
},
|
||||||
|
|
||||||
showDefaultActions() {
|
showDefaultActions() {
|
||||||
document.getElementById("searchResultsSection").style.display = "none";
|
document.getElementById(
|
||||||
|
"searchResultsSection",
|
||||||
|
).style.display = "none";
|
||||||
this.updateActions();
|
this.updateActions();
|
||||||
},
|
},
|
||||||
|
|
||||||
searchContent(query) {
|
searchContent(query) {
|
||||||
// Show search results section
|
// Show search results section
|
||||||
const resultsSection = document.getElementById("searchResultsSection");
|
const resultsSection = document.getElementById(
|
||||||
const resultsList = document.getElementById("searchResultsList");
|
"searchResultsSection",
|
||||||
|
);
|
||||||
|
const resultsList =
|
||||||
|
document.getElementById("searchResultsList");
|
||||||
|
|
||||||
resultsSection.style.display = "block";
|
resultsSection.style.display = "block";
|
||||||
|
|
||||||
// Update first action to be "Ask about: query"
|
// Update first action to be "Ask about: query"
|
||||||
const actionsContainer = document.getElementById("omniboxActions");
|
const actionsContainer =
|
||||||
const firstAction = actionsContainer.querySelector(".omnibox-action");
|
document.getElementById("omniboxActions");
|
||||||
|
const firstAction =
|
||||||
|
actionsContainer.querySelector(".omnibox-action");
|
||||||
if (firstAction) {
|
if (firstAction) {
|
||||||
firstAction.dataset.action = "chat";
|
firstAction.dataset.action = "chat";
|
||||||
firstAction.dataset.query = query;
|
firstAction.dataset.query = query;
|
||||||
firstAction.querySelector(".action-icon").textContent = "💬";
|
firstAction.querySelector(".action-icon").textContent =
|
||||||
|
"💬";
|
||||||
firstAction.querySelector(".action-text").textContent =
|
firstAction.querySelector(".action-text").textContent =
|
||||||
`Ask: "${query.substring(0, 30)}${query.length > 30 ? "..." : ""}"`;
|
`Ask: "${query.substring(0, 30)}${query.length > 30 ? "..." : ""}"`;
|
||||||
}
|
}
|
||||||
|
|
@ -449,8 +485,12 @@ const Omnibox = {
|
||||||
'<div class="no-results">No results found. Try asking the bot!</div>';
|
'<div class="no-results">No results found. Try asking the bot!</div>';
|
||||||
|
|
||||||
// Bind click events
|
// Bind click events
|
||||||
resultsList.querySelectorAll(".omnibox-result").forEach((btn) => {
|
resultsList
|
||||||
btn.addEventListener("click", () => this.navigateTo(btn.dataset.target));
|
.querySelectorAll(".omnibox-result")
|
||||||
|
.forEach((btn) => {
|
||||||
|
btn.addEventListener("click", () =>
|
||||||
|
this.navigateTo(btn.dataset.target),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -541,16 +581,17 @@ const Omnibox = {
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
description: "App settings",
|
description: "App settings",
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add plugin apps
|
// Add plugin apps
|
||||||
const pluginApps = window.Suite.getAllApps()
|
const pluginApps = window.Suite.getAllApps()
|
||||||
.filter((app) => app.searchable)
|
.filter(app => app.searchable)
|
||||||
.map((app) => ({
|
.map(app => ({
|
||||||
target: app.id,
|
target: app.id,
|
||||||
icon: app.icon || "📦",
|
icon: app.icon || "📦",
|
||||||
title: app.title || app.id,
|
title: app.title || app.id,
|
||||||
description: app.description || "App plugin",
|
description: app.description || "App plugin"
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const allItems = items.concat(pluginApps);
|
const allItems = items.concat(pluginApps);
|
||||||
|
|
@ -600,7 +641,9 @@ const Omnibox = {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
this.addMessage(
|
this.addMessage(
|
||||||
data.reply || data.message || "I received your message.",
|
data.reply ||
|
||||||
|
data.message ||
|
||||||
|
"I received your message.",
|
||||||
"bot",
|
"bot",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -617,7 +660,10 @@ const Omnibox = {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.removeTypingIndicator(typingId);
|
this.removeTypingIndicator(typingId);
|
||||||
// Fallback response when API is not available
|
// Fallback response when API is not available
|
||||||
this.addMessage(this.getFallbackResponse(message), "bot");
|
this.addMessage(
|
||||||
|
this.getFallbackResponse(message),
|
||||||
|
"bot",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -651,7 +697,8 @@ const Omnibox = {
|
||||||
<div class="message-content">${this.escapeHtml(text)}</div>
|
<div class="message-content">${this.escapeHtml(text)}</div>
|
||||||
`;
|
`;
|
||||||
this.chatMessages.appendChild(msgDiv);
|
this.chatMessages.appendChild(msgDiv);
|
||||||
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
|
this.chatMessages.scrollTop =
|
||||||
|
this.chatMessages.scrollHeight;
|
||||||
|
|
||||||
this.chatHistory.push({ role: sender, content: text });
|
this.chatHistory.push({ role: sender, content: text });
|
||||||
},
|
},
|
||||||
|
|
@ -668,7 +715,8 @@ const Omnibox = {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
this.chatMessages.appendChild(typingDiv);
|
this.chatMessages.appendChild(typingDiv);
|
||||||
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
|
this.chatMessages.scrollTop =
|
||||||
|
this.chatMessages.scrollHeight;
|
||||||
return id;
|
return id;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -679,13 +727,18 @@ const Omnibox = {
|
||||||
|
|
||||||
handleBotAction(action) {
|
handleBotAction(action) {
|
||||||
if (action.navigate) {
|
if (action.navigate) {
|
||||||
setTimeout(() => this.navigateTo(action.navigate), 1000);
|
setTimeout(
|
||||||
|
() => this.navigateTo(action.navigate),
|
||||||
|
1000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
expandToFullChat() {
|
expandToFullChat() {
|
||||||
this.close();
|
this.close();
|
||||||
const chatLink = document.querySelector('a[data-section="chat"]');
|
const chatLink = document.querySelector(
|
||||||
|
'a[data-section="chat"]',
|
||||||
|
);
|
||||||
if (chatLink) chatLink.click();
|
if (chatLink) chatLink.click();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -698,57 +751,13 @@ const Omnibox = {
|
||||||
|
|
||||||
// Initialize Omnibox when DOM is ready
|
// Initialize Omnibox when DOM is ready
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
// Detect bot name from pathname (e.g., /edu -> bot_name = "edu")
|
|
||||||
const detectBotFromPath = () => {
|
|
||||||
const pathname = window.location.pathname;
|
|
||||||
// Remove leading/trailing slashes and get first segment
|
|
||||||
const segments = pathname.replace(/^\/|\/$/g, "").split("/");
|
|
||||||
const firstSegment = segments[0];
|
|
||||||
|
|
||||||
// If first segment is not a known route, treat it as bot name
|
|
||||||
const knownRoutes = ["suite", "auth", "api", "static", "public"];
|
|
||||||
if (firstSegment && !knownRoutes.includes(firstSegment)) {
|
|
||||||
return firstSegment;
|
|
||||||
}
|
|
||||||
return "default";
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set global bot name
|
|
||||||
window.__INITIAL_BOT_NAME__ = detectBotFromPath();
|
|
||||||
console.log(`🤖 Bot detected from path: ${window.__INITIAL_BOT_NAME__}`);
|
|
||||||
|
|
||||||
// Check if bot is public to skip authentication
|
|
||||||
const checkBotPublicStatus = async () => {
|
|
||||||
try {
|
|
||||||
const botName = window.__INITIAL_BOT_NAME__;
|
|
||||||
if (!botName || botName === "default") return;
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/bot/config?bot_name=${encodeURIComponent(botName)}`,
|
|
||||||
);
|
|
||||||
if (response.ok) {
|
|
||||||
const config = await response.json();
|
|
||||||
if (config.public === true) {
|
|
||||||
window.__BOT_IS_PUBLIC__ = true;
|
|
||||||
console.log(
|
|
||||||
`✅ Bot '${botName}' is public - authentication not required`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Failed to check bot public status:", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Omnibox.init();
|
Omnibox.init();
|
||||||
console.log("🚀 Initializing General Bots with HTMX...");
|
console.log("🚀 Initializing General Bots with HTMX...");
|
||||||
|
|
||||||
// Check bot public status early
|
|
||||||
checkBotPublicStatus();
|
|
||||||
|
|
||||||
// Hide loading overlay
|
// Hide loading overlay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const loadingOverlay = document.getElementById("loadingOverlay");
|
const loadingOverlay =
|
||||||
|
document.getElementById("loadingOverlay");
|
||||||
if (loadingOverlay) {
|
if (loadingOverlay) {
|
||||||
loadingOverlay.classList.add("hidden");
|
loadingOverlay.classList.add("hidden");
|
||||||
}
|
}
|
||||||
|
|
@ -766,11 +775,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
const isOpen = appsDropdown.classList.toggle("show");
|
const isOpen = appsDropdown.classList.toggle("show");
|
||||||
appsBtn.setAttribute("aria-expanded", isOpen);
|
appsBtn.setAttribute("aria-expanded", isOpen);
|
||||||
// Close settings panel
|
// Close settings panel
|
||||||
if (settingsPanel) settingsPanel.classList.remove("show");
|
if (settingsPanel)
|
||||||
|
settingsPanel.classList.remove("show");
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
if (!appsDropdown.contains(e.target) && !appsBtn.contains(e.target)) {
|
if (
|
||||||
|
!appsDropdown.contains(e.target) &&
|
||||||
|
!appsBtn.contains(e.target)
|
||||||
|
) {
|
||||||
appsDropdown.classList.remove("show");
|
appsDropdown.classList.remove("show");
|
||||||
appsBtn.setAttribute("aria-expanded", "false");
|
appsBtn.setAttribute("aria-expanded", "false");
|
||||||
}
|
}
|
||||||
|
|
@ -800,7 +813,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
// Theme selection handling
|
// Theme selection handling
|
||||||
const themeOptions = document.querySelectorAll(".theme-option");
|
const themeOptions = document.querySelectorAll(".theme-option");
|
||||||
const savedTheme = localStorage.getItem("gb-theme") || "sentient";
|
const savedTheme =
|
||||||
|
localStorage.getItem("gb-theme") || "sentient";
|
||||||
|
|
||||||
// Apply saved theme
|
// Apply saved theme
|
||||||
document.body.setAttribute("data-theme", savedTheme);
|
document.body.setAttribute("data-theme", savedTheme);
|
||||||
|
|
@ -813,7 +827,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
const theme = option.getAttribute("data-theme");
|
const theme = option.getAttribute("data-theme");
|
||||||
document.body.setAttribute("data-theme", theme);
|
document.body.setAttribute("data-theme", theme);
|
||||||
localStorage.setItem("gb-theme", theme);
|
localStorage.setItem("gb-theme", theme);
|
||||||
themeOptions.forEach((o) => o.classList.remove("active"));
|
themeOptions.forEach((o) =>
|
||||||
|
o.classList.remove("active"),
|
||||||
|
);
|
||||||
option.classList.add("active");
|
option.classList.add("active");
|
||||||
|
|
||||||
// Update theme-color meta tag
|
// Update theme-color meta tag
|
||||||
|
|
@ -825,9 +841,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
orange: "#f97316",
|
orange: "#f97316",
|
||||||
sentient: "#d4f505",
|
sentient: "#d4f505",
|
||||||
};
|
};
|
||||||
const metaTheme = document.querySelector('meta[name="theme-color"]');
|
const metaTheme = document.querySelector(
|
||||||
|
'meta[name="theme-color"]',
|
||||||
|
);
|
||||||
if (metaTheme) {
|
if (metaTheme) {
|
||||||
metaTheme.setAttribute("content", themeColors[theme] || "#d4f505");
|
metaTheme.setAttribute(
|
||||||
|
"content",
|
||||||
|
themeColors[theme] || "#d4f505",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -938,13 +959,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
// Validate target exists before triggering HTMX load
|
// Validate target exists before triggering HTMX load
|
||||||
if (!mainContent) {
|
if (!mainContent) {
|
||||||
console.warn("handleHashChange: #main-content not found, skipping load");
|
console.warn(
|
||||||
|
"handleHashChange: #main-content not found, skipping load",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if main-content is in the DOM
|
// Check if main-content is in the DOM
|
||||||
if (!document.body.contains(mainContent)) {
|
if (!document.body.contains(mainContent)) {
|
||||||
console.warn("handleHashChange: #main-content not in DOM, skipping load");
|
console.warn(
|
||||||
|
"handleHashChange: #main-content not in DOM, skipping load",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -959,7 +984,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
// Debounce the load to prevent rapid double-requests
|
// Debounce the load to prevent rapid double-requests
|
||||||
pendingLoadTimeout = setTimeout(() => {
|
pendingLoadTimeout = setTimeout(() => {
|
||||||
// Re-check if section changed during debounce
|
// Re-check if section changed during debounce
|
||||||
const currentHash = window.location.hash.slice(1) || "chat";
|
const currentHash =
|
||||||
|
window.location.hash.slice(1) || "chat";
|
||||||
if (currentLoadedSection === currentHash) {
|
if (currentLoadedSection === currentHash) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -979,7 +1005,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
swap: "innerHTML",
|
swap: "innerHTML",
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("handleHashChange: HTMX ajax error:", e);
|
console.warn(
|
||||||
|
"handleHashChange: HTMX ajax error:",
|
||||||
|
e,
|
||||||
|
);
|
||||||
currentLoadedSection = null;
|
currentLoadedSection = null;
|
||||||
isLoadingSection = false;
|
isLoadingSection = false;
|
||||||
}
|
}
|
||||||
|
|
@ -990,7 +1019,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
// Listen for HTMX swaps to track loaded sections and prevent duplicates
|
// Listen for HTMX swaps to track loaded sections and prevent duplicates
|
||||||
document.body.addEventListener("htmx:afterSwap", (event) => {
|
document.body.addEventListener("htmx:afterSwap", (event) => {
|
||||||
if (event.detail.target && event.detail.target.id === "main-content") {
|
if (
|
||||||
|
event.detail.target &&
|
||||||
|
event.detail.target.id === "main-content"
|
||||||
|
) {
|
||||||
const hash = window.location.hash.slice(1) || "chat";
|
const hash = window.location.hash.slice(1) || "chat";
|
||||||
currentLoadedSection = hash;
|
currentLoadedSection = hash;
|
||||||
isLoadingSection = false;
|
isLoadingSection = false;
|
||||||
|
|
@ -999,18 +1031,27 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
// Reset tracking on swap errors
|
// Reset tracking on swap errors
|
||||||
document.body.addEventListener("htmx:swapError", (event) => {
|
document.body.addEventListener("htmx:swapError", (event) => {
|
||||||
if (event.detail.target && event.detail.target.id === "main-content") {
|
if (
|
||||||
|
event.detail.target &&
|
||||||
|
event.detail.target.id === "main-content"
|
||||||
|
) {
|
||||||
isLoadingSection = false;
|
isLoadingSection = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also listen for response errors
|
// Also listen for response errors
|
||||||
document.body.addEventListener("htmx:responseError", (event) => {
|
document.body.addEventListener(
|
||||||
if (event.detail.target && event.detail.target.id === "main-content") {
|
"htmx:responseError",
|
||||||
|
(event) => {
|
||||||
|
if (
|
||||||
|
event.detail.target &&
|
||||||
|
event.detail.target.id === "main-content"
|
||||||
|
) {
|
||||||
isLoadingSection = false;
|
isLoadingSection = false;
|
||||||
currentLoadedSection = null;
|
currentLoadedSection = null;
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Load initial content based on hash or default to chat
|
// Load initial content based on hash or default to chat
|
||||||
window.addEventListener("hashchange", handleHashChange);
|
window.addEventListener("hashchange", handleHashChange);
|
||||||
|
|
@ -1041,13 +1082,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
const list = document.getElementById("notificationsList");
|
const list = document.getElementById("notificationsList");
|
||||||
const btn = document.getElementById("notificationsBtn");
|
const btn = document.getElementById("notificationsBtn");
|
||||||
const panel = document.getElementById("notificationsPanel");
|
const panel = document.getElementById("notificationsPanel");
|
||||||
const clearBtn = document.getElementById("clearNotificationsBtn");
|
const clearBtn = document.getElementById(
|
||||||
|
"clearNotificationsBtn",
|
||||||
|
);
|
||||||
|
|
||||||
function updateBadge() {
|
function updateBadge() {
|
||||||
if (badge) {
|
if (badge) {
|
||||||
if (notifications.length > 0) {
|
if (notifications.length > 0) {
|
||||||
badge.textContent =
|
badge.textContent =
|
||||||
notifications.length > 99 ? "99+" : notifications.length;
|
notifications.length > 99
|
||||||
|
? "99+"
|
||||||
|
: notifications.length;
|
||||||
badge.style.display = "flex";
|
badge.style.display = "flex";
|
||||||
} else {
|
} else {
|
||||||
badge.style.display = "none";
|
badge.style.display = "none";
|
||||||
|
|
@ -1127,7 +1172,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
if (!panel.contains(e.target) && !btn.contains(e.target)) {
|
if (
|
||||||
|
!panel.contains(e.target) &&
|
||||||
|
!btn.contains(e.target)
|
||||||
|
) {
|
||||||
panel.classList.remove("show");
|
panel.classList.remove("show");
|
||||||
btn.setAttribute("aria-expanded", "false");
|
btn.setAttribute("aria-expanded", "false");
|
||||||
}
|
}
|
||||||
|
|
@ -1193,7 +1241,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
? "🔴"
|
? "🔴"
|
||||||
: "🟡",
|
: "🟡",
|
||||||
title:
|
title:
|
||||||
"Connection " + status.charAt(0).toUpperCase() + status.slice(1),
|
"Connection " +
|
||||||
|
status.charAt(0).toUpperCase() +
|
||||||
|
status.slice(1),
|
||||||
message: message || "",
|
message: message || "",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -1206,7 +1256,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
if (e.altKey && !e.ctrlKey && !e.shiftKey) {
|
if (e.altKey && !e.ctrlKey && !e.shiftKey) {
|
||||||
const num = parseInt(e.key);
|
const num = parseInt(e.key);
|
||||||
if (num >= 1 && num <= 9) {
|
if (num >= 1 && num <= 9) {
|
||||||
const items = document.querySelectorAll(".app-item");
|
const items =
|
||||||
|
document.querySelectorAll(".app-item");
|
||||||
if (items[num - 1]) {
|
if (items[num - 1]) {
|
||||||
items[num - 1].click();
|
items[num - 1].click();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -1230,18 +1281,21 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
const userName = document.getElementById("userName");
|
const userName = document.getElementById("userName");
|
||||||
const userEmail = document.getElementById("userEmail");
|
const userEmail = document.getElementById("userEmail");
|
||||||
const userAvatar = document.getElementById("userAvatar");
|
const userAvatar = document.getElementById("userAvatar");
|
||||||
const userAvatarLarge = document.getElementById("userAvatarLarge");
|
const userAvatarLarge =
|
||||||
|
document.getElementById("userAvatarLarge");
|
||||||
const authAction = document.getElementById("authAction");
|
const authAction = document.getElementById("authAction");
|
||||||
const authText = document.getElementById("authText");
|
const authText = document.getElementById("authText");
|
||||||
const authIcon = document.getElementById("authIcon");
|
const authIcon = document.getElementById("authIcon");
|
||||||
const settingsBtn = document.getElementById("settingsBtn");
|
|
||||||
const appsButton = document.getElementById("appsButton");
|
|
||||||
const notificationsBtn = document.getElementById("notificationsBtn");
|
|
||||||
|
|
||||||
const displayName =
|
const displayName =
|
||||||
user.display_name || user.first_name || user.username || "User";
|
user.display_name ||
|
||||||
|
user.first_name ||
|
||||||
|
user.username ||
|
||||||
|
"User";
|
||||||
const email = user.email || "";
|
const email = user.email || "";
|
||||||
const initial = (displayName.charAt(0) || "U").toUpperCase();
|
const initial = (
|
||||||
|
displayName.charAt(0) || "U"
|
||||||
|
).toUpperCase();
|
||||||
|
|
||||||
console.log("Updating user UI:", displayName, email);
|
console.log("Updating user UI:", displayName, email);
|
||||||
|
|
||||||
|
|
@ -1274,20 +1328,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
authIcon.innerHTML =
|
authIcon.innerHTML =
|
||||||
'<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line>';
|
'<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line>';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settingsBtn) settingsBtn.style.display = "";
|
|
||||||
if (appsButton) appsButton.style.display = "";
|
|
||||||
if (notificationsBtn) notificationsBtn.style.display = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadUserProfile() {
|
function loadUserProfile() {
|
||||||
var token =
|
var token =
|
||||||
localStorage.getItem("gb-access-token") ||
|
localStorage.getItem("gb-access-token") ||
|
||||||
sessionStorage.getItem("gb-access-token");
|
sessionStorage.getItem("gb-access-token");
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.log("No auth token found - user is signed out");
|
console.log("No auth token found");
|
||||||
updateSignedOutUI();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1300,60 +1348,22 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
headers: { Authorization: "Bearer " + token },
|
headers: { Authorization: "Bearer " + token },
|
||||||
})
|
})
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (!res.ok) {
|
if (!res.ok) throw new Error("Not authenticated");
|
||||||
console.log("User not authenticated");
|
|
||||||
updateSignedOutUI();
|
|
||||||
throw new Error("Not authenticated");
|
|
||||||
}
|
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(function (user) {
|
.then(function (user) {
|
||||||
console.log("User profile loaded:", user);
|
console.log("User profile loaded:", user);
|
||||||
updateUserUI(user);
|
updateUserUI(user);
|
||||||
localStorage.setItem("gb-user-data", JSON.stringify(user));
|
localStorage.setItem(
|
||||||
|
"gb-user-data",
|
||||||
|
JSON.stringify(user),
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch(function (err) {
|
.catch(function (err) {
|
||||||
console.log("Failed to load user profile:", err);
|
console.log("Failed to load user profile:", err);
|
||||||
updateSignedOutUI();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSignedOutUI() {
|
|
||||||
const userName = document.getElementById("userName");
|
|
||||||
const userEmail = document.getElementById("userEmail");
|
|
||||||
const userAvatar = document.getElementById("userAvatar");
|
|
||||||
const userAvatarLarge = document.getElementById("userAvatarLarge");
|
|
||||||
const authAction = document.getElementById("authAction");
|
|
||||||
const authText = document.getElementById("authText");
|
|
||||||
const authIcon = document.getElementById("authIcon");
|
|
||||||
const settingsBtn = document.getElementById("settingsBtn");
|
|
||||||
const appsButton = document.getElementById("appsButton");
|
|
||||||
const notificationsBtn = document.getElementById("notificationsBtn");
|
|
||||||
|
|
||||||
if (userName) userName.textContent = "User";
|
|
||||||
if (userEmail) userEmail.textContent = "user@example.com";
|
|
||||||
if (userAvatar) {
|
|
||||||
const avatarSpan = userAvatar.querySelector("span");
|
|
||||||
if (avatarSpan) avatarSpan.textContent = "U";
|
|
||||||
}
|
|
||||||
if (userAvatarLarge) userAvatarLarge.textContent = "U";
|
|
||||||
|
|
||||||
if (authAction) {
|
|
||||||
authAction.href = "/auth/login.html";
|
|
||||||
authAction.removeAttribute("onclick");
|
|
||||||
authAction.style.color = "var(--primary)";
|
|
||||||
}
|
|
||||||
if (authText) authText.textContent = "Sign in";
|
|
||||||
if (authIcon) {
|
|
||||||
authIcon.innerHTML =
|
|
||||||
'<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path><polyline points="10 17 15 12 21 12"></polyline><line x1="15" y1="12" x2="3" y2="12"></line>';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settingsBtn) settingsBtn.style.display = "none";
|
|
||||||
if (appsButton) appsButton.style.display = "none";
|
|
||||||
if (notificationsBtn) notificationsBtn.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to load cached user first
|
// Try to load cached user first
|
||||||
var cachedUser = localStorage.getItem("gb-user-data");
|
var cachedUser = localStorage.getItem("gb-user-data");
|
||||||
if (cachedUser) {
|
if (cachedUser) {
|
||||||
|
|
@ -1362,12 +1372,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
if (user && user.email) {
|
if (user && user.email) {
|
||||||
updateUserUI(user);
|
updateUserUI(user);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always fetch fresh user data
|
// Always fetch fresh user data
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", loadUserProfile);
|
document.addEventListener(
|
||||||
|
"DOMContentLoaded",
|
||||||
|
loadUserProfile,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
loadUserProfile();
|
loadUserProfile();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1343,5 +1343,5 @@
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/suite/mail/mail.css" />
|
<link rel="stylesheet" href="mail/mail.css" />
|
||||||
<script src="/suite/mail/mail.js"></script>
|
<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">
|
<div class="autotask-container" data-theme="sentient">
|
||||||
<!-- Top Navigation Bar -->
|
<!-- Top Navigation Bar -->
|
||||||
|
|
@ -478,6 +478,6 @@ Examples:
|
||||||
<!-- Toast Container -->
|
<!-- Toast Container -->
|
||||||
<div class="toast-container" id="toast-container"></div>
|
<div class="toast-container" id="toast-container"></div>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/suite/tasks/progress-panel.css" />
|
<link rel="stylesheet" href="tasks/progress-panel.css" />
|
||||||
<script src="/suite/tasks/progress-panel.js"></script>
|
<script src="tasks/progress-panel.js"></script>
|
||||||
<script src="/suite/tasks/autotask.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()">
|
<button class="btn-secondary" onclick="closeDecisionModal()">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -12,4 +312,4 @@
|
||||||
</div>
|
</div>
|
||||||
</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
|
// Task polling for async task creation
|
||||||
if (typeof activePollingTaskId === "undefined") {
|
let activePollingTaskId = null;
|
||||||
var activePollingTaskId = null;
|
let pollingInterval = null;
|
||||||
var pollingInterval = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startTaskPolling(taskId) {
|
function startTaskPolling(taskId) {
|
||||||
// Stop any existing polling
|
// Stop any existing polling
|
||||||
|
|
@ -631,9 +629,7 @@ function handleWebSocketMessage(data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store pending manifest updates for tasks whose elements aren't loaded yet
|
// Store pending manifest updates for tasks whose elements aren't loaded yet
|
||||||
if (typeof pendingManifestUpdates === "undefined") {
|
const pendingManifestUpdates = new Map();
|
||||||
var pendingManifestUpdates = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderManifestProgress(
|
function renderManifestProgress(
|
||||||
taskId,
|
taskId,
|
||||||
|
|
@ -2763,9 +2759,8 @@ function formatTime(seconds) {
|
||||||
// GLOBAL STYLES FOR TOAST ANIMATIONS
|
// GLOBAL STYLES FOR TOAST ANIMATIONS
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
if (typeof taskStyleElement === "undefined") {
|
const style = document.createElement("style");
|
||||||
var taskStyleElement = document.createElement("style");
|
style.textContent = `
|
||||||
taskStyleElement.textContent = `
|
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
@ -2810,8 +2805,7 @@ if (typeof taskStyleElement === "undefined") {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(taskStyleElement);
|
document.head.appendChild(style);
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// GOALS, PENDING INFO, SCHEDULERS, MONITORS
|
// GOALS, PENDING INFO, SCHEDULERS, MONITORS
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue