mirror of
https://github.com/kjanat/livegraphs-django.git
synced 2026-01-16 10:12:09 +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, JavaScript, and JSON files
|
||||||
[*.{css,scss,js,json}]
|
[*.{css,scss,js,json}]
|
||||||
indent_size = 2
|
indent_size = 4
|
||||||
|
|
||||||
# Markdown files
|
# Markdown files
|
||||||
[*.md]
|
[*.md]
|
||||||
|
|||||||
52
.prettierrc
52
.prettierrc
@ -1,30 +1,26 @@
|
|||||||
{
|
{
|
||||||
"arrowParens": "always",
|
"arrowParens": "always",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"embeddedLanguageFormatting": "auto",
|
"embeddedLanguageFormatting": "auto",
|
||||||
"htmlWhitespaceSensitivity": "css",
|
"htmlWhitespaceSensitivity": "css",
|
||||||
"insertPragma": false,
|
"insertPragma": false,
|
||||||
"jsxSingleQuote": false,
|
"jsxSingleQuote": false,
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"proseWrap": "preserve",
|
"proseWrap": "preserve",
|
||||||
"quoteProps": "as-needed",
|
"quoteProps": "as-needed",
|
||||||
"requirePragma": false,
|
"requirePragma": false,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
"useTabs": false,
|
"useTabs": false,
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": [
|
"files": ["*.html"],
|
||||||
"*.html"
|
"options": {
|
||||||
],
|
"parser": "jinja-template"
|
||||||
"options": {
|
}
|
||||||
"parser": "jinja-template"
|
}
|
||||||
}
|
],
|
||||||
}
|
"plugins": ["prettier-plugin-jinja-template"]
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"prettier-plugin-jinja-template"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
@ -143,25 +149,25 @@ 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:
|
||||||
|
|
||||||
|
|||||||
40
README.md
40
README.md
@ -32,7 +32,7 @@ A Django application that creates an analytics dashboard for chat session data.
|
|||||||
|
|
||||||
2. Create a virtual environment and activate it:
|
2. Create a virtual environment and activate it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
uv venv
|
uv venv
|
||||||
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||||
```
|
```
|
||||||
@ -99,23 +99,25 @@ A Django application that creates an analytics dashboard for chat session data.
|
|||||||
|
|
||||||
The CSV file should contain the following columns:
|
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
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.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)
|
||||||
|
|||||||
@ -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 */
|
||||||
|
}
|
||||||
|
|||||||
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);
|
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,40 +121,59 @@ 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) {
|
||||||
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
try {
|
||||||
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
||||||
|
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
||||||
|
|
||||||
Plotly.react(
|
Plotly.react(
|
||||||
"sessions-time-chart",
|
"sessions-time-chart",
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
x: timeSeriesX,
|
||||||
|
y: timeSeriesY,
|
||||||
|
type: "scatter",
|
||||||
|
mode: "lines+markers",
|
||||||
|
line: {
|
||||||
|
color: "rgb(75, 192, 192)",
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
marker: {
|
||||||
|
color: "rgb(75, 192, 192)",
|
||||||
|
size: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
{
|
{
|
||||||
x: timeSeriesX,
|
margin: { t: 10, r: 10, b: 40, l: 40 },
|
||||||
y: timeSeriesY,
|
xaxis: {
|
||||||
type: "scatter",
|
title: "Date",
|
||||||
mode: "lines+markers",
|
|
||||||
line: {
|
|
||||||
color: "rgb(75, 192, 192)",
|
|
||||||
width: 2,
|
|
||||||
},
|
},
|
||||||
marker: {
|
yaxis: {
|
||||||
color: "rgb(75, 192, 192)",
|
title: "Number of Sessions",
|
||||||
size: 6,
|
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
],
|
);
|
||||||
{
|
} catch (error) {
|
||||||
margin: { t: 10, r: 10, b: 40, l: 40 },
|
console.error("Error rendering time series chart:", error);
|
||||||
xaxis: {
|
document.getElementById("sessions-time-chart").innerHTML =
|
||||||
title: "Date",
|
'<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>';
|
||||||
},
|
}
|
||||||
yaxis: {
|
} else {
|
||||||
title: "Number of Sessions",
|
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
|
||||||
|
|||||||
@ -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 %} #}
|
||||||
|
|
||||||
{% block content %}
|
<div id="main-content">
|
||||||
{% endblock %}
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div class="container">
|
<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>
|
</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 }}
|
||||||
|
|||||||
@ -147,139 +147,161 @@
|
|||||||
{% 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);
|
||||||
|
|
||||||
// Sessions over time chart
|
console.log("Time series data loaded:", timeSeriesData);
|
||||||
const timeSeriesData = dashboardData.time_series_data;
|
console.log("Sentiment data loaded:", sentimentData);
|
||||||
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
console.log("Country data loaded:", countryData);
|
||||||
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
console.log("Category data loaded:", categoryData);
|
||||||
|
|
||||||
Plotly.newPlot(
|
// Sessions over time chart
|
||||||
"sessions-time-chart",
|
if (timeSeriesData && timeSeriesData.length > 0) {
|
||||||
[
|
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
||||||
{
|
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
||||||
x: timeSeriesX,
|
|
||||||
y: timeSeriesY,
|
Plotly.newPlot(
|
||||||
type: "scatter",
|
"sessions-time-chart",
|
||||||
mode: "lines+markers",
|
[
|
||||||
line: {
|
{
|
||||||
color: "rgb(75, 192, 192)",
|
x: timeSeriesX,
|
||||||
width: 2,
|
y: timeSeriesY,
|
||||||
},
|
type: "scatter",
|
||||||
marker: {
|
mode: "lines+markers",
|
||||||
color: "rgb(75, 192, 192)",
|
line: {
|
||||||
size: 6,
|
color: "rgb(75, 192, 192)",
|
||||||
},
|
width: 2,
|
||||||
},
|
},
|
||||||
],
|
marker: {
|
||||||
{
|
color: "rgb(75, 192, 192)",
|
||||||
margin: { t: 10, r: 10, b: 40, l: 40 },
|
size: 6,
|
||||||
xaxis: {
|
},
|
||||||
title: "Date",
|
},
|
||||||
},
|
],
|
||||||
yaxis: {
|
{
|
||||||
title: "Number of Sessions",
|
margin: { t: 10, r: 10, b: 40, l: 40 },
|
||||||
},
|
xaxis: {
|
||||||
|
title: "Date",
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
title: "Number of Sessions",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} 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) {
|
||||||
|
const sentimentLabels = sentimentData.map((item) => item.sentiment);
|
||||||
|
const sentimentValues = sentimentData.map((item) => item.count);
|
||||||
|
const sentimentColors = sentimentLabels.map((sentiment) => {
|
||||||
|
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
|
||||||
|
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
|
||||||
|
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
|
||||||
|
return "rgb(201, 203, 207)";
|
||||||
|
});
|
||||||
|
|
||||||
if (sentimentData.length > 0) {
|
Plotly.newPlot(
|
||||||
const sentimentLabels = sentimentData.map((item) => item.sentiment);
|
"sentiment-chart",
|
||||||
const sentimentValues = sentimentData.map((item) => item.count);
|
[
|
||||||
const sentimentColors = sentimentLabels.map((sentiment) => {
|
{
|
||||||
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
|
values: sentimentValues,
|
||||||
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
|
labels: sentimentLabels,
|
||||||
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
|
type: "pie",
|
||||||
return "rgb(201, 203, 207)";
|
marker: {
|
||||||
|
colors: sentimentColors,
|
||||||
|
},
|
||||||
|
hole: 0.4,
|
||||||
|
textinfo: "label+percent",
|
||||||
|
insidetextorientation: "radial",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
document.getElementById("sentiment-chart").innerHTML =
|
||||||
|
'<div class="text-center py-5"><p class="text-muted">No sentiment data available</p></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Country chart
|
||||||
|
if (countryData && countryData.length > 0) {
|
||||||
|
const countryLabels = countryData.map((item) => item.country);
|
||||||
|
const countryValues = countryData.map((item) => item.count);
|
||||||
|
|
||||||
|
Plotly.newPlot(
|
||||||
|
"country-chart",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
x: countryValues,
|
||||||
|
y: countryLabels,
|
||||||
|
type: "bar",
|
||||||
|
orientation: "h",
|
||||||
|
marker: {
|
||||||
|
color: "rgb(54, 162, 235)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
margin: { t: 10, r: 10, b: 40, l: 100 },
|
||||||
|
xaxis: {
|
||||||
|
title: "Number of Sessions",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
document.getElementById("country-chart").innerHTML =
|
||||||
|
'<div class="text-center py-5"><p class="text-muted">No country data available</p></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category chart
|
||||||
|
if (categoryData && categoryData.length > 0) {
|
||||||
|
const categoryLabels = categoryData.map((item) => item.category);
|
||||||
|
const categoryValues = categoryData.map((item) => item.count);
|
||||||
|
|
||||||
|
Plotly.newPlot(
|
||||||
|
"category-chart",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
labels: categoryLabels,
|
||||||
|
values: categoryValues,
|
||||||
|
type: "pie",
|
||||||
|
textinfo: "label+percent",
|
||||||
|
insidetextorientation: "radial",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
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>';
|
||||||
});
|
});
|
||||||
|
|
||||||
Plotly.newPlot(
|
|
||||||
"sentiment-chart",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
values: sentimentValues,
|
|
||||||
labels: sentimentLabels,
|
|
||||||
type: "pie",
|
|
||||||
marker: {
|
|
||||||
colors: sentimentColors,
|
|
||||||
},
|
|
||||||
hole: 0.4,
|
|
||||||
textinfo: "label+percent",
|
|
||||||
insidetextorientation: "radial",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
document.getElementById("sentiment-chart").innerHTML =
|
|
||||||
'<div class="text-center py-5"><p class="text-muted">No sentiment data available</p></div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Country chart
|
|
||||||
const countryData = dashboardData.country_data;
|
|
||||||
|
|
||||||
if (countryData.length > 0) {
|
|
||||||
const countryLabels = countryData.map((item) => item.country);
|
|
||||||
const countryValues = countryData.map((item) => item.count);
|
|
||||||
|
|
||||||
Plotly.newPlot(
|
|
||||||
"country-chart",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
x: countryValues,
|
|
||||||
y: countryLabels,
|
|
||||||
type: "bar",
|
|
||||||
orientation: "h",
|
|
||||||
marker: {
|
|
||||||
color: "rgb(54, 162, 235)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
margin: { t: 10, r: 10, b: 40, l: 100 },
|
|
||||||
xaxis: {
|
|
||||||
title: "Number of Sessions",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
document.getElementById("country-chart").innerHTML =
|
|
||||||
'<div class="text-center py-5"><p class="text-muted">No country data available</p></div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Category chart
|
|
||||||
const categoryData = dashboardData.category_data;
|
|
||||||
|
|
||||||
if (categoryData.length > 0) {
|
|
||||||
const categoryLabels = categoryData.map((item) => item.category);
|
|
||||||
const categoryValues = categoryData.map((item) => item.count);
|
|
||||||
|
|
||||||
Plotly.newPlot(
|
|
||||||
"category-chart",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
labels: categoryLabels,
|
|
||||||
values: categoryValues,
|
|
||||||
type: "pie",
|
|
||||||
textinfo: "label+percent",
|
|
||||||
insidetextorientation: "radial",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
document.getElementById("category-chart").innerHTML =
|
|
||||||
'<div class="text-center py-5"><p class="text-muted">No category data available</p></div>';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -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>
|
||||||
<a
|
{% if session.session_id %}
|
||||||
href="{% url 'chat_session_detail' session.session_id %}"
|
<a
|
||||||
class="btn btn-sm btn-outline-primary"
|
href="{% url 'chat_session_detail' session.session_id %}"
|
||||||
>
|
class="btn btn-sm btn-outline-primary"
|
||||||
<i class="fas fa-eye"></i>
|
>
|
||||||
</a>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
|
|||||||
@ -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">««</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 %}
|
|
||||||
</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 %}
|
||||||
|
|||||||
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>
|
<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">««</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 %}
|
|
||||||
</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 %}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user