botui/ui/suite/base.html

1330 lines
60 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}General Bots Suite{% endblock %}</title>
<!-- HTMX (local) -->
<script src="/js/vendor/htmx.min.js"></script>
<script src="/js/vendor/htmx-ws.js"></script>
<!-- Styles -->
<link rel="stylesheet" href="/css/base.css" />
<link rel="stylesheet" href="/css/app.css" />
<link rel="stylesheet" href="/themes/sentient/sentient.css" />
<link rel="stylesheet" href="/css/ai-panel.css" />
</head>
<body>
<!-- Skip navigation link for accessibility -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- ARIA live region for dynamic updates -->
<div
id="aria-live"
class="sr-only"
aria-live="polite"
aria-atomic="true"
></div>
<header class="app-header" role="banner">
<div class="header-left">
<a href="/" class="logo">
<div class="logo-icon">🤖</div>
<span>General Bots</span>
</a>
</div>
<div class="header-right">
<!-- Apps Menu -->
<div class="apps-menu">
<button
class="header-btn"
id="apps-btn"
aria-label="Applications"
aria-expanded="false"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="currentColor"
>
<circle cx="5" cy="5" r="2"></circle>
<circle cx="12" cy="5" r="2"></circle>
<circle cx="19" cy="5" r="2"></circle>
<circle cx="5" cy="12" r="2"></circle>
<circle cx="12" cy="12" r="2"></circle>
<circle cx="19" cy="12" r="2"></circle>
<circle cx="5" cy="19" r="2"></circle>
<circle cx="12" cy="19" r="2"></circle>
<circle cx="19" cy="19" r="2"></circle>
</svg>
</button>
<nav class="apps-dropdown" id="apps-dropdown" role="menu">
<div class="apps-dropdown-title">Applications</div>
<div class="apps-grid">
<a
href="#chat"
class="app-item"
role="menuitem"
hx-get="/chat/chat.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#3b82f6,
#1d4ed8
);
"
>
💬
</div>
<span>Chat</span>
</a>
<a
href="#drive"
class="app-item"
role="menuitem"
hx-get="/drive/index.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#f59e0b,
#d97706
);
"
>
📁
</div>
<span>Drive</span>
</a>
<a
href="#tasks"
class="app-item"
role="menuitem"
hx-get="/tasks/tasks.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#22c55e,
#16a34a
);
"
>
</div>
<span>Tasks</span>
</a>
<a
href="#mail"
class="app-item"
role="menuitem"
hx-get="/mail/mail.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#ef4444,
#dc2626
);
"
>
✉️
</div>
<span>Mail</span>
</a>
<a
href="#calendar"
class="app-item"
role="menuitem"
hx-get="/calendar/calendar.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#a855f7,
#7c3aed
);
"
>
📅
</div>
<span>Calendar</span>
</a>
<a
href="#meet"
class="app-item"
role="menuitem"
hx-get="/meet/meet.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#06b6d4,
#0891b2
);
"
>
🎥
</div>
<span>Meet</span>
</a>
<a
href="#paper"
class="app-item"
role="menuitem"
hx-get="/paper/paper.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#eab308,
#ca8a04
);
"
>
📝
</div>
<span>Paper</span>
</a>
<a
href="#research"
class="app-item"
role="menuitem"
hx-get="/research/research.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#ec4899,
#db2777
);
"
>
🔍
</div>
<span>Research</span>
</a>
<a
href="#sources"
class="app-item"
role="menuitem"
hx-get="/sources/index.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#14b8a6,
#0d9488
);
"
>
📚
</div>
<span>Sources</span>
</a>
<a
href="#analytics"
class="app-item"
role="menuitem"
hx-get="/analytics/analytics.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#6366f1,
#4f46e5
);
"
>
📊
</div>
<span>Analytics</span>
</a>
<a
href="#admin"
class="app-item"
role="menuitem"
hx-get="/admin/index.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#f43f5e,
#e11d48
);
"
>
⚙️
</div>
<span>Admin</span>
</a>
<a
href="#monitoring"
class="app-item"
role="menuitem"
hx-get="/monitoring/index.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#84cc16,
#65a30d
);
"
>
📈
</div>
<span>Monitoring</span>
</a>
</div>
</nav>
</div>
<!-- Settings Panel (Gear Icon) -->
<div class="settings-menu">
<button
class="header-btn"
id="settings-btn"
aria-label="Settings"
aria-expanded="false"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<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 class="settings-panel" id="settings-panel" role="menu">
<div class="settings-panel-title">Quick Settings</div>
<!-- Theme Selection -->
<div class="settings-section">
<div class="settings-section-title">Theme</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>
<!-- Quick Toggles -->
<div class="settings-section">
<div class="settings-section-title">
Quick Toggles
</div>
<div class="quick-toggles">
<div class="quick-toggle">
<span class="quick-toggle-label"
>Desktop Notifications</span
>
<div
class="toggle-switch active"
id="toggle-notifications"
onclick="toggleQuickSetting(this)"
></div>
</div>
<div class="quick-toggle">
<span class="quick-toggle-label"
>Sound Effects</span
>
<div
class="toggle-switch active"
id="toggle-sound"
onclick="toggleQuickSetting(this)"
></div>
</div>
<div class="quick-toggle">
<span class="quick-toggle-label"
>Compact Mode</span
>
<div
class="toggle-switch"
id="toggle-compact"
onclick="toggleQuickSetting(this)"
></div>
</div>
</div>
</div>
<div class="settings-divider"></div>
<!-- Shortcuts -->
<div class="settings-section">
<div class="settings-section-title">
Configuration
</div>
<a
href="#settings"
class="settings-shortcut"
hx-get="/settings/index.html"
hx-target="#main-content"
hx-push-url="true"
>
<div class="settings-shortcut-icon">
<svg
width="16"
height="16"
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-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-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 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9c.26.604.852.997 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"
/>
</svg>
</div>
<div class="settings-shortcut-text">
<div class="settings-shortcut-title">
All Settings
</div>
<div class="settings-shortcut-desc">
Account, sync, appearance & more
</div>
</div>
<span class="settings-shortcut-arrow"></span>
</a>
<a
href="#admin"
class="settings-shortcut"
hx-get="/admin/index.html"
hx-target="#main-content"
hx-push-url="true"
>
<div class="settings-shortcut-icon">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</div>
<div class="settings-shortcut-text">
<div class="settings-shortcut-title">
Admin Console
</div>
<div class="settings-shortcut-desc">
Bot management & configuration
</div>
</div>
<span class="settings-shortcut-arrow"></span>
</a>
<a
href="#monitoring"
class="settings-shortcut"
hx-get="/monitoring/index.html"
hx-target="#main-content"
hx-push-url="true"
>
<div class="settings-shortcut-icon">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M3 3v18h18" />
<path d="M18 9l-5 5-4-4-3 3" />
</svg>
</div>
<div class="settings-shortcut-text">
<div class="settings-shortcut-title">
Monitoring
</div>
<div class="settings-shortcut-desc">
System health & performance
</div>
</div>
<span class="settings-shortcut-arrow"></span>
</a>
<a
href="/keyboard-shortcuts"
class="settings-shortcut"
onclick="showKeyboardShortcuts(); return false;"
>
<div class="settings-shortcut-icon">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="2"
y="4"
width="20"
height="16"
rx="2"
/>
<path
d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M6 12h.01M10 12h.01M14 12h.01M18 12h.01M8 16h8"
/>
</svg>
</div>
<div class="settings-shortcut-text">
<div class="settings-shortcut-title">
Keyboard Shortcuts
</div>
<div class="settings-shortcut-desc">
View all available shortcuts
</div>
</div>
<span class="settings-shortcut-arrow"></span>
</a>
</div>
</div>
</div>
<!-- User Avatar -->
<button class="user-avatar" aria-label="User menu">
{{ user_initial|default("U") }}
</button>
</div>
</header>
<main class="app-main" role="main">
<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">
<div class="ai-panel-title">
<span class="ai-avatar">🤖</span>
<div>
<h3>AI Assistant</h3>
<p class="ai-status">Ready to help</p>
</div>
</div>
<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>
</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>
</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>
</main>
<div class="notifications-container" id="notifications"></div>
<script>
const appsBtn = document.getElementById("apps-btn");
const appsDropdown = document.getElementById("apps-dropdown");
const settingsBtn = document.getElementById("settings-btn");
const settingsPanel = document.getElementById("settings-panel");
appsBtn.addEventListener("click", (e) => {
e.stopPropagation();
const isOpen = appsDropdown.classList.toggle("show");
appsBtn.setAttribute("aria-expanded", isOpen);
settingsPanel.classList.remove("show");
});
settingsBtn.addEventListener("click", (e) => {
e.stopPropagation();
const isOpen = settingsPanel.classList.toggle("show");
settingsBtn.setAttribute("aria-expanded", isOpen);
appsDropdown.classList.remove("show");
});
document.addEventListener("click", (e) => {
if (
!appsDropdown.contains(e.target) &&
!appsBtn.contains(e.target)
) {
appsDropdown.classList.remove("show");
appsBtn.setAttribute("aria-expanded", "false");
}
if (
!settingsPanel.contains(e.target) &&
!settingsBtn.contains(e.target)
) {
settingsPanel.classList.remove("show");
settingsBtn.setAttribute("aria-expanded", "false");
}
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
appsDropdown.classList.remove("show");
settingsPanel.classList.remove("show");
appsBtn.setAttribute("aria-expanded", "false");
settingsBtn.setAttribute("aria-expanded", "false");
}
});
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();
appsDropdown.classList.remove("show");
}
if (e.key === ",") {
e.preventDefault();
settingsPanel.classList.toggle("show");
}
if (e.key === "s") {
e.preventDefault();
const settingsLink =
document.querySelector(`a[href="#settings"]`);
if (settingsLink) settingsLink.click();
}
}
});
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,
);
});
settingsPanel.classList.remove("show");
}
});
// Theme handling with dropdown
const themeSelector = document.getElementById("themeSelector");
const savedTheme = localStorage.getItem("gb-theme") || "sentient";
document.body.setAttribute("data-theme", savedTheme);
// 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);
// Also notify ThemeManager if it exists
if (window.ThemeManager && window.ThemeManager.loadTheme) {
window.ThemeManager.loadTheme(theme);
}
});
}
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");
}
});
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();
}
});
window.showNotification = function (
message,
type = "info",
duration = 5000,
) {
const container = document.getElementById("notifications");
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);
}
};
// 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 = `
<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>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<div class="error-state-title">Something went wrong</div>
<div class="error-state-message">${errorMessage}</div>
<div class="error-state-actions">
<button class="btn-retry" onclick="window.retryLastRequest(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M23 4v6h-6"></path>
<path d="M1 20v-6h6"></path>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10"></path>
<path d="M20.49 15a9 9 0 0 1-14.85 3.36L1 14"></path>
</svg>
Try Again
</button>
</div>
</div>
`;
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 = '<span class="spinner"></span> 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,
);
});
// =================================================================
// 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" },
],
tasks: [
{ 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" },
],
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" },
],
meet: [
{ 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" },
],
research: [
{ 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" },
],
analytics: [
{ 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" },
],
monitoring: [
{ 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" },
],
};
// Get current app from URL or hash
function getCurrentApp() {
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";
}
// Update body data-app attribute
function updateCurrentApp() {
const app = getCurrentApp();
document.body.setAttribute("data-app", app);
loadQuickActions(app);
}
// Load quick actions for current app
function loadQuickActions(app) {
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("");
}
// Handle quick action click
function handleQuickAction(action) {
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",
};
if (input && actionMessages[action]) {
input.value = actionMessages[action];
sendAIMessage();
}
}
// Toggle AI panel
function toggleAIPanel() {
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 message = input?.value?.trim();
if (!message || !messagesContainer) return;
// Add user message
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 = "";
// Scroll to bottom
messagesContainer.scrollTop = messagesContainer.scrollHeight;
// Show typing indicator
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";
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;
}, 1500);
}
// Escape HTML
function escapeHtml(text) {
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";
if (collapsed) {
document.body.classList.add("ai-panel-collapsed");
}
updateCurrentApp();
}
// Initialize on DOM ready
document.addEventListener("DOMContentLoaded", initAIPanel);
// Update app on navigation
document.body.addEventListener("htmx:afterSwap", function (e) {
if (e.detail.target.id === "main-content") {
updateCurrentApp();
}
});
// Also track hash changes
window.addEventListener("hashchange", updateCurrentApp);
</script>
{% block scripts %}{% endblock %}
</body>
</html>