diff --git a/dashboard_project/static/js/ajax-navigation.js b/dashboard_project/static/js/ajax-navigation.js index 12381f4..5e55513 100644 --- a/dashboard_project/static/js/ajax-navigation.js +++ b/dashboard_project/static/js/ajax-navigation.js @@ -6,269 +6,269 @@ * It intercepts link clicks, loads content via AJAX, and updates the browser history. */ +// Function to reload and execute scripts in new content +function reloadScripts(container) { + const scripts = container.getElementsByTagName("script"); + for (let script of scripts) { + const newScript = document.createElement("script"); + + // Copy all attributes + Array.from(script.attributes).forEach((attr) => { + newScript.setAttribute(attr.name, attr.value); + }); + + // Copy inline script content + newScript.textContent = script.textContent; + + // Replace old script with new one + script.parentNode.replaceChild(newScript, script); + } +} + +// Function to initialize scripts needed for the new page content +function initializePageScripts() { + // Re-initialize any custom scripts that might be needed + if (typeof setupAjaxPagination === "function") { + setupAjaxPagination(); + } + + // Initialize Bootstrap tooltips, popovers, etc. + if (typeof bootstrap !== "undefined") { + // Initialize tooltips + const tooltipTriggerList = [].slice.call( + document.querySelectorAll('[data-bs-toggle="tooltip"]'), + ); + tooltipTriggerList.map(function (tooltipTriggerEl) { + return new bootstrap.Tooltip(tooltipTriggerEl); + }); + + // Initialize popovers + const popoverTriggerList = [].slice.call( + document.querySelectorAll('[data-bs-toggle="popover"]'), + ); + popoverTriggerList.map(function (popoverTriggerEl) { + return new bootstrap.Popover(popoverTriggerEl); + }); + } +} + +// Function to set up AJAX navigation for the application +function setupAjaxNavigation() { + // Configuration + const config = { + mainContentSelector: "#main-content", // Selector for the main content area + navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX + loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator + excludePatterns: [ + // URL patterns to exclude from AJAX navigation + /\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads + /\/admin\//, // Admin pages + /\/accounts\/logout\//, // Logout page + /\/api\//, // API endpoints + ], + }; + + // Create and insert the loading indicator + if (!document.getElementById(config.loadingIndicatorId)) { + const loadingIndicator = document.createElement("div"); + loadingIndicator.id = config.loadingIndicatorId; + loadingIndicator.className = "position-fixed top-0 start-0 end-0"; + loadingIndicator.innerHTML = + '
'; + loadingIndicator.style.display = "none"; + loadingIndicator.style.zIndex = "9999"; + document.body.appendChild(loadingIndicator); + } + + // Get the loading indicator element + const loadingIndicator = document.getElementById(config.loadingIndicatorId); + + // Get the main content container + const mainContent = document.querySelector(config.mainContentSelector); + if (!mainContent) { + console.warn("Main content container not found. AJAX navigation disabled."); + return; + } + + // Function to check if a URL should be excluded from AJAX navigation + function shouldExcludeUrl(url) { + for (const pattern of config.excludePatterns) { + if (pattern.test(url)) { + return true; + } + } + return false; + } + + // Function to show the loading indicator + function showLoading() { + loadingIndicator.style.display = "block"; + } + + // Function to hide the loading indicator + function hideLoading() { + loadingIndicator.style.display = "none"; + } + + // Function to handle AJAX page navigation + function handlePageNavigation(url, pushState = true) { + if (shouldExcludeUrl(url)) { + window.location.href = url; + return; + } + showLoading(); + const currentScrollPos = window.scrollY; + fetch(url, { + headers: { + "X-Requested-With": "XMLHttpRequest", + "X-AJAX-Navigation": "true", + Accept: "text/html", + }, + }) + .then((response) => { + if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`); + return response.text(); + }) + .then((html) => { + // Parse the HTML and extract #main-content + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = html; + const newContent = tempDiv.querySelector(config.mainContentSelector); + if (!newContent) throw new Error("Could not find main content in the response"); + mainContent.innerHTML = newContent.innerHTML; + // Update the page title + const titleMatch = html.match(/(.*?)<\/title>/i); + if (titleMatch) document.title = titleMatch[1]; + // Re-initialize dynamic content + reloadScripts(mainContent); + attachEventListeners(); + initializePageScripts(); + if (pushState) { + history.pushState( + { url: url, title: document.title, scrollPos: currentScrollPos }, + document.title, + url, + ); + window.scrollTo({ top: 0, behavior: "smooth" }); + } else if (window.history.state && window.history.state.scrollPos) { + window.scrollTo({ top: window.history.state.scrollPos }); + } + hideLoading(); + }) + .catch((error) => { + console.error("Error during AJAX navigation:", error); + hideLoading(); + window.location.href = url; + }); + } + + // Function to handle form submissions + function handleFormSubmission(form, e) { + e.preventDefault(); + + // Show loading indicator + showLoading(); + + // Get form data + const formData = new FormData(form); + const method = form.method.toLowerCase(); + const url = form.action || window.location.href; + + // Configure fetch options + const fetchOptions = { + method: method, + headers: { + "X-AJAX-Navigation": "true", + }, + }; + + // Handle different HTTP methods + if (method === "get") { + const queryParams = new URLSearchParams(formData).toString(); + handlePageNavigation(url + (queryParams ? "?" + queryParams : "")); + } else { + fetchOptions.body = formData; + + fetch(url, fetchOptions) + .then((response) => { + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return response.json(); + }) + .then((data) => { + if (data.redirect) { + // Handle server-side redirects + handlePageNavigation(data.redirect, true); + } else { + // Update page content + mainContent.innerHTML = data.html; + document.title = data.title || document.title; + + // Re-initialize dynamic content + reloadScripts(mainContent); + attachEventListeners(); + initializePageScripts(); + + // Update URL if needed + if (data.url) { + history.pushState({ url: data.url }, document.title, data.url); + } + } + }) + .catch((error) => { + console.error("Form submission error:", error); + // Fallback to traditional form submission + form.submit(); + }) + .finally(() => { + hideLoading(); + }); + } + } + + // Function to attach event listeners to forms and links + function attachEventListeners() { + // Handle AJAX navigation links + document.querySelectorAll(config.navLinkSelector).forEach((link) => { + if (!link.dataset.ajaxNavInitialized) { + link.addEventListener("click", function (e) { + if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) { + return; // Let the browser handle these cases + } + e.preventDefault(); + handlePageNavigation(this.href); + }); + link.dataset.ajaxNavInitialized = "true"; + } + }); + + // Handle forms with AJAX + document + .querySelectorAll("form.ajax-form, form.search-form, form.filter-form") + .forEach((form) => { + if (!form.dataset.ajaxFormInitialized) { + form.addEventListener("submit", (e) => handleFormSubmission(form, e)); + form.dataset.ajaxFormInitialized = "true"; + } + }); + } + + // Initial attachment of event listeners + attachEventListeners(); + + // Handle browser back/forward buttons + window.addEventListener("popstate", function (event) { + if (event.state && event.state.url) { + handlePageNavigation(event.state.url, false); + } else { + // Fallback to current URL if no state + handlePageNavigation(window.location.href, false); + } + }); +} + document.addEventListener("DOMContentLoaded", function () { // Only initialize if AJAX navigation is enabled if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) { setupAjaxNavigation(); } - - // Function to set up AJAX navigation for the application - function setupAjaxNavigation() { - // Configuration - const config = { - mainContentSelector: "#main-content", // Selector for the main content area - navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX - loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator - excludePatterns: [ - // URL patterns to exclude from AJAX navigation - /\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads - /\/admin\//, // Admin pages - /\/accounts\/logout\//, // Logout page - /\/api\//, // API endpoints - ], - }; - - // Create and insert the loading indicator - if (!document.getElementById(config.loadingIndicatorId)) { - const loadingIndicator = document.createElement("div"); - loadingIndicator.id = config.loadingIndicatorId; - loadingIndicator.className = "position-fixed top-0 start-0 end-0"; - loadingIndicator.innerHTML = - '<div class="progress" style="height: 3px; border-radius: 0;"><div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" style="width: 100%"></div></div>'; - loadingIndicator.style.display = "none"; - loadingIndicator.style.zIndex = "9999"; - document.body.appendChild(loadingIndicator); - } - - // Get the loading indicator element - const loadingIndicator = document.getElementById(config.loadingIndicatorId); - - // Get the main content container - const mainContent = document.querySelector(config.mainContentSelector); - if (!mainContent) { - console.warn("Main content container not found. AJAX navigation disabled."); - return; - } - - // Function to check if a URL should be excluded from AJAX navigation - function shouldExcludeUrl(url) { - for (const pattern of config.excludePatterns) { - if (pattern.test(url)) { - return true; - } - } - return false; - } - - // Function to show the loading indicator - function showLoading() { - loadingIndicator.style.display = "block"; - } - - // Function to hide the loading indicator - function hideLoading() { - loadingIndicator.style.display = "none"; - } - - // Function to handle AJAX page navigation - function handlePageNavigation(url, pushState = true) { - if (shouldExcludeUrl(url)) { - window.location.href = url; - return; - } - showLoading(); - const currentScrollPos = window.scrollY; - fetch(url, { - headers: { - "X-Requested-With": "XMLHttpRequest", - "X-AJAX-Navigation": "true", - Accept: "text/html", - }, - }) - .then((response) => { - if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`); - return response.text(); - }) - .then((html) => { - // Parse the HTML and extract #main-content - const tempDiv = document.createElement("div"); - tempDiv.innerHTML = html; - const newContent = tempDiv.querySelector(config.mainContentSelector); - if (!newContent) throw new Error("Could not find main content in the response"); - mainContent.innerHTML = newContent.innerHTML; - // Update the page title - const titleMatch = html.match(/<title>(.*?)<\/title>/i); - if (titleMatch) document.title = titleMatch[1]; - // Re-initialize dynamic content - reloadScripts(mainContent); - attachEventListeners(); - initializePageScripts(); - if (pushState) { - history.pushState( - { url: url, title: document.title, scrollPos: currentScrollPos }, - document.title, - url, - ); - window.scrollTo({ top: 0, behavior: "smooth" }); - } else if (window.history.state && window.history.state.scrollPos) { - window.scrollTo({ top: window.history.state.scrollPos }); - } - hideLoading(); - }) - .catch((error) => { - console.error("Error during AJAX navigation:", error); - hideLoading(); - window.location.href = url; - }); - } - - // Function to reload and execute scripts in new content - function reloadScripts(container) { - const scripts = container.getElementsByTagName("script"); - for (let script of scripts) { - const newScript = document.createElement("script"); - - // Copy all attributes - Array.from(script.attributes).forEach((attr) => { - newScript.setAttribute(attr.name, attr.value); - }); - - // Copy inline script content - newScript.textContent = script.textContent; - - // Replace old script with new one - script.parentNode.replaceChild(newScript, script); - } - } - - // Function to handle form submissions - function handleFormSubmission(form, e) { - e.preventDefault(); - - // Show loading indicator - showLoading(); - - // Get form data - const formData = new FormData(form); - const method = form.method.toLowerCase(); - const url = form.action || window.location.href; - - // Configure fetch options - const fetchOptions = { - method: method, - headers: { - "X-AJAX-Navigation": "true", - }, - }; - - // Handle different HTTP methods - if (method === "get") { - const queryParams = new URLSearchParams(formData).toString(); - handlePageNavigation(url + (queryParams ? "?" + queryParams : "")); - } else { - fetchOptions.body = formData; - - fetch(url, fetchOptions) - .then((response) => { - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - return response.json(); - }) - .then((data) => { - if (data.redirect) { - // Handle server-side redirects - handlePageNavigation(data.redirect, true); - } else { - // Update page content - mainContent.innerHTML = data.html; - document.title = data.title || document.title; - - // Re-initialize dynamic content - reloadScripts(mainContent); - attachEventListeners(); - initializePageScripts(); - - // Update URL if needed - if (data.url) { - history.pushState({ url: data.url }, document.title, data.url); - } - } - }) - .catch((error) => { - console.error("Form submission error:", error); - // Fallback to traditional form submission - form.submit(); - }) - .finally(() => { - hideLoading(); - }); - } - } - - // Function to initialize scripts needed for the new page content - function initializePageScripts() { - // Re-initialize any custom scripts that might be needed - if (typeof setupAjaxPagination === "function") { - setupAjaxPagination(); - } - - // Initialize Bootstrap tooltips, popovers, etc. - if (typeof bootstrap !== "undefined") { - // Initialize tooltips - const tooltipTriggerList = [].slice.call( - document.querySelectorAll('[data-bs-toggle="tooltip"]'), - ); - tooltipTriggerList.map(function (tooltipTriggerEl) { - return new bootstrap.Tooltip(tooltipTriggerEl); - }); - - // Initialize popovers - const popoverTriggerList = [].slice.call( - document.querySelectorAll('[data-bs-toggle="popover"]'), - ); - popoverTriggerList.map(function (popoverTriggerEl) { - return new bootstrap.Popover(popoverTriggerEl); - }); - } - } - - // Function to attach event listeners to forms and links - function attachEventListeners() { - // Handle AJAX navigation links - document.querySelectorAll(config.navLinkSelector).forEach((link) => { - if (!link.dataset.ajaxNavInitialized) { - link.addEventListener("click", function (e) { - if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) { - return; // Let the browser handle these cases - } - e.preventDefault(); - handlePageNavigation(this.href); - }); - link.dataset.ajaxNavInitialized = "true"; - } - }); - - // Handle forms with AJAX - document - .querySelectorAll("form.ajax-form, form.search-form, form.filter-form") - .forEach((form) => { - if (!form.dataset.ajaxFormInitialized) { - form.addEventListener("submit", (e) => handleFormSubmission(form, e)); - form.dataset.ajaxFormInitialized = "true"; - } - }); - } - - // Initial attachment of event listeners - attachEventListeners(); - - // Handle browser back/forward buttons - window.addEventListener("popstate", function (event) { - if (event.state && event.state.url) { - handlePageNavigation(event.state.url, false); - } else { - // Fallback to current URL if no state - handlePageNavigation(window.location.href, false); - } - }); - } }); diff --git a/dashboard_project/static/js/ajax-pagination.js b/dashboard_project/static/js/ajax-pagination.js index c29d2ac..6dc90cf 100644 --- a/dashboard_project/static/js/ajax-pagination.js +++ b/dashboard_project/static/js/ajax-pagination.js @@ -1,107 +1,106 @@ /** - -* ajax-pagination.js - Common JavaScript for AJAX pagination across the application -* -* This script handles AJAX-based pagination for all pages in the Chat Analytics Dashboard. -* It intercepts pagination link clicks, loads content via AJAX, and updates the browser history. + * ajax-pagination.js - Common JavaScript for AJAX pagination across the application + * + * This script handles AJAX-based pagination for all pages in the Chat Analytics Dashboard. + * It intercepts pagination link clicks, loads content via AJAX, and updates the browser history. */ +// Function to set up AJAX pagination for the entire application +function setupAjaxPagination() { + // Configuration - can be customized per page if needed + const config = { + contentContainerId: "ajax-content-container", // ID of the container to update + loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner + paginationLinkClass: "pagination-link", // Class for pagination links + retryMessage: "An error occurred while loading data. Please try again.", + }; + + // Get container elements + const contentContainer = document.getElementById(config.contentContainerId); + const loadingSpinner = document.getElementById(config.loadingSpinnerId); + + // Exit if the page doesn't have the required elements + if (!contentContainer || !loadingSpinner) return; + + // Function to handle pagination clicks + function setupPaginationListeners() { + document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => { + link.addEventListener("click", function (e) { + e.preventDefault(); + handleAjaxNavigation(this.href); + + // Get the page number if available + const page = this.getAttribute("data-page"); + + // Update browser URL without refreshing + const newUrl = this.href; + history.pushState({ url: newUrl, page: page }, "", newUrl); + }); + }); + } + + // Function to handle AJAX navigation + function handleAjaxNavigation(url) { + // Show loading spinner + contentContainer.classList.add("d-none"); + loadingSpinner.classList.remove("d-none"); + + // Fetch data via AJAX + fetch(url, { + headers: { + "X-Requested-With": "XMLHttpRequest", + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error(`Network response was not ok: ${response.status}`); + } + return response.json(); + }) + .then((data) => { + if (data.status === "success") { + // Update the content + contentContainer.innerHTML = data.html_data; + + // Re-attach event listeners to new pagination links + setupPaginationListeners(); + + // Update any summary data if present and the page provides it + if (typeof updateSummary === "function" && data.summary) { + updateSummary(data); + } + + // Hide loading spinner, show content + loadingSpinner.classList.add("d-none"); + contentContainer.classList.remove("d-none"); + + // Scroll to top of the content container + contentContainer.scrollIntoView({ behavior: "smooth", block: "start" }); + } + }) + .catch((error) => { + console.error("Error fetching data:", error); + loadingSpinner.classList.add("d-none"); + contentContainer.classList.remove("d-none"); + alert(config.retryMessage); + }); + } + + // Initial setup of event listeners + setupPaginationListeners(); + + // Handle browser back/forward buttons + window.addEventListener("popstate", function (event) { + if (event.state && event.state.url) { + handleAjaxNavigation(event.state.url); + } else { + // If no state, fetch current URL + handleAjaxNavigation(window.location.href); + } + }); +} + document.addEventListener("DOMContentLoaded", function () { // Initialize AJAX pagination setupAjaxPagination(); - - // Function to set up AJAX pagination for the entire application - function setupAjaxPagination() { - // Configuration - can be customized per page if needed - const config = { - contentContainerId: "ajax-content-container", // ID of the container to update - loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner - paginationLinkClass: "pagination-link", // Class for pagination links - retryMessage: "An error occurred while loading data. Please try again.", - }; - - // Get container elements - const contentContainer = document.getElementById(config.contentContainerId); - const loadingSpinner = document.getElementById(config.loadingSpinnerId); - - // Exit if the page doesn't have the required elements - if (!contentContainer || !loadingSpinner) return; - - // Function to handle pagination clicks - function setupPaginationListeners() { - document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => { - link.addEventListener("click", function (e) { - e.preventDefault(); - handleAjaxNavigation(this.href); - - // Get the page number if available - const page = this.getAttribute("data-page"); - - // Update browser URL without refreshing - const newUrl = this.href; - history.pushState({ url: newUrl, page: page }, "", newUrl); - }); - }); - } - - // Function to handle AJAX navigation - function handleAjaxNavigation(url) { - // Show loading spinner - contentContainer.classList.add("d-none"); - loadingSpinner.classList.remove("d-none"); - - // Fetch data via AJAX - fetch(url, { - headers: { - "X-Requested-With": "XMLHttpRequest", - }, - }) - .then((response) => { - if (!response.ok) { - throw new Error(`Network response was not ok: ${response.status}`); - } - return response.json(); - }) - .then((data) => { - if (data.status === "success") { - // Update the content - contentContainer.innerHTML = data.html_data; - - // Re-attach event listeners to new pagination links - setupPaginationListeners(); - - // Update any summary data if present and the page provides it - if (typeof updateSummary === "function" && data.summary) { - updateSummary(data); - } - - // Hide loading spinner, show content - loadingSpinner.classList.add("d-none"); - contentContainer.classList.remove("d-none"); - - // Scroll to top of the content container - contentContainer.scrollIntoView({ behavior: "smooth", block: "start" }); - } - }) - .catch((error) => { - console.error("Error fetching data:", error); - loadingSpinner.classList.add("d-none"); - contentContainer.classList.remove("d-none"); - alert(config.retryMessage); - }); - } - - // Initial setup of event listeners - setupPaginationListeners(); - - // Handle browser back/forward buttons - window.addEventListener("popstate", function (event) { - if (event.state && event.state.url) { - handleAjaxNavigation(event.state.url); - } else { - // If no state, fetch current URL - handleAjaxNavigation(window.location.href); - } - }); - } }); diff --git a/dashboard_project/static/js/dashboard.js b/dashboard_project/static/js/dashboard.js index 5ce31c2..938705c 100644 --- a/dashboard_project/static/js/dashboard.js +++ b/dashboard_project/static/js/dashboard.js @@ -7,101 +7,290 @@ * customization. */ -document.addEventListener("DOMContentLoaded", function () { - // Set up Plotly default config based on theme - function updatePlotlyTheme() { - // Force a fresh check of the current theme - const isDarkMode = document.documentElement.getAttribute("data-bs-theme") === "dark"; - console.log("updatePlotlyTheme called - Current theme mode:", isDarkMode ? "dark" : "light"); +// Set up Plotly default config based on theme +function updatePlotlyTheme() { + // Force a fresh check of the current theme + const isDarkMode = document.documentElement.getAttribute("data-bs-theme") === "dark"; + console.log("updatePlotlyTheme called - Current theme mode:", isDarkMode ? "dark" : "light"); - window.plotlyDefaultLayout = { - font: { - color: isDarkMode ? "#f8f9fa" : "#212529", - family: - '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', - }, - paper_bgcolor: isDarkMode ? "#343a40" : "#ffffff", - plot_bgcolor: isDarkMode ? "#343a40" : "#ffffff", - colorway: [ - "#4285F4", - "#EA4335", - "#FBBC05", - "#34A853", - "#FF6D00", - "#46BDC6", - "#DB4437", - "#0F9D58", - "#AB47BC", - "#00ACC1", - ], - margin: { - l: 50, - r: 30, - t: 30, - b: 50, - pad: 10, - }, - hovermode: "closest", - xaxis: { - automargin: true, - gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)", - zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)", - title: { - font: { - color: isDarkMode ? "#f8f9fa" : "#212529", - }, - }, - tickfont: { - color: isDarkMode ? "#f8f9fa" : "#212529", - }, - }, - yaxis: { - automargin: true, - gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)", - zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)", - title: { - font: { - color: isDarkMode ? "#f8f9fa" : "#212529", - }, - }, - tickfont: { - color: isDarkMode ? "#f8f9fa" : "#212529", - }, - }, - legend: { + window.plotlyDefaultLayout = { + font: { + color: isDarkMode ? "#f8f9fa" : "#212529", + family: + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', + }, + paper_bgcolor: isDarkMode ? "#343a40" : "#ffffff", + plot_bgcolor: isDarkMode ? "#343a40" : "#ffffff", + colorway: [ + "#4285F4", + "#EA4335", + "#FBBC05", + "#34A853", + "#FF6D00", + "#46BDC6", + "#DB4437", + "#0F9D58", + "#AB47BC", + "#00ACC1", + ], + margin: { + l: 50, + r: 30, + t: 30, + b: 50, + pad: 10, + }, + hovermode: "closest", + xaxis: { + automargin: true, + gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)", + zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)", + title: { font: { color: isDarkMode ? "#f8f9fa" : "#212529", }, - bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)", }, - modebar: { - bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)", + tickfont: { color: isDarkMode ? "#f8f9fa" : "#212529", - activecolor: isDarkMode ? "#6ea8fe" : "#007bff", }, - }; - - // Config for specific chart types - window.plotlyBarConfig = { - ...window.plotlyDefaultLayout, - bargap: 0.1, - bargroupgap: 0.2, - }; - - window.plotlyPieConfig = { - ...window.plotlyDefaultLayout, - showlegend: true, - legend: { - ...window.plotlyDefaultLayout.legend, - xanchor: "center", - yanchor: "top", - y: -0.2, - x: 0.5, - orientation: "h", + }, + yaxis: { + automargin: true, + gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)", + zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)", + title: { + font: { + color: isDarkMode ? "#f8f9fa" : "#212529", + }, }, - }; + tickfont: { + color: isDarkMode ? "#f8f9fa" : "#212529", + }, + }, + legend: { + font: { + color: isDarkMode ? "#f8f9fa" : "#212529", + }, + bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)", + }, + modebar: { + bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)", + color: isDarkMode ? "#f8f9fa" : "#212529", + activecolor: isDarkMode ? "#6ea8fe" : "#007bff", + }, + }; + + // Config for specific chart types + window.plotlyBarConfig = { + ...window.plotlyDefaultLayout, + bargap: 0.1, + bargroupgap: 0.2, + }; + + window.plotlyPieConfig = { + ...window.plotlyDefaultLayout, + showlegend: true, + legend: { + ...window.plotlyDefaultLayout.legend, + xanchor: "center", + yanchor: "top", + y: -0.2, + x: 0.5, + orientation: "h", + }, + }; +} + +// Chart responsiveness +function resizeCharts() { + const charts = document.querySelectorAll(".chart-container"); + charts.forEach((chart) => { + if (chart.id && window.Plotly) { + Plotly.relayout(chart.id, { + "xaxis.automargin": true, + "yaxis.automargin": true, + }); + } + }); +} + +// Function to update dashboard statistics +function updateDashboardStats(data) { + // Update total sessions + const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3"); + if (totalSessionsElement) { + totalSessionsElement.textContent = data.total_sessions; } + // Update average response time + const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3"); + if (avgResponseTimeElement) { + avgResponseTimeElement.textContent = data.avg_response_time + "s"; + } + + // Update total tokens + const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3"); + if (totalTokensElement) { + totalTokensElement.textContent = data.total_tokens; + } + + // Update total cost + const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3"); + if (totalCostElement) { + totalCostElement.textContent = "€" + data.total_cost; + } +} + +// Function to update dashboard charts +function updateDashboardCharts(data) { + // Check if Plotly is available + if (!window.Plotly) { + console.error("Plotly library not loaded!"); + document.querySelectorAll(".chart-container").forEach((container) => { + container.innerHTML = + '<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>'; + }); + return; + } + + // Update sessions over time chart + const timeSeriesData = data.time_series_data; + if (timeSeriesData && timeSeriesData.length > 0) { + try { + const timeSeriesX = timeSeriesData.map((item) => item.date); + const timeSeriesY = timeSeriesData.map((item) => item.count); + + Plotly.react( + "sessions-time-chart", + [ + { + x: timeSeriesX, + y: timeSeriesY, + type: "scatter", + mode: "lines+markers", + line: { + color: "rgb(75, 192, 192)", + width: 2, + }, + marker: { + color: "rgb(75, 192, 192)", + size: 6, + }, + }, + ], + { + ...window.plotlyDefaultLayout, + margin: { t: 10, r: 10, b: 40, l: 40 }, + xaxis: { + ...window.plotlyDefaultLayout.xaxis, + title: "Date", + }, + yaxis: { + ...window.plotlyDefaultLayout.yaxis, + title: "Number of Sessions", + }, + }, + ); + } catch (error) { + console.error("Error rendering time series chart:", error); + document.getElementById("sessions-time-chart").innerHTML = + '<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>'; + } + } else { + document.getElementById("sessions-time-chart").innerHTML = + '<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>'; + } + + // Update sentiment chart + const sentimentData = data.sentiment_data; + if (sentimentData && sentimentData.length > 0 && window.Plotly) { + const sentimentLabels = sentimentData.map((item) => item.sentiment); + const sentimentValues = sentimentData.map((item) => item.count); + const sentimentColors = sentimentLabels.map((sentiment) => { + if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)"; + if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)"; + if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)"; + return "rgb(201, 203, 207)"; + }); + + Plotly.react( + "sentiment-chart", + [ + { + values: sentimentValues, + labels: sentimentLabels, + type: "pie", + marker: { + colors: sentimentColors, + }, + hole: 0.4, + textinfo: "label+percent", + insidetextorientation: "radial", + }, + ], + { + ...window.plotlyDefaultLayout, + margin: { t: 10, r: 10, b: 10, l: 10 }, + }, + ); + } + + // Update country chart + const countryData = data.country_data; + if (countryData && countryData.length > 0 && window.Plotly) { + const countryLabels = countryData.map((item) => item.country); + const countryValues = countryData.map((item) => item.count); + + Plotly.react( + "country-chart", + [ + { + x: countryValues, + y: countryLabels, + type: "bar", + orientation: "h", + marker: { + color: "rgb(54, 162, 235)", + }, + }, + ], + { + ...window.plotlyDefaultLayout, + margin: { t: 10, r: 10, b: 40, l: 100 }, + xaxis: { + ...window.plotlyDefaultLayout.xaxis, + title: "Number of Sessions", + }, + }, + ); + } + + // Update category chart + const categoryData = data.category_data; + if (categoryData && categoryData.length > 0 && window.Plotly) { + const categoryLabels = categoryData.map((item) => item.category); + const categoryValues = categoryData.map((item) => item.count); + + Plotly.react( + "category-chart", + [ + { + labels: categoryLabels, + values: categoryValues, + type: "pie", + textinfo: "label+percent", + insidetextorientation: "radial", + }, + ], + { + ...window.plotlyDefaultLayout, + margin: { t: 10, r: 10, b: 10, l: 10 }, + }, + ); + } +} + +document.addEventListener("DOMContentLoaded", function () { // Initialize theme setting updatePlotlyTheme(); @@ -122,19 +311,6 @@ document.addEventListener("DOMContentLoaded", function () { observer.observe(document.documentElement, { attributes: true }); - // Chart responsiveness - function resizeCharts() { - const charts = document.querySelectorAll(".chart-container"); - charts.forEach((chart) => { - if (chart.id && window.Plotly) { - Plotly.relayout(chart.id, { - "xaxis.automargin": true, - "yaxis.automargin": true, - }); - } - }); - } - // Refresh all charts with current theme function refreshAllCharts() { if (!window.Plotly) return; @@ -283,182 +459,6 @@ document.addEventListener("DOMContentLoaded", function () { }); } - // Function to update dashboard statistics - function updateDashboardStats(data) { - // Update total sessions - const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3"); - if (totalSessionsElement) { - totalSessionsElement.textContent = data.total_sessions; - } - - // Update average response time - const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3"); - if (avgResponseTimeElement) { - avgResponseTimeElement.textContent = data.avg_response_time + "s"; - } - - // Update total tokens - const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3"); - if (totalTokensElement) { - totalTokensElement.textContent = data.total_tokens; - } - - // Update total cost - const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3"); - if (totalCostElement) { - totalCostElement.textContent = "€" + data.total_cost; - } - } - - // Function to update dashboard charts - function updateDashboardCharts(data) { - // Check if Plotly is available - if (!window.Plotly) { - console.error("Plotly library not loaded!"); - document.querySelectorAll(".chart-container").forEach((container) => { - container.innerHTML = - '<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>'; - }); - return; - } - - // Update sessions over time chart - const timeSeriesData = data.time_series_data; - if (timeSeriesData && timeSeriesData.length > 0) { - try { - const timeSeriesX = timeSeriesData.map((item) => item.date); - const timeSeriesY = timeSeriesData.map((item) => item.count); - - Plotly.react( - "sessions-time-chart", - [ - { - x: timeSeriesX, - y: timeSeriesY, - type: "scatter", - mode: "lines+markers", - line: { - color: "rgb(75, 192, 192)", - width: 2, - }, - marker: { - color: "rgb(75, 192, 192)", - size: 6, - }, - }, - ], - { - ...window.plotlyDefaultLayout, - margin: { t: 10, r: 10, b: 40, l: 40 }, - xaxis: { - ...window.plotlyDefaultLayout.xaxis, - title: "Date", - }, - yaxis: { - ...window.plotlyDefaultLayout.yaxis, - title: "Number of Sessions", - }, - }, - ); - } catch (error) { - console.error("Error rendering time series chart:", error); - document.getElementById("sessions-time-chart").innerHTML = - '<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>'; - } - } else { - document.getElementById("sessions-time-chart").innerHTML = - '<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>'; - } - - // Update sentiment chart - const sentimentData = data.sentiment_data; - if (sentimentData && sentimentData.length > 0 && window.Plotly) { - const sentimentLabels = sentimentData.map((item) => item.sentiment); - const sentimentValues = sentimentData.map((item) => item.count); - const sentimentColors = sentimentLabels.map((sentiment) => { - if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)"; - if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)"; - if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)"; - return "rgb(201, 203, 207)"; - }); - - Plotly.react( - "sentiment-chart", - [ - { - values: sentimentValues, - labels: sentimentLabels, - type: "pie", - marker: { - colors: sentimentColors, - }, - hole: 0.4, - textinfo: "label+percent", - insidetextorientation: "radial", - }, - ], - { - ...window.plotlyDefaultLayout, - margin: { t: 10, r: 10, b: 10, l: 10 }, - }, - ); - } - - // Update country chart - const countryData = data.country_data; - if (countryData && countryData.length > 0 && window.Plotly) { - const countryLabels = countryData.map((item) => item.country); - const countryValues = countryData.map((item) => item.count); - - Plotly.react( - "country-chart", - [ - { - x: countryValues, - y: countryLabels, - type: "bar", - orientation: "h", - marker: { - color: "rgb(54, 162, 235)", - }, - }, - ], - { - ...window.plotlyDefaultLayout, - margin: { t: 10, r: 10, b: 40, l: 100 }, - xaxis: { - ...window.plotlyDefaultLayout.xaxis, - title: "Number of Sessions", - }, - }, - ); - } - - // Update category chart - const categoryData = data.category_data; - if (categoryData && categoryData.length > 0 && window.Plotly) { - const categoryLabels = categoryData.map((item) => item.category); - const categoryValues = categoryData.map((item) => item.count); - - Plotly.react( - "category-chart", - [ - { - labels: categoryLabels, - values: categoryValues, - type: "pie", - textinfo: "label+percent", - insidetextorientation: "radial", - }, - ], - { - ...window.plotlyDefaultLayout, - margin: { t: 10, r: 10, b: 10, l: 10 }, - }, - ); - } - } - // Dashboard selector const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]'); dashboardSelector.forEach((link) => { diff --git a/dashboard_project/static/js/main.js b/dashboard_project/static/js/main.js index 4884e82..4b358a4 100644 --- a/dashboard_project/static/js/main.js +++ b/dashboard_project/static/js/main.js @@ -6,6 +6,58 @@ * the entire application, including navigation, forms, and UI interactions. */ +// Handle sidebar collapse on small screens +function handleSidebarOnResize() { + if (window.innerWidth < 768) { + document.querySelector(".sidebar")?.classList.remove("show"); + } +} + +// Theme toggling functionality +function setTheme(theme, isUserPreference = false) { + console.log("Setting theme to:", theme, "User preference:", isUserPreference); + + // Update the HTML attribute that controls theme + document.documentElement.setAttribute("data-bs-theme", theme); + + // Save the theme preference to localStorage + localStorage.setItem("theme", theme); + + // If this was a user choice (from the toggle button), record that fact + if (isUserPreference) { + localStorage.setItem("userPreferredTheme", "true"); + } + + // Update toggle button icon + const themeToggle = document.getElementById("theme-toggle"); + if (themeToggle) { + const icon = themeToggle.querySelector("i"); + if (theme === "dark") { + icon.classList.remove("fa-moon"); + icon.classList.add("fa-sun"); + themeToggle.setAttribute("title", "Switch to light mode"); + themeToggle.setAttribute("aria-label", "Switch to light mode"); + } else { + icon.classList.remove("fa-sun"); + icon.classList.add("fa-moon"); + themeToggle.setAttribute("title", "Switch to dark mode"); + themeToggle.setAttribute("aria-label", "Switch to dark mode"); + } + } + + // If we're on a page with charts, refresh them to match the theme + if (typeof window.refreshAllCharts === "function") { + console.log("Calling refresh charts from theme toggle"); + // Add a small delay to ensure DOM updates have completed + setTimeout(() => window.refreshAllCharts(), 100); + } +} + +// Check if the user has a system preference for dark mode +function getSystemPreference() { + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; +} + document.addEventListener("DOMContentLoaded", function () { // Initialize tooltips var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); @@ -142,57 +194,7 @@ document.addEventListener("DOMContentLoaded", function () { }); }); - // Handle sidebar collapse on small screens - function handleSidebarOnResize() { - if (window.innerWidth < 768) { - document.querySelector(".sidebar")?.classList.remove("show"); - } - } - - window.addEventListener("resize", handleSidebarOnResize); // Theme toggling functionality - function setTheme(theme, isUserPreference = false) { - console.log("Setting theme to:", theme, "User preference:", isUserPreference); - - // Update the HTML attribute that controls theme - document.documentElement.setAttribute("data-bs-theme", theme); - - // Save the theme preference to localStorage - localStorage.setItem("theme", theme); - - // If this was a user choice (from the toggle button), record that fact - if (isUserPreference) { - localStorage.setItem("userPreferredTheme", "true"); - } - - // Update toggle button icon - const themeToggle = document.getElementById("theme-toggle"); - if (themeToggle) { - const icon = themeToggle.querySelector("i"); - if (theme === "dark") { - icon.classList.remove("fa-moon"); - icon.classList.add("fa-sun"); - themeToggle.setAttribute("title", "Switch to light mode"); - themeToggle.setAttribute("aria-label", "Switch to light mode"); - } else { - icon.classList.remove("fa-sun"); - icon.classList.add("fa-moon"); - themeToggle.setAttribute("title", "Switch to dark mode"); - themeToggle.setAttribute("aria-label", "Switch to dark mode"); - } - } - - // If we're on a page with charts, refresh them to match the theme - if (typeof window.refreshAllCharts === "function") { - console.log("Calling refresh charts from theme toggle"); - // Add a small delay to ensure DOM updates have completed - setTimeout(() => window.refreshAllCharts(), 100); - } - } - - // Check if the user has a system preference for dark mode - function getSystemPreference() { - return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; - } + window.addEventListener("resize", handleSidebarOnResize); // Initialize theme based on saved preference or system setting function initializeTheme() {