// Simple initialization for HTMX app
// Note: Chat module is self-contained in chat.html
// ==========================================
// SUITE CORE (Plugin Architecture)
// ==========================================
window.Suite = {
apps: new Map(),
registerApp(id, config) {
console.log(`[Suite] Registering app: ${id}`);
this.apps.set(id, {
id,
icon: "đĻ",
title: id,
description: "",
actions: [],
searchable: true,
...config,
});
// Trigger UI update if Omnibox is initialized
if (typeof Omnibox !== "undefined" && Omnibox.isActive) {
Omnibox.updateActions();
}
},
getApp(id) {
return this.apps.get(id);
},
getAllApps() {
return Array.from(this.apps.values());
},
// Helper to get actions for a specific context
getContextActions(contextId) {
const app = this.apps.get(contextId);
return app ? app.actions : null;
},
};
// ==========================================
// 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");
// Only bind events if all required elements exist
if (this.input && this.backdrop) {
this.bindEvents();
}
},
bindEvents() {
// Defensive: ensure elements exist before binding
if (!this.input || !this.backdrop) {
console.warn("[Omnibox] Required elements not found, skipping event binding");
return;
}
// 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",
},
],
sheet: [
{
icon: "đ",
text: "Analyze data",
action: "chat",
query: "Analyze this data",
},
{
icon: "đ",
text: "Create chart",
action: "chat",
query: "Create a chart for this data",
},
{
icon: "đ˛",
text: "Format currency",
action: "chat",
query: "Format as currency",
},
],
paper: [
{
icon: "đ",
text: "Summarize",
action: "chat",
query: "Summarize this document",
},
{
icon: "â¨",
text: "Fix grammar",
action: "chat",
query: "Fix grammar and spelling",
},
{
icon: "đ",
text: "Make formal",
action: "chat",
query: "Make the tone more formal",
},
],
default: [
{
icon: "đŦ",
text: "Chat with Bot",
action: "chat",
},
{
icon: "đ",
text: "Search everywhere",
action: "search",
},
],
};
// Check for plugin actions first
const pluginActions = window.Suite.getContextActions(currentApp);
let actions;
if (pluginActions && pluginActions.length > 0) {
actions = pluginActions;
} else {
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) => `
`,
)
.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) => `
`,
)
.join("") ||
'
No results found. Try asking the bot!
';
// 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",
},
];
// Add plugin apps
const pluginApps = window.Suite.getAllApps()
.filter((app) => app.searchable)
.map((app) => ({
target: app.id,
icon: app.icon || "đĻ",
title: app.title || app.id,
description: app.description || "App plugin",
}));
const allItems = items.concat(pluginApps);
return allItems.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 = `
${sender === "user" ? "đ¤" : "đ¤"}
${this.escapeHtml(text)}
`;
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 = `
đ¤
`;
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", () => {
// Detect bot name from pathname (e.g., /bot/cristo -> bot_name = "cristo", /edu -> bot_name = "edu")
const detectBotFromPath = () => {
const pathname = window.location.pathname;
// Remove leading/trailing slashes and split
const segments = pathname.replace(/^\/|\/$/g, "").split("/");
// Handle /bot/{bot_name} pattern
if (segments[0] === "bot" && segments[1]) {
return segments[1];
}
// For other patterns, use first segment if it's not a known route
const firstSegment = segments[0];
const knownRoutes = ["suite", "auth", "api", "static", "public", "bot"];
if (firstSegment && !knownRoutes.includes(firstSegment)) {
return firstSegment;
}
return "default";
};
// Set global bot name
window.__INITIAL_BOT_NAME__ = detectBotFromPath();
console.log(`đ¤ Bot detected from path: ${window.__INITIAL_BOT_NAME__}`);
// Check if bot is public to skip authentication
const checkBotPublicStatus = async () => {
try {
const botName = window.__INITIAL_BOT_NAME__;
if (!botName || botName === "default") return;
const response = await fetch(
`/api/bot/config?bot_name=${encodeURIComponent(botName)}`,
);
if (response.ok) {
const config = await response.json();
if (config.public === true) {
window.__BOT_IS_PUBLIC__ = true;
console.log(
`â
Bot '${botName}' is public - authentication not required`,
);
}
}
} catch (e) {
console.warn("Failed to check bot public status:", e);
}
};
Omnibox.init();
console.log("đ Initializing General Bots with HTMX...");
// Check bot public status early
checkBotPublicStatus();
// Provide a global function to hide the loading overlay
window.hideLoadingOverlay = function() {
const loadingOverlay = document.getElementById("loadingOverlay");
if (loadingOverlay && !loadingOverlay.classList.contains("hidden")) {
loadingOverlay.classList.add("hidden");
}
};
// Failsafe: hide after 10 seconds if no message arrives
setTimeout(window.hideLoadingOverlay, 10000);
// 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;
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;
}
});
// Also listen for response errors
document.body.addEventListener("htmx:responseError", (event) => {
if (event.detail.target && event.detail.target.id === "main-content") {
isLoadingSection = false;
currentLoadedSection = null;
}
});
// Load initial content based on hash or default to chat
window.addEventListener("hashchange", handleHashChange);
// Initial load - wait for HTMX to be ready
function initialLoad() {
if (typeof htmx !== "undefined" && htmx.ajax) {
handleHashChange();
} else {
setTimeout(initialLoad, 50);
}
}
// Skip SPA initialization on auth pages (login, register, etc.) and desktop
if (window.location.pathname.startsWith("/auth/")) {
console.log("[SPA] Skipping initialization on auth page");
} else if (document.getElementById('desktop-content')) {
console.log("[SPA] Skipping initialization on desktop page");
} else if (document.readyState === "complete") {
setTimeout(initialLoad, 50);
} else {
window.addEventListener("load", () => {
setTimeout(initialLoad, 50);
});
}
// ==========================================================================
// 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 =
'';
} else {
list.innerHTML = notifications
.map(
(n, i) => `
${n.icon || "đĸ"}
${n.title}
${n.message || ""}
${n.time}
${n.action ? `
` : ""}
`,
)
.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
(function () {
function updateUserUI(user) {
if (!user) return;
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");
const settingsBtn = document.getElementById("settingsBtn");
const appsButton = document.getElementById("appsButton");
const notificationsBtn = document.getElementById("notificationsBtn");
const displayName =
user.display_name || user.first_name || user.username || "User";
const email = user.email || "";
const initial = (displayName.charAt(0) || "U").toUpperCase();
console.log("Updating user UI:", displayName, email);
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();
fetch("/api/auth/logout", {
method: "POST",
}).finally(function () {
localStorage.removeItem("gb-access-token");
localStorage.removeItem("gb-refresh-token");
localStorage.removeItem("gb-user-data");
sessionStorage.removeItem("gb-access-token");
window.location.href = "/auth/login.html";
});
};
authAction.style.color = "var(--error)";
}
if (authText) authText.textContent = "Sign out";
if (authIcon) {
authIcon.innerHTML =
'';
}
if (settingsBtn) settingsBtn.style.display = "";
if (appsButton) appsButton.style.display = "";
if (notificationsBtn) notificationsBtn.style.display = "";
// Show omnibox (search bar) when signed in
const omnibox = document.getElementById("omnibox");
if (omnibox) omnibox.style.display = "";
// Show Drive, Tasks, CRM, and Calendar navigation when signed in (all instances)
const driveTabs = document.querySelectorAll('[data-section="drive"]');
const tasksTabs = document.querySelectorAll('[data-section="tasks"]');
const crmTabs = document.querySelectorAll('[data-section="crm"]');
const calendarTabs = document.querySelectorAll('[data-section="calendar"]');
driveTabs.forEach(tab => tab.style.display = "");
tasksTabs.forEach(tab => tab.style.display = "");
crmTabs.forEach(tab => tab.style.display = "");
calendarTabs.forEach(tab => tab.style.display = "");
}
function loadUserProfile() {
var token =
localStorage.getItem("gb-access-token") ||
sessionStorage.getItem("gb-access-token");
if (!token) {
console.log("No auth token found - user is signed out");
updateSignedOutUI();
return;
}
console.log(
"Loading user profile with token:",
token.substring(0, 10) + "...",
);
fetch("/api/auth/me", {
headers: { Authorization: "Bearer " + token },
})
.then(function (res) {
if (!res.ok) {
console.log("User not authenticated");
updateSignedOutUI();
throw new Error("Not authenticated");
}
return res.json();
})
.then(function (user) {
console.log("User profile loaded:", user);
updateUserUI(user);
localStorage.setItem("gb-user-data", JSON.stringify(user));
})
.catch(function (err) {
console.log("Failed to load user profile:", err);
updateSignedOutUI();
});
}
function updateSignedOutUI() {
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");
const settingsBtn = document.getElementById("settingsBtn");
const appsButton = document.getElementById("appsButton");
const notificationsBtn = document.getElementById("notificationsBtn");
if (userName) userName.textContent = "User";
if (userEmail) userEmail.textContent = "user@example.com";
if (userAvatar) {
const avatarSpan = userAvatar.querySelector("span");
if (avatarSpan) avatarSpan.textContent = "U";
}
if (userAvatarLarge) userAvatarLarge.textContent = "U";
if (authAction) {
authAction.href = "/auth/login.html";
authAction.removeAttribute("onclick");
authAction.style.color = "var(--primary)";
}
if (authText) authText.textContent = "Sign in";
if (authIcon) {
authIcon.innerHTML =
'';
}
if (settingsBtn) settingsBtn.style.display = "none";
if (appsButton) appsButton.style.display = "none";
if (notificationsBtn) notificationsBtn.style.display = "none";
// Hide omnibox (search bar) when signed out
const omnibox = document.getElementById("omnibox");
if (omnibox) omnibox.style.display = "none";
// Hide Drive, Tasks, CRM, and Calendar navigation when signed out (all instances)
const driveTabs = document.querySelectorAll('[data-section="drive"]');
const tasksTabs = document.querySelectorAll('[data-section="tasks"]');
const crmTabs = document.querySelectorAll('[data-section="crm"]');
const calendarTabs = document.querySelectorAll('[data-section="calendar"]');
driveTabs.forEach(tab => tab.style.display = "none");
tasksTabs.forEach(tab => tab.style.display = "none");
crmTabs.forEach(tab => tab.style.display = "none");
calendarTabs.forEach(tab => tab.style.display = "none");
}
// Try to load cached user first
var cachedUser = localStorage.getItem("gb-user-data");
if (cachedUser) {
try {
var user = JSON.parse(cachedUser);
if (user && user.email) {
updateUserUI(user);
}
} catch (e) {}
}
// Always fetch fresh user data
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", loadUserProfile);
} else {
loadUserProfile();
}
})();