botui/ui/suite/index.html
Rodrigo Rodriguez (Pragmatismo) a4b9d013e1 fix(auth): Integrate user menu with GBAuth service
- Use GBAuth service events for user profile updates
- Use correct storage keys (gb-access-token, gb-user-data)
- Listen to userUpdated, login, logout events
- Store tokens using AuthService.storeTokens() method
- Add fallback for when GBAuth is not immediately available
2026-01-07 07:03:07 -03:00

3017 lines
138 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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" />
<title>Chat - General Bots</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta
name="description"
content="General Bots - AI-powered workspace"
/>
<meta name="theme-color" content="#d4f505" />
<!-- Styles -->
<link rel="stylesheet" href="css/app.css" />
<link rel="stylesheet" href="css/apps-extended.css" />
<link rel="stylesheet" href="css/components.css" />
<link rel="stylesheet" href="css/base.css" />
<link rel="stylesheet" href="css/partials.css" />
<link rel="stylesheet" href="css/theme-sentient.css" />
<!-- App-specific CSS -->
<link rel="stylesheet" href="chat/chat.css" />
<link rel="stylesheet" href="calendar/calendar.css" />
<link rel="stylesheet" href="drive/drive.css" />
<link rel="stylesheet" href="mail/mail.css" />
<link rel="stylesheet" href="meet/meet.css" />
<link rel="stylesheet" href="paper/paper.css" />
<link rel="stylesheet" href="sheet/sheet.css" />
<link rel="stylesheet" href="slides/slides.css" />
<link rel="stylesheet" href="research/research.css" />
<link rel="stylesheet" href="tasks/tasks.css?v=20251230" />
<link rel="stylesheet" href="tasks/taskmd.css?v=20251230" />
<link rel="stylesheet" href="analytics/analytics.css" />
<link rel="stylesheet" href="monitoring/monitoring.css" />
<!-- Local Libraries (no external CDN dependencies) -->
<script src="js/vendor/htmx.min.js"></script>
<script src="js/vendor/htmx-ws.js"></script>
<script src="js/vendor/htmx-json-enc.js"></script>
<script src="js/vendor/marked.min.js"></script>
<!-- i18n -->
<script src="js/i18n.js"></script>
<!-- Enable HTMX to process inline scripts in swapped content -->
<script>
htmx.config.allowEval = true;
htmx.config.includeIndicatorStyles = false;
htmx.config.timeout = 300000; // 5 minutes for long-running LLM operations
// Global error handler to catch HTMX DOM manipulation errors
window.addEventListener("error", function (event) {
// Catch insertBefore errors from HTMX swap operations
if (
event.message &&
event.message.includes("insertBefore") &&
event.message.includes("null")
) {
console.warn(
"HTMX DOM error caught and suppressed:",
event.message,
);
event.preventDefault();
return true;
}
});
// Handle unhandled promise rejections from HTMX
window.addEventListener("unhandledrejection", function (event) {
if (
event.reason &&
event.reason.message &&
event.reason.message.includes("insertBefore")
) {
console.warn(
"HTMX promise rejection caught:",
event.reason.message,
);
event.preventDefault();
}
});
</script>
</head>
<body data-theme="sentient">
<!-- Loading overlay -->
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-spinner"></div>
</div>
<!-- Floating header -->
<header class="float-header" role="banner">
<!-- Left: Logo + App Tabs -->
<div class="header-left">
<button
class="logo-wrapper"
onclick="window.location.href='/#chat'"
title="General Bots"
aria-label="General Bots - Home"
>
<svg
class="logo-icon"
width="32"
height="20"
viewBox="0 0 140 80"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- Left antenna -->
<line x1="5" y1="40" x2="25" y2="40" />
<line x1="25" y1="28" x2="25" y2="52" />
<!-- Left gear (outer) -->
<circle cx="50" cy="40" r="16" />
<!-- Left gear (inner) -->
<circle cx="50" cy="40" r="6" />
<!-- Left gear teeth -->
<line x1="50" y1="20" x2="50" y2="26" />
<line x1="50" y1="54" x2="50" y2="60" />
<line x1="30" y1="40" x2="36" y2="40" />
<line x1="64" y1="40" x2="70" y2="40" />
<line x1="38" y1="28" x2="42" y2="32" />
<line x1="58" y1="48" x2="62" y2="52" />
<line x1="38" y1="52" x2="42" y2="48" />
<line x1="58" y1="32" x2="62" y2="28" />
<!-- Right gear (outer) -->
<circle cx="90" cy="40" r="16" />
<!-- Right gear (inner) -->
<circle cx="90" cy="40" r="6" />
<!-- Right gear teeth -->
<line x1="90" y1="20" x2="90" y2="26" />
<line x1="90" y1="54" x2="90" y2="60" />
<line x1="70" y1="40" x2="76" y2="40" />
<line x1="104" y1="40" x2="110" y2="40" />
<line x1="78" y1="28" x2="82" y2="32" />
<line x1="98" y1="48" x2="102" y2="52" />
<line x1="78" y1="52" x2="82" y2="48" />
<line x1="98" y1="32" x2="102" y2="28" />
<!-- Right antenna -->
<line x1="115" y1="40" x2="135" y2="40" />
<line x1="115" y1="28" x2="115" y2="52" />
</svg>
</button>
<!-- Main App Tabs -->
<nav class="header-app-tabs">
<a
class="app-tab active"
href="#chat"
data-section="chat"
hx-get="/suite/chat/chat.html"
hx-target="#main-content"
hx-push-url="/#chat"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
/>
</svg>
<span data-i18n="nav-chat">Chat</span>
</a>
<a
class="app-tab"
href="#mail"
data-section="mail"
hx-get="/suite/mail/mail.html"
hx-target="#main-content"
hx-push-url="/#mail"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"
/>
<polyline points="22,6 12,13 2,6" />
</svg>
<span data-i18n="nav-mail">E-mail</span>
</a>
<a
class="app-tab"
href="#calendar"
data-section="calendar"
hx-get="/suite/calendar/calendar.html"
hx-target="#main-content"
hx-push-url="/#calendar"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="3"
y="4"
width="18"
height="18"
rx="2"
ry="2"
/>
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
<span data-i18n="nav-calendar">Calendário</span>
</a>
<a
class="app-tab"
href="#drive"
data-section="drive"
hx-get="/suite/drive/drive.html"
hx-target="#main-content"
hx-push-url="/#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"
/>
</svg>
<span data-i18n="nav-drive">Arquivos</span>
</a>
<a
class="app-tab"
href="#tasks"
data-section="tasks"
hx-get="/suite/tasks/tasks.html"
hx-target="#main-content"
hx-push-url="/#tasks"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M9 11l3 3L22 4" />
<path
d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"
/>
</svg>
<span data-i18n="nav-tasks">Tarefas</span>
</a>
<a
class="app-tab"
href="#docs"
data-section="docs"
hx-get="/suite/docs/docs.html"
hx-target="#main-content"
hx-push-url="/#docs"
>
<svg
width="18"
height="18"
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" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
</svg>
<span data-i18n="nav-docs">Documentos</span>
</a>
<a
class="app-tab"
href="#sheet"
data-section="sheet"
hx-get="/suite/sheet/sheet.html"
hx-target="#main-content"
hx-push-url="/#sheet"
>
<svg
width="18"
height="18"
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="3" y1="9" x2="21" y2="9" />
<line x1="3" y1="15" x2="21" y2="15" />
<line x1="9" y1="3" x2="9" y2="21" />
<line x1="15" y1="3" x2="15" y2="21" />
</svg>
<span data-i18n="nav-sheet">Planilhas</span>
</a>
<a
class="app-tab"
href="#slides"
data-section="slides"
hx-get="/suite/slides/slides.html"
hx-target="#main-content"
hx-push-url="/#slides"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="2"
y="3"
width="20"
height="14"
rx="2"
ry="2"
/>
<line x1="8" y1="21" x2="16" y2="21" />
<line x1="12" y1="17" x2="12" y2="21" />
</svg>
<span data-i18n="nav-slides">Apresentações</span>
</a>
</nav>
<!-- Apps menu container (button + dropdown) -->
<div class="apps-menu-container" style="position: relative">
<button
class="icon-button apps-button"
id="appsButton"
title="All Applications"
aria-label="Open applications menu"
aria-expanded="false"
aria-haspopup="true"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<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>
<!-- Apps dropdown menu -->
<nav
class="apps-dropdown"
id="appsDropdown"
role="menu"
aria-label="Applications"
>
<div
class="apps-dropdown-title"
data-i18n="nav-all-apps"
>
All Applications
</div>
<div class="app-grid" role="group">
<!-- Chat -->
<a
class="app-item active"
href="#chat"
data-section="chat"
role="menuitem"
aria-label="Chat application"
hx-get="/suite/chat/chat.html"
hx-target="#main-content"
hx-push-url="/#chat"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
/>
</svg>
</div>
<span data-i18n="nav-chat">Chat</span>
</a>
<!-- Docs -->
<a
class="app-item"
href="#docs"
data-section="docs"
role="menuitem"
aria-label="Docs - Documents"
hx-get="/suite/docs/docs.html"
hx-target="#main-content"
hx-push-url="/#docs"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
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" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
</svg>
</div>
<span data-i18n="nav-docs">Documentos</span>
</a>
<!-- Sheet -->
<a
class="app-item"
href="#sheet"
data-section="sheet"
role="menuitem"
aria-label="Sheet - Spreadsheets"
hx-get="/suite/sheet/sheet.html"
hx-target="#main-content"
hx-push-url="/#sheet"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
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="3" y1="9" x2="21" y2="9" />
<line x1="3" y1="15" x2="21" y2="15" />
<line x1="9" y1="3" x2="9" y2="21" />
<line x1="15" y1="3" x2="15" y2="21" />
</svg>
</div>
<span data-i18n="nav-sheet">Planilhas</span>
</a>
<!-- Slides -->
<a
class="app-item"
href="#slides"
data-section="slides"
role="menuitem"
aria-label="Slides - Presentations"
hx-get="/suite/slides/slides.html"
hx-target="#main-content"
hx-push-url="/#slides"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="2"
y="3"
width="20"
height="14"
rx="2"
ry="2"
/>
<line x1="8" y1="21" x2="16" y2="21" />
<line x1="12" y1="17" x2="12" y2="21" />
</svg>
</div>
<span data-i18n="nav-slides"
>Apresentações</span
>
</a>
<!-- Editor -->
<a
class="app-item"
href="#editor"
data-section="editor"
role="menuitem"
aria-label="Editor - Code & Text"
hx-get="/editor.html"
hx-target="#main-content"
hx-push-url="/#editor"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="16 18 22 12 16 6" />
<polyline points="8 6 2 12 8 18" />
<line x1="12" y1="2" x2="12" y2="22" />
</svg>
</div>
<span data-i18n="nav-editor">Editor</span>
</a>
<!-- Mail -->
<a
class="app-item"
href="#mail"
data-section="mail"
role="menuitem"
aria-label="Mail application"
hx-get="/suite/mail/mail.html"
hx-target="#main-content"
hx-push-url="/#mail"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"
/>
<polyline points="22,6 12,13 2,6" />
</svg>
</div>
<span data-i18n="nav-mail">E-mail</span>
</a>
<!-- Drive -->
<a
class="app-item"
href="#drive"
data-section="drive"
role="menuitem"
aria-label="Drive application"
hx-get="/suite/drive/drive.html"
hx-target="#main-content"
hx-push-url="/#drive"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
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"
/>
</svg>
</div>
<span data-i18n="nav-drive">Arquivos</span>
</a>
<!-- Calendar -->
<a
class="app-item"
href="#calendar"
data-section="calendar"
role="menuitem"
aria-label="Calendar application"
hx-get="/suite/calendar/calendar.html"
hx-target="#main-content"
hx-push-url="/#calendar"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="3"
y="4"
width="18"
height="18"
rx="2"
ry="2"
/>
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
</div>
<span data-i18n="nav-calendar">Calendário</span>
</a>
<!-- Tasks -->
<a
class="app-item"
href="#tasks"
data-section="tasks"
role="menuitem"
aria-label="Tasks application"
hx-get="/suite/tasks/tasks.html"
hx-target="#main-content"
hx-push-url="/#tasks"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M9 11l3 3L22 4" />
<path
d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"
/>
</svg>
</div>
<span data-i18n="nav-tasks">Tarefas</span>
</a>
<!-- Research -->
<a
class="app-item"
href="#research"
data-section="research"
role="menuitem"
aria-label="Research application"
hx-get="/suite/research/research.html"
hx-target="#main-content"
hx-push-url="/#research"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
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" />
<path d="M11 8v6M8 11h6" />
</svg>
</div>
<span data-i18n="nav-research">Pesquisa</span>
</a>
<!-- Meet -->
<a
class="app-item"
href="#meet"
data-section="meet"
role="menuitem"
aria-label="Meet application"
hx-get="/suite/meet/meet.html"
hx-target="#main-content"
hx-push-url="/#meet"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polygon
points="23 7 16 12 23 17 23 7"
/>
<rect
x="1"
y="5"
width="15"
height="14"
rx="2"
ry="2"
/>
</svg>
</div>
<span data-i18n="nav-meet">Reuniões</span>
</a>
<!-- Analytics -->
<a
class="app-item"
href="#analytics"
data-section="analytics"
role="menuitem"
aria-label="Analytics Dashboard"
hx-get="/suite/analytics/analytics.html"
hx-target="#main-content"
hx-push-url="/#analytics"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="18" y1="20" x2="18" y2="10" />
<line x1="12" y1="20" x2="12" y2="4" />
<line x1="6" y1="20" x2="6" y2="14" />
</svg>
</div>
<span data-i18n="nav-analytics">Analytics</span>
</a>
<!-- Monitoring -->
<a
class="app-item"
href="#monitoring"
data-section="monitoring"
role="menuitem"
aria-label="System Monitoring"
hx-get="/suite/monitoring/monitoring.html"
hx-target="#main-content"
hx-push-url="/#monitoring"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" />
<circle cx="12" cy="12" r="6" />
<circle cx="12" cy="12" r="2" />
</svg>
</div>
<span data-i18n="nav-monitoring"
>Monitoring</span
>
</a>
<!-- Sources -->
<a
class="app-item"
href="#sources"
data-section="sources"
role="menuitem"
aria-label="Data Sources"
hx-get="/suite/sources/index.html"
hx-target="#main-content"
hx-push-url="/#sources"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<ellipse cx="12" cy="5" rx="9" ry="3" />
<path
d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"
/>
<path
d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"
/>
</svg>
</div>
<span data-i18n="nav-sources">Sources</span>
</a>
<!-- Tools -->
<a
class="app-item"
href="#tools"
data-section="tools"
role="menuitem"
aria-label="Tools"
hx-get="/suite/tools/compliance.html"
hx-target="#main-content"
hx-push-url="/#tools"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76Z"
/>
</svg>
</div>
<span data-i18n="nav-tools">Tools</span>
</a>
<!-- Designer (.bas Editor) -->
<a
class="app-item"
href="#designer"
data-section="designer"
role="menuitem"
aria-label="Dialog Designer"
hx-get="/designer.html"
hx-target="#main-content"
hx-push-url="/#designer"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M12 2L2 7l10 5 10-5-10-5z" />
<path d="M2 17l10 5 10-5" />
<path d="M2 12l10 5 10-5" />
</svg>
</div>
<span data-i18n="nav-designer">Designer</span>
</a>
<!-- Attendant -->
<a
class="app-item"
href="#attendant"
data-section="attendant"
role="menuitem"
aria-label="Attendant"
hx-get="/suite/attendant/index.html"
hx-target="#main-content"
hx-push-url="/#attendant"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
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" />
<path d="M12 2v2" />
</svg>
</div>
<span data-i18n="nav-attendant">Attendant</span>
</a>
</div>
</nav>
</div>
</div>
<!-- Center: Omnibox (Search + Chat) -->
<div class="header-center">
<div class="omnibox" id="omnibox">
<div class="omnibox-input-area">
<svg
class="omnibox-icon search-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>
<svg
class="omnibox-icon bot-icon"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
style="display: none"
>
<rect x="3" y="11" width="18" height="10" rx="2" />
<circle cx="9" cy="16" r="1" />
<circle cx="15" cy="16" r="1" />
<path d="M8 11V7a4 4 0 0 1 8 0v4" />
</svg>
<input
type="text"
class="omnibox-input"
placeholder="Search or ask anything..."
id="omniboxInput"
autocomplete="off"
/>
<div class="omnibox-hints">
<kbd class="omnibox-shortcut">⌘K</kbd>
<button
class="omnibox-mode-toggle"
id="omniboxModeToggle"
title="Toggle chat mode"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
/>
</svg>
</button>
</div>
</div>
<!-- Expandable Results/Chat Panel -->
<div class="omnibox-panel" id="omniboxPanel">
<!-- Search Results Section -->
<div class="omnibox-results" id="omniboxResults">
<div class="omnibox-section">
<div class="omnibox-section-title">
Quick Actions
</div>
<div
class="omnibox-actions"
id="omniboxActions"
>
<button
class="omnibox-action"
data-action="chat"
>
<span class="action-icon">💬</span>
<span class="action-text"
>Chat with Bot</span
>
<kbd></kbd>
</button>
<button
class="omnibox-action"
data-action="navigate"
data-target="chat"
>
<span class="action-icon">📱</span>
<span class="action-text"
>Open Chat App</span
>
</button>
<button
class="omnibox-action"
data-action="navigate"
data-target="tasks"
>
<span class="action-icon"></span>
<span class="action-text"
>Go to Tasks</span
>
</button>
<button
class="omnibox-action"
data-action="navigate"
data-target="mail"
>
<span class="action-icon">✉️</span>
<span class="action-text"
>Go to Mail</span
>
</button>
</div>
</div>
<div
class="omnibox-section"
id="searchResultsSection"
style="display: none"
>
<div class="omnibox-section-title">
Search Results
</div>
<div
class="omnibox-search-results"
id="searchResultsList"
></div>
</div>
</div>
<!-- Chat Section -->
<div
class="omnibox-chat"
id="omniboxChat"
style="display: none"
>
<div
class="omnibox-chat-messages"
id="omniboxChatMessages"
>
<div class="omnibox-chat-welcome">
<div class="welcome-icon">🤖</div>
<div class="welcome-text">
<strong>Hi! I'm your assistant.</strong>
<p>
Ask me anything or tell me what
you'd like to do.
</p>
</div>
</div>
</div>
<div class="omnibox-chat-input-area">
<input
type="text"
class="omnibox-chat-input"
placeholder="Type your message..."
id="omniboxChatInput"
/>
<button
class="omnibox-send-btn"
id="omniboxSendBtn"
>
<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>
</button>
</div>
<div class="omnibox-chat-footer">
<button
class="omnibox-back-btn"
id="omniboxBackBtn"
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M19 12H5M12 19l-7-7 7-7" />
</svg>
Back to search
</button>
<button
class="omnibox-expand-btn"
id="omniboxExpandBtn"
title="Open full chat"
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="15 3 21 3 21 9" />
<polyline points="9 21 3 21 3 15" />
<line x1="21" y1="3" x2="14" y2="10" />
<line x1="3" y1="21" x2="10" y2="14" />
</svg>
Open full chat
</button>
</div>
</div>
</div>
</div>
<div class="omnibox-backdrop" id="omniboxBackdrop"></div>
</div>
<!-- Right: Notifications + User -->
<div class="header-right">
<!-- Notifications Bell -->
<div class="notifications-menu">
<button
class="icon-button"
id="notificationsBtn"
title="Notifications"
aria-expanded="false"
>
<svg
width="20"
height="20"
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="notifications-badge"
id="notificationsBadge"
style="display: none"
>0</span
>
</button>
<div class="notifications-panel" id="notificationsPanel">
<div class="notifications-panel-header">
<span class="notifications-panel-title"
>Notifications</span
>
<button
class="notifications-clear-btn"
id="clearNotificationsBtn"
>
Clear all
</button>
</div>
<div class="notifications-list" id="notificationsList">
<div class="notifications-empty">
<span>🔔</span>
<p>No notifications</p>
</div>
</div>
</div>
</div>
<button
class="icon-button"
id="settingsBtn"
title="Settings"
aria-expanded="false"
aria-haspopup="true"
>
<svg
width="20"
height="20"
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>
<!-- Settings/Theme Panel -->
<div
class="settings-panel"
id="settingsPanel"
role="menu"
aria-label="Settings"
>
<div class="settings-panel-title">Theme</div>
<div class="theme-grid">
<!-- Core Themes -->
<button
class="theme-option theme-sentient"
data-theme="sentient"
title="Sentient"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">🤖 Sentient</span>
</button>
<button
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>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">🌑 Dark</span>
</button>
<button
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>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">☀️ Light</span>
</button>
<button
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>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">🌊 Ocean</span>
</button>
<button
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>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">💜 Violet</span>
</button>
<button
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>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">🌲 Forest</span>
</button>
<button
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>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">🌅 Sunset</span>
</button>
<!-- Retro Themes -->
<button
class="theme-option theme-cyberpunk"
data-theme="cyberpunk"
title="Cyberpunk"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">🌃 Cyberpunk</span>
</button>
<button
class="theme-option theme-retrowave"
data-theme="retrowave"
title="Retrowave"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">🌴 Retrowave</span>
</button>
<button
class="theme-option theme-vapordream"
data-theme="vapordream"
title="Vapor Dream"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name"
>💭 Vapor Dream</span
>
</button>
<button
class="theme-option theme-y2kglow"
data-theme="y2kglow"
title="Y2K"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">✨ Y2K</span>
</button>
<button
class="theme-option theme-arcadeflash"
data-theme="arcadeflash"
title="Arcade"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">🕹️ Arcade</span>
</button>
<button
class="theme-option theme-discofever"
data-theme="discofever"
title="Disco"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">🪩 Disco</span>
</button>
<button
class="theme-option theme-grungeera"
data-theme="grungeera"
title="Grunge"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">🎸 Grunge</span>
</button>
<!-- Classic Themes -->
<button
class="theme-option theme-jazzage"
data-theme="jazzage"
title="Jazz Age"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">🎺 Jazz Age</span>
</button>
<button
class="theme-option theme-mellowgold"
data-theme="mellowgold"
title="Mellow Gold"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name"
>🌻 Mellow Gold</span
>
</button>
<button
class="theme-option theme-midcenturymod"
data-theme="midcenturymod"
title="Mid Century"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name"
>🏠 Mid Century</span
>
</button>
<button
class="theme-option theme-polaroidmemories"
data-theme="polaroidmemories"
title="Polaroid"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">📷 Polaroid</span>
</button>
<button
class="theme-option theme-saturdaycartoons"
data-theme="saturdaycartoons"
title="Cartoons"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">📺 Cartoons</span>
</button>
<button
class="theme-option theme-seasidepostcard"
data-theme="seasidepostcard"
title="Seaside"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">🏖️ Seaside</span>
</button>
<button
class="theme-option theme-typewriter"
data-theme="typewriter"
title="Typewriter"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">⌨️ Typewriter</span>
</button>
<!-- Tech Themes -->
<button
class="theme-option theme-3dbevel"
data-theme="3dbevel"
title="3D Bevel"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">🔲 3D Bevel</span>
</button>
<button
class="theme-option theme-xeroxui"
data-theme="xeroxui"
title="Xerox UI"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">📠 Xerox UI</span>
</button>
<button
class="theme-option theme-xtreegold"
data-theme="xtreegold"
title="XTree Gold"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">📁 XTree Gold</span>
</button>
</div>
<a
class="settings-link"
href="#settings"
hx-get="/suite/settings/index.html"
hx-target="#main-content"
hx-push-url="/#settings"
>
<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-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>
<span>Settings</span>
</a>
</div>
<!-- User avatar with dropdown -->
<div class="user-avatar-container" style="position: relative">
<button
class="user-avatar"
id="userAvatar"
title="User Account"
aria-label="User account menu"
aria-expanded="false"
aria-haspopup="true"
>
<span aria-hidden="true">U</span>
</button>
<!-- User Menu Dropdown -->
<div class="user-menu" id="userMenu" style="display: none">
<div class="user-menu-header">
<div class="user-avatar-large" id="userAvatarLarge">
U
</div>
<div class="user-info">
<div class="user-name" id="userName">User</div>
<div class="user-email" id="userEmail">
user@example.com
</div>
</div>
</div>
<div class="user-menu-divider"></div>
<nav class="user-menu-nav">
<a
href="#settings"
class="user-menu-item"
hx-get="/suite/settings/index.html"
hx-target="#main-content"
hx-push-url="/#settings"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"
></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<span>Profile</span>
</a>
<a
href="#settings"
class="user-menu-item"
hx-get="/suite/settings/index.html"
hx-target="#main-content"
hx-push-url="/#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"></circle>
<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"
></path>
</svg>
<span>Settings</span>
</a>
<a
href="https://docs.pragmatismo.com.br"
target="_blank"
class="user-menu-item"
>
<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>
<path
d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"
></path>
<line
x1="12"
y1="17"
x2="12.01"
y2="17"
></line>
</svg>
<span>Help & Support</span>
</a>
</nav>
<div class="user-menu-divider"></div>
<a
href="/auth/login.html"
class="user-menu-item login-item"
id="authAction"
style="margin: 8px; color: var(--primary)"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
id="authIcon"
>
<path
d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"
></path>
<polyline points="10 17 15 12 10 7"></polyline>
<line x1="15" y1="12" x2="3" y2="12"></line>
</svg>
<span id="authText">Sign in</span>
</a>
</div>
</div>
</div>
</header>
<!-- Main content area -->
<main id="main-content" role="main">
<!-- Sections will be loaded dynamically -->
</main>
<!-- Core scripts -->
<script src="js/theme-manager.js"></script>
<script src="js/htmx-app.js"></script>
<script src="js/base.js"></script>
<script src="tasks/tasks.js?v=20260102"></script>
<!-- Application initialization -->
<script>
// Simple initialization for HTMX app
// Note: Chat module is self-contained in chat.html
// ==========================================
// OMNIBOX (Search + Chat) Functionality
// ==========================================
const Omnibox = {
isActive: false,
isChatMode: false,
chatHistory: [],
selectedIndex: 0,
init() {
this.omnibox = document.getElementById("omnibox");
this.input = document.getElementById("omniboxInput");
this.panel = document.getElementById("omniboxPanel");
this.backdrop = document.getElementById("omniboxBackdrop");
this.results = document.getElementById("omniboxResults");
this.chat = document.getElementById("omniboxChat");
this.chatMessages = document.getElementById(
"omniboxChatMessages",
);
this.chatInput =
document.getElementById("omniboxChatInput");
this.modeToggle =
document.getElementById("omniboxModeToggle");
this.bindEvents();
},
bindEvents() {
// Input focus/blur
this.input.addEventListener("focus", () => this.open());
this.backdrop.addEventListener("click", () => this.close());
// Input typing
this.input.addEventListener("input", (e) =>
this.handleInput(e.target.value),
);
// Keyboard navigation
this.input.addEventListener("keydown", (e) =>
this.handleKeydown(e),
);
this.chatInput?.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
// Mode toggle
this.modeToggle.addEventListener("click", (e) => {
e.stopPropagation();
this.toggleChatMode();
});
// Action buttons
document
.querySelectorAll(".omnibox-action")
.forEach((btn) => {
btn.addEventListener("click", () =>
this.handleAction(btn),
);
});
// Send button
document
.getElementById("omniboxSendBtn")
?.addEventListener("click", () => this.sendMessage());
// Back button
document
.getElementById("omniboxBackBtn")
?.addEventListener("click", () => this.showResults());
// Expand button
document
.getElementById("omniboxExpandBtn")
?.addEventListener("click", () =>
this.expandToFullChat(),
);
// Global shortcut (Cmd+K / Ctrl+K)
document.addEventListener("keydown", (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
this.input.focus();
this.open();
}
if (e.key === "Escape" && this.isActive) {
this.close();
}
});
},
open() {
this.isActive = true;
this.omnibox.classList.add("active");
this.updateActions();
},
close() {
this.isActive = false;
this.omnibox.classList.remove("active");
this.input.blur();
},
handleInput(value) {
if (value.trim()) {
this.searchContent(value);
} else {
this.showDefaultActions();
}
},
handleKeydown(e) {
const actions = document.querySelectorAll(
'.omnibox-action:not([style*="display: none"])',
);
if (e.key === "ArrowDown") {
e.preventDefault();
this.selectedIndex = Math.min(
this.selectedIndex + 1,
actions.length - 1,
);
this.updateSelection(actions);
} else if (e.key === "ArrowUp") {
e.preventDefault();
this.selectedIndex = Math.max(
this.selectedIndex - 1,
0,
);
this.updateSelection(actions);
} else if (e.key === "Enter") {
e.preventDefault();
const selected = actions[this.selectedIndex];
if (selected) {
this.handleAction(selected);
} else if (this.input.value.trim()) {
// Start chat with the query
this.startChatWithQuery(this.input.value);
}
}
},
updateSelection(actions) {
actions.forEach((a, i) => {
a.classList.toggle(
"selected",
i === this.selectedIndex,
);
});
},
updateActions() {
const currentApp = this.getCurrentApp();
const actionsContainer =
document.getElementById("omniboxActions");
const contextActions = {
chat: [
{
icon: "💬",
text: "New conversation",
action: "chat",
},
{
icon: "📋",
text: "View history",
action: "navigate",
target: "chat",
},
],
mail: [
{
icon: "✉️",
text: "Compose email",
action: "chat",
query: "Help me compose an email",
},
{
icon: "📥",
text: "Check inbox",
action: "navigate",
target: "mail",
},
],
tasks: [
{
icon: "✅",
text: "Create task",
action: "chat",
query: "Create a new task",
},
{
icon: "📋",
text: "Show my tasks",
action: "navigate",
target: "tasks",
},
],
calendar: [
{
icon: "📅",
text: "Schedule event",
action: "chat",
query: "Schedule a meeting",
},
{
icon: "📆",
text: "View calendar",
action: "navigate",
target: "calendar",
},
],
default: [
{
icon: "💬",
text: "Chat with Bot",
action: "chat",
},
{
icon: "🔍",
text: "Search everywhere",
action: "search",
},
],
};
const actions =
contextActions[currentApp] || contextActions.default;
// Add navigation shortcuts
const navActions = [
{
icon: "📱",
text: "Open Chat",
action: "navigate",
target: "chat",
},
{
icon: "✉️",
text: "Open Mail",
action: "navigate",
target: "mail",
},
{
icon: "✓",
text: "Open Tasks",
action: "navigate",
target: "tasks",
},
{
icon: "📅",
text: "Open Calendar",
action: "navigate",
target: "calendar",
},
];
actionsContainer.innerHTML = actions
.concat(navActions)
.map(
(a, i) => `
<button class="omnibox-action ${i === 0 ? "selected" : ""}"
data-action="${a.action}"
data-target="${a.target || ""}"
data-query="${a.query || ""}">
<span class="action-icon">${a.icon}</span>
<span class="action-text">${a.text}</span>
${i === 0 ? "<kbd>↵</kbd>" : ""}
</button>
`,
)
.join("");
// Rebind events
actionsContainer
.querySelectorAll(".omnibox-action")
.forEach((btn) => {
btn.addEventListener("click", () =>
this.handleAction(btn),
);
});
this.selectedIndex = 0;
},
getCurrentApp() {
const hash = window.location.hash.replace("#", "");
return hash || "default";
},
handleAction(btn) {
const action = btn.dataset.action;
const target = btn.dataset.target;
const query = btn.dataset.query;
if (action === "chat") {
if (query) {
this.startChatWithQuery(query);
} else {
this.showChat();
}
} else if (action === "navigate" && target) {
this.navigateTo(target);
} else if (action === "search") {
this.input.focus();
}
},
navigateTo(target) {
this.close();
const link = document.querySelector(
`a[data-section="${target}"]`,
);
if (link) {
link.click();
}
},
toggleChatMode() {
this.isChatMode = !this.isChatMode;
this.omnibox.classList.toggle("chat-mode", this.isChatMode);
if (this.isChatMode) {
this.input.placeholder = "Ask me anything...";
this.showChat();
} else {
this.input.placeholder = "Search or ask anything...";
this.showResults();
}
},
showChat() {
this.results.style.display = "none";
this.chat.style.display = "flex";
this.isChatMode = true;
this.omnibox.classList.add("chat-mode");
this.chatInput?.focus();
},
showResults() {
this.chat.style.display = "none";
this.results.style.display = "block";
this.isChatMode = false;
this.omnibox.classList.remove("chat-mode");
this.input.focus();
},
showDefaultActions() {
document.getElementById(
"searchResultsSection",
).style.display = "none";
this.updateActions();
},
searchContent(query) {
// Show search results section
const resultsSection = document.getElementById(
"searchResultsSection",
);
const resultsList =
document.getElementById("searchResultsList");
resultsSection.style.display = "block";
// Update first action to be "Ask about: query"
const actionsContainer =
document.getElementById("omniboxActions");
const firstAction =
actionsContainer.querySelector(".omnibox-action");
if (firstAction) {
firstAction.dataset.action = "chat";
firstAction.dataset.query = query;
firstAction.querySelector(".action-icon").textContent =
"💬";
firstAction.querySelector(".action-text").textContent =
`Ask: "${query.substring(0, 30)}${query.length > 30 ? "..." : ""}"`;
}
// Simple client-side search of navigation items
const searchResults = this.performSearch(query);
resultsList.innerHTML =
searchResults
.map(
(r) => `
<button class="omnibox-result" data-action="navigate" data-target="${r.target}">
<span class="result-icon">${r.icon}</span>
<div class="result-content">
<span class="result-title">${r.title}</span>
<span class="result-desc">${r.description}</span>
</div>
</button>
`,
)
.join("") ||
'<div class="no-results">No results found. Try asking the bot!</div>';
// Bind click events
resultsList
.querySelectorAll(".omnibox-result")
.forEach((btn) => {
btn.addEventListener("click", () =>
this.navigateTo(btn.dataset.target),
);
});
},
performSearch(query) {
const q = query.toLowerCase();
const items = [
{
target: "chat",
icon: "💬",
title: "Chat",
description: "Chat with the bot",
},
{
target: "mail",
icon: "✉️",
title: "Mail",
description: "Email inbox",
},
{
target: "tasks",
icon: "✓",
title: "Tasks",
description: "Task management",
},
{
target: "calendar",
icon: "📅",
title: "Calendar",
description: "Schedule and events",
},
{
target: "drive",
icon: "📁",
title: "Drive",
description: "File storage",
},
{
target: "paper",
icon: "📄",
title: "Documents",
description: "Document editor",
},
{
target: "sheet",
icon: "📊",
title: "Sheet",
description: "Spreadsheets",
},
{
target: "slides",
icon: "📽️",
title: "Slides",
description: "Presentations",
},
{
target: "editor",
icon: "📝",
title: "Editor",
description: "Code & text editor",
},
{
target: "designer",
icon: "🔷",
title: "Designer",
description: "Visual .bas editor",
},
{
target: "meet",
icon: "📹",
title: "Meet",
description: "Video meetings",
},
{
target: "research",
icon: "🔍",
title: "Research",
description: "Research assistant",
},
{
target: "analytics",
icon: "📊",
title: "Analytics",
description: "Data analytics",
},
{
target: "settings",
icon: "⚙️",
title: "Settings",
description: "App settings",
},
];
return items.filter(
(item) =>
item.title.toLowerCase().includes(q) ||
item.description.toLowerCase().includes(q),
);
},
startChatWithQuery(query) {
this.showChat();
this.input.value = "";
setTimeout(() => {
this.addMessage(query, "user");
this.sendToBot(query);
}, 100);
},
async sendMessage() {
const message = this.chatInput?.value.trim();
if (!message) return;
this.chatInput.value = "";
this.addMessage(message, "user");
await this.sendToBot(message);
},
async sendToBot(message) {
// Show typing indicator
const typingId = this.addTypingIndicator();
try {
// Call the bot API
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message,
context: this.getCurrentApp(),
}),
});
this.removeTypingIndicator(typingId);
if (response.ok) {
const data = await response.json();
this.addMessage(
data.reply ||
data.message ||
"I received your message.",
"bot",
);
// Handle any actions the bot suggests
if (data.action) {
this.handleBotAction(data.action);
}
} else {
this.addMessage(
"Sorry, I encountered an error. Please try again.",
"bot",
);
}
} catch (error) {
this.removeTypingIndicator(typingId);
// Fallback response when API is not available
this.addMessage(
this.getFallbackResponse(message),
"bot",
);
}
},
getFallbackResponse(message) {
const msg = message.toLowerCase();
if (msg.includes("help")) {
return "I can help you navigate the app, search for content, manage tasks, compose emails, and more. What would you like to do?";
} else if (msg.includes("task") || msg.includes("todo")) {
return "Would you like me to open Tasks for you? You can create, view, and manage your tasks there.";
} else if (msg.includes("email") || msg.includes("mail")) {
return "I can help with email! Would you like to compose a new message or check your inbox?";
} else if (
msg.includes("calendar") ||
msg.includes("meeting") ||
msg.includes("schedule")
) {
return "I can help with scheduling. Would you like to create an event or view your calendar?";
}
return (
"I understand you're asking about: \"" +
message +
'". How can I assist you further?'
);
},
addMessage(text, sender) {
const msgDiv = document.createElement("div");
msgDiv.className = `omnibox-message ${sender}`;
msgDiv.innerHTML = `
<div class="message-avatar">${sender === "user" ? "👤" : "🤖"}</div>
<div class="message-content">${this.escapeHtml(text)}</div>
`;
this.chatMessages.appendChild(msgDiv);
this.chatMessages.scrollTop =
this.chatMessages.scrollHeight;
this.chatHistory.push({ role: sender, content: text });
},
addTypingIndicator() {
const id = "typing-" + Date.now();
const typingDiv = document.createElement("div");
typingDiv.id = id;
typingDiv.className = "omnibox-message bot typing";
typingDiv.innerHTML = `
<div class="message-avatar">🤖</div>
<div class="message-content typing-indicator">
<span></span><span></span><span></span>
</div>
`;
this.chatMessages.appendChild(typingDiv);
this.chatMessages.scrollTop =
this.chatMessages.scrollHeight;
return id;
},
removeTypingIndicator(id) {
const el = document.getElementById(id);
if (el) el.remove();
},
handleBotAction(action) {
if (action.navigate) {
setTimeout(
() => this.navigateTo(action.navigate),
1000,
);
}
},
expandToFullChat() {
this.close();
const chatLink = document.querySelector(
'a[data-section="chat"]',
);
if (chatLink) chatLink.click();
},
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
},
};
// Initialize Omnibox when DOM is ready
document.addEventListener("DOMContentLoaded", () => {
Omnibox.init();
console.log("🚀 Initializing General Bots with HTMX...");
// Hide loading overlay
setTimeout(() => {
const loadingOverlay =
document.getElementById("loadingOverlay");
if (loadingOverlay) {
loadingOverlay.classList.add("hidden");
}
}, 500);
// Simple apps menu handling
const appsBtn = document.getElementById("appsButton");
const appsDropdown = document.getElementById("appsDropdown");
const settingsBtn = document.getElementById("settingsBtn");
const settingsPanel = document.getElementById("settingsPanel");
if (appsBtn && appsDropdown) {
appsBtn.addEventListener("click", (e) => {
e.stopPropagation();
const isOpen = appsDropdown.classList.toggle("show");
appsBtn.setAttribute("aria-expanded", isOpen);
// Close settings panel
if (settingsPanel)
settingsPanel.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");
}
});
}
// Settings panel handling
if (settingsBtn && settingsPanel) {
settingsBtn.addEventListener("click", (e) => {
e.stopPropagation();
const isOpen = settingsPanel.classList.toggle("show");
settingsBtn.setAttribute("aria-expanded", isOpen);
// Close apps dropdown
if (appsDropdown) appsDropdown.classList.remove("show");
});
document.addEventListener("click", (e) => {
if (
!settingsPanel.contains(e.target) &&
!settingsBtn.contains(e.target)
) {
settingsPanel.classList.remove("show");
settingsBtn.setAttribute("aria-expanded", "false");
}
});
}
// Theme selection handling
const themeOptions = document.querySelectorAll(".theme-option");
const savedTheme =
localStorage.getItem("gb-theme") || "sentient";
// Apply saved theme
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");
document.body.setAttribute("data-theme", theme);
localStorage.setItem("gb-theme", theme);
themeOptions.forEach((o) =>
o.classList.remove("active"),
);
option.classList.add("active");
// Update theme-color meta tag
const themeColors = {
dark: "#3b82f6",
light: "#3b82f6",
purple: "#a855f7",
green: "#22c55e",
orange: "#f97316",
sentient: "#d4f505",
};
const metaTheme = document.querySelector(
'meta[name="theme-color"]',
);
if (metaTheme) {
metaTheme.setAttribute(
"content",
themeColors[theme] || "#d4f505",
);
}
});
});
// List of sections that appear in header tabs
const headerTabSections = [
"chat",
"paper",
"sheet",
"slides",
"mail",
"calendar",
"drive",
"tasks",
];
// Update active states for navigation (single selection only)
function updateNavigationActive(section) {
// Remove all active states first
document
.querySelectorAll(".app-tab")
.forEach((t) => t.classList.remove("active"));
document
.querySelectorAll(".app-item")
.forEach((i) => i.classList.remove("active"));
appsBtn.classList.remove("active");
// Check if section is in header tabs
const isInHeaderTabs = headerTabSections.includes(section);
// Activate the matching app-tab if in header
if (isInHeaderTabs) {
const headerTab = document.querySelector(
`.app-tab[data-section="${section}"]`,
);
if (headerTab) {
headerTab.classList.add("active");
}
} else {
// Section is NOT in header tabs, activate apps button
appsBtn.classList.add("active");
}
// Always mark the app-item in dropdown as active
const appItem = document.querySelector(
`.app-item[data-section="${section}"]`,
);
if (appItem) {
appItem.classList.add("active");
}
}
// Handle app item clicks - update active state
document.querySelectorAll(".app-item").forEach((item) => {
item.addEventListener("click", function () {
const section = this.getAttribute("data-section");
updateNavigationActive(section);
appsDropdown.classList.remove("show");
appsBtn.setAttribute("aria-expanded", "false");
});
});
// Handle app tab clicks
document.querySelectorAll(".app-tab").forEach((tab) => {
tab.addEventListener("click", function () {
const section = this.getAttribute("data-section");
updateNavigationActive(section);
});
});
// Track currently loaded section to prevent duplicate loads
let currentLoadedSection = null;
let isLoadingSection = false;
let pendingLoadTimeout = null;
// Handle hash navigation
function handleHashChange(fromHtmxSwap = false) {
const hash = window.location.hash.slice(1) || "chat";
// Skip if already loaded this section or currently loading
if (currentLoadedSection === hash || isLoadingSection) {
return;
}
// If this was triggered by HTMX swap, just update tracking
if (fromHtmxSwap) {
currentLoadedSection = hash;
return;
}
updateNavigationActive(hash);
// Abort any pending load timeout
if (pendingLoadTimeout) {
clearTimeout(pendingLoadTimeout);
pendingLoadTimeout = null;
}
// Abort any in-flight HTMX requests for main-content
const mainContent = document.getElementById("main-content");
if (mainContent) {
try {
htmx.trigger(mainContent, "htmx:abort");
} catch (e) {
// Ignore abort errors
}
}
// Validate target exists before triggering HTMX load
if (!mainContent) {
console.warn(
"handleHashChange: #main-content not found, skipping load",
);
return;
}
// Check if main-content is in the DOM
if (!document.body.contains(mainContent)) {
console.warn(
"handleHashChange: #main-content not in DOM, skipping load",
);
return;
}
// Verify main-content has a valid parent (prevents insertBefore errors)
if (!mainContent.parentNode) {
console.warn(
"handleHashChange: #main-content has no parent, skipping load",
);
return;
}
// Debounce the load to prevent rapid double-requests
pendingLoadTimeout = setTimeout(() => {
// Re-check if section changed during debounce
const currentHash =
window.location.hash.slice(1) || "chat";
if (currentLoadedSection === currentHash) {
return;
}
// Trigger HTMX load
const appItem = document.querySelector(
`.app-item[data-section="${currentHash}"]`,
);
if (appItem) {
const hxGet = appItem.getAttribute("hx-get");
if (hxGet) {
try {
isLoadingSection = true;
currentLoadedSection = currentHash;
htmx.ajax("GET", hxGet, {
target: "#main-content",
swap: "innerHTML",
});
} catch (e) {
console.warn(
"handleHashChange: HTMX ajax error:",
e,
);
currentLoadedSection = null;
} finally {
isLoadingSection = false;
}
}
}
}, 50);
}
// Listen for HTMX swaps to track loaded sections and prevent duplicates
document.body.addEventListener("htmx:afterSwap", (event) => {
if (
event.detail.target &&
event.detail.target.id === "main-content"
) {
const hash = window.location.hash.slice(1) || "chat";
currentLoadedSection = hash;
isLoadingSection = false;
}
});
// Reset tracking on swap errors
document.body.addEventListener("htmx:swapError", (event) => {
if (
event.detail.target &&
event.detail.target.id === "main-content"
) {
isLoadingSection = false;
}
});
// Load initial content based on hash or default to chat
window.addEventListener("hashchange", handleHashChange);
// Initial load
setTimeout(() => {
handleHashChange();
}, 100);
// ==========================================================================
// GBAlerts - Global Notification System
// ==========================================================================
window.GBAlerts = (function () {
const notifications = [];
const badge = document.getElementById("notificationsBadge");
const list = document.getElementById("notificationsList");
const btn = document.getElementById("notificationsBtn");
const panel = document.getElementById("notificationsPanel");
const clearBtn = document.getElementById(
"clearNotificationsBtn",
);
function updateBadge() {
if (badge) {
if (notifications.length > 0) {
badge.textContent =
notifications.length > 99
? "99+"
: notifications.length;
badge.style.display = "flex";
} else {
badge.style.display = "none";
}
}
}
function renderList() {
if (!list) return;
if (notifications.length === 0) {
list.innerHTML =
'<div class="notifications-empty"><span>🔔</span><p>No notifications</p></div>';
} else {
list.innerHTML = notifications
.map(
(n, i) => `
<div class="notification-item notification-${n.type}" data-index="${i}">
<div class="notification-icon">${n.icon || "📢"}</div>
<div class="notification-content">
<div class="notification-title">${n.title}</div>
<div class="notification-message">${n.message || ""}</div>
<div class="notification-time">${n.time}</div>
</div>
${n.action ? `<button class="notification-action" onclick="GBAlerts.handleAction(${i})">${n.actionText || "Open"}</button>` : ""}
<button class="notification-dismiss" onclick="GBAlerts.dismiss(${i})">×</button>
</div>
`,
)
.join("");
}
}
function add(notification) {
notifications.unshift({
...notification,
time: new Date().toLocaleTimeString(),
});
updateBadge();
renderList();
// Auto-open panel when new notification arrives
if (panel) {
panel.classList.add("show");
if (btn) btn.setAttribute("aria-expanded", "true");
}
}
function dismiss(index) {
notifications.splice(index, 1);
updateBadge();
renderList();
}
function clearAll() {
notifications.length = 0;
updateBadge();
renderList();
}
function handleAction(index) {
const n = notifications[index];
if (n && n.action) {
if (typeof n.action === "function") {
n.action();
} else if (typeof n.action === "string") {
window.open(n.action, "_blank");
}
}
dismiss(index);
}
// Toggle panel
if (btn && panel) {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const isOpen = panel.classList.toggle("show");
btn.setAttribute("aria-expanded", isOpen);
});
document.addEventListener("click", (e) => {
if (
!panel.contains(e.target) &&
!btn.contains(e.target)
) {
panel.classList.remove("show");
btn.setAttribute("aria-expanded", "false");
}
});
}
if (clearBtn) {
clearBtn.addEventListener("click", clearAll);
}
// Convenience methods for common notifications
return {
add,
dismiss,
clearAll,
handleAction,
taskCompleted: function (title, url) {
add({
type: "success",
icon: "✅",
title: "Task Completed",
message: title,
action: url,
actionText: "Open App",
});
},
taskFailed: function (title, error) {
add({
type: "error",
icon: "❌",
title: "Task Failed",
message: title + (error ? ": " + error : ""),
});
},
info: function (title, message) {
add({
type: "info",
icon: "",
title: title,
message: message,
});
},
warning: function (title, message) {
add({
type: "warning",
icon: "⚠️",
title: title,
message: message,
});
},
connectionStatus: function (status, message) {
add({
type:
status === "connected"
? "success"
: status === "disconnected"
? "error"
: "warning",
icon:
status === "connected"
? "🟢"
: status === "disconnected"
? "🔴"
: "🟡",
title:
"Connection " +
status.charAt(0).toUpperCase() +
status.slice(1),
message: message || "",
});
},
};
})();
// Keyboard shortcuts
document.addEventListener("keydown", (e) => {
// Alt + number for quick app switching
if (e.altKey && !e.ctrlKey && !e.shiftKey) {
const num = parseInt(e.key);
if (num >= 1 && num <= 9) {
const items =
document.querySelectorAll(".app-item");
if (items[num - 1]) {
items[num - 1].click();
e.preventDefault();
}
}
}
// Alt + A to open apps menu
if (e.altKey && e.key.toLowerCase() === "a") {
appsBtn.click();
e.preventDefault();
}
});
});
// User Profile Loading - Integrates with GBAuth service
(function () {
function updateUserUI(user) {
const userName = document.getElementById("userName");
const userEmail = document.getElementById("userEmail");
const userAvatar = document.getElementById("userAvatar");
const userAvatarLarge =
document.getElementById("userAvatarLarge");
const authAction = document.getElementById("authAction");
const authText = document.getElementById("authText");
const authIcon = document.getElementById("authIcon");
if (user && user.email) {
const displayName =
user.display_name ||
user.first_name ||
user.username ||
"User";
const email = user.email || "";
const initial = (
displayName.charAt(0) || "U"
).toUpperCase();
if (userName) userName.textContent = displayName;
if (userEmail) userEmail.textContent = email;
if (userAvatar) {
const avatarSpan = userAvatar.querySelector("span");
if (avatarSpan) avatarSpan.textContent = initial;
}
if (userAvatarLarge)
userAvatarLarge.textContent = initial;
if (authAction) {
authAction.href = "#";
authAction.onclick = function (e) {
e.preventDefault();
if (window.GBAuth) {
window.GBAuth.logout();
}
};
authAction.style.color = "var(--error)";
}
if (authText) authText.textContent = "Sign out";
if (authIcon) {
authIcon.innerHTML =
'<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line>';
}
}
}
// Listen for auth service events
if (window.GBAuth) {
window.GBAuth.on("userUpdated", updateUserUI);
window.GBAuth.on("login", function () {
const user = window.GBAuth.getCurrentUser();
if (user) updateUserUI(user);
});
window.GBAuth.on("logout", function () {
window.location.href = "/auth/login.html";
});
// Load current user on page load
if (window.GBAuth.isAuthenticated()) {
const user = window.GBAuth.getCurrentUser();
if (user && user.email) {
updateUserUI(user);
} else {
// Fetch from API if not cached
window.AuthService.fetchCurrentUser().then(
updateUserUI,
);
}
}
} else {
// Fallback: wait for GBAuth to be available
document.addEventListener("DOMContentLoaded", function () {
setTimeout(function () {
if (
window.GBAuth &&
window.GBAuth.isAuthenticated()
) {
const user = window.GBAuth.getCurrentUser();
if (user) {
updateUserUI(user);
} else {
window.AuthService.fetchCurrentUser().then(
updateUserUI,
);
}
}
}, 100);
});
}
})();
</script>
</body>
</html>