Compare commits

...

10 commits

Author SHA1 Message Date
74b2e84137 Update Forgejo CI workflow - cross-platform builds (Linux, Windows, macOS)
Some checks failed
GBCI / build (push) Failing after 1m37s
2025-12-16 13:25:27 -03:00
45ca9b8bbf Make tasks app themable - use standard CSS variables instead of sentient-specific 2025-12-16 10:38:06 -03:00
d38edc6631 Restore 24 theme buttons in settings panel (core, retro, classic, tech themes) 2025-12-16 09:57:46 -03:00
9fe234aa3c Update UI components, styling, and add theme-sentient and intents 2025-12-15 23:16:09 -03:00
db06e42289 Redesign header: merge app tabs, add search center, remove header from tasks.html 2025-12-15 18:48:40 -03:00
d30d11ab9b Fix app launcher URLs - use absolute hash URLs to prevent path stacking 2025-12-15 18:24:06 -03:00
d9024a3ef6 Fix HTMX navigation - use absolute paths to prevent URL stacking 2025-12-15 16:57:27 -03:00
3439a5722c Fix all app CSS loading - add all app CSS to index.html head 2025-12-15 16:48:36 -03:00
c9602593a4 Fix drive.html to use proper CSS class structure matching existing drive.css 2025-12-15 16:44:07 -03:00
45b23af166 Fix suite app routing - use HTML files instead of API endpoints, add drive.html 2025-12-15 16:40:04 -03:00
30 changed files with 7153 additions and 2148 deletions

View file

@ -0,0 +1,108 @@
name: GBCI
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
build:
runs-on: gbo
steps:
- name: Disable SSL verification (temporary)
run: git config --global http.sslVerify false
- uses: actions/checkout@v4
- name: Install Rust
uses: msrd0/rust-toolchain@v1
with:
toolchain: stable
targets: |
x86_64-unknown-linux-gnu
aarch64-unknown-linux-gnu
x86_64-pc-windows-gnu
aarch64-pc-windows-msvc
x86_64-apple-darwin
aarch64-apple-darwin
- name: Install cross-compilation dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
gcc-aarch64-linux-gnu \
gcc-mingw-w64-x86-64 \
musl-tools
- name: Setup environment
run: |
sudo cp /opt/gbo/bin/system/botui.env .env
- name: Build Linux x86_64
run: |
cargo build --locked --release --target x86_64-unknown-linux-gnu
- name: Build Linux ARM64
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
run: |
cargo build --locked --release --target aarch64-unknown-linux-gnu
- name: Build Windows x86_64
env:
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc
run: |
cargo build --locked --release --target x86_64-pc-windows-gnu
- name: Build macOS x86_64 (cross-compile)
run: |
cargo build --locked --release --target x86_64-apple-darwin || echo "macOS x86_64 cross-compile requires osxcross"
- name: Build macOS ARM64 (cross-compile)
run: |
cargo build --locked --release --target aarch64-apple-darwin || echo "macOS ARM64 cross-compile requires osxcross"
- name: Organize release artifacts
run: |
mkdir -p release/linux-x86_64
mkdir -p release/linux-arm64
mkdir -p release/windows-x86_64
mkdir -p release/macos-x86_64
mkdir -p release/macos-arm64
# Linux x86_64
cp ./target/x86_64-unknown-linux-gnu/release/botui release/linux-x86_64/ || true
# Linux ARM64
cp ./target/aarch64-unknown-linux-gnu/release/botui release/linux-arm64/ || true
# Windows x86_64
cp ./target/x86_64-pc-windows-gnu/release/botui.exe release/windows-x86_64/ || true
# macOS x86_64
cp ./target/x86_64-apple-darwin/release/botui release/macos-x86_64/ || true
# macOS ARM64
cp ./target/aarch64-apple-darwin/release/botui release/macos-arm64/ || true
- name: Deploy binaries
run: |
sudo mkdir -p /opt/gbo/releases/botui/linux-x86_64
sudo mkdir -p /opt/gbo/releases/botui/linux-arm64
sudo mkdir -p /opt/gbo/releases/botui/windows-x86_64
sudo mkdir -p /opt/gbo/releases/botui/macos-x86_64
sudo mkdir -p /opt/gbo/releases/botui/macos-arm64
sudo cp -r release/* /opt/gbo/releases/botui/
sudo chmod -R 755 /opt/gbo/releases/botui/
- name: Deploy and restart local service
run: |
lxc exec bot:pragmatismo-system -- systemctl stop botui
sudo cp ./target/x86_64-unknown-linux-gnu/release/botui /opt/gbo/bin/botui
sudo chmod +x /opt/gbo/bin/botui
lxc exec bot:pragmatismo-system -- systemctl start botui

View file

@ -18,8 +18,7 @@ use log::{debug, error, info};
use serde::Deserialize;
use std::{fs, path::PathBuf};
use tokio_tungstenite::{
connect_async_tls_with_config,
tungstenite::protocol::Message as TungsteniteMessage,
connect_async_tls_with_config, tungstenite::protocol::Message as TungsteniteMessage,
};
use crate::shared::AppState;
@ -99,7 +98,10 @@ async fn proxy_api(
req: Request<Body>,
) -> Response<Body> {
let path = original_uri.path();
let query = original_uri.query().map(|q| format!("?{}", q)).unwrap_or_default();
let query = original_uri
.query()
.map(|q| format!("?{}", q))
.unwrap_or_default();
let method = req.method().clone();
let headers = req.headers().clone();
@ -217,7 +219,11 @@ async fn ws_proxy(
async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQuery) {
let backend_url = format!(
"{}/ws?session_id={}&user_id={}",
state.client.base_url().replace("https://", "wss://").replace("http://", "ws://"),
state
.client
.base_url()
.replace("https://", "wss://")
.replace("http://", "ws://"),
params.session_id,
params.user_id
);
@ -234,12 +240,8 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
let connector = tokio_tungstenite::Connector::NativeTls(tls_connector);
// Connect to backend WebSocket
let backend_result = connect_async_tls_with_config(
&backend_url,
None,
false,
Some(connector),
).await;
let backend_result =
connect_async_tls_with_config(&backend_url, None, false, Some(connector)).await;
let backend_socket = match backend_result {
Ok((socket, _)) => socket,
@ -260,22 +262,38 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
while let Some(msg) = client_rx.next().await {
match msg {
Ok(AxumMessage::Text(text)) => {
if backend_tx.send(TungsteniteMessage::Text(text)).await.is_err() {
if backend_tx
.send(TungsteniteMessage::Text(text))
.await
.is_err()
{
break;
}
}
Ok(AxumMessage::Binary(data)) => {
if backend_tx.send(TungsteniteMessage::Binary(data)).await.is_err() {
if backend_tx
.send(TungsteniteMessage::Binary(data))
.await
.is_err()
{
break;
}
}
Ok(AxumMessage::Ping(data)) => {
if backend_tx.send(TungsteniteMessage::Ping(data)).await.is_err() {
if backend_tx
.send(TungsteniteMessage::Ping(data))
.await
.is_err()
{
break;
}
}
Ok(AxumMessage::Pong(data)) => {
if backend_tx.send(TungsteniteMessage::Pong(data)).await.is_err() {
if backend_tx
.send(TungsteniteMessage::Pong(data))
.await
.is_err()
{
break;
}
}
@ -323,8 +341,32 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
/// Create WebSocket proxy router
fn create_ws_router() -> Router<AppState> {
Router::new().fallback(any(ws_proxy))
}
/// Create UI HTMX proxy router (for HTML fragment endpoints)
fn create_ui_router() -> Router<AppState> {
Router::new()
.fallback(get(ws_proxy))
// Email UI endpoints
.route("/email/accounts", any(proxy_api))
.route("/email/list", any(proxy_api))
.route("/email/folders", any(proxy_api))
.route("/email/compose", any(proxy_api))
.route("/email/labels", any(proxy_api))
.route("/email/templates", any(proxy_api))
.route("/email/signatures", any(proxy_api))
.route("/email/rules", any(proxy_api))
.route("/email/search", any(proxy_api))
.route("/email/auto-responder", any(proxy_api))
.route("/email/{id}", any(proxy_api))
.route("/email/{id}/delete", any(proxy_api))
// Calendar UI endpoints
.route("/calendar/list", any(proxy_api))
.route("/calendar/upcoming", any(proxy_api))
.route("/calendar/event/new", any(proxy_api))
.route("/calendar/new", any(proxy_api))
// Fallback for any other /ui/* routes
.fallback(any(proxy_api))
}
/// Configure and return the main router
@ -338,6 +380,8 @@ pub fn configure_router() -> Router {
.route("/health", get(health))
// API proxy routes
.nest("/api", create_api_router())
// UI HTMX proxy routes (for /ui/* endpoints that return HTML fragments)
.nest("/ui", create_ui_router())
// WebSocket proxy routes
.nest("/ws", create_ws_router())
// UI routes
@ -373,6 +417,62 @@ pub fn configure_router() -> Router {
"/suite/tasks",
tower_http::services::ServeDir::new(suite_path.join("tasks")),
)
.nest_service(
"/suite/calendar",
tower_http::services::ServeDir::new(suite_path.join("calendar")),
)
.nest_service(
"/suite/meet",
tower_http::services::ServeDir::new(suite_path.join("meet")),
)
.nest_service(
"/suite/paper",
tower_http::services::ServeDir::new(suite_path.join("paper")),
)
.nest_service(
"/suite/research",
tower_http::services::ServeDir::new(suite_path.join("research")),
)
.nest_service(
"/suite/analytics",
tower_http::services::ServeDir::new(suite_path.join("analytics")),
)
.nest_service(
"/suite/monitoring",
tower_http::services::ServeDir::new(suite_path.join("monitoring")),
)
.nest_service(
"/suite/admin",
tower_http::services::ServeDir::new(suite_path.join("admin")),
)
.nest_service(
"/suite/auth",
tower_http::services::ServeDir::new(suite_path.join("auth")),
)
.nest_service(
"/suite/settings",
tower_http::services::ServeDir::new(suite_path.join("settings")),
)
.nest_service(
"/suite/sources",
tower_http::services::ServeDir::new(suite_path.join("sources")),
)
.nest_service(
"/suite/attendant",
tower_http::services::ServeDir::new(suite_path.join("attendant")),
)
.nest_service(
"/suite/tools",
tower_http::services::ServeDir::new(suite_path.join("tools")),
)
.nest_service(
"/suite/assets",
tower_http::services::ServeDir::new(suite_path.join("assets")),
)
.nest_service(
"/suite/partials",
tower_http::services::ServeDir::new(suite_path.join("partials")),
)
// Legacy paths for backward compatibility (serve suite assets)
.nest_service(
"/js",

View file

@ -4,19 +4,20 @@
.admin-layout {
display: grid;
grid-template-columns: 260px 1fr;
min-height: calc(100vh - 56px);
background: var(--bg);
height: 100%;
min-height: 0;
background: var(--bg, var(--bg-primary, #0a0a0f));
color: var(--text, var(--text-primary, #ffffff));
}
/* Sidebar */
.admin-sidebar {
background: var(--surface);
border-right: 1px solid var(--border);
background: var(--surface, var(--bg-secondary, #12121a));
border-right: 1px solid var(--border, #2a2a3a);
display: flex;
flex-direction: column;
position: sticky;
top: 56px;
height: calc(100vh - 56px);
overflow-y: auto;
height: 100%;
}
.admin-header {
@ -138,10 +139,22 @@
justify-content: center;
}
.stat-icon.users { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
.stat-icon.groups { background: rgba(16, 185, 129, 0.1); color: #10b981; }
.stat-icon.bots { background: rgba(168, 85, 247, 0.1); color: #a855f7; }
.stat-icon.storage { background: rgba(249, 115, 22, 0.1); color: #f97316; }
.stat-icon.users {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
.stat-icon.groups {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.stat-icon.bots {
background: rgba(168, 85, 247, 0.1);
color: #a855f7;
}
.stat-icon.storage {
background: rgba(249, 115, 22, 0.1);
color: #f97316;
}
.stat-content {
display: flex;
@ -287,9 +300,15 @@
border-radius: 50%;
}
.health-status.healthy { background: #10b981; }
.health-status.warning { background: #f59e0b; }
.health-status.error { background: #ef4444; }
.health-status.healthy {
background: #10b981;
}
.health-status.warning {
background: #f59e0b;
}
.health-status.error {
background: #ef4444;
}
.health-value {
font-size: 24px;
@ -458,7 +477,9 @@
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
/* Responsive */

View file

@ -2,8 +2,10 @@
.attendant-container {
display: flex;
height: calc(100vh - 60px);
background: var(--bg);
height: 100%;
min-height: 0;
background: var(--bg, var(--bg-primary, #0a0a0f));
color: var(--text, var(--text-primary, #ffffff));
}
/* Queue Panel */
@ -300,10 +302,18 @@
margin-right: 6px;
}
.status-dot.online { background: #22c55e; }
.status-dot.away { background: #f59e0b; }
.status-dot.busy { background: #ef4444; }
.status-dot.offline { background: #6b7280; }
.status-dot.online {
background: #22c55e;
}
.status-dot.away {
background: #f59e0b;
}
.status-dot.busy {
background: #ef4444;
}
.status-dot.offline {
background: #6b7280;
}
/* Responsive */
@media (max-width: 1024px) {

View file

@ -362,182 +362,64 @@
<!-- Theme Selection -->
<div class="settings-section">
<div class="settings-section-title">Theme</div>
<div class="theme-grid">
<div
class="theme-option theme-dark"
data-theme="dark"
title="Dark"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div
class="theme-option-line"
style="width: 80%"
></div>
<div
class="theme-option-line"
style="width: 60%"
></div>
<div
class="theme-option-line"
style="width: 70%"
></div>
</div>
</div>
<span class="theme-option-name">Dark</span>
</div>
<div
class="theme-option theme-light"
data-theme="light"
title="Light"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div
class="theme-option-line"
style="width: 80%"
></div>
<div
class="theme-option-line"
style="width: 60%"
></div>
<div
class="theme-option-line"
style="width: 70%"
></div>
</div>
</div>
<span class="theme-option-name">Light</span>
</div>
<div
class="theme-option theme-blue"
data-theme="blue"
title="Ocean"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div
class="theme-option-line"
style="width: 80%"
></div>
<div
class="theme-option-line"
style="width: 60%"
></div>
<div
class="theme-option-line"
style="width: 70%"
></div>
</div>
</div>
<span class="theme-option-name">Ocean</span>
</div>
<div
class="theme-option theme-purple"
data-theme="purple"
title="Violet"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div
class="theme-option-line"
style="width: 80%"
></div>
<div
class="theme-option-line"
style="width: 60%"
></div>
<div
class="theme-option-line"
style="width: 70%"
></div>
</div>
</div>
<span class="theme-option-name"
>Violet</span
>
</div>
<div
class="theme-option theme-green"
data-theme="green"
title="Forest"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div
class="theme-option-line"
style="width: 80%"
></div>
<div
class="theme-option-line"
style="width: 60%"
></div>
<div
class="theme-option-line"
style="width: 70%"
></div>
</div>
</div>
<span class="theme-option-name"
>Forest</span
>
</div>
<div
class="theme-option theme-orange"
data-theme="orange"
title="Sunset"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div
class="theme-option-line"
style="width: 80%"
></div>
<div
class="theme-option-line"
style="width: 60%"
></div>
<div
class="theme-option-line"
style="width: 70%"
></div>
</div>
</div>
<span class="theme-option-name"
>Sunset</span
>
</div>
</div>
<select id="themeSelector" class="theme-dropdown">
<optgroup label="Core Themes">
<option value="sentient">
🤖 Sentient
</option>
<option value="dark">🌑 Dark</option>
<option value="light">☀️ Light</option>
<option value="blue">🌊 Ocean</option>
<option value="purple">💜 Violet</option>
<option value="green">🌲 Forest</option>
<option value="orange">🌅 Sunset</option>
</optgroup>
<optgroup label="Retro Themes">
<option value="cyberpunk">
🌃 Cyberpunk
</option>
<option value="retrowave">
🌴 Retrowave
</option>
<option value="vapordream">
💭 Vapor Dream
</option>
<option value="y2kglow">✨ Y2K</option>
<option value="arcadeflash">
🕹️ Arcade
</option>
<option value="discofever">🪩 Disco</option>
<option value="grungeera">🎸 Grunge</option>
</optgroup>
<optgroup label="Classic Themes">
<option value="jazzage">🎺 Jazz Age</option>
<option value="mellowgold">
🌻 Mellow Gold
</option>
<option value="midcenturymod">
🏠 Mid Century
</option>
<option value="polaroidmemories">
📷 Polaroid
</option>
<option value="saturdaycartoons">
📺 Cartoons
</option>
<option value="seasidepostcard">
🏖️ Seaside
</option>
<option value="typewriter">
⌨️ Typewriter
</option>
</optgroup>
<optgroup label="Tech Themes">
<option value="3dbevel">🔲 3D Bevel</option>
<option value="xeroxui">📠 Xerox UI</option>
<option value="xtreegold">
📁 XTree Gold
</option>
</optgroup>
</select>
</div>
<div class="settings-divider"></div>
@ -730,7 +612,7 @@
<div id="main-content" aria-busy="false" tabindex="-1">
{% block content %}{% endblock %}
</div>
<!-- AI Assistant Panel (visible on all screens except Chat) -->
<aside class="ai-assistant-panel" id="ai-panel">
<div class="ai-panel-header">
@ -741,31 +623,56 @@
<p class="ai-status">Ready to help</p>
</div>
</div>
<button class="ai-panel-toggle" onclick="toggleAIPanel()" aria-label="Close AI Panel"></button>
<button
class="ai-panel-toggle"
onclick="toggleAIPanel()"
aria-label="Close AI Panel"
>
</button>
</div>
<div class="ai-panel-messages" id="ai-messages">
<div class="ai-message assistant">
<div class="ai-message-bubble">Olá! Sou seu assistente AI. Posso ajudar com qualquer tarefa nesta tela.</div>
<div class="ai-message-bubble">
Olá! Sou seu assistente AI. Posso ajudar com
qualquer tarefa nesta tela.
</div>
</div>
</div>
<div class="ai-quick-actions">
<span class="quick-actions-label">AÇÕES RÁPIDAS</span>
<div class="quick-actions-grid" id="ai-quick-actions">
<!-- Quick actions loaded dynamically based on current app -->
</div>
</div>
<div class="ai-panel-input">
<input type="text" class="ai-input" placeholder="Como posso ajudar?" id="ai-input"
onkeypress="if(event.key==='Enter')sendAIMessage()">
<button class="ai-send-btn" onclick="sendAIMessage()" aria-label="Send message"></button>
<input
type="text"
class="ai-input"
placeholder="Como posso ajudar?"
id="ai-input"
onkeypress="if(event.key==='Enter')sendAIMessage()"
/>
<button
class="ai-send-btn"
onclick="sendAIMessage()"
aria-label="Send message"
>
</button>
</div>
</aside>
<!-- AI Panel Toggle Button (when panel is collapsed) -->
<button class="ai-panel-fab" id="ai-fab" onclick="toggleAIPanel()" aria-label="Open AI Assistant">
<button
class="ai-panel-fab"
id="ai-fab"
onclick="toggleAIPanel()"
aria-label="Open AI Assistant"
>
🤖
</button>
</main>
@ -868,23 +775,26 @@
}
});
// Theme handling
const themeOptions = document.querySelectorAll(".theme-option");
const savedTheme = localStorage.getItem("gb-theme") || "dark";
// Theme handling with dropdown
const themeSelector = document.getElementById("themeSelector");
const savedTheme = localStorage.getItem("gb-theme") || "sentient";
document.body.setAttribute("data-theme", savedTheme);
document
.querySelector(`.theme-option[data-theme="${savedTheme}"]`)
?.classList.add("active");
themeOptions.forEach((option) => {
option.addEventListener("click", () => {
const theme = option.getAttribute("data-theme");
// Set dropdown to saved value
if (themeSelector) {
themeSelector.value = savedTheme;
themeSelector.addEventListener("change", (e) => {
const theme = e.target.value;
document.body.setAttribute("data-theme", theme);
localStorage.setItem("gb-theme", theme);
themeOptions.forEach((o) => o.classList.remove("active"));
option.classList.add("active");
// Also notify ThemeManager if it exists
if (window.ThemeManager && window.ThemeManager.loadTheme) {
window.ThemeManager.loadTheme(theme);
}
});
});
}
function toggleQuickSetting(el) {
el.classList.toggle("active");
@ -1183,148 +1093,151 @@
// =================================================================
// AI ASSISTANT PANEL
// =================================================================
// Quick actions per app
const aiQuickActions = {
drive: [
{ label: 'Upload file', action: 'upload_file' },
{ label: 'Create folder', action: 'create_folder' },
{ label: 'Search files', action: 'search_files' },
{ label: 'Share', action: 'share_item' }
{ label: "Upload file", action: "upload_file" },
{ label: "Create folder", action: "create_folder" },
{ label: "Search files", action: "search_files" },
{ label: "Share", action: "share_item" },
],
tasks: [
{ label: 'New task', action: 'create_task' },
{ label: 'Due today', action: 'show_due_today' },
{ label: 'Summary', action: 'tasks_summary' },
{ label: 'Priorities', action: 'show_priorities' }
{ label: "New task", action: "create_task" },
{ label: "Due today", action: "show_due_today" },
{ label: "Summary", action: "tasks_summary" },
{ label: "Priorities", action: "show_priorities" },
],
mail: [
{ label: 'Compose', action: 'compose_email' },
{ label: 'Unread', action: 'show_unread' },
{ label: 'Search', action: 'search_mail' },
{ label: 'Summary', action: 'mail_summary' }
{ label: "Compose", action: "compose_email" },
{ label: "Unread", action: "show_unread" },
{ label: "Search", action: "search_mail" },
{ label: "Summary", action: "mail_summary" },
],
calendar: [
{ label: 'New event', action: 'create_event' },
{ label: 'Today', action: 'show_today' },
{ label: 'This week', action: 'show_week' },
{ label: 'Find time', action: 'find_free_time' }
{ label: "New event", action: "create_event" },
{ label: "Today", action: "show_today" },
{ label: "This week", action: "show_week" },
{ label: "Find time", action: "find_free_time" },
],
meet: [
{ label: 'Start call', action: 'start_meeting' },
{ label: 'Schedule', action: 'schedule_meeting' },
{ label: 'Join', action: 'join_meeting' }
{ label: "Start call", action: "start_meeting" },
{ label: "Schedule", action: "schedule_meeting" },
{ label: "Join", action: "join_meeting" },
],
paper: [
{ label: 'New doc', action: 'create_document' },
{ label: 'Templates', action: 'show_templates' },
{ label: 'Recent', action: 'show_recent' }
{ label: "New doc", action: "create_document" },
{ label: "Templates", action: "show_templates" },
{ label: "Recent", action: "show_recent" },
],
research: [
{ label: 'New search', action: 'new_research' },
{ label: 'Sources', action: 'show_sources' },
{ label: 'Citations', action: 'generate_citations' }
{ label: "New search", action: "new_research" },
{ label: "Sources", action: "show_sources" },
{ label: "Citations", action: "generate_citations" },
],
sources: [
{ label: 'Add source', action: 'add_source' },
{ label: 'Import', action: 'import_sources' },
{ label: 'Categories', action: 'show_categories' }
{ label: "Add source", action: "add_source" },
{ label: "Import", action: "import_sources" },
{ label: "Categories", action: "show_categories" },
],
analytics: [
{ label: 'Dashboard', action: 'show_dashboard' },
{ label: 'Reports', action: 'show_reports' },
{ label: 'Export', action: 'export_data' }
{ label: "Dashboard", action: "show_dashboard" },
{ label: "Reports", action: "show_reports" },
{ label: "Export", action: "export_data" },
],
admin: [
{ label: 'Users', action: 'manage_users' },
{ label: 'Settings', action: 'show_settings' },
{ label: 'Logs', action: 'show_logs' }
{ label: "Users", action: "manage_users" },
{ label: "Settings", action: "show_settings" },
{ label: "Logs", action: "show_logs" },
],
monitoring: [
{ label: 'Status', action: 'show_status' },
{ label: 'Alerts', action: 'show_alerts' },
{ label: 'Metrics', action: 'show_metrics' }
{ label: "Status", action: "show_status" },
{ label: "Alerts", action: "show_alerts" },
{ label: "Metrics", action: "show_metrics" },
],
default: [
{ label: 'Help', action: 'show_help' },
{ label: 'Shortcuts', action: 'show_shortcuts' },
{ label: 'Settings', action: 'open_settings' }
]
{ label: "Help", action: "show_help" },
{ label: "Shortcuts", action: "show_shortcuts" },
{ label: "Settings", action: "open_settings" },
],
};
// Get current app from URL or hash
function getCurrentApp() {
const hash = window.location.hash.replace('#', '');
const hash = window.location.hash.replace("#", "");
const path = window.location.pathname;
if (hash) return hash;
const match = path.match(/\/([a-z]+)\//);
return match ? match[1] : 'default';
return match ? match[1] : "default";
}
// Update body data-app attribute
function updateCurrentApp() {
const app = getCurrentApp();
document.body.setAttribute('data-app', app);
document.body.setAttribute("data-app", app);
loadQuickActions(app);
}
// Load quick actions for current app
function loadQuickActions(app) {
const container = document.getElementById('ai-quick-actions');
const container = document.getElementById("ai-quick-actions");
if (!container) return;
const actions = aiQuickActions[app] || aiQuickActions.default;
container.innerHTML = actions.map(a =>
`<button class="quick-action-btn" onclick="handleQuickAction('${a.action}')">${a.label}</button>`
).join('');
container.innerHTML = actions
.map(
(a) =>
`<button class="quick-action-btn" onclick="handleQuickAction('${a.action}')">${a.label}</button>`,
)
.join("");
}
// Handle quick action click
function handleQuickAction(action) {
const input = document.getElementById('ai-input');
const input = document.getElementById("ai-input");
const actionMessages = {
upload_file: 'Help me upload a file',
create_folder: 'Create a new folder',
search_files: 'Search for files',
share_item: 'Help me share this item',
create_task: 'Create a new task',
show_due_today: 'Show tasks due today',
tasks_summary: 'Give me a summary of my tasks',
show_priorities: 'Show my priority tasks',
compose_email: 'Help me compose an email',
show_unread: 'Show unread emails',
search_mail: 'Search my emails',
mail_summary: 'Summarize my inbox',
create_event: 'Create a calendar event',
show_today: 'Show today\'s schedule',
show_week: 'Show this week\'s events',
find_free_time: 'Find free time slots',
start_meeting: 'Start a new meeting',
schedule_meeting: 'Schedule a meeting',
join_meeting: 'Join a meeting',
create_document: 'Create a new document',
show_templates: 'Show document templates',
show_recent: 'Show recent documents',
new_research: 'Start new research',
show_sources: 'Show my sources',
generate_citations: 'Generate citations',
add_source: 'Add a new source',
import_sources: 'Import sources',
show_categories: 'Show categories',
show_dashboard: 'Show analytics dashboard',
show_reports: 'Show reports',
export_data: 'Export analytics data',
manage_users: 'Manage users',
show_settings: 'Show admin settings',
show_logs: 'Show system logs',
show_status: 'Show system status',
show_alerts: 'Show active alerts',
show_metrics: 'Show performance metrics',
show_help: 'Help me get started',
show_shortcuts: 'Show keyboard shortcuts',
open_settings: 'Open settings'
upload_file: "Help me upload a file",
create_folder: "Create a new folder",
search_files: "Search for files",
share_item: "Help me share this item",
create_task: "Create a new task",
show_due_today: "Show tasks due today",
tasks_summary: "Give me a summary of my tasks",
show_priorities: "Show my priority tasks",
compose_email: "Help me compose an email",
show_unread: "Show unread emails",
search_mail: "Search my emails",
mail_summary: "Summarize my inbox",
create_event: "Create a calendar event",
show_today: "Show today's schedule",
show_week: "Show this week's events",
find_free_time: "Find free time slots",
start_meeting: "Start a new meeting",
schedule_meeting: "Schedule a meeting",
join_meeting: "Join a meeting",
create_document: "Create a new document",
show_templates: "Show document templates",
show_recent: "Show recent documents",
new_research: "Start new research",
show_sources: "Show my sources",
generate_citations: "Generate citations",
add_source: "Add a new source",
import_sources: "Import sources",
show_categories: "Show categories",
show_dashboard: "Show analytics dashboard",
show_reports: "Show reports",
export_data: "Export analytics data",
manage_users: "Manage users",
show_settings: "Show admin settings",
show_logs: "Show system logs",
show_status: "Show system status",
show_alerts: "Show active alerts",
show_metrics: "Show performance metrics",
show_help: "Help me get started",
show_shortcuts: "Show keyboard shortcuts",
open_settings: "Open settings",
};
if (input && actionMessages[action]) {
input.value = actionMessages[action];
sendAIMessage();
@ -1333,77 +1246,83 @@
// Toggle AI panel
function toggleAIPanel() {
document.body.classList.toggle('ai-panel-collapsed');
localStorage.setItem('ai-panel-collapsed', document.body.classList.contains('ai-panel-collapsed'));
document.body.classList.toggle("ai-panel-collapsed");
localStorage.setItem(
"ai-panel-collapsed",
document.body.classList.contains("ai-panel-collapsed"),
);
}
// Send AI message
function sendAIMessage() {
const input = document.getElementById('ai-input');
const messagesContainer = document.getElementById('ai-messages');
const input = document.getElementById("ai-input");
const messagesContainer =
document.getElementById("ai-messages");
const message = input?.value?.trim();
if (!message || !messagesContainer) return;
// Add user message
const userMsg = document.createElement('div');
userMsg.className = 'ai-message user';
const userMsg = document.createElement("div");
userMsg.className = "ai-message user";
userMsg.innerHTML = `<div class="ai-message-bubble">${escapeHtml(message)}</div>`;
messagesContainer.appendChild(userMsg);
// Clear input
input.value = '';
input.value = "";
// Scroll to bottom
messagesContainer.scrollTop = messagesContainer.scrollHeight;
// Show typing indicator
const typing = document.createElement('div');
typing.className = 'ai-message assistant';
typing.id = 'ai-typing';
const typing = document.createElement("div");
typing.className = "ai-message assistant";
typing.id = "ai-typing";
typing.innerHTML = `<div class="ai-typing-indicator"><span></span><span></span><span></span></div>`;
messagesContainer.appendChild(typing);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
// Simulate AI response (replace with actual API call)
setTimeout(() => {
typing.remove();
const aiMsg = document.createElement('div');
aiMsg.className = 'ai-message assistant';
const aiMsg = document.createElement("div");
aiMsg.className = "ai-message assistant";
aiMsg.innerHTML = `<div class="ai-message-bubble">Entendi! Estou processando sua solicitação: "${escapeHtml(message)}". Como posso ajudar mais?</div>`;
messagesContainer.appendChild(aiMsg);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
messagesContainer.scrollTop =
messagesContainer.scrollHeight;
}, 1500);
}
// Escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// Restore AI panel state on load
function initAIPanel() {
const collapsed = localStorage.getItem('ai-panel-collapsed') === 'true';
const collapsed =
localStorage.getItem("ai-panel-collapsed") === "true";
if (collapsed) {
document.body.classList.add('ai-panel-collapsed');
document.body.classList.add("ai-panel-collapsed");
}
updateCurrentApp();
}
// Initialize on DOM ready
document.addEventListener('DOMContentLoaded', initAIPanel);
document.addEventListener("DOMContentLoaded", initAIPanel);
// Update app on navigation
document.body.addEventListener('htmx:afterSwap', function(e) {
if (e.detail.target.id === 'main-content') {
document.body.addEventListener("htmx:afterSwap", function (e) {
if (e.detail.target.id === "main-content") {
updateCurrentApp();
}
});
// Also track hash changes
window.addEventListener('hashchange', updateCurrentApp);
window.addEventListener("hashchange", updateCurrentApp);
</script>
{% block scripts %}{% endblock %}

View file

@ -3,9 +3,10 @@
/* Calendar Container */
.calendar-container {
display: flex;
height: calc(100vh - 60px);
background: var(--background);
color: var(--foreground);
height: 100%;
min-height: 0;
background: var(--background, var(--bg-primary, #0a0a0f));
color: var(--foreground, var(--text-primary, #ffffff));
}
/* Sidebar */
@ -135,7 +136,7 @@
}
.mini-day.has-events::after {
content: '';
content: "";
position: absolute;
bottom: 2px;
width: 4px;
@ -199,7 +200,7 @@
}
.calendar-checkbox.checked::after {
content: '✓';
content: "✓";
display: flex;
align-items: center;
justify-content: center;
@ -562,11 +563,26 @@
cursor: pointer;
}
.month-event.blue { background: rgba(59, 130, 246, 0.2); color: #3b82f6; }
.month-event.green { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
.month-event.red { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.month-event.purple { background: rgba(168, 85, 247, 0.2); color: #a855f7; }
.month-event.orange { background: rgba(249, 115, 22, 0.2); color: #f97316; }
.month-event.blue {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.month-event.green {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.month-event.red {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.month-event.purple {
background: rgba(168, 85, 247, 0.2);
color: #a855f7;
}
.month-event.orange {
background: rgba(249, 115, 22, 0.2);
color: #f97316;
}
.more-events {
font-size: 11px;
@ -592,11 +608,26 @@
z-index: 5;
}
.calendar-event.blue { background: rgba(59, 130, 246, 0.9); color: white; }
.calendar-event.green { background: rgba(34, 197, 94, 0.9); color: white; }
.calendar-event.red { background: rgba(239, 68, 68, 0.9); color: white; }
.calendar-event.purple { background: rgba(168, 85, 247, 0.9); color: white; }
.calendar-event.orange { background: rgba(249, 115, 22, 0.9); color: white; }
.calendar-event.blue {
background: rgba(59, 130, 246, 0.9);
color: white;
}
.calendar-event.green {
background: rgba(34, 197, 94, 0.9);
color: white;
}
.calendar-event.red {
background: rgba(239, 68, 68, 0.9);
color: white;
}
.calendar-event.purple {
background: rgba(168, 85, 247, 0.9);
color: white;
}
.calendar-event.orange {
background: rgba(249, 115, 22, 0.9);
color: white;
}
.event-title {
font-weight: 500;
@ -622,7 +653,7 @@
}
.current-time-indicator::before {
content: '';
content: "";
position: absolute;
left: -5px;
top: -4px;
@ -782,7 +813,9 @@
.color-option input:checked + .color-dot {
transform: scale(1.2);
box-shadow: 0 0 0 3px var(--background), 0 0 0 5px currentColor;
box-shadow:
0 0 0 3px var(--background),
0 0 0 5px currentColor;
}
.form-actions {
@ -909,7 +942,9 @@
text-decoration: none;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
transition:
background 0.2s,
border-color 0.2s;
}
.ical-btn:hover {

View file

@ -14,7 +14,7 @@
<!-- Quick Actions -->
<div class="sidebar-actions">
<button class="btn-primary-full" id="new-event-btn"
hx-get="/api/calendar/event/new"
hx-get="/ui/calendar/event/new"
hx-target="#event-modal-content"
hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -55,7 +55,7 @@
<div class="section-header">
<h3>My Calendars</h3>
<button class="btn-icon-sm" title="Add Calendar"
hx-get="/api/calendar/new"
hx-get="/ui/calendar/new"
hx-target="#calendar-modal-content">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
@ -64,7 +64,7 @@
</button>
</div>
<div class="calendars-list" id="calendars-list"
hx-get="/api/calendar/list"
hx-get="/ui/calendar/list"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Calendars loaded here -->
@ -75,7 +75,7 @@
<div class="sidebar-section">
<h3>Upcoming</h3>
<div class="upcoming-events" id="upcoming-events"
hx-get="/api/calendar/upcoming"
hx-get="/ui/calendar/upcoming"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Upcoming events loaded here -->
@ -423,7 +423,7 @@
<div class="form-group">
<label>Import to calendar</label>
<select name="calendar_id"
hx-get="/api/calendar/list"
hx-get="/ui/calendar/list"
hx-trigger="load"
hx-swap="innerHTML">
<option value="">Default Calendar</option>
@ -1692,6 +1692,8 @@
function updateCurrentTimeIndicator() {
const indicator = document.getElementById('current-time-indicator');
if (!indicator) return; // Guard against null element
const now = new Date();
const minutes = now.getHours() * 60 + now.getMinutes();
const top = (minutes / 60) * 48; // 48px per hour

View file

@ -41,8 +41,15 @@
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.2); }
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(1.2);
}
}
/* Messages Area */
@ -62,8 +69,14 @@
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.user {
@ -113,7 +126,7 @@
background: rgba(0, 0, 0, 0.1);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', monospace;
font-family: "Monaco", "Menlo", monospace;
font-size: 13px;
}
@ -165,25 +178,44 @@ footer {
/* Input Container */
.input-container {
display: flex;
gap: 8px;
gap: 12px;
align-items: center;
padding: 8px 12px;
background: var(--surface, var(--secondary-bg, #f9fafb));
border: 1px solid var(--border, var(--border-color, #e5e7eb));
border-radius: 28px;
}
[data-theme="dark"] .input-container,
[data-theme="sentient"] .input-container {
background: var(--surface, #1a1a24);
border-color: var(--border, #2a2a3a);
}
.input-container:focus-within {
border-color: var(--accent, var(--accent-color, #3b82f6));
box-shadow: 0 0 0 3px var(--accent-glow, rgba(59, 130, 246, 0.1));
}
#messageInput {
flex: 1;
padding: 12px 16px;
border-radius: 24px;
border: 1px solid var(--border-color, #e5e7eb);
background: var(--secondary-bg, #f9fafb);
padding: 8px 12px;
border-radius: 20px;
border: none;
background: transparent;
color: var(--text-primary, #1f2937);
font-size: 14px;
font-size: 15px;
outline: none;
transition: all 0.2s;
}
[data-theme="dark"] #messageInput,
[data-theme="sentient"] #messageInput {
color: var(--text-primary, #ffffff);
}
#messageInput:focus {
border-color: var(--accent-color, #3b82f6);
box-shadow: 0 0 0 3px var(--accent-light, rgba(59, 130, 246, 0.1));
outline: none;
}
#messageInput::placeholder {
@ -192,24 +224,41 @@ footer {
#voiceBtn,
#sendBtn {
width: 40px;
height: 40px;
width: 44px;
height: 44px;
min-width: 44px;
min-height: 44px;
border-radius: 50%;
border: none;
background: var(--accent-color, #3b82f6);
color: white;
background: var(--accent, var(--accent-color, #3b82f6));
color: var(--accent-foreground, white);
cursor: pointer;
display: flex;
display: flex !important;
align-items: center;
justify-content: center;
font-size: 16px;
font-size: 18px;
transition: all 0.2s;
flex-shrink: 0;
}
#sendBtn {
background: var(--accent, var(--accent-color, #3b82f6));
}
#voiceBtn {
background: var(--surface, var(--secondary-bg, #f9fafb));
color: var(--text-primary, #374151);
border: 1px solid var(--border, var(--border-color, #e5e7eb));
}
#voiceBtn:hover,
#sendBtn:hover {
transform: scale(1.05);
background: var(--accent-hover, #2563eb);
opacity: 0.9;
}
#sendBtn:hover {
background: var(--accent-hover, var(--accent, #2563eb));
}
#voiceBtn:active,
@ -217,6 +266,20 @@ footer {
transform: scale(0.95);
}
/* Dark/Sentient theme overrides */
[data-theme="dark"] #sendBtn,
[data-theme="sentient"] #sendBtn {
background: var(--accent, #d4f505);
color: #000000;
}
[data-theme="dark"] #voiceBtn,
[data-theme="sentient"] #voiceBtn {
background: var(--surface, #1a1a24);
color: var(--text-primary, #ffffff);
border-color: var(--border, #2a2a3a);
}
/* Scroll to Bottom Button */
.scroll-to-bottom {
position: fixed;
@ -251,13 +314,12 @@ footer {
.chat-layout {
padding: 0 12px;
}
.message-content {
max-width: 90%;
}
}
/* Projector Overlay */
.projector-overlay {
position: fixed;
@ -278,8 +340,12 @@ footer {
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* Container */
@ -394,7 +460,9 @@ footer {
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
/* Video Player */
@ -445,7 +513,7 @@ footer {
.projector-code pre {
margin: 0;
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
font-family: "Fira Code", "Monaco", "Consolas", monospace;
font-size: 14px;
line-height: 1.5;
color: #d4d4d4;

View file

@ -1,54 +1,73 @@
<link rel="stylesheet" href="/chat/chat.css" />
<link rel="stylesheet" href="chat/chat.css" />
<script>
// WebSocket URL - use relative path to go through botui proxy
const WS_BASE_URL = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const WS_BASE_URL =
window.location.protocol === "https:" ? "wss://" : "ws://";
const WS_URL = `${WS_BASE_URL}${window.location.host}`;
// Message Type Constants
const MessageType = { EXTERNAL: 0, USER: 1, BOT_RESPONSE: 2, CONTINUE: 3, SUGGESTION: 4, CONTEXT_CHANGE: 5 };
const MessageType = {
EXTERNAL: 0,
USER: 1,
BOT_RESPONSE: 2,
CONTINUE: 3,
SUGGESTION: 4,
CONTEXT_CHANGE: 5,
};
// State
let ws = null, currentSessionId = null, currentUserId = null, currentBotId = "default";
let isStreaming = false, streamingMessageId = null, currentStreamingContent = "";
let ws = null,
currentSessionId = null,
currentUserId = null,
currentBotId = "default";
let isStreaming = false,
streamingMessageId = null,
currentStreamingContent = "";
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
// Initialize auth and WebSocket
async function initChat() {
try {
updateConnectionStatus('connecting');
const botName = 'default';
updateConnectionStatus("connecting");
const botName = "default";
// Use the botui proxy for auth (handles SSL cert issues)
const response = await fetch(`/api/auth?bot_name=${encodeURIComponent(botName)}`);
const response = await fetch(
`/api/auth?bot_name=${encodeURIComponent(botName)}`,
);
const auth = await response.json();
currentUserId = auth.user_id;
currentSessionId = auth.session_id;
currentBotId = auth.bot_id || "default";
console.log("Auth:", { currentUserId, currentSessionId, currentBotId });
console.log("Auth:", {
currentUserId,
currentSessionId,
currentBotId,
});
connectWebSocket();
} catch (e) {
console.error("Auth failed:", e);
updateConnectionStatus('disconnected');
updateConnectionStatus("disconnected");
setTimeout(initChat, 3000);
}
}
function connectWebSocket() {
if (ws) ws.close();
// Use the botui proxy for WebSocket (handles SSL cert issues)
const url = `${WS_URL}/ws?session_id=${currentSessionId}&user_id=${currentUserId}`;
ws = new WebSocket(url);
ws.onopen = () => {
console.log("WebSocket connected");
updateConnectionStatus('connected');
updateConnectionStatus("connected");
reconnectAttempts = 0;
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'connected') return;
if (data.type === "connected") return;
if (data.message_type === MessageType.BOT_RESPONSE) {
processMessage(data);
}
@ -56,46 +75,46 @@
console.error("WS message error:", e);
}
};
ws.onclose = () => {
updateConnectionStatus('disconnected');
updateConnectionStatus("disconnected");
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
setTimeout(connectWebSocket, 1000 * reconnectAttempts);
}
};
ws.onerror = (e) => console.error("WebSocket error:", e);
}
function processMessage(data) {
if (data.is_complete) {
if (isStreaming) {
finalizeStreaming();
} else {
addMessage('bot', data.content);
addMessage("bot", data.content);
}
isStreaming = false;
} else {
if (!isStreaming) {
isStreaming = true;
streamingMessageId = 'streaming-' + Date.now();
currentStreamingContent = data.content || '';
addMessage('bot', currentStreamingContent, streamingMessageId);
streamingMessageId = "streaming-" + Date.now();
currentStreamingContent = data.content || "";
addMessage("bot", currentStreamingContent, streamingMessageId);
} else {
currentStreamingContent += data.content || '';
currentStreamingContent += data.content || "";
updateStreaming(currentStreamingContent);
}
}
}
function addMessage(sender, content, msgId = null) {
const messages = document.getElementById('messages');
const div = document.createElement('div');
const messages = document.getElementById("messages");
const div = document.createElement("div");
div.className = `message ${sender}`;
if (msgId) div.id = msgId;
if (sender === 'user') {
if (sender === "user") {
div.innerHTML = `<div class="message-content user-message">${escapeHtml(content)}</div>`;
} else {
div.innerHTML = `<div class="message-content bot-message">${marked.parse(content)}</div>`;
@ -103,82 +122,91 @@
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
function updateStreaming(content) {
const el = document.getElementById(streamingMessageId);
if (el) el.querySelector('.message-content').innerHTML = marked.parse(content);
if (el)
el.querySelector(".message-content").innerHTML =
marked.parse(content);
}
function finalizeStreaming() {
const el = document.getElementById(streamingMessageId);
if (el) {
el.querySelector('.message-content').innerHTML = marked.parse(currentStreamingContent);
el.removeAttribute('id');
el.querySelector(".message-content").innerHTML = marked.parse(
currentStreamingContent,
);
el.removeAttribute("id");
}
streamingMessageId = null;
currentStreamingContent = '';
currentStreamingContent = "";
}
function sendMessage() {
const input = document.getElementById('messageInput');
const input = document.getElementById("messageInput");
const content = input.value.trim();
if (!content || !ws || ws.readyState !== WebSocket.OPEN) return;
addMessage('user', content);
ws.send(JSON.stringify({
bot_id: currentBotId,
user_id: currentUserId,
session_id: currentSessionId,
channel: 'web',
content: content,
message_type: MessageType.USER,
timestamp: new Date().toISOString()
}));
input.value = '';
addMessage("user", content);
ws.send(
JSON.stringify({
bot_id: currentBotId,
user_id: currentUserId,
session_id: currentSessionId,
channel: "web",
content: content,
message_type: MessageType.USER,
timestamp: new Date().toISOString(),
}),
);
input.value = "";
input.focus();
}
function updateConnectionStatus(status) {
const el = document.getElementById('connectionStatus');
const el = document.getElementById("connectionStatus");
if (el) el.className = `connection-status ${status}`;
}
function escapeHtml(text) {
const div = document.createElement('div');
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// Initialize chat - runs immediately when script is executed
// (works both on full page load and HTMX partial load)
function setupChat() {
const input = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn');
const input = document.getElementById("messageInput");
const sendBtn = document.getElementById("sendBtn");
if (sendBtn) sendBtn.onclick = sendMessage;
if (input) {
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
input.addEventListener("keypress", (e) => {
if (e.key === "Enter") sendMessage();
});
}
initChat();
}
// Initialize after a micro-delay to ensure DOM is ready
// This works for both full page loads and HTMX partial loads
setTimeout(() => {
if (document.getElementById('messageInput') && !window.chatInitialized) {
if (
document.getElementById("messageInput") &&
!window.chatInitialized
) {
window.chatInitialized = true;
setupChat();
}
}, 0);
// Fallback for full page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
if (!window.chatInitialized) {
window.chatInitialized = true;
setupChat();

View file

@ -178,6 +178,86 @@
}
}
/* ============================================ */
/* SENTIENT THEME VARIABLES */
/* Dark background with neon lime/green accents */
/* ============================================ */
:root {
/* Sentient Core Colors - used by tasks, attendant, and other apps */
--sentient-accent: #d4f505;
--sentient-accent-hover: #bfdd04;
--sentient-accent-light: rgba(212, 245, 5, 0.15);
--sentient-accent-glow: rgba(212, 245, 5, 0.3);
/* Sentient Background Hierarchy */
--sentient-bg-primary: #0a0a0a;
--sentient-bg-secondary: #111111;
--sentient-bg-tertiary: #161616;
--sentient-bg-elevated: #1a1a1a;
--sentient-bg-hover: #1e1e1e;
--sentient-bg-active: #252525;
/* Sentient Borders */
--sentient-border: #2a2a2a;
--sentient-border-light: #222222;
--sentient-border-accent: rgba(212, 245, 5, 0.3);
/* Sentient Text Colors */
--sentient-text-primary: #ffffff;
--sentient-text-secondary: #888888;
--sentient-text-tertiary: #666666;
--sentient-text-muted: #444444;
--sentient-text-accent: #d4f505;
/* Sentient Status Colors */
--sentient-success: #22c55e;
--sentient-success-bg: rgba(34, 197, 94, 0.15);
--sentient-warning: #f59e0b;
--sentient-warning-bg: rgba(245, 158, 11, 0.15);
--sentient-error: #ef4444;
--sentient-error-bg: rgba(239, 68, 68, 0.15);
--sentient-info: #3b82f6;
--sentient-info-bg: rgba(59, 130, 246, 0.15);
/* Sentient Shadows */
--sentient-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
--sentient-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5);
--sentient-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.6);
--sentient-shadow-accent: 0 0 20px rgba(212, 245, 5, 0.2);
--sentient-shadow-glow: 0 0 30px rgba(212, 245, 5, 0.15);
/* Sentient Typography */
--sentient-font-family:
"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
/* Sentient Border Radius */
--sentient-radius-sm: 6px;
--sentient-radius-md: 8px;
--sentient-radius-lg: 12px;
--sentient-radius-xl: 16px;
--sentient-radius-full: 9999px;
}
/* Apply sentient variables when theme is active */
[data-theme="sentient"] {
--primary-bg: var(--sentient-bg-primary);
--primary-fg: var(--sentient-text-primary);
--secondary-bg: var(--sentient-bg-secondary);
--secondary-fg: var(--sentient-text-secondary);
--accent-color: var(--sentient-accent);
--accent-hover: var(--sentient-accent-hover);
--accent-light: var(--sentient-accent-light);
--border-color: var(--sentient-border);
--text-primary: var(--sentient-text-primary);
--text-secondary: var(--sentient-text-secondary);
--text-tertiary: var(--sentient-text-tertiary);
--success-color: var(--sentient-success);
--warning-color: var(--sentient-warning);
--error-color: var(--sentient-error);
--info-color: var(--sentient-info);
}
/* ============================================ */
/* GLOBAL RESETS */
/* ============================================ */
@ -216,10 +296,11 @@ body {
/* LAYOUT STRUCTURE */
/* ============================================ */
#main-content {
height: 100vh;
width: 100vw;
height: calc(100vh - 64px);
width: 100%;
overflow: hidden;
position: relative;
margin-top: 64px;
}
.section {
@ -261,12 +342,136 @@ body {
gap: var(--space-md);
}
.header-center {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
max-width: 400px;
margin: 0 var(--space-lg);
}
.header-right {
display: flex;
align-items: center;
gap: var(--space-sm);
}
/* ============================================ */
/* HEADER APP TABS */
/* ============================================ */
.header-app-tabs {
display: flex;
align-items: center;
gap: var(--space-xs);
margin-left: var(--space-md);
}
.app-tab {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border-radius: var(--radius-md);
color: var(--text-secondary);
text-decoration: none;
font-size: 13px;
font-weight: 500;
transition: all var(--transition-fast);
border: 1px solid transparent;
}
.app-tab:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.app-tab.active {
background: var(--accent-light);
color: var(--accent-color);
border-color: var(--accent-color);
}
.app-tab svg {
opacity: 0.7;
}
.app-tab.active svg {
opacity: 1;
}
/* ============================================ */
/* HEADER SEARCH */
/* ============================================ */
.header-search {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: 8px 14px;
background: var(--glass-bg);
border: 1px solid var(--border-color);
border-radius: var(--radius-full);
width: 100%;
transition: all var(--transition-fast);
}
.header-search:focus-within {
border-color: var(--accent-color);
box-shadow: 0 0 0 3px var(--accent-light);
}
.header-search svg {
color: var(--text-secondary);
flex-shrink: 0;
}
.header-search .search-input {
flex: 1;
border: none;
background: transparent;
color: var(--text-primary);
font-size: 14px;
outline: none;
}
.header-search .search-input::placeholder {
color: var(--text-muted);
}
.search-shortcut {
padding: 2px 6px;
background: var(--bg-hover);
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 11px;
color: var(--text-secondary);
font-family: inherit;
}
/* ============================================ */
/* NOTIFICATION BADGE */
/* ============================================ */
.icon-button {
position: relative;
}
.notification-badge {
position: absolute;
top: -2px;
right: -2px;
min-width: 16px;
height: 16px;
padding: 0 4px;
background: var(--error-color);
color: white;
font-size: 10px;
font-weight: 600;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.logo-wrapper {
display: flex;
align-items: center;

File diff suppressed because it is too large Load diff

View file

@ -83,6 +83,229 @@
--border: #5c3a1e;
}
[data-theme="sentient"] {
--primary: #d4f505;
--primary-hover: #bfdd04;
--primary-light: rgba(212, 245, 5, 0.15);
--bg: #0a0a0a;
--surface: #161616;
--surface-hover: #1e1e1e;
--border: #2a2a2a;
--text: #ffffff;
--text-secondary: #888888;
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
--info: #3b82f6;
}
/* Retro Themes */
[data-theme="cyberpunk"] {
--primary: #ff00ff;
--primary-hover: #cc00cc;
--primary-light: rgba(255, 0, 255, 0.15);
--bg: #0d0221;
--surface: #1a0a3e;
--surface-hover: #2a1a5e;
--border: #ff00ff33;
--text: #00ffff;
--text-secondary: #ff6ec7;
}
[data-theme="retrowave"] {
--primary: #ff6b9d;
--primary-hover: #ff4081;
--primary-light: rgba(255, 107, 157, 0.15);
--bg: #1a0a2e;
--surface: #2d1b4e;
--surface-hover: #3d2b6e;
--border: #ff6b9d33;
--text: #ffeaa7;
--text-secondary: #a29bfe;
}
[data-theme="vapordream"] {
--primary: #a29bfe;
--primary-hover: #6c5ce7;
--primary-light: rgba(162, 155, 254, 0.15);
--bg: #2d3436;
--surface: #636e72;
--surface-hover: #74b9ff;
--border: #a29bfe33;
--text: #ffeaa7;
--text-secondary: #fd79a8;
}
[data-theme="y2kglow"] {
--primary: #00ff00;
--primary-hover: #00cc00;
--primary-light: rgba(0, 255, 0, 0.15);
--bg: #000033;
--surface: #000066;
--surface-hover: #000099;
--border: #00ff0033;
--text: #ffffff;
--text-secondary: #00ffff;
}
[data-theme="arcadeflash"] {
--primary: #ffff00;
--primary-hover: #cccc00;
--primary-light: rgba(255, 255, 0, 0.15);
--bg: #000000;
--surface: #1a1a1a;
--surface-hover: #333333;
--border: #ffff0033;
--text: #ff0000;
--text-secondary: #00ff00;
}
[data-theme="discofever"] {
--primary: #ff1493;
--primary-hover: #ff69b4;
--primary-light: rgba(255, 20, 147, 0.15);
--bg: #1a0a1a;
--surface: #2d1a2d;
--surface-hover: #4a2a4a;
--border: #ff149333;
--text: #ffd700;
--text-secondary: #00ced1;
}
[data-theme="grungeera"] {
--primary: #8b4513;
--primary-hover: #a0522d;
--primary-light: rgba(139, 69, 19, 0.15);
--bg: #1a1a0a;
--surface: #2d2d1a;
--surface-hover: #3d3d2a;
--border: #8b451333;
--text: #daa520;
--text-secondary: #808000;
}
/* Classic Themes */
[data-theme="jazzage"] {
--primary: #d4af37;
--primary-hover: #b8960c;
--primary-light: rgba(212, 175, 55, 0.15);
--bg: #1a1a2e;
--surface: #16213e;
--surface-hover: #0f3460;
--border: #d4af3733;
--text: #eee8aa;
--text-secondary: #cd853f;
}
[data-theme="mellowgold"] {
--primary: #daa520;
--primary-hover: #b8860b;
--primary-light: rgba(218, 165, 32, 0.15);
--bg: #2c2416;
--surface: #3d3222;
--surface-hover: #4e422e;
--border: #daa52033;
--text: #faebd7;
--text-secondary: #d2b48c;
}
[data-theme="midcenturymod"] {
--primary: #e07b39;
--primary-hover: #c56a2d;
--primary-light: rgba(224, 123, 57, 0.15);
--bg: #2d4a3e;
--surface: #3d5a4e;
--surface-hover: #4d6a5e;
--border: #e07b3933;
--text: #f5f5dc;
--text-secondary: #8fbc8f;
}
[data-theme="polaroidmemories"] {
--primary: #e6b89c;
--primary-hover: #d4a384;
--primary-light: rgba(230, 184, 156, 0.15);
--bg: #f5f5dc;
--surface: #fffaf0;
--surface-hover: #faf0e6;
--border: #d2b48c;
--text: #4a4a4a;
--text-secondary: #8b7355;
}
[data-theme="saturdaycartoons"] {
--primary: #ff6347;
--primary-hover: #ff4500;
--primary-light: rgba(255, 99, 71, 0.15);
--bg: #4169e1;
--surface: #5179f1;
--surface-hover: #6189ff;
--border: #ff634733;
--text: #ffff00;
--text-secondary: #00ff00;
}
[data-theme="seasidepostcard"] {
--primary: #20b2aa;
--primary-hover: #008b8b;
--primary-light: rgba(32, 178, 170, 0.15);
--bg: #f0f8ff;
--surface: #e0ffff;
--surface-hover: #b0e0e6;
--border: #87ceeb;
--text: #2f4f4f;
--text-secondary: #5f9ea0;
}
[data-theme="typewriter"] {
--primary: #2f2f2f;
--primary-hover: #1a1a1a;
--primary-light: rgba(47, 47, 47, 0.15);
--bg: #f5f5dc;
--surface: #fffff0;
--surface-hover: #fafad2;
--border: #8b8b7a;
--text: #2f2f2f;
--text-secondary: #696969;
}
/* Tech Themes */
[data-theme="3dbevel"] {
--primary: #0000ff;
--primary-hover: #0000cc;
--primary-light: rgba(0, 0, 255, 0.15);
--bg: #c0c0c0;
--surface: #d4d4d4;
--surface-hover: #e8e8e8;
--border: #808080;
--text: #000000;
--text-secondary: #404040;
}
[data-theme="xeroxui"] {
--primary: #4a86cf;
--primary-hover: #3a76bf;
--primary-light: rgba(74, 134, 207, 0.15);
--bg: #e8e8e8;
--surface: #ffffff;
--surface-hover: #f0f0f0;
--border: #a0a0a0;
--text: #000000;
--text-secondary: #606060;
}
[data-theme="xtreegold"] {
--primary: #ffff00;
--primary-hover: #cccc00;
--primary-light: rgba(255, 255, 0, 0.15);
--bg: #000080;
--surface: #0000a0;
--surface-hover: #0000c0;
--border: #ffff0033;
--text: #ffff00;
--text-secondary: #00ffff;
}
/* ============================================ */
/* BASE RESETS */
/* ============================================ */
@ -94,7 +317,9 @@
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
@ -167,7 +392,9 @@ body {
color: var(--text-secondary);
border-radius: 8px;
cursor: pointer;
transition: background 0.2s, color 0.2s;
transition:
background 0.2s,
color 0.2s;
display: flex;
align-items: center;
justify-content: center;
@ -314,23 +541,67 @@ body {
}
/* ============================================ */
/* THEME SELECTOR */
/* THEME SELECTOR DROPDOWN */
/* ============================================ */
.theme-dropdown {
width: 100%;
padding: 10px 14px;
background: var(--surface, #161616);
border: 1px solid var(--border, #2a2a2a);
border-radius: 8px;
color: var(--text, #ffffff);
font-size: 14px;
font-family: inherit;
cursor: pointer;
transition: all 0.2s ease;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888888' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 36px;
}
.theme-dropdown:hover {
border-color: var(--text-secondary, #888888);
}
.theme-dropdown:focus {
outline: none;
border-color: var(--primary, #d4f505);
box-shadow: 0 0 0 3px rgba(212, 245, 5, 0.15);
}
.theme-dropdown option {
background: var(--surface, #161616);
color: var(--text, #ffffff);
padding: 8px;
}
.theme-dropdown optgroup {
background: var(--bg, #0a0a0a);
color: var(--text-secondary, #888888);
font-weight: 600;
font-style: normal;
}
/* Legacy theme-grid support (if needed) */
.theme-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
}
.theme-option {
position: relative;
aspect-ratio: 1;
border-radius: 10px;
border-radius: 8px;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s;
overflow: hidden;
padding: 0;
background: none;
}
.theme-option:hover {
@ -357,8 +628,8 @@ body {
}
.theme-option-dot {
width: 6px;
height: 6px;
width: 5px;
height: 5px;
border-radius: 50%;
margin-right: 2px;
}
@ -379,41 +650,392 @@ body {
bottom: 2px;
left: 0;
right: 0;
font-size: 0.6rem;
font-size: 0.5rem;
text-align: center;
color: inherit;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 2px;
}
/* Theme Preview Colors */
.theme-dark .theme-option-inner { background: #0f172a; color: #f8fafc; }
.theme-dark .theme-option-header { background: #1e293b; }
.theme-dark .theme-option-dot { background: #3b82f6; }
.theme-dark .theme-option-line { background: #334155; }
.theme-dark .theme-option-inner {
background: #0f172a;
color: #f8fafc;
}
.theme-dark .theme-option-header {
background: #1e293b;
}
.theme-dark .theme-option-dot {
background: #3b82f6;
}
.theme-dark .theme-option-line {
background: #334155;
}
.theme-light .theme-option-inner { background: #f8fafc; color: #1e293b; }
.theme-light .theme-option-header { background: #ffffff; }
.theme-light .theme-option-dot { background: #3b82f6; }
.theme-light .theme-option-line { background: #e2e8f0; }
.theme-light .theme-option-inner {
background: #f8fafc;
color: #1e293b;
}
.theme-light .theme-option-header {
background: #ffffff;
}
.theme-light .theme-option-dot {
background: #3b82f6;
}
.theme-light .theme-option-line {
background: #e2e8f0;
}
.theme-blue .theme-option-inner { background: #0c1929; color: #f8fafc; }
.theme-blue .theme-option-header { background: #1a2f47; }
.theme-blue .theme-option-dot { background: #0ea5e9; }
.theme-blue .theme-option-line { background: #2d4a6f; }
.theme-blue .theme-option-inner {
background: #0c1929;
color: #f8fafc;
}
.theme-blue .theme-option-header {
background: #1a2f47;
}
.theme-blue .theme-option-dot {
background: #0ea5e9;
}
.theme-blue .theme-option-line {
background: #2d4a6f;
}
.theme-purple .theme-option-inner { background: #1a0a2e; color: #f8fafc; }
.theme-purple .theme-option-header { background: #2d1b4e; }
.theme-purple .theme-option-dot { background: #a855f7; }
.theme-purple .theme-option-line { background: #4c2f7e; }
.theme-purple .theme-option-inner {
background: #1a0a2e;
color: #f8fafc;
}
.theme-purple .theme-option-header {
background: #2d1b4e;
}
.theme-purple .theme-option-dot {
background: #a855f7;
}
.theme-purple .theme-option-line {
background: #4c2f7e;
}
.theme-green .theme-option-inner { background: #0a1f15; color: #f8fafc; }
.theme-green .theme-option-header { background: #14332a; }
.theme-green .theme-option-dot { background: #22c55e; }
.theme-green .theme-option-line { background: #28604f; }
.theme-green .theme-option-inner {
background: #0a1f15;
color: #f8fafc;
}
.theme-green .theme-option-header {
background: #14332a;
}
.theme-green .theme-option-dot {
background: #22c55e;
}
.theme-green .theme-option-line {
background: #28604f;
}
.theme-orange .theme-option-inner { background: #1a1008; color: #f8fafc; }
.theme-orange .theme-option-header { background: #2d1c0f; }
.theme-orange .theme-option-dot { background: #f97316; }
.theme-orange .theme-option-line { background: #5c3a1e; }
.theme-orange .theme-option-inner {
background: #1a1008;
color: #f8fafc;
}
.theme-orange .theme-option-header {
background: #2d1c0f;
}
.theme-orange .theme-option-dot {
background: #f97316;
}
.theme-orange .theme-option-line {
background: #5c3a1e;
}
.theme-sentient .theme-option-inner {
background: #0a0a0a;
color: #ffffff;
}
.theme-sentient .theme-option-header {
background: #161616;
}
.theme-sentient .theme-option-dot {
background: #d4f505;
}
.theme-sentient .theme-option-line {
background: #2a2a2a;
}
/* Retro Theme Previews */
.theme-cyberpunk .theme-option-inner {
background: #0d0221;
color: #00ffff;
}
.theme-cyberpunk .theme-option-header {
background: #1a0a3e;
}
.theme-cyberpunk .theme-option-dot {
background: #ff00ff;
}
.theme-cyberpunk .theme-option-line {
background: #ff00ff33;
}
.theme-retrowave .theme-option-inner {
background: #1a0a2e;
color: #ffeaa7;
}
.theme-retrowave .theme-option-header {
background: #2d1b4e;
}
.theme-retrowave .theme-option-dot {
background: #ff6b9d;
}
.theme-retrowave .theme-option-line {
background: #ff6b9d33;
}
.theme-vapordream .theme-option-inner {
background: #2d3436;
color: #ffeaa7;
}
.theme-vapordream .theme-option-header {
background: #636e72;
}
.theme-vapordream .theme-option-dot {
background: #a29bfe;
}
.theme-vapordream .theme-option-line {
background: #a29bfe33;
}
.theme-y2kglow .theme-option-inner {
background: #000033;
color: #ffffff;
}
.theme-y2kglow .theme-option-header {
background: #000066;
}
.theme-y2kglow .theme-option-dot {
background: #00ff00;
}
.theme-y2kglow .theme-option-line {
background: #00ff0033;
}
.theme-arcadeflash .theme-option-inner {
background: #000000;
color: #ff0000;
}
.theme-arcadeflash .theme-option-header {
background: #1a1a1a;
}
.theme-arcadeflash .theme-option-dot {
background: #ffff00;
}
.theme-arcadeflash .theme-option-line {
background: #ffff0033;
}
.theme-discofever .theme-option-inner {
background: #1a0a1a;
color: #ffd700;
}
.theme-discofever .theme-option-header {
background: #2d1a2d;
}
.theme-discofever .theme-option-dot {
background: #ff1493;
}
.theme-discofever .theme-option-line {
background: #ff149333;
}
.theme-grungeera .theme-option-inner {
background: #1a1a0a;
color: #daa520;
}
.theme-grungeera .theme-option-header {
background: #2d2d1a;
}
.theme-grungeera .theme-option-dot {
background: #8b4513;
}
.theme-grungeera .theme-option-line {
background: #8b451333;
}
/* Classic Theme Previews */
.theme-jazzage .theme-option-inner {
background: #1a1a2e;
color: #eee8aa;
}
.theme-jazzage .theme-option-header {
background: #16213e;
}
.theme-jazzage .theme-option-dot {
background: #d4af37;
}
.theme-jazzage .theme-option-line {
background: #d4af3733;
}
.theme-mellowgold .theme-option-inner {
background: #2c2416;
color: #faebd7;
}
.theme-mellowgold .theme-option-header {
background: #3d3222;
}
.theme-mellowgold .theme-option-dot {
background: #daa520;
}
.theme-mellowgold .theme-option-line {
background: #daa52033;
}
.theme-midcenturymod .theme-option-inner {
background: #2d4a3e;
color: #f5f5dc;
}
.theme-midcenturymod .theme-option-header {
background: #3d5a4e;
}
.theme-midcenturymod .theme-option-dot {
background: #e07b39;
}
.theme-midcenturymod .theme-option-line {
background: #e07b3933;
}
.theme-polaroidmemories .theme-option-inner {
background: #f5f5dc;
color: #4a4a4a;
}
.theme-polaroidmemories .theme-option-header {
background: #fffaf0;
}
.theme-polaroidmemories .theme-option-dot {
background: #e6b89c;
}
.theme-polaroidmemories .theme-option-line {
background: #d2b48c;
}
.theme-saturdaycartoons .theme-option-inner {
background: #4169e1;
color: #ffff00;
}
.theme-saturdaycartoons .theme-option-header {
background: #5179f1;
}
.theme-saturdaycartoons .theme-option-dot {
background: #ff6347;
}
.theme-saturdaycartoons .theme-option-line {
background: #ff634733;
}
.theme-seasidepostcard .theme-option-inner {
background: #f0f8ff;
color: #2f4f4f;
}
.theme-seasidepostcard .theme-option-header {
background: #e0ffff;
}
.theme-seasidepostcard .theme-option-dot {
background: #20b2aa;
}
.theme-seasidepostcard .theme-option-line {
background: #87ceeb;
}
.theme-typewriter .theme-option-inner {
background: #f5f5dc;
color: #2f2f2f;
}
.theme-typewriter .theme-option-header {
background: #fffff0;
}
.theme-typewriter .theme-option-dot {
background: #2f2f2f;
}
.theme-typewriter .theme-option-line {
background: #8b8b7a;
}
/* Tech Theme Previews */
.theme-3dbevel .theme-option-inner {
background: #c0c0c0;
color: #000000;
}
.theme-3dbevel .theme-option-header {
background: #d4d4d4;
}
.theme-3dbevel .theme-option-dot {
background: #0000ff;
}
.theme-3dbevel .theme-option-line {
background: #808080;
}
.theme-xeroxui .theme-option-inner {
background: #e8e8e8;
color: #000000;
}
.theme-xeroxui .theme-option-header {
background: #ffffff;
}
.theme-xeroxui .theme-option-dot {
background: #4a86cf;
}
.theme-xeroxui .theme-option-line {
background: #a0a0a0;
}
.theme-xtreegold .theme-option-inner {
background: #000080;
color: #ffff00;
}
.theme-xtreegold .theme-option-header {
background: #0000a0;
}
.theme-xtreegold .theme-option-dot {
background: #ffff00;
}
.theme-xtreegold .theme-option-line {
background: #ffff0033;
}
/* ============================================ */
/* SETTINGS PANEL */
/* ============================================ */
.settings-panel {
position: absolute;
top: calc(100% + 8px);
right: 0;
width: 320px;
max-height: 70vh;
overflow-y: auto;
background: var(--surface, #161616);
border: 1px solid var(--border, #2a2a2a);
border-radius: 12px;
padding: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.2s ease;
z-index: 1100;
}
.settings-panel.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.settings-panel-title {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary, #888888);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
/* ============================================ */
/* SETTINGS SHORTCUTS */
@ -647,7 +1269,12 @@ body {
/* ============================================ */
.skeleton {
background: linear-gradient(90deg, var(--border) 25%, var(--surface-hover) 50%, var(--border) 75%);
background: linear-gradient(
90deg,
var(--border) 25%,
var(--surface-hover) 50%,
var(--border) 75%
);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
border-radius: 4px;
@ -668,8 +1295,12 @@ body {
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* ============================================ */
@ -687,7 +1318,9 @@ body {
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
/* ============================================ */
@ -717,10 +1350,18 @@ body {
animation: slideIn 0.3s ease;
}
.notification.success { border-left: 3px solid var(--success); }
.notification.error { border-left: 3px solid var(--error); }
.notification.warning { border-left: 3px solid var(--warning); }
.notification.info { border-left: 3px solid var(--info); }
.notification.success {
border-left: 3px solid var(--success);
}
.notification.error {
border-left: 3px solid var(--error);
}
.notification.warning {
border-left: 3px solid var(--warning);
}
.notification.info {
border-left: 3px solid var(--info);
}
@keyframes slideIn {
from {
@ -789,13 +1430,13 @@ a:focus-visible {
.logo span {
display: none;
}
.apps-dropdown,
.settings-panel {
width: calc(100vw - 2rem);
right: -0.5rem;
}
.theme-grid {
grid-template-columns: repeat(3, 1fr);
}

View file

@ -413,18 +413,44 @@ select:focus {
/* MODALS */
/* ============================================ */
.modal {
.modal-overlay {
position: fixed;
inset: 0;
background: hsla(var(--foreground) / 0.5);
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
z-index: 1100;
padding: 16px;
animation: modalFadeIn 0.2s ease;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
}
.modal-overlay.show {
opacity: 1;
visibility: visible;
}
.modal {
width: 100%;
max-width: 520px;
max-height: calc(100vh - 32px);
background: var(--surface, #161616);
border: 1px solid var(--border, #2a2a2a);
border-radius: 16px;
overflow: hidden;
display: flex;
flex-direction: column;
transform: scale(0.95) translateY(-10px);
transition: transform 0.2s ease;
}
.modal-overlay.show .modal {
transform: scale(1) translateY(0);
}
/* Legacy support */
.modal.hidden {
display: none;
}
@ -442,23 +468,12 @@ select:focus {
width: 100%;
max-width: 480px;
max-height: calc(100vh - 32px);
background: hsl(var(--card));
background: var(--surface, #161616);
border: 1px solid var(--border, #2a2a2a);
border-radius: 12px;
overflow: hidden;
display: flex;
flex-direction: column;
animation: modalSlideIn 0.2s ease;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.95) translateY(-10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.modal-sm .modal-content {
@ -482,28 +497,97 @@ select:focus {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid hsl(var(--border));
padding: 20px 24px;
border-bottom: 1px solid var(--border, #2a2a2a);
}
.modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--text, #ffffff);
margin: 0;
}
.modal-close {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-secondary, #888888);
cursor: pointer;
transition: all 0.2s ease;
}
.modal-close:hover {
background: var(--surface-hover, #1e1e1e);
color: var(--text, #ffffff);
}
.modal-body {
flex: 1;
padding: 20px;
overflow-y: auto;
padding: 24px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 20px;
border-top: 1px solid hsl(var(--border));
padding: 16px 24px;
border-top: 1px solid var(--border, #2a2a2a);
}
/* Form styles within modals */
.form-group {
margin-bottom: 20px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--text-secondary, #888888);
margin-bottom: 8px;
}
.form-input {
width: 100%;
padding: 12px 14px;
background: var(--bg, #0a0a0a);
border: 1px solid var(--border, #2a2a2a);
border-radius: 8px;
color: var(--text, #ffffff);
font-size: 14px;
transition: all 0.2s ease;
}
.form-input:focus {
outline: none;
border-color: var(--accent, #d4f505);
box-shadow: 0 0 0 3px rgba(212, 245, 5, 0.1);
}
.form-input::placeholder {
color: var(--text-tertiary, #666666);
}
textarea.form-input {
resize: vertical;
min-height: 80px;
}
select.form-input {
cursor: pointer;
}
/* ============================================ */
@ -940,7 +1024,7 @@ select:focus {
.divider-text::before,
.divider-text::after {
content: '';
content: "";
flex: 1;
height: 1px;
background: hsl(var(--border));
@ -1019,28 +1103,70 @@ select:focus {
justify-content: space-between;
}
.gap-1 { gap: 4px; }
.gap-2 { gap: 8px; }
.gap-3 { gap: 12px; }
.gap-4 { gap: 16px; }
.gap-5 { gap: 20px; }
.gap-1 {
gap: 4px;
}
.gap-2 {
gap: 8px;
}
.gap-3 {
gap: 12px;
}
.gap-4 {
gap: 16px;
}
.gap-5 {
gap: 20px;
}
.p-1 { padding: 4px; }
.p-2 { padding: 8px; }
.p-3 { padding: 12px; }
.p-4 { padding: 16px; }
.p-5 { padding: 20px; }
.p-1 {
padding: 4px;
}
.p-2 {
padding: 8px;
}
.p-3 {
padding: 12px;
}
.p-4 {
padding: 16px;
}
.p-5 {
padding: 20px;
}
.m-1 { margin: 4px; }
.m-2 { margin: 8px; }
.m-3 { margin: 12px; }
.m-4 { margin: 16px; }
.m-5 { margin: 20px; }
.m-1 {
margin: 4px;
}
.m-2 {
margin: 8px;
}
.m-3 {
margin: 12px;
}
.m-4 {
margin: 16px;
}
.m-5 {
margin: 20px;
}
.rounded { border-radius: 6px; }
.rounded-lg { border-radius: 12px; }
.rounded-full { border-radius: 9999px; }
.rounded {
border-radius: 6px;
}
.rounded-lg {
border-radius: 12px;
}
.rounded-full {
border-radius: 9999px;
}
.shadow-sm { box-shadow: 0 1px 2px hsla(var(--foreground) / 0.05); }
.shadow { box-shadow: 0 2px 8px hsla(var(--foreground) / 0.08); }
.shadow-lg { box-shadow: 0 8px 24px hsla(var(--foreground) / 0.12); }
.shadow-sm {
box-shadow: 0 1px 2px hsla(var(--foreground) / 0.05);
}
.shadow {
box-shadow: 0 2px 8px hsla(var(--foreground) / 0.08);
}
.shadow-lg {
box-shadow: 0 8px 24px hsla(var(--foreground) / 0.12);
}

View file

@ -1,102 +1,111 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #0f172a;
color: #e2e8f0;
height: 100vh;
overflow: hidden;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
sans-serif;
background: #0f172a;
color: #e2e8f0;
height: 100vh;
overflow: hidden;
}
/* Navbar */
nav {
background: #1e293b;
border-bottom: 2px solid #334155;
padding: 0 1rem;
display: flex;
align-items: center;
height: 60px;
gap: 0.5rem;
background: #1e293b;
border-bottom: 2px solid #334155;
padding: 0 1rem;
display: flex;
align-items: center;
height: 60px;
gap: 0.5rem;
}
nav .logo {
font-size: 1.5rem;
font-weight: bold;
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-right: auto;
font-size: 1.5rem;
font-weight: bold;
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-right: auto;
}
nav a {
color: #94a3b8;
text-decoration: none;
padding: 0.75rem 1.25rem;
border-radius: 0.5rem;
transition: all 0.2s;
font-weight: 500;
color: #94a3b8;
text-decoration: none;
padding: 0.75rem 1.25rem;
border-radius: 0.5rem;
transition: all 0.2s;
font-weight: 500;
}
nav a:hover {
background: #334155;
color: #e2e8f0;
background: #334155;
color: #e2e8f0;
}
nav a.active {
background: #3b82f6;
color: white;
background: #3b82f6;
color: white;
}
/* Main Content */
#main-content {
height: calc(100vh - 60px);
overflow: hidden;
height: calc(100vh - 64px);
overflow: hidden;
position: relative;
}
.content-section {
display: none;
height: 100%;
overflow: auto;
display: none;
height: 100%;
overflow: auto;
}
.content-section.active {
display: block;
display: block;
}
/* Panel Styles */
.panel {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.5rem;
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.5rem;
}
/* Buttons */
button {
font-family: inherit;
font-family: inherit;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
opacity: 0.5;
cursor: not-allowed;
}
/* Utility */
h1, h2, h3 {
margin-bottom: 1rem;
h1,
h2,
h3 {
margin-bottom: 1rem;
}
.text-sm {
font-size: 0.875rem;
font-size: 0.875rem;
}
.text-xs {
font-size: 0.75rem;
font-size: 0.75rem;
}
.text-gray {
color: #94a3b8;
color: #94a3b8;
}
[x-cloak] {
display: none !important;
display: none !important;
}

View file

@ -0,0 +1,977 @@
/* General Bots Suite - Sentient Theme */
/* Dark background with neon lime/green accents */
/* Inspired by modern AI dashboard aesthetics */
[data-theme="sentient"] {
/* Core Colors */
--accent: #d4f505;
--accent-hover: #bfdd04;
--accent-light: rgba(212, 245, 5, 0.15);
--accent-glow: rgba(212, 245, 5, 0.3);
--accent-rgb: 212, 245, 5;
/* Background Hierarchy */
--bg: #0a0a0a;
--bg-secondary: #111111;
--surface: #161616;
--surface-hover: #1e1e1e;
--surface-active: #252525;
--surface-elevated: #1a1a1a;
/* Border Colors */
--border: #2a2a2a;
--border-light: #222222;
--border-accent: rgba(212, 245, 5, 0.3);
/* Text Colors */
--text: #ffffff;
--text-secondary: #888888;
--text-tertiary: #666666;
--text-muted: #444444;
--text-accent: #d4f505;
/* Status Colors */
--success: #22c55e;
--success-bg: rgba(34, 197, 94, 0.15);
--warning: #f59e0b;
--warning-bg: rgba(245, 158, 11, 0.15);
--error: #ef4444;
--error-bg: rgba(239, 68, 68, 0.15);
--info: #3b82f6;
--info-bg: rgba(59, 130, 246, 0.15);
/* Component Specific */
--header-bg: rgba(10, 10, 10, 0.95);
--header-border: #1e1e1e;
--sidebar-bg: #0f0f0f;
--sidebar-border: #1e1e1e;
--card-bg: #161616;
--card-border: #2a2a2a;
--card-hover-border: rgba(212, 245, 5, 0.4);
--input-bg: #1a1a1a;
--input-border: #2a2a2a;
--input-focus-border: #d4f505;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.6);
--shadow-accent: 0 0 20px rgba(212, 245, 5, 0.2);
--shadow-glow: 0 0 30px rgba(212, 245, 5, 0.15);
/* HSL Bridge Variables for compatibility */
--background: 0 0% 4%;
--foreground: 0 0% 100%;
--card: 0 0% 9%;
--card-foreground: 0 0% 100%;
--popover: 0 0% 7%;
--popover-foreground: 0 0% 100%;
--primary: 67 96% 49%;
--primary-foreground: 0 0% 0%;
--secondary: 0 0% 12%;
--secondary-foreground: 0 0% 100%;
--muted: 0 0% 15%;
--muted-foreground: 0 0% 53%;
--accent-hsl: 67 96% 49%;
--accent-foreground: 0 0% 0%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 100%;
--border-hsl: 0 0% 17%;
--input-hsl: 0 0% 17%;
--ring: 67 96% 49%;
}
/* ============================================ */
/* SENTIENT THEME - GLOBAL OVERRIDES */
/* ============================================ */
[data-theme="sentient"] body {
background: var(--bg);
color: var(--text);
}
/* Header Styling */
[data-theme="sentient"] .float-header {
background: var(--header-bg);
border-bottom: 1px solid var(--header-border);
backdrop-filter: blur(12px);
}
[data-theme="sentient"] .header-app-tabs .app-tab {
color: var(--text-secondary);
background: transparent;
border: 1px solid transparent;
border-radius: 8px;
transition: all 0.2s ease;
}
[data-theme="sentient"] .header-app-tabs .app-tab:hover {
color: var(--text);
background: var(--surface-hover);
}
[data-theme="sentient"] .header-app-tabs .app-tab.active {
color: #000000;
background: var(--accent);
border-color: var(--accent);
font-weight: 600;
}
[data-theme="sentient"] .header-app-tabs .app-tab.active svg {
stroke: #000000;
}
/* Search Bar */
[data-theme="sentient"] .header-search {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
}
[data-theme="sentient"] .header-search:focus-within {
border-color: var(--accent);
box-shadow: var(--shadow-accent);
}
[data-theme="sentient"] .search-shortcut {
background: var(--surface-hover);
color: var(--text-tertiary);
border: 1px solid var(--border);
}
/* Icon Buttons */
[data-theme="sentient"] .icon-button {
color: var(--text-secondary);
background: transparent;
border: 1px solid transparent;
border-radius: 8px;
transition: all 0.2s ease;
}
[data-theme="sentient"] .icon-button:hover {
color: var(--text);
background: var(--surface-hover);
border-color: var(--border);
}
/* Notification Badge */
[data-theme="sentient"] .notification-badge {
background: var(--accent);
color: #000000;
font-weight: 700;
}
/* User Avatar */
[data-theme="sentient"] .user-avatar {
border: 2px solid var(--border);
}
[data-theme="sentient"] .user-avatar:hover {
border-color: var(--accent);
}
/* ============================================ */
/* CARDS & PANELS */
/* ============================================ */
[data-theme="sentient"] .card,
[data-theme="sentient"] .panel,
[data-theme="sentient"] .intent-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 12px;
}
[data-theme="sentient"] .card:hover,
[data-theme="sentient"] .panel:hover,
[data-theme="sentient"] .intent-card:hover {
border-color: var(--card-hover-border);
}
/* ============================================ */
/* BUTTONS */
/* ============================================ */
[data-theme="sentient"] .btn-primary,
[data-theme="sentient"] .button-primary {
background: var(--accent);
color: #000000;
border: none;
font-weight: 600;
border-radius: 8px;
transition: all 0.2s ease;
}
[data-theme="sentient"] .btn-primary:hover,
[data-theme="sentient"] .button-primary:hover {
background: var(--accent-hover);
box-shadow: var(--shadow-accent);
}
[data-theme="sentient"] .btn-secondary,
[data-theme="sentient"] .button-secondary {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
border-radius: 8px;
}
[data-theme="sentient"] .btn-secondary:hover,
[data-theme="sentient"] .button-secondary:hover {
background: var(--surface-hover);
border-color: var(--text-secondary);
}
[data-theme="sentient"] .btn-ghost {
background: transparent;
color: var(--text-secondary);
border: 1px solid transparent;
}
[data-theme="sentient"] .btn-ghost:hover {
background: var(--surface-hover);
color: var(--text);
}
/* ============================================ */
/* STATUS FILTERS / TABS */
/* ============================================ */
[data-theme="sentient"] .status-filter,
[data-theme="sentient"] .filter-tab {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: transparent;
border: 1px solid var(--border);
border-radius: 20px;
color: var(--text-secondary);
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
[data-theme="sentient"] .status-filter:hover,
[data-theme="sentient"] .filter-tab:hover {
background: var(--surface-hover);
color: var(--text);
}
[data-theme="sentient"] .status-filter.active,
[data-theme="sentient"] .filter-tab.active {
background: var(--accent);
color: #000000;
border-color: var(--accent);
font-weight: 600;
}
[data-theme="sentient"] .status-filter .count,
[data-theme="sentient"] .filter-tab .count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
background: var(--surface-hover);
border-radius: 10px;
font-size: 12px;
font-weight: 600;
}
[data-theme="sentient"] .status-filter.active .count,
[data-theme="sentient"] .filter-tab.active .count {
background: rgba(0, 0, 0, 0.2);
color: #000000;
}
/* ============================================ */
/* PROGRESS BARS */
/* ============================================ */
[data-theme="sentient"] .progress-bar {
height: 8px;
background: var(--surface-hover);
border-radius: 4px;
overflow: hidden;
}
[data-theme="sentient"] .progress-bar-fill {
height: 100%;
background: var(--accent);
border-radius: 4px;
transition: width 0.3s ease;
}
[data-theme="sentient"] .progress-percentage {
color: var(--accent);
font-weight: 600;
font-size: 14px;
}
/* ============================================ */
/* BADGES & TAGS */
/* ============================================ */
[data-theme="sentient"] .badge {
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
[data-theme="sentient"] .badge-primary,
[data-theme="sentient"] .badge-active {
background: var(--accent-light);
color: var(--accent);
}
[data-theme="sentient"] .badge-success,
[data-theme="sentient"] .badge-complete {
background: var(--success-bg);
color: var(--success);
}
[data-theme="sentient"] .badge-warning,
[data-theme="sentient"] .badge-pending {
background: var(--warning-bg);
color: var(--warning);
}
[data-theme="sentient"] .badge-danger,
[data-theme="sentient"] .badge-blocked {
background: var(--error-bg);
color: var(--error);
}
[data-theme="sentient"] .badge-secondary,
[data-theme="sentient"] .badge-paused {
background: var(--surface-hover);
color: var(--text-secondary);
}
/* Tag pills */
[data-theme="sentient"] .tag {
display: inline-flex;
align-items: center;
padding: 4px 12px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
color: var(--text-secondary);
font-size: 12px;
}
/* ============================================ */
/* INTENT/TASK CARDS */
/* ============================================ */
[data-theme="sentient"] .intent-card {
padding: 20px;
margin-bottom: 16px;
}
[data-theme="sentient"] .intent-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
[data-theme="sentient"] .intent-title {
font-size: 16px;
font-weight: 600;
color: var(--text);
}
[data-theme="sentient"] .intent-status {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-secondary);
font-size: 14px;
}
[data-theme="sentient"] .intent-status.active {
color: var(--accent);
}
[data-theme="sentient"] .intent-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 16px;
}
[data-theme="sentient"] .intent-meta-item {
display: inline-flex;
align-items: center;
padding: 6px 12px;
background: var(--surface-hover);
border-radius: 8px;
font-size: 13px;
color: var(--text-secondary);
}
[data-theme="sentient"] .intent-health {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--accent-light);
border-radius: 8px;
font-size: 13px;
font-weight: 600;
}
[data-theme="sentient"] .intent-health-good {
color: var(--accent);
}
[data-theme="sentient"] .intent-health-warning {
background: var(--warning-bg);
color: var(--warning);
}
[data-theme="sentient"] .intent-health-bad {
background: var(--error-bg);
color: var(--error);
}
/* ============================================ */
/* DECISION REQUIRED PANEL */
/* ============================================ */
[data-theme="sentient"] .decision-panel {
background: var(--surface);
border: 1px solid var(--accent);
border-radius: 12px;
padding: 20px;
box-shadow: var(--shadow-glow);
}
[data-theme="sentient"] .decision-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
color: var(--accent);
font-weight: 700;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
[data-theme="sentient"] .decision-title {
color: var(--text);
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
}
[data-theme="sentient"] .decision-context {
background: var(--surface-hover);
border-radius: 8px;
padding: 16px;
color: var(--text-secondary);
font-size: 14px;
line-height: 1.6;
margin-bottom: 16px;
}
[data-theme="sentient"] .decision-actions {
display: flex;
gap: 12px;
}
[data-theme="sentient"] .decision-btn-primary {
background: var(--accent);
color: #000000;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
[data-theme="sentient"] .decision-btn-primary:hover {
background: var(--accent-hover);
box-shadow: var(--shadow-accent);
}
[data-theme="sentient"] .decision-btn-secondary {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
padding: 10px 20px;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
[data-theme="sentient"] .decision-btn-secondary:hover {
background: var(--surface-hover);
border-color: var(--text-secondary);
}
/* ============================================ */
/* PROGRESS LOG */
/* ============================================ */
[data-theme="sentient"] .progress-log {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
}
[data-theme="sentient"] .progress-log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
color: var(--text);
font-weight: 600;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
[data-theme="sentient"] .progress-log-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid var(--border-light);
}
[data-theme="sentient"] .progress-log-item:last-child {
border-bottom: none;
}
[data-theme="sentient"] .progress-log-icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
flex-shrink: 0;
}
[data-theme="sentient"] .progress-log-icon.complete {
background: var(--success-bg);
color: var(--success);
}
[data-theme="sentient"] .progress-log-icon.pending {
background: var(--warning-bg);
color: var(--warning);
}
[data-theme="sentient"] .progress-log-icon.active {
background: var(--accent-light);
color: var(--accent);
}
[data-theme="sentient"] .progress-log-content {
flex: 1;
}
[data-theme="sentient"] .progress-log-title {
color: var(--text);
font-weight: 500;
margin-bottom: 4px;
}
[data-theme="sentient"] .progress-log-detail {
color: var(--text-tertiary);
font-size: 13px;
}
[data-theme="sentient"] .progress-log-step {
display: inline-flex;
align-items: center;
padding: 4px 10px;
background: var(--accent);
color: #000000;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
}
[data-theme="sentient"] .progress-log-step.complete {
background: var(--success);
}
[data-theme="sentient"] .progress-log-time {
color: var(--text-tertiary);
font-size: 12px;
flex-shrink: 0;
}
/* ============================================ */
/* SIDEBAR / NAVIGATION */
/* ============================================ */
[data-theme="sentient"] .sidebar {
background: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
}
[data-theme="sentient"] .sidebar-item {
color: var(--text-secondary);
padding: 10px 16px;
border-radius: 8px;
margin: 4px 8px;
transition: all 0.2s ease;
}
[data-theme="sentient"] .sidebar-item:hover {
background: var(--surface-hover);
color: var(--text);
}
[data-theme="sentient"] .sidebar-item.active {
background: var(--accent-light);
color: var(--accent);
}
/* ============================================ */
/* DROPDOWN MENUS */
/* ============================================ */
[data-theme="sentient"] .apps-dropdown,
[data-theme="sentient"] .dropdown-menu {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: var(--shadow-lg);
}
[data-theme="sentient"] .apps-dropdown-title {
color: var(--text-secondary);
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
[data-theme="sentient"] .app-item {
color: var(--text-secondary);
padding: 12px;
border-radius: 8px;
transition: all 0.2s ease;
}
[data-theme="sentient"] .app-item:hover {
background: var(--surface-hover);
color: var(--text);
}
[data-theme="sentient"] .app-item.active {
background: var(--accent-light);
color: var(--accent);
}
/* ============================================ */
/* FORMS & INPUTS */
/* ============================================ */
[data-theme="sentient"] input,
[data-theme="sentient"] textarea,
[data-theme="sentient"] select {
background: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--text);
border-radius: 8px;
padding: 10px 14px;
transition: all 0.2s ease;
}
[data-theme="sentient"] input:focus,
[data-theme="sentient"] textarea:focus,
[data-theme="sentient"] select:focus {
border-color: var(--input-focus-border);
outline: none;
box-shadow: 0 0 0 3px var(--accent-light);
}
[data-theme="sentient"] input::placeholder,
[data-theme="sentient"] textarea::placeholder {
color: var(--text-tertiary);
}
/* ============================================ */
/* MODALS */
/* ============================================ */
[data-theme="sentient"] .modal-content {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
box-shadow: var(--shadow-lg);
}
[data-theme="sentient"] .modal-header {
border-bottom: 1px solid var(--border);
padding: 20px 24px;
}
[data-theme="sentient"] .modal-header h3 {
color: var(--text);
}
[data-theme="sentient"] .modal-body {
padding: 24px;
}
[data-theme="sentient"] .modal-footer {
border-top: 1px solid var(--border);
padding: 16px 24px;
}
/* ============================================ */
/* TABLES */
/* ============================================ */
[data-theme="sentient"] table {
width: 100%;
border-collapse: collapse;
}
[data-theme="sentient"] th {
background: var(--surface-hover);
color: var(--text-secondary);
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--border);
}
[data-theme="sentient"] td {
padding: 14px 16px;
border-bottom: 1px solid var(--border-light);
color: var(--text);
}
[data-theme="sentient"] tr:hover td {
background: var(--surface-hover);
}
/* ============================================ */
/* SCROLLBAR */
/* ============================================ */
[data-theme="sentient"] ::-webkit-scrollbar {
width: 8px;
height: 8px;
}
[data-theme="sentient"] ::-webkit-scrollbar-track {
background: var(--bg);
}
[data-theme="sentient"] ::-webkit-scrollbar-thumb {
background: var(--surface-hover);
border-radius: 4px;
}
[data-theme="sentient"] ::-webkit-scrollbar-thumb:hover {
background: var(--border);
}
/* ============================================ */
/* LOADING STATES */
/* ============================================ */
[data-theme="sentient"] .loading-overlay {
background: rgba(10, 10, 10, 0.9);
}
[data-theme="sentient"] .loading-spinner {
border-color: var(--surface-hover);
border-top-color: var(--accent);
}
[data-theme="sentient"] .skeleton {
background: linear-gradient(
90deg,
var(--surface) 25%,
var(--surface-hover) 50%,
var(--surface) 75%
);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
}
@keyframes skeleton-loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* ============================================ */
/* CHAT SPECIFIC */
/* ============================================ */
[data-theme="sentient"] .message-user {
background: var(--accent);
color: #000000;
}
[data-theme="sentient"] .message-bot {
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
}
[data-theme="sentient"] .chat-input-container {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
}
[data-theme="sentient"] .chat-input-container:focus-within {
border-color: var(--accent);
}
/* ============================================ */
/* ANIMATIONS */
/* ============================================ */
[data-theme="sentient"] .pulse-accent {
animation: pulse-accent 2s infinite;
}
@keyframes pulse-accent {
0%, 100% {
box-shadow: 0 0 0 0 var(--accent-glow);
}
50% {
box-shadow: 0 0 0 10px transparent;
}
}
/* ============================================ */
/* STATS & METRICS */
/* ============================================ */
[data-theme="sentient"] .stat-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
}
[data-theme="sentient"] .stat-value {
color: var(--text);
font-size: 28px;
font-weight: 700;
}
[data-theme="sentient"] .stat-label {
color: var(--text-secondary);
font-size: 14px;
margin-top: 4px;
}
[data-theme="sentient"] .stat-highlight {
color: var(--accent);
}
/* Time saved indicator */
[data-theme="sentient"] .time-saved {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: var(--accent-light);
border-radius: 20px;
color: var(--accent);
font-weight: 600;
font-size: 14px;
}
/* ============================================ */
/* DETAIL VIEW */
/* ============================================ */
[data-theme="sentient"] .detail-panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
}
[data-theme="sentient"] .detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid var(--border);
}
[data-theme="sentient"] .detail-title {
font-size: 18px;
font-weight: 600;
color: var(--text);
}
[data-theme="sentient"] .detail-body {
padding: 20px;
}
[data-theme="sentient"] .detail-section {
margin-bottom: 24px;
}
[data-theme="sentient"] .detail-section-title {
color: var(--text-secondary);
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
/* ============================================ */
/* PAUSE/ACTION BUTTONS */
/* ============================================ */
[data-theme="sentient"] .action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
border: 1px solid var(--border);
background: transparent;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
}
[data-theme="sentient"] .action-btn:hover {
background: var(--surface-hover);
color: var(--text);
border-color: var(--text-secondary);
}
[data-theme="sentient"] .action-btn.pause {
border-color: var(--warning);
color: var(--warning);
}
[data-theme="sentient"] .action-btn.pause:hover {

View file

@ -2,17 +2,22 @@
.drive-container {
display: flex;
height: calc(100vh - 60px);
background: var(--bg);
height: 100%;
min-height: 0;
background: var(--bg, var(--bg-primary, #0a0a0f));
color: var(--text, var(--text-primary, #ffffff));
}
/* Drive Sidebar */
.drive-sidebar {
width: 240px;
background: var(--surface);
border-right: 1px solid var(--border);
min-width: 240px;
background: var(--surface, var(--bg-secondary, #12121a));
border-right: 1px solid var(--border, #2a2a3a);
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
}
.drive-sidebar-header {
@ -76,6 +81,87 @@
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
}
/* Drive Header */
.drive-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-bottom: 1px solid var(--border, #2a2a3a);
flex-shrink: 0;
}
/* Drive Content - File Grid */
.drive-content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.file-list {
display: flex;
flex-direction: column;
gap: 2px;
}
.file-list-header {
display: grid;
grid-template-columns: 1fr 150px 100px;
gap: 16px;
padding: 8px 16px;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary, #6b6b80);
border-bottom: 1px solid var(--border, #2a2a3a);
}
.file-list-item,
.drive-file-item {
display: grid;
grid-template-columns: 1fr 150px 100px;
gap: 16px;
padding: 12px 16px;
align-items: center;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s;
}
.file-list-item:hover,
.drive-file-item:hover {
background: var(--surface-hover, rgba(255, 255, 255, 0.05));
}
.file-list-item.selected,
.drive-file-item.selected {
background: var(--accent-light, rgba(59, 130, 246, 0.1));
}
.file-name {
display: flex;
align-items: center;
gap: 12px;
font-weight: 500;
color: var(--text, var(--text-primary, #ffffff));
}
.file-icon {
font-size: 20px;
}
.file-date,
.file-size {
font-size: 13px;
color: var(--text-secondary, #6b6b80);
}
.drive-toolbar {
@ -164,11 +250,26 @@
border-radius: 8px;
}
.file-icon.folder { background: rgba(249, 115, 22, 0.1); color: #f97316; }
.file-icon.document { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
.file-icon.image { background: rgba(168, 85, 247, 0.1); color: #a855f7; }
.file-icon.video { background: rgba(239, 68, 68, 0.1); color: #ef4444; }
.file-icon.audio { background: rgba(34, 197, 94, 0.1); color: #22c55e; }
.file-icon.folder {
background: rgba(249, 115, 22, 0.1);
color: #f97316;
}
.file-icon.document {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
.file-icon.image {
background: rgba(168, 85, 247, 0.1);
color: #a855f7;
}
.file-icon.video {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.file-icon.audio {
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
}
.file-name {
font-size: 13px;

474
ui/suite/drive/drive.html Normal file
View file

@ -0,0 +1,474 @@
<!-- Drive - File Management -->
<link rel="stylesheet" href="drive/drive.css" />
<div class="drive-container" id="drive-app">
<!-- Sidebar -->
<aside class="drive-sidebar" id="drive-sidebar">
<div class="drive-sidebar-header">
<h2>Drive</h2>
<button class="btn-icon" id="toggle-drive-sidebar" title="Collapse">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="11 17 6 12 11 7"></polyline>
</svg>
</button>
</div>
<!-- Quick Actions -->
<div class="drive-sidebar-actions">
<button
class="btn-primary-full"
id="upload-btn"
onclick="uploadFile()"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
<span>Upload</span>
</button>
<button
class="btn-secondary-full"
id="new-folder-btn"
onclick="createFolder()"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
></path>
<line x1="12" y1="11" x2="12" y2="17"></line>
<line x1="9" y1="14" x2="15" y2="14"></line>
</svg>
<span>New Folder</span>
</button>
</div>
<!-- Navigation -->
<nav class="drive-nav">
<div class="drive-nav-item active" data-view="my-drive">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
></path>
</svg>
<span>My Drive</span>
</div>
<div class="drive-nav-item" data-view="shared">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
<span>Shared with me</span>
</div>
<div class="drive-nav-item" data-view="recent">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
<span>Recent</span>
</div>
<div class="drive-nav-item" data-view="starred">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polygon
points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
></polygon>
</svg>
<span>Starred</span>
</div>
<div class="drive-nav-item" data-view="trash">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="3 6 5 6 21 6"></polyline>
<path
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
></path>
</svg>
<span>Trash</span>
</div>
</nav>
<!-- Storage -->
<div class="drive-storage">
<div class="storage-bar">
<div class="storage-fill" style="width: 62%"></div>
</div>
<div class="storage-text">12.4 GB of 20 GB used</div>
</div>
</aside>
<!-- Main Content -->
<main class="drive-main">
<!-- Toolbar -->
<div class="drive-toolbar">
<div class="drive-toolbar-left">
<div class="drive-breadcrumb">
<button
class="breadcrumb-item"
onclick="navigateTo('root')"
>
My Drive
</button>
<span class="breadcrumb-sep">/</span>
<span class="breadcrumb-current">Projects</span>
</div>
</div>
<div class="drive-toolbar-right">
<button
class="btn-icon view-toggle"
data-view="grid"
title="Grid view"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
</svg>
</button>
<button
class="btn-icon view-toggle active"
data-view="list"
title="List view"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="8" y1="6" x2="21" y2="6"></line>
<line x1="8" y1="12" x2="21" y2="12"></line>
<line x1="8" y1="18" x2="21" y2="18"></line>
<line x1="3" y1="6" x2="3.01" y2="6"></line>
<line x1="3" y1="12" x2="3.01" y2="12"></line>
<line x1="3" y1="18" x2="3.01" y2="18"></line>
</svg>
</button>
</div>
</div>
<!-- File List -->
<div class="drive-content" id="drive-content">
<!-- List Header -->
<div class="drive-list-header">
<div class="file-col file-name-col">Name</div>
<div class="file-col file-modified-col">Modified</div>
<div class="file-col file-size-col">Size</div>
<div class="file-col file-actions-col"></div>
</div>
<!-- Folders -->
<div
class="drive-file-item folder"
data-id="1"
onclick="openFolder(this)"
hx-get="/api/drive/folder/1"
hx-target="#drive-content"
hx-swap="innerHTML"
>
<div class="file-col file-name-col">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="#5f6368"
stroke="none"
>
<path
d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"
/>
</svg>
<span>Documents</span>
</div>
<div class="file-col file-modified-col">Dec 15, 2024</div>
<div class="file-col file-size-col"></div>
<div class="file-col file-actions-col">
<button class="btn-icon-sm" title="More options"></button>
</div>
</div>
<div
class="drive-file-item folder"
data-id="2"
onclick="openFolder(this)"
>
<div class="file-col file-name-col">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="#5f6368"
stroke="none"
>
<path
d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"
/>
</svg>
<span>Images</span>
</div>
<div class="file-col file-modified-col">Dec 14, 2024</div>
<div class="file-col file-size-col"></div>
<div class="file-col file-actions-col">
<button class="btn-icon-sm" title="More options"></button>
</div>
</div>
<div
class="drive-file-item folder"
data-id="3"
onclick="openFolder(this)"
>
<div class="file-col file-name-col">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="#5f6368"
stroke="none"
>
<path
d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"
/>
</svg>
<span>Source Code</span>
</div>
<div class="file-col file-modified-col">Dec 12, 2024</div>
<div class="file-col file-size-col"></div>
<div class="file-col file-actions-col">
<button class="btn-icon-sm" title="More options"></button>
</div>
</div>
<!-- Files -->
<div
class="drive-file-item"
data-id="10"
onclick="selectFile(this)"
>
<div class="file-col file-name-col">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="#ea4335"
stroke="none"
>
<path
d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"
/>
</svg>
<span>project-report.pdf</span>
</div>
<div class="file-col file-modified-col">Dec 15, 2024</div>
<div class="file-col file-size-col">2.4 MB</div>
<div class="file-col file-actions-col">
<button class="btn-icon-sm" title="Download"></button>
<button class="btn-icon-sm" title="More options"></button>
</div>
</div>
<div
class="drive-file-item"
data-id="11"
onclick="selectFile(this)"
>
<div class="file-col file-name-col">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="#4285f4"
stroke="none"
>
<path
d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"
/>
</svg>
<span>meeting-notes.docx</span>
</div>
<div class="file-col file-modified-col">Dec 14, 2024</div>
<div class="file-col file-size-col">156 KB</div>
<div class="file-col file-actions-col">
<button class="btn-icon-sm" title="Download"></button>
<button class="btn-icon-sm" title="More options"></button>
</div>
</div>
<div
class="drive-file-item"
data-id="12"
onclick="selectFile(this)"
>
<div class="file-col file-name-col">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="#0f9d58"
stroke="none"
>
<path
d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"
/>
</svg>
<span>analytics-2025.xlsx</span>
</div>
<div class="file-col file-modified-col">Dec 13, 2024</div>
<div class="file-col file-size-col">890 KB</div>
<div class="file-col file-actions-col">
<button class="btn-icon-sm" title="Download"></button>
<button class="btn-icon-sm" title="More options"></button>
</div>
</div>
<div
class="drive-file-item"
data-id="13"
onclick="selectFile(this)"
>
<div class="file-col file-name-col">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="#fbbc04"
stroke="none"
>
<path
d="M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z"
/>
</svg>
<span>dashboard-mockup.png</span>
</div>
<div class="file-col file-modified-col">Dec 12, 2024</div>
<div class="file-col file-size-col">1.8 MB</div>
<div class="file-col file-actions-col">
<button class="btn-icon-sm" title="Download"></button>
<button class="btn-icon-sm" title="More options"></button>
</div>
</div>
<div
class="drive-file-item"
data-id="14"
onclick="selectFile(this)"
>
<div class="file-col file-name-col">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="#ea4335"
stroke="none"
>
<path
d="M18,4L20,8H17L15,4H13L15,8H12L10,4H8L10,8H7L5,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V4H18Z"
/>
</svg>
<span>demo-video.mp4</span>
</div>
<div class="file-col file-modified-col">Dec 10, 2024</div>
<div class="file-col file-size-col">125 MB</div>
<div class="file-col file-actions-col">
<button class="btn-icon-sm" title="Download"></button>
<button class="btn-icon-sm" title="More options"></button>
</div>
</div>
<div
class="drive-file-item"
data-id="15"
onclick="selectFile(this)"
>
<div class="file-col file-name-col">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="#5f6368"
stroke="none"
>
<path
d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-6 10H6v-2h8v2zm4-4H6v-2h12v2z"
/>
</svg>
<span>release-v2.0.zip</span>
</div>
<div class="file-col file-modified-col">Dec 8, 2024</div>
<div class="file-col file-size-col">45 MB</div>
<div class="file-col file-actions-col">
<button class="btn-icon-sm" title="Download"></button>
<button class="btn-icon-sm" title="More options"></button>
</div>
</div>
</div>
</main>
</div>
<script src="drive/drive.js"></script>

View file

@ -1,162 +1,199 @@
/* Drive Module JavaScript */
(function() {
'use strict';
(function () {
"use strict";
let selectedFiles = [];
let currentPath = '/';
let viewMode = 'grid';
let selectedFiles = [];
let currentPath = "/";
let viewMode = "grid";
function init() {
bindNavigation();
bindFileSelection();
bindDragAndDrop();
bindContextMenu();
bindKeyboardShortcuts();
// Global function for onclick handlers in HTML
window.selectFile = function (element) {
const fileId = element.dataset.id;
const isSelected = element.classList.contains("selected");
// If not holding Ctrl/Cmd, deselect all others first
if (!event.ctrlKey && !event.metaKey) {
document
.querySelectorAll(
".drive-file-item.selected, .file-item.selected, .file-card.selected, .file-list-item.selected",
)
.forEach((f) => f.classList.remove("selected"));
}
function bindNavigation() {
document.querySelectorAll('.drive-nav-item').forEach(item => {
item.addEventListener('click', function() {
document.querySelectorAll('.drive-nav-item').forEach(i => i.classList.remove('active'));
this.classList.add('active');
});
});
}
function bindFileSelection() {
document.querySelectorAll('.file-card, .file-list-item').forEach(file => {
file.addEventListener('click', function(e) {
if (e.ctrlKey || e.metaKey) {
this.classList.toggle('selected');
} else {
document.querySelectorAll('.file-card.selected, .file-list-item.selected')
.forEach(f => f.classList.remove('selected'));
this.classList.add('selected');
}
updateSelectedFiles();
});
file.addEventListener('dblclick', function() {
const isFolder = this.dataset.type === 'folder';
if (isFolder) {
navigateToFolder(this.dataset.path);
} else {
openFile(this.dataset.id);
}
});
});
}
function bindDragAndDrop() {
const uploadZone = document.querySelector('.upload-zone');
if (!uploadZone) return;
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event => {
uploadZone.addEventListener(event, (e) => {
e.preventDefault();
e.stopPropagation();
});
});
['dragenter', 'dragover'].forEach(event => {
uploadZone.addEventListener(event, () => uploadZone.classList.add('dragover'));
});
['dragleave', 'drop'].forEach(event => {
uploadZone.addEventListener(event, () => uploadZone.classList.remove('dragover'));
});
uploadZone.addEventListener('drop', (e) => {
const files = e.dataTransfer.files;
handleFileUpload(files);
});
}
function bindContextMenu() {
document.addEventListener('contextmenu', (e) => {
const fileCard = e.target.closest('.file-card, .file-list-item');
if (fileCard) {
e.preventDefault();
showContextMenu(e.clientX, e.clientY, fileCard);
}
});
document.addEventListener('click', hideContextMenu);
}
function bindKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
if (e.key === 'Delete' && selectedFiles.length > 0) {
deleteSelectedFiles();
}
if (e.key === 'a' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
selectAllFiles();
}
if (e.key === 'Escape') {
deselectAllFiles();
hideContextMenu();
}
});
}
function updateSelectedFiles() {
selectedFiles = Array.from(document.querySelectorAll('.file-card.selected, .file-list-item.selected'))
.map(f => f.dataset.id);
}
function navigateToFolder(path) {
currentPath = path;
// HTMX handles actual navigation
}
function openFile(fileId) {
console.log('Opening file:', fileId);
}
function handleFileUpload(files) {
console.log('Uploading files:', files);
}
function showContextMenu(x, y, fileCard) {
hideContextMenu();
const menu = document.getElementById('context-menu');
if (menu) {
menu.style.left = x + 'px';
menu.style.top = y + 'px';
menu.classList.remove('hidden');
menu.dataset.fileId = fileCard.dataset.id;
}
}
function hideContextMenu() {
const menu = document.getElementById('context-menu');
if (menu) menu.classList.add('hidden');
}
function selectAllFiles() {
document.querySelectorAll('.file-card, .file-list-item').forEach(f => f.classList.add('selected'));
updateSelectedFiles();
}
function deselectAllFiles() {
document.querySelectorAll('.file-card.selected, .file-list-item.selected')
.forEach(f => f.classList.remove('selected'));
selectedFiles = [];
}
function deleteSelectedFiles() {
if (confirm(`Delete ${selectedFiles.length} file(s)?`)) {
console.log('Deleting:', selectedFiles);
}
}
window.DriveModule = { init };
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
// Toggle selection on clicked element
if (isSelected && (event.ctrlKey || event.metaKey)) {
element.classList.remove("selected");
} else {
init();
element.classList.add("selected");
}
updateSelectedFiles();
};
function init() {
bindNavigation();
bindFileSelection();
bindDragAndDrop();
bindContextMenu();
bindKeyboardShortcuts();
}
function bindNavigation() {
document.querySelectorAll(".drive-nav-item").forEach((item) => {
item.addEventListener("click", function () {
document
.querySelectorAll(".drive-nav-item")
.forEach((i) => i.classList.remove("active"));
this.classList.add("active");
});
});
}
function bindFileSelection() {
document.querySelectorAll(".file-card, .file-list-item").forEach((file) => {
file.addEventListener("click", function (e) {
if (e.ctrlKey || e.metaKey) {
this.classList.toggle("selected");
} else {
document
.querySelectorAll(".file-card.selected, .file-list-item.selected")
.forEach((f) => f.classList.remove("selected"));
this.classList.add("selected");
}
updateSelectedFiles();
});
file.addEventListener("dblclick", function () {
const isFolder = this.dataset.type === "folder";
if (isFolder) {
navigateToFolder(this.dataset.path);
} else {
openFile(this.dataset.id);
}
});
});
}
function bindDragAndDrop() {
const uploadZone = document.querySelector(".upload-zone");
if (!uploadZone) return;
["dragenter", "dragover", "dragleave", "drop"].forEach((event) => {
uploadZone.addEventListener(event, (e) => {
e.preventDefault();
e.stopPropagation();
});
});
["dragenter", "dragover"].forEach((event) => {
uploadZone.addEventListener(event, () =>
uploadZone.classList.add("dragover"),
);
});
["dragleave", "drop"].forEach((event) => {
uploadZone.addEventListener(event, () =>
uploadZone.classList.remove("dragover"),
);
});
uploadZone.addEventListener("drop", (e) => {
const files = e.dataTransfer.files;
handleFileUpload(files);
});
}
function bindContextMenu() {
document.addEventListener("contextmenu", (e) => {
const fileCard = e.target.closest(".file-card, .file-list-item");
if (fileCard) {
e.preventDefault();
showContextMenu(e.clientX, e.clientY, fileCard);
}
});
document.addEventListener("click", hideContextMenu);
}
function bindKeyboardShortcuts() {
document.addEventListener("keydown", (e) => {
if (e.key === "Delete" && selectedFiles.length > 0) {
deleteSelectedFiles();
}
if (e.key === "a" && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
selectAllFiles();
}
if (e.key === "Escape") {
deselectAllFiles();
hideContextMenu();
}
});
}
function updateSelectedFiles() {
selectedFiles = Array.from(
document.querySelectorAll(
".file-card.selected, .file-list-item.selected",
),
).map((f) => f.dataset.id);
}
function navigateToFolder(path) {
currentPath = path;
// HTMX handles actual navigation
}
function openFile(fileId) {
console.log("Opening file:", fileId);
}
function handleFileUpload(files) {
console.log("Uploading files:", files);
}
function showContextMenu(x, y, fileCard) {
hideContextMenu();
const menu = document.getElementById("context-menu");
if (menu) {
menu.style.left = x + "px";
menu.style.top = y + "px";
menu.classList.remove("hidden");
menu.dataset.fileId = fileCard.dataset.id;
}
}
function hideContextMenu() {
const menu = document.getElementById("context-menu");
if (menu) menu.classList.add("hidden");
}
function selectAllFiles() {
document
.querySelectorAll(".file-card, .file-list-item")
.forEach((f) => f.classList.add("selected"));
updateSelectedFiles();
}
function deselectAllFiles() {
document
.querySelectorAll(".file-card.selected, .file-list-item.selected")
.forEach((f) => f.classList.remove("selected"));
selectedFiles = [];
}
function deleteSelectedFiles() {
if (confirm(`Delete ${selectedFiles.length} file(s)?`)) {
console.log("Deleting:", selectedFiles);
}
}
window.DriveModule = { init };
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();

View file

@ -148,21 +148,7 @@
</div>
</div>
<div class="toolbar-center">
<div class="search-box">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input type="text"
placeholder="Search in Drive..."
name="q"
hx-get="/api/drive/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#file-grid"
hx-swap="innerHTML">
</div>
</div>
<div class="toolbar-actions">
<div class="view-toggle">

File diff suppressed because it is too large Load diff

View file

@ -12,253 +12,316 @@ const settingsPanel = document.getElementById("settings-panel");
// Apps Menu Toggle
if (appsBtn) {
appsBtn.addEventListener("click", (e) => {
e.stopPropagation();
const isOpen = appsDropdown.classList.toggle("show");
appsBtn.setAttribute("aria-expanded", isOpen);
settingsPanel.classList.remove("show");
});
appsBtn.addEventListener("click", (e) => {
e.stopPropagation();
const isOpen = appsDropdown.classList.toggle("show");
appsBtn.setAttribute("aria-expanded", isOpen);
settingsPanel.classList.remove("show");
});
}
// Settings Panel Toggle
if (settingsBtn) {
settingsBtn.addEventListener("click", (e) => {
e.stopPropagation();
const isOpen = settingsPanel.classList.toggle("show");
settingsBtn.setAttribute("aria-expanded", isOpen);
appsDropdown.classList.remove("show");
});
settingsBtn.addEventListener("click", (e) => {
e.stopPropagation();
const isOpen = settingsPanel.classList.toggle("show");
settingsBtn.setAttribute("aria-expanded", isOpen);
appsDropdown.classList.remove("show");
});
}
// Close dropdowns when clicking outside
document.addEventListener("click", (e) => {
if (appsDropdown && !appsDropdown.contains(e.target) && !appsBtn.contains(e.target)) {
appsDropdown.classList.remove("show");
appsBtn.setAttribute("aria-expanded", "false");
}
if (settingsPanel && !settingsPanel.contains(e.target) && !settingsBtn.contains(e.target)) {
settingsPanel.classList.remove("show");
settingsBtn.setAttribute("aria-expanded", "false");
}
if (
appsDropdown &&
!appsDropdown.contains(e.target) &&
!appsBtn.contains(e.target)
) {
appsDropdown.classList.remove("show");
appsBtn.setAttribute("aria-expanded", "false");
}
if (
settingsPanel &&
!settingsPanel.contains(e.target) &&
!settingsBtn.contains(e.target)
) {
settingsPanel.classList.remove("show");
settingsBtn.setAttribute("aria-expanded", "false");
}
});
// Escape key closes dropdowns
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
if (appsDropdown) appsDropdown.classList.remove("show");
if (settingsPanel) settingsPanel.classList.remove("show");
if (appsBtn) appsBtn.setAttribute("aria-expanded", "false");
if (settingsBtn) settingsBtn.setAttribute("aria-expanded", "false");
}
if (e.key === "Escape") {
if (appsDropdown) appsDropdown.classList.remove("show");
if (settingsPanel) settingsPanel.classList.remove("show");
if (appsBtn) appsBtn.setAttribute("aria-expanded", "false");
if (settingsBtn) settingsBtn.setAttribute("aria-expanded", "false");
}
});
// Alt+key shortcuts for navigation
document.addEventListener("keydown", (e) => {
if (e.altKey && !e.ctrlKey && !e.shiftKey) {
const shortcuts = {
1: "chat",
2: "drive",
3: "tasks",
4: "mail",
5: "calendar",
6: "meet",
7: "paper",
8: "research",
9: "sources",
0: "analytics",
a: "admin",
m: "monitoring",
};
if (shortcuts[e.key]) {
e.preventDefault();
const link = document.querySelector(`a[href="#${shortcuts[e.key]}"]`);
if (link) link.click();
if (appsDropdown) appsDropdown.classList.remove("show");
}
if (e.key === ",") {
e.preventDefault();
if (settingsPanel) settingsPanel.classList.toggle("show");
}
if (e.key === "s") {
e.preventDefault();
const settingsLink = document.querySelector(`a[href="#settings"]`);
if (settingsLink) settingsLink.click();
}
if (e.altKey && !e.ctrlKey && !e.shiftKey) {
const shortcuts = {
1: "chat",
2: "drive",
3: "tasks",
4: "mail",
5: "calendar",
6: "meet",
7: "paper",
8: "research",
9: "sources",
0: "analytics",
a: "admin",
m: "monitoring",
};
if (shortcuts[e.key]) {
e.preventDefault();
const link = document.querySelector(`a[href="#${shortcuts[e.key]}"]`);
if (link) link.click();
if (appsDropdown) appsDropdown.classList.remove("show");
}
if (e.key === ",") {
e.preventDefault();
if (settingsPanel) settingsPanel.classList.toggle("show");
}
if (e.key === "s") {
e.preventDefault();
const settingsLink = document.querySelector(`a[href="#settings"]`);
if (settingsLink) settingsLink.click();
}
}
});
// Update active app on HTMX swap
document.body.addEventListener("htmx:afterSwap", (e) => {
if (e.detail.target.id === "main-content") {
const hash = window.location.hash || "#chat";
document.querySelectorAll(".app-item").forEach((item) => {
item.classList.toggle("active", item.getAttribute("href") === hash);
});
if (settingsPanel) settingsPanel.classList.remove("show");
}
if (e.detail.target.id === "main-content") {
const hash = window.location.hash || "#chat";
document.querySelectorAll(".app-item").forEach((item) => {
item.classList.toggle("active", item.getAttribute("href") === hash);
});
if (settingsPanel) settingsPanel.classList.remove("show");
}
});
// Theme handling
// Available themes: dark, light, blue, purple, green, orange, sentient
const themeOptions = document.querySelectorAll(".theme-option");
const savedTheme = localStorage.getItem("gb-theme") || "dark";
const savedTheme = localStorage.getItem("gb-theme") || "sentient";
document.body.setAttribute("data-theme", savedTheme);
document.querySelector(`.theme-option[data-theme="${savedTheme}"]`)?.classList.add("active");
document
.querySelector(`.theme-option[data-theme="${savedTheme}"]`)
?.classList.add("active");
// Update theme-color meta tag based on theme
function updateThemeColor(theme) {
const themeColors = {
// Core Themes
dark: "#3b82f6",
light: "#3b82f6",
blue: "#0ea5e9",
purple: "#a855f7",
green: "#22c55e",
orange: "#f97316",
sentient: "#d4f505",
// Retro Themes
cyberpunk: "#ff00ff",
retrowave: "#ff6b9d",
vapordream: "#a29bfe",
y2kglow: "#00ff00",
arcadeflash: "#ffff00",
discofever: "#ff1493",
grungeera: "#8b4513",
// Classic Themes
jazzage: "#d4af37",
mellowgold: "#daa520",
midcenturymod: "#e07b39",
polaroidmemories: "#e6b89c",
saturdaycartoons: "#ff6347",
seasidepostcard: "#20b2aa",
typewriter: "#2f2f2f",
// Tech Themes
"3dbevel": "#0000ff",
xeroxui: "#4a86cf",
xtreegold: "#ffff00",
};
const metaTheme = document.querySelector('meta[name="theme-color"]');
if (metaTheme) {
metaTheme.setAttribute("content", themeColors[theme] || "#d4f505");
}
}
updateThemeColor(savedTheme);
themeOptions.forEach((option) => {
option.addEventListener("click", () => {
const theme = option.getAttribute("data-theme");
document.body.setAttribute("data-theme", theme);
localStorage.setItem("gb-theme", theme);
themeOptions.forEach((o) => o.classList.remove("active"));
option.classList.add("active");
});
option.addEventListener("click", () => {
const theme = option.getAttribute("data-theme");
document.body.setAttribute("data-theme", theme);
localStorage.setItem("gb-theme", theme);
themeOptions.forEach((o) => o.classList.remove("active"));
option.classList.add("active");
updateThemeColor(theme);
});
});
// Global theme setter function (can be called from settings or elsewhere)
window.setTheme = function (theme) {
document.body.setAttribute("data-theme", theme);
localStorage.setItem("gb-theme", theme);
themeOptions.forEach((o) => {
o.classList.toggle("active", o.getAttribute("data-theme") === theme);
});
updateThemeColor(theme);
};
// Quick Settings Toggle
function toggleQuickSetting(el) {
el.classList.toggle("active");
const setting = el.id.replace("toggle-", "");
localStorage.setItem(`gb-${setting}`, el.classList.contains("active"));
el.classList.toggle("active");
const setting = el.id.replace("toggle-", "");
localStorage.setItem(`gb-${setting}`, el.classList.contains("active"));
}
// Load quick toggle states
["notifications", "sound", "compact"].forEach((setting) => {
const saved = localStorage.getItem(`gb-${setting}`);
const toggle = document.getElementById(`toggle-${setting}`);
if (toggle && saved !== null) {
toggle.classList.toggle("active", saved === "true");
}
const saved = localStorage.getItem(`gb-${setting}`);
const toggle = document.getElementById(`toggle-${setting}`);
if (toggle && saved !== null) {
toggle.classList.toggle("active", saved === "true");
}
});
// Show keyboard shortcuts notification
function showKeyboardShortcuts() {
window.showNotification(
"Alt+1-9,0 for apps, Alt+A Admin, Alt+M Monitoring, Alt+S Settings, Alt+, quick settings",
"info",
8000
);
window.showNotification(
"Alt+1-9,0 for apps, Alt+A Admin, Alt+M Monitoring, Alt+S Settings, Alt+, quick settings",
"info",
8000,
);
}
// Accessibility: Announce page changes to screen readers
function announceToScreenReader(message) {
const liveRegion = document.getElementById("aria-live");
if (liveRegion) {
liveRegion.textContent = message;
// Clear after announcement
setTimeout(() => {
liveRegion.textContent = "";
}, 1000);
}
const liveRegion = document.getElementById("aria-live");
if (liveRegion) {
liveRegion.textContent = message;
// Clear after announcement
setTimeout(() => {
liveRegion.textContent = "";
}, 1000);
}
}
// HTMX accessibility hooks
document.body.addEventListener("htmx:beforeRequest", function (e) {
const target = e.detail.target;
if (target && target.id === "main-content") {
target.setAttribute("aria-busy", "true");
announceToScreenReader("Loading content...");
}
const target = e.detail.target;
if (target && target.id === "main-content") {
target.setAttribute("aria-busy", "true");
announceToScreenReader("Loading content...");
}
});
document.body.addEventListener("htmx:afterSwap", function (e) {
const target = e.detail.target;
if (target && target.id === "main-content") {
target.setAttribute("aria-busy", "false");
// Focus management: move focus to main content after navigation
target.focus();
announceToScreenReader("Content loaded");
}
const target = e.detail.target;
if (target && target.id === "main-content") {
target.setAttribute("aria-busy", "false");
// Focus management: move focus to main content after navigation
target.focus();
announceToScreenReader("Content loaded");
}
});
document.body.addEventListener("htmx:responseError", function (e) {
const target = e.detail.target;
if (target) {
target.setAttribute("aria-busy", "false");
}
announceToScreenReader("Error loading content. Please try again.");
const target = e.detail.target;
if (target) {
target.setAttribute("aria-busy", "false");
}
announceToScreenReader("Error loading content. Please try again.");
});
// Keyboard navigation for apps grid
document.addEventListener("keydown", function (e) {
const appsGrid = document.querySelector(".apps-grid");
if (!appsGrid || !appsGrid.closest(".show")) return;
const appsGrid = document.querySelector(".apps-grid");
if (!appsGrid || !appsGrid.closest(".show")) return;
const items = Array.from(appsGrid.querySelectorAll(".app-item"));
const currentIndex = items.findIndex((item) => item === document.activeElement);
const items = Array.from(appsGrid.querySelectorAll(".app-item"));
const currentIndex = items.findIndex(
(item) => item === document.activeElement,
);
if (currentIndex === -1) return;
if (currentIndex === -1) return;
let newIndex = currentIndex;
const columns = 3; // Grid has 3 columns on desktop
let newIndex = currentIndex;
const columns = 3; // Grid has 3 columns on desktop
switch (e.key) {
case "ArrowRight":
newIndex = Math.min(currentIndex + 1, items.length - 1);
break;
case "ArrowLeft":
newIndex = Math.max(currentIndex - 1, 0);
break;
case "ArrowDown":
newIndex = Math.min(currentIndex + columns, items.length - 1);
break;
case "ArrowUp":
newIndex = Math.max(currentIndex - columns, 0);
break;
case "Home":
newIndex = 0;
break;
case "End":
newIndex = items.length - 1;
break;
default:
return;
}
switch (e.key) {
case "ArrowRight":
newIndex = Math.min(currentIndex + 1, items.length - 1);
break;
case "ArrowLeft":
newIndex = Math.max(currentIndex - 1, 0);
break;
case "ArrowDown":
newIndex = Math.min(currentIndex + columns, items.length - 1);
break;
case "ArrowUp":
newIndex = Math.max(currentIndex - columns, 0);
break;
case "Home":
newIndex = 0;
break;
case "End":
newIndex = items.length - 1;
break;
default:
return;
}
if (newIndex !== currentIndex) {
e.preventDefault();
items[newIndex].focus();
}
if (newIndex !== currentIndex) {
e.preventDefault();
items[newIndex].focus();
}
});
// Notification System
window.showNotification = function (message, type = "info", duration = 5000) {
const container = document.getElementById("notifications");
if (!container) return;
const notification = document.createElement("div");
notification.className = `notification ${type}`;
notification.innerHTML = `
const container = document.getElementById("notifications");
if (!container) return;
const notification = document.createElement("div");
notification.className = `notification ${type}`;
notification.innerHTML = `
<div class="notification-content">
<div class="notification-message">${message}</div>
</div>
<button class="notification-close" onclick="this.parentElement.remove()" style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:1.25rem;">×</button>
`;
container.appendChild(notification);
if (duration > 0) {
setTimeout(() => notification.remove(), duration);
}
container.appendChild(notification);
if (duration > 0) {
setTimeout(() => notification.remove(), duration);
}
};
// Global HTMX error handling with retry mechanism
const htmxRetryConfig = {
maxRetries: 3,
retryDelay: 1000,
retryCount: new Map(),
maxRetries: 3,
retryDelay: 1000,
retryCount: new Map(),
};
function getRetryKey(elt) {
return (
elt.getAttribute("hx-get") ||
elt.getAttribute("hx-post") ||
elt.getAttribute("hx-put") ||
elt.getAttribute("hx-delete") ||
elt.id ||
Math.random().toString(36)
);
return (
elt.getAttribute("hx-get") ||
elt.getAttribute("hx-post") ||
elt.getAttribute("hx-put") ||
elt.getAttribute("hx-delete") ||
elt.id ||
Math.random().toString(36)
);
}
function showErrorState(target, errorMessage, retryCallback) {
const errorHtml = `
const errorHtml = `
<div class="error-state">
<svg class="error-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
@ -280,95 +343,108 @@ function showErrorState(target, errorMessage, retryCallback) {
</div>
</div>
`;
target.innerHTML = errorHtml;
target.dataset.retryCallback = retryCallback;
target.innerHTML = errorHtml;
target.dataset.retryCallback = retryCallback;
}
window.retryLastRequest = function (btn) {
const target = btn.closest(".error-state").parentElement;
const retryCallback = target.dataset.retryCallback;
const target = btn.closest(".error-state").parentElement;
const retryCallback = target.dataset.retryCallback;
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Retrying...';
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Retrying...';
if (retryCallback && window[retryCallback]) {
window[retryCallback]();
if (retryCallback && window[retryCallback]) {
window[retryCallback]();
} else {
// Try to re-trigger HTMX request
const triggers = target.querySelectorAll("[hx-get], [hx-post]");
if (triggers.length > 0) {
htmx.trigger(triggers[0], "htmx:trigger");
} else {
// Try to re-trigger HTMX request
const triggers = target.querySelectorAll("[hx-get], [hx-post]");
if (triggers.length > 0) {
htmx.trigger(triggers[0], "htmx:trigger");
} else {
// Reload the current app
const activeApp = document.querySelector(".app-item.active");
if (activeApp) {
activeApp.click();
}
}
// Reload the current app
const activeApp = document.querySelector(".app-item.active");
if (activeApp) {
activeApp.click();
}
}
}
};
// Handle HTMX errors globally
document.body.addEventListener("htmx:responseError", function (e) {
const target = e.detail.target;
const xhr = e.detail.xhr;
const retryKey = getRetryKey(e.detail.elt);
const target = e.detail.target;
const xhr = e.detail.xhr;
const retryKey = getRetryKey(e.detail.elt);
let currentRetries = htmxRetryConfig.retryCount.get(retryKey) || 0;
let currentRetries = htmxRetryConfig.retryCount.get(retryKey) || 0;
// Auto-retry for network errors (status 0) or server errors (5xx)
if ((xhr.status === 0 || xhr.status >= 500) && currentRetries < htmxRetryConfig.maxRetries) {
htmxRetryConfig.retryCount.set(retryKey, currentRetries + 1);
const delay = htmxRetryConfig.retryDelay * Math.pow(2, currentRetries);
// Auto-retry for network errors (status 0) or server errors (5xx)
if (
(xhr.status === 0 || xhr.status >= 500) &&
currentRetries < htmxRetryConfig.maxRetries
) {
htmxRetryConfig.retryCount.set(retryKey, currentRetries + 1);
const delay = htmxRetryConfig.retryDelay * Math.pow(2, currentRetries);
window.showNotification(
`Request failed. Retrying in ${delay / 1000}s... (${currentRetries + 1}/${htmxRetryConfig.maxRetries})`,
"warning",
delay
);
window.showNotification(
`Request failed. Retrying in ${delay / 1000}s... (${currentRetries + 1}/${htmxRetryConfig.maxRetries})`,
"warning",
delay,
);
setTimeout(() => {
htmx.trigger(e.detail.elt, "htmx:trigger");
}, delay);
} else {
// Max retries reached or client error - show error state
htmxRetryConfig.retryCount.delete(retryKey);
setTimeout(() => {
htmx.trigger(e.detail.elt, "htmx:trigger");
}, delay);
} else {
// Max retries reached or client error - show error state
htmxRetryConfig.retryCount.delete(retryKey);
let errorMessage = "We couldn't load the content.";
if (xhr.status === 401) {
errorMessage = "Your session has expired. Please log in again.";
} else if (xhr.status === 403) {
errorMessage = "You don't have permission to access this resource.";
} else if (xhr.status === 404) {
errorMessage = "The requested content was not found.";
} else if (xhr.status >= 500) {
errorMessage = "The server is experiencing issues. Please try again later.";
} else if (xhr.status === 0) {
errorMessage = "Unable to connect. Please check your internet connection.";
}
if (target && target.id === "main-content") {
showErrorState(target, errorMessage);
} else {
window.showNotification(errorMessage, "error", 8000);
}
let errorMessage = "We couldn't load the content.";
if (xhr.status === 401) {
errorMessage = "Your session has expired. Please log in again.";
} else if (xhr.status === 403) {
errorMessage = "You don't have permission to access this resource.";
} else if (xhr.status === 404) {
errorMessage = "The requested content was not found.";
} else if (xhr.status >= 500) {
errorMessage =
"The server is experiencing issues. Please try again later.";
} else if (xhr.status === 0) {
errorMessage =
"Unable to connect. Please check your internet connection.";
}
if (target && target.id === "main-content") {
showErrorState(target, errorMessage);
} else {
window.showNotification(errorMessage, "error", 8000);
}
}
});
// Clear retry count on successful request
document.body.addEventListener("htmx:afterRequest", function (e) {
if (e.detail.successful) {
const retryKey = getRetryKey(e.detail.elt);
htmxRetryConfig.retryCount.delete(retryKey);
}
if (e.detail.successful) {
const retryKey = getRetryKey(e.detail.elt);
htmxRetryConfig.retryCount.delete(retryKey);
}
});
// Handle timeout errors
document.body.addEventListener("htmx:timeout", function (e) {
window.showNotification("Request timed out. Please try again.", "warning", 5000);
window.showNotification(
"Request timed out. Please try again.",
"warning",
5000,
);
});
// Handle send errors (network issues before request sent)
document.body.addEventListener("htmx:sendError", function (e) {
window.showNotification("Network error. Please check your connection.", "error", 5000);
window.showNotification(
"Network error. Please check your connection.",
"error",
5000,
);
});

View file

@ -1,98 +1,118 @@
// HTMX-based application initialization
(function() {
'use strict';
(function () {
"use strict";
// Configuration
const config = {
wsUrl: '/ws',
apiBase: '/api',
reconnectDelay: 3000,
maxReconnectAttempts: 5
};
// Configuration
const config = {
wsUrl: "/ws",
apiBase: "/api",
reconnectDelay: 3000,
maxReconnectAttempts: 5,
};
// State
let reconnectAttempts = 0;
let wsConnection = null;
// State
let reconnectAttempts = 0;
let wsConnection = null;
// Initialize HTMX extensions
function initHTMX() {
// Configure HTMX
htmx.config.defaultSwapStyle = 'innerHTML';
htmx.config.defaultSettleDelay = 100;
htmx.config.timeout = 10000;
// Initialize HTMX extensions
function initHTMX() {
// Configure HTMX
htmx.config.defaultSwapStyle = "innerHTML";
htmx.config.defaultSettleDelay = 100;
htmx.config.timeout = 10000;
// Add CSRF token to all requests if available
document.body.addEventListener('htmx:configRequest', (event) => {
const token = localStorage.getItem('csrf_token');
if (token) {
event.detail.headers['X-CSRF-Token'] = token;
}
});
// Add CSRF token to all requests if available
document.body.addEventListener("htmx:configRequest", (event) => {
const token = localStorage.getItem("csrf_token");
if (token) {
event.detail.headers["X-CSRF-Token"] = token;
}
});
// Handle errors globally
document.body.addEventListener('htmx:responseError', (event) => {
console.error('HTMX Error:', event.detail);
showNotification('Connection error. Please try again.', 'error');
});
// Handle errors globally
document.body.addEventListener("htmx:responseError", (event) => {
console.error("HTMX Error:", event.detail);
showNotification("Connection error. Please try again.", "error");
});
// Handle successful swaps
document.body.addEventListener('htmx:afterSwap', (event) => {
// Auto-scroll messages if in chat
const messages = document.getElementById('messages');
if (messages && event.detail.target === messages) {
messages.scrollTop = messages.scrollHeight;
}
});
// Handle before swap to prevent errors when target doesn't exist
document.body.addEventListener("htmx:beforeSwap", (event) => {
const target = event.detail.target;
const status = event.detail.xhr?.status;
// Handle WebSocket messages
document.body.addEventListener('htmx:wsMessage', (event) => {
handleWebSocketMessage(JSON.parse(event.detail.message));
});
// If target doesn't exist or response is 404, prevent the swap
if (!target || status === 404) {
event.detail.shouldSwap = false;
return;
}
// Handle WebSocket connection events
document.body.addEventListener('htmx:wsConnecting', () => {
updateConnectionStatus('connecting');
});
// For empty responses, set empty content to prevent insertBefore errors
if (
!event.detail.serverResponse ||
event.detail.serverResponse.trim() === ""
) {
event.detail.serverResponse = "<!-- empty -->";
}
});
document.body.addEventListener('htmx:wsOpen', () => {
updateConnectionStatus('connected');
reconnectAttempts = 0;
});
document.body.addEventListener('htmx:wsClose', () => {
updateConnectionStatus('disconnected');
attemptReconnect();
});
}
// Handle successful swaps
document.body.addEventListener("htmx:afterSwap", (event) => {
// Auto-scroll messages if in chat
const messages = document.getElementById("messages");
if (messages && event.detail.target === messages) {
messages.scrollTop = messages.scrollHeight;
}
});
// Handle WebSocket messages
function handleWebSocketMessage(message) {
switch(message.type) {
case 'message':
appendMessage(message);
break;
case 'notification':
showNotification(message.text, message.severity);
break;
case 'status':
updateStatus(message);
break;
case 'suggestion':
addSuggestion(message.text);
break;
default:
console.log('Unknown message type:', message.type);
}
document.body.addEventListener("htmx:wsMessage", (event) => {
handleWebSocketMessage(JSON.parse(event.detail.message));
});
// Handle WebSocket connection events
document.body.addEventListener("htmx:wsConnecting", () => {
updateConnectionStatus("connecting");
});
document.body.addEventListener("htmx:wsOpen", () => {
updateConnectionStatus("connected");
reconnectAttempts = 0;
});
document.body.addEventListener("htmx:wsClose", () => {
updateConnectionStatus("disconnected");
attemptReconnect();
});
}
// Handle WebSocket messages
function handleWebSocketMessage(message) {
switch (message.type) {
case "message":
appendMessage(message);
break;
case "notification":
showNotification(message.text, message.severity);
break;
case "status":
updateStatus(message);
break;
case "suggestion":
addSuggestion(message.text);
break;
default:
console.log("Unknown message type:", message.type);
}
}
// Append message to chat
function appendMessage(message) {
const messagesEl = document.getElementById('messages');
if (!messagesEl) return;
// Append message to chat
function appendMessage(message) {
const messagesEl = document.getElementById("messages");
if (!messagesEl) return;
const messageEl = document.createElement('div');
messageEl.className = `message ${message.sender === 'user' ? 'user' : 'bot'}`;
messageEl.innerHTML = `
const messageEl = document.createElement("div");
messageEl.className = `message ${message.sender === "user" ? "user" : "bot"}`;
messageEl.innerHTML = `
<div class="message-content">
<span class="sender">${message.sender}</span>
<span class="text">${escapeHtml(message.text)}</span>
@ -100,216 +120,218 @@
</div>
`;
messagesEl.appendChild(messageEl);
messagesEl.scrollTop = messagesEl.scrollHeight;
messagesEl.appendChild(messageEl);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
// Add suggestion chip
function addSuggestion(text) {
const suggestionsEl = document.getElementById("suggestions");
if (!suggestionsEl) return;
const chip = document.createElement("button");
chip.className = "suggestion-chip";
chip.textContent = text;
chip.setAttribute("hx-post", "/api/sessions/current/message");
chip.setAttribute("hx-vals", JSON.stringify({ content: text }));
chip.setAttribute("hx-target", "#messages");
chip.setAttribute("hx-swap", "beforeend");
suggestionsEl.appendChild(chip);
htmx.process(chip);
}
// Update connection status
function updateConnectionStatus(status) {
const statusEl = document.getElementById("connectionStatus");
if (!statusEl) return;
statusEl.className = `connection-status ${status}`;
statusEl.textContent = status.charAt(0).toUpperCase() + status.slice(1);
}
// Update general status
function updateStatus(message) {
const statusEl = document.getElementById("status-" + message.id);
if (statusEl) {
statusEl.textContent = message.text;
statusEl.className = `status ${message.severity}`;
}
}
// Show notification
function showNotification(text, type = "info") {
const notification = document.createElement("div");
notification.className = `notification ${type}`;
notification.textContent = text;
const container = document.getElementById("notifications") || document.body;
container.appendChild(notification);
setTimeout(() => {
notification.classList.add("fade-out");
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Attempt to reconnect WebSocket
function attemptReconnect() {
if (reconnectAttempts >= config.maxReconnectAttempts) {
showNotification("Connection lost. Please refresh the page.", "error");
return;
}
// Add suggestion chip
function addSuggestion(text) {
const suggestionsEl = document.getElementById('suggestions');
if (!suggestionsEl) return;
reconnectAttempts++;
setTimeout(() => {
console.log(`Reconnection attempt ${reconnectAttempts}...`);
htmx.trigger(document.body, "htmx:wsReconnect");
}, config.reconnectDelay);
}
const chip = document.createElement('button');
chip.className = 'suggestion-chip';
chip.textContent = text;
chip.setAttribute('hx-post', '/api/sessions/current/message');
chip.setAttribute('hx-vals', JSON.stringify({content: text}));
chip.setAttribute('hx-target', '#messages');
chip.setAttribute('hx-swap', 'beforeend');
// Utility: Escape HTML
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
suggestionsEl.appendChild(chip);
htmx.process(chip);
}
// Utility: Format timestamp
function formatTime(timestamp) {
if (!timestamp) return "";
const date = new Date(timestamp);
return date.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
}
// Update connection status
function updateConnectionStatus(status) {
const statusEl = document.getElementById('connectionStatus');
if (!statusEl) return;
// Handle navigation
function initNavigation() {
// Update active nav item on page change
document.addEventListener("htmx:pushedIntoHistory", (event) => {
const path = event.detail.path;
updateActiveNav(path);
});
statusEl.className = `connection-status ${status}`;
statusEl.textContent = status.charAt(0).toUpperCase() + status.slice(1);
}
// Handle browser back/forward
window.addEventListener("popstate", (event) => {
updateActiveNav(window.location.pathname);
});
}
// Update general status
function updateStatus(message) {
const statusEl = document.getElementById('status-' + message.id);
if (statusEl) {
statusEl.textContent = message.text;
statusEl.className = `status ${message.severity}`;
// Update active navigation item
function updateActiveNav(path) {
document.querySelectorAll(".nav-item, .app-item").forEach((item) => {
const href = item.getAttribute("href");
if (href === path || (path === "/" && href === "/chat")) {
item.classList.add("active");
} else {
item.classList.remove("active");
}
});
}
// Initialize keyboard shortcuts
function initKeyboardShortcuts() {
document.addEventListener("keydown", (e) => {
// Send message on Enter (when in input)
if (e.key === "Enter" && !e.shiftKey) {
const input = document.getElementById("messageInput");
if (input && document.activeElement === input) {
e.preventDefault();
const form = input.closest("form");
if (form) {
htmx.trigger(form, "submit");
}
}
}
}
// Show notification
function showNotification(text, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = text;
// Focus input on /
if (e.key === "/" && document.activeElement.tagName !== "INPUT") {
e.preventDefault();
const input = document.getElementById("messageInput");
if (input) input.focus();
}
const container = document.getElementById('notifications') || document.body;
container.appendChild(notification);
setTimeout(() => {
notification.classList.add('fade-out');
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Attempt to reconnect WebSocket
function attemptReconnect() {
if (reconnectAttempts >= config.maxReconnectAttempts) {
showNotification('Connection lost. Please refresh the page.', 'error');
return;
// Escape to blur input
if (e.key === "Escape") {
const input = document.getElementById("messageInput");
if (input && document.activeElement === input) {
input.blur();
}
}
});
}
reconnectAttempts++;
setTimeout(() => {
console.log(`Reconnection attempt ${reconnectAttempts}...`);
htmx.trigger(document.body, 'htmx:wsReconnect');
}, config.reconnectDelay);
}
// Initialize scroll behavior
function initScrollBehavior() {
const scrollBtn = document.getElementById("scrollToBottom");
const messages = document.getElementById("messages");
// Utility: Escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
if (scrollBtn && messages) {
// Show/hide scroll button
messages.addEventListener("scroll", () => {
const isAtBottom =
messages.scrollHeight - messages.scrollTop <=
messages.clientHeight + 100;
scrollBtn.style.display = isAtBottom ? "none" : "flex";
});
// Utility: Format timestamp
function formatTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
return date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
// Scroll to bottom on click
scrollBtn.addEventListener("click", () => {
messages.scrollTo({
top: messages.scrollHeight,
behavior: "smooth",
});
});
}
}
// Handle navigation
function initNavigation() {
// Update active nav item on page change
document.addEventListener('htmx:pushedIntoHistory', (event) => {
const path = event.detail.path;
updateActiveNav(path);
});
// Handle browser back/forward
window.addEventListener('popstate', (event) => {
updateActiveNav(window.location.pathname);
});
// Initialize theme if ThemeManager exists
function initTheme() {
if (window.ThemeManager) {
ThemeManager.init();
}
}
// Update active navigation item
function updateActiveNav(path) {
document.querySelectorAll('.nav-item, .app-item').forEach(item => {
const href = item.getAttribute('href');
if (href === path || (path === '/' && href === '/chat')) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
}
// Main initialization
function init() {
console.log("Initializing HTMX application...");
// Initialize HTMX
initHTMX();
// Initialize navigation
initNavigation();
// Initialize keyboard shortcuts
function initKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// Send message on Enter (when in input)
if (e.key === 'Enter' && !e.shiftKey) {
const input = document.getElementById('messageInput');
if (input && document.activeElement === input) {
e.preventDefault();
const form = input.closest('form');
if (form) {
htmx.trigger(form, 'submit');
}
}
}
// Focus input on /
if (e.key === '/' && document.activeElement.tagName !== 'INPUT') {
e.preventDefault();
const input = document.getElementById('messageInput');
if (input) input.focus();
}
// Escape to blur input
if (e.key === 'Escape') {
const input = document.getElementById('messageInput');
if (input && document.activeElement === input) {
input.blur();
}
}
});
}
initKeyboardShortcuts();
// Initialize scroll behavior
function initScrollBehavior() {
const scrollBtn = document.getElementById('scrollToBottom');
const messages = document.getElementById('messages');
initScrollBehavior();
if (scrollBtn && messages) {
// Show/hide scroll button
messages.addEventListener('scroll', () => {
const isAtBottom = messages.scrollHeight - messages.scrollTop <= messages.clientHeight + 100;
scrollBtn.style.display = isAtBottom ? 'none' : 'flex';
});
// Initialize theme
initTheme();
// Scroll to bottom on click
scrollBtn.addEventListener('click', () => {
messages.scrollTo({
top: messages.scrollHeight,
behavior: 'smooth'
});
});
}
}
// Set initial active nav
updateActiveNav(window.location.pathname);
// Initialize theme if ThemeManager exists
function initTheme() {
if (window.ThemeManager) {
ThemeManager.init();
}
}
console.log("HTMX application initialized");
}
// Main initialization
function init() {
console.log('Initializing HTMX application...');
// Wait for DOM and HTMX to be ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
// Initialize HTMX
initHTMX();
// Initialize navigation
initNavigation();
// Initialize keyboard shortcuts
initKeyboardShortcuts();
// Initialize scroll behavior
initScrollBehavior();
// Initialize theme
initTheme();
// Set initial active nav
updateActiveNav(window.location.pathname);
console.log('HTMX application initialized');
}
// Wait for DOM and HTMX to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Expose public API
window.BotServerApp = {
showNotification,
appendMessage,
updateConnectionStatus,
config
};
// Expose public API
window.BotServerApp = {
showNotification,
appendMessage,
updateConnectionStatus,
config,
};
})();

View file

@ -12,7 +12,7 @@
<!-- Folder List -->
<div class="folders-section">
<div class="nav-item active" data-folder="inbox" hx-get="/api/email/list?folder=inbox" hx-target="#mail-list" hx-swap="innerHTML">
<div class="nav-item active" data-folder="inbox" hx-get="/ui/email/list?folder=inbox" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/>
<path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>
@ -20,20 +20,20 @@
<span>Inbox</span>
<span class="folder-badge unread" id="inbox-count">0</span>
</div>
<div class="nav-item" data-folder="starred" hx-get="/api/email/list?folder=starred" hx-target="#mail-list" hx-swap="innerHTML">
<div class="nav-item" data-folder="starred" hx-get="/ui/email/list?folder=starred" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg>
<span>Starred</span>
</div>
<div class="nav-item" data-folder="sent" hx-get="/api/email/list?folder=sent" hx-target="#mail-list" hx-swap="innerHTML">
<div class="nav-item" data-folder="sent" hx-get="/ui/email/list?folder=sent" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"/>
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg>
<span>Sent</span>
</div>
<div class="nav-item" data-folder="scheduled" hx-get="/api/email/list?folder=scheduled" hx-target="#mail-list" hx-swap="innerHTML">
<div class="nav-item" data-folder="scheduled" hx-get="/ui/email/list?folder=scheduled" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
@ -41,7 +41,7 @@
<span>Scheduled</span>
<span class="folder-badge" id="scheduled-count">0</span>
</div>
<div class="nav-item" data-folder="drafts" hx-get="/api/email/list?folder=drafts" hx-target="#mail-list" hx-swap="innerHTML">
<div class="nav-item" data-folder="drafts" hx-get="/ui/email/list?folder=drafts" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
@ -58,7 +58,7 @@
</svg>
<span>Tracking</span>
</div>
<div class="nav-item" data-folder="spam" hx-get="/api/email/list?folder=spam" hx-target="#mail-list" hx-swap="innerHTML">
<div class="nav-item" data-folder="spam" hx-get="/ui/email/list?folder=spam" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
@ -66,7 +66,7 @@
</svg>
<span>Spam</span>
</div>
<div class="nav-item" data-folder="trash" hx-get="/api/email/list?folder=trash" hx-target="#mail-list" hx-swap="innerHTML">
<div class="nav-item" data-folder="trash" hx-get="/ui/email/list?folder=trash" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
@ -87,7 +87,7 @@
</button>
</div>
<div id="accounts-list"
hx-get="/api/email/accounts"
hx-get="/ui/email/accounts"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Accounts loaded here -->
@ -105,7 +105,7 @@
</svg>
</button>
</div>
<div id="labels-list" hx-get="/api/email/labels" hx-trigger="load" hx-swap="innerHTML">
<div id="labels-list" hx-get="/ui/email/labels" hx-trigger="load" hx-swap="innerHTML">
<div class="label-item" style="--label-color: #ef4444;">
<span class="label-dot"></span>
<span>Important</span>
@ -169,7 +169,7 @@
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input type="text" placeholder="Search emails..." id="email-search"
hx-get="/api/email/search" hx-trigger="keyup changed delay:300ms"
hx-get="/ui/email/search" hx-trigger="keyup changed delay:300ms"
hx-target="#mail-list" hx-include="this" name="q"/>
</div>
<button class="icon-btn" onclick="refreshMailList()" title="Refresh">
@ -212,7 +212,7 @@
</button>
</div>
<div id="mail-list" hx-get="/api/email/list?folder=inbox" hx-trigger="load" hx-swap="innerHTML">
<div id="mail-list" hx-get="/ui/email/list?folder=inbox" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-state">
<div class="spinner"></div>
<p>Loading emails...</p>
@ -421,7 +421,7 @@
</svg>
</button>
</div>
<div class="templates-list" id="templates-list" hx-get="/api/email/templates" hx-trigger="load" hx-swap="innerHTML">
<div class="templates-list" id="templates-list" hx-get="/ui/email/templates" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-state"><div class="spinner"></div></div>
</div>
<div class="modal-footer">
@ -443,7 +443,7 @@
</svg>
</button>
</div>
<div class="signatures-list" id="signatures-list" hx-get="/api/email/signatures" hx-trigger="load" hx-swap="innerHTML">
<div class="signatures-list" id="signatures-list" hx-get="/ui/email/signatures" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-state"><div class="spinner"></div></div>
</div>
<div class="modal-footer">
@ -465,7 +465,7 @@
</svg>
</button>
</div>
<div class="rules-list" id="rules-list" hx-get="/api/email/rules" hx-trigger="load" hx-swap="innerHTML">
<div class="rules-list" id="rules-list" hx-get="/ui/email/rules" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-state"><div class="spinner"></div></div>
</div>
<div class="modal-footer">
@ -487,7 +487,7 @@
</svg>
</button>
</div>
<form id="autoresponder-form" hx-post="/api/email/auto-responder" hx-swap="none">
<form id="autoresponder-form" hx-post="/ui/email/auto-responder" hx-swap="none">
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" name="enabled" id="autoresponder-enabled"/>
@ -1013,19 +1013,28 @@
to { transform: rotate(360deg); }
}
/* Modals */
.modal {
/* Modals - dialog elements are hidden by default */
dialog.modal {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
background: transparent;
border: none;
padding: 1rem;
z-index: 1000;
max-width: 100vw;
max-height: 100vh;
width: 100%;
height: 100%;
display: none;
}
dialog.modal[open] {
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal::backdrop {
dialog.modal::backdrop {
background: rgba(0, 0, 0, 0.5);
}

View file

@ -1,75 +1,78 @@
/* Logs page JavaScript */
// Logs State
let isStreaming = true;
let autoScroll = true;
let logCounts = { debug: 0, info: 0, warn: 0, error: 0, fatal: 0 };
let searchDebounceTimer = null;
let currentFilters = {
level: 'all',
service: 'all',
search: ''
};
let logsWs = null;
// Logs State - guard against duplicate declarations on HTMX reload
if (typeof window.logsModuleInitialized === "undefined") {
window.logsModuleInitialized = true;
var isStreaming = true;
var autoScroll = true;
var logCounts = { debug: 0, info: 0, warn: 0, error: 0, fatal: 0 };
var searchDebounceTimer = null;
var currentFilters = {
level: "all",
service: "all",
search: "",
};
var logsWs = null;
}
function initLogsWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
logsWs = new WebSocket(`${protocol}//${window.location.host}/ws/logs`);
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
logsWs = new WebSocket(`${protocol}//${window.location.host}/ws/logs`);
logsWs.onopen = function() {
updateLogsConnectionStatus('connected', 'Connected');
};
logsWs.onopen = function () {
updateLogsConnectionStatus("connected", "Connected");
};
logsWs.onclose = function() {
updateLogsConnectionStatus('disconnected', 'Disconnected');
// Reconnect after 3 seconds
setTimeout(initLogsWebSocket, 3000);
};
logsWs.onclose = function () {
updateLogsConnectionStatus("disconnected", "Disconnected");
// Reconnect after 3 seconds
setTimeout(initLogsWebSocket, 3000);
};
logsWs.onerror = function() {
updateLogsConnectionStatus('disconnected', 'Error');
};
logsWs.onerror = function () {
updateLogsConnectionStatus("disconnected", "Error");
};
logsWs.onmessage = function(event) {
if (!isStreaming) return;
logsWs.onmessage = function (event) {
if (!isStreaming) return;
try {
const logData = JSON.parse(event.data);
appendLog(logData);
} catch (e) {
console.error('Failed to parse log message:', e);
}
};
try {
const logData = JSON.parse(event.data);
appendLog(logData);
} catch (e) {
console.error("Failed to parse log message:", e);
}
};
}
function updateLogsConnectionStatus(status, text) {
const statusEl = document.getElementById('connection-status');
if (statusEl) {
statusEl.className = `connection-status ${status}`;
statusEl.querySelector('.status-text').textContent = text;
}
const statusEl = document.getElementById("connection-status");
if (statusEl) {
statusEl.className = `connection-status ${status}`;
statusEl.querySelector(".status-text").textContent = text;
}
}
function appendLog(log) {
const stream = document.getElementById('log-stream');
if (!stream) return;
const stream = document.getElementById("log-stream");
if (!stream) return;
const placeholder = stream.querySelector('.log-placeholder');
if (placeholder) {
placeholder.remove();
}
const placeholder = stream.querySelector(".log-placeholder");
if (placeholder) {
placeholder.remove();
}
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.dataset.level = log.level || 'info';
entry.dataset.service = log.service || 'unknown';
entry.dataset.id = log.id || Date.now();
const entry = document.createElement("div");
entry.className = "log-entry";
entry.dataset.level = log.level || "info";
entry.dataset.service = log.service || "unknown";
entry.dataset.id = log.id || Date.now();
entry.innerHTML = `
entry.innerHTML = `
<span class="log-timestamp">${formatLogTimestamp(log.timestamp)}</span>
<span class="log-level">${(log.level || 'INFO').toUpperCase()}</span>
<span class="log-service">${log.service || 'unknown'}</span>
<span class="log-message">${escapeLogHtml(log.message || '')}</span>
<span class="log-level">${(log.level || "INFO").toUpperCase()}</span>
<span class="log-service">${log.service || "unknown"}</span>
<span class="log-message">${escapeLogHtml(log.message || "")}</span>
<button class="log-expand" onclick="expandLog(this)" title="View details">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"></polyline>
@ -77,134 +80,142 @@ function appendLog(log) {
</button>
`;
// Store full log data for detail view
entry._logData = log;
// Store full log data for detail view
entry._logData = log;
// Apply current filters
if (!matchesLogFilters(entry)) {
entry.classList.add('hidden');
}
// Apply current filters
if (!matchesLogFilters(entry)) {
entry.classList.add("hidden");
}
stream.appendChild(entry);
stream.appendChild(entry);
// Update counts
const level = log.level || 'info';
if (logCounts[level] !== undefined) {
logCounts[level]++;
const countEl = document.getElementById(`${level}-count`);
if (countEl) countEl.textContent = logCounts[level];
}
const totalEl = document.getElementById('total-count');
if (totalEl) {
totalEl.textContent = Object.values(logCounts).reduce((a, b) => a + b, 0);
}
// Update counts
const level = log.level || "info";
if (logCounts[level] !== undefined) {
logCounts[level]++;
const countEl = document.getElementById(`${level}-count`);
if (countEl) countEl.textContent = logCounts[level];
}
const totalEl = document.getElementById("total-count");
if (totalEl) {
totalEl.textContent = Object.values(logCounts).reduce((a, b) => a + b, 0);
}
// Auto-scroll to bottom
if (autoScroll) {
stream.scrollTop = stream.scrollHeight;
}
// Auto-scroll to bottom
if (autoScroll) {
stream.scrollTop = stream.scrollHeight;
}
// Limit log entries to prevent memory issues
const maxEntries = 1000;
while (stream.children.length > maxEntries) {
const removed = stream.firstChild;
if (removed._logData) {
const removedLevel = removed._logData.level || 'info';
if (logCounts[removedLevel] > 0) {
logCounts[removedLevel]--;
}
}
stream.removeChild(removed);
// Limit log entries to prevent memory issues
const maxEntries = 1000;
while (stream.children.length > maxEntries) {
const removed = stream.firstChild;
if (removed._logData) {
const removedLevel = removed._logData.level || "info";
if (logCounts[removedLevel] > 0) {
logCounts[removedLevel]--;
}
}
stream.removeChild(removed);
}
}
function formatLogTimestamp(timestamp) {
if (!timestamp) return '--';
const date = new Date(timestamp);
return date.toISOString().replace('T', ' ').slice(0, 23);
if (!timestamp) return "--";
const date = new Date(timestamp);
return date.toISOString().replace("T", " ").slice(0, 23);
}
function escapeLogHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
function matchesLogFilters(entry) {
// Level filter
if (currentFilters.level !== 'all' && entry.dataset.level !== currentFilters.level) {
return false;
}
// Level filter
if (
currentFilters.level !== "all" &&
entry.dataset.level !== currentFilters.level
) {
return false;
}
// Service filter
if (currentFilters.service !== 'all' && entry.dataset.service !== currentFilters.service) {
return false;
}
// Service filter
if (
currentFilters.service !== "all" &&
entry.dataset.service !== currentFilters.service
) {
return false;
}
// Search filter
if (currentFilters.search) {
const text = entry.textContent.toLowerCase();
if (!text.includes(currentFilters.search.toLowerCase())) {
return false;
}
// Search filter
if (currentFilters.search) {
const text = entry.textContent.toLowerCase();
if (!text.includes(currentFilters.search.toLowerCase())) {
return false;
}
}
return true;
return true;
}
function applyLogFilters() {
currentFilters.level = document.getElementById('log-level-filter')?.value || 'all';
currentFilters.service = document.getElementById('service-filter')?.value || 'all';
currentFilters.level =
document.getElementById("log-level-filter")?.value || "all";
currentFilters.service =
document.getElementById("service-filter")?.value || "all";
const entries = document.querySelectorAll('.log-entry');
entries.forEach(entry => {
if (matchesLogFilters(entry)) {
entry.classList.remove('hidden');
} else {
entry.classList.add('hidden');
}
});
const entries = document.querySelectorAll(".log-entry");
entries.forEach((entry) => {
if (matchesLogFilters(entry)) {
entry.classList.remove("hidden");
} else {
entry.classList.add("hidden");
}
});
}
function debounceLogSearch(value) {
clearTimeout(searchDebounceTimer);
searchDebounceTimer = setTimeout(() => {
currentFilters.search = value;
applyLogFilters();
}, 300);
clearTimeout(searchDebounceTimer);
searchDebounceTimer = setTimeout(() => {
currentFilters.search = value;
applyLogFilters();
}, 300);
}
function toggleStream() {
isStreaming = !isStreaming;
const btn = document.getElementById('stream-toggle');
if (!btn) return;
isStreaming = !isStreaming;
const btn = document.getElementById("stream-toggle");
if (!btn) return;
if (isStreaming) {
btn.classList.remove('paused');
btn.innerHTML = `
if (isStreaming) {
btn.classList.remove("paused");
btn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="6" y="4" width="4" height="16"></rect>
<rect x="14" y="4" width="4" height="16"></rect>
</svg>
<span>Pause</span>
`;
} else {
btn.classList.add('paused');
btn.innerHTML = `
} else {
btn.classList.add("paused");
btn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>
<span>Resume</span>
`;
}
}
}
function clearLogs() {
if (confirm('Are you sure you want to clear all logs?')) {
const stream = document.getElementById('log-stream');
if (!stream) return;
if (confirm("Are you sure you want to clear all logs?")) {
const stream = document.getElementById("log-stream");
if (!stream) return;
stream.innerHTML = `
stream.innerHTML = `
<div class="log-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
@ -218,119 +229,129 @@ function clearLogs() {
</div>
`;
// Reset counts
logCounts = { debug: 0, info: 0, warn: 0, error: 0, fatal: 0 };
Object.keys(logCounts).forEach(level => {
const el = document.getElementById(`${level}-count`);
if (el) el.textContent = '0';
});
const totalEl = document.getElementById('total-count');
if (totalEl) totalEl.textContent = '0';
}
// Reset counts
logCounts = { debug: 0, info: 0, warn: 0, error: 0, fatal: 0 };
Object.keys(logCounts).forEach((level) => {
const el = document.getElementById(`${level}-count`);
if (el) el.textContent = "0";
});
const totalEl = document.getElementById("total-count");
if (totalEl) totalEl.textContent = "0";
}
}
function downloadLogs() {
const entries = document.querySelectorAll('.log-entry');
let logs = [];
const entries = document.querySelectorAll(".log-entry");
let logs = [];
entries.forEach(entry => {
if (entry._logData) {
logs.push(entry._logData);
}
});
entries.forEach((entry) => {
if (entry._logData) {
logs.push(entry._logData);
}
});
const blob = new Blob([JSON.stringify(logs, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `logs-${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
const blob = new Blob([JSON.stringify(logs, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `logs-${new Date().toISOString().slice(0, 19).replace(/[T:]/g, "-")}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function scrollToTop() {
const stream = document.getElementById('log-stream');
if (stream) {
stream.scrollTop = 0;
autoScroll = false;
updateLogScrollButtons();
}
const stream = document.getElementById("log-stream");
if (stream) {
stream.scrollTop = 0;
autoScroll = false;
updateLogScrollButtons();
}
}
function scrollToBottom() {
const stream = document.getElementById('log-stream');
if (stream) {
stream.scrollTop = stream.scrollHeight;
autoScroll = true;
updateLogScrollButtons();
}
const stream = document.getElementById("log-stream");
if (stream) {
stream.scrollTop = stream.scrollHeight;
autoScroll = true;
updateLogScrollButtons();
}
}
function updateLogScrollButtons() {
const topBtn = document.getElementById('scroll-top-btn');
const bottomBtn = document.getElementById('scroll-bottom-btn');
if (topBtn) topBtn.classList.toggle('active', !autoScroll);
if (bottomBtn) bottomBtn.classList.toggle('active', autoScroll);
const topBtn = document.getElementById("scroll-top-btn");
const bottomBtn = document.getElementById("scroll-bottom-btn");
if (topBtn) topBtn.classList.toggle("active", !autoScroll);
if (bottomBtn) bottomBtn.classList.toggle("active", autoScroll);
}
function expandLog(btn) {
const entry = btn.closest('.log-entry');
const logData = entry._logData || {
timestamp: entry.querySelector('.log-timestamp').textContent,
level: entry.dataset.level,
service: entry.dataset.service,
message: entry.querySelector('.log-message').textContent
};
const entry = btn.closest(".log-entry");
const logData = entry._logData || {
timestamp: entry.querySelector(".log-timestamp").textContent,
level: entry.dataset.level,
service: entry.dataset.service,
message: entry.querySelector(".log-message").textContent,
};
const panel = document.getElementById('log-detail-panel');
const content = document.getElementById('log-detail-content');
if (!panel || !content) return;
const panel = document.getElementById("log-detail-panel");
const content = document.getElementById("log-detail-content");
if (!panel || !content) return;
content.innerHTML = `
content.innerHTML = `
<div class="detail-section">
<div class="detail-label">Timestamp</div>
<div class="detail-value">${logData.timestamp || '--'}</div>
<div class="detail-value">${logData.timestamp || "--"}</div>
</div>
<div class="detail-section">
<div class="detail-label">Level</div>
<div class="detail-value">${(logData.level || 'info').toUpperCase()}</div>
<div class="detail-value">${(logData.level || "info").toUpperCase()}</div>
</div>
<div class="detail-section">
<div class="detail-label">Service</div>
<div class="detail-value">${logData.service || 'unknown'}</div>
<div class="detail-value">${logData.service || "unknown"}</div>
</div>
<div class="detail-section">
<div class="detail-label">Message</div>
<div class="detail-value">${escapeLogHtml(logData.message || '')}</div>
<div class="detail-value">${escapeLogHtml(logData.message || "")}</div>
</div>
${logData.stack ? `
${
logData.stack
? `
<div class="detail-section">
<div class="detail-label">Stack Trace</div>
<div class="detail-value">${escapeLogHtml(logData.stack)}</div>
</div>
` : ''}
${logData.context ? `
`
: ""
}
${
logData.context
? `
<div class="detail-section">
<div class="detail-label">Context</div>
<div class="detail-value">${escapeLogHtml(JSON.stringify(logData.context, null, 2))}</div>
</div>
` : ''}
`
: ""
}
`;
panel.classList.add('open');
panel.classList.add("open");
}
function closeLogDetail() {
const panel = document.getElementById('log-detail-panel');
if (panel) panel.classList.remove('open');
const panel = document.getElementById("log-detail-panel");
if (panel) panel.classList.remove("open");
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
// Initialize WebSocket connection if on logs page
if (document.getElementById('log-stream')) {
initLogsWebSocket();
}
document.addEventListener("DOMContentLoaded", function () {
// Initialize WebSocket connection if on logs page
if (document.getElementById("log-stream")) {
initLogsWebSocket();
}
});

View file

@ -1,67 +1,75 @@
/* Monitoring module - shared/base JavaScript */
function setActiveNav(element) {
document.querySelectorAll('.monitoring-nav .nav-item').forEach(item => {
item.classList.remove('active');
});
element.classList.add('active');
document.querySelectorAll(".monitoring-nav .nav-item").forEach((item) => {
item.classList.remove("active");
});
element.classList.add("active");
// Update page title
const title = element.querySelector('span:not(.alert-badge):not(.health-indicator)').textContent;
document.getElementById('page-title').textContent = title;
// Update page title
const title = element.querySelector(
"span:not(.alert-badge):not(.health-indicator)",
).textContent;
document.getElementById("page-title").textContent = title;
}
function updateTimeRange(range) {
// Store selected time range
localStorage.setItem('monitoring-time-range', range);
// Store selected time range
localStorage.setItem("monitoring-time-range", range);
// Trigger refresh of current view
htmx.trigger('#monitoring-content', 'refresh');
// Trigger refresh of current view
htmx.trigger("#monitoring-content", "refresh");
}
function refreshMonitoring() {
htmx.trigger('#monitoring-content', 'refresh');
htmx.trigger("#monitoring-content", "refresh");
// Visual feedback
const btn = event.currentTarget;
btn.classList.add('active');
setTimeout(() => btn.classList.remove('active'), 500);
// Visual feedback
const btn = event.currentTarget;
btn.classList.add("active");
setTimeout(() => btn.classList.remove("active"), 500);
}
let autoRefresh = true;
function toggleAutoRefresh() {
autoRefresh = !autoRefresh;
const btn = document.getElementById('auto-refresh-btn');
btn.classList.toggle('active', autoRefresh);
// Guard against duplicate declarations on HTMX reload
if (typeof window.monitoringModuleInitialized === "undefined") {
window.monitoringModuleInitialized = true;
var autoRefresh = true;
}
if (autoRefresh) {
// Re-enable polling by refreshing the page content
htmx.trigger('#monitoring-content', 'refresh');
}
function toggleAutoRefresh() {
autoRefresh = !autoRefresh;
const btn = document.getElementById("auto-refresh-btn");
btn.classList.toggle("active", autoRefresh);
if (autoRefresh) {
// Re-enable polling by refreshing the page content
htmx.trigger("#monitoring-content", "refresh");
}
}
function exportData() {
const timeRange = document.getElementById('time-range').value;
window.open(`/api/monitoring/export?range=${timeRange}`, '_blank');
const timeRange = document.getElementById("time-range").value;
window.open(`/api/monitoring/export?range=${timeRange}`, "_blank");
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
// Restore time range preference
const savedRange = localStorage.getItem('monitoring-time-range');
if (savedRange) {
const timeRangeEl = document.getElementById('time-range');
if (timeRangeEl) timeRangeEl.value = savedRange;
}
document.addEventListener("DOMContentLoaded", function () {
// Restore time range preference
const savedRange = localStorage.getItem("monitoring-time-range");
if (savedRange) {
const timeRangeEl = document.getElementById("time-range");
if (timeRangeEl) timeRangeEl.value = savedRange;
}
// Set auto-refresh button state
const autoRefreshBtn = document.getElementById('auto-refresh-btn');
if (autoRefreshBtn) autoRefreshBtn.classList.toggle('active', autoRefresh);
// Set auto-refresh button state
const autoRefreshBtn = document.getElementById("auto-refresh-btn");
if (autoRefreshBtn) autoRefreshBtn.classList.toggle("active", autoRefresh);
});
// Handle HTMX events for loading states
document.body.addEventListener('htmx:beforeRequest', function(evt) {
if (evt.target.id === 'monitoring-content') {
evt.target.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Loading...</p></div>';
}
document.body.addEventListener("htmx:beforeRequest", function (evt) {
if (evt.target.id === "monitoring-content") {
evt.target.innerHTML =
'<div class="loading-state"><div class="spinner"></div><p>Loading...</p></div>';
}
});

View file

@ -6,9 +6,10 @@
/* Research Container */
.research-container {
display: flex;
height: calc(100vh - 60px);
background: var(--background);
color: var(--foreground);
height: 100%;
min-height: 0;
background: var(--background, var(--bg-primary, #0a0a0f));
color: var(--foreground, var(--text-primary, #ffffff));
}
/* Sidebar */
@ -398,12 +399,22 @@
animation: bounce 1.4s infinite ease-in-out both;
}
.indicator-dots span:nth-child(1) { animation-delay: -0.32s; }
.indicator-dots span:nth-child(2) { animation-delay: -0.16s; }
.indicator-dots span:nth-child(1) {
animation-delay: -0.32s;
}
.indicator-dots span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
.indicator-text {
@ -557,7 +568,7 @@
background: var(--muted);
padding: 2px 6px;
border-radius: 4px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-family: "JetBrains Mono", "Fira Code", monospace;
font-size: 13px;
}

379
ui/suite/tasks/intents.html Normal file
View file

@ -0,0 +1,379 @@
<!-- Intents Dashboard - Sentient Theme -->
<!-- AI-powered intent tracking and decision management -->
<div class="intents-dashboard">
<!-- Dashboard Header -->
<div class="dashboard-header">
<div class="dashboard-title">
<h1>Dashboard</h1>
<span class="dashboard-subtitle">Analytics</span>
</div>
<div class="dashboard-actions">
<div class="search-bar">
<svg
class="search-bar-icon"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
<input
type="text"
class="search-bar-input"
placeholder="Search Intents"
/>
</div>
<div class="profile-selector">
<div class="profile-avatar">A</div>
<span class="profile-name">Profile 1</span>
<svg
class="profile-dropdown-icon"
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</div>
<div class="notification-indicator">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg>
<span class="notification-count">3</span>
</div>
<button class="btn-new-intent">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
New Intent
</button>
<button class="icon-button" title="Settings">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="3" />
<path
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
/>
</svg>
</button>
</div>
</div>
<!-- Status Filters -->
<div class="status-filters">
<button class="status-filter active">
<svg
class="icon"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="20 6 9 17 4 12" />
</svg>
Complete
<span class="count">8</span>
</button>
<button class="status-filter">
<svg
class="icon"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" />
<polygon points="10 8 16 12 10 16 10 8" />
</svg>
Active Intents
<span class="count">12</span>
</button>
<button class="status-filter">
<svg
class="icon"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<line x1="9" y1="9" x2="15" y2="15" />
<line x1="15" y1="9" x2="9" y2="15" />
</svg>
Awaiting Decision
<span class="count">5</span>
</button>
<button class="status-filter">
<svg
class="icon"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="6" y="4" width="4" height="16" />
<rect x="14" y="4" width="4" height="16" />
</svg>
Paused
<span class="count">2</span>
</button>
<button class="status-filter">
<svg
class="icon"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
Blocked/Issues
<span class="count">1</span>
</button>
<div class="time-saved-indicator">
<svg
class="icon"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
Active Time Saved: 23.5 hrs this week
</div>
</div>
<!-- Main Content Area -->
<div class="dashboard-layout">
<!-- Left Column: Intent Cards -->
<div class="dashboard-main">
<div class="intents-grid">
<!-- Intent Card 1: Active -->
<article class="intent-card">
<div class="intent-card-header">
<h3 class="intent-title">
Launch Q4 Campaign for Verse
</h3>
</div>
<div class="intent-progress">
<div class="progress-bar">
<div
class="progress-bar-fill"
style="width: 75%"
></div>
</div>
<span class="progress-text">5/7 Steps</span>
<span class="progress-text">75%</span>
</div>
<div class="intent-status-line">
<span class="intent-status-label">STATUS</span>
<span class="intent-status-badge active">
<svg
width="10"
height="10"
viewBox="0 0 24 24"
fill="currentColor"
>
<circle cx="12" cy="12" r="6" />
</svg>
Active
</span>
</div>
<div class="intent-description">
<strong>Budget Allocation: 50k</strong><br />
<span class="pending"
>Distributing budget in media channels</span
>
<span
class="intent-meta-tag"
style="margin-left: 8px; display: inline-flex"
>Est: 15min ⏱</span
>
</div>
<div class="intent-status-line" style="margin-top: 16px">
<span class="intent-status-label">INTEGRITY</span>
</div>
<div class="intent-meta">
<span class="intent-meta-tag">Started 2h ago</span>
<span class="intent-meta-tag">Due Nov 15th</span>
<span class="intent-health good"
>Intent Health 90%</span
>
</div>
<a href="#" class="intent-detailed-link">Detailed View</a>
</article>
<!-- Intent Card 2: Awaiting Decision -->
<article class="intent-card selected">
<div class="intent-card-header">
<h3 class="intent-title">
Make a financial CRM for Deloitte
</h3>
</div>
<div class="intent-progress">
<div class="progress-bar">
<div
class="progress-bar-fill warning"
style="width: 45%"
></div>
</div>
<span class="progress-text warning">25/60 Steps</span>
<span class="progress-text warning">45%</span>
</div>
<div class="intent-status-line">
<span class="intent-status-label">STATUS</span>
<span class="intent-status-badge awaiting">
<svg
width="10"
height="10"
viewBox="0 0 24 24"
fill="currentColor"
>
<rect
x="3"
y="3"
width="18"
height="18"
rx="2"
/>
</svg>
Awaiting Decision
</span>
</div>
<div class="intent-description">
<strong>Implement User Authentication System</strong
><br />
<span class="pending"
>The authentication system needs to balance
security...</span
>
<span
class="intent-meta-tag"
style="margin-left: 8px; display: inline-flex"
>Pending ⏸</span
>
</div>
<div class="intent-status-line" style="margin-top: 16px">
<span class="intent-status-label">INTEGRITY</span>
</div>
<div class="intent-meta">
<span class="intent-meta-tag">Started 5d ago</span>
<span class="intent-meta-tag">Due Nov 30th</span>
<span class="intent-health warning"
>Intent Health 85%</span
>
</div>
</article>
</div>
</div>
<!-- Right Column: Detail Panel -->
<div class="dashboard-sidebar">
<div class="detail-panel">
<!-- Detail Header -->
<div class="detail-panel-header">
<div class="detail-panel-nav">
<button class="nav-btn" title="Previous">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="15 18 9 12 15 6" />
</svg>
</button>
</div>
<h2>Make a financial CRM for Deloitte</h2>
<div class="detail-panel-actions">
<span class="intent-status-badge awaiting">
<svg
width="10"
height="10"
viewBox="0 0 24 24"
fill="currentColor"
>
<rect
x="3"
y="3"
width="18"
height="18"
rx="2"
/>
</svg>
Awaiting Decision
</span>
<button class="action-btn-pause" title="Pause Intent">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="6" y="4" width="4" height="16" />
<rect x="14" y="4" width="4" height="16" />
</svg>
</button>
</div>
</div>
<!-- Progress Bar in Header -->
<div class="detail-status">
<div class="detail-progress-bar" style="flex: 1"></div>
</div>
</div>
</div>
</div>
</div>

View file

@ -10,10 +10,14 @@
.tasks-app {
display: flex;
flex-direction: column;
min-height: 100vh;
background: var(--sentient-bg-primary);
color: var(--sentient-text-primary);
font-family: var(--sentient-font-family);
height: 100%;
min-height: 0;
background: var(--bg);
color: var(--text);
font-family: var(
--font-family,
system-ui, -apple-system, sans-serif
);
}
/* =============================================================================
@ -25,8 +29,35 @@
align-items: center;
justify-content: space-between;
padding: 16px 24px;
background: var(--sentient-bg-secondary);
border-bottom: 1px solid var(--sentient-border);
background: var(--surface);
border-bottom: 1px solid var(--border);
}
/* New Intent Button */
.btn-new-intent {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: var(--primary);
color: var(--accent-foreground, #000000);
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
margin-left: auto;
}
.btn-new-intent:hover {
background: var(--primary-hover);
box-shadow: 0 0 20px var(--primary-light);
}
.btn-new-intent svg {
width: 16px;
height: 16px;
}
.topbar-left {
@ -41,13 +72,25 @@
gap: 10px;
font-size: 18px;
font-weight: 700;
color: var(--sentient-text-primary);
color: var(--text);
}
/* Tasks List Title Row */
.tasks-list-title {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.tasks-list-title h1 {
margin: 0;
}
.topbar-logo-icon {
width: 32px;
height: 32px;
background: var(--sentient-accent);
background: var(--primary);
border-radius: 8px;
display: flex;
align-items: center;
@ -65,9 +108,9 @@
padding: 8px 16px;
background: transparent;
border: none;
border-radius: var(--sentient-radius-sm);
color: var(--sentient-text-secondary);
font-family: var(--sentient-font-family);
border-radius: 6px;
color: var(--text-secondary);
font-family: system-ui, -apple-system, sans-serif;
font-size: 14px;
font-weight: 500;
cursor: pointer;
@ -75,13 +118,13 @@
}
.topbar-nav-item:hover {
background: var(--sentient-bg-tertiary);
color: var(--sentient-text-primary);
background: var(--surface-hover);
color: var(--text);
}
.topbar-nav-item.active {
background: var(--sentient-bg-tertiary);
color: var(--sentient-accent);
background: var(--surface-hover);
color: var(--primary);
}
.topbar-center {
@ -100,7 +143,7 @@
left: 14px;
top: 50%;
transform: translateY(-50%);
color: var(--sentient-text-muted);
color: var(--text-secondary);
font-size: 16px;
pointer-events: none;
}
@ -108,22 +151,22 @@
.topbar-search-input {
width: 100%;
padding: 10px 16px 10px 44px;
background: var(--sentient-bg-tertiary);
border: 1px solid var(--sentient-border);
border-radius: var(--sentient-radius-md);
color: var(--sentient-text-primary);
background: var(--surface-hover);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 14px;
transition: all 0.2s ease;
}
.topbar-search-input::placeholder {
color: var(--sentient-text-muted);
color: var(--text-secondary);
}
.topbar-search-input:focus {
outline: none;
border-color: var(--sentient-accent);
box-shadow: 0 0 0 3px var(--sentient-accent-dim);
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-light);
}
.topbar-right {
@ -139,9 +182,9 @@
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid var(--sentient-border);
border-radius: var(--sentient-radius-sm);
color: var(--sentient-text-secondary);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-secondary);
font-size: 18px;
cursor: pointer;
transition: all 0.2s ease;
@ -149,8 +192,8 @@
}
.topbar-icon-btn:hover {
background: var(--sentient-bg-tertiary);
color: var(--sentient-text-primary);
background: var(--surface-hover);
color: var(--text);
}
.topbar-icon-btn .notification-dot {
@ -159,7 +202,7 @@
right: 8px;
width: 8px;
height: 8px;
background: var(--sentient-error);
background: var(--error);
border-radius: 50%;
}
@ -168,21 +211,21 @@
align-items: center;
gap: 10px;
padding: 6px 12px 6px 6px;
background: var(--sentient-bg-tertiary);
border: 1px solid var(--sentient-border);
border-radius: var(--sentient-radius-md);
background: var(--surface-hover);
border: 1px solid var(--border);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.topbar-profile:hover {
border-color: var(--sentient-border-hover);
border-color: var(--border);
}
.topbar-avatar {
width: 28px;
height: 28px;
background: linear-gradient(135deg, var(--sentient-accent), #a5d622);
background: linear-gradient(135deg, var(--primary), #a5d622);
border-radius: 6px;
display: flex;
align-items: center;
@ -195,11 +238,11 @@
.topbar-profile-name {
font-size: 13px;
font-weight: 500;
color: var(--sentient-text-primary);
color: var(--text);
}
.topbar-profile-arrow {
color: var(--sentient-text-muted);
color: var(--text-secondary);
font-size: 12px;
}
@ -211,6 +254,7 @@
display: flex;
flex: 1;
overflow: hidden;
min-height: 0;
}
/* =============================================================================
@ -221,13 +265,17 @@
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid var(--sentient-border);
border-right: 1px solid var(--border);
overflow: hidden;
min-height: 0;
}
.tasks-list-header {
padding: 20px 24px;
border-bottom: 1px solid var(--sentient-border);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
position: relative;
z-index: 1;
}
.tasks-list-title {
@ -240,13 +288,13 @@
.tasks-list-title h1 {
font-size: 24px;
font-weight: 700;
color: var(--sentient-text-primary);
color: var(--text);
margin: 0;
}
.tasks-count {
font-size: 14px;
color: var(--sentient-text-muted);
color: var(--text-secondary);
}
/* Status Filter Pills */
@ -254,6 +302,8 @@
display: flex;
gap: 8px;
flex-wrap: wrap;
position: relative;
z-index: 1;
}
.status-pill {
@ -261,10 +311,10 @@
align-items: center;
gap: 8px;
padding: 8px 14px;
background: var(--sentient-bg-tertiary);
border: 1px solid var(--sentient-border);
background: var(--surface-hover);
border: 1px solid var(--border);
border-radius: 20px;
color: var(--sentient-text-secondary);
color: var(--text-secondary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
@ -272,44 +322,44 @@
}
.status-pill:hover {
border-color: var(--sentient-border-hover);
color: var(--sentient-text-primary);
border-color: var(--border);
color: var(--text);
}
.status-pill.active {
background: var(--sentient-accent-dim);
border-color: var(--sentient-accent);
color: var(--sentient-accent);
background: var(--primary-light);
border-color: var(--primary);
color: var(--primary);
}
.status-pill.complete.active {
background: rgba(34, 197, 94, 0.15);
border-color: var(--sentient-success);
color: var(--sentient-success);
border-color: var(--success);
color: var(--success);
}
.status-pill.active-intents.active {
background: rgba(59, 130, 246, 0.15);
border-color: var(--sentient-info);
color: var(--sentient-info);
border-color: var(--info);
color: var(--info);
}
.status-pill.awaiting.active {
background: rgba(245, 158, 11, 0.15);
border-color: var(--sentient-warning);
color: var(--sentient-warning);
border-color: var(--warning);
color: var(--warning);
}
.status-pill.paused.active {
background: rgba(139, 92, 246, 0.15);
border-color: var(--sentient-paused);
color: var(--sentient-paused);
border-color: var(--warning);
color: var(--warning);
}
.status-pill.blocked.active {
background: rgba(239, 68, 68, 0.15);
border-color: var(--sentient-error);
color: var(--sentient-error);
border-color: var(--error);
color: var(--error);
}
.status-pill .pill-count {
@ -333,9 +383,9 @@
/* Task Card */
.task-card {
background: var(--sentient-bg-card);
border: 1px solid var(--sentient-border);
border-radius: var(--sentient-radius-lg);
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
cursor: pointer;
@ -343,13 +393,13 @@
}
.task-card:hover {
border-color: var(--sentient-border-hover);
border-color: var(--border);
transform: translateX(4px);
}
.task-card.selected {
border-color: var(--sentient-accent);
background: var(--sentient-accent-dim);
border-color: var(--primary);
background: var(--primary-light);
}
.task-card-header {
@ -362,14 +412,14 @@
.task-card-title {
font-size: 15px;
font-weight: 600;
color: var(--sentient-text-primary);
color: var(--text);
margin: 0 0 4px 0;
line-height: 1.4;
}
.task-card-subtitle {
font-size: 13px;
color: var(--sentient-text-muted);
color: var(--text-secondary);
}
.task-card-status {
@ -383,27 +433,27 @@
.task-card-status.running {
background: rgba(59, 130, 246, 0.15);
color: var(--sentient-info);
color: var(--info);
}
.task-card-status.complete {
background: rgba(34, 197, 94, 0.15);
color: var(--sentient-success);
color: var(--success);
}
.task-card-status.awaiting {
background: rgba(245, 158, 11, 0.15);
color: var(--sentient-warning);
color: var(--warning);
}
.task-card-status.paused {
background: rgba(139, 92, 246, 0.15);
color: var(--sentient-paused);
color: var(--warning);
}
.task-card-status.blocked {
background: rgba(239, 68, 68, 0.15);
color: var(--sentient-error);
color: var(--error);
}
/* Task Progress */
@ -413,7 +463,7 @@
.task-progress-bar {
height: 6px;
background: var(--sentient-bg-tertiary);
background: var(--surface-hover);
border-radius: 3px;
overflow: hidden;
margin-bottom: 6px;
@ -421,13 +471,13 @@
.task-progress-fill {
height: 100%;
background: var(--sentient-accent);
background: var(--primary);
border-radius: 3px;
transition: width 0.3s ease;
}
.task-progress-fill.success {
background: var(--sentient-success);
background: var(--success);
}
.task-progress-info {
@ -437,12 +487,12 @@
}
.task-progress-percent {
color: var(--sentient-accent);
color: var(--primary);
font-weight: 600;
}
.task-progress-steps {
color: var(--sentient-text-muted);
color: var(--text-secondary);
}
/* Task Card Meta */
@ -451,7 +501,7 @@
align-items: center;
gap: 12px;
font-size: 12px;
color: var(--sentient-text-muted);
color: var(--text-secondary);
}
.task-card-meta-item {
@ -468,13 +518,13 @@
width: 480px;
display: flex;
flex-direction: column;
background: var(--sentient-bg-secondary);
background: var(--surface);
overflow: hidden;
}
.task-detail-header {
padding: 20px 24px;
border-bottom: 1px solid var(--sentient-border);
border-bottom: 1px solid var(--border);
}
.task-detail-title-row {
@ -487,7 +537,7 @@
.task-detail-title {
font-size: 18px;
font-weight: 600;
color: var(--sentient-text-primary);
color: var(--text);
margin: 0;
line-height: 1.4;
}
@ -503,18 +553,18 @@
display: flex;
align-items: center;
justify-content: center;
background: var(--sentient-bg-tertiary);
border: 1px solid var(--sentient-border);
border-radius: var(--sentient-radius-sm);
color: var(--sentient-text-secondary);
background: var(--surface-hover);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-secondary);
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.task-detail-action-btn:hover {
background: var(--sentient-bg-hover);
color: var(--sentient-text-primary);
background: var(--surface-hover);
color: var(--text);
}
.task-detail-meta {
@ -528,11 +578,11 @@
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--sentient-text-secondary);
color: var(--text-secondary);
}
.task-detail-meta-item .icon {
color: var(--sentient-text-muted);
color: var(--text-secondary);
}
/* Task Detail Scroll */
@ -546,7 +596,7 @@
.decision-required-section {
background: rgba(245, 158, 11, 0.08);
border: 1px solid rgba(245, 158, 11, 0.25);
border-radius: var(--sentient-radius-lg);
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
}
@ -566,21 +616,21 @@
display: flex;
align-items: center;
justify-content: center;
color: var(--sentient-warning);
color: var(--warning);
font-size: 14px;
}
.decision-required-title {
font-size: 14px;
font-weight: 600;
color: var(--sentient-warning);
color: var(--warning);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.decision-required-description {
font-size: 14px;
color: var(--sentient-text-secondary);
color: var(--text-secondary);
margin-bottom: 16px;
line-height: 1.5;
}
@ -596,27 +646,27 @@
align-items: center;
gap: 12px;
padding: 12px 14px;
background: var(--sentient-bg-tertiary);
border: 1px solid var(--sentient-border);
border-radius: var(--sentient-radius-md);
background: var(--surface-hover);
border: 1px solid var(--border);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.decision-option:hover {
border-color: var(--sentient-accent);
background: var(--sentient-accent-dim);
border-color: var(--primary);
background: var(--primary-light);
}
.decision-option.selected {
border-color: var(--sentient-accent);
background: var(--sentient-accent-dim);
border-color: var(--primary);
background: var(--primary-light);
}
.decision-option-radio {
width: 18px;
height: 18px;
border: 2px solid var(--sentient-border-hover);
border: 2px solid var(--border);
border-radius: 50%;
display: flex;
align-items: center;
@ -625,14 +675,14 @@
}
.decision-option.selected .decision-option-radio {
border-color: var(--sentient-accent);
border-color: var(--primary);
}
.decision-option.selected .decision-option-radio::after {
content: "";
width: 8px;
height: 8px;
background: var(--sentient-accent);
background: var(--primary);
border-radius: 50%;
}
@ -643,13 +693,13 @@
.decision-option-label {
font-size: 14px;
font-weight: 500;
color: var(--sentient-text-primary);
color: var(--text);
margin-bottom: 2px;
}
.decision-option-desc {
font-size: 12px;
color: var(--sentient-text-muted);
color: var(--text-secondary);
}
.decision-actions {
@ -661,7 +711,7 @@
.decision-btn {
flex: 1;
padding: 10px 16px;
border-radius: var(--sentient-radius-md);
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
@ -669,25 +719,25 @@
}
.decision-btn-primary {
background: var(--sentient-accent);
background: var(--primary);
border: none;
color: #000;
}
.decision-btn-primary:hover {
background: #d4ff4a;
box-shadow: var(--sentient-shadow-glow);
box-shadow: 0 0 20px var(--primary-light);
}
.decision-btn-secondary {
background: transparent;
border: 1px solid var(--sentient-border);
color: var(--sentient-text-secondary);
border: 1px solid var(--border);
color: var(--text-secondary);
}
.decision-btn-secondary:hover {
background: var(--sentient-bg-tertiary);
color: var(--sentient-text-primary);
background: var(--surface-hover);
color: var(--text);
}
/* Progress Log Section */
@ -705,7 +755,7 @@
.progress-log-title {
font-size: 14px;
font-weight: 600;
color: var(--sentient-text-primary);
color: var(--text);
text-transform: uppercase;
letter-spacing: 0.5px;
}
@ -713,14 +763,14 @@
.progress-log-toggle {
background: none;
border: none;
color: var(--sentient-text-muted);
color: var(--text-secondary);
font-size: 12px;
cursor: pointer;
transition: color 0.2s ease;
}
.progress-log-toggle:hover {
color: var(--sentient-text-primary);
color: var(--text);
}
/* Step Items */
@ -735,23 +785,23 @@
align-items: flex-start;
gap: 12px;
padding: 12px 14px;
background: var(--sentient-bg-tertiary);
border-radius: var(--sentient-radius-sm);
background: var(--surface-hover);
border-radius: 6px;
transition: all 0.2s ease;
}
.step-item:first-child {
border-radius: var(--sentient-radius-md) var(--sentient-radius-md)
var(--sentient-radius-sm) var(--sentient-radius-sm);
border-radius: 8px 8px
6px 6px;
}
.step-item:last-child {
border-radius: var(--sentient-radius-sm) var(--sentient-radius-sm)
var(--sentient-radius-md) var(--sentient-radius-md);
border-radius: 6px 6px
8px 8px;
}
.step-item:only-child {
border-radius: var(--sentient-radius-md);
border-radius: 8px;
}
.step-icon {
@ -766,23 +816,23 @@
}
.step-item.completed .step-icon {
background: var(--sentient-success);
background: var(--success);
color: #000;
}
.step-item.active .step-icon {
background: var(--sentient-accent);
background: var(--primary);
color: #000;
animation: pulse 2s infinite;
}
.step-item.pending .step-icon {
background: var(--sentient-bg-hover);
color: var(--sentient-text-muted);
background: var(--surface-hover);
color: var(--text-secondary);
}
.step-item.error .step-icon {
background: var(--sentient-error);
background: var(--error);
color: #fff;
}
@ -794,22 +844,22 @@
.step-name {
font-size: 14px;
font-weight: 500;
color: var(--sentient-text-primary);
color: var(--text);
margin-bottom: 2px;
}
.step-item.pending .step-name {
color: var(--sentient-text-muted);
color: var(--text-secondary);
}
.step-detail {
font-size: 12px;
color: var(--sentient-text-muted);
color: var(--text-secondary);
}
.step-time {
font-size: 11px;
color: var(--sentient-text-muted);
color: var(--text-secondary);
flex-shrink: 0;
}
@ -818,8 +868,8 @@
============================================================================= */
.agent-activity-section {
border-top: 1px solid var(--sentient-border);
background: var(--sentient-bg-primary);
border-top: 1px solid var(--border);
background: var(--bg);
}
.agent-activity-header {
@ -827,8 +877,8 @@
align-items: center;
justify-content: space-between;
padding: 12px 24px;
background: var(--sentient-bg-secondary);
border-bottom: 1px solid var(--sentient-border);
background: var(--surface);
border-bottom: 1px solid var(--border);
}
.agent-activity-title {
@ -837,7 +887,7 @@
gap: 10px;
font-size: 12px;
font-weight: 600;
color: var(--sentient-text-secondary);
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
@ -845,7 +895,7 @@
.agent-status-dot {
width: 8px;
height: 8px;
background: var(--sentient-accent);
background: var(--primary);
border-radius: 50%;
animation: pulse 2s infinite;
}
@ -858,24 +908,24 @@
.agent-activity-btn {
padding: 4px 8px;
background: transparent;
border: 1px solid var(--sentient-border);
border-radius: var(--sentient-radius-sm);
color: var(--sentient-text-muted);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-secondary);
font-size: 11px;
cursor: pointer;
transition: all 0.2s ease;
}
.agent-activity-btn:hover {
background: var(--sentient-bg-tertiary);
color: var(--sentient-text-primary);
background: var(--surface-hover);
color: var(--text);
}
.agent-activity-log {
padding: 16px 24px;
max-height: 180px;
overflow-y: auto;
font-family: var(--sentient-font-mono);
font-family: monospace;
font-size: 12px;
line-height: 1.6;
}
@ -887,33 +937,33 @@
}
.activity-timestamp {
color: var(--sentient-text-muted);
color: var(--text-secondary);
min-width: 65px;
flex-shrink: 0;
}
.activity-message {
color: var(--sentient-text-secondary);
color: var(--text-secondary);
}
.activity-line.success .activity-message {
color: var(--sentient-success);
color: var(--success);
}
.activity-line.warning .activity-message {
color: var(--sentient-warning);
color: var(--warning);
}
.activity-line.error .activity-message {
color: var(--sentient-error);
color: var(--error);
}
.activity-line.accent .activity-message {
color: var(--sentient-accent);
color: var(--primary);
}
.activity-line.info .activity-message {
color: var(--sentient-info);
color: var(--info);
}
/* =============================================================================
@ -938,13 +988,13 @@
.empty-state-title {
font-size: 18px;
font-weight: 600;
color: var(--sentient-text-primary);
color: var(--text);
margin-bottom: 8px;
}
.empty-state-description {
font-size: 14px;
color: var(--sentient-text-muted);
color: var(--text-secondary);
max-width: 280px;
}
@ -963,8 +1013,8 @@
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid var(--sentient-bg-tertiary);
border-top-color: var(--sentient-accent);
border: 3px solid var(--surface-hover);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 16px;
@ -978,7 +1028,7 @@
.loading-text {
font-size: 14px;
color: var(--sentient-text-muted);
color: var(--text-secondary);
}
/* =============================================================================
@ -998,7 +1048,7 @@
.tasks-list-panel {
border-right: none;
border-bottom: 1px solid var(--sentient-border);
border-bottom: 1px solid var(--border);
max-height: 50vh;
}

View file

@ -3,48 +3,7 @@
Automated Intelligent Task Management Interface
============================================================================= -->
<link rel="stylesheet" href="/themes/sentient/sentient.css" />
<link rel="stylesheet" href="/suite/tasks/tasks.css" />
<div class="tasks-app sentient-theme">
<!-- Top Header Bar -->
<header class="tasks-topbar">
<div class="topbar-left">
<div class="topbar-logo">
<div class="topbar-logo-icon"></div>
<span>Sentient</span>
</div>
<nav class="topbar-nav">
<button class="topbar-nav-item active">Dashboard</button>
<button class="topbar-nav-item">Analytics</button>
</nav>
</div>
<div class="topbar-center">
<div class="topbar-search">
<span class="topbar-search-icon">🔍</span>
<input
type="text"
class="topbar-search-input"
placeholder="Search tasks, intents, logs..."
/>
</div>
</div>
<div class="topbar-right">
<button class="topbar-icon-btn" title="Notifications">
🔔
<span class="notification-dot"></span>
</button>
<button class="topbar-icon-btn" title="Settings">⚙️</button>
<div class="topbar-profile">
<div class="topbar-avatar">JD</div>
<span class="topbar-profile-name">John Doe</span>
<span class="topbar-profile-arrow"></span>
</div>
</div>
</header>
<div class="tasks-app">
<!-- Main Content Area -->
<main class="tasks-main">
<!-- Task List Panel (Left) -->
@ -55,6 +14,24 @@
<span class="tasks-count" id="tasks-total-count"
>24 tasks</span
>
<button
class="btn-new-intent"
onclick="showNewIntentModal()"
title="Create new intent"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
New Intent
</button>
</div>
<!-- Status Filter Pills -->
@ -546,4 +523,156 @@
</main>
</div>
<script src="/suite/tasks/tasks-sentient.js"></script>
<!-- New Intent Modal -->
<div class="modal-overlay" id="newIntentModal">
<div class="modal">
<div class="modal-header">
<h3>Create New Intent</h3>
<button class="modal-close" onclick="closeNewIntentModal()">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
<div class="modal-body">
<form
id="newIntentForm"
hx-post="/api/tasks"
hx-target="#task-list"
hx-swap="afterbegin"
>
<div class="form-group">
<label class="form-label" for="intentTitle"
>Intent Title</label
>
<input
type="text"
class="form-input"
id="intentTitle"
name="title"
placeholder="e.g., Launch Q4 Campaign for Verse"
required
/>
</div>
<div class="form-group">
<label class="form-label" for="intentDescription"
>Description</label
>
<textarea
class="form-input"
id="intentDescription"
name="description"
rows="3"
placeholder="Describe what this intent should accomplish..."
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" for="intentDueDate"
>Due Date</label
>
<input
type="date"
class="form-input"
id="intentDueDate"
name="due_date"
/>
</div>
<div class="form-group">
<label class="form-label" for="intentPriority"
>Priority</label
>
<select
class="form-input"
id="intentPriority"
name="priority"
>
<option value="normal">Normal</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label" for="intentGoal"
>Goal / Success Criteria</label
>
<textarea
class="form-input"
id="intentGoal"
name="goal"
rows="2"
placeholder="How will you know when this intent is complete?"
></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button
class="decision-btn decision-btn-secondary"
onclick="closeNewIntentModal()"
>
Cancel
</button>
<button
class="decision-btn decision-btn-primary"
type="submit"
form="newIntentForm"
>
Create Intent
</button>
</div>
</div>
</div>
<script>
// Conditionally load tasks.js only if not already loaded
if (typeof TasksState === "undefined") {
const script = document.createElement("script");
script.src = "tasks/tasks.js";
document.head.appendChild(script);
}
</script>
<script>
// New Intent Modal functions
function showNewIntentModal() {
document.getElementById("newIntentModal").classList.add("show");
document.getElementById("intentTitle").focus();
}
function closeNewIntentModal() {
document.getElementById("newIntentModal").classList.remove("show");
document.getElementById("newIntentForm").reset();
}
// Close modal on escape key
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
closeNewIntentModal();
}
});
// Close modal when clicking overlay
document
.getElementById("newIntentModal")
?.addEventListener("click", (e) => {
if (e.target.classList.contains("modal-overlay")) {
closeNewIntentModal();
}
});
// Close modal after successful form submission
document.body.addEventListener("htmx:afterRequest", (e) => {
if (e.detail.elt.id === "newIntentForm" && e.detail.successful) {
closeNewIntentModal();
}
});
</script>

View file

@ -7,13 +7,16 @@
// STATE MANAGEMENT
// =============================================================================
const TasksState = {
selectedTaskId: 2, // Default selected task
currentFilter: "complete",
tasks: [],
wsConnection: null,
agentLogPaused: false,
};
// Prevent duplicate declaration when script is reloaded via HTMX
if (typeof TasksState === "undefined") {
var TasksState = {
selectedTaskId: 2, // Default selected task
currentFilter: "complete",
tasks: [],
wsConnection: null,
agentLogPaused: false,
};
}
// =============================================================================
// INITIALIZATION