diff --git a/src/ui_server/mod.rs b/src/ui_server/mod.rs index 1a9f6c1..5507e28 100644 --- a/src/ui_server/mod.rs +++ b/src/ui_server/mod.rs @@ -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, ) -> Response { 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 { + Router::new().fallback(any(ws_proxy)) +} + +/// Create UI HTMX proxy router (for HTML fragment endpoints) +fn create_ui_router() -> Router { 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", diff --git a/ui/suite/admin/admin.css b/ui/suite/admin/admin.css index 95a7fc0..8900c36 100644 --- a/ui/suite/admin/admin.css +++ b/ui/suite/admin/admin.css @@ -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 */ diff --git a/ui/suite/attendant/attendant.css b/ui/suite/attendant/attendant.css index 665c74c..68f1e17 100644 --- a/ui/suite/attendant/attendant.css +++ b/ui/suite/attendant/attendant.css @@ -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) { diff --git a/ui/suite/base.html b/ui/suite/base.html index cee8d2b..2b0a2a8 100644 --- a/ui/suite/base.html +++ b/ui/suite/base.html @@ -362,182 +362,64 @@
Theme
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Dark -
-
-
-
-
-
-
-
-
-
-
-
-
-
- Light -
-
-
-
-
-
-
-
-
-
-
-
-
-
- Ocean -
-
-
-
-
-
-
-
-
-
-
-
-
-
- Violet -
-
-
-
-
-
-
-
-
-
-
-
-
-
- Forest -
-
-
-
-
-
-
-
-
-
-
-
-
-
- Sunset -
-
+
@@ -730,7 +612,7 @@
{% block content %}{% endblock %}
- + - + - @@ -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 => - `` - ).join(''); + container.innerHTML = actions + .map( + (a) => + ``, + ) + .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 = `
${escapeHtml(message)}
`; 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 = `
`; 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 = `
Entendi! Estou processando sua solicitaรงรฃo: "${escapeHtml(message)}". Como posso ajudar mais?
`; 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); {% block scripts %}{% endblock %} diff --git a/ui/suite/calendar/calendar.css b/ui/suite/calendar/calendar.css index e7ac9c6..8b2c389 100644 --- a/ui/suite/calendar/calendar.css +++ b/ui/suite/calendar/calendar.css @@ -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 { diff --git a/ui/suite/calendar/calendar.html b/ui/suite/calendar/calendar.html index bce0ba6..9156a27 100644 --- a/ui/suite/calendar/calendar.html +++ b/ui/suite/calendar/calendar.html @@ -14,7 +14,7 @@
@@ -75,7 +75,7 @@ -
- -
+
diff --git a/ui/suite/index.html b/ui/suite/index.html index 6a709cf..986b763 100644 --- a/ui/suite/index.html +++ b/ui/suite/index.html @@ -8,13 +8,14 @@ name="description" content="General Bots - AI-powered workspace" /> - + + @@ -41,7 +42,7 @@ - +
@@ -231,7 +232,13 @@ 3 - + + +
`; - 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 = ' Retrying...'; + btn.disabled = true; + btn.innerHTML = ' 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, + ); }); diff --git a/ui/suite/js/htmx-app.js b/ui/suite/js/htmx-app.js index 3de9f87..4a095d1 100644 --- a/ui/suite/js/htmx-app.js +++ b/ui/suite/js/htmx-app.js @@ -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 = ""; + } + }); - 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 = `
${message.sender} ${escapeHtml(message.text)} @@ -100,216 +120,218 @@
`; - 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, + }; })(); diff --git a/ui/suite/mail/mail.html b/ui/suite/mail/mail.html index ed32515..70d5f0c 100644 --- a/ui/suite/mail/mail.html +++ b/ui/suite/mail/mail.html @@ -12,7 +12,7 @@
-