diff --git a/.editorconfig b/.editorconfig
index f2cee7e..096689d 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -26,7 +26,7 @@ indent_size = 2
# CSS, JavaScript, and JSON files
[*.{css,scss,js,json}]
-indent_size = 2
+indent_size = 4
# Markdown files
[*.md]
diff --git a/.prettierrc b/.prettierrc
index e2094a6..a17d342 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,30 +1,26 @@
{
- "arrowParens": "always",
- "bracketSpacing": true,
- "embeddedLanguageFormatting": "auto",
- "htmlWhitespaceSensitivity": "css",
- "insertPragma": false,
- "jsxSingleQuote": false,
- "printWidth": 100,
- "proseWrap": "preserve",
- "quoteProps": "as-needed",
- "requirePragma": false,
- "semi": true,
- "singleQuote": false,
- "tabWidth": 2,
- "trailingComma": "es5",
- "useTabs": false,
- "overrides": [
- {
- "files": [
- "*.html"
- ],
- "options": {
- "parser": "jinja-template"
- }
- }
- ],
- "plugins": [
- "prettier-plugin-jinja-template"
- ]
+ "arrowParens": "always",
+ "bracketSpacing": true,
+ "embeddedLanguageFormatting": "auto",
+ "htmlWhitespaceSensitivity": "css",
+ "insertPragma": false,
+ "jsxSingleQuote": false,
+ "printWidth": 100,
+ "proseWrap": "preserve",
+ "quoteProps": "as-needed",
+ "requirePragma": false,
+ "semi": true,
+ "singleQuote": false,
+ "tabWidth": 2,
+ "trailingComma": "es5",
+ "useTabs": false,
+ "overrides": [
+ {
+ "files": ["*.html"],
+ "options": {
+ "parser": "jinja-template"
+ }
+ }
+ ],
+ "plugins": ["prettier-plugin-jinja-template"]
}
diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md
index 7bae7e4..5a4028c 100644
--- a/IMPLEMENTATION_SUMMARY.md
+++ b/IMPLEMENTATION_SUMMARY.md
@@ -3,16 +3,19 @@
## Core Features Implemented
1. **Multi-Tenant Architecture**:
+
- Companies have isolated data and user access
- Users belong to specific companies
- Role-based permissions (admin, company admin, regular user)
2. **Data Management**:
+
- CSV file upload and processing
- Data source management
- Chat session records with comprehensive metadata
3. **Dashboard Visualization**:
+
- Interactive charts using Plotly.js
- Key metrics and KPIs
- Time-series analysis
@@ -21,18 +24,21 @@
- Category distribution
4. **Search and Analysis**:
+
- Full-text search across chat sessions
- Filtering by various attributes
- Detailed view of individual chat sessions
- Transcript viewing
5. **User Management**:
+
- User registration and authentication
- Profile management
- Password change functionality
- Role assignment
6. **Admin Interface**:
+
- Company management
- User administration
- Data source oversight
@@ -67,6 +73,7 @@
### Data Flow
1. **Upload Process**:
+
- File validation
- CSV parsing
- Data normalization
@@ -74,6 +81,7 @@
- Association with company
2. **Dashboard Generation**:
+
- Data aggregation
- Statistical calculations
- Chart data preparation
diff --git a/PRETTIER_SETUP.md b/PRETTIER_SETUP.md
index 89fa15f..3e47249 100644
--- a/PRETTIER_SETUP.md
+++ b/PRETTIER_SETUP.md
@@ -55,10 +55,7 @@ If you need to prevent Prettier from formatting a section of your template:
```html
{# prettier-ignore #}
-
This section will not be formatted by Prettier.
diff --git a/PROJECT_OVERVIEW.md b/PROJECT_OVERVIEW.md
index f7692d7..274c061 100644
--- a/PROJECT_OVERVIEW.md
+++ b/PROJECT_OVERVIEW.md
@@ -70,10 +70,12 @@ This will create:
## Usage Flow
1. **Admin Setup**:
+
- Admin creates companies
- Admin creates users and assigns them to companies
2. **Company Admin**:
+
- Uploads CSV files with chat data
- Creates and configures dashboards
- Manages company users
diff --git a/QUICK_START_GUIDE.md b/QUICK_START_GUIDE.md
index 5033aa9..d62a4a0 100644
--- a/QUICK_START_GUIDE.md
+++ b/QUICK_START_GUIDE.md
@@ -92,10 +92,12 @@ This will create:
### Admin Tasks
1. **Access Admin Panel**:
+
- Go to
- Login with your admin credentials
2. **Create a Company**:
+
- Go to Companies > Add Company
- Fill in the company details and save
@@ -108,10 +110,12 @@ This will create:
### Company Admin Tasks
1. **Login to Dashboard**:
+
- Go to
- Login with your company admin credentials
2. **Upload Chat Data**:
+
- Click on "Upload Data" in the sidebar
- Fill in the data source details
- Select a CSV file containing chat data
@@ -126,11 +130,13 @@ This will create:
### Regular User Tasks
1. **View Dashboard**:
+
- Login with your user credentials
- The dashboard will show automatically
- Select different dashboards from the sidebar
2. **Search Chat Sessions**:
+
- Click on "Search" in the top navigation
- Enter search terms
- Use filters to refine results
@@ -143,25 +149,25 @@ This will create:
Your CSV files should include the following columns:
-| Column | Description | Type |
-| ----------------- | ------------------------------- | -------- |
-| session_id | Unique ID for the chat | String |
-| start_time | Session start time | Datetime |
-| end_time | Session end time | Datetime |
-| ip_address | User's IP address | String |
-| country | User's country | String |
-| language | Chat language | String |
-| messages_sent | Number of messages | Integer |
-| sentiment | Sentiment analysis result | String |
-| escalated | Whether chat was escalated | Boolean |
-| forwarded_hr | Whether chat was sent to HR | Boolean |
-| full_transcript | Complete chat text | Text |
-| avg_response_time | Average response time (seconds) | Float |
-| tokens | Number of tokens used | Integer |
-| tokens_eur | Cost in EUR | Float |
-| category | Chat category | String |
-| initial_msg | First user message | Text |
-| user_rating | User satisfaction rating | String |
+| Column | Description | Type |
+| ------------------- | ------------------------------- | -------- |
+| `session_id` | Unique ID for the chat | String |
+| `start_time` | Session start time | Datetime |
+| `end_time` | Session end time | Datetime |
+| `ip_address` | User's IP address | String |
+| `country` | User's country | String |
+| `language` | Chat language | String |
+| `messages_sent` | Number of messages | Integer |
+| `sentiment` | Sentiment analysis result | String |
+| `escalated` | Whether chat was escalated | Boolean |
+| `forwarded_hr` | Whether chat was sent to HR | Boolean |
+| `full_transcript` | Complete chat text | Text |
+| `avg_response_time` | Average response time (seconds) | Float |
+| `tokens` | Number of tokens used | Integer |
+| `tokens_eur` | Cost in EUR | Float |
+| `category` | Chat category | String |
+| `initial_msg` | First user message | Text |
+| `user_rating` | User satisfaction rating | String |
Example CSV row:
diff --git a/README.md b/README.md
index 29057eb..a9f5e9e 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ A Django application that creates an analytics dashboard for chat session data.
2. Create a virtual environment and activate it:
- ```sh
+ ```sh
uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
```
@@ -99,23 +99,25 @@ A Django application that creates an analytics dashboard for chat session data.
The CSV file should contain the following columns:
-- session_id: Unique identifier for the chat session
-- start_time: When the session started (datetime)
-- end_time: When the session ended (datetime)
-- ip_address: IP address of the user
-- country: Country of the user
-- language: Language used in the conversation
-- messages_sent: Number of messages in the conversation (integer)
-- sentiment: Sentiment analysis of the conversation (string)
-- escalated: Whether the conversation was escalated (boolean)
-- forwarded_hr: Whether the conversation was forwarded to HR (boolean)
-- full_transcript: Full transcript of the conversation (text)
-- avg_response_time: Average response time in seconds (float)
-- tokens: Total number of tokens used (integer)
-- tokens_eur: Cost of tokens in EUR (float)
-- category: Category of the conversation (string)
-- initial_msg: First message from the user (text)
-- user_rating: User rating of the conversation (string)
+| Column | Description |
+| ------------------- | ------------------------------------------------------ |
+| `session_id` | Unique identifier for the chat session |
+| `start_time` | When the session started (datetime) |
+| `end_time` | When the session ended (datetime) |
+| `ip_address` | IP address of the user |
+| `country` | Country of the user |
+| `language` | Language used in the conversation |
+| `messages_sent` | Number of messages in the conversation (integer) |
+| `sentiment` | Sentiment analysis of the conversation (string) |
+| `escalated` | Whether the conversation was escalated (boolean) |
+| `forwarded_hr` | Whether the conversation was forwarded to HR (boolean) |
+| `full_transcript` | Full transcript of the conversation (text) |
+| `avg_response_time` | Average response time in seconds (float) |
+| `tokens` | Total number of tokens used (integer) |
+| `tokens_eur` | Cost of tokens in EUR (float) |
+| `category` | Category of the conversation (string) |
+| `initial_msg` | First message from the user (text) |
+| `user_rating` | User rating of the conversation (string) |
## Future Enhancements
@@ -128,4 +130,4 @@ The CSV file should contain the following columns:
## License
-This project is licensed under the MIT License - see the LICENSE file for details.
+This project is unlicensed. Usage is restricted to personal and educational purposes only. For commercial use, please contact the author.
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..a85dde3
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,13 @@
+# TODO List
+
+- When I zoom into the dasboard page, the graphs don't scale/adjust to fit the window until I completely refresh the page, can we solve that?
+- Add export to CSV and PDF functionality to the dashboard.
+- Add a button to download the CSV file for the selected company.
+- Make it possible to modify the column names in the CSV file through the admin interface.
+- Add possibility to add a company logo in the admin interface.
+- Add periodic download from https://proto.notso.ai/jumbo/chats possibility for the jumbo company.
+ - Authentication: Basic Auth
+ - URL: https://proto.notso.ai/jumbo/chats
+ - Username: jumboadmin
+ - Password: jumboadmin
+
diff --git a/dashboard_project/dashboard/views.py b/dashboard_project/dashboard/views.py
index 74b55bc..53c3ae0 100644
--- a/dashboard_project/dashboard/views.py
+++ b/dashboard_project/dashboard/views.py
@@ -9,6 +9,7 @@ from django.core.paginator import Paginator
from django.db.models import Avg, Q
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
+from django.template.loader import render_to_string
from django.utils import timezone
from .forms import DashboardForm, DataSourceUploadForm
@@ -16,6 +17,11 @@ from .models import ChatSession, Dashboard, DataSource
from .utils import generate_dashboard_data, process_csv_file
+def is_ajax_navigation(request):
+ """Check if this is an AJAX navigation request"""
+ return request.headers.get("X-AJAX-Navigation") == "true"
+
+
@login_required
def dashboard_view(request):
"""Main dashboard view"""
@@ -57,23 +63,27 @@ def dashboard_view(request):
# Generate dashboard data
dashboard_data = generate_dashboard_data(selected_dashboard.data_sources.all())
- # Convert dashboard data to JSON for use in JavaScript
- dashboard_data_json = json.dumps(
- {
- "sentiment_data": dashboard_data["sentiment_data"],
- "country_data": dashboard_data["country_data"],
- "category_data": dashboard_data["category_data"],
- "time_series_data": dashboard_data["time_series_data"],
- }
- )
+ # Convert each component of dashboard data to JSON
+ sentiment_data_json = json.dumps(dashboard_data["sentiment_data"])
+ country_data_json = json.dumps(dashboard_data["country_data"])
+ category_data_json = json.dumps(dashboard_data["category_data"])
+ time_series_data_json = json.dumps(dashboard_data["time_series_data"])
context = {
"dashboards": dashboards,
"selected_dashboard": selected_dashboard,
"dashboard_data": dashboard_data,
- "dashboard_data_json": dashboard_data_json,
+ "sentiment_data_json": sentiment_data_json,
+ "country_data_json": country_data_json,
+ "category_data_json": category_data_json,
+ "time_series_data_json": time_series_data_json,
}
+ # Check if this is an AJAX navigation request
+ if is_ajax_navigation(request):
+ html_content = render_to_string("dashboard/dashboard.html", context, request=request)
+ return JsonResponse({"html": html_content, "title": "Dashboard | Chat Analytics"})
+
return render(request, "dashboard/dashboard.html", context)
@@ -124,6 +134,11 @@ def upload_data_view(request):
"data_sources": data_sources,
}
+ # Check if this is an AJAX navigation request
+ if is_ajax_navigation(request):
+ html_content = render_to_string("dashboard/upload.html", context, request=request)
+ return JsonResponse({"html": html_content, "title": "Upload Data | Chat Analytics"})
+
return render(request, "dashboard/upload.html", context)
@@ -155,6 +170,11 @@ def data_source_detail_view(request, data_source_id):
"page_obj": page_obj,
}
+ # Check if this is an AJAX navigation request
+ if is_ajax_navigation(request):
+ html_content = render_to_string("dashboard/data_source_detail.html", context, request=request)
+ return JsonResponse({"html": html_content, "title": f"{data_source.name} | Chat Analytics"})
+
return render(request, "dashboard/data_source_detail.html", context)
@@ -177,6 +197,11 @@ def chat_session_detail_view(request, session_id):
"session": chat_session,
}
+ # Check if this is an AJAX navigation request
+ if is_ajax_navigation(request):
+ html_content = render_to_string("dashboard/chat_session_detail.html", context, request=request)
+ return JsonResponse({"html": html_content, "title": f"Chat Session {session_id} | Chat Analytics"})
+
return render(request, "dashboard/chat_session_detail.html", context)
@@ -209,6 +234,11 @@ def create_dashboard_view(request):
"is_create": True,
}
+ # Check if this is an AJAX navigation request
+ if is_ajax_navigation(request):
+ html_content = render_to_string("dashboard/dashboard_form.html", context, request=request)
+ return JsonResponse({"html": html_content, "title": "Create Dashboard | Chat Analytics"})
+
return render(request, "dashboard/dashboard_form.html", context)
@@ -244,6 +274,11 @@ def edit_dashboard_view(request, dashboard_id):
"is_create": False,
}
+ # Check if this is an AJAX navigation request
+ if is_ajax_navigation(request):
+ html_content = render_to_string("dashboard/dashboard_form.html", context, request=request)
+ return JsonResponse({"html": html_content, "title": f"Edit Dashboard: {dashboard.name} | Chat Analytics"})
+
return render(request, "dashboard/dashboard_form.html", context)
@@ -313,8 +348,33 @@ def dashboard_data_api(request, dashboard_id):
if not company:
return JsonResponse({"error": "User not associated with a company"}, status=403)
+ # Get time range filter if provided
+ time_range = request.GET.get("time_range", "all")
+
dashboard = get_object_or_404(Dashboard, id=dashboard_id, company=company)
- dashboard_data = generate_dashboard_data(dashboard.data_sources.all())
+
+ # Get data sources for this dashboard
+ data_sources = dashboard.data_sources.all()
+
+ # Apply time filter if needed
+ filtered_data_sources = data_sources
+ if time_range and time_range != "all":
+ # This is a placeholder comment - implement time filtering in a real app
+ # You would filter ChatSessions based on time_range here
+ pass
+
+ # Generate the dashboard data
+ dashboard_data = generate_dashboard_data(filtered_data_sources)
+
+ # Ensure values are JSON serializable
+ for key in ["sentiment_data", "country_data", "category_data"]:
+ dashboard_data[key] = list(dashboard_data[key])
+
+ # Format time series data for proper date serialization
+ if "time_series_data" in dashboard_data:
+ for item in dashboard_data["time_series_data"]:
+ if "date" in item and not isinstance(item["date"], str):
+ item["date"] = item["date"].strftime("%Y-%m-%d")
return JsonResponse(dashboard_data)
@@ -373,6 +433,34 @@ def search_chat_sessions(request):
"data_source": data_source,
}
+ # Check if this is an AJAX navigation request
+ if is_ajax_navigation(request):
+ html_content = render_to_string("dashboard/search_results.html", context, request=request)
+ return JsonResponse({"html": html_content, "title": "Search Chat Sessions | Chat Analytics"})
+
+ # Check if this is an AJAX pagination request
+ if request.headers.get("X-Requested-With") == "XMLHttpRequest":
+ return JsonResponse(
+ {
+ "status": "success",
+ "html_data": render(request, "dashboard/partials/search_results_table.html", context).content.decode(
+ "utf-8"
+ ),
+ "page_obj": {
+ "number": page_obj.number,
+ "has_previous": page_obj.has_previous(),
+ "has_next": page_obj.has_next(),
+ "previous_page_number": page_obj.previous_page_number() if page_obj.has_previous() else None,
+ "next_page_number": page_obj.next_page_number() if page_obj.has_next() else None,
+ "paginator": {
+ "num_pages": page_obj.paginator.num_pages,
+ "count": page_obj.paginator.count,
+ },
+ },
+ "query": query,
+ }
+ )
+
return render(request, "dashboard/search_results.html", context)
@@ -449,4 +537,33 @@ def data_view(request):
"escalation_rate": escalation_rate,
}
+ # Check if this is an AJAX navigation request
+ if is_ajax_navigation(request):
+ html_content = render_to_string("dashboard/data_view.html", context, request=request)
+ return JsonResponse({"html": html_content, "title": "Data View | Chat Analytics"})
+
+ # Check if this is an AJAX pagination request
+ if request.headers.get("X-Requested-With") == "XMLHttpRequest":
+ return JsonResponse(
+ {
+ "status": "success",
+ "html_data": render(request, "dashboard/partials/data_table.html", context).content.decode("utf-8"),
+ "page_obj": {
+ "number": page_obj.number,
+ "has_previous": page_obj.has_previous(),
+ "has_next": page_obj.has_next(),
+ "previous_page_number": page_obj.previous_page_number() if page_obj.has_previous() else None,
+ "next_page_number": page_obj.next_page_number() if page_obj.has_next() else None,
+ "paginator": {
+ "num_pages": page_obj.paginator.num_pages,
+ "count": page_obj.paginator.count,
+ },
+ },
+ "view": view,
+ "avg_response_time": avg_response_time,
+ "avg_messages": avg_messages,
+ "escalation_rate": escalation_rate,
+ }
+ )
+
return render(request, "dashboard/data_view.html", context)
diff --git a/dashboard_project/static/css/dashboard.css b/dashboard_project/static/css/dashboard.css
index e7d52ea..2f4b788 100644
--- a/dashboard_project/static/css/dashboard.css
+++ b/dashboard_project/static/css/dashboard.css
@@ -260,3 +260,21 @@
font-size: 1.5rem;
}
}
+
+/* --- Stat Boxes Alignment Fix (Bottom Align, No Overlap) --- */
+.stats-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1.5rem;
+ align-items: stretch;
+}
+.stats-card {
+ flex: 1 1 0;
+ min-width: 200px;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end; /* Push content to bottom */
+ align-items: flex-start;
+ box-sizing: border-box;
+ /* Remove min-height/height for natural stretch */
+}
diff --git a/dashboard_project/static/js/ajax-navigation.js b/dashboard_project/static/js/ajax-navigation.js
new file mode 100644
index 0000000..9a7d57a
--- /dev/null
+++ b/dashboard_project/static/js/ajax-navigation.js
@@ -0,0 +1,273 @@
+/**
+ * ajax-navigation.js - JavaScript for AJAX-based navigation across the entire application
+ *
+ * This script handles AJAX navigation between pages in the Chat Analytics Dashboard.
+ * It intercepts link clicks, loads content via AJAX, and updates the browser history.
+ */
+
+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 =
+ '
';
+ 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 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
new file mode 100644
index 0000000..bcec9a7
--- /dev/null
+++ b/dashboard_project/static/js/ajax-pagination.js
@@ -0,0 +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.
+ */
+
+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 ffadc90..fe0d29c 100644
--- a/dashboard_project/static/js/dashboard.js
+++ b/dashboard_project/static/js/dashboard.js
@@ -55,8 +55,14 @@ document.addEventListener("DOMContentLoaded", function () {
document.querySelector("main").appendChild(loadingOverlay);
fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`)
- .then((response) => response.json())
+ .then((response) => {
+ if (!response.ok) {
+ throw new Error(`Network response was not ok: ${response.status}`);
+ }
+ return response.json();
+ })
.then((data) => {
+ console.log("Dashboard API response:", data);
updateDashboardStats(data);
updateDashboardCharts(data);
@@ -115,40 +121,59 @@ document.addEventListener("DOMContentLoaded", function () {
// 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 =
+ 'Chart library not available. Please refresh the page.
';
+ });
+ return;
+ }
+
// Update sessions over time chart
const timeSeriesData = data.time_series_data;
- if (timeSeriesData && timeSeriesData.length > 0 && window.Plotly) {
- const timeSeriesX = timeSeriesData.map((item) => item.date);
- const timeSeriesY = timeSeriesData.map((item) => item.count);
+ 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",
- [
+ 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,
+ },
+ },
+ ],
{
- x: timeSeriesX,
- y: timeSeriesY,
- type: "scatter",
- mode: "lines+markers",
- line: {
- color: "rgb(75, 192, 192)",
- width: 2,
+ margin: { t: 10, r: 10, b: 40, l: 40 },
+ xaxis: {
+ title: "Date",
},
- marker: {
- color: "rgb(75, 192, 192)",
- size: 6,
+ yaxis: {
+ title: "Number of Sessions",
},
- },
- ],
- {
- margin: { t: 10, r: 10, b: 40, l: 40 },
- xaxis: {
- title: "Date",
- },
- yaxis: {
- title: "Number of Sessions",
- },
- }
- );
+ }
+ );
+ } catch (error) {
+ console.error("Error rendering time series chart:", error);
+ document.getElementById("sessions-time-chart").innerHTML =
+ '';
+ }
+ } else {
+ document.getElementById("sessions-time-chart").innerHTML =
+ 'No time series data available
';
}
// Update sentiment chart
diff --git a/dashboard_project/templates/base.html b/dashboard_project/templates/base.html
index a498201..7fae940 100644
--- a/dashboard_project/templates/base.html
+++ b/dashboard_project/templates/base.html
@@ -49,21 +49,21 @@
#}
{# {% endif %} #}
- {% block content %}
- {% endblock %}
+