Refactor HTML templates for improved readability and consistency

- Updated search_results_table.html to enhance formatting and maintain consistent indentation.
- Refined search_results.html layout for better structure and clarity.
- Improved upload.html for better organization and readability of the upload form and data source table.
- Removed unnecessary lines in package.json and streamlined devDependencies section.
This commit is contained in:
2025-05-17 21:45:50 +02:00
parent 6b19cbcb51
commit e8f2d2adc2
35 changed files with 3406 additions and 3588 deletions

View File

@ -18,8 +18,8 @@ indent_size = 4
# HTML and Django/Jinja2 template files # HTML and Django/Jinja2 template files
[*.{html,htm}] [*.{html,htm}]
indent_style = tab indent_size = 2
indent_size = 4
# Allow prettier to format Django/Jinja templates properly # Allow prettier to format Django/Jinja templates properly
# The following comment options can be used in individual files if needed: # The following comment options can be used in individual files if needed:
# <!-- prettier-ignore --> # <!-- prettier-ignore -->

1
.gitignore vendored
View File

@ -407,6 +407,7 @@ pyrightconfig.json
*Zone.Identifier *Zone.Identifier
examples/ examples/
**/migrations/[0-9]**.py **/migrations/[0-9]**.py
package-lock.json
# UV specific # UV specific
.uv/ .uv/

View File

@ -3,6 +3,7 @@ default_install_hook_types:
- post-checkout - post-checkout
- post-merge - post-merge
- post-rewrite - post-rewrite
repos: repos:
# uv hooks for dependency management # uv hooks for dependency management
- repo: https://github.com/astral-sh/uv-pre-commit - repo: https://github.com/astral-sh/uv-pre-commit
@ -33,18 +34,18 @@ repos:
# rev: 3.0.7 # rev: 3.0.7
# hooks: # hooks:
# - id: djhtml # - id: djhtml
# entry: djhtml --tabwidth 4 -- # entry: djhtml --tabwidth 4
# - id: djcss
# - id: djjs
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8 rev: v3.1.0
hooks: hooks:
- id: prettier - id: prettier
types_or: [javascript, jsx, ts, tsx, css, scss, html, json, yaml, markdown] types_or: [javascript, jsx, ts, tsx, css, scss, html, json, yaml, markdown]
additional_dependencies: additional_dependencies:
- prettier - prettier
- prettier-plugin-jinja-template - prettier-plugin-jinja-template
# types_or: [javascript, jsx, ts, tsx, css, scss, json, yaml, markdown]
# exclude: '.*\.html$'
# Ruff for linting and formatting # Ruff for linting and formatting
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit

View File

@ -47,3 +47,6 @@ docker-compose.override.yml
.vscode/ .vscode/
*.swp *.swp
*.swo *.swo
# Ignore all SQLite3 files:
**/*.sqlite3

View File

@ -11,7 +11,7 @@
"requirePragma": false, "requirePragma": false,
"semi": true, "semi": true,
"singleQuote": false, "singleQuote": false,
"useTabs": true, "useTabs": false,
"overrides": [ "overrides": [
{ {
"files": ["*.html"], "files": ["*.html"],

90
.vscode/settings.json vendored
View File

@ -1,47 +1,47 @@
{ {
"[css]": { "[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true "editor.formatOnSave": true
}, },
"[html]": { "[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true "editor.formatOnSave": true
}, },
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true "editor.formatOnSave": true
}, },
"[python]": { "[python]": {
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": "explicit", "source.fixAll": "explicit",
"source.organizeImports": "explicit" "source.organizeImports": "explicit"
}, },
"editor.defaultFormatter": "charliermarsh.ruff", "editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true "editor.formatOnSave": true
}, },
"[toml]": { "[toml]": {
"editor.defaultFormatter": "tamasfe.even-better-toml" "editor.defaultFormatter": "tamasfe.even-better-toml"
}, },
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"emmet.includeLanguages": { "emmet.includeLanguages": {
"django-html": "html", "django-html": "html",
"jinja-html": "html" "jinja-html": "html"
}, },
"emmet.syntaxProfiles": { "emmet.syntaxProfiles": {
"html": { "html": {
"inline_break": 2 "inline_break": 2
} }
}, },
"files.associations": { "files.associations": {
"*.html": "html" "*.html": "html"
}, },
"html.format.wrapAttributes": "auto", "html.format.wrapAttributes": "auto",
"html.format.wrapLineLength": 100, "html.format.wrapLineLength": 100,
"notebook.codeActionsOnSave": { "notebook.codeActionsOnSave": {
"notebook.source.fixAll": "explicit", "notebook.source.fixAll": "explicit",
"notebook.source.organizeImports": "explicit" "notebook.source.organizeImports": "explicit"
}, },
"notebook.formatOnSave.enabled": true, "notebook.formatOnSave.enabled": true,
"prettier.requireConfig": true "prettier.requireConfig": true
} }

View File

@ -1,4 +1,4 @@
.PHONY: venv install install-dev lint test format clean run migrate makemigrations superuser setup-node format-js .PHONY: venv install install-dev lint test format clean run migrate makemigrations superuser setup-node
# Create a virtual environment # Create a virtual environment
venv: venv:
@ -25,13 +25,9 @@ format:
uv run -m ruff format dashboard_project uv run -m ruff format dashboard_project
uv run -m black dashboard_project uv run -m black dashboard_project
# Format JavaScript/CSS/HTML files with Prettier
format-js:
npx run format
# Setup Node.js dependencies # Setup Node.js dependencies
setup-node: setup-node:
npm install npm install --include=dev
# Clean Python cache files # Clean Python cache files
clean: clean:
@ -45,6 +41,9 @@ clean:
find . -type d -name ".coverage" -exec rm -rf {} + find . -type d -name ".coverage" -exec rm -rf {} +
find . -type d -name "htmlcov" -exec rm -rf {} + find . -type d -name "htmlcov" -exec rm -rf {} +
find . -type d -name ".ruff_cache" -exec rm -rf {} + find . -type d -name ".ruff_cache" -exec rm -rf {} +
find . -type d -name ".mypy_cache" -exec rm -rf {} +
find . -type d -name ".tox" -exec rm -rf {} +
find . -type d -name "node_modules" -exec rm -rf {} +
rm -rf build/ rm -rf build/
rm -rf dist/ rm -rf dist/

View File

@ -64,12 +64,12 @@ For VSCode users, install the Prettier extension and add these settings to your
```json ```json
{ {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"[html]": { "[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true "editor.formatOnSave": true
}, },
"prettier.requireConfig": true "prettier.requireConfig": true
} }
``` ```

86
TODO.md
View File

@ -1,31 +1,57 @@
# TODO List # LiveGraphs Project TODO
- When I zoom into the dasboard page, the graphs don't scale/adjust to fit the window until I completely refresh the page, can we solve that? ## Dashboard UI Improvements
- Add export functionality to the dashboard:
- File formats: ### Responsiveness
- CSV
- Excel - [ ] Fix dashboard graphs scaling/adjustment when zooming (currently requires page refresh)
- JSON
- XML ### Theming
- HTML
- PDF - [ ] Add dark mode/light mode toggle
- Make the export button a dropdown with the following options: - [ ] Add Notso AI branding elements
- Export as CSV - [ ] Implement responsive table design (reduce rows to fit screen)
- Export as Excel
- Export as JSON ### Data Export
- Export as XML
- Export as HTML - [ ] Implement multi-format export functionality
- Export as PDF - [ ] CSV format
- Make the export data section folded by default and only show the export button. - [ ] Excel format
- Adjust the downloaded file name to include the company name, date and time of the export. - [ ] JSON format
- Add a button to download the CSV file for the selected company. - [ ] XML format
- Make it possible to modify the column names in the CSV file through the admin interface. - [ ] HTML format
- Add possibility to add a company logo in the admin interface. - [ ] PDF format
- Add periodic download from <https://proto.notso.ai/XY/chats> possibility for the XY company. - [ ] Create dropdown menu for export options
- Authentication: Basic Auth - [ ] Make export data section collapsible (folded by default)
- URL: <https://proto.notso.ai/XY/chats> - [ ] Add company name, date and timestamp to exported filenames
- Username: xxxx
- Password: xxxx ## Admin Interface Enhancements
- Reduce amount of rows in the table to fit the screen.
- Add dark mode/theming to the dashboard. ### Company Management
- Add Notso AI branding to the dashboard.
- [ ] Add company logo upload functionality
- [ ] Add direct CSV download button for each company (superusers only)
- [ ] Include company name, date and timestamp in filename
- [ ] Add UI for customizing CSV column names
## Data Integration
### External Data Sources
- [ ] Implement periodic data download from external API
- [ ] Source: <https://proto.notso.ai/XY/chats>
- [ ] Authentication: Basic Auth
- [ ] Credentials: [stored securely]
- [ ] Add scheduling options for data refresh
## Technical Debt
### Performance Optimization
- [ ] Profile and optimize dashboard rendering
- [ ] Implement lazy loading for dashboard elements
### Testing
- [ ] Add unit tests for export functionality
- [ ] Add integration tests for data import process

View File

@ -91,8 +91,8 @@ AUTH_PASSWORD_VALIDATORS = [
] ]
# Internationalization # Internationalization
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "nl"
TIME_ZONE = "UTC" TIME_ZONE = "Europe/Amsterdam"
USE_I18N = True USE_I18N = True
USE_TZ = True USE_TZ = True

View File

@ -4,311 +4,311 @@
/* Dashboard grid layout */ /* Dashboard grid layout */
.dashboard-grid { .dashboard-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
/* Slightly larger minmax for widgets */ /* Slightly larger minmax for widgets */
gap: 1.5rem; gap: 1.5rem;
/* Increased gap */ /* Increased gap */
} }
/* Dashboard widget cards */ /* Dashboard widget cards */
.dashboard-widget { .dashboard-widget {
display: flex; display: flex;
/* Allow flex for content alignment */ /* Allow flex for content alignment */
flex-direction: column; flex-direction: column;
/* Stack header, body, footer vertically */ /* Stack header, body, footer vertically */
height: 100%; height: 100%;
/* Ensure widgets fill grid cell height */ /* Ensure widgets fill grid cell height */
} }
.dashboard-widget .card-header { .dashboard-widget .card-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.dashboard-widget .card-header .widget-title { .dashboard-widget .card-header .widget-title {
font-size: 1.1rem; font-size: 1.1rem;
/* Slightly larger widget titles */ /* Slightly larger widget titles */
font-weight: 600; font-weight: 600;
} }
.dashboard-widget .card-header .widget-actions { .dashboard-widget .card-header .widget-actions {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
} }
.dashboard-widget .card-header .widget-actions .btn { .dashboard-widget .card-header .widget-actions .btn {
width: 32px; width: 32px;
/* Slightly larger action buttons */ /* Slightly larger action buttons */
height: 32px; height: 32px;
padding: 0; padding: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 0.85rem; font-size: 0.85rem;
background-color: transparent; background-color: transparent;
border: 1px solid transparent; border: 1px solid transparent;
color: #6c757d; color: #6c757d;
} }
.dashboard-widget .card-header .widget-actions .btn:hover { .dashboard-widget .card-header .widget-actions .btn:hover {
background-color: #f0f0f0; background-color: #f0f0f0;
border-color: #e0e0e0; border-color: #e0e0e0;
color: #333; color: #333;
} }
.dashboard-widget .card-body { .dashboard-widget .card-body {
flex-grow: 1; flex-grow: 1;
/* Allow card body to take available space */ /* Allow card body to take available space */
padding: 1.25rem; padding: 1.25rem;
/* Consistent padding */ /* Consistent padding */
} }
/* Chart widgets */ /* Chart widgets */
.chart-widget .card-body { .chart-widget .card-body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.chart-widget .chart-container { .chart-widget .chart-container {
flex: 1; flex: 1;
min-height: 250px; min-height: 250px;
/* Adjusted min-height */ /* Adjusted min-height */
width: 100%; width: 100%;
/* Ensure it takes full width of card body */ /* Ensure it takes full width of card body */
} }
/* Stat widgets / Stat Cards */ /* Stat widgets / Stat Cards */
.stat-card { .stat-card {
text-align: center; text-align: center;
padding: 1.5rem; padding: 1.5rem;
/* Generous padding */ /* Generous padding */
} }
.stat-card .stat-icon { .stat-card .stat-icon {
font-size: 2.25rem; font-size: 2.25rem;
/* Larger icon */ /* Larger icon */
margin-bottom: 1rem; margin-bottom: 1rem;
display: inline-block; display: inline-block;
width: 4.5rem; width: 4.5rem;
height: 4.5rem; height: 4.5rem;
line-height: 4.5rem; line-height: 4.5rem;
text-align: center; text-align: center;
border-radius: 50%; border-radius: 50%;
background-color: #e9f2ff; background-color: #e9f2ff;
/* Light blue background for icon */ /* Light blue background for icon */
color: #007bff; color: #007bff;
/* Primary color for icon */ /* Primary color for icon */
} }
.stat-card .stat-value { .stat-card .stat-value {
font-size: 2.25rem; font-size: 2.25rem;
/* Larger stat value */ /* Larger stat value */
font-weight: 700; font-weight: 700;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
/* Reduced margin */ /* Reduced margin */
line-height: 1.1; line-height: 1.1;
color: #212529; color: #212529;
/* Darker color for value */ /* Darker color for value */
} }
.stat-card .stat-label { .stat-card .stat-label {
font-size: 0.9rem; font-size: 0.9rem;
/* Slightly larger label */ /* Slightly larger label */
color: #6c757d; color: #6c757d;
margin-bottom: 0; margin-bottom: 0;
} }
/* Dashboard theme variations */ /* Dashboard theme variations */
.dashboard-theme-light .card { .dashboard-theme-light .card {
background-color: #fff; background-color: #fff;
} }
.dashboard-theme-dark { .dashboard-theme-dark {
background-color: #212529; background-color: #212529;
color: #f8f9fa; color: #f8f9fa;
} }
.dashboard-theme-dark .card { .dashboard-theme-dark .card {
background-color: #343a40; background-color: #343a40;
color: #f8f9fa; color: #f8f9fa;
border-color: #495057; border-color: #495057;
} }
.dashboard-theme-dark .card-header { .dashboard-theme-dark .card-header {
background-color: #495057; background-color: #495057;
border-bottom-color: #6c757d; border-bottom-color: #6c757d;
} }
.dashboard-theme-dark .stat-card .stat-label { .dashboard-theme-dark .stat-card .stat-label {
color: #adb5bd; color: #adb5bd;
} }
/* Time period selector */ /* Time period selector */
.time-period-selector { .time-period-selector {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
/* Increased gap */ /* Increased gap */
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
/* Increased margin */ /* Increased margin */
} }
.time-period-selector .btn-group { .time-period-selector .btn-group {
flex-wrap: wrap; flex-wrap: wrap;
} }
.time-period-selector .btn { .time-period-selector .btn {
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
/* Bootstrap-like padding */ /* Bootstrap-like padding */
font-size: 0.875rem; font-size: 0.875rem;
} }
/* Custom metric selector */ /* Custom metric selector */
.metric-selector { .metric-selector {
max-width: 100%; max-width: 100%;
overflow-x: auto; overflow-x: auto;
white-space: nowrap; white-space: nowrap;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.metric-selector .nav-link { .metric-selector .nav-link {
white-space: nowrap; white-space: nowrap;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
font-weight: 500; font-weight: 500;
} }
.metric-selector .nav-link.active { .metric-selector .nav-link.active {
background-color: #007bff; background-color: #007bff;
color: white; color: white;
border-radius: 0.25rem; border-radius: 0.25rem;
} }
/* Dashboard loading states */ /* Dashboard loading states */
.widget-placeholder { .widget-placeholder {
min-height: 300px; min-height: 300px;
background: linear-gradient(90deg, #e9ecef 25%, #f8f9fa 50%, #e9ecef 75%); background: linear-gradient(90deg, #e9ecef 25%, #f8f9fa 50%, #e9ecef 75%);
/* Lighter gradient */ /* Lighter gradient */
background-size: 200% 100%; background-size: 200% 100%;
animation: loading 1.8s infinite ease-in-out; animation: loading 1.8s infinite ease-in-out;
/* Smoother animation */ /* Smoother animation */
border-radius: 0.5rem; border-radius: 0.5rem;
/* Consistent with cards */ /* Consistent with cards */
} }
@keyframes loading { @keyframes loading {
0% { 0% {
background-position: 200% 0; background-position: 200% 0;
} }
100% { 100% {
background-position: -200% 0; background-position: -200% 0;
} }
} }
/* Dashboard empty states */ /* Dashboard empty states */
.empty-state { .empty-state {
padding: 2.5rem; padding: 2.5rem;
/* Increased padding */ /* Increased padding */
text-align: center; text-align: center;
color: #6c757d; color: #6c757d;
background-color: #f8f9fa; background-color: #f8f9fa;
/* Light background for empty state */ /* Light background for empty state */
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px dashed #ced4da; border: 1px dashed #ced4da;
/* Dashed border */ /* Dashed border */
} }
.empty-state .empty-state-icon { .empty-state .empty-state-icon {
font-size: 3.5rem; font-size: 3.5rem;
/* Larger icon */ /* Larger icon */
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
opacity: 0.4; opacity: 0.4;
} }
.empty-state .empty-state-message { .empty-state .empty-state-message {
font-size: 1.2rem; font-size: 1.2rem;
/* Slightly larger message */ /* Slightly larger message */
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
font-weight: 500; font-weight: 500;
} }
.empty-state .btn { .empty-state .btn {
margin-top: 1rem; margin-top: 1rem;
} }
/* Responsive adjustments */ /* Responsive adjustments */
@media (width <=767.98px) { @media (width <=767.98px) {
.dashboard-grid { .dashboard-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.stat-card { .stat-card {
padding: 1rem; padding: 1rem;
} }
.stat-card .stat-icon { .stat-card .stat-icon {
font-size: 1.5rem; font-size: 1.5rem;
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
line-height: 3rem; line-height: 3rem;
} }
.stat-card .stat-value { .stat-card .stat-value {
font-size: 1.5rem; font-size: 1.5rem;
} }
} }
/* --- Stat Boxes Alignment Fix (Bottom Align, No Overlap) --- */ /* --- Stat Boxes Alignment Fix (Bottom Align, No Overlap) --- */
.stats-row { .stats-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 1.5rem; gap: 1.5rem;
align-items: stretch; align-items: stretch;
} }
.stats-card { .stats-card {
flex: 1 1 0; flex: 1 1 0;
min-width: 200px; min-width: 200px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: flex-end;
/* Push content to bottom */ /* Push content to bottom */
align-items: flex-start; align-items: flex-start;
box-sizing: border-box; box-sizing: border-box;
/* Remove min-height/height for natural stretch */ /* Remove min-height/height for natural stretch */
} }

View File

@ -4,361 +4,361 @@
/* General Styles */ /* General Styles */
body { body {
font-family: font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
background-color: #f4f7f9; background-color: #f4f7f9;
/* Lighter, cleaner background */ /* Lighter, cleaner background */
color: #333; color: #333;
/* Darker text for better contrast */ /* Darker text for better contrast */
line-height: 1.6; line-height: 1.6;
display: flex; display: flex;
/* Added for sticky footer */ /* Added for sticky footer */
flex-direction: column; flex-direction: column;
/* Added for sticky footer */ /* Added for sticky footer */
min-height: 100vh; min-height: 100vh;
/* Ensures body takes at least full viewport height */ /* Ensures body takes at least full viewport height */
} }
/* Navbar adjustments (if needed, Bootstrap usually handles this well) */ /* Navbar adjustments (if needed, Bootstrap usually handles this well) */
.navbar { .navbar {
box-shadow: 0 2px 4px rgb(0 0 0 / 5%); box-shadow: 0 2px 4px rgb(0 0 0 / 5%);
/* Subtle shadow for depth */ /* Subtle shadow for depth */
} }
/* Helper Classes */ /* Helper Classes */
.text-truncate-2 { .text-truncate-2 {
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2; line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
} }
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
.min-w-150 { .min-w-150 {
min-width: 150px; min-width: 150px;
} }
/* Card styles */ /* Card styles */
.card { .card {
border: 1px solid #e0e5e9; border: 1px solid #e0e5e9;
/* Lighter border */ /* Lighter border */
border-radius: 0.5rem; border-radius: 0.5rem;
/* Slightly more rounded corners */ /* Slightly more rounded corners */
box-shadow: 0 4px 12px rgb(0 0 0 / 8%); box-shadow: 0 4px 12px rgb(0 0 0 / 8%);
/* Softer, more modern shadow */ /* Softer, more modern shadow */
transition: transition:
transform 0.2s ease-in-out, transform 0.2s ease-in-out,
box-shadow 0.2s ease-in-out; box-shadow 0.2s ease-in-out;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
/* Consistent margin */ /* Consistent margin */
} }
.card-hover:hover { .card-hover:hover {
transform: translateY(-3px); transform: translateY(-3px);
box-shadow: 0 6px 16px rgb(0 0 0 / 10%); box-shadow: 0 6px 16px rgb(0 0 0 / 10%);
} }
.card-header { .card-header {
background-color: #fff; background-color: #fff;
/* Clean white header */ /* Clean white header */
border-bottom: 1px solid #e0e5e9; border-bottom: 1px solid #e0e5e9;
font-weight: 500; font-weight: 500;
/* Slightly bolder header text */ /* Slightly bolder header text */
padding: 0.75rem 1.25rem; padding: 0.75rem 1.25rem;
} }
.card-title { .card-title {
font-size: 1.15rem; font-size: 1.15rem;
/* Adjusted card title size */ /* Adjusted card title size */
font-weight: 600; font-weight: 600;
} }
/* Sidebar enhancements */ /* Sidebar enhancements */
.sidebar { .sidebar {
background-color: #fff; background-color: #fff;
/* White sidebar for a cleaner look */ /* White sidebar for a cleaner look */
border-right: 1px solid #e0e5e9; border-right: 1px solid #e0e5e9;
box-shadow: 2px 0 5px rgb(0 0 0 / 3%); box-shadow: 2px 0 5px rgb(0 0 0 / 3%);
transition: all 0.3s; transition: all 0.3s;
} }
.sidebar-sticky { .sidebar-sticky {
padding-top: 1rem; padding-top: 1rem;
} }
.sidebar .nav-link { .sidebar .nav-link {
color: #4a5568; color: #4a5568;
/* Softer link color */ /* Softer link color */
padding: 0.65rem 1.25rem; padding: 0.65rem 1.25rem;
/* Adjusted padding */ /* Adjusted padding */
border-radius: 0.375rem; border-radius: 0.375rem;
/* Bootstrap-like rounded corners for links */ /* Bootstrap-like rounded corners for links */
margin: 0.1rem 0.5rem; margin: 0.1rem 0.5rem;
/* Margin around links */ /* Margin around links */
font-weight: 500; font-weight: 500;
} }
.sidebar .nav-link:hover { .sidebar .nav-link:hover {
color: #007bff; color: #007bff;
/* Primary color on hover */ /* Primary color on hover */
background-color: #e9f2ff; background-color: #e9f2ff;
/* Light blue background on hover */ /* Light blue background on hover */
} }
.sidebar .nav-link.active { .sidebar .nav-link.active {
color: #007bff; color: #007bff;
background-color: #d6e4ff; background-color: #d6e4ff;
/* Slightly darker blue for active */ /* Slightly darker blue for active */
font-weight: 600; font-weight: 600;
} }
.sidebar .nav-link i.me-2 { .sidebar .nav-link i.me-2 {
width: 20px; width: 20px;
/* Ensure icons align well */ /* Ensure icons align well */
text-align: center; text-align: center;
margin-right: 0.75rem !important; margin-right: 0.75rem !important;
/* Consistent icon spacing */ /* Consistent icon spacing */
} }
.sidebar .nav-header { .sidebar .nav-header {
font-size: 0.8rem; font-size: 0.8rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
color: #718096; color: #718096;
/* Softer header color */ /* Softer header color */
padding: 0.5rem 1.25rem; padding: 0.5rem 1.25rem;
margin-top: 1rem; margin-top: 1rem;
} }
/* Dashboard stats cards */ /* Dashboard stats cards */
.stats-card { .stats-card {
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
} }
.stats-card h3 { .stats-card h3 {
font-size: 1.75rem; font-size: 1.75rem;
font-weight: 600; font-weight: 600;
} }
.stats-card p { .stats-card p {
font-size: 0.875rem; font-size: 0.875rem;
margin-bottom: 0; margin-bottom: 0;
opacity: 0.8; opacity: 0.8;
} }
/* Chart containers */ /* Chart containers */
.chart-container { .chart-container {
width: 100%; width: 100%;
height: 300px; height: 300px;
position: relative; position: relative;
} }
/* Loading overlay */ /* Loading overlay */
.loading-overlay { .loading-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgb(255 255 255 / 70%); background-color: rgb(255 255 255 / 70%);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 9999; z-index: 9999;
} }
/* Table enhancements */ /* Table enhancements */
.table { .table {
border-color: #e0e5e9; border-color: #e0e5e9;
} }
.table th { .table th {
font-weight: 600; font-weight: 600;
/* Bolder table headers */ /* Bolder table headers */
color: #4a5568; color: #4a5568;
background-color: #f8f9fc; background-color: #f8f9fc;
/* Light background for headers */ /* Light background for headers */
} }
.table-striped tbody tr:nth-of-type(odd) { .table-striped tbody tr:nth-of-type(odd) {
background-color: rgb(0 0 0 / 2%); background-color: rgb(0 0 0 / 2%);
/* Very subtle striping */ /* Very subtle striping */
} }
.table-hover tbody tr:hover { .table-hover tbody tr:hover {
background-color: #e9f2ff; background-color: #e9f2ff;
/* Consistent hover with sidebar */ /* Consistent hover with sidebar */
} }
/* Form improvements */ /* Form improvements */
.form-control, .form-control,
.form-select { .form-select {
border-color: #ced4da; border-color: #ced4da;
border-radius: 0.375rem; border-radius: 0.375rem;
/* Consistent border radius */ /* Consistent border radius */
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
/* Adjusted padding */ /* Adjusted padding */
} }
.form-control:focus, .form-control:focus,
.form-select:focus { .form-select:focus {
border-color: #86b7fe; border-color: #86b7fe;
/* Bootstrap focus color */ /* Bootstrap focus color */
box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 25%); box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 25%);
/* Bootstrap focus shadow */ /* Bootstrap focus shadow */
} }
/* Button styling */ /* Button styling */
.btn { .btn {
border-radius: 0.375rem; border-radius: 0.375rem;
/* Consistent border radius */ /* Consistent border radius */
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
/* Standard button padding */ /* Standard button padding */
font-weight: 500; font-weight: 500;
transition: transition:
background-color 0.15s ease-in-out, background-color 0.15s ease-in-out,
border-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
box-shadow 0.15s ease-in-out; box-shadow 0.15s ease-in-out;
} }
.btn-primary { .btn-primary {
background-color: #007bff; background-color: #007bff;
border-color: #007bff; border-color: #007bff;
} }
.btn-primary:hover { .btn-primary:hover {
background-color: #0069d9; background-color: #0069d9;
border-color: #0062cc; border-color: #0062cc;
} }
.btn-secondary { .btn-secondary {
background-color: #6c757d; background-color: #6c757d;
border-color: #6c757d; border-color: #6c757d;
} }
.btn-secondary:hover { .btn-secondary:hover {
background-color: #5a6268; background-color: #5a6268;
border-color: #545b62; border-color: #545b62;
} }
/* Alert styling */ /* Alert styling */
.alert { .alert {
border-radius: 0.375rem; border-radius: 0.375rem;
padding: 0.9rem 1.25rem; padding: 0.9rem 1.25rem;
} }
/* Chat transcript styling */ /* Chat transcript styling */
.chat-transcript { .chat-transcript {
background-color: #f8f9fa; background-color: #f8f9fa;
border: 1px solid #e9ecef; border: 1px solid #e9ecef;
border-radius: 0.25rem; border-radius: 0.25rem;
padding: 1rem; padding: 1rem;
max-height: 500px; max-height: 500px;
overflow-y: auto; overflow-y: auto;
font-size: 0.875rem; font-size: 0.875rem;
} }
.chat-transcript pre { .chat-transcript pre {
white-space: pre-wrap; white-space: pre-wrap;
font-family: inherit; font-family: inherit;
margin-bottom: 0; margin-bottom: 0;
} }
/* Footer styling */ /* Footer styling */
footer { footer {
background-color: #fff; background-color: #fff;
/* White footer */ /* White footer */
border-top: 1px solid #e0e5e9; border-top: 1px solid #e0e5e9;
padding: 1.5rem 0; padding: 1.5rem 0;
color: #6c757d; color: #6c757d;
font-size: 0.9rem; font-size: 0.9rem;
margin-top: auto; margin-top: auto;
/* Added for sticky footer */ /* Added for sticky footer */
} }
/* Responsive adjustments */ /* Responsive adjustments */
@media (width <=767.98px) { @media (width <=767.98px) {
.main-content { .main-content {
margin-left: 0; margin-left: 0;
} }
.stats-card h3 { .stats-card h3 {
font-size: 1.5rem; font-size: 1.5rem;
} }
.chart-container { .chart-container {
height: 250px; height: 250px;
} }
.card-title { .card-title {
font-size: 1.25rem; font-size: 1.25rem;
} }
} }
/* Print styles */ /* Print styles */
@media print { @media print {
.sidebar, .sidebar,
.navbar, .navbar,
.btn, .btn,
footer { footer {
display: none !important; display: none !important;
} }
.main-content { .main-content {
margin-left: 0 !important; margin-left: 0 !important;
padding: 0 !important; padding: 0 !important;
} }
.card { .card {
break-inside: avoid; break-inside: avoid;
border: none !important; border: none !important;
box-shadow: none !important; box-shadow: none !important;
} }
.chart-container { .chart-container {
break-inside: avoid; break-inside: avoid;
height: auto !important; height: auto !important;
} }
} }

View File

@ -6,269 +6,269 @@
*/ */
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Only initialize if AJAX navigation is enabled // Only initialize if AJAX navigation is enabled
if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) { if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) {
setupAjaxNavigation(); setupAjaxNavigation();
} }
// Function to set up AJAX navigation for the application // Function to set up AJAX navigation for the application
function setupAjaxNavigation() { function setupAjaxNavigation() {
// Configuration // Configuration
const config = { const config = {
mainContentSelector: "#main-content", // Selector for the main content area mainContentSelector: "#main-content", // Selector for the main content area
navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX
loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator
excludePatterns: [ excludePatterns: [
// URL patterns to exclude from AJAX navigation // URL patterns to exclude from AJAX navigation
/\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads /\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads
/\/admin\//, // Admin pages /\/admin\//, // Admin pages
/\/accounts\/logout\//, // Logout page /\/accounts\/logout\//, // Logout page
/\/api\//, // API endpoints /\/api\//, // API endpoints
], ],
}; };
// Create and insert the loading indicator // Create and insert the loading indicator
if (!document.getElementById(config.loadingIndicatorId)) { if (!document.getElementById(config.loadingIndicatorId)) {
const loadingIndicator = document.createElement("div"); const loadingIndicator = document.createElement("div");
loadingIndicator.id = config.loadingIndicatorId; loadingIndicator.id = config.loadingIndicatorId;
loadingIndicator.className = "position-fixed top-0 start-0 end-0"; loadingIndicator.className = "position-fixed top-0 start-0 end-0";
loadingIndicator.innerHTML = loadingIndicator.innerHTML =
'<div class="progress" style="height: 3px; border-radius: 0;"><div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" style="width: 100%"></div></div>'; '<div class="progress" style="height: 3px; border-radius: 0;"><div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" style="width: 100%"></div></div>';
loadingIndicator.style.display = "none"; loadingIndicator.style.display = "none";
loadingIndicator.style.zIndex = "9999"; loadingIndicator.style.zIndex = "9999";
document.body.appendChild(loadingIndicator); document.body.appendChild(loadingIndicator);
} }
// Get the loading indicator element // Get the loading indicator element
const loadingIndicator = document.getElementById(config.loadingIndicatorId); const loadingIndicator = document.getElementById(config.loadingIndicatorId);
// Get the main content container // Get the main content container
const mainContent = document.querySelector(config.mainContentSelector); const mainContent = document.querySelector(config.mainContentSelector);
if (!mainContent) { if (!mainContent) {
console.warn("Main content container not found. AJAX navigation disabled."); console.warn("Main content container not found. AJAX navigation disabled.");
return; return;
} }
// Function to check if a URL should be excluded from AJAX navigation // Function to check if a URL should be excluded from AJAX navigation
function shouldExcludeUrl(url) { function shouldExcludeUrl(url) {
for (const pattern of config.excludePatterns) { for (const pattern of config.excludePatterns) {
if (pattern.test(url)) { if (pattern.test(url)) {
return true; return true;
} }
} }
return false; return false;
} }
// Function to show the loading indicator // Function to show the loading indicator
function showLoading() { function showLoading() {
loadingIndicator.style.display = "block"; loadingIndicator.style.display = "block";
} }
// Function to hide the loading indicator // Function to hide the loading indicator
function hideLoading() { function hideLoading() {
loadingIndicator.style.display = "none"; loadingIndicator.style.display = "none";
} }
// Function to handle AJAX page navigation // Function to handle AJAX page navigation
function handlePageNavigation(url, pushState = true) { function handlePageNavigation(url, pushState = true) {
if (shouldExcludeUrl(url)) { if (shouldExcludeUrl(url)) {
window.location.href = url; window.location.href = url;
return; return;
} }
showLoading(); showLoading();
const currentScrollPos = window.scrollY; const currentScrollPos = window.scrollY;
fetch(url, { fetch(url, {
headers: { headers: {
"X-Requested-With": "XMLHttpRequest", "X-Requested-With": "XMLHttpRequest",
"X-AJAX-Navigation": "true", "X-AJAX-Navigation": "true",
Accept: "text/html", Accept: "text/html",
}, },
}) })
.then((response) => { .then((response) => {
if (!response.ok) if (!response.ok)
throw new Error(`Network response was not ok: ${response.status}`); throw new Error(`Network response was not ok: ${response.status}`);
return response.text(); return response.text();
}) })
.then((html) => { .then((html) => {
// Parse the HTML and extract #main-content // Parse the HTML and extract #main-content
const tempDiv = document.createElement("div"); const tempDiv = document.createElement("div");
tempDiv.innerHTML = html; tempDiv.innerHTML = html;
const newContent = tempDiv.querySelector(config.mainContentSelector); const newContent = tempDiv.querySelector(config.mainContentSelector);
if (!newContent) throw new Error("Could not find main content in the response"); if (!newContent) throw new Error("Could not find main content in the response");
mainContent.innerHTML = newContent.innerHTML; mainContent.innerHTML = newContent.innerHTML;
// Update the page title // Update the page title
const titleMatch = html.match(/<title>(.*?)<\/title>/i); const titleMatch = html.match(/<title>(.*?)<\/title>/i);
if (titleMatch) document.title = titleMatch[1]; if (titleMatch) document.title = titleMatch[1];
// Re-initialize dynamic content // Re-initialize dynamic content
reloadScripts(mainContent); reloadScripts(mainContent);
attachEventListeners(); attachEventListeners();
initializePageScripts(); initializePageScripts();
if (pushState) { if (pushState) {
history.pushState( history.pushState(
{ url: url, title: document.title, scrollPos: currentScrollPos }, { url: url, title: document.title, scrollPos: currentScrollPos },
document.title, document.title,
url, url,
); );
window.scrollTo({ top: 0, behavior: "smooth" }); window.scrollTo({ top: 0, behavior: "smooth" });
} else if (window.history.state && window.history.state.scrollPos) { } else if (window.history.state && window.history.state.scrollPos) {
window.scrollTo({ top: window.history.state.scrollPos }); window.scrollTo({ top: window.history.state.scrollPos });
} }
hideLoading(); hideLoading();
}) })
.catch((error) => { .catch((error) => {
console.error("Error during AJAX navigation:", error); console.error("Error during AJAX navigation:", error);
hideLoading(); hideLoading();
window.location.href = url; window.location.href = url;
}); });
} }
// Function to reload and execute scripts in new content // Function to reload and execute scripts in new content
function reloadScripts(container) { function reloadScripts(container) {
const scripts = container.getElementsByTagName("script"); const scripts = container.getElementsByTagName("script");
for (let script of scripts) { for (let script of scripts) {
const newScript = document.createElement("script"); const newScript = document.createElement("script");
// Copy all attributes // Copy all attributes
Array.from(script.attributes).forEach((attr) => { Array.from(script.attributes).forEach((attr) => {
newScript.setAttribute(attr.name, attr.value); newScript.setAttribute(attr.name, attr.value);
}); });
// Copy inline script content // Copy inline script content
newScript.textContent = script.textContent; newScript.textContent = script.textContent;
// Replace old script with new one // Replace old script with new one
script.parentNode.replaceChild(newScript, script); script.parentNode.replaceChild(newScript, script);
} }
} }
// Function to handle form submissions // Function to handle form submissions
function handleFormSubmission(form, e) { function handleFormSubmission(form, e) {
e.preventDefault(); e.preventDefault();
// Show loading indicator // Show loading indicator
showLoading(); showLoading();
// Get form data // Get form data
const formData = new FormData(form); const formData = new FormData(form);
const method = form.method.toLowerCase(); const method = form.method.toLowerCase();
const url = form.action || window.location.href; const url = form.action || window.location.href;
// Configure fetch options // Configure fetch options
const fetchOptions = { const fetchOptions = {
method: method, method: method,
headers: { headers: {
"X-AJAX-Navigation": "true", "X-AJAX-Navigation": "true",
}, },
}; };
// Handle different HTTP methods // Handle different HTTP methods
if (method === "get") { if (method === "get") {
const queryParams = new URLSearchParams(formData).toString(); const queryParams = new URLSearchParams(formData).toString();
handlePageNavigation(url + (queryParams ? "?" + queryParams : "")); handlePageNavigation(url + (queryParams ? "?" + queryParams : ""));
} else { } else {
fetchOptions.body = formData; fetchOptions.body = formData;
fetch(url, fetchOptions) fetch(url, fetchOptions)
.then((response) => { .then((response) => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.json(); return response.json();
}) })
.then((data) => { .then((data) => {
if (data.redirect) { if (data.redirect) {
// Handle server-side redirects // Handle server-side redirects
handlePageNavigation(data.redirect, true); handlePageNavigation(data.redirect, true);
} else { } else {
// Update page content // Update page content
mainContent.innerHTML = data.html; mainContent.innerHTML = data.html;
document.title = data.title || document.title; document.title = data.title || document.title;
// Re-initialize dynamic content // Re-initialize dynamic content
reloadScripts(mainContent); reloadScripts(mainContent);
attachEventListeners(); attachEventListeners();
initializePageScripts(); initializePageScripts();
// Update URL if needed // Update URL if needed
if (data.url) { if (data.url) {
history.pushState({ url: data.url }, document.title, data.url); history.pushState({ url: data.url }, document.title, data.url);
} }
} }
}) })
.catch((error) => { .catch((error) => {
console.error("Form submission error:", error); console.error("Form submission error:", error);
// Fallback to traditional form submission // Fallback to traditional form submission
form.submit(); form.submit();
}) })
.finally(() => { .finally(() => {
hideLoading(); hideLoading();
}); });
} }
} }
// Function to initialize scripts needed for the new page content // Function to initialize scripts needed for the new page content
function initializePageScripts() { function initializePageScripts() {
// Re-initialize any custom scripts that might be needed // Re-initialize any custom scripts that might be needed
if (typeof setupAjaxPagination === "function") { if (typeof setupAjaxPagination === "function") {
setupAjaxPagination(); setupAjaxPagination();
} }
// Initialize Bootstrap tooltips, popovers, etc. // Initialize Bootstrap tooltips, popovers, etc.
if (typeof bootstrap !== "undefined") { if (typeof bootstrap !== "undefined") {
// Initialize tooltips // Initialize tooltips
const tooltipTriggerList = [].slice.call( const tooltipTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="tooltip"]'), document.querySelectorAll('[data-bs-toggle="tooltip"]'),
); );
tooltipTriggerList.map(function (tooltipTriggerEl) { tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl); return new bootstrap.Tooltip(tooltipTriggerEl);
}); });
// Initialize popovers // Initialize popovers
const popoverTriggerList = [].slice.call( const popoverTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="popover"]'), document.querySelectorAll('[data-bs-toggle="popover"]'),
); );
popoverTriggerList.map(function (popoverTriggerEl) { popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl); return new bootstrap.Popover(popoverTriggerEl);
}); });
} }
} }
// Function to attach event listeners to forms and links // Function to attach event listeners to forms and links
function attachEventListeners() { function attachEventListeners() {
// Handle AJAX navigation links // Handle AJAX navigation links
document.querySelectorAll(config.navLinkSelector).forEach((link) => { document.querySelectorAll(config.navLinkSelector).forEach((link) => {
if (!link.dataset.ajaxNavInitialized) { if (!link.dataset.ajaxNavInitialized) {
link.addEventListener("click", function (e) { link.addEventListener("click", function (e) {
if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) { if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) {
return; // Let the browser handle these cases return; // Let the browser handle these cases
} }
e.preventDefault(); e.preventDefault();
handlePageNavigation(this.href); handlePageNavigation(this.href);
}); });
link.dataset.ajaxNavInitialized = "true"; link.dataset.ajaxNavInitialized = "true";
} }
}); });
// Handle forms with AJAX // Handle forms with AJAX
document document
.querySelectorAll("form.ajax-form, form.search-form, form.filter-form") .querySelectorAll("form.ajax-form, form.search-form, form.filter-form")
.forEach((form) => { .forEach((form) => {
if (!form.dataset.ajaxFormInitialized) { if (!form.dataset.ajaxFormInitialized) {
form.addEventListener("submit", (e) => handleFormSubmission(form, e)); form.addEventListener("submit", (e) => handleFormSubmission(form, e));
form.dataset.ajaxFormInitialized = "true"; form.dataset.ajaxFormInitialized = "true";
} }
}); });
} }
// Initial attachment of event listeners // Initial attachment of event listeners
attachEventListeners(); attachEventListeners();
// Handle browser back/forward buttons // Handle browser back/forward buttons
window.addEventListener("popstate", function (event) { window.addEventListener("popstate", function (event) {
if (event.state && event.state.url) { if (event.state && event.state.url) {
handlePageNavigation(event.state.url, false); handlePageNavigation(event.state.url, false);
} else { } else {
// Fallback to current URL if no state // Fallback to current URL if no state
handlePageNavigation(window.location.href, false); handlePageNavigation(window.location.href, false);
} }
}); });
} }
}); });

View File

@ -6,101 +6,101 @@
*/ */
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Initialize AJAX pagination // Initialize AJAX pagination
setupAjaxPagination(); setupAjaxPagination();
// Function to set up AJAX pagination for the entire application // Function to set up AJAX pagination for the entire application
function setupAjaxPagination() { function setupAjaxPagination() {
// Configuration - can be customized per page if needed // Configuration - can be customized per page if needed
const config = { const config = {
contentContainerId: "ajax-content-container", // ID of the container to update contentContainerId: "ajax-content-container", // ID of the container to update
loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner
paginationLinkClass: "pagination-link", // Class for pagination links paginationLinkClass: "pagination-link", // Class for pagination links
retryMessage: "An error occurred while loading data. Please try again.", retryMessage: "An error occurred while loading data. Please try again.",
}; };
// Get container elements // Get container elements
const contentContainer = document.getElementById(config.contentContainerId); const contentContainer = document.getElementById(config.contentContainerId);
const loadingSpinner = document.getElementById(config.loadingSpinnerId); const loadingSpinner = document.getElementById(config.loadingSpinnerId);
// Exit if the page doesn't have the required elements // Exit if the page doesn't have the required elements
if (!contentContainer || !loadingSpinner) return; if (!contentContainer || !loadingSpinner) return;
// Function to handle pagination clicks // Function to handle pagination clicks
function setupPaginationListeners() { function setupPaginationListeners() {
document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => { document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => {
link.addEventListener("click", function (e) { link.addEventListener("click", function (e) {
e.preventDefault(); e.preventDefault();
handleAjaxNavigation(this.href); handleAjaxNavigation(this.href);
// Get the page number if available // Get the page number if available
const page = this.getAttribute("data-page"); const page = this.getAttribute("data-page");
// Update browser URL without refreshing // Update browser URL without refreshing
const newUrl = this.href; const newUrl = this.href;
history.pushState({ url: newUrl, page: page }, "", newUrl); history.pushState({ url: newUrl, page: page }, "", newUrl);
}); });
}); });
} }
// Function to handle AJAX navigation // Function to handle AJAX navigation
function handleAjaxNavigation(url) { function handleAjaxNavigation(url) {
// Show loading spinner // Show loading spinner
contentContainer.classList.add("d-none"); contentContainer.classList.add("d-none");
loadingSpinner.classList.remove("d-none"); loadingSpinner.classList.remove("d-none");
// Fetch data via AJAX // Fetch data via AJAX
fetch(url, { fetch(url, {
headers: { headers: {
"X-Requested-With": "XMLHttpRequest", "X-Requested-With": "XMLHttpRequest",
}, },
}) })
.then((response) => { .then((response) => {
if (!response.ok) { if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`); throw new Error(`Network response was not ok: ${response.status}`);
} }
return response.json(); return response.json();
}) })
.then((data) => { .then((data) => {
if (data.status === "success") { if (data.status === "success") {
// Update the content // Update the content
contentContainer.innerHTML = data.html_data; contentContainer.innerHTML = data.html_data;
// Re-attach event listeners to new pagination links // Re-attach event listeners to new pagination links
setupPaginationListeners(); setupPaginationListeners();
// Update any summary data if present and the page provides it // Update any summary data if present and the page provides it
if (typeof updateSummary === "function" && data.summary) { if (typeof updateSummary === "function" && data.summary) {
updateSummary(data); updateSummary(data);
} }
// Hide loading spinner, show content // Hide loading spinner, show content
loadingSpinner.classList.add("d-none"); loadingSpinner.classList.add("d-none");
contentContainer.classList.remove("d-none"); contentContainer.classList.remove("d-none");
// Scroll to top of the content container // Scroll to top of the content container
contentContainer.scrollIntoView({ behavior: "smooth", block: "start" }); contentContainer.scrollIntoView({ behavior: "smooth", block: "start" });
} }
}) })
.catch((error) => { .catch((error) => {
console.error("Error fetching data:", error); console.error("Error fetching data:", error);
loadingSpinner.classList.add("d-none"); loadingSpinner.classList.add("d-none");
contentContainer.classList.remove("d-none"); contentContainer.classList.remove("d-none");
alert(config.retryMessage); alert(config.retryMessage);
}); });
} }
// Initial setup of event listeners // Initial setup of event listeners
setupPaginationListeners(); setupPaginationListeners();
// Handle browser back/forward buttons // Handle browser back/forward buttons
window.addEventListener("popstate", function (event) { window.addEventListener("popstate", function (event) {
if (event.state && event.state.url) { if (event.state && event.state.url) {
handleAjaxNavigation(event.state.url); handleAjaxNavigation(event.state.url);
} else { } else {
// If no state, fetch current URL // If no state, fetch current URL
handleAjaxNavigation(window.location.href); handleAjaxNavigation(window.location.href);
} }
}); });
} }
}); });

View File

@ -7,272 +7,272 @@
*/ */
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Chart responsiveness // Chart responsiveness
function resizeCharts() { function resizeCharts() {
const charts = document.querySelectorAll(".chart-container"); const charts = document.querySelectorAll(".chart-container");
charts.forEach((chart) => { charts.forEach((chart) => {
if (chart.id && window.Plotly) { if (chart.id && window.Plotly) {
Plotly.relayout(chart.id, { Plotly.relayout(chart.id, {
"xaxis.automargin": true, "xaxis.automargin": true,
"yaxis.automargin": true, "yaxis.automargin": true,
}); });
} }
}); });
} }
// Handle window resize // Handle window resize
window.addEventListener("resize", function () { window.addEventListener("resize", function () {
if (window.Plotly) { if (window.Plotly) {
resizeCharts(); resizeCharts();
} }
}); });
// Time range filtering // Time range filtering
const timeRangeDropdown = document.getElementById("timeRangeDropdown"); const timeRangeDropdown = document.getElementById("timeRangeDropdown");
if (timeRangeDropdown) { if (timeRangeDropdown) {
const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item"); const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item");
timeRangeLinks.forEach((link) => { timeRangeLinks.forEach((link) => {
link.addEventListener("click", function (e) { link.addEventListener("click", function (e) {
const url = new URL(this.href); const url = new URL(this.href);
const dashboardId = url.searchParams.get("dashboard_id"); const dashboardId = url.searchParams.get("dashboard_id");
const timeRange = url.searchParams.get("time_range"); const timeRange = url.searchParams.get("time_range");
// Fetch updated data via AJAX // Fetch updated data via AJAX
if (dashboardId) { if (dashboardId) {
fetchDashboardData(dashboardId, timeRange); fetchDashboardData(dashboardId, timeRange);
e.preventDefault(); e.preventDefault();
} }
}); });
}); });
} }
// Function to fetch dashboard data // Function to fetch dashboard data
function fetchDashboardData(dashboardId, timeRange) { function fetchDashboardData(dashboardId, timeRange) {
const loadingOverlay = document.createElement("div"); const loadingOverlay = document.createElement("div");
loadingOverlay.className = "loading-overlay"; loadingOverlay.className = "loading-overlay";
loadingOverlay.innerHTML = loadingOverlay.innerHTML =
'<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>'; '<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>';
document.querySelector("main").appendChild(loadingOverlay); document.querySelector("main").appendChild(loadingOverlay);
fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`) fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`)
.then((response) => { .then((response) => {
if (!response.ok) { if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`); throw new Error(`Network response was not ok: ${response.status}`);
} }
return response.json(); return response.json();
}) })
.then((data) => { .then((data) => {
console.log("Dashboard API response:", data); console.log("Dashboard API response:", data);
updateDashboardStats(data); updateDashboardStats(data);
updateDashboardCharts(data); updateDashboardCharts(data);
// Update URL without page reload // Update URL without page reload
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.searchParams.set("dashboard_id", dashboardId); url.searchParams.set("dashboard_id", dashboardId);
if (timeRange) { if (timeRange) {
url.searchParams.set("time_range", timeRange); url.searchParams.set("time_range", timeRange);
} }
window.history.pushState({}, "", url); window.history.pushState({}, "", url);
document.querySelector(".loading-overlay").remove(); document.querySelector(".loading-overlay").remove();
}) })
.catch((error) => { .catch((error) => {
console.error("Error fetching dashboard data:", error); console.error("Error fetching dashboard data:", error);
document.querySelector(".loading-overlay").remove(); document.querySelector(".loading-overlay").remove();
// Show error message // Show error message
const alertElement = document.createElement("div"); const alertElement = document.createElement("div");
alertElement.className = "alert alert-danger alert-dismissible fade show"; alertElement.className = "alert alert-danger alert-dismissible fade show";
alertElement.setAttribute("role", "alert"); alertElement.setAttribute("role", "alert");
alertElement.innerHTML = ` alertElement.innerHTML = `
Error loading dashboard data. Please try again. Error loading dashboard data. Please try again.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`; `;
document.querySelector("main").prepend(alertElement); document.querySelector("main").prepend(alertElement);
}); });
} }
// Function to update dashboard statistics // Function to update dashboard statistics
function updateDashboardStats(data) { function updateDashboardStats(data) {
// Update total sessions // Update total sessions
const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3"); const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3");
if (totalSessionsElement) { if (totalSessionsElement) {
totalSessionsElement.textContent = data.total_sessions; totalSessionsElement.textContent = data.total_sessions;
} }
// Update average response time // Update average response time
const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3"); const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3");
if (avgResponseTimeElement) { if (avgResponseTimeElement) {
avgResponseTimeElement.textContent = data.avg_response_time + "s"; avgResponseTimeElement.textContent = data.avg_response_time + "s";
} }
// Update total tokens // Update total tokens
const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3"); const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3");
if (totalTokensElement) { if (totalTokensElement) {
totalTokensElement.textContent = data.total_tokens; totalTokensElement.textContent = data.total_tokens;
} }
// Update total cost // Update total cost
const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3"); const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3");
if (totalCostElement) { if (totalCostElement) {
totalCostElement.textContent = "€" + data.total_cost; totalCostElement.textContent = "€" + data.total_cost;
} }
} }
// Function to update dashboard charts // Function to update dashboard charts
function updateDashboardCharts(data) { function updateDashboardCharts(data) {
// Check if Plotly is available // Check if Plotly is available
if (!window.Plotly) { if (!window.Plotly) {
console.error("Plotly library not loaded!"); console.error("Plotly library not loaded!");
document.querySelectorAll(".chart-container").forEach((container) => { document.querySelectorAll(".chart-container").forEach((container) => {
container.innerHTML = container.innerHTML =
'<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>'; '<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>';
}); });
return; return;
} }
// Update sessions over time chart // Update sessions over time chart
const timeSeriesData = data.time_series_data; const timeSeriesData = data.time_series_data;
if (timeSeriesData && timeSeriesData.length > 0) { if (timeSeriesData && timeSeriesData.length > 0) {
try { try {
const timeSeriesX = timeSeriesData.map((item) => item.date); const timeSeriesX = timeSeriesData.map((item) => item.date);
const timeSeriesY = timeSeriesData.map((item) => item.count); const timeSeriesY = timeSeriesData.map((item) => item.count);
Plotly.react( Plotly.react(
"sessions-time-chart", "sessions-time-chart",
[ [
{ {
x: timeSeriesX, x: timeSeriesX,
y: timeSeriesY, y: timeSeriesY,
type: "scatter", type: "scatter",
mode: "lines+markers", mode: "lines+markers",
line: { line: {
color: "rgb(75, 192, 192)", color: "rgb(75, 192, 192)",
width: 2, width: 2,
}, },
marker: { marker: {
color: "rgb(75, 192, 192)", color: "rgb(75, 192, 192)",
size: 6, size: 6,
}, },
}, },
], ],
{ {
margin: { t: 10, r: 10, b: 40, l: 40 }, margin: { t: 10, r: 10, b: 40, l: 40 },
xaxis: { xaxis: {
title: "Date", title: "Date",
}, },
yaxis: { yaxis: {
title: "Number of Sessions", title: "Number of Sessions",
}, },
}, },
); );
} catch (error) { } catch (error) {
console.error("Error rendering time series chart:", error); console.error("Error rendering time series chart:", error);
document.getElementById("sessions-time-chart").innerHTML = document.getElementById("sessions-time-chart").innerHTML =
'<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>'; '<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>';
} }
} else { } else {
document.getElementById("sessions-time-chart").innerHTML = document.getElementById("sessions-time-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>'; '<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
} }
// Update sentiment chart // Update sentiment chart
const sentimentData = data.sentiment_data; const sentimentData = data.sentiment_data;
if (sentimentData && sentimentData.length > 0 && window.Plotly) { if (sentimentData && sentimentData.length > 0 && window.Plotly) {
const sentimentLabels = sentimentData.map((item) => item.sentiment); const sentimentLabels = sentimentData.map((item) => item.sentiment);
const sentimentValues = sentimentData.map((item) => item.count); const sentimentValues = sentimentData.map((item) => item.count);
const sentimentColors = sentimentLabels.map((sentiment) => { const sentimentColors = sentimentLabels.map((sentiment) => {
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)"; if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)"; if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)"; if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
return "rgb(201, 203, 207)"; return "rgb(201, 203, 207)";
}); });
Plotly.react( Plotly.react(
"sentiment-chart", "sentiment-chart",
[ [
{ {
values: sentimentValues, values: sentimentValues,
labels: sentimentLabels, labels: sentimentLabels,
type: "pie", type: "pie",
marker: { marker: {
colors: sentimentColors, colors: sentimentColors,
}, },
hole: 0.4, hole: 0.4,
textinfo: "label+percent", textinfo: "label+percent",
insidetextorientation: "radial", insidetextorientation: "radial",
}, },
], ],
{ {
margin: { t: 10, r: 10, b: 10, l: 10 }, margin: { t: 10, r: 10, b: 10, l: 10 },
}, },
); );
} }
// Update country chart // Update country chart
const countryData = data.country_data; const countryData = data.country_data;
if (countryData && countryData.length > 0 && window.Plotly) { if (countryData && countryData.length > 0 && window.Plotly) {
const countryLabels = countryData.map((item) => item.country); const countryLabels = countryData.map((item) => item.country);
const countryValues = countryData.map((item) => item.count); const countryValues = countryData.map((item) => item.count);
Plotly.react( Plotly.react(
"country-chart", "country-chart",
[ [
{ {
x: countryValues, x: countryValues,
y: countryLabels, y: countryLabels,
type: "bar", type: "bar",
orientation: "h", orientation: "h",
marker: { marker: {
color: "rgb(54, 162, 235)", color: "rgb(54, 162, 235)",
}, },
}, },
], ],
{ {
margin: { t: 10, r: 10, b: 40, l: 100 }, margin: { t: 10, r: 10, b: 40, l: 100 },
xaxis: { xaxis: {
title: "Number of Sessions", title: "Number of Sessions",
}, },
}, },
); );
} }
// Update category chart // Update category chart
const categoryData = data.category_data; const categoryData = data.category_data;
if (categoryData && categoryData.length > 0 && window.Plotly) { if (categoryData && categoryData.length > 0 && window.Plotly) {
const categoryLabels = categoryData.map((item) => item.category); const categoryLabels = categoryData.map((item) => item.category);
const categoryValues = categoryData.map((item) => item.count); const categoryValues = categoryData.map((item) => item.count);
Plotly.react( Plotly.react(
"category-chart", "category-chart",
[ [
{ {
labels: categoryLabels, labels: categoryLabels,
values: categoryValues, values: categoryValues,
type: "pie", type: "pie",
textinfo: "label+percent", textinfo: "label+percent",
insidetextorientation: "radial", insidetextorientation: "radial",
}, },
], ],
{ {
margin: { t: 10, r: 10, b: 10, l: 10 }, margin: { t: 10, r: 10, b: 10, l: 10 },
}, },
); );
} }
} }
// Dashboard selector // Dashboard selector
const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]'); const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]');
dashboardSelector.forEach((link) => { dashboardSelector.forEach((link) => {
link.addEventListener("click", function (e) { link.addEventListener("click", function (e) {
const url = new URL(this.href); const url = new URL(this.href);
const dashboardId = url.searchParams.get("dashboard_id"); const dashboardId = url.searchParams.get("dashboard_id");
// Fetch updated data via AJAX // Fetch updated data via AJAX
if (dashboardId) { if (dashboardId) {
fetchDashboardData(dashboardId); fetchDashboardData(dashboardId);
e.preventDefault(); e.preventDefault();
} }
}); });
}); });
}); });

View File

@ -6,147 +6,147 @@
*/ */
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Initialize tooltips // Initialize tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl); return new bootstrap.Tooltip(tooltipTriggerEl);
}); });
// Initialize popovers // Initialize popovers
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')); var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) { var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl); return new bootstrap.Popover(popoverTriggerEl);
}); });
// Toggle sidebar on mobile // Toggle sidebar on mobile
const sidebarToggle = document.querySelector("#sidebarToggle"); const sidebarToggle = document.querySelector("#sidebarToggle");
if (sidebarToggle) { if (sidebarToggle) {
sidebarToggle.addEventListener("click", function () { sidebarToggle.addEventListener("click", function () {
document.querySelector(".sidebar").classList.toggle("show"); document.querySelector(".sidebar").classList.toggle("show");
}); });
} }
// Auto-dismiss alerts after 5 seconds // Auto-dismiss alerts after 5 seconds
setTimeout(function () { setTimeout(function () {
var alerts = document.querySelectorAll(".alert:not(.alert-important)"); var alerts = document.querySelectorAll(".alert:not(.alert-important)");
alerts.forEach(function (alert) { alerts.forEach(function (alert) {
if (alert && bootstrap.Alert.getInstance(alert)) { if (alert && bootstrap.Alert.getInstance(alert)) {
bootstrap.Alert.getInstance(alert).close(); bootstrap.Alert.getInstance(alert).close();
} }
}); });
}, 5000); }, 5000);
// Form validation // Form validation
const forms = document.querySelectorAll(".needs-validation"); const forms = document.querySelectorAll(".needs-validation");
forms.forEach(function (form) { forms.forEach(function (form) {
form.addEventListener( form.addEventListener(
"submit", "submit",
function (event) { function (event) {
if (!form.checkValidity()) { if (!form.checkValidity()) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
form.classList.add("was-validated"); form.classList.add("was-validated");
}, },
false, false,
); );
}); });
// Confirm dialogs // Confirm dialogs
const confirmButtons = document.querySelectorAll("[data-confirm]"); const confirmButtons = document.querySelectorAll("[data-confirm]");
confirmButtons.forEach(function (button) { confirmButtons.forEach(function (button) {
button.addEventListener("click", function (event) { button.addEventListener("click", function (event) {
if (!confirm(this.dataset.confirm || "Are you sure?")) { if (!confirm(this.dataset.confirm || "Are you sure?")) {
event.preventDefault(); event.preventDefault();
} }
}); });
}); });
// Back button // Back button
const backButtons = document.querySelectorAll(".btn-back"); const backButtons = document.querySelectorAll(".btn-back");
backButtons.forEach(function (button) { backButtons.forEach(function (button) {
button.addEventListener("click", function (event) { button.addEventListener("click", function (event) {
event.preventDefault(); event.preventDefault();
window.history.back(); window.history.back();
}); });
}); });
// File input customization // File input customization
const fileInputs = document.querySelectorAll(".custom-file-input"); const fileInputs = document.querySelectorAll(".custom-file-input");
fileInputs.forEach(function (input) { fileInputs.forEach(function (input) {
input.addEventListener("change", function (e) { input.addEventListener("change", function (e) {
const fileName = this.files[0]?.name || "Choose file"; const fileName = this.files[0]?.name || "Choose file";
const nextSibling = this.nextElementSibling; const nextSibling = this.nextElementSibling;
if (nextSibling) { if (nextSibling) {
nextSibling.innerText = fileName; nextSibling.innerText = fileName;
} }
}); });
}); });
// Search form submit on enter // Search form submit on enter
const searchInputs = document.querySelectorAll(".search-input"); const searchInputs = document.querySelectorAll(".search-input");
searchInputs.forEach(function (input) { searchInputs.forEach(function (input) {
input.addEventListener("keypress", function (e) { input.addEventListener("keypress", function (e) {
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
this.closest("form").submit(); this.closest("form").submit();
} }
}); });
}); });
// Toggle password visibility // Toggle password visibility
const togglePasswordButtons = document.querySelectorAll(".toggle-password"); const togglePasswordButtons = document.querySelectorAll(".toggle-password");
togglePasswordButtons.forEach(function (button) { togglePasswordButtons.forEach(function (button) {
button.addEventListener("click", function () { button.addEventListener("click", function () {
const target = document.querySelector(this.dataset.target); const target = document.querySelector(this.dataset.target);
if (target) { if (target) {
const type = target.getAttribute("type") === "password" ? "text" : "password"; const type = target.getAttribute("type") === "password" ? "text" : "password";
target.setAttribute("type", type); target.setAttribute("type", type);
this.querySelector("i").classList.toggle("fa-eye"); this.querySelector("i").classList.toggle("fa-eye");
this.querySelector("i").classList.toggle("fa-eye-slash"); this.querySelector("i").classList.toggle("fa-eye-slash");
} }
}); });
}); });
// Dropdown menu positioning // Dropdown menu positioning
const dropdowns = document.querySelectorAll(".dropdown-menu"); const dropdowns = document.querySelectorAll(".dropdown-menu");
dropdowns.forEach(function (dropdown) { dropdowns.forEach(function (dropdown) {
dropdown.addEventListener("click", function (e) { dropdown.addEventListener("click", function (e) {
e.stopPropagation(); e.stopPropagation();
}); });
}); });
// Responsive table handling // Responsive table handling
const tables = document.querySelectorAll(".table-responsive"); const tables = document.querySelectorAll(".table-responsive");
if (window.innerWidth < 768) { if (window.innerWidth < 768) {
tables.forEach(function (table) { tables.forEach(function (table) {
table.classList.add("table-responsive-force"); table.classList.add("table-responsive-force");
}); });
} }
// Handle special links (printable views, exports) // Handle special links (printable views, exports)
const printLinks = document.querySelectorAll(".print-link"); const printLinks = document.querySelectorAll(".print-link");
printLinks.forEach(function (link) { printLinks.forEach(function (link) {
link.addEventListener("click", function (e) { link.addEventListener("click", function (e) {
e.preventDefault(); e.preventDefault();
window.print(); window.print();
}); });
}); });
const exportLinks = document.querySelectorAll("[data-export]"); const exportLinks = document.querySelectorAll("[data-export]");
exportLinks.forEach(function (link) { exportLinks.forEach(function (link) {
link.addEventListener("click", function (e) { link.addEventListener("click", function (e) {
// Handle export functionality if needed // Handle export functionality if needed
console.log("Export requested:", this.dataset.export); console.log("Export requested:", this.dataset.export);
}); });
}); });
// Handle sidebar collapse on small screens // Handle sidebar collapse on small screens
function handleSidebarOnResize() { function handleSidebarOnResize() {
if (window.innerWidth < 768) { if (window.innerWidth < 768) {
document.querySelector(".sidebar")?.classList.remove("show"); document.querySelector(".sidebar")?.classList.remove("show");
} }
} }
window.addEventListener("resize", handleSidebarOnResize); window.addEventListener("resize", handleSidebarOnResize);
}); });

View File

@ -1,31 +1,27 @@
<!-- templates/accounts/login.html --> <!-- templates/accounts/login.html -->
{% extends 'base.html' %} {% extends 'base.html' %} {% load crispy_forms_tags %}
{% load crispy_forms_tags %} {% block title %}
Login | Chat Analytics
{% block title %}Login | Chat Analytics{% endblock %} {% endblock %}
{% block content %}
{% block content %} <div class="row justify-content-center">
<div class="row justify-content-center"> <div class="col-md-6">
<div class="col-md-6"> <div class="card mt-4">
<div class="card mt-4"> <div class="card-header">
<div class="card-header"> <h4 class="card-title mb-0">Login</h4>
<h4 class="card-title mb-0">Login</h4> </div>
</div> <div class="card-body">
<div class="card-body"> <form method="post">
<form method="post"> {% csrf_token %} {{ form|crispy }}
{% csrf_token %} <div class="d-grid gap-2">
{{ form|crispy }} <button type="submit" class="btn btn-primary">Login</button>
<div class="d-grid gap-2"> </div>
<button type="submit" class="btn btn-primary">Login</button> </form>
</div> </div>
</form> <div class="card-footer text-center">
</div> <p class="mb-0">Don't have an account? <a href="{% url 'register' %}">Register</a></p>
<div class="card-footer text-center"> </div>
<p class="mb-0"> </div>
Don't have an account? <a href="{% url 'register' %}">Register</a> </div>
</p> </div>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,37 +1,34 @@
<!-- templates/accounts/password_change.html --> <!-- templates/accounts/password_change.html -->
{% extends 'base.html' %} {% extends 'base.html' %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title %}Change Password | Chat Analytics{% endblock %} {% block title %}Change Password | Chat Analytics{% endblock %}
{% block content %} {% block content %}
<div <div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom" 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> <h1 class="h2">Change Password</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'profile' %}" class="btn btn-sm btn-outline-secondary"> <a href="{% url 'profile' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Profile <i class="fas fa-arrow-left"></i> Back to Profile
</a> </a>
</div> </div>
</div> </div>
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0">Change Your Password</h5> <h5 class="card-title mb-0">Change Your Password</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %} {{ form|crispy }}
{{ form|crispy }} <div class="d-grid gap-2">
<div class="d-grid gap-2"> <button type="submit" class="btn btn-primary">Change Password</button>
<button type="submit" class="btn btn-primary">Change Password</button> </div>
</div> </form>
</form> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,44 +1,38 @@
<!-- templates/accounts/password_change_done.html --> <!-- templates/accounts/password_change_done.html -->
{% extends 'base.html' %} {% extends 'base.html' %} {% block title %}Password Changed | Chat Analytics{% endblock %}
{% block title %}Password Changed | Chat Analytics{% endblock %}
{% block content %} {% block content %}
<div <div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom" 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> <h1 class="h2">Password Changed</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'profile' %}" class="btn btn-sm btn-outline-secondary"> <a href="{% url 'profile' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Profile <i class="fas fa-arrow-left"></i> Back to Profile
</a> </a>
</div> </div>
</div> </div>
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<div class="card"> <div class="card">
<div class="card-header bg-success text-white"> <div class="card-header bg-success text-white">
<h5 class="card-title mb-0">Password Changed Successfully</h5> <h5 class="card-title mb-0">Password Changed Successfully</h5>
</div> </div>
<div class="card-body text-center"> <div class="card-body text-center">
<div class="mb-4"> <div class="mb-4">
<i class="fas fa-check-circle fa-4x text-success mb-3"></i> <i class="fas fa-check-circle fa-4x text-success mb-3"></i>
<h4>Your password has been changed successfully!</h4> <h4>Your password has been changed successfully!</h4>
<p> <p>Your new password is now active. You can use it the next time you log in.</p>
Your new password is now active. You can use it the next time you log </div>
in.
</p>
</div>
<div class="mt-4"> <div class="mt-4">
<a href="{% url 'profile' %}" class="btn btn-primary">Return to Profile</a> <a href="{% url 'profile' %}" class="btn btn-primary">Return to Profile</a>
<a href="{% url 'dashboard' %}" class="btn btn-outline-secondary ms-2" <a href="{% url 'dashboard' %}" class="btn btn-outline-secondary ms-2"
>Go to Dashboard</a >Go to Dashboard</a
> >
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,213 +1,187 @@
<!-- templates/accounts/profile.html --> <!-- templates/accounts/profile.html -->
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}My Profile | Chat Analytics{% endblock %} {% block title %}My Profile | Chat Analytics{% endblock %}
{% block content %} {% block content %}
<div <div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom" 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> <h1 class="h2">My Profile</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary"> <a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Dashboard <i class="fas fa-arrow-left"></i> Back to Dashboard
</a> </a>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0">Account Information</h5> <h5 class="card-title mb-0">Account Information</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-4 fw-bold">Username:</div> <div class="col-md-4 fw-bold">Username:</div>
<div class="col-md-8">{{ user.username }}</div> <div class="col-md-8">{{ user.username }}</div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-4 fw-bold">Email:</div> <div class="col-md-4 fw-bold">Email:</div>
<div class="col-md-8">{{ user.email }}</div> <div class="col-md-8">{{ user.email }}</div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-4 fw-bold">Company:</div> <div class="col-md-4 fw-bold">Company:</div>
<div class="col-md-8"> <div class="col-md-8">
{% if user.company %} {% if user.company %}
{{ user.company.name }} {{ user.company.name }}
{% else %} {% else %}
<span class="text-muted">Not assigned to a company</span> <span class="text-muted">Not assigned to a company</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-4 fw-bold">Role:</div> <div class="col-md-4 fw-bold">Role:</div>
<div class="col-md-8"> <div class="col-md-8">
{% if user.is_staff %} {% if user.is_staff %}
<span class="badge bg-danger">Admin</span> <span class="badge bg-danger">Admin</span>
{% elif user.is_company_admin %} {% elif user.is_company_admin %}
<span class="badge bg-primary">Company Admin</span> <span class="badge bg-primary">Company Admin</span>
{% else %} {% else %}
<span class="badge bg-secondary">User</span> <span class="badge bg-secondary">User</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-4 fw-bold">Last Login:</div> <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 class="col-md-8">{{ user.last_login|date:"F d, Y H:i" }}</div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-4 fw-bold">Date Joined:</div> <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 class="col-md-8">{{ user.date_joined|date:"F d, Y H:i" }}</div>
</div> </div>
</div> </div>
<div class="card-footer"> <div class="card-footer">
<a href="{% url 'password_change' %}" class="btn btn-primary" <a href="{% url 'password_change' %}" class="btn btn-primary">Change Password</a>
>Change Password</a </div>
> </div>
</div> </div>
</div>
</div>
{% if user.company %} {% if user.company %}
<div class="col-md-6"> <div class="col-md-6">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0">Company Information</h5> <h5 class="card-title mb-0">Company Information</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-4 fw-bold">Company Name:</div> <div class="col-md-4 fw-bold">Company Name:</div>
<div class="col-md-8">{{ user.company.name }}</div> <div class="col-md-8">{{ user.company.name }}</div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-4 fw-bold">Description:</div> <div class="col-md-4 fw-bold">Description:</div>
<div class="col-md-8"> <div class="col-md-8">
{{ user.company.description|default:"No description available." }} {{ user.company.description|default:"No description available." }}
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-4 fw-bold">Created:</div> <div class="col-md-4 fw-bold">Created:</div>
<div class="col-md-8">{{ user.company.created_at|date:"F d, Y" }}</div> <div class="col-md-8">{{ user.company.created_at|date:"F d, Y" }}</div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-4 fw-bold">Total Employees:</div> <div class="col-md-4 fw-bold">Total Employees:</div>
<div class="col-md-8">{{ user.company.employees.count }}</div> <div class="col-md-8">{{ user.company.employees.count }}</div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-4 fw-bold">Data Sources:</div> <div class="col-md-4 fw-bold">Data Sources:</div>
<div class="col-md-8">{{ user.company.data_sources.count }}</div> <div class="col-md-8">{{ user.company.data_sources.count }}</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% if user.is_company_admin or user.is_staff %} {% if user.is_company_admin or user.is_staff %}
<div class="row mt-4"> <div class="row mt-4">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0">Admin Actions</h5> <h5 class="card-title mb-0">Admin Actions</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
{% if user.is_staff %} {% if user.is_staff %}
<div class="col-md-4 mb-3"> <div class="col-md-4 mb-3">
<div class="card h-100"> <div class="card h-100">
<div class="card-body text-center"> <div class="card-body text-center">
<h5 class="card-title">Manage Users</h5> <h5 class="card-title">Manage Users</h5>
<p class="card-text"> <p class="card-text">Manage users and assign them to companies.</p>
Manage users and assign them to companies. <a
</p> href="{% url 'admin:accounts_customuser_changelist' %}"
<a class="btn btn-primary"
href="{% url 'admin:accounts_customuser_changelist' %}" >Manage Users</a
class="btn btn-primary" >
>Manage Users</a </div>
> </div>
</div> </div>
</div> <div class="col-md-4 mb-3">
</div> <div class="card h-100">
<div class="col-md-4 mb-3"> <div class="card-body text-center">
<div class="card h-100"> <h5 class="card-title">Manage Companies</h5>
<div class="card-body text-center"> <p class="card-text">Create and edit companies in the system.</p>
<h5 class="card-title">Manage Companies</h5> <a
<p class="card-text"> href="{% url 'admin:accounts_company_changelist' %}"
Create and edit companies in the system. class="btn btn-primary"
</p> >Manage Companies</a
<a >
href="{% url 'admin:accounts_company_changelist' %}" </div>
class="btn btn-primary" </div>
>Manage Companies</a </div>
> <div class="col-md-4 mb-3">
</div> <div class="card h-100">
</div> <div class="card-body text-center">
</div> <h5 class="card-title">Admin Dashboard</h5>
<div class="col-md-4 mb-3"> <p class="card-text">Go to the full admin dashboard.</p>
<div class="card h-100"> <a href="{% url 'admin:index' %}" class="btn btn-primary">Admin Dashboard</a>
<div class="card-body text-center"> </div>
<h5 class="card-title">Admin Dashboard</h5> </div>
<p class="card-text">Go to the full admin dashboard.</p> </div>
<a {% elif user.is_company_admin %}
href="{% url 'admin:index' %}" <div class="col-md-4 mb-3">
class="btn btn-primary" <div class="card h-100">
>Admin Dashboard</a <div class="card-body text-center">
> <h5 class="card-title">Manage Dashboards</h5>
</div> <p class="card-text">Create and edit dashboards for your company.</p>
</div> <a href="{% url 'create_dashboard' %}" class="btn btn-primary"
</div> >Manage Dashboards</a
{% elif user.is_company_admin %} >
<div class="col-md-4 mb-3"> </div>
<div class="card h-100"> </div>
<div class="card-body text-center"> </div>
<h5 class="card-title">Manage Dashboards</h5> <div class="col-md-4 mb-3">
<p class="card-text"> <div class="card h-100">
Create and edit dashboards for your company. <div class="card-body text-center">
</p> <h5 class="card-title">Upload Data</h5>
<a <p class="card-text">Upload and manage data sources for analysis.</p>
href="{% url 'create_dashboard' %}" <a href="{% url 'upload_data' %}" class="btn btn-primary">Upload Data</a>
class="btn btn-primary" </div>
>Manage Dashboards</a </div>
> </div>
</div> <div class="col-md-4 mb-3">
</div> <div class="card h-100">
</div> <div class="card-body text-center">
<div class="col-md-4 mb-3"> <h5 class="card-title">Search Sessions</h5>
<div class="card h-100"> <p class="card-text">Search and analyze chat sessions.</p>
<div class="card-body text-center"> <a href="{% url 'search_chat_sessions' %}" class="btn btn-primary"
<h5 class="card-title">Upload Data</h5> >Search Sessions</a
<p class="card-text"> >
Upload and manage data sources for analysis. </div>
</p> </div>
<a </div>
href="{% url 'upload_data' %}" {% endif %}
class="btn btn-primary" </div>
>Upload Data</a </div>
> </div>
</div> </div>
</div> </div>
</div> {% endif %}
<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 %} {% endblock %}

View File

@ -1,31 +1,27 @@
<!-- templates/accounts/register.html --> <!-- templates/accounts/register.html -->
{% extends 'base.html' %} {% extends 'base.html' %} {% load crispy_forms_tags %}
{% load crispy_forms_tags %} {% block title %}
Register | Chat Analytics
{% block title %}Register | Chat Analytics{% endblock %} {% endblock %}
{% block content %}
{% block content %} <div class="row justify-content-center">
<div class="row justify-content-center"> <div class="col-md-6">
<div class="col-md-6"> <div class="card mt-4">
<div class="card mt-4"> <div class="card-header">
<div class="card-header"> <h4 class="card-title mb-0">Register</h4>
<h4 class="card-title mb-0">Register</h4> </div>
</div> <div class="card-body">
<div class="card-body"> <form method="post">
<form method="post"> {% csrf_token %} {{ form|crispy }}
{% csrf_token %} <div class="d-grid gap-2">
{{ form|crispy }} <button type="submit" class="btn btn-primary">Register</button>
<div class="d-grid gap-2"> </div>
<button type="submit" class="btn btn-primary">Register</button> </form>
</div> </div>
</form> <div class="card-footer text-center">
</div> <p class="mb-0">Already have an account? <a href="{% url 'login' %}">Login</a></p>
<div class="card-footer text-center"> </div>
<p class="mb-0"> </div>
Already have an account? <a href="{% url 'login' %}">Login</a> </div>
</p> </div>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -2,339 +2,315 @@
{% load static %} {% load static %}
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}Chat Analytics Dashboard{% endblock %}</title> <title>{% block title %}Chat Analytics Dashboard{% endblock %}</title>
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
<link <link
href="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css" href="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css"
rel="stylesheet" rel="stylesheet"
/> />
<!-- Font Awesome --> <!-- Font Awesome -->
<link <link
rel="stylesheet" rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@latest/css/all.min.css" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@latest/css/all.min.css"
/> />
<!-- Plotly.js --> <!-- Plotly.js -->
<script <script
src="https://cdn.jsdelivr.net/npm/plotly.js@latest/dist/plotly.min.js" src="https://cdn.jsdelivr.net/npm/plotly.js@latest/dist/plotly.min.js"
charset="utf-8" charset="utf-8"
></script> ></script>
<!-- Custom CSS --> <!-- Custom CSS -->
<link rel="stylesheet" href="{% static 'css/style.css' %}" /> <link rel="stylesheet" href="{% static 'css/style.css' %}" />
<link rel="stylesheet" href="{% static 'css/dashboard.css' %}" /> <link rel="stylesheet" href="{% static 'css/dashboard.css' %}" />
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
</head> </head>
<body> <body>
<!-- Navbar --> <!-- Navbar -->
<nav class="navbar navbar-expand-md navbar-dark bg-dark absolute-top"> <nav class="navbar navbar-expand-md navbar-dark bg-dark absolute-top">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="{% url 'dashboard' %}">Chat Analytics</a> <a class="navbar-brand" href="{% url 'dashboard' %}">Chat Analytics</a>
<button <button
class="navbar-toggler" class="navbar-toggler"
type="button" type="button"
data-bs-toggle="collapse" data-bs-toggle="collapse"
data-bs-target="#navbarCollapse" data-bs-target="#navbarCollapse"
> >
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarCollapse"> <div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-md-0"> <ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
href="{% url 'dashboard' %}" href="{% url 'dashboard' %}"
>Dashboard</a >Dashboard</a
> >
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}"
href="{% url 'upload_data' %}" href="{% url 'upload_data' %}"
>Upload Data</a >Upload Data</a
> >
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}"
href="{% url 'search_chat_sessions' %}" href="{% url 'search_chat_sessions' %}"
>Search</a >Search</a
> >
</li> </li>
</ul> </ul>
<div class="d-flex"> <div class="d-flex">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="dropdown"> <div class="dropdown">
<button <button
class="btn btn-outline-light dropdown-toggle" class="btn btn-outline-light dropdown-toggle"
type="button" type="button"
id="userDropdown" id="userDropdown"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-expanded="false" aria-expanded="false"
> >
{% if user.company %} {% if user.company %}
<span class="badge bg-info me-1" <span class="badge bg-info me-1">{{ user.company.name }}</span>
>{{ user.company.name }}</span {% endif %}
> {{ user.username }}
{% endif %} </button>
{{ user.username }} <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
</button> <li>
<ul <a class="dropdown-item ajax-nav-link" href="{% url 'profile' %}">Profile</a>
class="dropdown-menu dropdown-menu-end" </li>
aria-labelledby="userDropdown" {% if user.is_staff %}
> <li>
<li> <a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a>
<a </li>
class="dropdown-item ajax-nav-link" {% endif %}
href="{% url 'profile' %}" <li>
>Profile</a <hr class="dropdown-divider" />
> </li>
</li> <li>
{% if user.is_staff %} <a class="dropdown-item" href="{% url 'logout' %}">Logout</a>
<li> </li>
<a class="dropdown-item" href="{% url 'admin:index' %}" </ul>
>Admin</a </div>
> {% else %}
</li> <a href="{% url 'login' %}" class="btn btn-outline-light me-2">Login</a>
{% endif %} <a href="{% url 'register' %}" class="btn btn-light">Register</a>
<li> {% endif %}
<hr class="dropdown-divider" /> </div>
</li> </div>
<li> </div>
<a class="dropdown-item" href="{% url 'logout' %}" </nav>
>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="container-fluid">
<div class="row"> <div class="row">
<!-- Sidebar --> <!-- Sidebar -->
<nav <nav
id="sidebarMenu" id="sidebarMenu"
class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse sticky-top h-100 p-0" 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"> <div class="sidebar-sticky pt-3">
{% block sidebar %} {% block sidebar %}
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
href="{% url 'dashboard' %}" href="{% url 'dashboard' %}"
> >
<i class="fas fa-tachometer-alt me-2"></i> <i class="fas fa-tachometer-alt me-2"></i>
Dashboard Dashboard
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}"
href="{% url 'upload_data' %}" href="{% url 'upload_data' %}"
> >
<i class="fas fa-upload me-2"></i> <i class="fas fa-upload me-2"></i>
Upload Data Upload Data
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}"
href="{% url 'search_chat_sessions' %}" href="{% url 'search_chat_sessions' %}"
> >
<i class="fas fa-search me-2"></i> <i class="fas fa-search me-2"></i>
Search Search
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'data_view' %}active{% endif %}" class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'data_view' %}active{% endif %}"
href="{% url 'data_view' %}" href="{% url 'data_view' %}"
> >
<i class="fas fa-table me-2"></i> <i class="fas fa-table me-2"></i>
Data View Data View
</a> </a>
</li> </li>
{% if user.is_authenticated and user.company %} {% if user.is_authenticated and user.company %}
{% if dashboards %} {% if dashboards %}
<li class="nav-header"><strong>Dashboards</strong></li> <li class="nav-header"><strong>Dashboards</strong></li>
{% for dashboard in dashboards %} {% for dashboard in dashboards %}
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link {% if selected_dashboard.id == dashboard.id %}active{% endif %}" class="nav-link {% if selected_dashboard.id == dashboard.id %}active{% endif %}"
href="{% url 'dashboard' %}?dashboard_id={{ dashboard.id }}" href="{% url 'dashboard' %}?dashboard_id={{ dashboard.id }}"
> >
<i class="fas fa-chart-line me-2"></i> <i class="fas fa-chart-line me-2"></i>
{{ dashboard.name }} {{ dashboard.name }}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'create_dashboard' %}"> <a class="nav-link" href="{% url 'create_dashboard' %}">
<i class="fas fa-plus-circle me-2"></i> <i class="fas fa-plus-circle me-2"></i>
New Dashboard New Dashboard
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% if data_sources %} {% if data_sources %}
<li class="nav-header"><strong>Data Sources</strong></li> <li class="nav-header"><strong>Data Sources</strong></li>
{% for data_source in data_sources %} {% for data_source in data_sources %}
<li class="nav-item"> <li class="nav-item">
<a <a class="nav-link" href="{% url 'data_source_detail' data_source.id %}">
class="nav-link" <i class="fas fa-database me-2"></i>
href="{% url 'data_source_detail' data_source.id %}" {{ data_source.name }}
> </a>
<i class="fas fa-database me-2"></i> </li>
{{ data_source.name }} {% endfor %}
</a> {% endif %}
</li> {% endif %}
{% endfor %} </ul>
{% endif %} {% endblock %}
{% endif %} </div>
</ul> </nav>
{% endblock %}
</div>
</nav>
<!-- Main content --> <!-- Main content -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content"> <main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
{# {% if messages %} #} {# {% if messages %} #}
{# <div class="messages mt-3"> #} {# <div class="messages mt-3"> #}
{# {% for message in messages %} #} {# {% 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"> #} {# <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 }} #} {# {{ message }} #}
{# <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> #} {# <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> #}
{# </div> #} {# </div> #}
{# {% endfor %} #} {# {% endfor %} #}
{# </div> #} {# </div> #}
{# {% endif %} #} {# {% endif %} #}
<div id="main-content"> <div id="main-content">{% block content %}{% endblock %}</div>
{% block content %} </main>
{% endblock %} </div>
</div> </div>
</main>
</div>
</div>
<footer> <footer>
<div class="container"> <div class="container">
<p>&copy; {% now "Y" %} KJANAT All rights reserved. | Chat Analytics Dashboard.</p> <p>&copy; {% now "Y" %} KJANAT All rights reserved. | Chat Analytics Dashboard.</p>
</div> </div>
</footer> </footer>
<!-- Bootstrap JS --> <!-- Bootstrap JS -->
<script <script
src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js" src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"
crossorigin="anonymous" crossorigin="anonymous"
></script> ></script>
<!-- jQuery (for Ajax) --> <!-- jQuery (for Ajax) -->
<script <script
src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js" src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"
crossorigin="anonymous" crossorigin="anonymous"
></script> ></script>
<!-- Custom JavaScript --> <!-- Custom JavaScript -->
<script src="{% static 'js/main.js' %}"></script> <script src="{% static 'js/main.js' %}"></script>
<script src="{% static 'js/ajax-pagination.js' %}"></script> <script src="{% static 'js/ajax-pagination.js' %}"></script>
<script src="{% static 'js/ajax-navigation.js' %}"></script> <script src="{% static 'js/ajax-navigation.js' %}"></script>
<!-- Enable AJAX Navigation --> <!-- Enable AJAX Navigation -->
<script> <script>
// Enable AJAX navigation for the entire application // Enable AJAX navigation for the entire application
var ENABLE_AJAX_NAVIGATION = true; var ENABLE_AJAX_NAVIGATION = true;
</script> </script>
<!-- Check if Plotly loaded successfully --> <!-- Check if Plotly loaded successfully -->
<script> <script>
if (typeof Plotly === "undefined") { if (typeof Plotly === "undefined") {
console.error("Plotly library failed to load. Will attempt to load fallback."); console.error("Plotly library failed to load. Will attempt to load fallback.");
// Try to load Plotly from alternative source // Try to load Plotly from alternative source
const script = document.createElement("script"); const script = document.createElement("script");
script.src = "https://cdn.jsdelivr.net/npm/plotly.js@latest/dist/plotly.min.js"; script.src = "https://cdn.jsdelivr.net/npm/plotly.js@latest/dist/plotly.min.js";
script.async = true; script.async = true;
script.crossOrigin = "anonymous"; script.crossOrigin = "anonymous";
document.head.appendChild(script); document.head.appendChild(script);
} }
</script> </script>
{% block extra_js %} {% block extra_js %}
{{ block.super }} {{ block.super }}
{% if messages %} {% if messages %}
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1100;"> <div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1100">
<!-- Toasts will be appended here --> <!-- Toasts will be appended here -->
</div> </div>
{% for message in messages %} {% for message in messages %}
<!-- Pre-render message data that will be used by JavaScript --> <!-- Pre-render message data that will be used by JavaScript -->
<script type="application/json" id="message-data-{{ forloop.counter }}"> <script type="application/json" id="message-data-{{ forloop.counter }}">
{ {
"message": "{{ message|escapejs }}", "message": "{{ message|escapejs }}",
"tags": "{{ message.tags|default:'' }}" "tags": "{{ message.tags|default:'' }}"
} }
</script> </script>
{% endfor %} {% endfor %}
<script> <script>
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const toastContainer = document.querySelector(".toast-container"); const toastContainer = document.querySelector(".toast-container");
if (!toastContainer) return; if (!toastContainer) return;
// Find all message data elements // Find all message data elements
const messageDataElements = document.querySelectorAll( const messageDataElements = document.querySelectorAll('script[id^="message-data-"]');
'script[id^="message-data-"]', messageDataElements.forEach(function (dataElement) {
); try {
messageDataElements.forEach(function (dataElement) { const messageData = JSON.parse(dataElement.textContent);
try { createToast(messageData.message, messageData.tags);
const messageData = JSON.parse(dataElement.textContent); } catch (e) {
createToast(messageData.message, messageData.tags); console.error("Error parsing message data:", e);
} catch (e) { }
console.error("Error parsing message data:", e); });
}
});
function createToast(messageText, messageTags) { function createToast(messageText, messageTags) {
let toastClass = ""; let toastClass = "";
let autohide = true; let autohide = true;
let delay = 5000; let delay = 5000;
if (messageTags.includes("debug")) { if (messageTags.includes("debug")) {
toastClass = "bg-secondary text-white"; toastClass = "bg-secondary text-white";
} else if (messageTags.includes("info")) { } else if (messageTags.includes("info")) {
toastClass = "bg-info text-dark"; toastClass = "bg-info text-dark";
} else if (messageTags.includes("success")) { } else if (messageTags.includes("success")) {
toastClass = "bg-success text-white"; toastClass = "bg-success text-white";
} else if (messageTags.includes("warning")) { } else if (messageTags.includes("warning")) {
toastClass = "bg-warning text-dark"; toastClass = "bg-warning text-dark";
autohide = false; autohide = false;
} else if (messageTags.includes("error")) { } else if (messageTags.includes("error")) {
toastClass = "bg-danger text-white"; toastClass = "bg-danger text-white";
autohide = false; autohide = false;
} else { } else {
toastClass = "bg-light text-dark"; toastClass = "bg-light text-dark";
} }
const toastId = const toastId =
"toast-" + "toast-" + Date.now() + "-" + Math.random().toString(36).substring(2, 11);
Date.now() + const toastHtml = `
"-" +
Math.random().toString(36).substring(2, 11);
const toastHtml = `
<div id="${toastId}" class="toast ${toastClass}" role="alert" aria-live="assertive" aria-atomic="true"> <div id="${toastId}" class="toast ${toastClass}" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header"> <div class="toast-header">
<strong class="me-auto">Notification</strong> <strong class="me-auto">Notification</strong>
@ -346,25 +322,25 @@
</div> </div>
</div>`; </div>`;
toastContainer.insertAdjacentHTML("beforeend", toastHtml); toastContainer.insertAdjacentHTML("beforeend", toastHtml);
const toastElement = document.getElementById(toastId); const toastElement = document.getElementById(toastId);
if (toastElement) { if (toastElement) {
const toast = new bootstrap.Toast(toastElement, { const toast = new bootstrap.Toast(toastElement, {
autohide: autohide, autohide: autohide,
delay: autohide ? delay : undefined, delay: autohide ? delay : undefined,
}); });
toastElement.addEventListener("hidden.bs.toast", function () { toastElement.addEventListener("hidden.bs.toast", function () {
toastElement.remove(); toastElement.remove();
}); });
toast.show(); toast.show();
} }
} }
}); });
</script> </script>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
</body> </body>
</html> </html>

View File

@ -1,145 +1,133 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}
{% block title %}Chat Session {{ session.session_id }} | Chat Analytics{% endblock %} Chat Session {{ session.session_id }} | Chat Analytics
{% endblock %}
{% block content %} {% block content %}
<div <div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom" 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> <h1 class="h2">Chat Session: {{ session.session_id }}</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
<a <a
href="{% url 'data_source_detail' session.data_source.id %}" href="{% url 'data_source_detail' session.data_source.id %}"
class="btn btn-sm btn-outline-secondary" class="btn btn-sm btn-outline-secondary"
> >
<i class="fas fa-arrow-left"></i> Back to Data Source <i class="fas fa-arrow-left"></i> Back to Data Source
</a> </a>
</div> </div>
</div> </div>
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-8"> <div class="col-md-8">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0">Session Information</h5> <h5 class="card-title mb-0">Session Information</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<p><strong>Session ID:</strong> {{ session.session_id }}</p> <p><strong>Session ID:</strong> {{ session.session_id }}</p>
<p> <p>
<strong>Start Time:</strong> <strong>Start Time:</strong>
{{ session.start_time|date:"F d, Y H:i" }} {{ session.start_time|date:"F d, Y H:i" }}
</p> </p>
<p> <p><strong>End Time:</strong> {{ session.end_time|date:"F d, Y H:i" }}</p>
<strong>End Time:</strong> {{ session.end_time|date:"F d, Y H:i" }} <p><strong>IP Address:</strong> {{ session.ip_address|default:"N/A" }}</p>
</p> <p><strong>Country:</strong> {{ session.country|default:"N/A" }}</p>
<p> <p><strong>Language:</strong> {{ session.language|default:"N/A" }}</p>
<strong>IP Address:</strong> {{ session.ip_address|default:"N/A" }} </div>
</p> <div class="col-md-6">
<p><strong>Country:</strong> {{ session.country|default:"N/A" }}</p> <p><strong>Messages Sent:</strong> {{ session.messages_sent }}</p>
<p><strong>Language:</strong> {{ session.language|default:"N/A" }}</p> <p>
</div> <strong>Average Response Time:</strong>
<div class="col-md-6"> {{ session.avg_response_time|floatformat:2 }}s
<p><strong>Messages Sent:</strong> {{ session.messages_sent }}</p> </p>
<p> <p><strong>Tokens:</strong> {{ session.tokens }}</p>
<strong>Average Response Time:</strong> <p><strong>Token Cost:</strong> €{{ session.tokens_eur|floatformat:2 }}</p>
{{ session.avg_response_time|floatformat:2 }}s <p><strong>Category:</strong> {{ session.category|default:"N/A" }}</p>
</p> <p>
<p><strong>Tokens:</strong> {{ session.tokens }}</p> <strong>Sentiment:</strong>
<p> {% if session.sentiment %}
<strong>Token Cost:</strong> €{{ session.tokens_eur|floatformat:2 }} {% if 'positive' in session.sentiment|lower %}
</p> <span class="badge bg-success">{{ session.sentiment }}</span>
<p><strong>Category:</strong> {{ session.category|default:"N/A" }}</p> {% elif 'negative' in session.sentiment|lower %}
<p> <span class="badge bg-danger">{{ session.sentiment }}</span>
<strong>Sentiment:</strong> {% elif 'neutral' in session.sentiment|lower %}
{% if session.sentiment %} <span class="badge bg-warning">{{ session.sentiment }}</span>
{% if 'positive' in session.sentiment|lower %} {% else %}
<span class="badge bg-success" <span class="badge bg-secondary">{{ session.sentiment }}</span>
>{{ session.sentiment }}</span {% endif %}
> {% else %}
{% elif 'negative' in session.sentiment|lower %} <span class="text-muted">N/A</span>
<span class="badge bg-danger">{{ session.sentiment }}</span> {% endif %}
{% elif 'neutral' in session.sentiment|lower %} </p>
<span class="badge bg-warning" </div>
>{{ session.sentiment }}</span </div>
> <div class="row mt-3">
{% else %} <div class="col-12">
<span class="badge bg-secondary" <p class="mb-2"><strong>Initial Message:</strong></p>
>{{ session.sentiment }}</span <div class="card bg-light">
> <div class="card-body">
{% endif %} <p class="mb-0">{{ session.initial_msg|default:"N/A" }}</p>
{% else %} </div>
<span class="text-muted">N/A</span> </div>
{% endif %} </div>
</p> </div>
</div> </div>
</div> </div>
<div class="row mt-3"> </div>
<div class="col-12"> <div class="col-md-4">
<p class="mb-2"><strong>Initial Message:</strong></p> <div class="card">
<div class="card bg-light"> <div class="card-header">
<div class="card-body"> <h5 class="card-title mb-0">Additional Info</h5>
<p class="mb-0">{{ session.initial_msg|default:"N/A" }}</p> </div>
</div> <div class="card-body">
</div> <p>
</div> <strong>Escalated:</strong> {% if session.escalated %}
</div> <span class="badge bg-danger">Yes</span>
</div> {% else %}
</div> <span class="badge bg-success">No</span>
</div> {% endif %}
<div class="col-md-4"> </p>
<div class="card"> <p>
<div class="card-header"> <strong>Forwarded to HR:</strong> {% if session.forwarded_hr %}
<h5 class="card-title mb-0">Additional Info</h5> <span class="badge bg-danger">Yes</span>
</div> {% else %}
<div class="card-body"> <span class="badge bg-success">No</span>
<p> {% endif %}
<strong>Escalated:</strong> {% if session.escalated %} </p>
<span class="badge bg-danger">Yes</span> <p><strong>User Rating:</strong> {{ session.user_rating|default:"N/A" }}</p>
{% else %} <hr />
<span class="badge bg-success">No</span> <p>
{% endif %} <strong>Data Source:</strong>
</p> <a href="{% url 'data_source_detail' session.data_source.id %}"
<p> >{{ session.data_source.name }}</a
<strong>Forwarded to HR:</strong> {% if session.forwarded_hr %} >
<span class="badge bg-danger">Yes</span> </p>
{% else %} <p><strong>Company:</strong> {{ session.data_source.company.name }}</p>
<span class="badge bg-success">No</span> </div>
{% endif %} </div>
</p> </div>
<p><strong>User Rating:</strong> {{ session.user_rating|default:"N/A" }}</p> </div>
<hr />
<p> <div class="row">
<strong>Data Source:</strong> <div class="col-12">
<a href="{% url 'data_source_detail' session.data_source.id %}" <div class="card">
>{{ session.data_source.name }}</a <div class="card-header">
> <h5 class="card-title mb-0">Full Transcript</h5>
</p> </div>
<p><strong>Company:</strong> {{ session.data_source.company.name }}</p> <div class="card-body">
</div> {% if session.full_transcript %}
</div> <div class="chat-transcript" style="max-height: 500px; overflow-y: auto">
</div> <pre style="white-space: pre-wrap; font-family: inherit">
</div> {{ session.full_transcript }}</pre
>
<div class="row"> </div>
<div class="col-12"> {% else %}
<div class="card"> <p class="text-center text-muted">No transcript available.</p>
<div class="card-header"> {% endif %}
<h5 class="card-title mb-0">Full Transcript</h5> </div>
</div> </div>
<div class="card-body"> </div>
{% if session.full_transcript %} </div>
<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 %} {% endblock %}

View File

@ -1,329 +1,320 @@
<!-- templates/dashboard/dashboard.html --> <!-- templates/dashboard/dashboard.html -->
{% extends 'base.html' %} {% extends 'base.html' %} {% load static %}
{% load static %} {% block title %}
Dashboard | Chat Analytics
{% block title %}Dashboard | Chat Analytics{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div <div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom" 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> <h1 class="h2">{{ selected_dashboard.name }}</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2"> <div class="btn-group me-2">
<a <a
href="{% url 'edit_dashboard' selected_dashboard.id %}" href="{% url 'edit_dashboard' selected_dashboard.id %}"
class="btn btn-sm btn-outline-secondary" class="btn btn-sm btn-outline-secondary"
> >
<i class="fas fa-edit"></i> Edit <i class="fas fa-edit"></i> Edit
</a> </a>
<a <a
href="{% url 'delete_dashboard' selected_dashboard.id %}" href="{% url 'delete_dashboard' selected_dashboard.id %}"
class="btn btn-sm btn-outline-danger" class="btn btn-sm btn-outline-danger"
> >
<i class="fas fa-trash"></i> Delete <i class="fas fa-trash"></i> Delete
</a> </a>
<a <a
href="{% url 'export_chats_csv' %}?dashboard_id={{ selected_dashboard.id }}" href="{% url 'export_chats_csv' %}?dashboard_id={{ selected_dashboard.id }}"
class="btn btn-sm btn-outline-success" class="btn btn-sm btn-outline-success"
> >
<i class="fas fa-file-csv"></i> Export CSV <i class="fas fa-file-csv"></i> Export CSV
</a> </a>
</div> </div>
<div class="dropdown"> <div class="dropdown">
<button <button
class="btn btn-sm btn-outline-primary dropdown-toggle" class="btn btn-sm btn-outline-primary dropdown-toggle"
type="button" type="button"
id="timeRangeDropdown" id="timeRangeDropdown"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-expanded="false" aria-expanded="false"
> >
<i class="fas fa-calendar"></i> Time Range <i class="fas fa-calendar"></i> Time Range
</button> </button>
<ul class="dropdown-menu" aria-labelledby="timeRangeDropdown"> <ul class="dropdown-menu" aria-labelledby="timeRangeDropdown">
<li> <li>
<a <a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=7"
class="dropdown-item" >Last 7 days</a
href="?dashboard_id={{ selected_dashboard.id }}&time_range=7" >
>Last 7 days</a </li>
> <li>
</li> <a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=30"
<li> >Last 30 days</a
<a >
class="dropdown-item" </li>
href="?dashboard_id={{ selected_dashboard.id }}&time_range=30" <li>
>Last 30 days</a <a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=90"
> >Last 90 days</a
</li> >
<li> </li>
<a <li>
class="dropdown-item" <a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=all"
href="?dashboard_id={{ selected_dashboard.id }}&time_range=90" >All time</a
>Last 90 days</a >
> </li>
</li> </ul>
<li> </div>
<a </div>
class="dropdown-item" </div>
href="?dashboard_id={{ selected_dashboard.id }}&time_range=all"
>All time</a
>
</li>
</ul>
</div>
</div>
</div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-3"> <div class="col-md-3">
<div class="card stats-card bg-primary text-white"> <div class="card stats-card bg-primary text-white">
<div class="card-body"> <div class="card-body">
<h6 class="card-title">Total Sessions</h6> <h6 class="card-title">Total Sessions</h6>
<h3>{{ dashboard_data.total_sessions }}</h3> <h3>{{ dashboard_data.total_sessions }}</h3>
<p>Chat conversations</p> <p>Chat conversations</p>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="card stats-card bg-success text-white"> <div class="card stats-card bg-success text-white">
<div class="card-body"> <div class="card-body">
<h6 class="card-title">Avg Response Time</h6> <h6 class="card-title">Avg Response Time</h6>
<h3>{{ dashboard_data.avg_response_time }}s</h3> <h3>{{ dashboard_data.avg_response_time }}s</h3>
<p>Average response</p> <p>Average response</p>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="card stats-card bg-info text-white"> <div class="card stats-card bg-info text-white">
<div class="card-body"> <div class="card-body">
<h6 class="card-title">Total Tokens</h6> <h6 class="card-title">Total Tokens</h6>
<h3>{{ dashboard_data.total_tokens }}</h3> <h3>{{ dashboard_data.total_tokens }}</h3>
<p>Total usage</p> <p>Total usage</p>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="card stats-card bg-warning text-white"> <div class="card stats-card bg-warning text-white">
<div class="card-body"> <div class="card-body">
<h6 class="card-title">Total Cost</h6> <h6 class="card-title">Total Cost</h6>
<h3>€{{ dashboard_data.total_cost }}</h3> <h3>€{{ dashboard_data.total_cost }}</h3>
<p>Token cost</p> <p>Token cost</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0">Sessions Over Time</h5> <h5 class="card-title mb-0">Sessions Over Time</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="sessions-time-chart" class="chart-container"></div> <div id="sessions-time-chart" class="chart-container"></div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0">Sentiment Analysis</h5> <h5 class="card-title mb-0">Sentiment Analysis</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="sentiment-chart" class="chart-container"></div> <div id="sentiment-chart" class="chart-container"></div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row mt-4"> <div class="row mt-4">
<div class="col-md-6"> <div class="col-md-6">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0">Top Countries</h5> <h5 class="card-title mb-0">Top Countries</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="country-chart" class="chart-container"></div> <div id="country-chart" class="chart-container"></div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0">Categories</h5> <h5 class="card-title mb-0">Categories</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="category-chart" class="chart-container"></div> <div id="category-chart" class="chart-container"></div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
<!-- Store the JSON data in script tags to avoid parsing issues --> <!-- Store the JSON data in script tags to avoid parsing issues -->
<script type="application/json" id="time-series-data">{{ time_series_data_json|safe }}</script> <script type="application/json" id="time-series-data">
<script type="application/json" id="sentiment-data">{{ sentiment_data_json|safe }}</script> {{ time_series_data_json|safe }}
<script type="application/json" id="country-data">{{ country_data_json|safe }}</script> </script>
<script type="application/json" id="category-data">{{ category_data_json|safe }}</script> <script type="application/json" id="sentiment-data">
<!-- prettier-ignore-end --> {{ sentiment_data_json|safe }}
</script>
<script type="application/json" id="country-data">
{{ country_data_json|safe }}
</script>
<script type="application/json" id="category-data">
{{ category_data_json|safe }}
</script>
<!-- prettier-ignore-end -->
<script> <script>
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
try { try {
// Parse the dashboard data components from script tags // Parse the dashboard data components from script tags
const timeSeriesData = JSON.parse( const timeSeriesData = JSON.parse(document.getElementById("time-series-data").textContent);
document.getElementById("time-series-data").textContent, const sentimentData = JSON.parse(document.getElementById("sentiment-data").textContent);
); const countryData = JSON.parse(document.getElementById("country-data").textContent);
const sentimentData = JSON.parse( const categoryData = JSON.parse(document.getElementById("category-data").textContent);
document.getElementById("sentiment-data").textContent,
);
const countryData = JSON.parse(document.getElementById("country-data").textContent);
const categoryData = JSON.parse(
document.getElementById("category-data").textContent,
);
console.log("Time series data loaded:", timeSeriesData); console.log("Time series data loaded:", timeSeriesData);
console.log("Sentiment data loaded:", sentimentData); console.log("Sentiment data loaded:", sentimentData);
console.log("Country data loaded:", countryData); console.log("Country data loaded:", countryData);
console.log("Category data loaded:", categoryData); console.log("Category data loaded:", categoryData);
// Sessions over time chart // Sessions over time chart
if (timeSeriesData && timeSeriesData.length > 0) { if (timeSeriesData && timeSeriesData.length > 0) {
const timeSeriesX = timeSeriesData.map((item) => item.date); const timeSeriesX = timeSeriesData.map((item) => item.date);
const timeSeriesY = timeSeriesData.map((item) => item.count); const timeSeriesY = timeSeriesData.map((item) => item.count);
Plotly.newPlot( Plotly.newPlot(
"sessions-time-chart", "sessions-time-chart",
[ [
{ {
x: timeSeriesX, x: timeSeriesX,
y: timeSeriesY, y: timeSeriesY,
type: "scatter", type: "scatter",
mode: "lines+markers", mode: "lines+markers",
line: { line: {
color: "rgb(75, 192, 192)", color: "rgb(75, 192, 192)",
width: 2, width: 2,
}, },
marker: { marker: {
color: "rgb(75, 192, 192)", color: "rgb(75, 192, 192)",
size: 6, size: 6,
}, },
}, },
], ],
{ {
margin: { t: 10, r: 10, b: 40, l: 40 }, margin: { t: 10, r: 10, b: 40, l: 40 },
xaxis: { xaxis: {
title: "Date", title: "Date",
}, },
yaxis: { yaxis: {
title: "Number of Sessions", title: "Number of Sessions",
}, },
}, },
); );
} else { } else {
document.getElementById("sessions-time-chart").innerHTML = document.getElementById("sessions-time-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>'; '<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
} }
// Sentiment analysis chart // Sentiment analysis chart
if (sentimentData && sentimentData.length > 0) { if (sentimentData && sentimentData.length > 0) {
const sentimentLabels = sentimentData.map((item) => item.sentiment); const sentimentLabels = sentimentData.map((item) => item.sentiment);
const sentimentValues = sentimentData.map((item) => item.count); const sentimentValues = sentimentData.map((item) => item.count);
const sentimentColors = sentimentLabels.map((sentiment) => { const sentimentColors = sentimentLabels.map((sentiment) => {
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)"; if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
if (sentiment.toLowerCase().includes("negative")) if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
return "rgb(255, 99, 132)"; if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)"; return "rgb(201, 203, 207)";
return "rgb(201, 203, 207)"; });
});
Plotly.newPlot( Plotly.newPlot(
"sentiment-chart", "sentiment-chart",
[ [
{ {
values: sentimentValues, values: sentimentValues,
labels: sentimentLabels, labels: sentimentLabels,
type: "pie", type: "pie",
marker: { marker: {
colors: sentimentColors, colors: sentimentColors,
}, },
hole: 0.4, hole: 0.4,
textinfo: "label+percent", textinfo: "label+percent",
insidetextorientation: "radial", insidetextorientation: "radial",
}, },
], ],
{ {
margin: { t: 10, r: 10, b: 10, l: 10 }, margin: { t: 10, r: 10, b: 10, l: 10 },
}, },
); );
} else { } else {
document.getElementById("sentiment-chart").innerHTML = document.getElementById("sentiment-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No sentiment data available</p></div>'; '<div class="text-center py-5"><p class="text-muted">No sentiment data available</p></div>';
} }
// Country chart // Country chart
if (countryData && countryData.length > 0) { if (countryData && countryData.length > 0) {
const countryLabels = countryData.map((item) => item.country); const countryLabels = countryData.map((item) => item.country);
const countryValues = countryData.map((item) => item.count); const countryValues = countryData.map((item) => item.count);
Plotly.newPlot( Plotly.newPlot(
"country-chart", "country-chart",
[ [
{ {
x: countryValues, x: countryValues,
y: countryLabels, y: countryLabels,
type: "bar", type: "bar",
orientation: "h", orientation: "h",
marker: { marker: {
color: "rgb(54, 162, 235)", color: "rgb(54, 162, 235)",
}, },
}, },
], ],
{ {
margin: { t: 10, r: 10, b: 40, l: 100 }, margin: { t: 10, r: 10, b: 40, l: 100 },
xaxis: { xaxis: {
title: "Number of Sessions", title: "Number of Sessions",
}, },
}, },
); );
} else { } else {
document.getElementById("country-chart").innerHTML = document.getElementById("country-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No country data available</p></div>'; '<div class="text-center py-5"><p class="text-muted">No country data available</p></div>';
} }
// Category chart // Category chart
if (categoryData && categoryData.length > 0) { if (categoryData && categoryData.length > 0) {
const categoryLabels = categoryData.map((item) => item.category); const categoryLabels = categoryData.map((item) => item.category);
const categoryValues = categoryData.map((item) => item.count); const categoryValues = categoryData.map((item) => item.count);
Plotly.newPlot( Plotly.newPlot(
"category-chart", "category-chart",
[ [
{ {
labels: categoryLabels, labels: categoryLabels,
values: categoryValues, values: categoryValues,
type: "pie", type: "pie",
textinfo: "label+percent", textinfo: "label+percent",
insidetextorientation: "radial", insidetextorientation: "radial",
}, },
], ],
{ {
margin: { t: 10, r: 10, b: 10, l: 10 }, margin: { t: 10, r: 10, b: 10, l: 10 },
}, },
); );
} else { } else {
document.getElementById("category-chart").innerHTML = document.getElementById("category-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No category data available</p></div>'; '<div class="text-center py-5"><p class="text-muted">No category data available</p></div>';
} }
} catch (error) { } catch (error) {
console.error("Error rendering charts:", error); console.error("Error rendering charts:", error);
document.querySelectorAll(".chart-container").forEach((container) => { document.querySelectorAll(".chart-container").forEach((container) => {
container.innerHTML = container.innerHTML =
'<div class="text-center py-5"><p class="text-danger">Error loading chart data. Please refresh the page.</p></div>'; '<div class="text-center py-5"><p class="text-danger">Error loading chart data. Please refresh the page.</p></div>';
}); });
} }
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,44 +1,40 @@
{% extends 'base.html' %} {% extends 'base.html' %} {% block title %}Delete Dashboard | Chat Analytics{% endblock %}
{% block title %}Delete Dashboard | Chat Analytics{% endblock %}
{% block content %} {% block content %}
<div <div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom" 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> <h1 class="h2">Delete Dashboard</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary"> <a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Dashboard <i class="fas fa-arrow-left"></i> Back to Dashboard
</a> </a>
</div> </div>
</div> </div>
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<div class="card border-danger"> <div class="card border-danger">
<div class="card-header bg-danger text-white"> <div class="card-header bg-danger text-white">
<h5 class="card-title mb-0">Confirm Deletion</h5> <h5 class="card-title mb-0">Confirm Deletion</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<p class="lead"> <p class="lead">
Are you sure you want to delete the dashboard Are you sure you want to delete the dashboard "<strong>{{ dashboard.name }}</strong>"?
"<strong>{{ dashboard.name }}</strong>"? </p>
</p> <p>
<p> This action cannot be undone. The dashboard will be permanently deleted, but the
This action cannot be undone. The dashboard will be permanently deleted, but underlying data sources will remain intact.
the underlying data sources will remain intact. </p>
</p>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<div class="d-flex justify-content-between mt-4"> <div class="d-flex justify-content-between mt-4">
<a href="{% url 'dashboard' %}" class="btn btn-secondary">Cancel</a> <a href="{% url 'dashboard' %}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-danger">Delete Dashboard</button> <button type="submit" class="btn btn-danger">Delete Dashboard</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,43 +1,43 @@
{% extends 'base.html' %} {% extends 'base.html' %} {% load crispy_forms_tags %}
{% load crispy_forms_tags %}
{% block title %} {% block title %}
{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %} {% if is_create %}
| Chat Analytics Create Dashboard
{% else %}
Edit Dashboard
{% endif %}
| Chat Analytics
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div <div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom" 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> <h1 class="h2">{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary"> <a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Dashboard <i class="fas fa-arrow-left"></i> Back to Dashboard
</a> </a>
</div> </div>
</div> </div>
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-8"> <div class="col-md-8">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0"> <h5 class="card-title mb-0">
{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %} {% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}
</h5> </h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %} {{ form|crispy }}
{{ form|crispy }} <div class="d-grid gap-2">
<div class="d-grid gap-2"> <button type="submit" class="btn btn-primary">
<button type="submit" class="btn btn-primary"> {% if is_create %}Create Dashboard{% else %}Update Dashboard{% endif %}
{% if is_create %}Create Dashboard{% else %}Update Dashboard{% endif %} </button>
</button> </div>
</div> </form>
</form> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,53 +1,47 @@
<!-- templates/dashboard/data_source_confirm_delete.html --> <!-- templates/dashboard/data_source_confirm_delete.html -->
{% extends 'base.html' %} {% extends 'base.html' %} {% block title %}Delete Data Source | Chat Analytics{% endblock %}
{% block title %}Delete Data Source | Chat Analytics{% endblock %}
{% block content %} {% block content %}
<div <div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom" 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> <h1 class="h2">Delete Data Source</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
<a <a
href="{% url 'data_source_detail' data_source.id %}" href="{% url 'data_source_detail' data_source.id %}"
class="btn btn-sm btn-outline-secondary" class="btn btn-sm btn-outline-secondary"
> >
<i class="fas fa-arrow-left"></i> Back to Data Source <i class="fas fa-arrow-left"></i> Back to Data Source
</a> </a>
</div> </div>
</div> </div>
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<div class="card border-danger"> <div class="card border-danger">
<div class="card-header bg-danger text-white"> <div class="card-header bg-danger text-white">
<h5 class="card-title mb-0">Confirm Deletion</h5> <h5 class="card-title mb-0">Confirm Deletion</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<p class="lead"> <p class="lead">
Are you sure you want to delete the data source Are you sure you want to delete the data source
"<strong>{{ data_source.name }}</strong>"? "<strong>{{ data_source.name }}</strong>"?
</p> </p>
<p> <p>
This action cannot be undone. The data source and all associated chat This action cannot be undone. The data source and all associated chat sessions
sessions ({{ data_source.chat_sessions.count }} sessions) will be ({{ data_source.chat_sessions.count }} sessions) will be permanently deleted.
permanently deleted. </p>
</p>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<div class="d-flex justify-content-between mt-4"> <div class="d-flex justify-content-between mt-4">
<a <a href="{% url 'data_source_detail' data_source.id %}" class="btn btn-secondary"
href="{% url 'data_source_detail' data_source.id %}" >Cancel</a
class="btn btn-secondary" >
>Cancel</a <button type="submit" class="btn btn-danger">Delete Data Source</button>
> </div>
<button type="submit" class="btn btn-danger">Delete Data Source</button> </form>
</div> </div>
</form> </div>
</div> </div>
</div> </div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,255 +1,229 @@
<!-- templates/dashboard/data_source_detail.html --> <!-- templates/dashboard/data_source_detail.html -->
{% extends 'base.html' %} {% extends 'base.html' %} {% load dashboard_extras %}
{% load dashboard_extras %} {% block title %}
{{ data_source.name }}
{% block title %}{{ data_source.name }} | Chat Analytics{% endblock %} | Chat Analytics
{% endblock %}
{% block content %} {% block content %}
<div <div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom" 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> <h1 class="h2">{{ data_source.name }}</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'upload_data' %}" class="btn btn-sm btn-outline-secondary me-2"> <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 <i class="fas fa-arrow-left"></i> Back to Data Sources
</a> </a>
<a <a
href="{% url 'export_chats_csv' %}?data_source_id={{ data_source.id }}" href="{% url 'export_chats_csv' %}?data_source_id={{ data_source.id }}"
class="btn btn-sm btn-outline-success me-2" class="btn btn-sm btn-outline-success me-2"
> >
<i class="fas fa-file-csv"></i> Export CSV <i class="fas fa-file-csv"></i> Export CSV
</a> </a>
<a <a href="{% url 'delete_data_source' data_source.id %}" class="btn btn-sm btn-outline-danger">
href="{% url 'delete_data_source' data_source.id %}" <i class="fas fa-trash"></i> Delete
class="btn btn-sm btn-outline-danger" </a>
> </div>
<i class="fas fa-trash"></i> Delete </div>
</a>
</div> <div class="row mb-4">
</div> <div class="col-md-8">
<div class="card">
<div class="row mb-4"> <div class="card-header">
<div class="col-md-8"> <h5 class="card-title mb-0">Data Source Details</h5>
<div class="card"> </div>
<div class="card-header"> <div class="card-body">
<h5 class="card-title mb-0">Data Source Details</h5> <div class="row">
</div> <div class="col-md-6">
<div class="card-body"> <p><strong>Name:</strong> {{ data_source.name }}</p>
<div class="row"> <p>
<div class="col-md-6"> <strong>Uploaded At:</strong>
<p><strong>Name:</strong> {{ data_source.name }}</p> {{ data_source.uploaded_at|date:"F d, Y H:i" }}
<p> </p>
<strong>Uploaded At:</strong> <p><strong>File:</strong> {{ data_source.file.name|split:"/"|last }}</p>
{{ data_source.uploaded_at|date:"F d, Y H:i" }} </div>
</p> <div class="col-md-6">
<p><strong>File:</strong> {{ data_source.file.name|split:"/"|last }}</p> <p><strong>Company:</strong> {{ data_source.company.name }}</p>
</div> <p><strong>Total Sessions:</strong> {{ page_obj.paginator.count }}</p>
<div class="col-md-6"> <p><strong>Description:</strong> {{ data_source.description }}</p>
<p><strong>Company:</strong> {{ data_source.company.name }}</p> </div>
<p><strong>Total Sessions:</strong> {{ page_obj.paginator.count }}</p> </div>
<p><strong>Description:</strong> {{ data_source.description }}</p> </div>
</div> </div>
</div> </div>
</div> <div class="col-md-4">
</div> <div class="card">
</div> <div class="card-header">
<div class="col-md-4"> <h5 class="card-title mb-0">Filter Sessions</h5>
<div class="card"> </div>
<div class="card-header"> <div class="card-body">
<h5 class="card-title mb-0">Filter Sessions</h5> <form method="get" action="{% url 'search_chat_sessions' %}">
</div> <div class="input-group mb-3">
<div class="card-body"> <input
<form method="get" action="{% url 'search_chat_sessions' %}"> type="text"
<div class="input-group mb-3"> name="q"
<input class="form-control"
type="text" placeholder="Search sessions..."
name="q" aria-label="Search sessions"
class="form-control" />
placeholder="Search sessions..." <input type="hidden" name="data_source_id" value="{{ data_source.id }}" />
aria-label="Search sessions" <button class="btn btn-outline-primary" type="submit">
/> <i class="fas fa-search"></i>
<input </button>
type="hidden" </div>
name="data_source_id" </form>
value="{{ data_source.id }}" </div>
/> </div>
<button class="btn btn-outline-primary" type="submit"> </div>
<i class="fas fa-search"></i> </div>
</button>
</div> <div class="row">
</form> <div class="col-12">
</div> <div class="card">
</div> <div class="card-header">
</div> <h5 class="card-title mb-0">Chat Sessions ({{ page_obj.paginator.count }})</h5>
</div> </div>
<div class="card-body">
<div class="row"> <div class="table-responsive">
<div class="col-12"> <table class="table table-striped table-hover">
<div class="card"> <thead>
<div class="card-header"> <tr>
<h5 class="card-title mb-0">Chat Sessions ({{ page_obj.paginator.count }})</h5> <th>Session ID</th>
</div> <th>Start Time</th>
<div class="card-body"> <th>Country</th>
<div class="table-responsive"> <th>Language</th>
<table class="table table-striped table-hover"> <th>Sentiment</th>
<thead> <th>Messages</th>
<tr> <th>Tokens</th>
<th>Session ID</th> <th>Category</th>
<th>Start Time</th> <th>Actions</th>
<th>Country</th> </tr>
<th>Language</th> </thead>
<th>Sentiment</th> <tbody>
<th>Messages</th> {% for session in page_obj %}
<th>Tokens</th> <tr>
<th>Category</th> <td>{{ session.session_id|truncatechars:10 }}</td>
<th>Actions</th> <td>{{ session.start_time|date:"M d, Y H:i" }}</td>
</tr> <td>{{ session.country }}</td>
</thead> <td>{{ session.language }}</td>
<tbody> <td>
{% for session in page_obj %} {% if session.sentiment %}
<tr> {% if 'positive' in session.sentiment|lower %}
<td>{{ session.session_id|truncatechars:10 }}</td> <span class="badge bg-success">{{ session.sentiment }}</span>
<td>{{ session.start_time|date:"M d, Y H:i" }}</td> {% elif 'negative' in session.sentiment|lower %}
<td>{{ session.country }}</td> <span class="badge bg-danger">{{ session.sentiment }}</span>
<td>{{ session.language }}</td> {% elif 'neutral' in session.sentiment|lower %}
<td> <span class="badge bg-warning">{{ session.sentiment }}</span>
{% if session.sentiment %} {% else %}
{% if 'positive' in session.sentiment|lower %} <span class="badge bg-secondary">{{ session.sentiment }}</span>
<span class="badge bg-success" {% endif %}
>{{ session.sentiment }}</span {% else %}
> <span class="text-muted">N/A</span>
{% elif 'negative' in session.sentiment|lower %} {% endif %}
<span class="badge bg-danger" </td>
>{{ session.sentiment }}</span <td>{{ session.messages_sent }}</td>
> <td>{{ session.tokens }}</td>
{% elif 'neutral' in session.sentiment|lower %} <td>{{ session.category|default:"N/A" }}</td>
<span class="badge bg-warning" <td>
>{{ session.sentiment }}</span {% if session.session_id %}
> <a
{% else %} href="{% url 'chat_session_detail' session.session_id %}"
<span class="badge bg-secondary" class="btn btn-sm btn-outline-primary"
>{{ session.sentiment }}</span >
> <i class="fas fa-eye"></i>
{% endif %} </a>
{% else %} {% else %}
<span class="text-muted">N/A</span> <button class="btn btn-sm btn-outline-secondary" disabled>
{% endif %} <i class="fas fa-eye-slash"></i>
</td> </button>
<td>{{ session.messages_sent }}</td> {% endif %}
<td>{{ session.tokens }}</td> </td>
<td>{{ session.category|default:"N/A" }}</td> </tr>
<td> {% empty %}
{% if session.session_id %} <tr>
<a <td colspan="9" class="text-center">No chat sessions found.</td>
href="{% url 'chat_session_detail' session.session_id %}" </tr>
class="btn btn-sm btn-outline-primary" {% endfor %}
> </tbody>
<i class="fas fa-eye"></i> </table>
</a> </div>
{% else %}
<button {% if page_obj.paginator.num_pages > 1 %}
class="btn btn-sm btn-outline-secondary" <nav aria-label="Page navigation" class="mt-4">
disabled <ul class="pagination justify-content-center">
> {% if page_obj.has_previous %}
<i class="fas fa-eye-slash"></i> <li class="page-item">
</button> <a class="page-link" href="?page=1" aria-label="First">
{% endif %} <span aria-hidden="true">&laquo;&laquo;</span>
</td> </a>
</tr> </li>
{% empty %} <li class="page-item">
<tr> <a
<td colspan="9" class="text-center"> class="page-link"
No chat sessions found. href="?page={{ page_obj.previous_page_number }}"
</td> aria-label="Previous"
</tr> >
{% endfor %} <span aria-hidden="true">&laquo;</span>
</tbody> </a>
</table> </li>
</div> {% else %}
<li class="page-item disabled">
{% if page_obj.paginator.num_pages > 1 %} <a class="page-link" href="#" aria-label="First">
<nav aria-label="Page navigation" class="mt-4"> <span aria-hidden="true">&laquo;&laquo;</span>
<ul class="pagination justify-content-center"> </a>
{% if page_obj.has_previous %} </li>
<li class="page-item"> <li class="page-item disabled">
<a class="page-link" href="?page=1" aria-label="First"> <a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;&laquo;</span> <span aria-hidden="true">&laquo;</span>
</a> </a>
</li> </li>
<li class="page-item"> {% endif %}
<a {% for num in page_obj.paginator.page_range %}
class="page-link" {% if page_obj.number == num %}
href="?page={{ page_obj.previous_page_number }}" <li class="page-item active">
aria-label="Previous" <a class="page-link" href="?page={{ num }}">{{ num }}</a>
> </li>
<span aria-hidden="true">&laquo;</span> {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
</a> <li class="page-item">
</li> <a class="page-link" href="?page={{ num }}">{{ num }}</a>
{% else %} </li>
<li class="page-item disabled"> {% endif %}
<a class="page-link" href="#" aria-label="First"> {% endfor %}
<span aria-hidden="true">&laquo;&laquo;</span> {% if page_obj.has_next %}
</a> <li class="page-item">
</li> <a
<li class="page-item disabled"> class="page-link"
<a class="page-link" href="#" aria-label="Previous"> href="?page={{ page_obj.next_page_number }}"
<span aria-hidden="true">&laquo;</span> aria-label="Next"
</a> >
</li> <span aria-hidden="true">&raquo;</span>
{% endif %} </a>
</li>
{% for num in page_obj.paginator.page_range %} <li class="page-item">
{% if page_obj.number == num %} <a
<li class="page-item active"> class="page-link"
<a class="page-link" href="?page={{ num }}" href="?page={{ page_obj.paginator.num_pages }}"
>{{ num }}</a aria-label="Last"
> >
</li> <span aria-hidden="true">&raquo;&raquo;</span>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %} </a>
<li class="page-item"> </li>
<a class="page-link" href="?page={{ num }}" {% else %}
>{{ num }}</a <li class="page-item disabled">
> <a class="page-link" href="#" aria-label="Next">
</li> <span aria-hidden="true">&raquo;</span>
{% endif %} </a>
{% endfor %} </li>
<li class="page-item disabled">
{% if page_obj.has_next %} <a class="page-link" href="#" aria-label="Last">
<li class="page-item"> <span aria-hidden="true">&raquo;&raquo;</span>
<a </a>
class="page-link" </li>
href="?page={{ page_obj.next_page_number }}" {% endif %}
aria-label="Next" </ul>
> </nav>
<span aria-hidden="true">&raquo;</span> {% endif %}
</a> </div>
</li> </div>
<li class="page-item"> </div>
<a </div>
class="page-link"
href="?page={{ page_obj.paginator.num_pages }}"
aria-label="Last"
>
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,320 +1,276 @@
<!-- templates/dashboard/data_view.html --> <!-- templates/dashboard/data_view.html -->
{% extends 'base.html' %} {% extends 'base.html' %} {% load dashboard_extras %}
{% load dashboard_extras %} {% block title %}
Data View | Chat Analytics
{% block title %}Data View | Chat Analytics{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div <div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom" 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> <h1 class="h2">Data View</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2"> <div class="btn-group me-2">
<a <a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary ajax-nav-link">
href="{% url 'dashboard' %}" <i class="fas fa-arrow-left"></i> Back to Dashboard
class="btn btn-sm btn-outline-secondary ajax-nav-link" </a>
> {% if selected_data_source %}
<i class="fas fa-arrow-left"></i> Back to Dashboard <a
</a> href="{% url 'data_source_detail' selected_data_source.id %}"
{% if selected_data_source %} class="btn btn-sm btn-outline-secondary ajax-nav-link"
<a >
href="{% url 'data_source_detail' selected_data_source.id %}" <i class="fas fa-database"></i> View Source
class="btn btn-sm btn-outline-secondary ajax-nav-link" </a>
> {% endif %}
<i class="fas fa-database"></i> View Source </div>
</a> <div class="dropdown">
{% endif %} <button
</div> class="btn btn-sm btn-outline-primary dropdown-toggle"
<div class="dropdown"> type="button"
<button id="dataViewDropdown"
class="btn btn-sm btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown"
type="button" aria-expanded="false"
id="dataViewDropdown" >
data-bs-toggle="dropdown" <i class="fas fa-filter"></i> Filter
aria-expanded="false" </button>
> <ul class="dropdown-menu" aria-labelledby="dataViewDropdown">
<i class="fas fa-filter"></i> Filter <li>
</button> <a class="dropdown-item ajax-nav-link" href="?view=all">All Sessions</a>
<ul class="dropdown-menu" aria-labelledby="dataViewDropdown"> </li>
<li> <li>
<a class="dropdown-item ajax-nav-link" href="?view=all">All Sessions</a> <a class="dropdown-item ajax-nav-link" href="?view=recent">Recent Sessions</a>
</li> </li>
<li> <li>
<a class="dropdown-item ajax-nav-link" href="?view=recent" <a class="dropdown-item ajax-nav-link" href="?view=positive">Positive Sentiment</a>
>Recent Sessions</a </li>
> <li>
</li> <a class="dropdown-item ajax-nav-link" href="?view=negative">Negative Sentiment</a>
<li> </li>
<a class="dropdown-item ajax-nav-link" href="?view=positive" <li>
>Positive Sentiment</a <a class="dropdown-item ajax-nav-link" href="?view=escalated">Escalated Sessions</a>
> </li>
</li> </ul>
<li> </div>
<a class="dropdown-item ajax-nav-link" href="?view=negative" </div>
>Negative Sentiment</a </div>
>
</li>
<li>
<a class="dropdown-item ajax-nav-link" href="?view=escalated"
>Escalated Sessions</a
>
</li>
</ul>
</div>
</div>
</div>
<!-- Data Source Selection --> <!-- Data Source Selection -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0">Data Source Selection</h5> <h5 class="card-title mb-0">Data Source Selection</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="get" class="row g-3 align-items-center filter-form"> <form method="get" class="row g-3 align-items-center filter-form">
<div class="col-md-6"> <div class="col-md-6">
<select <select name="data_source_id" class="form-select" aria-label="Select Data Source">
name="data_source_id" <option value="">All Data Sources</option>
class="form-select" {% for ds in data_sources %}
aria-label="Select Data Source" <option
> value="{{ ds.id }}"
<option value="">All Data Sources</option> {% if selected_data_source.id == ds.id %}
{% for ds in data_sources %} selected
<option {% endif %}
value="{{ ds.id }}" >
{% if selected_data_source.id == ds.id %}selected{% endif %} {{ ds.name }}
> </option>
{{ ds.name }} {% endfor %}
</option> </select>
{% endfor %} </div>
</select> <div class="col-md-4">
</div> <select name="view" class="form-select" aria-label="Select View">
<div class="col-md-4"> <option value="all" {% if view == 'all' %}selected{% endif %}>All Sessions</option>
<select name="view" class="form-select" aria-label="Select View"> <option value="recent" {% if view == 'recent' %}selected{% endif %}>
<option value="all" {% if view == 'all' %}selected{% endif %}> Recent Sessions
All Sessions </option>
</option> <option value="positive" {% if view == 'positive' %}selected{% endif %}>
<option value="recent" {% if view == 'recent' %}selected{% endif %}> Positive Sentiment
Recent Sessions </option>
</option> <option value="negative" {% if view == 'negative' %}selected{% endif %}>
<option Negative Sentiment
value="positive" </option>
{% if view == 'positive' %}selected{% endif %} <option value="escalated" {% if view == 'escalated' %}selected{% endif %}>
> Escalated Sessions
Positive Sentiment </option>
</option> </select>
<option </div>
value="negative" <div class="col-md-2">
{% if view == 'negative' %}selected{% endif %} <button type="submit" class="btn btn-primary w-100">Apply</button>
> </div>
Negative Sentiment </form>
</option> </div>
<option </div>
value="escalated" </div>
{% if view == 'escalated' %}selected{% endif %} </div>
>
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>
<!-- Export to CSV --> <!-- Export to CSV -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0">Export Data</h5> <h5 class="card-title mb-0">Export Data</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<form <form id="export-form" method="get" action="{% url 'export_chats_csv' %}" class="row g-3">
id="export-form" <!-- Pass current filters to export -->
method="get" <input type="hidden" name="data_source_id" value="{{ selected_data_source.id }}" />
action="{% url 'export_chats_csv' %}" <input type="hidden" name="view" value="{{ view }}" />
class="row g-3"
>
<!-- Pass current filters to export -->
<input
type="hidden"
name="data_source_id"
value="{{ selected_data_source.id }}"
/>
<input type="hidden" name="view" value="{{ view }}" />
<div class="col-md-3"> <div class="col-md-3">
<label for="start_date" class="form-label">Start Date</label> <label for="start_date" class="form-label">Start Date</label>
<input <input type="date" name="start_date" id="start_date" class="form-control" />
type="date" </div>
name="start_date" <div class="col-md-3">
id="start_date" <label for="end_date" class="form-label">End Date</label>
class="form-control" <input type="date" name="end_date" id="end_date" class="form-control" />
/> </div>
</div> <div class="col-md-3">
<div class="col-md-3"> <label for="country" class="form-label">Country</label>
<label for="end_date" class="form-label">End Date</label> <input
<input type="date" name="end_date" id="end_date" class="form-control" /> type="text"
</div> name="country"
<div class="col-md-3"> id="country"
<label for="country" class="form-label">Country</label> class="form-control"
<input placeholder="Country"
type="text" />
name="country" </div>
id="country" <div class="col-md-3">
class="form-control" <label for="sentiment" class="form-label">Sentiment</label>
placeholder="Country" <select name="sentiment" id="sentiment" class="form-select">
/> <option value="">All</option>
</div> <option value="positive">Positive</option>
<div class="col-md-3"> <option value="negative">Negative</option>
<label for="sentiment" class="form-label">Sentiment</label> <option value="neutral">Neutral</option>
<select name="sentiment" id="sentiment" class="form-select"> </select>
<option value="">All</option> </div>
<option value="positive">Positive</option> <div class="col-md-3">
<option value="negative">Negative</option> <label for="escalated" class="form-label">Escalated</label>
<option value="neutral">Neutral</option> <select name="escalated" id="escalated" class="form-select">
</select> <option value="">All</option>
</div> <option value="true">Yes</option>
<div class="col-md-3"> <option value="false">No</option>
<label for="escalated" class="form-label">Escalated</label> </select>
<select name="escalated" id="escalated" class="form-select"> </div>
<option value="">All</option> <div class="col-md-3 d-flex align-items-end">
<option value="true">Yes</option> <button type="submit" class="btn btn-success w-100">
<option value="false">No</option> <i class="fas fa-file-csv me-1"></i> Export to CSV
</select> </button>
</div> </div>
<div class="col-md-3 d-flex align-items-end"> </form>
<button type="submit" class="btn btn-success w-100"> </div>
<i class="fas fa-file-csv me-1"></i> Export to CSV </div>
</button> </div>
</div> </div>
</form>
</div>
</div>
</div>
</div>
<!-- Data Table --> <!-- Data Table -->
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0"> <h5 class="card-title mb-0">
Chat Sessions Chat Sessions
{% if selected_data_source %} {% if selected_data_source %}
for {{ selected_data_source.name }} for {{ selected_data_source.name }}
{% endif %} {% endif %}
{% if view != 'all' %} {% if view != 'all' %}({{ view|title }}){% endif %}
({{ view|title }}) </h5>
{% endif %} <span class="badge bg-primary">{{ page_obj.paginator.count }} sessions</span>
</h5> </div>
<span class="badge bg-primary">{{ page_obj.paginator.count }} sessions</span> <div class="card-body">
</div> <!-- Loading spinner shown during AJAX requests -->
<div class="card-body"> <div id="ajax-loading-spinner" class="text-center py-4 d-none">
<!-- Loading spinner shown during AJAX requests --> <div class="spinner-border text-primary" role="status">
<div id="ajax-loading-spinner" class="text-center py-4 d-none"> <span class="visually-hidden">Loading...</span>
<div class="spinner-border text-primary" role="status"> </div>
<span class="visually-hidden">Loading...</span> <p class="mt-2">Loading data...</p>
</div> </div>
<p class="mt-2">Loading data...</p>
</div>
<!-- Data table container that will be updated via AJAX --> <!-- Data table container that will be updated via AJAX -->
<div id="ajax-content-container"> <div id="ajax-content-container">{% include "dashboard/partials/data_table.html" %}</div>
{% include "dashboard/partials/data_table.html" %} </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
<!-- Data Summary --> <!-- Data Summary -->
{% if page_obj %} {% if page_obj %}
<div class="row mt-4"> <div class="row mt-4">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0">Summary</h5> <h5 class="card-title mb-0">Summary</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
<div class="card stats-card bg-light"> <div class="card stats-card bg-light">
<div class="card-body"> <div class="card-body">
<h6 class="card-title">Total Sessions</h6> <h6 class="card-title">Total Sessions</h6>
<h3>{{ page_obj.paginator.count }}</h3> <h3>{{ page_obj.paginator.count }}</h3>
<p>Chat conversations</p> <p>Chat conversations</p>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="card stats-card bg-light"> <div class="card stats-card bg-light">
<div class="card-body"> <div class="card-body">
<h6 class="card-title">Avg Response Time</h6> <h6 class="card-title">Avg Response Time</h6>
<h3>{{ avg_response_time|floatformat:2 }}s</h3> <h3>{{ avg_response_time|floatformat:2 }}s</h3>
<p>Average response</p> <p>Average response</p>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="card stats-card bg-light"> <div class="card stats-card bg-light">
<div class="card-body"> <div class="card-body">
<h6 class="card-title">Avg Messages</h6> <h6 class="card-title">Avg Messages</h6>
<h3>{{ avg_messages|floatformat:1 }}</h3> <h3>{{ avg_messages|floatformat:1 }}</h3>
<p>Per conversation</p> <p>Per conversation</p>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="card stats-card bg-light"> <div class="card stats-card bg-light">
<div class="card-body"> <div class="card-body">
<h6 class="card-title">Escalation Rate</h6> <h6 class="card-title">Escalation Rate</h6>
<h3>{{ escalation_rate|floatformat:1 }}%</h3> <h3>{{ escalation_rate|floatformat:1 }}%</h3>
<p>Escalated sessions</p> <p>Escalated sessions</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<script> <script>
// Function to update the summary section with new data // Function to update the summary section with new data
function updateSummary(data) { function updateSummary(data) {
if (document.querySelector(".stats-card h3:nth-of-type(1)")) { if (document.querySelector(".stats-card h3:nth-of-type(1)")) {
document.querySelector(".stats-card h3:nth-of-type(1)").textContent = document.querySelector(".stats-card h3:nth-of-type(1)").textContent =
data.page_obj.paginator.count; data.page_obj.paginator.count;
} }
if (document.querySelector(".stats-card h3:nth-of-type(2)")) { if (document.querySelector(".stats-card h3:nth-of-type(2)")) {
document.querySelector(".stats-card h3:nth-of-type(2)").textContent = document.querySelector(".stats-card h3:nth-of-type(2)").textContent =
data.avg_response_time !== null && data.avg_response_time !== undefined data.avg_response_time !== null && data.avg_response_time !== undefined
? data.avg_response_time.toFixed(2) + "s" ? data.avg_response_time.toFixed(2) + "s"
: "0.00s"; : "0.00s";
} }
if (document.querySelector(".stats-card h3:nth-of-type(3)")) { if (document.querySelector(".stats-card h3:nth-of-type(3)")) {
document.querySelector(".stats-card h3:nth-of-type(3)").textContent = document.querySelector(".stats-card h3:nth-of-type(3)").textContent =
data.avg_messages !== null && data.avg_messages !== undefined data.avg_messages !== null && data.avg_messages !== undefined
? data.avg_messages.toFixed(1) ? data.avg_messages.toFixed(1)
: "0.0"; : "0.0";
} }
if (document.querySelector(".stats-card h3:nth-of-type(4)")) { if (document.querySelector(".stats-card h3:nth-of-type(4)")) {
document.querySelector(".stats-card h3:nth-of-type(4)").textContent = document.querySelector(".stats-card h3:nth-of-type(4)").textContent =
data.escalation_rate !== null && data.escalation_rate !== undefined data.escalation_rate !== null && data.escalation_rate !== undefined
? data.escalation_rate.toFixed(1) + "%" ? data.escalation_rate.toFixed(1) + "%"
: "0.0%"; : "0.0%";
} }
} }
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,44 +1,37 @@
<!-- templates/dashboard/no_company.html --> <!-- templates/dashboard/no_company.html -->
{% extends 'base.html' %} {% extends 'base.html' %} {% block title %}No Company | Chat Analytics{% endblock %}
{% block title %}No Company | Chat Analytics{% endblock %}
{% block content %} {% block content %}
<div <div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom" 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> <h1 class="h2">No Company Association</h1>
</div> </div>
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-8"> <div class="col-md-8">
<div class="card"> <div class="card">
<div class="card-header bg-warning text-dark"> <div class="card-header bg-warning text-dark">
<h5 class="card-title mb-0">Account Not Associated with a Company</h5> <h5 class="card-title mb-0">Account Not Associated with a Company</h5>
</div> </div>
<div class="card-body text-center"> <div class="card-body text-center">
<div class="mb-4"> <div class="mb-4">
<i class="fas fa-building fa-4x text-warning mb-3"></i> <i class="fas fa-building fa-4x text-warning mb-3"></i>
<h4>You are not currently associated with any company</h4> <h4>You are not currently associated with any company</h4>
<p class="lead"> <p class="lead">You need to be associated with a company to access the dashboard.</p>
You need to be associated with a company to access the dashboard. </div>
</p>
</div>
<p> <p>
Please contact an administrator to have your account assigned to a company. Please contact an administrator to have your account assigned to a company. Once your
Once your account is associated with a company, you'll be able to access the account is associated with a company, you'll be able to access the dashboard and its
dashboard and its features. features.
</p> </p>
<div class="mt-4"> <div class="mt-4">
<a href="{% url 'profile' %}" class="btn btn-primary">View Your Profile</a> <a href="{% url 'profile' %}" class="btn btn-primary">View Your Profile</a>
<a href="{% url 'logout' %}" class="btn btn-outline-secondary ms-2" <a href="{% url 'logout' %}" class="btn btn-outline-secondary ms-2">Logout</a>
>Logout</a </div>
> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
{% endblock %} {% endblock %}

View File

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

View File

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

View File

@ -1,93 +1,81 @@
<!-- templates/dashboard/search_results.html --> <!-- templates/dashboard/search_results.html -->
{% extends 'base.html' %} {% extends 'base.html' %} {% block title %}Search Results | Chat Analytics{% endblock %}
{% block title %}Search Results | Chat Analytics{% endblock %}
{% block content %} {% block content %}
<div <div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom" 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> <h1 class="h2">Search Results</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary ajax-nav-link"> <a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary ajax-nav-link">
<i class="fas fa-arrow-left"></i> Back to Dashboard <i class="fas fa-arrow-left"></i> Back to Dashboard
</a> </a>
</div> </div>
</div> </div>
<div class="row mb-4"> <div class="row mb-4">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0">Search Chat Sessions</h5> <h5 class="card-title mb-0">Search Chat Sessions</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<form <form method="get" action="{% url 'search_chat_sessions' %}" class="search-form">
method="get" <div class="input-group">
action="{% url 'search_chat_sessions' %}" <input
class="search-form" type="text"
> name="q"
<div class="input-group"> class="form-control"
<input placeholder="Search sessions..."
type="text" value="{{ query }}"
name="q" aria-label="Search sessions"
class="form-control" />
placeholder="Search sessions..." {% if data_source %}
value="{{ query }}" <input type="hidden" name="data_source_id" value="{{ data_source.id }}" />
aria-label="Search sessions" {% endif %}
/> <button class="btn btn-outline-primary" type="submit">
{% if data_source %} <i class="fas fa-search"></i> Search
<input </button>
type="hidden" </div>
name="data_source_id" <div class="mt-2 text-muted">
value="{{ data_source.id }}" <small
/> >Search by session ID, country, language, sentiment, category, or message
{% endif %} content.</small
<button class="btn btn-outline-primary" type="submit"> >
<i class="fas fa-search"></i> Search </div>
</button> </form>
</div> </div>
<div class="mt-2 text-muted"> </div>
<small </div>
>Search by session ID, country, language, sentiment, category, or </div>
message content.</small
>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0"> <h5 class="card-title mb-0">
Results {% if query %}for "{{ query }}"{% endif %} Results {% if query %}for "{{ query }}"{% endif %}
{% if data_source %}in {{ data_source.name }}{% endif %} {% if data_source %}in {{ data_source.name }}{% endif %}
({{ page_obj.paginator.count }}) ({{ page_obj.paginator.count }})
</h5> </h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<!-- Loading spinner shown during AJAX requests --> <!-- Loading spinner shown during AJAX requests -->
<div id="ajax-loading-spinner" class="text-center py-4 d-none"> <div id="ajax-loading-spinner" class="text-center py-4 d-none">
<div class="spinner-border text-primary" role="status"> <div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
<p class="mt-2">Loading data...</p> <p class="mt-2">Loading data...</p>
</div> </div>
<!-- Search results container that will be updated via AJAX --> <!-- Search results container that will be updated via AJAX -->
<div id="ajax-content-container"> <div id="ajax-content-container">
{% include "dashboard/partials/search_results_table.html" %} {% include "dashboard/partials/search_results_table.html" %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<!-- No need for extra JavaScript here, using common ajax-pagination.js --> <!-- No need for extra JavaScript here, using common ajax-pagination.js -->
{% endblock %} {% endblock %}

View File

@ -1,197 +1,195 @@
<!-- templates/dashboard/upload.html --> <!-- templates/dashboard/upload.html -->
{% extends 'base.html' %} {% extends 'base.html' %} {% load crispy_forms_tags %} {% load dashboard_extras %}
{% load crispy_forms_tags %} {% block title %}
{% load dashboard_extras %} Upload Data | Chat Analytics
{% endblock %}
{% block title %}Upload Data | Chat Analytics{% endblock %} {% block content %}
<div
{% block content %} class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
<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">
<h1 class="h2">Upload Data</h1> <a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
<div class="btn-toolbar mb-2 mb-md-0"> <i class="fas fa-arrow-left"></i> Back to Dashboard
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary"> </a>
<i class="fas fa-arrow-left"></i> Back to Dashboard </div>
</a> </div>
</div>
</div> <div class="row">
<div class="col-md-6">
<div class="row"> <div class="card">
<div class="col-md-6"> <div class="card-header">
<div class="card"> <h5 class="card-title mb-0">Upload CSV File</h5>
<div class="card-header"> </div>
<h5 class="card-title mb-0">Upload CSV File</h5> <div class="card-body">
</div> <form method="post" enctype="multipart/form-data">
<div class="card-body"> {% csrf_token %} {{ form|crispy }}
<form method="post" enctype="multipart/form-data"> <div class="d-grid gap-2">
{% csrf_token %} {{ form|crispy }} <button type="submit" class="btn btn-primary">Upload</button>
<div class="d-grid gap-2"> </div>
<button type="submit" class="btn btn-primary">Upload</button> </form>
</div> </div>
</form> </div>
</div> </div>
</div>
</div> <div class="col-md-6">
<div class="card">
<div class="col-md-6"> <div class="card-header">
<div class="card"> <h5 class="card-title mb-0">CSV File Format</h5>
<div class="card-header"> </div>
<h5 class="card-title mb-0">CSV File Format</h5> <div class="card-body">
</div> <p>The CSV file should contain the following columns:</p>
<div class="card-body"> <table class="table table-sm">
<p>The CSV file should contain the following columns:</p> <thead>
<table class="table table-sm"> <tr>
<thead> <th>Column</th>
<tr> <th>Description</th>
<th>Column</th> <th>Type</th>
<th>Description</th> </tr>
<th>Type</th> </thead>
</tr> <tbody>
</thead> <tr>
<tbody> <td>session_id</td>
<tr> <td>Unique identifier for the chat session</td>
<td>session_id</td> <td>String</td>
<td>Unique identifier for the chat session</td> </tr>
<td>String</td> <tr>
</tr> <td>start_time</td>
<tr> <td>When the session started</td>
<td>start_time</td> <td>Datetime</td>
<td>When the session started</td> </tr>
<td>Datetime</td> <tr>
</tr> <td>end_time</td>
<tr> <td>When the session ended</td>
<td>end_time</td> <td>Datetime</td>
<td>When the session ended</td> </tr>
<td>Datetime</td> <tr>
</tr> <td>ip_address</td>
<tr> <td>IP address of the user</td>
<td>ip_address</td> <td>String</td>
<td>IP address of the user</td> </tr>
<td>String</td> <tr>
</tr> <td>country</td>
<tr> <td>Country of the user</td>
<td>country</td> <td>String</td>
<td>Country of the user</td> </tr>
<td>String</td> <tr>
</tr> <td>language</td>
<tr> <td>Language used in the conversation</td>
<td>language</td> <td>String</td>
<td>Language used in the conversation</td> </tr>
<td>String</td> <tr>
</tr> <td>messages_sent</td>
<tr> <td>Number of messages in the conversation</td>
<td>messages_sent</td> <td>Integer</td>
<td>Number of messages in the conversation</td> </tr>
<td>Integer</td> <tr>
</tr> <td>sentiment</td>
<tr> <td>Sentiment analysis of the conversation</td>
<td>sentiment</td> <td>String</td>
<td>Sentiment analysis of the conversation</td> </tr>
<td>String</td> <tr>
</tr> <td>escalated</td>
<tr> <td>Whether the conversation was escalated</td>
<td>escalated</td> <td>Boolean</td>
<td>Whether the conversation was escalated</td> </tr>
<td>Boolean</td> <tr>
</tr> <td>forwarded_hr</td>
<tr> <td>Whether the conversation was forwarded to HR</td>
<td>forwarded_hr</td> <td>Boolean</td>
<td>Whether the conversation was forwarded to HR</td> </tr>
<td>Boolean</td> <tr>
</tr> <td>full_transcript</td>
<tr> <td>Full transcript of the conversation</td>
<td>full_transcript</td> <td>Text</td>
<td>Full transcript of the conversation</td> </tr>
<td>Text</td> <tr>
</tr> <td>avg_response_time</td>
<tr> <td>Average response time in seconds</td>
<td>avg_response_time</td> <td>Float</td>
<td>Average response time in seconds</td> </tr>
<td>Float</td> <tr>
</tr> <td>tokens</td>
<tr> <td>Total number of tokens used</td>
<td>tokens</td> <td>Integer</td>
<td>Total number of tokens used</td> </tr>
<td>Integer</td> <tr>
</tr> <td>tokens_eur</td>
<tr> <td>Cost of tokens in EUR</td>
<td>tokens_eur</td> <td>Float</td>
<td>Cost of tokens in EUR</td> </tr>
<td>Float</td> <tr>
</tr> <td>category</td>
<tr> <td>Category of the conversation</td>
<td>category</td> <td>String</td>
<td>Category of the conversation</td> </tr>
<td>String</td> <tr>
</tr> <td>initial_msg</td>
<tr> <td>First message from the user</td>
<td>initial_msg</td> <td>Text</td>
<td>First message from the user</td> </tr>
<td>Text</td> <tr>
</tr> <td>user_rating</td>
<tr> <td>User rating of the conversation</td>
<td>user_rating</td> <td>String</td>
<td>User rating of the conversation</td> </tr>
<td>String</td> </tbody>
</tr> </table>
</tbody> </div>
</table> </div>
</div> </div>
</div> </div>
</div>
</div> {% if data_sources %}
<div class="row mt-4">
{% if data_sources %} <div class="col-12">
<div class="row mt-4"> <div class="card">
<div class="col-12"> <div class="card-header">
<div class="card"> <h5 class="card-title mb-0">Uploaded Data Sources</h5>
<div class="card-header"> </div>
<h5 class="card-title mb-0">Uploaded Data Sources</h5> <div class="card-body">
</div> <div class="table-responsive">
<div class="card-body"> <table class="table table-striped table-hover">
<div class="table-responsive"> <thead>
<table class="table table-striped table-hover"> <tr>
<thead> <th>Name</th>
<tr> <th>Description</th>
<th>Name</th> <th>Uploaded</th>
<th>Description</th> <th>File</th>
<th>Uploaded</th> <th>Sessions</th>
<th>File</th> <th>Actions</th>
<th>Sessions</th> </tr>
<th>Actions</th> </thead>
</tr> <tbody>
</thead> {% for data_source in data_sources %}
<tbody> <tr>
{% for data_source in data_sources %} <td>{{ data_source.name }}</td>
<tr> <td>{{ data_source.description|truncatechars:50 }}</td>
<td>{{ data_source.name }}</td> <td>{{ data_source.uploaded_at|date:"M d, Y H:i" }}</td>
<td>{{ data_source.description|truncatechars:50 }}</td> <td>{{ data_source.file.name|split:"/"|last }}</td>
<td>{{ data_source.uploaded_at|date:"M d, Y H:i" }}</td> <td>{{ data_source.chat_sessions.count }}</td>
<td>{{ data_source.file.name|split:"/"|last }}</td> <td>
<td>{{ data_source.chat_sessions.count }}</td> <a
<td> href="{% url 'data_source_detail' data_source.id %}"
<a class="btn btn-sm btn-outline-primary"
href="{% url 'data_source_detail' data_source.id %}" >
class="btn btn-sm btn-outline-primary" <i class="fas fa-eye"></i>
> </a>
<i class="fas fa-eye"></i> <a
</a> href="{% url 'delete_data_source' data_source.id %}"
<a class="btn btn-sm btn-outline-danger"
href="{% url 'delete_data_source' data_source.id %}" >
class="btn btn-sm btn-outline-danger" <i class="fas fa-trash"></i>
> </a>
<i class="fas fa-trash"></i> </td>
</a> </tr>
</td> {% endfor %}
</tr> </tbody>
{% endfor %} </table>
</tbody> </div>
</table> </div>
</div> </div>
</div> </div>
</div> </div>
</div> {% endif %}
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -1,17 +1,6 @@
{ {
"name": "livegraphsdjango", "devDependencies": {
"version": "0.1.0", "prettier": "^3.5.3",
"description": "Live Graphs Django Dashboard", "prettier-plugin-jinja-template": "^2.1.0"
"private": true, }
"scripts": {
"lint": "eslint dashboard_project/static/js/",
"format": "prettier --write \"dashboard_project/static/**/*.{js,css,html}\"",
"format:check": "prettier --check \"dashboard_project/static/**/*.{js,css,html}\"",
"stylelint": "stylelint \"dashboard_project/static/css/**/*.css\" --fix",
"stylelint:check": "stylelint \"dashboard_project/static/css/**/*.css\""
},
"devDependencies": {
"prettier": "^3.5.3",
"prettier-plugin-jinja-template": "^2.1.0"
}
} }