mirror of
https://github.com/kjanat/livegraphs-django.git
synced 2026-01-16 09:02:11 +01:00
Initial commit
This commit is contained in:
1
dashboard_project/__init__.py
Normal file
1
dashboard_project/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left empty to mark the directory as a Python package
|
||||
1
dashboard_project/accounts/__init__.py
Normal file
1
dashboard_project/accounts/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left empty to mark the directory as a Python package
|
||||
77
dashboard_project/accounts/admin.py
Normal file
77
dashboard_project/accounts/admin.py
Normal file
@ -0,0 +1,77 @@
|
||||
# accounts/admin.py
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
|
||||
from .forms import CustomUserChangeForm, CustomUserCreationForm
|
||||
from .models import Company, CustomUser
|
||||
|
||||
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
add_form = CustomUserCreationForm
|
||||
form = CustomUserChangeForm
|
||||
model = CustomUser
|
||||
list_display = ("username", "email", "company", "is_company_admin", "is_staff")
|
||||
list_filter = ("is_staff", "is_active", "company", "is_company_admin")
|
||||
fieldsets = (
|
||||
(None, {"fields": ("username", "email", "password")}),
|
||||
(
|
||||
"Permissions",
|
||||
{
|
||||
"fields": (
|
||||
"is_active",
|
||||
"is_staff",
|
||||
"is_superuser",
|
||||
"groups",
|
||||
"user_permissions",
|
||||
)
|
||||
},
|
||||
),
|
||||
("Company", {"fields": ("company", "is_company_admin")}),
|
||||
("Important dates", {"fields": ("last_login", "date_joined")}),
|
||||
)
|
||||
add_fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"classes": ("wide",),
|
||||
"fields": (
|
||||
"username",
|
||||
"email",
|
||||
"password1",
|
||||
"password2",
|
||||
"company",
|
||||
"is_company_admin",
|
||||
"is_staff",
|
||||
"is_active",
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
search_fields = ("username", "email", "company__name")
|
||||
ordering = ("username",)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super().save_model(request, obj, form, change)
|
||||
if obj.is_superuser and not obj.company:
|
||||
default_company, created = Company.objects.get_or_create(
|
||||
name="Default Organization",
|
||||
defaults={"description": "Default company for new superusers."},
|
||||
)
|
||||
obj.company = default_company
|
||||
obj.is_company_admin = True # Optionally make the superuser an admin of this default company
|
||||
obj.save()
|
||||
|
||||
|
||||
class CompanyAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "created_at", "get_employee_count")
|
||||
search_fields = ("name", "description")
|
||||
|
||||
def get_employee_count(self, obj):
|
||||
return obj.employees.count()
|
||||
|
||||
get_employee_count.short_description = "Employees"
|
||||
|
||||
|
||||
admin.site.register(CustomUser, CustomUserAdmin)
|
||||
admin.site.register(Company, CompanyAdmin)
|
||||
8
dashboard_project/accounts/apps.py
Normal file
8
dashboard_project/accounts/apps.py
Normal file
@ -0,0 +1,8 @@
|
||||
# accounts/apps.py
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "accounts"
|
||||
45
dashboard_project/accounts/forms.py
Normal file
45
dashboard_project/accounts/forms.py
Normal file
@ -0,0 +1,45 @@
|
||||
# accounts/forms.py
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
|
||||
from .models import Company, CustomUser
|
||||
|
||||
|
||||
class CustomUserCreationForm(UserCreationForm):
|
||||
"""Form for creating new users"""
|
||||
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ("username", "email", "password1", "password2")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Add help text for fields
|
||||
self.fields["email"].required = True
|
||||
self.fields["email"].help_text = "Required. Enter a valid email address."
|
||||
|
||||
|
||||
class CustomUserChangeForm(forms.ModelForm):
|
||||
"""Form for updating users"""
|
||||
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ("username", "email", "company", "is_company_admin")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Only staff members can change company and admin status
|
||||
if not kwargs.get("instance") or not kwargs.get("instance").is_staff:
|
||||
if "company" in self.fields:
|
||||
self.fields["company"].disabled = True
|
||||
if "is_company_admin" in self.fields:
|
||||
self.fields["is_company_admin"].disabled = True
|
||||
|
||||
|
||||
class CompanyForm(forms.ModelForm):
|
||||
"""Form for creating and updating companies"""
|
||||
|
||||
class Meta:
|
||||
model = Company
|
||||
fields = ("name", "description")
|
||||
1
dashboard_project/accounts/migrations/__init__.py
Normal file
1
dashboard_project/accounts/migrations/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left empty to mark the directory as a Python package
|
||||
34
dashboard_project/accounts/models.py
Normal file
34
dashboard_project/accounts/models.py
Normal file
@ -0,0 +1,34 @@
|
||||
# accounts/models.py
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
|
||||
class CustomUser(AbstractUser):
|
||||
"""Custom user model to extend the default Django user"""
|
||||
|
||||
company = models.ForeignKey(
|
||||
"Company",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="employees",
|
||||
)
|
||||
is_company_admin = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
"""Model for companies that will access the dashboard"""
|
||||
|
||||
name = models.CharField(max_length=100)
|
||||
description = models.TextField(blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Companies"
|
||||
0
dashboard_project/accounts/tests.py
Normal file
0
dashboard_project/accounts/tests.py
Normal file
28
dashboard_project/accounts/urls.py
Normal file
28
dashboard_project/accounts/urls.py
Normal file
@ -0,0 +1,28 @@
|
||||
# accounts/urls.py
|
||||
|
||||
from allauth.account import views as allauth_views
|
||||
from django.contrib.auth import views as auth_views
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"login/",
|
||||
auth_views.LoginView.as_view(template_name="accounts/login.html"),
|
||||
name="login",
|
||||
),
|
||||
path("logout/", allauth_views.LogoutView.as_view(), name="logout"), # Use allauth logout view
|
||||
path("register/", views.register_view, name="register"),
|
||||
path("profile/", views.profile_view, name="profile"),
|
||||
path(
|
||||
"password_change/",
|
||||
auth_views.PasswordChangeView.as_view(template_name="accounts/password_change.html"),
|
||||
name="password_change",
|
||||
),
|
||||
path(
|
||||
"password_change/done/",
|
||||
auth_views.PasswordChangeDoneView.as_view(template_name="accounts/password_change_done.html"),
|
||||
name="password_change_done",
|
||||
),
|
||||
]
|
||||
72
dashboard_project/accounts/views.py
Normal file
72
dashboard_project/accounts/views.py
Normal file
@ -0,0 +1,72 @@
|
||||
# accounts/views.py
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.contrib.auth import login
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import redirect, render
|
||||
|
||||
from .forms import CompanyForm, CustomUserCreationForm
|
||||
from .models import Company, CustomUser
|
||||
|
||||
|
||||
def register_view(request):
|
||||
"""View for user registration"""
|
||||
if request.method == "POST":
|
||||
form = CustomUserCreationForm(request.POST)
|
||||
if form.is_valid():
|
||||
user = form.save()
|
||||
login(request, user)
|
||||
messages.success(request, "Registration successful.")
|
||||
return redirect("dashboard")
|
||||
else:
|
||||
messages.error(request, "Registration failed. Please correct the errors.")
|
||||
else:
|
||||
form = CustomUserCreationForm()
|
||||
|
||||
return render(request, "accounts/register.html", {"form": form})
|
||||
|
||||
|
||||
@login_required
|
||||
def profile_view(request):
|
||||
"""View for user profile"""
|
||||
user = request.user
|
||||
company = user.company
|
||||
|
||||
context = {
|
||||
"user": user,
|
||||
"company": company,
|
||||
}
|
||||
|
||||
return render(request, "accounts/profile.html", context)
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def company_create_view(request):
|
||||
"""View for creating companies (admin only)"""
|
||||
if request.method == "POST":
|
||||
form = CompanyForm(request.POST)
|
||||
if form.is_valid():
|
||||
company = form.save()
|
||||
messages.success(request, f"Company '{company.name}' created successfully.")
|
||||
return redirect("admin:accounts_company_changelist")
|
||||
else:
|
||||
messages.error(request, "Failed to create company. Please correct the errors.")
|
||||
else:
|
||||
form = CompanyForm()
|
||||
|
||||
return render(request, "admin/accounts/company/create.html", {"form": form})
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def company_users_view(request, company_id):
|
||||
"""View for managing users in a company (admin only)"""
|
||||
company = Company.objects.get(pk=company_id)
|
||||
users = CustomUser.objects.filter(company=company)
|
||||
|
||||
context = {
|
||||
"company": company,
|
||||
"users": users,
|
||||
}
|
||||
|
||||
return render(request, "admin/accounts/company/users.html", context)
|
||||
16
dashboard_project/asgi.py
Normal file
16
dashboard_project/asgi.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for dashboard_project project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
|
||||
|
||||
application = get_asgi_application()
|
||||
1
dashboard_project/dashboard/__init__.py
Normal file
1
dashboard_project/dashboard/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left empty to mark the directory as a Python package
|
||||
65
dashboard_project/dashboard/admin.py
Normal file
65
dashboard_project/dashboard/admin.py
Normal file
@ -0,0 +1,65 @@
|
||||
# dashboard/admin.py
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import ChatSession, Dashboard, DataSource
|
||||
|
||||
|
||||
class DataSourceAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "company", "uploaded_at", "get_session_count")
|
||||
list_filter = ("company", "uploaded_at")
|
||||
search_fields = ("name", "description", "company__name")
|
||||
ordering = ("-uploaded_at",)
|
||||
|
||||
def get_session_count(self, obj):
|
||||
return obj.chat_sessions.count()
|
||||
|
||||
get_session_count.short_description = "Sessions"
|
||||
|
||||
|
||||
class ChatSessionAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"session_id",
|
||||
"get_company",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"country",
|
||||
"language",
|
||||
"sentiment",
|
||||
)
|
||||
list_filter = (
|
||||
"data_source__company",
|
||||
"start_time",
|
||||
"country",
|
||||
"language",
|
||||
"sentiment",
|
||||
"escalated",
|
||||
"forwarded_hr",
|
||||
)
|
||||
search_fields = (
|
||||
"session_id",
|
||||
"country",
|
||||
"language",
|
||||
"initial_msg",
|
||||
"full_transcript",
|
||||
)
|
||||
ordering = ("-start_time",)
|
||||
|
||||
def get_company(self, obj):
|
||||
return obj.data_source.company.name
|
||||
|
||||
get_company.short_description = "Company"
|
||||
get_company.admin_order_field = "data_source__company__name"
|
||||
|
||||
|
||||
class DashboardAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "company", "created_at", "updated_at")
|
||||
list_filter = ("company", "created_at")
|
||||
search_fields = ("name", "description", "company__name")
|
||||
filter_horizontal = ("data_sources",)
|
||||
ordering = ("-updated_at",)
|
||||
|
||||
|
||||
admin.site.register(DataSource, DataSourceAdmin)
|
||||
admin.site.register(ChatSession, ChatSessionAdmin)
|
||||
admin.site.register(Dashboard, DashboardAdmin)
|
||||
8
dashboard_project/dashboard/apps.py
Normal file
8
dashboard_project/dashboard/apps.py
Normal file
@ -0,0 +1,8 @@
|
||||
# dashboard/apps.py
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DashboardConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "dashboard"
|
||||
49
dashboard_project/dashboard/forms.py
Normal file
49
dashboard_project/dashboard/forms.py
Normal file
@ -0,0 +1,49 @@
|
||||
# dashboard/forms.py
|
||||
|
||||
from django import forms
|
||||
|
||||
from .models import Dashboard, DataSource
|
||||
|
||||
|
||||
class DataSourceUploadForm(forms.ModelForm):
|
||||
"""Form for uploading CSV files"""
|
||||
|
||||
class Meta:
|
||||
model = DataSource
|
||||
fields = ["name", "description", "file"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.company = kwargs.pop("company", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
if self.company:
|
||||
instance.company = self.company
|
||||
if commit:
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class DashboardForm(forms.ModelForm):
|
||||
"""Form for creating and editing dashboards"""
|
||||
|
||||
class Meta:
|
||||
model = Dashboard
|
||||
fields = ["name", "description", "data_sources"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.company = kwargs.pop("company", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.company:
|
||||
self.fields["data_sources"].queryset = DataSource.objects.filter(company=self.company)
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
if self.company:
|
||||
instance.company = self.company
|
||||
if commit:
|
||||
instance.save()
|
||||
self.save_m2m()
|
||||
return instance
|
||||
2
dashboard_project/dashboard/management/__init__.py
Normal file
2
dashboard_project/dashboard/management/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# dashboard/management/__init__.py
|
||||
# This file is intentionally left empty to mark the directory as a Python package
|
||||
@ -0,0 +1,2 @@
|
||||
# dashboard/management/commands/__init__.py
|
||||
# This file is intentionally left empty to mark the directory as a Python package
|
||||
@ -0,0 +1,277 @@
|
||||
# dashboard/management/commands/create_sample_data.py
|
||||
|
||||
import csv
|
||||
import io
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from accounts.models import Company
|
||||
from dashboard.models import ChatSession, Dashboard, DataSource
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Create sample data for testing"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
self.stdout.write("Creating sample data...")
|
||||
|
||||
# Create admin user if it doesn't exist
|
||||
if not User.objects.filter(username="admin").exists():
|
||||
admin_user = User.objects.create_superuser(username="admin", email="admin@example.com", password="admin123")
|
||||
self.stdout.write(self.style.SUCCESS(f"Created admin user: {admin_user.username}"))
|
||||
else:
|
||||
admin_user = User.objects.get(username="admin")
|
||||
self.stdout.write(f"Admin user already exists: {admin_user.username}")
|
||||
|
||||
# Create companies
|
||||
companies = []
|
||||
company_names = ["Acme Inc.", "TechCorp", "GlobalServices"]
|
||||
|
||||
for name in company_names:
|
||||
company, created = Company.objects.get_or_create(
|
||||
name=name, defaults={"description": f"Sample company: {name}"}
|
||||
)
|
||||
companies.append(company)
|
||||
|
||||
if created:
|
||||
self.stdout.write(self.style.SUCCESS(f"Created company: {company.name}"))
|
||||
else:
|
||||
self.stdout.write(f"Company already exists: {company.name}")
|
||||
|
||||
# Create users for each company
|
||||
for i, company in enumerate(companies):
|
||||
# Company admin
|
||||
username = f"admin_{company.name.lower().replace(' ', '_')}"
|
||||
if not User.objects.filter(username=username).exists():
|
||||
user = User.objects.create_user(
|
||||
username=username,
|
||||
email=f"{username}@example.com",
|
||||
password="password123",
|
||||
company=company,
|
||||
is_company_admin=True,
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"Created company admin: {user.username}"))
|
||||
|
||||
# Regular users
|
||||
for j in range(2):
|
||||
username = f"user_{company.name.lower().replace(' ', '_')}_{j + 1}"
|
||||
if not User.objects.filter(username=username).exists():
|
||||
user = User.objects.create_user(
|
||||
username=username,
|
||||
email=f"{username}@example.com",
|
||||
password="password123",
|
||||
company=company,
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"Created user: {user.username}"))
|
||||
|
||||
# Create sample data for each company
|
||||
for company in companies:
|
||||
self._create_sample_data_for_company(company)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Sample data created successfully!"))
|
||||
|
||||
def _create_sample_data_for_company(self, company):
|
||||
# Create sample CSV data
|
||||
csv_data = self._generate_sample_csv_data(company.name)
|
||||
|
||||
# Create data source
|
||||
data_source_name = f"{company.name} Chat Data"
|
||||
try:
|
||||
data_source = DataSource.objects.get(name=data_source_name, company=company)
|
||||
self.stdout.write(f"Data source already exists: {data_source.name}")
|
||||
except DataSource.DoesNotExist:
|
||||
# Create file from CSV data
|
||||
csv_file = ContentFile(csv_data.encode("utf-8"))
|
||||
data_source = DataSource.objects.create(
|
||||
name=data_source_name,
|
||||
description=f"Sample chat data for {company.name}",
|
||||
company=company,
|
||||
)
|
||||
data_source.file.save(f"{company.name.lower().replace(' ', '_')}_chat_data.csv", csv_file)
|
||||
self.stdout.write(self.style.SUCCESS(f"Created data source: {data_source.name}"))
|
||||
|
||||
# Parse CSV data and create chat sessions
|
||||
reader = csv.DictReader(io.StringIO(csv_data))
|
||||
for row in reader:
|
||||
# Convert datetime strings to datetime objects
|
||||
start_time = datetime.strptime(row["start_time"], "%Y-%m-%d %H:%M:%S")
|
||||
end_time = datetime.strptime(row["end_time"], "%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Convert boolean strings to actual booleans
|
||||
escalated = row["escalated"].lower() in ["true", "yes", "1", "t", "y"]
|
||||
forwarded_hr = row["forwarded_hr"].lower() in [
|
||||
"true",
|
||||
"yes",
|
||||
"1",
|
||||
"t",
|
||||
"y",
|
||||
]
|
||||
|
||||
# Create chat session
|
||||
ChatSession.objects.create(
|
||||
data_source=data_source,
|
||||
session_id=row["session_id"],
|
||||
start_time=timezone.make_aware(start_time),
|
||||
end_time=timezone.make_aware(end_time),
|
||||
ip_address=row["ip_address"],
|
||||
country=row["country"],
|
||||
language=row["language"],
|
||||
messages_sent=int(row["messages_sent"]),
|
||||
sentiment=row["sentiment"],
|
||||
escalated=escalated,
|
||||
forwarded_hr=forwarded_hr,
|
||||
full_transcript=row["full_transcript"],
|
||||
avg_response_time=float(row["avg_response_time"]),
|
||||
tokens=int(row["tokens"]),
|
||||
tokens_eur=float(row["tokens_eur"]),
|
||||
category=row["category"],
|
||||
initial_msg=row["initial_msg"],
|
||||
user_rating=row["user_rating"],
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"Created {reader.line_num} chat sessions"))
|
||||
|
||||
# Create default dashboard
|
||||
dashboard_name = f"{company.name} Dashboard"
|
||||
try:
|
||||
dashboard = Dashboard.objects.get(name=dashboard_name, company=company)
|
||||
self.stdout.write(f"Dashboard already exists: {dashboard.name}")
|
||||
except Dashboard.DoesNotExist:
|
||||
dashboard = Dashboard.objects.create(
|
||||
name=dashboard_name,
|
||||
description=f"Default dashboard for {company.name}",
|
||||
company=company,
|
||||
)
|
||||
dashboard.data_sources.add(data_source)
|
||||
self.stdout.write(self.style.SUCCESS(f"Created dashboard: {dashboard.name}"))
|
||||
|
||||
def _generate_sample_csv_data(self, company_name):
|
||||
"""Generate sample CSV data for a company"""
|
||||
rows = []
|
||||
headers = [
|
||||
"session_id",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"ip_address",
|
||||
"country",
|
||||
"language",
|
||||
"messages_sent",
|
||||
"sentiment",
|
||||
"escalated",
|
||||
"forwarded_hr",
|
||||
"full_transcript",
|
||||
"avg_response_time",
|
||||
"tokens",
|
||||
"tokens_eur",
|
||||
"category",
|
||||
"initial_msg",
|
||||
"user_rating",
|
||||
]
|
||||
|
||||
# Sample data for generating random values
|
||||
countries = [
|
||||
"USA",
|
||||
"UK",
|
||||
"Germany",
|
||||
"France",
|
||||
"Spain",
|
||||
"Italy",
|
||||
"Japan",
|
||||
"Australia",
|
||||
"Canada",
|
||||
"Brazil",
|
||||
]
|
||||
languages = ["English", "Spanish", "German", "French", "Japanese", "Portuguese"]
|
||||
sentiments = [
|
||||
"Positive",
|
||||
"Negative",
|
||||
"Neutral",
|
||||
"Very Positive",
|
||||
"Very Negative",
|
||||
]
|
||||
categories = ["Support", "Sales", "Technical", "Billing", "General"]
|
||||
ratings = ["Excellent", "Good", "Average", "Poor", "Terrible", ""]
|
||||
|
||||
# Generate rows
|
||||
num_rows = random.randint(50, 100)
|
||||
|
||||
for i in range(num_rows):
|
||||
# Generate random dates in the last 30 days
|
||||
end_date = datetime.now() - timedelta(days=random.randint(0, 30))
|
||||
start_date = end_date - timedelta(minutes=random.randint(5, 60))
|
||||
|
||||
# Generate random IP address
|
||||
ip = ".".join(str(random.randint(0, 255)) for _ in range(4))
|
||||
|
||||
# Random country and language
|
||||
country = random.choice(countries)
|
||||
language = random.choice(languages)
|
||||
|
||||
# Random message count
|
||||
messages_sent = random.randint(3, 20)
|
||||
|
||||
# Random sentiment
|
||||
sentiment = random.choice(sentiments)
|
||||
|
||||
# Random escalation and forwarding
|
||||
escalated = random.random() < 0.2 # 20% chance of escalation
|
||||
forwarded_hr = random.random() < 0.1 # 10% chance of forwarding to HR
|
||||
|
||||
# Generate a sample transcript
|
||||
transcript = (
|
||||
"User: Hello, I need help with my account.\n"
|
||||
"Agent: Hello! I'd be happy to help. What seems to be the issue?\n"
|
||||
"User: I can't log in to my account.\n"
|
||||
"Agent: I understand. Let me help you reset your password."
|
||||
)
|
||||
|
||||
# Random response time, tokens, and cost
|
||||
avg_response_time = round(random.uniform(0.5, 10.0), 2)
|
||||
tokens = random.randint(100, 2000)
|
||||
tokens_eur = round(tokens * 0.00002, 4) # Example rate: €0.00002 per token
|
||||
|
||||
# Random category
|
||||
category = random.choice(categories)
|
||||
|
||||
# Initial message
|
||||
initial_msg = "Hello, I need help with my account."
|
||||
|
||||
# Random rating
|
||||
user_rating = random.choice(ratings)
|
||||
|
||||
# Create row
|
||||
row = {
|
||||
"session_id": f"{company_name.lower().replace(' ', '_')}_{i + 1}",
|
||||
"start_time": start_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"end_time": end_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"ip_address": ip,
|
||||
"country": country,
|
||||
"language": language,
|
||||
"messages_sent": str(messages_sent),
|
||||
"sentiment": sentiment,
|
||||
"escalated": str(escalated),
|
||||
"forwarded_hr": str(forwarded_hr),
|
||||
"full_transcript": transcript,
|
||||
"avg_response_time": str(avg_response_time),
|
||||
"tokens": str(tokens),
|
||||
"tokens_eur": str(tokens_eur),
|
||||
"category": category,
|
||||
"initial_msg": initial_msg,
|
||||
"user_rating": user_rating,
|
||||
}
|
||||
|
||||
rows.append(row)
|
||||
|
||||
# Write to CSV string
|
||||
output = io.StringIO()
|
||||
writer = csv.DictWriter(output, fieldnames=headers)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
return output.getvalue()
|
||||
1
dashboard_project/dashboard/migrations/__init__.py
Normal file
1
dashboard_project/dashboard/migrations/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left empty to mark the directory as a Python package
|
||||
57
dashboard_project/dashboard/models.py
Normal file
57
dashboard_project/dashboard/models.py
Normal file
@ -0,0 +1,57 @@
|
||||
# dashboard/models.py
|
||||
|
||||
from accounts.models import Company
|
||||
from django.db import models
|
||||
|
||||
|
||||
class DataSource(models.Model):
|
||||
"""Model for uploaded data sources (CSV files)"""
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True)
|
||||
file = models.FileField(upload_to="data_sources/")
|
||||
uploaded_at = models.DateTimeField(auto_now_add=True)
|
||||
company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name="data_sources")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class ChatSession(models.Model):
|
||||
"""Model to store parsed chat session data from CSV"""
|
||||
|
||||
data_source = models.ForeignKey(DataSource, on_delete=models.CASCADE, related_name="chat_sessions")
|
||||
session_id = models.CharField(max_length=255)
|
||||
start_time = models.DateTimeField(null=True, blank=True)
|
||||
end_time = models.DateTimeField(null=True, blank=True)
|
||||
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||
country = models.CharField(max_length=100, blank=True)
|
||||
language = models.CharField(max_length=50, blank=True)
|
||||
messages_sent = models.IntegerField(default=0)
|
||||
sentiment = models.CharField(max_length=50, blank=True)
|
||||
escalated = models.BooleanField(default=False)
|
||||
forwarded_hr = models.BooleanField(default=False)
|
||||
full_transcript = models.TextField(blank=True)
|
||||
avg_response_time = models.FloatField(null=True, blank=True)
|
||||
tokens = models.IntegerField(default=0)
|
||||
tokens_eur = models.FloatField(null=True, blank=True)
|
||||
category = models.CharField(max_length=100, blank=True)
|
||||
initial_msg = models.TextField(blank=True)
|
||||
user_rating = models.CharField(max_length=50, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Session {self.session_id}"
|
||||
|
||||
|
||||
class Dashboard(models.Model):
|
||||
"""Model for custom dashboards that can be created by users"""
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True)
|
||||
company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name="dashboards")
|
||||
data_sources = models.ManyToManyField(DataSource, related_name="dashboards")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
2
dashboard_project/dashboard/templatetags/__init__.py
Normal file
2
dashboard_project/dashboard/templatetags/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# dashboard/templatetags/__init__.py
|
||||
# This file is intentionally left empty to mark the directory as a Python package
|
||||
56
dashboard_project/dashboard/templatetags/dashboard_extras.py
Normal file
56
dashboard_project/dashboard/templatetags/dashboard_extras.py
Normal file
@ -0,0 +1,56 @@
|
||||
# dashboard/templatetags/dashboard_extras.py
|
||||
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def split(value, delimiter):
|
||||
"""Split a string into a list based on the delimiter"""
|
||||
return value.split(delimiter)
|
||||
|
||||
|
||||
@register.filter
|
||||
def get_item(dictionary, key):
|
||||
"""Get an item from a dictionary using the key"""
|
||||
return dictionary.get(key)
|
||||
|
||||
|
||||
@register.filter
|
||||
def truncate_middle(value, max_length):
|
||||
"""Truncate a string in the middle, keeping the beginning and end"""
|
||||
if len(value) <= max_length:
|
||||
return value
|
||||
|
||||
# Calculate how many characters to keep at the start and end
|
||||
half_max = max_length // 2
|
||||
start = value[:half_max]
|
||||
end = value[-half_max:]
|
||||
|
||||
return f"{start}...{end}"
|
||||
|
||||
|
||||
@register.filter
|
||||
def format_duration(seconds):
|
||||
"""Format seconds into a human-readable duration"""
|
||||
if not seconds:
|
||||
return "0s"
|
||||
|
||||
minutes, seconds = divmod(int(seconds), 60)
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
|
||||
if hours > 0:
|
||||
return f"{hours}h {minutes}m {seconds}s"
|
||||
elif minutes > 0:
|
||||
return f"{minutes}m {seconds}s"
|
||||
else:
|
||||
return f"{seconds}s"
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def url_replace(request, field, value):
|
||||
"""Replace a GET parameter in the current URL"""
|
||||
dict_ = request.GET.copy()
|
||||
dict_[field] = value
|
||||
return dict_.urlencode()
|
||||
0
dashboard_project/dashboard/tests.py
Normal file
0
dashboard_project/dashboard/tests.py
Normal file
43
dashboard_project/dashboard/urls.py
Normal file
43
dashboard_project/dashboard/urls.py
Normal file
@ -0,0 +1,43 @@
|
||||
# dashboard/urls.py
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.dashboard_view, name="dashboard"),
|
||||
path("upload/", views.upload_data_view, name="upload_data"),
|
||||
path(
|
||||
"data-source/<int:data_source_id>/",
|
||||
views.data_source_detail_view,
|
||||
name="data_source_detail",
|
||||
),
|
||||
path(
|
||||
"chat-session/<str:session_id>/",
|
||||
views.chat_session_detail_view,
|
||||
name="chat_session_detail",
|
||||
),
|
||||
path("dashboard/create/", views.create_dashboard_view, name="create_dashboard"),
|
||||
path(
|
||||
"dashboard/<int:dashboard_id>/edit/",
|
||||
views.edit_dashboard_view,
|
||||
name="edit_dashboard",
|
||||
),
|
||||
path(
|
||||
"dashboard/<int:dashboard_id>/delete/",
|
||||
views.delete_dashboard_view,
|
||||
name="delete_dashboard",
|
||||
),
|
||||
path(
|
||||
"data-source/<int:data_source_id>/delete/",
|
||||
views.delete_data_source_view,
|
||||
name="delete_data_source",
|
||||
),
|
||||
path(
|
||||
"api/dashboard/<int:dashboard_id>/data/",
|
||||
views.dashboard_data_api,
|
||||
name="dashboard_data_api",
|
||||
),
|
||||
path("search/", views.search_chat_sessions, name="search_chat_sessions"),
|
||||
path("data-view/", views.data_view, name="data_view"),
|
||||
]
|
||||
161
dashboard_project/dashboard/utils.py
Normal file
161
dashboard_project/dashboard/utils.py
Normal file
@ -0,0 +1,161 @@
|
||||
# dashboard/utils.py
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from django.db import models
|
||||
from django.utils.timezone import make_aware
|
||||
|
||||
from .models import ChatSession
|
||||
|
||||
|
||||
def process_csv_file(data_source):
|
||||
"""
|
||||
Process the uploaded CSV file and create ChatSession objects
|
||||
|
||||
Args:
|
||||
data_source: DataSource model instance containing the CSV file
|
||||
"""
|
||||
try:
|
||||
# Read the CSV file
|
||||
file_path = data_source.file.path
|
||||
df = pd.read_csv(file_path)
|
||||
|
||||
# Process each row and create ChatSession objects
|
||||
for _, row in df.iterrows():
|
||||
# Handle datetime fields
|
||||
start_time = None
|
||||
end_time = None
|
||||
|
||||
if "start_time" in row and pd.notna(row["start_time"]):
|
||||
try:
|
||||
start_time = make_aware(pd.to_datetime(row["start_time"]))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if "end_time" in row and pd.notna(row["end_time"]):
|
||||
try:
|
||||
end_time = make_aware(pd.to_datetime(row["end_time"]))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Convert boolean fields
|
||||
escalated = str(row.get("escalated", "")).lower() in [
|
||||
"true",
|
||||
"yes",
|
||||
"1",
|
||||
"t",
|
||||
"y",
|
||||
]
|
||||
forwarded_hr = str(row.get("forwarded_hr", "")).lower() in [
|
||||
"true",
|
||||
"yes",
|
||||
"1",
|
||||
"t",
|
||||
"y",
|
||||
]
|
||||
|
||||
# Create ChatSession object
|
||||
session = ChatSession(
|
||||
data_source=data_source,
|
||||
session_id=str(row.get("session_id", "")),
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
ip_address=row.get("ip_address") if pd.notna(row.get("ip_address", np.nan)) else None,
|
||||
country=str(row.get("country", "")),
|
||||
language=str(row.get("language", "")),
|
||||
messages_sent=int(row.get("messages_sent", 0)) if pd.notna(row.get("messages_sent", np.nan)) else 0,
|
||||
sentiment=str(row.get("sentiment", "")),
|
||||
escalated=escalated,
|
||||
forwarded_hr=forwarded_hr,
|
||||
full_transcript=str(row.get("full_transcript", "")),
|
||||
avg_response_time=float(row.get("avg_response_time", 0))
|
||||
if pd.notna(row.get("avg_response_time", np.nan))
|
||||
else None,
|
||||
tokens=int(row.get("tokens", 0)) if pd.notna(row.get("tokens", np.nan)) else 0,
|
||||
tokens_eur=float(row.get("tokens_eur", 0)) if pd.notna(row.get("tokens_eur", np.nan)) else None,
|
||||
category=str(row.get("category", "")),
|
||||
initial_msg=str(row.get("initial_msg", "")),
|
||||
user_rating=str(row.get("user_rating", "")),
|
||||
)
|
||||
session.save()
|
||||
|
||||
return True, f"Successfully processed {len(df)} records."
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Error processing CSV file: {str(e)}"
|
||||
|
||||
|
||||
def generate_dashboard_data(data_sources):
|
||||
"""
|
||||
Generate aggregated data for dashboard visualization
|
||||
|
||||
Args:
|
||||
data_sources: QuerySet of DataSource objects
|
||||
|
||||
Returns:
|
||||
dict: Dictionary containing aggregated data for various charts
|
||||
"""
|
||||
# Get all chat sessions for the selected data sources
|
||||
chat_sessions = ChatSession.objects.filter(data_source__in=data_sources)
|
||||
|
||||
if not chat_sessions.exists():
|
||||
return {
|
||||
"total_sessions": 0,
|
||||
"avg_response_time": 0,
|
||||
"total_tokens": 0,
|
||||
"total_cost": 0,
|
||||
"sentiment_data": [],
|
||||
"country_data": [],
|
||||
"category_data": [],
|
||||
"time_series_data": [],
|
||||
}
|
||||
|
||||
# Basic statistics
|
||||
total_sessions = chat_sessions.count()
|
||||
avg_response_time = (
|
||||
chat_sessions.filter(avg_response_time__isnull=False).aggregate(avg=models.Avg("avg_response_time"))["avg"] or 0
|
||||
)
|
||||
total_tokens = chat_sessions.aggregate(sum=models.Sum("tokens"))["sum"] or 0
|
||||
total_cost = chat_sessions.filter(tokens_eur__isnull=False).aggregate(sum=models.Sum("tokens_eur"))["sum"] or 0
|
||||
|
||||
# Sentiment distribution
|
||||
sentiment_data = (
|
||||
chat_sessions.exclude(sentiment="").values("sentiment").annotate(count=models.Count("id")).order_by("-count")
|
||||
)
|
||||
|
||||
# Country distribution
|
||||
country_data = (
|
||||
chat_sessions.exclude(country="")
|
||||
.values("country")
|
||||
.annotate(count=models.Count("id"))
|
||||
.order_by("-count")[:10] # Top 10 countries
|
||||
)
|
||||
|
||||
# Category distribution
|
||||
category_data = (
|
||||
chat_sessions.exclude(category="").values("category").annotate(count=models.Count("id")).order_by("-count")
|
||||
)
|
||||
|
||||
# Time series data (sessions per day)
|
||||
time_series_query = (
|
||||
chat_sessions.filter(start_time__isnull=False)
|
||||
.annotate(date=models.functions.TruncDate("start_time"))
|
||||
.values("date")
|
||||
.annotate(count=models.Count("id"))
|
||||
.order_by("date")
|
||||
)
|
||||
|
||||
time_series_data = [
|
||||
{"date": entry["date"].strftime("%Y-%m-%d"), "count": entry["count"]} for entry in time_series_query
|
||||
]
|
||||
|
||||
return {
|
||||
"total_sessions": total_sessions,
|
||||
"avg_response_time": round(avg_response_time, 2),
|
||||
"total_tokens": total_tokens,
|
||||
"total_cost": round(total_cost, 2),
|
||||
"sentiment_data": list(sentiment_data),
|
||||
"country_data": list(country_data),
|
||||
"category_data": list(category_data),
|
||||
"time_series_data": time_series_data,
|
||||
}
|
||||
452
dashboard_project/dashboard/views.py
Normal file
452
dashboard_project/dashboard/views.py
Normal file
@ -0,0 +1,452 @@
|
||||
# dashboard/views.py
|
||||
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Avg, Q
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils import timezone
|
||||
|
||||
from .forms import DashboardForm, DataSourceUploadForm
|
||||
from .models import ChatSession, Dashboard, DataSource
|
||||
from .utils import generate_dashboard_data, process_csv_file
|
||||
|
||||
|
||||
@login_required
|
||||
def dashboard_view(request):
|
||||
"""Main dashboard view"""
|
||||
user = request.user
|
||||
company = user.company
|
||||
|
||||
if not company:
|
||||
messages.warning(
|
||||
request,
|
||||
"You are not associated with any company. Please contact an administrator.",
|
||||
)
|
||||
return render(request, "dashboard/no_company.html")
|
||||
|
||||
# Get the user's dashboards or create a default one
|
||||
dashboards = Dashboard.objects.filter(company=company)
|
||||
|
||||
if not dashboards.exists():
|
||||
# Create a default dashboard if none exists
|
||||
data_sources = DataSource.objects.filter(company=company)
|
||||
if data_sources.exists():
|
||||
default_dashboard = Dashboard.objects.create(
|
||||
name="Default Dashboard",
|
||||
description="Automatically created dashboard",
|
||||
company=company,
|
||||
)
|
||||
default_dashboard.data_sources.set(data_sources)
|
||||
dashboards = [default_dashboard]
|
||||
else:
|
||||
# No data sources available
|
||||
return redirect("upload_data")
|
||||
|
||||
# Use the first dashboard by default or the one specified in the request
|
||||
selected_dashboard_id = request.GET.get("dashboard_id")
|
||||
if selected_dashboard_id:
|
||||
selected_dashboard = get_object_or_404(Dashboard, id=selected_dashboard_id, company=company)
|
||||
else:
|
||||
selected_dashboard = dashboards.first()
|
||||
|
||||
# Generate dashboard data
|
||||
dashboard_data = generate_dashboard_data(selected_dashboard.data_sources.all())
|
||||
|
||||
# Convert dashboard data to JSON for use in JavaScript
|
||||
dashboard_data_json = json.dumps(
|
||||
{
|
||||
"sentiment_data": dashboard_data["sentiment_data"],
|
||||
"country_data": dashboard_data["country_data"],
|
||||
"category_data": dashboard_data["category_data"],
|
||||
"time_series_data": dashboard_data["time_series_data"],
|
||||
}
|
||||
)
|
||||
|
||||
context = {
|
||||
"dashboards": dashboards,
|
||||
"selected_dashboard": selected_dashboard,
|
||||
"dashboard_data": dashboard_data,
|
||||
"dashboard_data_json": dashboard_data_json,
|
||||
}
|
||||
|
||||
return render(request, "dashboard/dashboard.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def upload_data_view(request):
|
||||
"""View for uploading CSV files"""
|
||||
user = request.user
|
||||
company = user.company
|
||||
|
||||
if not company:
|
||||
messages.warning(
|
||||
request,
|
||||
"You are not associated with any company. Please contact an administrator.",
|
||||
)
|
||||
return redirect("dashboard")
|
||||
|
||||
if request.method == "POST":
|
||||
form = DataSourceUploadForm(request.POST, request.FILES, company=company)
|
||||
if form.is_valid():
|
||||
data_source = form.save()
|
||||
|
||||
# Process the uploaded CSV file
|
||||
success, message = process_csv_file(data_source)
|
||||
|
||||
if success:
|
||||
messages.success(request, f"File uploaded successfully. {message}")
|
||||
|
||||
# Add the new data source to all existing dashboards
|
||||
dashboards = Dashboard.objects.filter(company=company)
|
||||
for dashboard in dashboards:
|
||||
dashboard.data_sources.add(data_source)
|
||||
|
||||
return redirect("dashboard")
|
||||
else:
|
||||
# If processing failed, delete the data source
|
||||
data_source.delete()
|
||||
messages.error(request, message)
|
||||
else:
|
||||
messages.error(request, "Form is invalid. Please correct the errors.")
|
||||
else:
|
||||
form = DataSourceUploadForm()
|
||||
|
||||
# List existing data sources
|
||||
data_sources = DataSource.objects.filter(company=company).order_by("-uploaded_at")
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"data_sources": data_sources,
|
||||
}
|
||||
|
||||
return render(request, "dashboard/upload.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def data_source_detail_view(request, data_source_id):
|
||||
"""View for viewing details of a data source"""
|
||||
user = request.user
|
||||
company = user.company
|
||||
|
||||
if not company:
|
||||
messages.warning(
|
||||
request,
|
||||
"You are not associated with any company. Please contact an administrator.",
|
||||
)
|
||||
return redirect("dashboard")
|
||||
|
||||
data_source = get_object_or_404(DataSource, id=data_source_id, company=company)
|
||||
|
||||
# Get all chat sessions for this data source
|
||||
chat_sessions = ChatSession.objects.filter(data_source=data_source).order_by("-start_time")
|
||||
|
||||
# Pagination
|
||||
paginator = Paginator(chat_sessions, 20) # Show 20 records per page
|
||||
page_number = request.GET.get("page")
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
context = {
|
||||
"data_source": data_source,
|
||||
"page_obj": page_obj,
|
||||
}
|
||||
|
||||
return render(request, "dashboard/data_source_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def chat_session_detail_view(request, session_id):
|
||||
"""View for viewing details of a chat session"""
|
||||
user = request.user
|
||||
company = user.company
|
||||
|
||||
if not company:
|
||||
messages.warning(
|
||||
request,
|
||||
"You are not associated with any company. Please contact an administrator.",
|
||||
)
|
||||
return redirect("dashboard")
|
||||
|
||||
chat_session = get_object_or_404(ChatSession, session_id=session_id, data_source__company=company)
|
||||
|
||||
context = {
|
||||
"session": chat_session,
|
||||
}
|
||||
|
||||
return render(request, "dashboard/chat_session_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def create_dashboard_view(request):
|
||||
"""View for creating a custom dashboard"""
|
||||
user = request.user
|
||||
company = user.company
|
||||
|
||||
if not company:
|
||||
messages.warning(
|
||||
request,
|
||||
"You are not associated with any company. Please contact an administrator.",
|
||||
)
|
||||
return redirect("dashboard")
|
||||
|
||||
if request.method == "POST":
|
||||
form = DashboardForm(request.POST, company=company)
|
||||
if form.is_valid():
|
||||
dashboard = form.save()
|
||||
messages.success(request, f"Dashboard '{dashboard.name}' created successfully.")
|
||||
return redirect("dashboard")
|
||||
else:
|
||||
messages.error(request, "Failed to create dashboard. Please correct the errors.")
|
||||
else:
|
||||
form = DashboardForm(company=company)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"is_create": True,
|
||||
}
|
||||
|
||||
return render(request, "dashboard/dashboard_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_dashboard_view(request, dashboard_id):
|
||||
"""View for editing a dashboard"""
|
||||
user = request.user
|
||||
company = user.company
|
||||
|
||||
if not company:
|
||||
messages.warning(
|
||||
request,
|
||||
"You are not associated with any company. Please contact an administrator.",
|
||||
)
|
||||
return redirect("dashboard")
|
||||
|
||||
dashboard = get_object_or_404(Dashboard, id=dashboard_id, company=company)
|
||||
|
||||
if request.method == "POST":
|
||||
form = DashboardForm(request.POST, instance=dashboard, company=company)
|
||||
if form.is_valid():
|
||||
dashboard = form.save()
|
||||
messages.success(request, f"Dashboard '{dashboard.name}' updated successfully.")
|
||||
return redirect("dashboard")
|
||||
else:
|
||||
messages.error(request, "Failed to update dashboard. Please correct the errors.")
|
||||
else:
|
||||
form = DashboardForm(instance=dashboard, company=company)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"dashboard": dashboard,
|
||||
"is_create": False,
|
||||
}
|
||||
|
||||
return render(request, "dashboard/dashboard_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def delete_dashboard_view(request, dashboard_id):
|
||||
"""View for deleting a dashboard"""
|
||||
user = request.user
|
||||
company = user.company
|
||||
|
||||
if not company:
|
||||
messages.warning(
|
||||
request,
|
||||
"You are not associated with any company. Please contact an administrator.",
|
||||
)
|
||||
return redirect("dashboard")
|
||||
|
||||
dashboard = get_object_or_404(Dashboard, id=dashboard_id, company=company)
|
||||
|
||||
if request.method == "POST":
|
||||
dashboard_name = dashboard.name
|
||||
dashboard.delete()
|
||||
messages.success(request, f"Dashboard '{dashboard_name}' deleted successfully.")
|
||||
return redirect("dashboard")
|
||||
|
||||
context = {
|
||||
"dashboard": dashboard,
|
||||
}
|
||||
|
||||
return render(request, "dashboard/dashboard_confirm_delete.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def delete_data_source_view(request, data_source_id):
|
||||
"""View for deleting a data source"""
|
||||
user = request.user
|
||||
company = user.company
|
||||
|
||||
if not company:
|
||||
messages.warning(
|
||||
request,
|
||||
"You are not associated with any company. Please contact an administrator.",
|
||||
)
|
||||
return redirect("dashboard")
|
||||
|
||||
data_source = get_object_or_404(DataSource, id=data_source_id, company=company)
|
||||
|
||||
if request.method == "POST":
|
||||
data_source_name = data_source.name
|
||||
data_source.delete()
|
||||
messages.success(request, f"Data source '{data_source_name}' deleted successfully.")
|
||||
return redirect("upload_data")
|
||||
|
||||
context = {
|
||||
"data_source": data_source,
|
||||
}
|
||||
|
||||
return render(request, "dashboard/data_source_confirm_delete.html", context)
|
||||
|
||||
|
||||
# API views for dashboard data
|
||||
@login_required
|
||||
def dashboard_data_api(request, dashboard_id):
|
||||
"""API endpoint for dashboard data"""
|
||||
user = request.user
|
||||
company = user.company
|
||||
|
||||
if not company:
|
||||
return JsonResponse({"error": "User not associated with a company"}, status=403)
|
||||
|
||||
dashboard = get_object_or_404(Dashboard, id=dashboard_id, company=company)
|
||||
dashboard_data = generate_dashboard_data(dashboard.data_sources.all())
|
||||
|
||||
return JsonResponse(dashboard_data)
|
||||
|
||||
|
||||
@login_required
|
||||
def search_chat_sessions(request):
|
||||
"""View for searching chat sessions"""
|
||||
user = request.user
|
||||
company = user.company
|
||||
|
||||
if not company:
|
||||
messages.warning(
|
||||
request,
|
||||
"You are not associated with any company. Please contact an administrator.",
|
||||
)
|
||||
return redirect("dashboard")
|
||||
|
||||
query = request.GET.get("q", "")
|
||||
data_source_id = request.GET.get("data_source_id")
|
||||
|
||||
# Base queryset
|
||||
chat_sessions = ChatSession.objects.filter(data_source__company=company)
|
||||
|
||||
# Filter by data source if provided
|
||||
if data_source_id:
|
||||
chat_sessions = chat_sessions.filter(data_source_id=data_source_id)
|
||||
|
||||
# Apply search query if provided
|
||||
if query:
|
||||
chat_sessions = chat_sessions.filter(
|
||||
Q(session_id__icontains=query)
|
||||
| Q(country__icontains=query)
|
||||
| Q(language__icontains=query)
|
||||
| Q(sentiment__icontains=query)
|
||||
| Q(category__icontains=query)
|
||||
| Q(initial_msg__icontains=query)
|
||||
| Q(full_transcript__icontains=query)
|
||||
)
|
||||
|
||||
# Order by most recent first
|
||||
chat_sessions = chat_sessions.order_by("-start_time")
|
||||
|
||||
# Pagination
|
||||
paginator = Paginator(chat_sessions, 20) # Show 20 records per page
|
||||
page_number = request.GET.get("page")
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
# Get data source for context if filtered by data source
|
||||
data_source = None
|
||||
if data_source_id:
|
||||
data_source = get_object_or_404(DataSource, id=data_source_id, company=company)
|
||||
|
||||
context = {
|
||||
"query": query,
|
||||
"page_obj": page_obj,
|
||||
"data_source": data_source,
|
||||
}
|
||||
|
||||
return render(request, "dashboard/search_results.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def data_view(request):
|
||||
"""View for viewing all data with filtering options"""
|
||||
user = request.user
|
||||
company = user.company
|
||||
|
||||
if not company:
|
||||
messages.warning(
|
||||
request,
|
||||
"You are not associated with any company. Please contact an administrator.",
|
||||
)
|
||||
return redirect("dashboard")
|
||||
|
||||
# Get available data sources
|
||||
data_sources = DataSource.objects.filter(company=company)
|
||||
|
||||
# Get selected data source if any
|
||||
data_source_id = request.GET.get("data_source_id")
|
||||
selected_data_source = None
|
||||
if data_source_id:
|
||||
selected_data_source = get_object_or_404(DataSource, id=data_source_id, company=company)
|
||||
|
||||
# Base queryset
|
||||
chat_sessions = ChatSession.objects.filter(data_source__company=company)
|
||||
|
||||
# Apply data source filter if selected
|
||||
if selected_data_source:
|
||||
chat_sessions = chat_sessions.filter(data_source=selected_data_source)
|
||||
|
||||
# Apply view filter if any
|
||||
view = request.GET.get("view", "all")
|
||||
|
||||
if view == "recent":
|
||||
# Sessions from the last 7 days
|
||||
seven_days_ago = timezone.now() - timedelta(days=7)
|
||||
chat_sessions = chat_sessions.filter(start_time__gte=seven_days_ago)
|
||||
elif view == "positive":
|
||||
# Sessions with positive sentiment
|
||||
chat_sessions = chat_sessions.filter(Q(sentiment__icontains="positive"))
|
||||
elif view == "negative":
|
||||
# Sessions with negative sentiment
|
||||
chat_sessions = chat_sessions.filter(Q(sentiment__icontains="negative"))
|
||||
elif view == "escalated":
|
||||
# Escalated sessions
|
||||
chat_sessions = chat_sessions.filter(escalated=True)
|
||||
|
||||
# Order by most recent first
|
||||
chat_sessions = chat_sessions.order_by("-start_time")
|
||||
|
||||
# Calculate some statistics
|
||||
total_sessions = chat_sessions.count()
|
||||
avg_response_time = (
|
||||
chat_sessions.filter(avg_response_time__isnull=False).aggregate(avg=Avg("avg_response_time"))["avg"] or 0
|
||||
)
|
||||
avg_messages = chat_sessions.filter(messages_sent__gt=0).aggregate(avg=Avg("messages_sent"))["avg"] or 0
|
||||
escalated_count = chat_sessions.filter(escalated=True).count()
|
||||
escalation_rate = (escalated_count / total_sessions * 100) if total_sessions > 0 else 0
|
||||
|
||||
# Pagination
|
||||
paginator = Paginator(chat_sessions, 20) # Show 20 records per page
|
||||
page_number = request.GET.get("page")
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
context = {
|
||||
"data_sources": data_sources,
|
||||
"selected_data_source": selected_data_source,
|
||||
"page_obj": page_obj,
|
||||
"view": view,
|
||||
"avg_response_time": avg_response_time,
|
||||
"avg_messages": avg_messages,
|
||||
"escalation_rate": escalation_rate,
|
||||
}
|
||||
|
||||
return render(request, "dashboard/data_view.html", context)
|
||||
1
dashboard_project/dashboard_project/__init__.py
Normal file
1
dashboard_project/dashboard_project/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left empty to mark the directory as a Python package
|
||||
16
dashboard_project/dashboard_project/asgi.py
Normal file
16
dashboard_project/dashboard_project/asgi.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for dashboard_project project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
|
||||
|
||||
application = get_asgi_application()
|
||||
130
dashboard_project/dashboard_project/settings.py
Normal file
130
dashboard_project/dashboard_project/settings.py
Normal file
@ -0,0 +1,130 @@
|
||||
# dashboard_project/settings.py
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = "django-insecure-your-secret-key-here"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.sites",
|
||||
# Third-party apps
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
"allauth.socialaccount",
|
||||
"crispy_forms",
|
||||
"crispy_bootstrap5",
|
||||
# Custom apps
|
||||
"dashboard.apps.DashboardConfig",
|
||||
"accounts.apps.AccountsConfig",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"allauth.account.middleware.AccountMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "dashboard_project.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [os.path.join(BASE_DIR, "templates")],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "dashboard_project.wsgi.application"
|
||||
|
||||
# Database
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
# Internationalization
|
||||
LANGUAGE_CODE = "en-us"
|
||||
TIME_ZONE = "UTC"
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
STATIC_URL = "static/"
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(BASE_DIR, "static"),
|
||||
]
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
|
||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
|
||||
# Media files
|
||||
MEDIA_URL = "/media/"
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||
|
||||
# Default primary key field type
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
# Crispy Forms
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||
|
||||
# Authentication
|
||||
AUTH_USER_MODEL = "accounts.CustomUser"
|
||||
LOGIN_REDIRECT_URL = "dashboard"
|
||||
LOGOUT_REDIRECT_URL = "login"
|
||||
ACCOUNT_LOGOUT_ON_GET = True
|
||||
|
||||
# django-allauth
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
"allauth.account.auth_backends.AuthenticationBackend",
|
||||
]
|
||||
SITE_ID = 1
|
||||
ACCOUNT_EMAIL_VERIFICATION = "none"
|
||||
17
dashboard_project/dashboard_project/urls.py
Normal file
17
dashboard_project/dashboard_project/urls.py
Normal file
@ -0,0 +1,17 @@
|
||||
# dashboard_project/urls.py
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("accounts/", include("accounts.urls")),
|
||||
path("dashboard/", include("dashboard.urls")),
|
||||
path("", RedirectView.as_view(url="dashboard/", permanent=False)),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
16
dashboard_project/dashboard_project/wsgi.py
Normal file
16
dashboard_project/dashboard_project/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for dashboard_project project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
19
dashboard_project/manage.py
Normal file
19
dashboard_project/manage.py
Normal file
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError("Couldn't import Django. Are you sure it's installed?") from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
262
dashboard_project/static/css/dashboard.css
Normal file
262
dashboard_project/static/css/dashboard.css
Normal file
@ -0,0 +1,262 @@
|
||||
/**
|
||||
* dashboard.css - Styles specific to dashboard functionality
|
||||
*/
|
||||
|
||||
/* Dashboard grid layout */
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
/* Slightly larger minmax for widgets */
|
||||
gap: 1.5rem;
|
||||
/* Increased gap */
|
||||
}
|
||||
|
||||
/* Dashboard widget cards */
|
||||
.dashboard-widget {
|
||||
display: flex;
|
||||
/* Allow flex for content alignment */
|
||||
flex-direction: column;
|
||||
/* Stack header, body, footer vertically */
|
||||
height: 100%;
|
||||
/* Ensure widgets fill grid cell height */
|
||||
}
|
||||
|
||||
.dashboard-widget .card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dashboard-widget .card-header .widget-title {
|
||||
font-size: 1.1rem;
|
||||
/* Slightly larger widget titles */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard-widget .card-header .widget-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dashboard-widget .card-header .widget-actions .btn {
|
||||
width: 32px;
|
||||
/* Slightly larger action buttons */
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.85rem;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.dashboard-widget .card-header .widget-actions .btn:hover {
|
||||
background-color: #f0f0f0;
|
||||
border-color: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.dashboard-widget .card-body {
|
||||
flex-grow: 1;
|
||||
/* Allow card body to take available space */
|
||||
padding: 1.25rem;
|
||||
/* Consistent padding */
|
||||
}
|
||||
|
||||
/* Chart widgets */
|
||||
.chart-widget .card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-widget .chart-container {
|
||||
flex: 1;
|
||||
min-height: 250px;
|
||||
/* Adjusted min-height */
|
||||
width: 100%;
|
||||
/* Ensure it takes full width of card body */
|
||||
}
|
||||
|
||||
/* Stat widgets / Stat Cards */
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
/* Generous padding */
|
||||
}
|
||||
|
||||
.stat-card .stat-icon {
|
||||
font-size: 2.25rem;
|
||||
/* Larger icon */
|
||||
margin-bottom: 1rem;
|
||||
display: inline-block;
|
||||
width: 4.5rem;
|
||||
height: 4.5rem;
|
||||
line-height: 4.5rem;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
background-color: #e9f2ff;
|
||||
/* Light blue background for icon */
|
||||
color: #007bff;
|
||||
/* Primary color for icon */
|
||||
}
|
||||
|
||||
.stat-card .stat-value {
|
||||
font-size: 2.25rem;
|
||||
/* Larger stat value */
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
/* Reduced margin */
|
||||
line-height: 1.1;
|
||||
color: #212529;
|
||||
/* Darker color for value */
|
||||
}
|
||||
|
||||
.stat-card .stat-label {
|
||||
font-size: 0.9rem;
|
||||
/* Slightly larger label */
|
||||
color: #6c757d;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Dashboard theme variations */
|
||||
.dashboard-theme-light .card {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.dashboard-theme-dark {
|
||||
background-color: #212529;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.dashboard-theme-dark .card {
|
||||
background-color: #343a40;
|
||||
color: #f8f9fa;
|
||||
border-color: #495057;
|
||||
}
|
||||
|
||||
.dashboard-theme-dark .card-header {
|
||||
background-color: #495057;
|
||||
border-bottom-color: #6c757d;
|
||||
}
|
||||
|
||||
.dashboard-theme-dark .stat-card .stat-label {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
/* Time period selector */
|
||||
.time-period-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
/* Increased gap */
|
||||
margin-bottom: 1.5rem;
|
||||
/* Increased margin */
|
||||
}
|
||||
|
||||
.time-period-selector .btn-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.time-period-selector .btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
/* Bootstrap-like padding */
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Custom metric selector */
|
||||
.metric-selector {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.metric-selector .nav-link {
|
||||
white-space: nowrap;
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.metric-selector .nav-link.active {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Dashboard loading states */
|
||||
.widget-placeholder {
|
||||
min-height: 300px;
|
||||
background: linear-gradient(90deg, #e9ecef 25%, #f8f9fa 50%, #e9ecef 75%);
|
||||
/* Lighter gradient */
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.8s infinite ease-in-out;
|
||||
/* Smoother animation */
|
||||
border-radius: 0.5rem;
|
||||
/* Consistent with cards */
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dashboard empty states */
|
||||
.empty-state {
|
||||
padding: 2.5rem;
|
||||
/* Increased padding */
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
background-color: #f8f9fa;
|
||||
/* Light background for empty state */
|
||||
border-radius: 0.5rem;
|
||||
border: 1px dashed #ced4da;
|
||||
/* Dashed border */
|
||||
}
|
||||
|
||||
.empty-state .empty-state-icon {
|
||||
font-size: 3.5rem;
|
||||
/* Larger icon */
|
||||
margin-bottom: 1.5rem;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.empty-state .empty-state-message {
|
||||
font-size: 1.2rem;
|
||||
/* Slightly larger message */
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-state .btn {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 767.98px) {
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.stat-card .stat-icon {
|
||||
font-size: 1.5rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.stat-card .stat-value {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
328
dashboard_project/static/css/style.css
Normal file
328
dashboard_project/static/css/style.css
Normal file
@ -0,0 +1,328 @@
|
||||
/**
|
||||
* style.css - Global styles for the application
|
||||
*/
|
||||
|
||||
/* General Styles */
|
||||
body {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
background-color: #f4f7f9;
|
||||
/* Lighter, cleaner background */
|
||||
color: #333;
|
||||
/* Darker text for better contrast */
|
||||
line-height: 1.6;
|
||||
display: flex;
|
||||
/* Added for sticky footer */
|
||||
flex-direction: column;
|
||||
/* Added for sticky footer */
|
||||
min-height: 100vh;
|
||||
/* Ensures body takes at least full viewport height */
|
||||
}
|
||||
|
||||
/* Navbar adjustments (if needed, Bootstrap usually handles this well) */
|
||||
.navbar {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
/* Subtle shadow for depth */
|
||||
}
|
||||
|
||||
/* Helper Classes */
|
||||
.text-truncate-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.min-w-150 {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
/* Card styles */
|
||||
.card {
|
||||
border: 1px solid #e0e5e9;
|
||||
/* Lighter border */
|
||||
border-radius: 0.5rem;
|
||||
/* Slightly more rounded corners */
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
/* Softer, more modern shadow */
|
||||
transition:
|
||||
transform 0.2s ease-in-out,
|
||||
box-shadow 0.2s ease-in-out;
|
||||
margin-bottom: 1.5rem;
|
||||
/* Consistent margin */
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #ffffff;
|
||||
/* Clean white header */
|
||||
border-bottom: 1px solid #e0e5e9;
|
||||
font-weight: 500;
|
||||
/* Slightly bolder header text */
|
||||
padding: 0.75rem 1.25rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.15rem;
|
||||
/* Adjusted card title size */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Sidebar enhancements */
|
||||
.sidebar {
|
||||
background-color: #ffffff;
|
||||
/* White sidebar for a cleaner look */
|
||||
border-right: 1px solid #e0e5e9;
|
||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.03);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.sidebar-sticky {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
color: #4a5568;
|
||||
/* Softer link color */
|
||||
padding: 0.65rem 1.25rem;
|
||||
/* Adjusted padding */
|
||||
border-radius: 0.375rem;
|
||||
/* Bootstrap-like rounded corners for links */
|
||||
margin: 0.1rem 0.5rem;
|
||||
/* Margin around links */
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover {
|
||||
color: #007bff;
|
||||
/* Primary color on hover */
|
||||
background-color: #e9f2ff;
|
||||
/* Light blue background on hover */
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
background-color: #d6e4ff;
|
||||
/* Slightly darker blue for active */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar .nav-link i.me-2 {
|
||||
width: 20px;
|
||||
/* Ensure icons align well */
|
||||
text-align: center;
|
||||
margin-right: 0.75rem !important;
|
||||
/* Consistent icon spacing */
|
||||
}
|
||||
|
||||
.sidebar .nav-header {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #718096;
|
||||
/* Softer header color */
|
||||
padding: 0.5rem 1.25rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* Dashboard stats cards */
|
||||
.stats-card {
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stats-card h3 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stats-card p {
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Chart containers */
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Loading overlay */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* Table enhancements */
|
||||
.table {
|
||||
border-color: #e0e5e9;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: 600;
|
||||
/* Bolder table headers */
|
||||
color: #4a5568;
|
||||
background-color: #f8f9fc;
|
||||
/* Light background for headers */
|
||||
}
|
||||
|
||||
.table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
/* Very subtle striping */
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: #e9f2ff;
|
||||
/* Consistent hover with sidebar */
|
||||
}
|
||||
|
||||
/* Form improvements */
|
||||
.form-control,
|
||||
.form-select {
|
||||
border-color: #ced4da;
|
||||
border-radius: 0.375rem;
|
||||
/* Consistent border radius */
|
||||
padding: 0.5rem 0.75rem;
|
||||
/* Adjusted padding */
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: #86b7fe;
|
||||
/* Bootstrap focus color */
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
/* Bootstrap focus shadow */
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
.btn {
|
||||
border-radius: 0.375rem;
|
||||
/* Consistent border radius */
|
||||
padding: 0.5rem 1rem;
|
||||
/* Standard button padding */
|
||||
font-weight: 500;
|
||||
transition:
|
||||
background-color 0.15s ease-in-out,
|
||||
border-color 0.15s ease-in-out,
|
||||
box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0069d9;
|
||||
border-color: #0062cc;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #5a6268;
|
||||
border-color: #545b62;
|
||||
}
|
||||
|
||||
/* Alert styling */
|
||||
.alert {
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.9rem 1.25rem;
|
||||
}
|
||||
|
||||
/* Chat transcript styling */
|
||||
.chat-transcript {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.chat-transcript pre {
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Footer styling */
|
||||
footer {
|
||||
background-color: #ffffff;
|
||||
/* White footer */
|
||||
border-top: 1px solid #e0e5e9;
|
||||
padding: 1.5rem 0;
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
margin-top: auto;
|
||||
/* Added for sticky footer */
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 767.98px) {
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.stats-card h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.sidebar,
|
||||
.navbar,
|
||||
.btn,
|
||||
footer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
break-inside: avoid;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
break-inside: avoid;
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
253
dashboard_project/static/js/dashboard.js
Normal file
253
dashboard_project/static/js/dashboard.js
Normal file
@ -0,0 +1,253 @@
|
||||
/**
|
||||
* dashboard.js - JavaScript for the dashboard functionality
|
||||
*
|
||||
* This file handles the interactive features of the dashboard,
|
||||
* including chart refreshing, dashboard filtering, and dashboard
|
||||
* customization.
|
||||
*/
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Chart responsiveness
|
||||
function resizeCharts() {
|
||||
const charts = document.querySelectorAll(".chart-container");
|
||||
charts.forEach((chart) => {
|
||||
if (chart.id && window.Plotly) {
|
||||
Plotly.relayout(chart.id, {
|
||||
"xaxis.automargin": true,
|
||||
"yaxis.automargin": true,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener("resize", function () {
|
||||
if (window.Plotly) {
|
||||
resizeCharts();
|
||||
}
|
||||
});
|
||||
|
||||
// Time range filtering
|
||||
const timeRangeDropdown = document.getElementById("timeRangeDropdown");
|
||||
if (timeRangeDropdown) {
|
||||
const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item");
|
||||
timeRangeLinks.forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
const url = new URL(this.href);
|
||||
const dashboardId = url.searchParams.get("dashboard_id");
|
||||
const timeRange = url.searchParams.get("time_range");
|
||||
|
||||
// Fetch updated data via AJAX
|
||||
if (dashboardId) {
|
||||
fetchDashboardData(dashboardId, timeRange);
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Function to fetch dashboard data
|
||||
function fetchDashboardData(dashboardId, timeRange) {
|
||||
const loadingOverlay = document.createElement("div");
|
||||
loadingOverlay.className = "loading-overlay";
|
||||
loadingOverlay.innerHTML =
|
||||
'<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>';
|
||||
document.querySelector("main").appendChild(loadingOverlay);
|
||||
|
||||
fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
updateDashboardStats(data);
|
||||
updateDashboardCharts(data);
|
||||
|
||||
// Update URL without page reload
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("dashboard_id", dashboardId);
|
||||
if (timeRange) {
|
||||
url.searchParams.set("time_range", timeRange);
|
||||
}
|
||||
window.history.pushState({}, "", url);
|
||||
|
||||
document.querySelector(".loading-overlay").remove();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching dashboard data:", error);
|
||||
document.querySelector(".loading-overlay").remove();
|
||||
|
||||
// Show error message
|
||||
const alertElement = document.createElement("div");
|
||||
alertElement.className = "alert alert-danger alert-dismissible fade show";
|
||||
alertElement.setAttribute("role", "alert");
|
||||
alertElement.innerHTML = `
|
||||
Error loading dashboard data. Please try again.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
`;
|
||||
document.querySelector("main").prepend(alertElement);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to update dashboard statistics
|
||||
function updateDashboardStats(data) {
|
||||
// Update total sessions
|
||||
const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3");
|
||||
if (totalSessionsElement) {
|
||||
totalSessionsElement.textContent = data.total_sessions;
|
||||
}
|
||||
|
||||
// Update average response time
|
||||
const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3");
|
||||
if (avgResponseTimeElement) {
|
||||
avgResponseTimeElement.textContent = data.avg_response_time + "s";
|
||||
}
|
||||
|
||||
// Update total tokens
|
||||
const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3");
|
||||
if (totalTokensElement) {
|
||||
totalTokensElement.textContent = data.total_tokens;
|
||||
}
|
||||
|
||||
// Update total cost
|
||||
const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3");
|
||||
if (totalCostElement) {
|
||||
totalCostElement.textContent = "€" + data.total_cost;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to update dashboard charts
|
||||
function updateDashboardCharts(data) {
|
||||
// Update sessions over time chart
|
||||
const timeSeriesData = data.time_series_data;
|
||||
if (timeSeriesData && timeSeriesData.length > 0 && window.Plotly) {
|
||||
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
||||
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
||||
|
||||
Plotly.react(
|
||||
"sessions-time-chart",
|
||||
[
|
||||
{
|
||||
x: timeSeriesX,
|
||||
y: timeSeriesY,
|
||||
type: "scatter",
|
||||
mode: "lines+markers",
|
||||
line: {
|
||||
color: "rgb(75, 192, 192)",
|
||||
width: 2,
|
||||
},
|
||||
marker: {
|
||||
color: "rgb(75, 192, 192)",
|
||||
size: 6,
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
margin: { t: 10, r: 10, b: 40, l: 40 },
|
||||
xaxis: {
|
||||
title: "Date",
|
||||
},
|
||||
yaxis: {
|
||||
title: "Number of Sessions",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Update sentiment chart
|
||||
const sentimentData = data.sentiment_data;
|
||||
if (sentimentData && sentimentData.length > 0 && window.Plotly) {
|
||||
const sentimentLabels = sentimentData.map((item) => item.sentiment);
|
||||
const sentimentValues = sentimentData.map((item) => item.count);
|
||||
const sentimentColors = sentimentLabels.map((sentiment) => {
|
||||
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
|
||||
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
|
||||
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
|
||||
return "rgb(201, 203, 207)";
|
||||
});
|
||||
|
||||
Plotly.react(
|
||||
"sentiment-chart",
|
||||
[
|
||||
{
|
||||
values: sentimentValues,
|
||||
labels: sentimentLabels,
|
||||
type: "pie",
|
||||
marker: {
|
||||
colors: sentimentColors,
|
||||
},
|
||||
hole: 0.4,
|
||||
textinfo: "label+percent",
|
||||
insidetextorientation: "radial",
|
||||
},
|
||||
],
|
||||
{
|
||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Update country chart
|
||||
const countryData = data.country_data;
|
||||
if (countryData && countryData.length > 0 && window.Plotly) {
|
||||
const countryLabels = countryData.map((item) => item.country);
|
||||
const countryValues = countryData.map((item) => item.count);
|
||||
|
||||
Plotly.react(
|
||||
"country-chart",
|
||||
[
|
||||
{
|
||||
x: countryValues,
|
||||
y: countryLabels,
|
||||
type: "bar",
|
||||
orientation: "h",
|
||||
marker: {
|
||||
color: "rgb(54, 162, 235)",
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
margin: { t: 10, r: 10, b: 40, l: 100 },
|
||||
xaxis: {
|
||||
title: "Number of Sessions",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Update category chart
|
||||
const categoryData = data.category_data;
|
||||
if (categoryData && categoryData.length > 0 && window.Plotly) {
|
||||
const categoryLabels = categoryData.map((item) => item.category);
|
||||
const categoryValues = categoryData.map((item) => item.count);
|
||||
|
||||
Plotly.react(
|
||||
"category-chart",
|
||||
[
|
||||
{
|
||||
labels: categoryLabels,
|
||||
values: categoryValues,
|
||||
type: "pie",
|
||||
textinfo: "label+percent",
|
||||
insidetextorientation: "radial",
|
||||
},
|
||||
],
|
||||
{
|
||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard selector
|
||||
const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]');
|
||||
dashboardSelector.forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
const url = new URL(this.href);
|
||||
const dashboardId = url.searchParams.get("dashboard_id");
|
||||
|
||||
// Fetch updated data via AJAX
|
||||
if (dashboardId) {
|
||||
fetchDashboardData(dashboardId);
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
152
dashboard_project/static/js/main.js
Normal file
152
dashboard_project/static/js/main.js
Normal file
@ -0,0 +1,152 @@
|
||||
/**
|
||||
* main.js - Global JavaScript functionality
|
||||
*
|
||||
* This file contains general JavaScript functionality used across
|
||||
* the entire application, including navigation, forms, and UI interactions.
|
||||
*/
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Initialize tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Initialize popovers
|
||||
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
||||
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
|
||||
return new bootstrap.Popover(popoverTriggerEl);
|
||||
});
|
||||
|
||||
// Toggle sidebar on mobile
|
||||
const sidebarToggle = document.querySelector("#sidebarToggle");
|
||||
if (sidebarToggle) {
|
||||
sidebarToggle.addEventListener("click", function () {
|
||||
document.querySelector(".sidebar").classList.toggle("show");
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-dismiss alerts after 5 seconds
|
||||
setTimeout(function () {
|
||||
var alerts = document.querySelectorAll(".alert:not(.alert-important)");
|
||||
alerts.forEach(function (alert) {
|
||||
if (alert && bootstrap.Alert.getInstance(alert)) {
|
||||
bootstrap.Alert.getInstance(alert).close();
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
// Form validation
|
||||
const forms = document.querySelectorAll(".needs-validation");
|
||||
forms.forEach(function (form) {
|
||||
form.addEventListener(
|
||||
"submit",
|
||||
function (event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
form.classList.add("was-validated");
|
||||
},
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
// Confirm dialogs
|
||||
const confirmButtons = document.querySelectorAll("[data-confirm]");
|
||||
confirmButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function (event) {
|
||||
if (!confirm(this.dataset.confirm || "Are you sure?")) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Back button
|
||||
const backButtons = document.querySelectorAll(".btn-back");
|
||||
backButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
window.history.back();
|
||||
});
|
||||
});
|
||||
|
||||
// File input customization
|
||||
const fileInputs = document.querySelectorAll(".custom-file-input");
|
||||
fileInputs.forEach(function (input) {
|
||||
input.addEventListener("change", function (e) {
|
||||
const fileName = this.files[0]?.name || "Choose file";
|
||||
const nextSibling = this.nextElementSibling;
|
||||
if (nextSibling) {
|
||||
nextSibling.innerText = fileName;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Search form submit on enter
|
||||
const searchInputs = document.querySelectorAll(".search-input");
|
||||
searchInputs.forEach(function (input) {
|
||||
input.addEventListener("keypress", function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.closest("form").submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle password visibility
|
||||
const togglePasswordButtons = document.querySelectorAll(".toggle-password");
|
||||
togglePasswordButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function () {
|
||||
const target = document.querySelector(this.dataset.target);
|
||||
if (target) {
|
||||
const type = target.getAttribute("type") === "password" ? "text" : "password";
|
||||
target.setAttribute("type", type);
|
||||
this.querySelector("i").classList.toggle("fa-eye");
|
||||
this.querySelector("i").classList.toggle("fa-eye-slash");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Dropdown menu positioning
|
||||
const dropdowns = document.querySelectorAll(".dropdown-menu");
|
||||
dropdowns.forEach(function (dropdown) {
|
||||
dropdown.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
// Responsive table handling
|
||||
const tables = document.querySelectorAll(".table-responsive");
|
||||
if (window.innerWidth < 768) {
|
||||
tables.forEach(function (table) {
|
||||
table.classList.add("table-responsive-force");
|
||||
});
|
||||
}
|
||||
|
||||
// Handle special links (printable views, exports)
|
||||
const printLinks = document.querySelectorAll(".print-link");
|
||||
printLinks.forEach(function (link) {
|
||||
link.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
window.print();
|
||||
});
|
||||
});
|
||||
|
||||
const exportLinks = document.querySelectorAll("[data-export]");
|
||||
exportLinks.forEach(function (link) {
|
||||
link.addEventListener("click", function (e) {
|
||||
// Handle export functionality if needed
|
||||
console.log("Export requested:", this.dataset.export);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle sidebar collapse on small screens
|
||||
function handleSidebarOnResize() {
|
||||
if (window.innerWidth < 768) {
|
||||
document.querySelector(".sidebar")?.classList.remove("show");
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", handleSidebarOnResize);
|
||||
});
|
||||
29
dashboard_project/templates/accounts/login.html
Normal file
29
dashboard_project/templates/accounts/login.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!-- templates/accounts/login.html -->
|
||||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Login | Chat Analytics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title mb-0">Login</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<p class="mb-0">Don't have an account? <a href="{% url 'register' %}">Register</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
37
dashboard_project/templates/accounts/password_change.html
Normal file
37
dashboard_project/templates/accounts/password_change.html
Normal file
@ -0,0 +1,37 @@
|
||||
<!-- templates/accounts/password_change.html -->
|
||||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Change Password | Chat Analytics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div
|
||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||
>
|
||||
<h1 class="h2">Change Password</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<a href="{% url 'profile' %}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Profile
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Change Your Password</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">Change Password</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -0,0 +1,41 @@
|
||||
<!-- templates/accounts/password_change_done.html -->
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Password Changed | Chat Analytics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div
|
||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||
>
|
||||
<h1 class="h2">Password Changed</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<a href="{% url 'profile' %}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Profile
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="card-title mb-0">Password Changed Successfully</h5>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-4">
|
||||
<i class="fas fa-check-circle fa-4x text-success mb-3"></i>
|
||||
<h4>Your password has been changed successfully!</h4>
|
||||
<p>Your new password is now active. You can use it the next time you log in.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{% url 'profile' %}" class="btn btn-primary">Return to Profile</a>
|
||||
<a href="{% url 'dashboard' %}" class="btn btn-outline-secondary ms-2"
|
||||
>Go to Dashboard</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
189
dashboard_project/templates/accounts/profile.html
Normal file
189
dashboard_project/templates/accounts/profile.html
Normal file
@ -0,0 +1,189 @@
|
||||
<!-- templates/accounts/profile.html -->
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}My Profile | Chat Analytics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div
|
||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||
>
|
||||
<h1 class="h2">My Profile</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Account Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Username:</div>
|
||||
<div class="col-md-8">{{ user.username }}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Email:</div>
|
||||
<div class="col-md-8">{{ user.email }}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Company:</div>
|
||||
<div class="col-md-8">
|
||||
{% if user.company %}
|
||||
{{ user.company.name }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not assigned to a company</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Role:</div>
|
||||
<div class="col-md-8">
|
||||
{% if user.is_staff %}
|
||||
<span class="badge bg-danger">Admin</span>
|
||||
{% elif user.is_company_admin %}
|
||||
<span class="badge bg-primary">Company Admin</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">User</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Last Login:</div>
|
||||
<div class="col-md-8">{{ user.last_login|date:"F d, Y H:i" }}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Date Joined:</div>
|
||||
<div class="col-md-8">{{ user.date_joined|date:"F d, Y H:i" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a href="{% url 'password_change' %}" class="btn btn-primary">Change Password</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if user.company %}
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Company Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Company Name:</div>
|
||||
<div class="col-md-8">{{ user.company.name }}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Description:</div>
|
||||
<div class="col-md-8">
|
||||
{{ user.company.description|default:"No description available." }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Created:</div>
|
||||
<div class="col-md-8">{{ user.company.created_at|date:"F d, Y" }}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Total Employees:</div>
|
||||
<div class="col-md-8">{{ user.company.employees.count }}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Data Sources:</div>
|
||||
<div class="col-md-8">{{ user.company.data_sources.count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if user.is_company_admin or user.is_staff %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Admin Actions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
{% if user.is_staff %}
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Manage Users</h5>
|
||||
<p class="card-text">Manage users and assign them to companies.</p>
|
||||
<a
|
||||
href="{% url 'admin:accounts_customuser_changelist' %}"
|
||||
class="btn btn-primary"
|
||||
>Manage Users</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Manage Companies</h5>
|
||||
<p class="card-text">Create and edit companies in the system.</p>
|
||||
<a
|
||||
href="{% url 'admin:accounts_company_changelist' %}"
|
||||
class="btn btn-primary"
|
||||
>Manage Companies</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Admin Dashboard</h5>
|
||||
<p class="card-text">Go to the full admin dashboard.</p>
|
||||
<a href="{% url 'admin:index' %}" class="btn btn-primary">Admin Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif user.is_company_admin %}
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Manage Dashboards</h5>
|
||||
<p class="card-text">Create and edit dashboards for your company.</p>
|
||||
<a href="{% url 'create_dashboard' %}" class="btn btn-primary"
|
||||
>Manage Dashboards</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Upload Data</h5>
|
||||
<p class="card-text">Upload and manage data sources for analysis.</p>
|
||||
<a href="{% url 'upload_data' %}" class="btn btn-primary">Upload Data</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Search Sessions</h5>
|
||||
<p class="card-text">Search and analyze chat sessions.</p>
|
||||
<a href="{% url 'search_chat_sessions' %}" class="btn btn-primary"
|
||||
>Search Sessions</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
29
dashboard_project/templates/accounts/register.html
Normal file
29
dashboard_project/templates/accounts/register.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!-- templates/accounts/register.html -->
|
||||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Register | Chat Analytics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title mb-0">Register</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">Register</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<p class="mb-0">Already have an account? <a href="{% url 'login' %}">Login</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
312
dashboard_project/templates/base.html
Normal file
312
dashboard_project/templates/base.html
Normal file
@ -0,0 +1,312 @@
|
||||
<!-- templates/base.html -->
|
||||
{% load static %}
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{% block title %}Chat Analytics Dashboard{% endblock %}</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@latest/css/all.min.css"
|
||||
/>
|
||||
|
||||
<!-- Plotly.js -->
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/plotly.js@latest/dist/plotly.min.js"
|
||||
charset="utf-8"
|
||||
></script>
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="{% static 'css/style.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'css/dashboard.css' %}" />
|
||||
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark absolute-top">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{% url 'dashboard' %}">Chat Analytics</a>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarCollapse"
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-md-0">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
|
||||
href="{% url 'dashboard' %}"
|
||||
>Dashboard</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}"
|
||||
href="{% url 'upload_data' %}"
|
||||
>Upload Data</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}"
|
||||
href="{% url 'search_chat_sessions' %}"
|
||||
>Search</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="d-flex">
|
||||
{% if user.is_authenticated %}
|
||||
<div class="dropdown">
|
||||
<button
|
||||
class="btn btn-outline-light dropdown-toggle"
|
||||
type="button"
|
||||
id="userDropdown"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{% if user.company %}
|
||||
<span class="badge bg-info me-1">{{ user.company.name }}</span>
|
||||
{% endif %}
|
||||
{{ user.username }}
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
|
||||
<li><a class="dropdown-item" href="{% url 'profile' %}">Profile</a></li>
|
||||
{% if user.is_staff %}
|
||||
<li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<hr class="dropdown-divider" />
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="{% url 'logout' %}">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="{% url 'login' %}" class="btn btn-outline-light me-2">Login</a>
|
||||
<a href="{% url 'register' %}" class="btn btn-light">Register</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Sidebar -->
|
||||
<nav
|
||||
id="sidebarMenu"
|
||||
class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse sticky-top h-100 p-0"
|
||||
>
|
||||
<div class="sidebar-sticky pt-3">
|
||||
{% block sidebar %}
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
|
||||
href="{% url 'dashboard' %}"
|
||||
>
|
||||
<i class="fas fa-tachometer-alt me-2"></i>
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}"
|
||||
href="{% url 'upload_data' %}"
|
||||
>
|
||||
<i class="fas fa-upload me-2"></i>
|
||||
Upload Data
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}"
|
||||
href="{% url 'search_chat_sessions' %}"
|
||||
>
|
||||
<i class="fas fa-search me-2"></i>
|
||||
Search
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link {% if request.resolver_match.url_name == 'data_view' %}active{% endif %}"
|
||||
href="{% url 'data_view' %}"
|
||||
>
|
||||
<i class="fas fa-table me-2"></i>
|
||||
Data View
|
||||
</a>
|
||||
</li>
|
||||
{% if user.is_authenticated and user.company %}
|
||||
{% if dashboards %}
|
||||
<li class="nav-header"><strong>Dashboards</strong></li>
|
||||
{% for dashboard in dashboards %}
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link {% if selected_dashboard.id == dashboard.id %}active{% endif %}"
|
||||
href="{% url 'dashboard' %}?dashboard_id={{ dashboard.id }}"
|
||||
>
|
||||
<i class="fas fa-chart-line me-2"></i>
|
||||
{{ dashboard.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'create_dashboard' %}">
|
||||
<i class="fas fa-plus-circle me-2"></i>
|
||||
New Dashboard
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if data_sources %}
|
||||
<li class="nav-header"><strong>Data Sources</strong></li>
|
||||
{% for data_source in data_sources %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'data_source_detail' data_source.id %}">
|
||||
<i class="fas fa-database me-2"></i>
|
||||
{{ data_source.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
|
||||
{# {% if messages %} #}
|
||||
{# <div class="messages mt-3"> #}
|
||||
{# {% for message in messages %} #}
|
||||
{# <div class="alert {% if message.tags == 'error' %}alert-danger{% elif message.tags == 'success' %}alert-success{% elif message.tags == 'warning' %}alert-warning{% else %}alert-info{% endif %} alert-dismissible fade show" role="alert"> #}
|
||||
{# {{ message }} #}
|
||||
{# <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> #}
|
||||
{# </div> #}
|
||||
{# {% endfor %} #}
|
||||
{# </div> #}
|
||||
{# {% endif %} #}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>© {% now "Y" %} Chat Analytics Dashboard. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- jQuery (for Ajax) -->
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
{% if messages %}
|
||||
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1100;">
|
||||
<!-- Toasts will be appended here -->
|
||||
</div>
|
||||
|
||||
{% for message in messages %}
|
||||
<!-- Pre-render message data that will be used by JavaScript -->
|
||||
<script type="application/json" id="message-data-{{ forloop.counter }}">
|
||||
{
|
||||
"message": "{{ message|escapejs }}",
|
||||
"tags": "{{ message.tags|default:'' }}"
|
||||
}
|
||||
</script>
|
||||
{% endfor %}
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const toastContainer = document.querySelector(".toast-container");
|
||||
if (!toastContainer) return;
|
||||
|
||||
// Find all message data elements
|
||||
const messageDataElements = document.querySelectorAll('script[id^="message-data-"]');
|
||||
messageDataElements.forEach(function (dataElement) {
|
||||
try {
|
||||
const messageData = JSON.parse(dataElement.textContent);
|
||||
createToast(messageData.message, messageData.tags);
|
||||
} catch (e) {
|
||||
console.error("Error parsing message data:", e);
|
||||
}
|
||||
});
|
||||
|
||||
function createToast(messageText, messageTags) {
|
||||
let toastClass = "";
|
||||
let autohide = true;
|
||||
let delay = 5000;
|
||||
|
||||
if (messageTags.includes("debug")) {
|
||||
toastClass = "bg-secondary text-white";
|
||||
} else if (messageTags.includes("info")) {
|
||||
toastClass = "bg-info text-dark";
|
||||
} else if (messageTags.includes("success")) {
|
||||
toastClass = "bg-success text-white";
|
||||
} else if (messageTags.includes("warning")) {
|
||||
toastClass = "bg-warning text-dark";
|
||||
autohide = false;
|
||||
} else if (messageTags.includes("error")) {
|
||||
toastClass = "bg-danger text-white";
|
||||
autohide = false;
|
||||
} else {
|
||||
toastClass = "bg-light text-dark";
|
||||
}
|
||||
|
||||
const toastId =
|
||||
"toast-" + Date.now() + "-" + Math.random().toString(36).substring(2, 11);
|
||||
const toastHtml = `
|
||||
<div id="${toastId}" class="toast ${toastClass}" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto">Notification</strong>
|
||||
<small class="${toastClass.includes("text-white") ? "" : "text-muted"} me-2">Just now</small>
|
||||
<button type="button" class="btn-close ${toastClass.includes("text-white") ? "btn-close-white" : ""}" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
${messageText}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
toastContainer.insertAdjacentHTML("beforeend", toastHtml);
|
||||
|
||||
const toastElement = document.getElementById(toastId);
|
||||
if (toastElement) {
|
||||
const toast = new bootstrap.Toast(toastElement, {
|
||||
autohide: autohide,
|
||||
delay: autohide ? delay : undefined,
|
||||
});
|
||||
|
||||
toastElement.addEventListener("hidden.bs.toast", function () {
|
||||
toastElement.remove();
|
||||
});
|
||||
|
||||
toast.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
130
dashboard_project/templates/dashboard/chat_session_detail.html
Normal file
130
dashboard_project/templates/dashboard/chat_session_detail.html
Normal file
@ -0,0 +1,130 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Chat Session {{ session.session_id }} | Chat Analytics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div
|
||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||
>
|
||||
<h1 class="h2">Chat Session: {{ session.session_id }}</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<a
|
||||
href="{% url 'data_source_detail' session.data_source.id %}"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
>
|
||||
<i class="fas fa-arrow-left"></i> Back to Data Source
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Session Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Session ID:</strong> {{ session.session_id }}</p>
|
||||
<p><strong>Start Time:</strong> {{ session.start_time|date:"F d, Y H:i" }}</p>
|
||||
<p><strong>End Time:</strong> {{ session.end_time|date:"F d, Y H:i" }}</p>
|
||||
<p><strong>IP Address:</strong> {{ session.ip_address|default:"N/A" }}</p>
|
||||
<p><strong>Country:</strong> {{ session.country|default:"N/A" }}</p>
|
||||
<p><strong>Language:</strong> {{ session.language|default:"N/A" }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>Messages Sent:</strong> {{ session.messages_sent }}</p>
|
||||
<p>
|
||||
<strong>Average Response Time:</strong>
|
||||
{{ session.avg_response_time|floatformat:2 }}s
|
||||
</p>
|
||||
<p><strong>Tokens:</strong> {{ session.tokens }}</p>
|
||||
<p><strong>Token Cost:</strong> €{{ session.tokens_eur|floatformat:2 }}</p>
|
||||
<p><strong>Category:</strong> {{ session.category|default:"N/A" }}</p>
|
||||
<p>
|
||||
<strong>Sentiment:</strong>
|
||||
{% 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 %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<p class="mb-2"><strong>Initial Message:</strong></p>
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<p class="mb-0">{{ session.initial_msg|default:"N/A" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Additional Info</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
<strong>Escalated:</strong> {% if session.escalated %}
|
||||
<span class="badge bg-danger">Yes</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success">No</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Forwarded to HR:</strong> {% if session.forwarded_hr %}
|
||||
<span class="badge bg-danger">Yes</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success">No</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p><strong>User Rating:</strong> {{ session.user_rating|default:"N/A" }}</p>
|
||||
<hr />
|
||||
<p>
|
||||
<strong>Data Source:</strong>
|
||||
<a href="{% url 'data_source_detail' session.data_source.id %}"
|
||||
>{{ session.data_source.name }}</a
|
||||
>
|
||||
</p>
|
||||
<p><strong>Company:</strong> {{ session.data_source.company.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Full Transcript</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if session.full_transcript %}
|
||||
<div class="chat-transcript" style="max-height: 500px; overflow-y: auto;">
|
||||
<pre style="white-space: pre-wrap; font-family: inherit;">
|
||||
{{ session.full_transcript }}</pre
|
||||
>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center text-muted">No transcript available.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
286
dashboard_project/templates/dashboard/dashboard.html
Normal file
286
dashboard_project/templates/dashboard/dashboard.html
Normal file
@ -0,0 +1,286 @@
|
||||
<!-- templates/dashboard/dashboard.html -->
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Dashboard | Chat Analytics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div
|
||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||
>
|
||||
<h1 class="h2">{{ selected_dashboard.name }}</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<a
|
||||
href="{% url 'edit_dashboard' selected_dashboard.id %}"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
>
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</a>
|
||||
<a
|
||||
href="{% url 'delete_dashboard' selected_dashboard.id %}"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
>
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</a>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button
|
||||
class="btn btn-sm btn-outline-primary dropdown-toggle"
|
||||
type="button"
|
||||
id="timeRangeDropdown"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i class="fas fa-calendar"></i> Time Range
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="timeRangeDropdown">
|
||||
<li>
|
||||
<a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=7"
|
||||
>Last 7 days</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=30"
|
||||
>Last 30 days</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=90"
|
||||
>Last 90 days</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=all"
|
||||
>All time</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<div class="card stats-card bg-primary text-white">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">Total Sessions</h6>
|
||||
<h3>{{ dashboard_data.total_sessions }}</h3>
|
||||
<p>Chat conversations</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stats-card bg-success text-white">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">Avg Response Time</h6>
|
||||
<h3>{{ dashboard_data.avg_response_time }}s</h3>
|
||||
<p>Average response</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stats-card bg-info text-white">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">Total Tokens</h6>
|
||||
<h3>{{ dashboard_data.total_tokens }}</h3>
|
||||
<p>Total usage</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stats-card bg-warning text-white">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">Total Cost</h6>
|
||||
<h3>€{{ dashboard_data.total_cost }}</h3>
|
||||
<p>Token cost</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Sessions Over Time</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="sessions-time-chart" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Sentiment Analysis</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="sentiment-chart" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Top Countries</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="country-chart" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Categories</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="category-chart" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Parse the dashboard data from JSON
|
||||
const dashboardData = JSON.parse("{{ dashboard_data_json|safe }}");
|
||||
|
||||
// Sessions over time chart
|
||||
const timeSeriesData = dashboardData.time_series_data;
|
||||
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
||||
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
||||
|
||||
Plotly.newPlot(
|
||||
"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,
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
margin: { t: 10, r: 10, b: 40, l: 40 },
|
||||
xaxis: {
|
||||
title: "Date",
|
||||
},
|
||||
yaxis: {
|
||||
title: "Number of Sessions",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Sentiment analysis chart
|
||||
const sentimentData = dashboardData.sentiment_data;
|
||||
|
||||
if (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)";
|
||||
});
|
||||
|
||||
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>
|
||||
{% endblock %}
|
||||
@ -0,0 +1,43 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Delete Dashboard | Chat Analytics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div
|
||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||
>
|
||||
<h1 class="h2">Delete Dashboard</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h5 class="card-title mb-0">Confirm Deletion</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="lead">
|
||||
Are you sure you want to delete the dashboard "<strong>{{ dashboard.name }}</strong>"?
|
||||
</p>
|
||||
<p>
|
||||
This action cannot be undone. The dashboard will be permanently deleted, but the
|
||||
underlying data sources will remain intact.
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{% url 'dashboard' %}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-danger">Delete Dashboard</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
43
dashboard_project/templates/dashboard/dashboard_form.html
Normal file
43
dashboard_project/templates/dashboard/dashboard_form.html
Normal file
@ -0,0 +1,43 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}
|
||||
{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}
|
||||
| Chat Analytics
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div
|
||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||
>
|
||||
<h1 class="h2">{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% if is_create %}Create Dashboard{% else %}Update Dashboard{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -0,0 +1,50 @@
|
||||
<!-- templates/dashboard/data_source_confirm_delete.html -->
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Delete Data Source | Chat Analytics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div
|
||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||
>
|
||||
<h1 class="h2">Delete Data Source</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<a
|
||||
href="{% url 'data_source_detail' data_source.id %}"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
>
|
||||
<i class="fas fa-arrow-left"></i> Back to Data Source
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h5 class="card-title mb-0">Confirm Deletion</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="lead">
|
||||
Are you sure you want to delete the data source
|
||||
"<strong>{{ data_source.name }}</strong>"?
|
||||
</p>
|
||||
<p>
|
||||
This action cannot be undone. The data source and all associated chat sessions
|
||||
({{ data_source.chat_sessions.count }} sessions) will be permanently deleted.
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{% url 'data_source_detail' data_source.id %}" class="btn btn-secondary"
|
||||
>Cancel</a
|
||||
>
|
||||
<button type="submit" class="btn btn-danger">Delete Data Source</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
216
dashboard_project/templates/dashboard/data_source_detail.html
Normal file
216
dashboard_project/templates/dashboard/data_source_detail.html
Normal file
@ -0,0 +1,216 @@
|
||||
<!-- templates/dashboard/data_source_detail.html -->
|
||||
{% extends 'base.html' %}
|
||||
{% load dashboard_extras %}
|
||||
|
||||
{% block title %}{{ data_source.name }} | Chat Analytics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div
|
||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||
>
|
||||
<h1 class="h2">{{ data_source.name }}</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<a href="{% url 'upload_data' %}" class="btn btn-sm btn-outline-secondary me-2">
|
||||
<i class="fas fa-arrow-left"></i> Back to Data Sources
|
||||
</a>
|
||||
<a href="{% url 'delete_data_source' data_source.id %}" class="btn btn-sm btn-outline-danger">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Data Source Details</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Name:</strong> {{ data_source.name }}</p>
|
||||
<p><strong>Uploaded At:</strong> {{ data_source.uploaded_at|date:"F d, Y H:i" }}</p>
|
||||
<p><strong>File:</strong> {{ data_source.file.name|split:"/"|last }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>Company:</strong> {{ data_source.company.name }}</p>
|
||||
<p><strong>Total Sessions:</strong> {{ page_obj.paginator.count }}</p>
|
||||
<p><strong>Description:</strong> {{ data_source.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Filter Sessions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="get" action="{% url 'search_chat_sessions' %}">
|
||||
<div class="input-group mb-3">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
class="form-control"
|
||||
placeholder="Search sessions..."
|
||||
aria-label="Search sessions"
|
||||
/>
|
||||
<input type="hidden" name="data_source_id" value="{{ data_source.id }}" />
|
||||
<button class="btn btn-outline-primary" type="submit">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Chat Sessions ({{ page_obj.paginator.count }})</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Session ID</th>
|
||||
<th>Start Time</th>
|
||||
<th>Country</th>
|
||||
<th>Language</th>
|
||||
<th>Sentiment</th>
|
||||
<th>Messages</th>
|
||||
<th>Tokens</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 }}</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.tokens }}</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>
|
||||
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<nav aria-label="Page navigation" class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1" aria-label="First">
|
||||
<span aria-hidden="true">««</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a
|
||||
class="page-link"
|
||||
href="?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="?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="?page={{ num }}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a
|
||||
class="page-link"
|
||||
href="?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="?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>
|
||||
{% endblock %}
|
||||
316
dashboard_project/templates/dashboard/data_view.html
Normal file
316
dashboard_project/templates/dashboard/data_view.html
Normal file
@ -0,0 +1,316 @@
|
||||
<!-- templates/dashboard/data_view.html -->
|
||||
{% extends 'base.html' %}
|
||||
{% load dashboard_extras %}
|
||||
|
||||
{% block title %}Data View | Chat Analytics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div
|
||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||
>
|
||||
<h1 class="h2">Data View</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||
</a>
|
||||
{% if selected_data_source %}
|
||||
<a
|
||||
href="{% url 'data_source_detail' selected_data_source.id %}"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
>
|
||||
<i class="fas fa-database"></i> View Source
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button
|
||||
class="btn btn-sm btn-outline-primary dropdown-toggle"
|
||||
type="button"
|
||||
id="dataViewDropdown"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i class="fas fa-filter"></i> Filter
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="dataViewDropdown">
|
||||
<li><a class="dropdown-item" href="?view=all">All Sessions</a></li>
|
||||
<li><a class="dropdown-item" href="?view=recent">Recent Sessions</a></li>
|
||||
<li><a class="dropdown-item" href="?view=positive">Positive Sentiment</a></li>
|
||||
<li><a class="dropdown-item" href="?view=negative">Negative Sentiment</a></li>
|
||||
<li><a class="dropdown-item" href="?view=escalated">Escalated Sessions</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Source Selection -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Data Source Selection</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="get" class="row g-3 align-items-center">
|
||||
<div class="col-md-6">
|
||||
<select name="data_source_id" class="form-select" aria-label="Select Data Source">
|
||||
<option value="">All Data Sources</option>
|
||||
{% for ds in data_sources %}
|
||||
<option
|
||||
value="{{ ds.id }}"
|
||||
{% if selected_data_source.id == ds.id %}selected{% endif %}
|
||||
>
|
||||
{{ ds.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<select name="view" class="form-select" aria-label="Select View">
|
||||
<option value="all" {% if view == 'all' %}selected{% endif %}>All Sessions</option>
|
||||
<option value="recent" {% if view == 'recent' %}selected{% endif %}>
|
||||
Recent Sessions
|
||||
</option>
|
||||
<option value="positive" {% if view == 'positive' %}selected{% endif %}>
|
||||
Positive Sentiment
|
||||
</option>
|
||||
<option value="negative" {% if view == 'negative' %}selected{% endif %}>
|
||||
Negative Sentiment
|
||||
</option>
|
||||
<option value="escalated" {% if view == 'escalated' %}selected{% endif %}>
|
||||
Escalated Sessions
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-primary w-100">Apply</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
Chat Sessions
|
||||
{% if selected_data_source %}
|
||||
for {{ selected_data_source.name }}
|
||||
{% endif %}
|
||||
{% if view != 'all' %}
|
||||
({{ view|title }})
|
||||
{% endif %}
|
||||
</h5>
|
||||
<span class="badge bg-primary">{{ page_obj.paginator.count }} sessions</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Session ID</th>
|
||||
<th>Start Time</th>
|
||||
<th>Country</th>
|
||||
<th>Language</th>
|
||||
<th>Messages</th>
|
||||
<th>Sentiment</th>
|
||||
<th>Response Time</th>
|
||||
<th>Category</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for session in page_obj %}
|
||||
<tr>
|
||||
<td>{{ session.session_id|truncatechars:10 }}</td>
|
||||
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
|
||||
<td>{{ session.country|default:"N/A" }}</td>
|
||||
<td>{{ session.language|default:"N/A" }}</td>
|
||||
<td>{{ session.messages_sent }}</td>
|
||||
<td>
|
||||
{% if session.sentiment %}
|
||||
{% if 'positive' in session.sentiment|lower %}
|
||||
<span class="badge bg-success">{{ session.sentiment }}</span>
|
||||
{% elif 'negative' in session.sentiment|lower %}
|
||||
<span class="badge bg-danger">{{ session.sentiment }}</span>
|
||||
{% elif 'neutral' in session.sentiment|lower %}
|
||||
<span class="badge bg-warning">{{ session.sentiment }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ session.sentiment }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ session.avg_response_time|floatformat:2 }}s</td>
|
||||
<td>{{ session.category|default:"N/A" }}</td>
|
||||
<td>
|
||||
<a
|
||||
href="{% url 'chat_session_detail' session.session_id %}"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
>
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="9" class="text-center">No chat sessions found.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<nav aria-label="Page navigation" class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a
|
||||
class="page-link"
|
||||
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page=1"
|
||||
aria-label="First"
|
||||
>
|
||||
<span aria-hidden="true">««</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a
|
||||
class="page-link"
|
||||
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.previous_page_number }}"
|
||||
aria-label="Previous"
|
||||
>
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="First">
|
||||
<span aria-hidden="true">««</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<a
|
||||
class="page-link"
|
||||
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
|
||||
>{{ num }}</a
|
||||
>
|
||||
</li>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<li class="page-item">
|
||||
<a
|
||||
class="page-link"
|
||||
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
|
||||
>{{ num }}</a
|
||||
>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a
|
||||
class="page-link"
|
||||
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.next_page_number }}"
|
||||
aria-label="Next"
|
||||
>
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a
|
||||
class="page-link"
|
||||
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
|
||||
aria-label="Last"
|
||||
>
|
||||
<span aria-hidden="true">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Last">
|
||||
<span aria-hidden="true">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Summary -->
|
||||
{% if page_obj %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Summary</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="card stats-card bg-light">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">Total Sessions</h6>
|
||||
<h3>{{ page_obj.paginator.count }}</h3>
|
||||
<p>Chat conversations</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stats-card bg-light">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">Avg Response Time</h6>
|
||||
<h3>{{ avg_response_time|floatformat:2 }}s</h3>
|
||||
<p>Average response</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stats-card bg-light">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">Avg Messages</h6>
|
||||
<h3>{{ avg_messages|floatformat:1 }}</h3>
|
||||
<p>Per conversation</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stats-card bg-light">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">Escalation Rate</h6>
|
||||
<h3>{{ escalation_rate|floatformat:1 }}%</h3>
|
||||
<p>Escalated sessions</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
40
dashboard_project/templates/dashboard/no_company.html
Normal file
40
dashboard_project/templates/dashboard/no_company.html
Normal file
@ -0,0 +1,40 @@
|
||||
<!-- templates/dashboard/no_company.html -->
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}No Company | Chat Analytics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div
|
||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||
>
|
||||
<h1 class="h2">No Company Association</h1>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header bg-warning text-dark">
|
||||
<h5 class="card-title mb-0">Account Not Associated with a Company</h5>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-4">
|
||||
<i class="fas fa-building fa-4x text-warning mb-3"></i>
|
||||
<h4>You are not currently associated with any company</h4>
|
||||
<p class="lead">You need to be associated with a company to access the dashboard.</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Please contact an administrator to have your account assigned to a company. Once your
|
||||
account is associated with a company, you'll be able to access the dashboard and its
|
||||
features.
|
||||
</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{% url 'profile' %}" class="btn btn-primary">View Your Profile</a>
|
||||
<a href="{% url 'logout' %}" class="btn btn-outline-secondary ms-2">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
222
dashboard_project/templates/dashboard/search_results.html
Normal file
222
dashboard_project/templates/dashboard/search_results.html
Normal file
@ -0,0 +1,222 @@
|
||||
<!-- templates/dashboard/search_results.html -->
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Search Results | Chat Analytics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div
|
||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||
>
|
||||
<h1 class="h2">Search Results</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Search Chat Sessions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="get" action="{% url 'search_chat_sessions' %}">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
class="form-control"
|
||||
placeholder="Search sessions..."
|
||||
value="{{ query }}"
|
||||
aria-label="Search sessions"
|
||||
/>
|
||||
{% if data_source %}
|
||||
<input type="hidden" name="data_source_id" value="{{ data_source.id }}" />
|
||||
{% endif %}
|
||||
<button class="btn btn-outline-primary" type="submit">
|
||||
<i class="fas fa-search"></i> Search
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 text-muted">
|
||||
<small
|
||||
>Search by session ID, country, language, sentiment, category, or message
|
||||
content.</small
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
Results {% if query %}for "{{ query }}"{% endif %}
|
||||
{% if data_source %}in {{ data_source.name }}{% endif %}
|
||||
({{ page_obj.paginator.count }})
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Session ID</th>
|
||||
<th>Start Time</th>
|
||||
<th>Data Source</th>
|
||||
<th>Country</th>
|
||||
<th>Language</th>
|
||||
<th>Sentiment</th>
|
||||
<th>Messages</th>
|
||||
<th>Category</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for session in page_obj %}
|
||||
<tr>
|
||||
<td>{{ session.session_id|truncatechars:10 }}</td>
|
||||
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
|
||||
<td>
|
||||
<a href="{% url 'data_source_detail' session.data_source.id %}"
|
||||
>{{ session.data_source.name|truncatechars:15 }}</a
|
||||
>
|
||||
</td>
|
||||
<td>{{ session.country }}</td>
|
||||
<td>{{ session.language }}</td>
|
||||
<td>
|
||||
{% if session.sentiment %}
|
||||
{% if 'positive' in session.sentiment|lower %}
|
||||
<span class="badge bg-success">{{ session.sentiment }}</span>
|
||||
{% elif 'negative' in session.sentiment|lower %}
|
||||
<span class="badge bg-danger">{{ session.sentiment }}</span>
|
||||
{% elif 'neutral' in session.sentiment|lower %}
|
||||
<span class="badge bg-warning">{{ session.sentiment }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ session.sentiment }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ session.messages_sent }}</td>
|
||||
<td>{{ session.category|default:"N/A" }}</td>
|
||||
<td>
|
||||
<a
|
||||
href="{% url 'chat_session_detail' session.session_id %}"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
>
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="9" class="text-center">
|
||||
No chat sessions found matching your criteria.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<nav aria-label="Page navigation" class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a
|
||||
class="page-link"
|
||||
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page=1"
|
||||
aria-label="First"
|
||||
>
|
||||
<span aria-hidden="true">««</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a
|
||||
class="page-link"
|
||||
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.previous_page_number }}"
|
||||
aria-label="Previous"
|
||||
>
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="First">
|
||||
<span aria-hidden="true">««</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<a
|
||||
class="page-link"
|
||||
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
|
||||
>{{ num }}</a
|
||||
>
|
||||
</li>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<li class="page-item">
|
||||
<a
|
||||
class="page-link"
|
||||
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
|
||||
>{{ num }}</a
|
||||
>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a
|
||||
class="page-link"
|
||||
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.next_page_number }}"
|
||||
aria-label="Next"
|
||||
>
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a
|
||||
class="page-link"
|
||||
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
|
||||
aria-label="Last"
|
||||
>
|
||||
<span aria-hidden="true">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Last">
|
||||
<span aria-hidden="true">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
197
dashboard_project/templates/dashboard/upload.html
Normal file
197
dashboard_project/templates/dashboard/upload.html
Normal file
@ -0,0 +1,197 @@
|
||||
<!-- templates/dashboard/upload.html -->
|
||||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load dashboard_extras %}
|
||||
|
||||
{% block title %}Upload Data | Chat Analytics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div
|
||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||
>
|
||||
<h1 class="h2">Upload Data</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Upload CSV File</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %} {{ form|crispy }}
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">CSV File Format</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>The CSV file should contain the following columns:</p>
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Column</th>
|
||||
<th>Description</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>session_id</td>
|
||||
<td>Unique identifier for the chat session</td>
|
||||
<td>String</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>start_time</td>
|
||||
<td>When the session started</td>
|
||||
<td>Datetime</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>end_time</td>
|
||||
<td>When the session ended</td>
|
||||
<td>Datetime</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ip_address</td>
|
||||
<td>IP address of the user</td>
|
||||
<td>String</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>country</td>
|
||||
<td>Country of the user</td>
|
||||
<td>String</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>language</td>
|
||||
<td>Language used in the conversation</td>
|
||||
<td>String</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>messages_sent</td>
|
||||
<td>Number of messages in the conversation</td>
|
||||
<td>Integer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>sentiment</td>
|
||||
<td>Sentiment analysis of the conversation</td>
|
||||
<td>String</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>escalated</td>
|
||||
<td>Whether the conversation was escalated</td>
|
||||
<td>Boolean</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>forwarded_hr</td>
|
||||
<td>Whether the conversation was forwarded to HR</td>
|
||||
<td>Boolean</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>full_transcript</td>
|
||||
<td>Full transcript of the conversation</td>
|
||||
<td>Text</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>avg_response_time</td>
|
||||
<td>Average response time in seconds</td>
|
||||
<td>Float</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>tokens</td>
|
||||
<td>Total number of tokens used</td>
|
||||
<td>Integer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>tokens_eur</td>
|
||||
<td>Cost of tokens in EUR</td>
|
||||
<td>Float</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>category</td>
|
||||
<td>Category of the conversation</td>
|
||||
<td>String</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>initial_msg</td>
|
||||
<td>First message from the user</td>
|
||||
<td>Text</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>user_rating</td>
|
||||
<td>User rating of the conversation</td>
|
||||
<td>String</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if data_sources %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Uploaded Data Sources</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Uploaded</th>
|
||||
<th>File</th>
|
||||
<th>Sessions</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for data_source in data_sources %}
|
||||
<tr>
|
||||
<td>{{ data_source.name }}</td>
|
||||
<td>{{ data_source.description|truncatechars:50 }}</td>
|
||||
<td>{{ data_source.uploaded_at|date:"M d, Y H:i" }}</td>
|
||||
<td>{{ data_source.file.name|split:"/"|last }}</td>
|
||||
<td>{{ data_source.chat_sessions.count }}</td>
|
||||
<td>
|
||||
<a
|
||||
href="{% url 'data_source_detail' data_source.id %}"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
>
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a
|
||||
href="{% url 'delete_data_source' data_source.id %}"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
16
dashboard_project/wsgi.py
Normal file
16
dashboard_project/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for dashboard_project project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
Reference in New Issue
Block a user