// 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) => ` `, ) .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", }, ]; 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 = `
${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", () => { 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; 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); } } 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 = '
🔔

No notifications

'; } 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 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 = ''; } } function loadUserProfile() { var token = localStorage.getItem("gb-access-token") || sessionStorage.getItem("gb-access-token"); if (!token) { console.log("No auth token found"); 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) 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); }); } // 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(); } })();