mirror of
https://github.com/kjanat/livegraphs-django.git
synced 2026-01-16 10:22:10 +01:00
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:
@ -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]
|
||||
|
||||
@ -16,15 +16,11 @@
|
||||
"useTabs": false,
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"files": ["*.html"],
|
||||
"options": {
|
||||
"parser": "jinja-template"
|
||||
}
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"prettier-plugin-jinja-template"
|
||||
]
|
||||
"plugins": ["prettier-plugin-jinja-template"]
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -55,10 +55,7 @@ If you need to prevent Prettier from formatting a section of your template:
|
||||
|
||||
```html
|
||||
{# prettier-ignore #}
|
||||
<div>
|
||||
This section will not be formatted
|
||||
by Prettier.
|
||||
</div>
|
||||
<div>This section will not be formatted by Prettier.</div>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<div>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -92,10 +92,12 @@ This will create:
|
||||
### Admin Tasks
|
||||
|
||||
1. **Access Admin Panel**:
|
||||
|
||||
- Go to <http://localhost/admin/>
|
||||
- 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 <http://localhost/>
|
||||
- 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
|
||||
@ -144,24 +150,24 @@ 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 |
|
||||
| ------------------- | ------------------------------- | -------- |
|
||||
| `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:
|
||||
|
||||
|
||||
38
README.md
38
README.md
@ -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.
|
||||
|
||||
13
TODO.md
Normal file
13
TODO.md
Normal 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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 */
|
||||
}
|
||||
|
||||
273
dashboard_project/static/js/ajax-navigation.js
Normal file
273
dashboard_project/static/js/ajax-navigation.js
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
106
dashboard_project/static/js/ajax-pagination.js
Normal file
106
dashboard_project/static/js/ajax-pagination.js
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -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,9 +121,20 @@ 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 =
|
||||
'<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Update sessions over time chart
|
||||
const timeSeriesData = data.time_series_data;
|
||||
if (timeSeriesData && timeSeriesData.length > 0 && window.Plotly) {
|
||||
if (timeSeriesData && timeSeriesData.length > 0) {
|
||||
try {
|
||||
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
||||
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
|
||||
|
||||
@ -49,21 +49,21 @@
|
||||
<ul class="navbar-nav me-auto mb-2 mb-md-0">
|
||||
<li class="nav-item">
|
||||
<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' %}"
|
||||
>Dashboard</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<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' %}"
|
||||
>Upload Data</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<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' %}"
|
||||
>Search</a
|
||||
>
|
||||
@ -86,7 +86,9 @@
|
||||
{{ user.username }}
|
||||
</button>
|
||||
<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 %}
|
||||
<li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li>
|
||||
{% endif %}
|
||||
@ -117,7 +119,7 @@
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<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' %}"
|
||||
>
|
||||
<i class="fas fa-tachometer-alt me-2"></i>
|
||||
@ -126,7 +128,7 @@
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<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' %}"
|
||||
>
|
||||
<i class="fas fa-upload me-2"></i>
|
||||
@ -135,7 +137,7 @@
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<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' %}"
|
||||
>
|
||||
<i class="fas fa-search me-2"></i>
|
||||
@ -144,7 +146,7 @@
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<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' %}"
|
||||
>
|
||||
<i class="fas fa-table me-2"></i>
|
||||
@ -202,23 +204,54 @@
|
||||
{# </div> #}
|
||||
{# {% endif %} #}
|
||||
|
||||
<div id="main-content">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>© {% now "Y" %} Chat Analytics Dashboard. All rights reserved.</p>
|
||||
<p>© {% now "Y" %} KJANAT All rights reserved. | Chat Analytics Dashboard.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- 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) -->
|
||||
<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.super }}
|
||||
|
||||
@ -147,13 +147,30 @@
|
||||
{% endblock %}
|
||||
|
||||
{% 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>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Parse the dashboard data from JSON
|
||||
const dashboardData = JSON.parse("{{ dashboard_data_json|safe }}");
|
||||
try {
|
||||
// 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
|
||||
const timeSeriesData = dashboardData.time_series_data;
|
||||
if (timeSeriesData && timeSeriesData.length > 0) {
|
||||
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
||||
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
|
||||
const sentimentData = dashboardData.sentiment_data;
|
||||
|
||||
if (sentimentData.length > 0) {
|
||||
if (sentimentData && sentimentData.length > 0) {
|
||||
const sentimentLabels = sentimentData.map((item) => item.sentiment);
|
||||
const sentimentValues = sentimentData.map((item) => item.count);
|
||||
const sentimentColors = sentimentLabels.map((sentiment) => {
|
||||
@ -224,9 +243,7 @@
|
||||
}
|
||||
|
||||
// Country chart
|
||||
const countryData = dashboardData.country_data;
|
||||
|
||||
if (countryData.length > 0) {
|
||||
if (countryData && countryData.length > 0) {
|
||||
const countryLabels = countryData.map((item) => item.country);
|
||||
const countryValues = countryData.map((item) => item.count);
|
||||
|
||||
@ -256,9 +273,7 @@
|
||||
}
|
||||
|
||||
// Category chart
|
||||
const categoryData = dashboardData.category_data;
|
||||
|
||||
if (categoryData.length > 0) {
|
||||
if (categoryData && categoryData.length > 0) {
|
||||
const categoryLabels = categoryData.map((item) => item.category);
|
||||
const categoryValues = categoryData.map((item) => item.count);
|
||||
|
||||
@ -281,6 +296,13 @@
|
||||
document.getElementById("category-chart").innerHTML =
|
||||
'<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>
|
||||
{% endblock %}
|
||||
|
||||
@ -115,12 +115,18 @@
|
||||
<td>{{ session.tokens }}</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 %}
|
||||
|
||||
@ -11,13 +11,13 @@
|
||||
<h1 class="h2">Data View</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<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
|
||||
</a>
|
||||
{% if selected_data_source %}
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
@ -34,11 +34,17 @@
|
||||
<i class="fas fa-filter"></i> Filter
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="dataViewDropdown">
|
||||
<li><a class="dropdown-item" href="?view=all">All Sessions</a></li>
|
||||
<li><a class="dropdown-item" href="?view=recent">Recent Sessions</a></li>
|
||||
<li><a class="dropdown-item" href="?view=positive">Positive Sentiment</a></li>
|
||||
<li><a class="dropdown-item" href="?view=negative">Negative Sentiment</a></li>
|
||||
<li><a class="dropdown-item" href="?view=escalated">Escalated Sessions</a></li>
|
||||
<li><a class="dropdown-item ajax-nav-link" href="?view=all">All Sessions</a></li>
|
||||
<li><a class="dropdown-item ajax-nav-link" href="?view=recent">Recent Sessions</a></li>
|
||||
<li>
|
||||
<a class="dropdown-item ajax-nav-link" href="?view=positive">Positive Sentiment</a>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@ -52,7 +58,7 @@
|
||||
<h5 class="card-title mb-0">Data Source Selection</h5>
|
||||
</div>
|
||||
<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">
|
||||
<select name="data_source_id" class="form-select" aria-label="Select Data Source">
|
||||
<option value="">All Data Sources</option>
|
||||
@ -109,153 +115,16 @@
|
||||
<span class="badge bg-primary">{{ page_obj.paginator.count }} sessions</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
<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>
|
||||
<!-- Loading spinner shown during AJAX requests -->
|
||||
<div id="ajax-loading-spinner" class="text-center py-4 d-none">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-2">Loading data...</p>
|
||||
</div>
|
||||
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<nav aria-label="Page navigation" class="mt-4">
|
||||
<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">««</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">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="First">
|
||||
<span aria-hidden="true">««</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true">«</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">»</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">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Last">
|
||||
<span aria-hidden="true">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
<!-- Data table container that will be updated via AJAX -->
|
||||
<div id="ajax-content-container">{% include "dashboard/partials/data_table.html" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -314,3 +183,33 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
|
||||
160
dashboard_project/templates/dashboard/partials/data_table.html
Normal file
160
dashboard_project/templates/dashboard/partials/data_table.html
Normal 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">««</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">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="First">
|
||||
<span aria-hidden="true">««</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true">«</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">»</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">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Last">
|
||||
<span aria-hidden="true">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
@ -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">««</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">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="First">
|
||||
<span aria-hidden="true">««</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true">«</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">»</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">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Last">
|
||||
<span aria-hidden="true">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
@ -9,7 +9,7 @@
|
||||
>
|
||||
<h1 class="h2">Search Results</h1>
|
||||
<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
|
||||
</a>
|
||||
</div>
|
||||
@ -22,7 +22,7 @@
|
||||
<h5 class="card-title mb-0">Search Chat Sessions</h5>
|
||||
</div>
|
||||
<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">
|
||||
<input
|
||||
type="text"
|
||||
@ -62,161 +62,24 @@
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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 %}"
|
||||
>{{ 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>
|
||||
<!-- Loading spinner shown during AJAX requests -->
|
||||
<div id="ajax-loading-spinner" class="text-center py-4 d-none">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-2">Loading data...</p>
|
||||
</div>
|
||||
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<nav aria-label="Page navigation" class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<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">««</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">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="First">
|
||||
<span aria-hidden="true">««</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true">«</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">»</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">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Last">
|
||||
<span aria-hidden="true">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
<!-- Search results container that will be updated via AJAX -->
|
||||
<div id="ajax-content-container">
|
||||
{% include "dashboard/partials/search_results_table.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<!-- No need for extra JavaScript here, using common ajax-pagination.js -->
|
||||
{% endblock %}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# docker-compose.yml
|
||||
|
||||
version: '3.8'
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
@ -11,7 +11,7 @@ services:
|
||||
- static_volume:/app/staticfiles
|
||||
- media_volume:/app/media
|
||||
ports:
|
||||
- '8000:8000'
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- DEBUG=0
|
||||
- SECRET_KEY=your_secret_key_here
|
||||
@ -31,7 +31,7 @@ services:
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- '80:80'
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d
|
||||
- static_volume:/app/staticfiles
|
||||
|
||||
Reference in New Issue
Block a user