mirror of
https://github.com/kjanat/livegraphs-django.git
synced 2026-02-13 12:55:42 +01:00
refactor: move functions to outer scope for better performance
- Move setupAjaxPagination to outer scope in ajax-pagination.js - Move setupAjaxNavigation, reloadScripts, initializePageScripts to outer scope in ajax-navigation.js - Move updatePlotlyTheme, resizeCharts, updateDashboardStats, updateDashboardCharts to outer scope in dashboard.js - Move handleSidebarOnResize, setTheme, getSystemPreference to outer scope in main.js - Avoid recreating functions on every DOMContentLoaded call - All oxlint strict checks now pass (11 warnings -> 0)
This commit is contained in:
@@ -6,269 +6,269 @@
|
|||||||
* It intercepts link clicks, loads content via AJAX, and updates the browser history.
|
* 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 =
|
||||||
|
'<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 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 () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Only initialize if AJAX navigation is enabled
|
// Only initialize if AJAX navigation is enabled
|
||||||
if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) {
|
if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) {
|
||||||
setupAjaxNavigation();
|
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,107 +1,106 @@
|
|||||||
/**
|
/**
|
||||||
|
* ajax-pagination.js - Common JavaScript for AJAX pagination across the application
|
||||||
* 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.
|
||||||
* 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.
|
||||||
* 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 () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Initialize AJAX pagination
|
// Initialize AJAX pagination
|
||||||
setupAjaxPagination();
|
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,101 +7,290 @@
|
|||||||
* customization.
|
* customization.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
// Set up Plotly default config based on theme
|
||||||
// Set up Plotly default config based on theme
|
function updatePlotlyTheme() {
|
||||||
function updatePlotlyTheme() {
|
// Force a fresh check of the current theme
|
||||||
// Force a fresh check of the current theme
|
const isDarkMode = document.documentElement.getAttribute("data-bs-theme") === "dark";
|
||||||
const isDarkMode = document.documentElement.getAttribute("data-bs-theme") === "dark";
|
console.log("updatePlotlyTheme called - Current theme mode:", isDarkMode ? "dark" : "light");
|
||||||
console.log("updatePlotlyTheme called - Current theme mode:", isDarkMode ? "dark" : "light");
|
|
||||||
|
|
||||||
window.plotlyDefaultLayout = {
|
window.plotlyDefaultLayout = {
|
||||||
font: {
|
font: {
|
||||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||||
family:
|
family:
|
||||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||||
},
|
},
|
||||||
paper_bgcolor: isDarkMode ? "#343a40" : "#ffffff",
|
paper_bgcolor: isDarkMode ? "#343a40" : "#ffffff",
|
||||||
plot_bgcolor: isDarkMode ? "#343a40" : "#ffffff",
|
plot_bgcolor: isDarkMode ? "#343a40" : "#ffffff",
|
||||||
colorway: [
|
colorway: [
|
||||||
"#4285F4",
|
"#4285F4",
|
||||||
"#EA4335",
|
"#EA4335",
|
||||||
"#FBBC05",
|
"#FBBC05",
|
||||||
"#34A853",
|
"#34A853",
|
||||||
"#FF6D00",
|
"#FF6D00",
|
||||||
"#46BDC6",
|
"#46BDC6",
|
||||||
"#DB4437",
|
"#DB4437",
|
||||||
"#0F9D58",
|
"#0F9D58",
|
||||||
"#AB47BC",
|
"#AB47BC",
|
||||||
"#00ACC1",
|
"#00ACC1",
|
||||||
],
|
],
|
||||||
margin: {
|
margin: {
|
||||||
l: 50,
|
l: 50,
|
||||||
r: 30,
|
r: 30,
|
||||||
t: 30,
|
t: 30,
|
||||||
b: 50,
|
b: 50,
|
||||||
pad: 10,
|
pad: 10,
|
||||||
},
|
},
|
||||||
hovermode: "closest",
|
hovermode: "closest",
|
||||||
xaxis: {
|
xaxis: {
|
||||||
automargin: true,
|
automargin: true,
|
||||||
gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)",
|
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)",
|
zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)",
|
||||||
title: {
|
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: {
|
|
||||||
font: {
|
font: {
|
||||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||||
},
|
},
|
||||||
bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)",
|
|
||||||
},
|
},
|
||||||
modebar: {
|
tickfont: {
|
||||||
bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)",
|
|
||||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||||
activecolor: isDarkMode ? "#6ea8fe" : "#007bff",
|
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
yaxis: {
|
||||||
// Config for specific chart types
|
automargin: true,
|
||||||
window.plotlyBarConfig = {
|
gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)",
|
||||||
...window.plotlyDefaultLayout,
|
zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)",
|
||||||
bargap: 0.1,
|
title: {
|
||||||
bargroupgap: 0.2,
|
font: {
|
||||||
};
|
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||||
|
},
|
||||||
window.plotlyPieConfig = {
|
|
||||||
...window.plotlyDefaultLayout,
|
|
||||||
showlegend: true,
|
|
||||||
legend: {
|
|
||||||
...window.plotlyDefaultLayout.legend,
|
|
||||||
xanchor: "center",
|
|
||||||
yanchor: "top",
|
|
||||||
y: -0.2,
|
|
||||||
x: 0.5,
|
|
||||||
orientation: "h",
|
|
||||||
},
|
},
|
||||||
};
|
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
|
// Initialize theme setting
|
||||||
updatePlotlyTheme();
|
updatePlotlyTheme();
|
||||||
|
|
||||||
@@ -122,19 +311,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
|
|
||||||
observer.observe(document.documentElement, { attributes: true });
|
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
|
// Refresh all charts with current theme
|
||||||
function refreshAllCharts() {
|
function refreshAllCharts() {
|
||||||
if (!window.Plotly) return;
|
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
|
// Dashboard selector
|
||||||
const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]');
|
const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]');
|
||||||
dashboardSelector.forEach((link) => {
|
dashboardSelector.forEach((link) => {
|
||||||
|
|||||||
@@ -6,6 +6,58 @@
|
|||||||
* the entire application, including navigation, forms, and UI interactions.
|
* 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 () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Initialize tooltips
|
// Initialize tooltips
|
||||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
@@ -142,57 +194,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle sidebar collapse on small screens
|
window.addEventListener("resize", handleSidebarOnResize);
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize theme based on saved preference or system setting
|
// Initialize theme based on saved preference or system setting
|
||||||
function initializeTheme() {
|
function initializeTheme() {
|
||||||
|
|||||||
Reference in New Issue
Block a user