Enhance AJAX navigation and pagination across dashboard templates

- Implemented AJAX-based navigation for links and forms to improve user experience.
- Added loading indicators during AJAX requests to enhance feedback.
- Refactored data tables and search results to load content dynamically via AJAX.
- Created partial templates for data tables and search results to streamline rendering.
- Updated pagination links to work with AJAX, maintaining browser history.
- Added JavaScript files for handling AJAX navigation and pagination.
- Improved session detail view with conditional rendering for action buttons.
- Updated Docker Compose file for consistency in version formatting.
- Created a TODO list for future enhancements and features.
This commit is contained in:
2025-05-17 02:28:38 +02:00
parent fe69bdbc94
commit 482bea1ba5
21 changed files with 1275 additions and 565 deletions

View File

@ -26,7 +26,7 @@ indent_size = 2
# CSS, JavaScript, and JSON files # CSS, JavaScript, and JSON files
[*.{css,scss,js,json}] [*.{css,scss,js,json}]
indent_size = 2 indent_size = 4
# Markdown files # Markdown files
[*.md] [*.md]

View File

@ -16,15 +16,11 @@
"useTabs": false, "useTabs": false,
"overrides": [ "overrides": [
{ {
"files": [ "files": ["*.html"],
"*.html"
],
"options": { "options": {
"parser": "jinja-template" "parser": "jinja-template"
} }
} }
], ],
"plugins": [ "plugins": ["prettier-plugin-jinja-template"]
"prettier-plugin-jinja-template"
]
} }

View File

@ -3,16 +3,19 @@
## Core Features Implemented ## Core Features Implemented
1. **Multi-Tenant Architecture**: 1. **Multi-Tenant Architecture**:
- Companies have isolated data and user access - Companies have isolated data and user access
- Users belong to specific companies - Users belong to specific companies
- Role-based permissions (admin, company admin, regular user) - Role-based permissions (admin, company admin, regular user)
2. **Data Management**: 2. **Data Management**:
- CSV file upload and processing - CSV file upload and processing
- Data source management - Data source management
- Chat session records with comprehensive metadata - Chat session records with comprehensive metadata
3. **Dashboard Visualization**: 3. **Dashboard Visualization**:
- Interactive charts using Plotly.js - Interactive charts using Plotly.js
- Key metrics and KPIs - Key metrics and KPIs
- Time-series analysis - Time-series analysis
@ -21,18 +24,21 @@
- Category distribution - Category distribution
4. **Search and Analysis**: 4. **Search and Analysis**:
- Full-text search across chat sessions - Full-text search across chat sessions
- Filtering by various attributes - Filtering by various attributes
- Detailed view of individual chat sessions - Detailed view of individual chat sessions
- Transcript viewing - Transcript viewing
5. **User Management**: 5. **User Management**:
- User registration and authentication - User registration and authentication
- Profile management - Profile management
- Password change functionality - Password change functionality
- Role assignment - Role assignment
6. **Admin Interface**: 6. **Admin Interface**:
- Company management - Company management
- User administration - User administration
- Data source oversight - Data source oversight
@ -67,6 +73,7 @@
### Data Flow ### Data Flow
1. **Upload Process**: 1. **Upload Process**:
- File validation - File validation
- CSV parsing - CSV parsing
- Data normalization - Data normalization
@ -74,6 +81,7 @@
- Association with company - Association with company
2. **Dashboard Generation**: 2. **Dashboard Generation**:
- Data aggregation - Data aggregation
- Statistical calculations - Statistical calculations
- Chart data preparation - Chart data preparation

View File

@ -55,10 +55,7 @@ If you need to prevent Prettier from formatting a section of your template:
```html ```html
{# prettier-ignore #} {# prettier-ignore #}
<div> <div>This section will not be formatted by Prettier.</div>
This section will not be formatted
by Prettier.
</div>
<!-- prettier-ignore --> <!-- prettier-ignore -->
<div> <div>

View File

@ -70,10 +70,12 @@ This will create:
## Usage Flow ## Usage Flow
1. **Admin Setup**: 1. **Admin Setup**:
- Admin creates companies - Admin creates companies
- Admin creates users and assigns them to companies - Admin creates users and assigns them to companies
2. **Company Admin**: 2. **Company Admin**:
- Uploads CSV files with chat data - Uploads CSV files with chat data
- Creates and configures dashboards - Creates and configures dashboards
- Manages company users - Manages company users

View File

@ -92,10 +92,12 @@ This will create:
### Admin Tasks ### Admin Tasks
1. **Access Admin Panel**: 1. **Access Admin Panel**:
- Go to <http://localhost/admin/> - Go to <http://localhost/admin/>
- Login with your admin credentials - Login with your admin credentials
2. **Create a Company**: 2. **Create a Company**:
- Go to Companies > Add Company - Go to Companies > Add Company
- Fill in the company details and save - Fill in the company details and save
@ -108,10 +110,12 @@ This will create:
### Company Admin Tasks ### Company Admin Tasks
1. **Login to Dashboard**: 1. **Login to Dashboard**:
- Go to <http://localhost/> - Go to <http://localhost/>
- Login with your company admin credentials - Login with your company admin credentials
2. **Upload Chat Data**: 2. **Upload Chat Data**:
- Click on "Upload Data" in the sidebar - Click on "Upload Data" in the sidebar
- Fill in the data source details - Fill in the data source details
- Select a CSV file containing chat data - Select a CSV file containing chat data
@ -126,11 +130,13 @@ This will create:
### Regular User Tasks ### Regular User Tasks
1. **View Dashboard**: 1. **View Dashboard**:
- Login with your user credentials - Login with your user credentials
- The dashboard will show automatically - The dashboard will show automatically
- Select different dashboards from the sidebar - Select different dashboards from the sidebar
2. **Search Chat Sessions**: 2. **Search Chat Sessions**:
- Click on "Search" in the top navigation - Click on "Search" in the top navigation
- Enter search terms - Enter search terms
- Use filters to refine results - Use filters to refine results
@ -144,24 +150,24 @@ This will create:
Your CSV files should include the following columns: Your CSV files should include the following columns:
| Column | Description | Type | | Column | Description | Type |
| ----------------- | ------------------------------- | -------- | | ------------------- | ------------------------------- | -------- |
| session_id | Unique ID for the chat | String | | `session_id` | Unique ID for the chat | String |
| start_time | Session start time | Datetime | | `start_time` | Session start time | Datetime |
| end_time | Session end time | Datetime | | `end_time` | Session end time | Datetime |
| ip_address | User's IP address | String | | `ip_address` | User's IP address | String |
| country | User's country | String | | `country` | User's country | String |
| language | Chat language | String | | `language` | Chat language | String |
| messages_sent | Number of messages | Integer | | `messages_sent` | Number of messages | Integer |
| sentiment | Sentiment analysis result | String | | `sentiment` | Sentiment analysis result | String |
| escalated | Whether chat was escalated | Boolean | | `escalated` | Whether chat was escalated | Boolean |
| forwarded_hr | Whether chat was sent to HR | Boolean | | `forwarded_hr` | Whether chat was sent to HR | Boolean |
| full_transcript | Complete chat text | Text | | `full_transcript` | Complete chat text | Text |
| avg_response_time | Average response time (seconds) | Float | | `avg_response_time` | Average response time (seconds) | Float |
| tokens | Number of tokens used | Integer | | `tokens` | Number of tokens used | Integer |
| tokens_eur | Cost in EUR | Float | | `tokens_eur` | Cost in EUR | Float |
| category | Chat category | String | | `category` | Chat category | String |
| initial_msg | First user message | Text | | `initial_msg` | First user message | Text |
| user_rating | User satisfaction rating | String | | `user_rating` | User satisfaction rating | String |
Example CSV row: Example CSV row:

View File

@ -99,23 +99,25 @@ A Django application that creates an analytics dashboard for chat session data.
The CSV file should contain the following columns: The CSV file should contain the following columns:
- session_id: Unique identifier for the chat session | Column | Description |
- start_time: When the session started (datetime) | ------------------- | ------------------------------------------------------ |
- end_time: When the session ended (datetime) | `session_id` | Unique identifier for the chat session |
- ip_address: IP address of the user | `start_time` | When the session started (datetime) |
- country: Country of the user | `end_time` | When the session ended (datetime) |
- language: Language used in the conversation | `ip_address` | IP address of the user |
- messages_sent: Number of messages in the conversation (integer) | `country` | Country of the user |
- sentiment: Sentiment analysis of the conversation (string) | `language` | Language used in the conversation |
- escalated: Whether the conversation was escalated (boolean) | `messages_sent` | Number of messages in the conversation (integer) |
- forwarded_hr: Whether the conversation was forwarded to HR (boolean) | `sentiment` | Sentiment analysis of the conversation (string) |
- full_transcript: Full transcript of the conversation (text) | `escalated` | Whether the conversation was escalated (boolean) |
- avg_response_time: Average response time in seconds (float) | `forwarded_hr` | Whether the conversation was forwarded to HR (boolean) |
- tokens: Total number of tokens used (integer) | `full_transcript` | Full transcript of the conversation (text) |
- tokens_eur: Cost of tokens in EUR (float) | `avg_response_time` | Average response time in seconds (float) |
- category: Category of the conversation (string) | `tokens` | Total number of tokens used (integer) |
- initial_msg: First message from the user (text) | `tokens_eur` | Cost of tokens in EUR (float) |
- user_rating: User rating of the conversation (string) | `category` | Category of the conversation (string) |
| `initial_msg` | First message from the user (text) |
| `user_rating` | User rating of the conversation (string) |
## Future Enhancements ## Future Enhancements
@ -128,4 +130,4 @@ The CSV file should contain the following columns:
## License ## 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.

13
TODO.md Normal file
View File

@ -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

View File

@ -9,6 +9,7 @@ from django.core.paginator import Paginator
from django.db.models import Avg, Q from django.db.models import Avg, Q
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string
from django.utils import timezone from django.utils import timezone
from .forms import DashboardForm, DataSourceUploadForm from .forms import DashboardForm, DataSourceUploadForm
@ -16,6 +17,11 @@ from .models import ChatSession, Dashboard, DataSource
from .utils import generate_dashboard_data, process_csv_file 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 @login_required
def dashboard_view(request): def dashboard_view(request):
"""Main dashboard view""" """Main dashboard view"""
@ -57,23 +63,27 @@ def dashboard_view(request):
# Generate dashboard data # Generate dashboard data
dashboard_data = generate_dashboard_data(selected_dashboard.data_sources.all()) dashboard_data = generate_dashboard_data(selected_dashboard.data_sources.all())
# Convert dashboard data to JSON for use in JavaScript # Convert each component of dashboard data to JSON
dashboard_data_json = json.dumps( sentiment_data_json = json.dumps(dashboard_data["sentiment_data"])
{ country_data_json = json.dumps(dashboard_data["country_data"])
"sentiment_data": dashboard_data["sentiment_data"], category_data_json = json.dumps(dashboard_data["category_data"])
"country_data": dashboard_data["country_data"], time_series_data_json = json.dumps(dashboard_data["time_series_data"])
"category_data": dashboard_data["category_data"],
"time_series_data": dashboard_data["time_series_data"],
}
)
context = { context = {
"dashboards": dashboards, "dashboards": dashboards,
"selected_dashboard": selected_dashboard, "selected_dashboard": selected_dashboard,
"dashboard_data": dashboard_data, "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) return render(request, "dashboard/dashboard.html", context)
@ -124,6 +134,11 @@ def upload_data_view(request):
"data_sources": data_sources, "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) return render(request, "dashboard/upload.html", context)
@ -155,6 +170,11 @@ def data_source_detail_view(request, data_source_id):
"page_obj": page_obj, "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) return render(request, "dashboard/data_source_detail.html", context)
@ -177,6 +197,11 @@ def chat_session_detail_view(request, session_id):
"session": chat_session, "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) return render(request, "dashboard/chat_session_detail.html", context)
@ -209,6 +234,11 @@ def create_dashboard_view(request):
"is_create": True, "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) return render(request, "dashboard/dashboard_form.html", context)
@ -244,6 +274,11 @@ def edit_dashboard_view(request, dashboard_id):
"is_create": False, "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) return render(request, "dashboard/dashboard_form.html", context)
@ -313,8 +348,33 @@ def dashboard_data_api(request, dashboard_id):
if not company: if not company:
return JsonResponse({"error": "User not associated with a company"}, status=403) 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 = 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) return JsonResponse(dashboard_data)
@ -373,6 +433,34 @@ def search_chat_sessions(request):
"data_source": data_source, "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) return render(request, "dashboard/search_results.html", context)
@ -449,4 +537,33 @@ def data_view(request):
"escalation_rate": escalation_rate, "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) return render(request, "dashboard/data_view.html", context)

View File

@ -260,3 +260,21 @@
font-size: 1.5rem; 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 */
}

View File

@ -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 =
'<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);
}
});
}
});

View File

@ -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);
}
});
}
});

View File

@ -55,8 +55,14 @@ document.addEventListener("DOMContentLoaded", function () {
document.querySelector("main").appendChild(loadingOverlay); document.querySelector("main").appendChild(loadingOverlay);
fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`) 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) => { .then((data) => {
console.log("Dashboard API response:", data);
updateDashboardStats(data); updateDashboardStats(data);
updateDashboardCharts(data); updateDashboardCharts(data);
@ -115,9 +121,20 @@ document.addEventListener("DOMContentLoaded", function () {
// Function to update dashboard charts // Function to update dashboard charts
function updateDashboardCharts(data) { 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 // Update sessions over time chart
const timeSeriesData = data.time_series_data; const timeSeriesData = data.time_series_data;
if (timeSeriesData && timeSeriesData.length > 0 && window.Plotly) { if (timeSeriesData && timeSeriesData.length > 0) {
try {
const timeSeriesX = timeSeriesData.map((item) => item.date); const timeSeriesX = timeSeriesData.map((item) => item.date);
const timeSeriesY = timeSeriesData.map((item) => item.count); const timeSeriesY = timeSeriesData.map((item) => item.count);
@ -149,6 +166,14 @@ document.addEventListener("DOMContentLoaded", function () {
}, },
} }
); );
} 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 // Update sentiment chart

View File

@ -49,21 +49,21 @@
<ul class="navbar-nav me-auto mb-2 mb-md-0"> <ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
href="{% url 'dashboard' %}" href="{% url 'dashboard' %}"
>Dashboard</a >Dashboard</a
> >
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}"
href="{% url 'upload_data' %}" href="{% url 'upload_data' %}"
>Upload Data</a >Upload Data</a
> >
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}"
href="{% url 'search_chat_sessions' %}" href="{% url 'search_chat_sessions' %}"
>Search</a >Search</a
> >
@ -86,7 +86,9 @@
{{ user.username }} {{ user.username }}
</button> </button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li><a class="dropdown-item" href="{% url 'profile' %}">Profile</a></li> <li>
<a class="dropdown-item ajax-nav-link" href="{% url 'profile' %}">Profile</a>
</li>
{% if user.is_staff %} {% if user.is_staff %}
<li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li> <li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li>
{% endif %} {% endif %}
@ -117,7 +119,7 @@
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
href="{% url 'dashboard' %}" href="{% url 'dashboard' %}"
> >
<i class="fas fa-tachometer-alt me-2"></i> <i class="fas fa-tachometer-alt me-2"></i>
@ -126,7 +128,7 @@
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}"
href="{% url 'upload_data' %}" href="{% url 'upload_data' %}"
> >
<i class="fas fa-upload me-2"></i> <i class="fas fa-upload me-2"></i>
@ -135,7 +137,7 @@
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}"
href="{% url 'search_chat_sessions' %}" href="{% url 'search_chat_sessions' %}"
> >
<i class="fas fa-search me-2"></i> <i class="fas fa-search me-2"></i>
@ -144,7 +146,7 @@
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link {% if request.resolver_match.url_name == 'data_view' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'data_view' %}active{% endif %}"
href="{% url 'data_view' %}" href="{% url 'data_view' %}"
> >
<i class="fas fa-table me-2"></i> <i class="fas fa-table me-2"></i>
@ -202,23 +204,54 @@
{# </div> #} {# </div> #}
{# {% endif %} #} {# {% endif %} #}
<div id="main-content">
{% block content %} {% block content %}
{% endblock %} {% endblock %}
</div>
</main> </main>
</div> </div>
</div> </div>
<footer> <footer>
<div class="container"> <div class="container">
<p>&copy; {% now "Y" %} Chat Analytics Dashboard. All rights reserved.</p> <p>&copy; {% now "Y" %} KJANAT All rights reserved. | Chat Analytics Dashboard.</p>
</div> </div>
</footer> </footer>
<!-- Bootstrap JS --> <!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"></script> <script
src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"
crossorigin="anonymous"
></script>
<!-- jQuery (for Ajax) --> <!-- jQuery (for Ajax) -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <script
src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"
crossorigin="anonymous"
></script>
<!-- Custom JavaScript -->
<script src="{% static 'js/main.js' %}"></script>
<script src="{% static 'js/ajax-pagination.js' %}"></script>
<script src="{% static 'js/ajax-navigation.js' %}"></script>
<!-- Enable AJAX Navigation -->
<script>
// Enable AJAX navigation for the entire application
var ENABLE_AJAX_NAVIGATION = true;
</script>
<!-- Check if Plotly loaded successfully -->
<script>
if (typeof Plotly === "undefined") {
console.error("Plotly library failed to load. Will attempt to load fallback.");
// Try to load Plotly from alternative source
const script = document.createElement("script");
script.src = "https://cdn.jsdelivr.net/npm/plotly.js@latest/dist/plotly.min.js";
script.async = true;
script.crossOrigin = "anonymous";
document.head.appendChild(script);
}
</script>
{% block extra_js %} {% block extra_js %}
{{ block.super }} {{ block.super }}

View File

@ -147,13 +147,30 @@
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<!-- prettier-ignore-start -->
<!-- Store the JSON data in script tags to avoid parsing issues -->
<script type="application/json" id="time-series-data">{{ time_series_data_json|safe }}</script>
<script type="application/json" id="sentiment-data">{{ sentiment_data_json|safe }}</script>
<script type="application/json" id="country-data">{{ country_data_json|safe }}</script>
<script type="application/json" id="category-data">{{ category_data_json|safe }}</script>
<!-- prettier-ignore-end -->
<script> <script>
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Parse the dashboard data from JSON try {
const dashboardData = JSON.parse("{{ dashboard_data_json|safe }}"); // Parse the dashboard data components from script tags
const timeSeriesData = JSON.parse(document.getElementById("time-series-data").textContent);
const sentimentData = JSON.parse(document.getElementById("sentiment-data").textContent);
const countryData = JSON.parse(document.getElementById("country-data").textContent);
const categoryData = JSON.parse(document.getElementById("category-data").textContent);
console.log("Time series data loaded:", timeSeriesData);
console.log("Sentiment data loaded:", sentimentData);
console.log("Country data loaded:", countryData);
console.log("Category data loaded:", categoryData);
// Sessions over time chart // Sessions over time chart
const timeSeriesData = dashboardData.time_series_data; if (timeSeriesData && timeSeriesData.length > 0) {
const timeSeriesX = timeSeriesData.map((item) => item.date); const timeSeriesX = timeSeriesData.map((item) => item.date);
const timeSeriesY = timeSeriesData.map((item) => item.count); const timeSeriesY = timeSeriesData.map((item) => item.count);
@ -185,11 +202,13 @@
}, },
} }
); );
} else {
document.getElementById("sessions-time-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
}
// Sentiment analysis chart // Sentiment analysis chart
const sentimentData = dashboardData.sentiment_data; if (sentimentData && sentimentData.length > 0) {
if (sentimentData.length > 0) {
const sentimentLabels = sentimentData.map((item) => item.sentiment); const sentimentLabels = sentimentData.map((item) => item.sentiment);
const sentimentValues = sentimentData.map((item) => item.count); const sentimentValues = sentimentData.map((item) => item.count);
const sentimentColors = sentimentLabels.map((sentiment) => { const sentimentColors = sentimentLabels.map((sentiment) => {
@ -224,9 +243,7 @@
} }
// Country chart // Country chart
const countryData = dashboardData.country_data; if (countryData && countryData.length > 0) {
if (countryData.length > 0) {
const countryLabels = countryData.map((item) => item.country); const countryLabels = countryData.map((item) => item.country);
const countryValues = countryData.map((item) => item.count); const countryValues = countryData.map((item) => item.count);
@ -256,9 +273,7 @@
} }
// Category chart // Category chart
const categoryData = dashboardData.category_data; if (categoryData && categoryData.length > 0) {
if (categoryData.length > 0) {
const categoryLabels = categoryData.map((item) => item.category); const categoryLabels = categoryData.map((item) => item.category);
const categoryValues = categoryData.map((item) => item.count); const categoryValues = categoryData.map((item) => item.count);
@ -281,6 +296,13 @@
document.getElementById("category-chart").innerHTML = document.getElementById("category-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No category data available</p></div>'; '<div class="text-center py-5"><p class="text-muted">No category data available</p></div>';
} }
} catch (error) {
console.error("Error rendering charts:", error);
document.querySelectorAll(".chart-container").forEach((container) => {
container.innerHTML =
'<div class="text-center py-5"><p class="text-danger">Error loading chart data. Please refresh the page.</p></div>';
});
}
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -115,12 +115,18 @@
<td>{{ session.tokens }}</td> <td>{{ session.tokens }}</td>
<td>{{ session.category|default:"N/A" }}</td> <td>{{ session.category|default:"N/A" }}</td>
<td> <td>
{% if session.session_id %}
<a <a
href="{% url 'chat_session_detail' session.session_id %}" href="{% url 'chat_session_detail' session.session_id %}"
class="btn btn-sm btn-outline-primary" class="btn btn-sm btn-outline-primary"
> >
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
{% else %}
<button class="btn btn-sm btn-outline-secondary" disabled>
<i class="fas fa-eye-slash"></i>
</button>
{% endif %}
</td> </td>
</tr> </tr>
{% empty %} {% empty %}

View File

@ -11,13 +11,13 @@
<h1 class="h2">Data View</h1> <h1 class="h2">Data View</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2"> <div class="btn-group me-2">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary"> <a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary ajax-nav-link">
<i class="fas fa-arrow-left"></i> Back to Dashboard <i class="fas fa-arrow-left"></i> Back to Dashboard
</a> </a>
{% if selected_data_source %} {% if selected_data_source %}
<a <a
href="{% url 'data_source_detail' selected_data_source.id %}" href="{% url 'data_source_detail' selected_data_source.id %}"
class="btn btn-sm btn-outline-secondary" class="btn btn-sm btn-outline-secondary ajax-nav-link"
> >
<i class="fas fa-database"></i> View Source <i class="fas fa-database"></i> View Source
</a> </a>
@ -34,11 +34,17 @@
<i class="fas fa-filter"></i> Filter <i class="fas fa-filter"></i> Filter
</button> </button>
<ul class="dropdown-menu" aria-labelledby="dataViewDropdown"> <ul class="dropdown-menu" aria-labelledby="dataViewDropdown">
<li><a class="dropdown-item" href="?view=all">All Sessions</a></li> <li><a class="dropdown-item ajax-nav-link" href="?view=all">All Sessions</a></li>
<li><a class="dropdown-item" href="?view=recent">Recent Sessions</a></li> <li><a class="dropdown-item ajax-nav-link" href="?view=recent">Recent Sessions</a></li>
<li><a class="dropdown-item" href="?view=positive">Positive Sentiment</a></li> <li>
<li><a class="dropdown-item" href="?view=negative">Negative Sentiment</a></li> <a class="dropdown-item ajax-nav-link" href="?view=positive">Positive Sentiment</a>
<li><a class="dropdown-item" href="?view=escalated">Escalated Sessions</a></li> </li>
<li>
<a class="dropdown-item ajax-nav-link" href="?view=negative">Negative Sentiment</a>
</li>
<li>
<a class="dropdown-item ajax-nav-link" href="?view=escalated">Escalated Sessions</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>
@ -52,7 +58,7 @@
<h5 class="card-title mb-0">Data Source Selection</h5> <h5 class="card-title mb-0">Data Source Selection</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="get" class="row g-3 align-items-center"> <form method="get" class="row g-3 align-items-center filter-form">
<div class="col-md-6"> <div class="col-md-6">
<select name="data_source_id" class="form-select" aria-label="Select Data Source"> <select name="data_source_id" class="form-select" aria-label="Select Data Source">
<option value="">All Data Sources</option> <option value="">All Data Sources</option>
@ -109,153 +115,16 @@
<span class="badge bg-primary">{{ page_obj.paginator.count }} sessions</span> <span class="badge bg-primary">{{ page_obj.paginator.count }} sessions</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <!-- Loading spinner shown during AJAX requests -->
<table class="table table-striped table-hover"> <div id="ajax-loading-spinner" class="text-center py-4 d-none">
<thead> <div class="spinner-border text-primary" role="status">
<tr> <span class="visually-hidden">Loading...</span>
<th>Session ID</th> </div>
<th>Start Time</th> <p class="mt-2">Loading data...</p>
<th>Country</th>
<th>Language</th>
<th>Messages</th>
<th>Sentiment</th>
<th>Response Time</th>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for session in page_obj %}
<tr>
<td>{{ session.session_id|truncatechars:10 }}</td>
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
<td>{{ session.country|default:"N/A" }}</td>
<td>{{ session.language|default:"N/A" }}</td>
<td>{{ session.messages_sent }}</td>
<td>
{% if session.sentiment %}
{% if 'positive' in session.sentiment|lower %}
<span class="badge bg-success">{{ session.sentiment }}</span>
{% elif 'negative' in session.sentiment|lower %}
<span class="badge bg-danger">{{ session.sentiment }}</span>
{% elif 'neutral' in session.sentiment|lower %}
<span class="badge bg-warning">{{ session.sentiment }}</span>
{% else %}
<span class="badge bg-secondary">{{ session.sentiment }}</span>
{% endif %}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>{{ session.avg_response_time|floatformat:2 }}s</td>
<td>{{ session.category|default:"N/A" }}</td>
<td>
<a
href="{% url 'chat_session_detail' session.session_id %}"
class="btn btn-sm btn-outline-primary"
>
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center">No chat sessions found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% if page_obj.paginator.num_pages > 1 %} <!-- Data table container that will be updated via AJAX -->
<nav aria-label="Page navigation" class="mt-4"> <div id="ajax-content-container">{% include "dashboard/partials/data_table.html" %}</div>
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a
class="page-link"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page=1"
aria-label="First"
>
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.previous_page_number }}"
aria-label="Previous"
>
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a
class="page-link"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a
class="page-link"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a
class="page-link"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.next_page_number }}"
aria-label="Next"
>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
aria-label="Last"
>
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -314,3 +183,33 @@
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extra_js %}
<script>
// Function to update the summary section with new data
function updateSummary(data) {
if (document.querySelector(".stats-card h3:nth-of-type(1)")) {
document.querySelector(".stats-card h3:nth-of-type(1)").textContent =
data.page_obj.paginator.count;
}
if (document.querySelector(".stats-card h3:nth-of-type(2)")) {
document.querySelector(".stats-card h3:nth-of-type(2)").textContent =
data.avg_response_time !== null && data.avg_response_time !== undefined
? data.avg_response_time.toFixed(2) + "s"
: "0.00s";
}
if (document.querySelector(".stats-card h3:nth-of-type(3)")) {
document.querySelector(".stats-card h3:nth-of-type(3)").textContent =
data.avg_messages !== null && data.avg_messages !== undefined
? data.avg_messages.toFixed(1)
: "0.0";
}
if (document.querySelector(".stats-card h3:nth-of-type(4)")) {
document.querySelector(".stats-card h3:nth-of-type(4)").textContent =
data.escalation_rate !== null && data.escalation_rate !== undefined
? data.escalation_rate.toFixed(1) + "%"
: "0.0%";
}
}
</script>
{% endblock %}

View File

@ -0,0 +1,160 @@
<!-- templates/dashboard/partials/data_table.html -->
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Session ID</th>
<th>Start Time</th>
<th>Country</th>
<th>Language</th>
<th>Messages</th>
<th>Sentiment</th>
<th>Response Time</th>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for session in page_obj %}
<tr>
<td>{{ session.session_id|truncatechars:10 }}</td>
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
<td>{{ session.country|default:"N/A" }}</td>
<td>{{ session.language|default:"N/A" }}</td>
<td>{{ session.messages_sent }}</td>
<td>
{% if session.sentiment %}
{% if 'positive' in session.sentiment|lower %}
<span class="badge bg-success">{{ session.sentiment }}</span>
{% elif 'negative' in session.sentiment|lower %}
<span class="badge bg-danger">{{ session.sentiment }}</span>
{% elif 'neutral' in session.sentiment|lower %}
<span class="badge bg-warning">{{ session.sentiment }}</span>
{% else %}
<span class="badge bg-secondary">{{ session.sentiment }}</span>
{% endif %}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>{{ session.avg_response_time|floatformat:2 }}s</td>
<td>{{ session.category|default:"N/A" }}</td>
<td>
{% if session.session_id %}
<a
href="{% url 'chat_session_detail' session.session_id %}"
class="btn btn-sm btn-outline-primary ajax-nav-link"
>
<i class="fas fa-eye"></i>
</a>
{% else %}
<button class="btn btn-sm btn-outline-secondary" disabled>
<i class="fas fa-eye-slash"></i>
</button>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center">No chat sessions found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<nav aria-label="Page navigation" class="mt-4" id="pagination-container">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="1"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page=1"
aria-label="First"
>
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.previous_page_number }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.previous_page_number }}"
aria-label="Previous"
>
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a
class="page-link pagination-link"
data-page="{{ num }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ num }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.next_page_number }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.next_page_number }}"
aria-label="Next"
>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.paginator.num_pages }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
aria-label="Last"
>
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}

View File

@ -0,0 +1,164 @@
<!-- templates/dashboard/partials/search_results_table.html -->
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Session ID</th>
<th>Start Time</th>
<th>Data Source</th>
<th>Country</th>
<th>Language</th>
<th>Sentiment</th>
<th>Messages</th>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for session in page_obj %}
<tr>
<td>{{ session.session_id|truncatechars:10 }}</td>
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
<td>
<a href="{% url 'data_source_detail' session.data_source.id %}" class="ajax-nav-link"
>{{ session.data_source.name|truncatechars:15 }}</a
>
</td>
<td>{{ session.country }}</td>
<td>{{ session.language }}</td>
<td>
{% if session.sentiment %}
{% if 'positive' in session.sentiment|lower %}
<span class="badge bg-success">{{ session.sentiment }}</span>
{% elif 'negative' in session.sentiment|lower %}
<span class="badge bg-danger">{{ session.sentiment }}</span>
{% elif 'neutral' in session.sentiment|lower %}
<span class="badge bg-warning">{{ session.sentiment }}</span>
{% else %}
<span class="badge bg-secondary">{{ session.sentiment }}</span>
{% endif %}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>{{ session.messages_sent }}</td>
<td>{{ session.category|default:"N/A" }}</td>
<td>
{% if session.session_id %}
<a
href="{% url 'chat_session_detail' session.session_id %}"
class="btn btn-sm btn-outline-primary"
>
<i class="fas fa-eye"></i>
</a>
{% else %}
<button class="btn btn-sm btn-outline-secondary" disabled>
<i class="fas fa-eye-slash"></i>
</button>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center">No chat sessions found matching your criteria.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<nav aria-label="Page navigation" class="mt-4" id="pagination-container">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="1"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page=1"
aria-label="First"
>
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.previous_page_number }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.previous_page_number }}"
aria-label="Previous"
>
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a
class="page-link pagination-link"
data-page="{{ num }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ num }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.next_page_number }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.next_page_number }}"
aria-label="Next"
>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.paginator.num_pages }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
aria-label="Last"
>
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}

View File

@ -9,7 +9,7 @@
> >
<h1 class="h2">Search Results</h1> <h1 class="h2">Search Results</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary"> <a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary ajax-nav-link">
<i class="fas fa-arrow-left"></i> Back to Dashboard <i class="fas fa-arrow-left"></i> Back to Dashboard
</a> </a>
</div> </div>
@ -22,7 +22,7 @@
<h5 class="card-title mb-0">Search Chat Sessions</h5> <h5 class="card-title mb-0">Search Chat Sessions</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="get" action="{% url 'search_chat_sessions' %}"> <form method="get" action="{% url 'search_chat_sessions' %}" class="search-form">
<div class="input-group"> <div class="input-group">
<input <input
type="text" type="text"
@ -62,161 +62,24 @@
</h5> </h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <!-- Loading spinner shown during AJAX requests -->
<table class="table table-striped table-hover"> <div id="ajax-loading-spinner" class="text-center py-4 d-none">
<thead> <div class="spinner-border text-primary" role="status">
<tr> <span class="visually-hidden">Loading...</span>
<th>Session ID</th> </div>
<th>Start Time</th> <p class="mt-2">Loading data...</p>
<th>Data Source</th>
<th>Country</th>
<th>Language</th>
<th>Sentiment</th>
<th>Messages</th>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for session in page_obj %}
<tr>
<td>{{ session.session_id|truncatechars:10 }}</td>
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
<td>
<a href="{% url 'data_source_detail' session.data_source.id %}"
>{{ session.data_source.name|truncatechars:15 }}</a
>
</td>
<td>{{ session.country }}</td>
<td>{{ session.language }}</td>
<td>
{% if session.sentiment %}
{% if 'positive' in session.sentiment|lower %}
<span class="badge bg-success">{{ session.sentiment }}</span>
{% elif 'negative' in session.sentiment|lower %}
<span class="badge bg-danger">{{ session.sentiment }}</span>
{% elif 'neutral' in session.sentiment|lower %}
<span class="badge bg-warning">{{ session.sentiment }}</span>
{% else %}
<span class="badge bg-secondary">{{ session.sentiment }}</span>
{% endif %}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>{{ session.messages_sent }}</td>
<td>{{ session.category|default:"N/A" }}</td>
<td>
<a
href="{% url 'chat_session_detail' session.session_id %}"
class="btn btn-sm btn-outline-primary"
>
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center">
No chat sessions found matching your criteria.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% if page_obj.paginator.num_pages > 1 %} <!-- Search results container that will be updated via AJAX -->
<nav aria-label="Page navigation" class="mt-4"> <div id="ajax-content-container">
<ul class="pagination justify-content-center"> {% include "dashboard/partials/search_results_table.html" %}
{% if page_obj.has_previous %} </div>
<li class="page-item">
<a
class="page-link"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page=1"
aria-label="First"
>
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.previous_page_number }}"
aria-label="Previous"
>
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a
class="page-link"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a
class="page-link"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a
class="page-link"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.next_page_number }}"
aria-label="Next"
>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
aria-label="Last"
>
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %}
<!-- No need for extra JavaScript here, using common ajax-pagination.js -->
{% endblock %}

View File

@ -1,6 +1,6 @@
# docker-compose.yml # docker-compose.yml
version: '3.8' version: "3.8"
services: services:
web: web:
@ -11,7 +11,7 @@ services:
- static_volume:/app/staticfiles - static_volume:/app/staticfiles
- media_volume:/app/media - media_volume:/app/media
ports: ports:
- '8000:8000' - "8000:8000"
environment: environment:
- DEBUG=0 - DEBUG=0
- SECRET_KEY=your_secret_key_here - SECRET_KEY=your_secret_key_here
@ -31,7 +31,7 @@ services:
nginx: nginx:
image: nginx:latest image: nginx:latest
ports: ports:
- '80:80' - "80:80"
volumes: volumes:
- ./nginx/conf.d:/etc/nginx/conf.d - ./nginx/conf.d:/etc/nginx/conf.d
- static_volume:/app/staticfiles - static_volume:/app/staticfiles