/** * Base Module JavaScript * Core functionality for the General Bots Suite * Handles navigation, theme, settings, accessibility, and HTMX error handling */ // DOM Elements const appsBtn = document.getElementById("apps-btn"); const appsDropdown = document.getElementById("apps-dropdown"); const settingsBtn = document.getElementById("settings-btn"); 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"); }); } // 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"); }); } // 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"); } }); // 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"); } }); // 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(); } } }); // 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"); } }); // Theme handling // Available themes: dark, light, blue, purple, green, orange, sentient const themeOptions = document.querySelectorAll(".theme-option"); const savedTheme = localStorage.getItem("gb-theme") || "sentient"; document.body.setAttribute("data-theme", savedTheme); 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"); 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")); } // 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"); } }); // 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, ); } // 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); } } // 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..."); } }); 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"); } }); 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."); }); // Keyboard navigation for apps grid document.addEventListener("keydown", function (e) { 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, ); if (currentIndex === -1) return; 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; } 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 = `
${message}
`; 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(), }; 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) ); } function showErrorState(target, errorMessage, retryCallback) { const errorHtml = `
Something went wrong
${errorMessage}
`; target.innerHTML = errorHtml; target.dataset.retryCallback = retryCallback; } window.retryLastRequest = function (btn) { const target = btn.closest(".error-state").parentElement; const retryCallback = target.dataset.retryCallback; btn.disabled = true; btn.innerHTML = ' Retrying...'; 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 { // 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); 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); 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); 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); } }); // Handle timeout errors document.body.addEventListener("htmx:timeout", function (e) { 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, ); });