mirror of
https://github.com/kjanat/livegraphs-django.git
synced 2026-01-16 11:42:10 +01:00
Initial commit
This commit is contained in:
45
.editorconfig
Normal file
45
.editorconfig
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# EditorConfig is a standardized configuration file that helps define
|
||||||
|
# consistent coding styles across different editors and IDEs
|
||||||
|
# https://editorconfig.org/
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Python files
|
||||||
|
[*.py]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# HTML and Django/Jinja2 template files
|
||||||
|
[*.{html,htm}]
|
||||||
|
indent_size = 2
|
||||||
|
# Allow prettier to format Django/Jinja templates properly
|
||||||
|
# The following comment options can be used in individual files if needed:
|
||||||
|
# <!-- prettier-ignore -->
|
||||||
|
# {# prettier-ignore #}
|
||||||
|
|
||||||
|
# CSS, JavaScript, and JSON files
|
||||||
|
[*.{css,scss,js,json}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Markdown files
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
# YAML files
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Makefile (requires tabs)
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
# Docker-related files
|
||||||
|
[{Dockerfile,docker-compose.yml}]
|
||||||
|
indent_size = 2
|
||||||
409
.gitignore
vendored
Normal file
409
.gitignore
vendored
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/django,python,node
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=django,python,node
|
||||||
|
|
||||||
|
### Django ###
|
||||||
|
*.log
|
||||||
|
*.pot
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
media
|
||||||
|
|
||||||
|
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
|
||||||
|
# in your Git repository. Update and uncomment the following line accordingly.
|
||||||
|
# <django-project-name>/staticfiles/
|
||||||
|
|
||||||
|
### Django.Python Stack ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
### Node Patch ###
|
||||||
|
# Serverless Webpack directories
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
# SvelteKit build / generate output
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
|
||||||
|
### Python Patch ###
|
||||||
|
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||||
|
poetry.toml
|
||||||
|
|
||||||
|
# ruff
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# LSP config files
|
||||||
|
pyrightconfig.json
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/django,python,node
|
||||||
|
|
||||||
|
# Custom ignores from here on
|
||||||
|
# Local environment variables
|
||||||
|
*Zone.Identifier
|
||||||
|
examples/
|
||||||
|
**/migrations/[0-9]**.py
|
||||||
49
.prettierignore
Normal file
49
.prettierignore
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# Python virtual environment
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# Static files collected by Django
|
||||||
|
staticfiles/
|
||||||
|
dashboard_project/staticfiles/
|
||||||
|
|
||||||
|
# Media files
|
||||||
|
media/
|
||||||
|
dashboard_project/media/
|
||||||
|
|
||||||
|
# Python cache files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Database files
|
||||||
|
*.sqlite3
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# Django migration files
|
||||||
|
*/migrations/*.py
|
||||||
|
!*/migrations/__init__.py
|
||||||
|
|
||||||
|
# Node modules (if you use npm/yarn for frontend)
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker-compose.override.yml
|
||||||
|
|
||||||
|
# IDE specific files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
30
.prettierrc
Normal file
30
.prettierrc
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "always",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"embeddedLanguageFormatting": "auto",
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"insertPragma": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"printWidth": 100,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"requirePragma": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"useTabs": false,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.html"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"parser": "jinja-template"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"prettier-plugin-jinja-template"
|
||||||
|
]
|
||||||
|
}
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
47
.vscode/settings.json
vendored
Normal file
47
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"[css]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[html]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[python]": {
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll": "explicit",
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "charliermarsh.ruff",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[toml]": {
|
||||||
|
"editor.defaultFormatter": "tamasfe.even-better-toml"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"emmet.includeLanguages": {
|
||||||
|
"django-html": "html",
|
||||||
|
"jinja-html": "html"
|
||||||
|
},
|
||||||
|
"emmet.syntaxProfiles": {
|
||||||
|
"html": {
|
||||||
|
"inline_break": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files.associations": {
|
||||||
|
"*.html": "html"
|
||||||
|
},
|
||||||
|
"html.format.wrapAttributes": "auto",
|
||||||
|
"html.format.wrapLineLength": 100,
|
||||||
|
"notebook.codeActionsOnSave": {
|
||||||
|
"notebook.source.fixAll": "explicit",
|
||||||
|
"notebook.source.organizeImports": "explicit"
|
||||||
|
},
|
||||||
|
"notebook.formatOnSave.enabled": true,
|
||||||
|
"prettier.requireConfig": true,
|
||||||
|
}
|
||||||
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Dockerfile
|
||||||
|
|
||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
ENV DJANGO_SETTINGS_MODULE=dashboard_project.settings
|
||||||
|
|
||||||
|
# Set work directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN uv pip install -e .
|
||||||
|
|
||||||
|
# Copy project
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Collect static files
|
||||||
|
RUN python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
# Run gunicorn
|
||||||
|
CMD ["gunicorn", "dashboard_project.wsgi:application", "--bind", "0.0.0.0:8000"]
|
||||||
126
IMPLEMENTATION_SUMMARY.md
Normal file
126
IMPLEMENTATION_SUMMARY.md
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# Chat Analytics Dashboard: Implementation Summary
|
||||||
|
|
||||||
|
## Core Features Implemented
|
||||||
|
|
||||||
|
1. **Multi-Tenant Architecture**:
|
||||||
|
- Companies have isolated data and user access
|
||||||
|
- Users belong to specific companies
|
||||||
|
- Role-based permissions (admin, company admin, regular user)
|
||||||
|
|
||||||
|
2. **Data Management**:
|
||||||
|
- CSV file upload and processing
|
||||||
|
- Data source management
|
||||||
|
- Chat session records with comprehensive metadata
|
||||||
|
|
||||||
|
3. **Dashboard Visualization**:
|
||||||
|
- Interactive charts using Plotly.js
|
||||||
|
- Key metrics and KPIs
|
||||||
|
- Time-series analysis
|
||||||
|
- Geographic distribution
|
||||||
|
- Sentiment analysis
|
||||||
|
- Category distribution
|
||||||
|
|
||||||
|
4. **Search and Analysis**:
|
||||||
|
- Full-text search across chat sessions
|
||||||
|
- Filtering by various attributes
|
||||||
|
- Detailed view of individual chat sessions
|
||||||
|
- Transcript viewing
|
||||||
|
|
||||||
|
5. **User Management**:
|
||||||
|
- User registration and authentication
|
||||||
|
- Profile management
|
||||||
|
- Password change functionality
|
||||||
|
- Role assignment
|
||||||
|
|
||||||
|
6. **Admin Interface**:
|
||||||
|
- Company management
|
||||||
|
- User administration
|
||||||
|
- Data source oversight
|
||||||
|
- System-wide configuration
|
||||||
|
|
||||||
|
7. **Responsive Design**:
|
||||||
|
- Mobile-friendly interface using Bootstrap 5
|
||||||
|
- Consistent layout and navigation
|
||||||
|
- Accessible UI components
|
||||||
|
|
||||||
|
## Technical Implementation
|
||||||
|
|
||||||
|
### Backend (Django)
|
||||||
|
|
||||||
|
- **Custom User Model**: Extended for company association and roles
|
||||||
|
- **Database Models**: Structured for efficient data storage and queries
|
||||||
|
- **View Logic**: Separation of concerns with dedicated view functions
|
||||||
|
- **Form Handling**: Validated data input and file uploads
|
||||||
|
- **Data Processing**: CSV parsing and structured storage
|
||||||
|
- **Template Context**: Prepared data for frontend rendering
|
||||||
|
- **URL Routing**: Clean URL structure
|
||||||
|
- **Access Control**: Permission checks throughout
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
- **Bootstrap 5**: For responsive layout and UI components
|
||||||
|
- **Plotly.js**: For interactive charts and visualizations
|
||||||
|
- **jQuery**: For AJAX functionality
|
||||||
|
- **Font Awesome**: For icons
|
||||||
|
- **Custom CSS**: For styling enhancements
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
1. **Upload Process**:
|
||||||
|
- File validation
|
||||||
|
- CSV parsing
|
||||||
|
- Data normalization
|
||||||
|
- Record creation
|
||||||
|
- Association with company
|
||||||
|
|
||||||
|
2. **Dashboard Generation**:
|
||||||
|
- Data aggregation
|
||||||
|
- Statistical calculations
|
||||||
|
- Chart data preparation
|
||||||
|
- JSON serialization for frontend
|
||||||
|
|
||||||
|
3. **User Authentication**:
|
||||||
|
- Login/registration handling
|
||||||
|
- Session management
|
||||||
|
- Permission checks
|
||||||
|
- Access control based on company
|
||||||
|
|
||||||
|
### Deployment Configuration
|
||||||
|
|
||||||
|
- **Docker**: Containerization for consistent deployment
|
||||||
|
- **Docker Compose**: Multi-container orchestration
|
||||||
|
- **Nginx**: Web server and static file serving
|
||||||
|
- **PostgreSQL**: Production-ready database
|
||||||
|
- **Gunicorn**: WSGI HTTP server
|
||||||
|
|
||||||
|
## API Structure
|
||||||
|
|
||||||
|
While the current implementation does not have a formal REST API, the foundation is in place for adding one in the future:
|
||||||
|
|
||||||
|
1. **Dashboard API**: Already implemented for chart data (JSON responses)
|
||||||
|
2. **Data Source API**: Potential endpoint for uploading data programmatically
|
||||||
|
3. **Chat Session API**: Could expose data for external integration
|
||||||
|
|
||||||
|
## Testing and Development
|
||||||
|
|
||||||
|
- **Sample Data Generation**: Management command to create test data
|
||||||
|
- **Local Development Setup**: Easy configuration with sqlite
|
||||||
|
- **Production Deployment**: Docker-based for scalability
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- **Authentication**: Django's secure authentication system
|
||||||
|
- **Data Isolation**: Company-specific queries prevent data leakage
|
||||||
|
- **Password Management**: Secure password handling
|
||||||
|
- **CSRF Protection**: Django's built-in CSRF protection
|
||||||
|
- **Input Validation**: Form validation for all user inputs
|
||||||
|
|
||||||
|
## Future Extensions
|
||||||
|
|
||||||
|
The architecture supports easy extension for:
|
||||||
|
|
||||||
|
1. **API Integration**: Direct connection to chat platforms
|
||||||
|
2. **Real-time Updates**: WebSockets for live dashboard updates
|
||||||
|
3. **Advanced Analytics**: Machine learning integration
|
||||||
|
4. **Customizable Reports**: Report generation and scheduling
|
||||||
|
5. **Enhanced Visualization**: More chart types and interactive features
|
||||||
67
PRETTIER_SETUP.md
Normal file
67
PRETTIER_SETUP.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Prettier for Django/Jinja Templates
|
||||||
|
|
||||||
|
This project uses Prettier with the `prettier-plugin-jinja-template` plugin to format HTML templates with Django/Jinja syntax.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
To use Prettier with your Django templates, you'll need to install Prettier and the Jinja template plugin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using npm
|
||||||
|
npm install --save-dev prettier prettier-plugin-jinja-template
|
||||||
|
|
||||||
|
# Or using yarn
|
||||||
|
yarn add --dev prettier prettier-plugin-jinja-template
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Once installed, you can format your Django templates using:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format a specific file
|
||||||
|
npx prettier --write path/to/template.html
|
||||||
|
|
||||||
|
# Format all HTML files
|
||||||
|
npx prettier --write "**/*.html"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Without install
|
||||||
|
|
||||||
|
If you don't want to install the plugin, you can use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx prettier --plugin=prettier-plugin-jinja-template --parser=jinja-template --write **/*.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## VSCode Integration
|
||||||
|
|
||||||
|
For VSCode users, install the Prettier extension and add these settings to your `.vscode/settings.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[html]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"prettier.requireConfig": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ignoring Parts of Templates
|
||||||
|
|
||||||
|
If you need to prevent Prettier from formatting a section of your template:
|
||||||
|
|
||||||
|
```html
|
||||||
|
{# prettier-ignore #}
|
||||||
|
<div>
|
||||||
|
This section will not be formatted
|
||||||
|
by Prettier.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<div>
|
||||||
|
This works too.
|
||||||
|
</div>
|
||||||
|
```
|
||||||
126
PROJECT_OVERVIEW.md
Normal file
126
PROJECT_OVERVIEW.md
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# Chat Analytics Dashboard Project
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This Django project creates a multi-tenant dashboard application for analyzing chat session data. Companies can upload their chat data (in CSV format) and view analytics and metrics through an interactive dashboard. The application supports user authentication, role-based access control, and separate data isolation for different companies.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
The project consists of two main Django apps:
|
||||||
|
|
||||||
|
1. **accounts**: Handles user authentication, company management, and user roles
|
||||||
|
2. **dashboard**: Manages data sources, chat sessions, and dashboard visualization
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- **Multi-company Support**: Each company has their own private dashboards and data
|
||||||
|
- **User Management**: Different user roles (admin, company admin, regular user)
|
||||||
|
- **CSV File Upload**: Upload and process CSV files containing chat session data
|
||||||
|
- **Interactive Dashboard**: Visualize chat data with charts and metrics
|
||||||
|
- **Search Functionality**: Find specific chat sessions based on various criteria
|
||||||
|
- **Data Exploration**: Drill down into individual chat sessions for detailed analysis
|
||||||
|
|
||||||
|
## Setup and Installation
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- Django 4.0+
|
||||||
|
- Other dependencies listed in `requirements.txt`
|
||||||
|
|
||||||
|
### Installation Steps
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
2. Set up a virtual environment
|
||||||
|
3. Install dependencies with `pip install -r requirements.txt`
|
||||||
|
4. Run database migrations with `python manage.py migrate`
|
||||||
|
5. Create a superuser with `python manage.py createsuperuser`
|
||||||
|
6. Start the development server with `python manage.py runserver`
|
||||||
|
|
||||||
|
### Creating Sample Data
|
||||||
|
|
||||||
|
To quickly populate the application with sample data, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python manage.py create_sample_data
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create:
|
||||||
|
|
||||||
|
- An admin user (username: admin, password: admin123)
|
||||||
|
- Three sample companies
|
||||||
|
- Company admin users for each company
|
||||||
|
- Regular users for each company
|
||||||
|
- Sample chat data for each company
|
||||||
|
- Default dashboards for each company
|
||||||
|
|
||||||
|
## Models
|
||||||
|
|
||||||
|
### Accounts App
|
||||||
|
|
||||||
|
- **CustomUser**: Extends Django's User model with company association and role
|
||||||
|
- **Company**: Represents a company with users and data sources
|
||||||
|
|
||||||
|
### Dashboard App
|
||||||
|
|
||||||
|
- **DataSource**: Represents an uploaded CSV file with chat data
|
||||||
|
- **ChatSession**: Stores individual chat session data parsed from CSV
|
||||||
|
- **Dashboard**: Allows configuration of custom dashboards with selected data sources
|
||||||
|
|
||||||
|
## Usage Flow
|
||||||
|
|
||||||
|
1. **Admin Setup**:
|
||||||
|
- Admin creates companies
|
||||||
|
- Admin creates users and assigns them to companies
|
||||||
|
|
||||||
|
2. **Company Admin**:
|
||||||
|
- Uploads CSV files with chat data
|
||||||
|
- Creates and configures dashboards
|
||||||
|
- Manages company users
|
||||||
|
|
||||||
|
3. **Regular Users**:
|
||||||
|
- View dashboards
|
||||||
|
- Search and explore chat data
|
||||||
|
- Analyze chat metrics
|
||||||
|
|
||||||
|
## CSV Format
|
||||||
|
|
||||||
|
The application expects CSV files with the following columns:
|
||||||
|
|
||||||
|
- **session_id**: Unique identifier for each chat session
|
||||||
|
- **start_time**: When the chat session started
|
||||||
|
- **end_time**: When the chat session ended
|
||||||
|
- **ip_address**: User's IP address
|
||||||
|
- **country**: User's country
|
||||||
|
- **language**: Language used in the chat
|
||||||
|
- **messages_sent**: Number of messages in the conversation
|
||||||
|
- **sentiment**: Sentiment analysis result (Positive, Neutral, Negative)
|
||||||
|
- **escalated**: Whether the chat was escalated
|
||||||
|
- **forwarded_hr**: Whether the chat was forwarded to HR
|
||||||
|
- **full_transcript**: Complete chat transcript
|
||||||
|
- **avg_response_time**: Average response time in seconds
|
||||||
|
- **tokens**: Number of tokens used (for AI chat systems)
|
||||||
|
- **tokens_eur**: Cost of tokens in EUR
|
||||||
|
- **category**: Chat category or topic
|
||||||
|
- **initial_msg**: First message from the user
|
||||||
|
- **user_rating**: User satisfaction rating
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
For production deployment, the project includes:
|
||||||
|
|
||||||
|
- **Dockerfile**: For containerizing the application
|
||||||
|
- **docker-compose.yml**: For orchestrating the application with PostgreSQL and Nginx
|
||||||
|
- **Nginx Configuration**: For serving the application and static files
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- **API Integration**: Direct integration with chat systems
|
||||||
|
- **Real-time Updates**: Live dashboard updates as new chats occur
|
||||||
|
- **Advanced Analytics**: More detailed and customizable metrics
|
||||||
|
- **Export Functionality**: Export reports and analysis
|
||||||
|
- **Customizable Themes**: Company-specific branding
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For any issues or questions, please create an issue in the repository or contact the project maintainers.
|
||||||
243
QUICK_START_GUIDE.md
Normal file
243
QUICK_START_GUIDE.md
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
# Chat Analytics Dashboard: Quick Start Guide
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This guide will help you quickly set up and start using the Chat Analytics Dashboard.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
#### Option 1: Local Development
|
||||||
|
|
||||||
|
1. **Clone the repository**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone <repository-url>
|
||||||
|
cd dashboard_project
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Set up a virtual environment**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv venv
|
||||||
|
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Install dependencies**: # from pyproject.toml
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Set up the database**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Create admin user**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Start the development server**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Access the application**:
|
||||||
|
Open your browser and go to <http://127.0.0.1:8000/>
|
||||||
|
|
||||||
|
#### Option 2: Docker Deployment
|
||||||
|
|
||||||
|
1. **Clone the repository**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone <repository-url>
|
||||||
|
cd dashboard_project
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build and start the containers**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create admin user**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker-compose exec web python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Access the application**:
|
||||||
|
Open your browser and go to <http://localhost/>
|
||||||
|
|
||||||
|
### Creating Sample Data (Optional)
|
||||||
|
|
||||||
|
To quickly populate the system with sample data:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python manage.py create_sample_data
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create:
|
||||||
|
|
||||||
|
- Admin user (username: admin, password: admin123)
|
||||||
|
- Three companies with users
|
||||||
|
- Sample chat data and dashboards
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Admin Tasks
|
||||||
|
|
||||||
|
1. **Access Admin Panel**:
|
||||||
|
- Go to <http://localhost/admin/>
|
||||||
|
- Login with your admin credentials
|
||||||
|
|
||||||
|
2. **Create a Company**:
|
||||||
|
- Go to Companies > Add Company
|
||||||
|
- Fill in the company details and save
|
||||||
|
|
||||||
|
3. **Create Users**:
|
||||||
|
- Go to Users > Add User
|
||||||
|
- Fill in user details
|
||||||
|
- Assign the user to a company
|
||||||
|
- Set appropriate permissions (staff status, company admin)
|
||||||
|
|
||||||
|
### Company Admin Tasks
|
||||||
|
|
||||||
|
1. **Login to Dashboard**:
|
||||||
|
- Go to <http://localhost/>
|
||||||
|
- Login with your company admin credentials
|
||||||
|
|
||||||
|
2. **Upload Chat Data**:
|
||||||
|
- Click on "Upload Data" in the sidebar
|
||||||
|
- Fill in the data source details
|
||||||
|
- Select a CSV file containing chat data
|
||||||
|
- Click "Upload"
|
||||||
|
|
||||||
|
3. **Create a Dashboard**:
|
||||||
|
- Click on "New Dashboard" in the sidebar
|
||||||
|
- Fill in the dashboard details
|
||||||
|
- Select data sources to include
|
||||||
|
- Click "Create Dashboard"
|
||||||
|
|
||||||
|
### Regular User Tasks
|
||||||
|
|
||||||
|
1. **View Dashboard**:
|
||||||
|
- Login with your user credentials
|
||||||
|
- The dashboard will show automatically
|
||||||
|
- Select different dashboards from the sidebar
|
||||||
|
|
||||||
|
2. **Search Chat Sessions**:
|
||||||
|
- Click on "Search" in the top navigation
|
||||||
|
- Enter search terms
|
||||||
|
- Use filters to refine results
|
||||||
|
|
||||||
|
3. **View Session Details**:
|
||||||
|
- In search results, click the eye icon for a session
|
||||||
|
- View complete session information and transcript
|
||||||
|
|
||||||
|
## CSV Format
|
||||||
|
|
||||||
|
Your CSV files should include the following columns:
|
||||||
|
|
||||||
|
| Column | Description | Type |
|
||||||
|
| ----------------- | ------------------------------- | -------- |
|
||||||
|
| session_id | Unique ID for the chat | String |
|
||||||
|
| start_time | Session start time | Datetime |
|
||||||
|
| end_time | Session end time | Datetime |
|
||||||
|
| ip_address | User's IP address | String |
|
||||||
|
| country | User's country | String |
|
||||||
|
| language | Chat language | String |
|
||||||
|
| messages_sent | Number of messages | Integer |
|
||||||
|
| sentiment | Sentiment analysis result | String |
|
||||||
|
| escalated | Whether chat was escalated | Boolean |
|
||||||
|
| forwarded_hr | Whether chat was sent to HR | Boolean |
|
||||||
|
| full_transcript | Complete chat text | Text |
|
||||||
|
| avg_response_time | Average response time (seconds) | Float |
|
||||||
|
| tokens | Number of tokens used | Integer |
|
||||||
|
| tokens_eur | Cost in EUR | Float |
|
||||||
|
| category | Chat category | String |
|
||||||
|
| initial_msg | First user message | Text |
|
||||||
|
| user_rating | User satisfaction rating | String |
|
||||||
|
|
||||||
|
Example CSV row:
|
||||||
|
|
||||||
|
```csv
|
||||||
|
acme_1,2023-05-01 10:30:00,2023-05-01 10:45:00,192.168.1.1,USA,English,10,Positive,FALSE,FALSE,"User: Hello\nAgent: Hi there!",2.5,500,0.01,Support,Hello I need help,Good
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dashboard Features
|
||||||
|
|
||||||
|
### Overview Panel
|
||||||
|
|
||||||
|
The main dashboard shows:
|
||||||
|
|
||||||
|
- Total chat sessions
|
||||||
|
- Average response time
|
||||||
|
- Total tokens used
|
||||||
|
- Total cost
|
||||||
|
|
||||||
|
### Charts
|
||||||
|
|
||||||
|
The dashboard includes:
|
||||||
|
|
||||||
|
- **Sessions Over Time**: Line chart showing chat volume trends
|
||||||
|
- **Sentiment Analysis**: Pie chart of positive/negative/neutral chats
|
||||||
|
- **Top Countries**: Bar chart of user countries
|
||||||
|
- **Categories**: Distribution of chat categories
|
||||||
|
|
||||||
|
### Data Source Details
|
||||||
|
|
||||||
|
View details for each data source:
|
||||||
|
|
||||||
|
- Upload date and time
|
||||||
|
- Total sessions
|
||||||
|
- Source description
|
||||||
|
- List of all chat sessions from the source
|
||||||
|
|
||||||
|
### Session Details
|
||||||
|
|
||||||
|
For each chat session, you can view:
|
||||||
|
|
||||||
|
- Session metadata (time, location, etc.)
|
||||||
|
- Full chat transcript
|
||||||
|
- Performance metrics
|
||||||
|
- User sentiment and rating
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### CSV Upload Issues
|
||||||
|
|
||||||
|
If your CSV upload fails:
|
||||||
|
|
||||||
|
- Ensure all required columns are present
|
||||||
|
- Check date formats (should be YYYY-MM-DD HH:MM:SS)
|
||||||
|
- Verify boolean values (TRUE/FALSE, Yes/No, 1/0)
|
||||||
|
- Check for special characters in text fields
|
||||||
|
|
||||||
|
### Access Issues
|
||||||
|
|
||||||
|
If you can't access certain features:
|
||||||
|
|
||||||
|
- Verify your user role (admin, company admin, or regular user)
|
||||||
|
- Ensure you're assigned to the correct company
|
||||||
|
- Check if you're trying to access another company's data
|
||||||
|
|
||||||
|
### Empty Dashboard
|
||||||
|
|
||||||
|
If your dashboard is empty:
|
||||||
|
|
||||||
|
- Verify that data sources have been uploaded
|
||||||
|
- Check that the dashboard is configured to use those data sources
|
||||||
|
- Ensure the CSV was processed successfully
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
If you encounter any issues:
|
||||||
|
|
||||||
|
- Check the documentation
|
||||||
|
- Contact your system administrator
|
||||||
|
- File an issue in the project repository
|
||||||
131
README.md
Normal file
131
README.md
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
# Chat Analytics Dashboard
|
||||||
|
|
||||||
|
A Django application that creates an analytics dashboard for chat session data. The application allows different companies to have their own dashboards and view their own data.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Multi-company support with user authentication
|
||||||
|
- CSV file upload and processing
|
||||||
|
- Interactive dashboard with charts and visualizations
|
||||||
|
- Detailed data views for chat sessions
|
||||||
|
- Search functionality to find specific chat sessions
|
||||||
|
- Admin interface for managing users and companies
|
||||||
|
- Responsive design using Bootstrap 5
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.13+
|
||||||
|
- Django 5.0+
|
||||||
|
- PostgreSQL (optional, SQLite is fine for development)
|
||||||
|
- Other dependencies listed in [`pyproject.toml`](pyproject.toml)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone <repository-url>
|
||||||
|
cd dashboard_project
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create a virtual environment and activate it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv venv
|
||||||
|
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install dependencies:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run migrations:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv run python manage.py makemigrations
|
||||||
|
uv run python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Create a superuser:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv run python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Run the development server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv run python manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Access the application at <http://127.0.0.1:8000/>
|
||||||
|
|
||||||
|
### Using Docker
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone <repository-url>
|
||||||
|
cd dashboard_project
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Build and run with Docker Compose:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create a superuser:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker-compose exec web python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Access the application at <http://localhost/>
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Login as the superuser you created.
|
||||||
|
2. Go to the admin interface (<http://localhost/admin/>) and create companies and users.
|
||||||
|
3. Assign users to companies.
|
||||||
|
4. Upload CSV files for each company.
|
||||||
|
5. View the analytics dashboard.
|
||||||
|
|
||||||
|
## CSV File Format
|
||||||
|
|
||||||
|
The CSV file should contain the following columns:
|
||||||
|
|
||||||
|
- session_id: Unique identifier for the chat session
|
||||||
|
- start_time: When the session started (datetime)
|
||||||
|
- end_time: When the session ended (datetime)
|
||||||
|
- ip_address: IP address of the user
|
||||||
|
- country: Country of the user
|
||||||
|
- language: Language used in the conversation
|
||||||
|
- messages_sent: Number of messages in the conversation (integer)
|
||||||
|
- sentiment: Sentiment analysis of the conversation (string)
|
||||||
|
- escalated: Whether the conversation was escalated (boolean)
|
||||||
|
- forwarded_hr: Whether the conversation was forwarded to HR (boolean)
|
||||||
|
- full_transcript: Full transcript of the conversation (text)
|
||||||
|
- avg_response_time: Average response time in seconds (float)
|
||||||
|
- tokens: Total number of tokens used (integer)
|
||||||
|
- tokens_eur: Cost of tokens in EUR (float)
|
||||||
|
- category: Category of the conversation (string)
|
||||||
|
- initial_msg: First message from the user (text)
|
||||||
|
- user_rating: User rating of the conversation (string)
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- API integration for real-time data
|
||||||
|
- More advanced visualizations
|
||||||
|
- Custom reports
|
||||||
|
- Export functionality
|
||||||
|
- Theme customization
|
||||||
|
- User access control with more granular permissions
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||||
1
dashboard_project/__init__.py
Normal file
1
dashboard_project/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# This file is intentionally left empty to mark the directory as a Python package
|
||||||
1
dashboard_project/accounts/__init__.py
Normal file
1
dashboard_project/accounts/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# This file is intentionally left empty to mark the directory as a Python package
|
||||||
77
dashboard_project/accounts/admin.py
Normal file
77
dashboard_project/accounts/admin.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# accounts/admin.py
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
|
||||||
|
from .forms import CustomUserChangeForm, CustomUserCreationForm
|
||||||
|
from .models import Company, CustomUser
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserAdmin(UserAdmin):
|
||||||
|
add_form = CustomUserCreationForm
|
||||||
|
form = CustomUserChangeForm
|
||||||
|
model = CustomUser
|
||||||
|
list_display = ("username", "email", "company", "is_company_admin", "is_staff")
|
||||||
|
list_filter = ("is_staff", "is_active", "company", "is_company_admin")
|
||||||
|
fieldsets = (
|
||||||
|
(None, {"fields": ("username", "email", "password")}),
|
||||||
|
(
|
||||||
|
"Permissions",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"is_active",
|
||||||
|
"is_staff",
|
||||||
|
"is_superuser",
|
||||||
|
"groups",
|
||||||
|
"user_permissions",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
("Company", {"fields": ("company", "is_company_admin")}),
|
||||||
|
("Important dates", {"fields": ("last_login", "date_joined")}),
|
||||||
|
)
|
||||||
|
add_fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"classes": ("wide",),
|
||||||
|
"fields": (
|
||||||
|
"username",
|
||||||
|
"email",
|
||||||
|
"password1",
|
||||||
|
"password2",
|
||||||
|
"company",
|
||||||
|
"is_company_admin",
|
||||||
|
"is_staff",
|
||||||
|
"is_active",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
search_fields = ("username", "email", "company__name")
|
||||||
|
ordering = ("username",)
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
if obj.is_superuser and not obj.company:
|
||||||
|
default_company, created = Company.objects.get_or_create(
|
||||||
|
name="Default Organization",
|
||||||
|
defaults={"description": "Default company for new superusers."},
|
||||||
|
)
|
||||||
|
obj.company = default_company
|
||||||
|
obj.is_company_admin = True # Optionally make the superuser an admin of this default company
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "created_at", "get_employee_count")
|
||||||
|
search_fields = ("name", "description")
|
||||||
|
|
||||||
|
def get_employee_count(self, obj):
|
||||||
|
return obj.employees.count()
|
||||||
|
|
||||||
|
get_employee_count.short_description = "Employees"
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(CustomUser, CustomUserAdmin)
|
||||||
|
admin.site.register(Company, CompanyAdmin)
|
||||||
8
dashboard_project/accounts/apps.py
Normal file
8
dashboard_project/accounts/apps.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# accounts/apps.py
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "accounts"
|
||||||
45
dashboard_project/accounts/forms.py
Normal file
45
dashboard_project/accounts/forms.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# accounts/forms.py
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.auth.forms import UserCreationForm
|
||||||
|
|
||||||
|
from .models import Company, CustomUser
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserCreationForm(UserCreationForm):
|
||||||
|
"""Form for creating new users"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CustomUser
|
||||||
|
fields = ("username", "email", "password1", "password2")
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
# Add help text for fields
|
||||||
|
self.fields["email"].required = True
|
||||||
|
self.fields["email"].help_text = "Required. Enter a valid email address."
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserChangeForm(forms.ModelForm):
|
||||||
|
"""Form for updating users"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CustomUser
|
||||||
|
fields = ("username", "email", "company", "is_company_admin")
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
# Only staff members can change company and admin status
|
||||||
|
if not kwargs.get("instance") or not kwargs.get("instance").is_staff:
|
||||||
|
if "company" in self.fields:
|
||||||
|
self.fields["company"].disabled = True
|
||||||
|
if "is_company_admin" in self.fields:
|
||||||
|
self.fields["is_company_admin"].disabled = True
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyForm(forms.ModelForm):
|
||||||
|
"""Form for creating and updating companies"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Company
|
||||||
|
fields = ("name", "description")
|
||||||
1
dashboard_project/accounts/migrations/__init__.py
Normal file
1
dashboard_project/accounts/migrations/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# This file is intentionally left empty to mark the directory as a Python package
|
||||||
34
dashboard_project/accounts/models.py
Normal file
34
dashboard_project/accounts/models.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# accounts/models.py
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUser(AbstractUser):
|
||||||
|
"""Custom user model to extend the default Django user"""
|
||||||
|
|
||||||
|
company = models.ForeignKey(
|
||||||
|
"Company",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="employees",
|
||||||
|
)
|
||||||
|
is_company_admin = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.username
|
||||||
|
|
||||||
|
|
||||||
|
class Company(models.Model):
|
||||||
|
"""Model for companies that will access the dashboard"""
|
||||||
|
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = "Companies"
|
||||||
0
dashboard_project/accounts/tests.py
Normal file
0
dashboard_project/accounts/tests.py
Normal file
28
dashboard_project/accounts/urls.py
Normal file
28
dashboard_project/accounts/urls.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# accounts/urls.py
|
||||||
|
|
||||||
|
from allauth.account import views as allauth_views
|
||||||
|
from django.contrib.auth import views as auth_views
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"login/",
|
||||||
|
auth_views.LoginView.as_view(template_name="accounts/login.html"),
|
||||||
|
name="login",
|
||||||
|
),
|
||||||
|
path("logout/", allauth_views.LogoutView.as_view(), name="logout"), # Use allauth logout view
|
||||||
|
path("register/", views.register_view, name="register"),
|
||||||
|
path("profile/", views.profile_view, name="profile"),
|
||||||
|
path(
|
||||||
|
"password_change/",
|
||||||
|
auth_views.PasswordChangeView.as_view(template_name="accounts/password_change.html"),
|
||||||
|
name="password_change",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"password_change/done/",
|
||||||
|
auth_views.PasswordChangeDoneView.as_view(template_name="accounts/password_change_done.html"),
|
||||||
|
name="password_change_done",
|
||||||
|
),
|
||||||
|
]
|
||||||
72
dashboard_project/accounts/views.py
Normal file
72
dashboard_project/accounts/views.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# accounts/views.py
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
|
from django.contrib.auth import login
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.shortcuts import redirect, render
|
||||||
|
|
||||||
|
from .forms import CompanyForm, CustomUserCreationForm
|
||||||
|
from .models import Company, CustomUser
|
||||||
|
|
||||||
|
|
||||||
|
def register_view(request):
|
||||||
|
"""View for user registration"""
|
||||||
|
if request.method == "POST":
|
||||||
|
form = CustomUserCreationForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
user = form.save()
|
||||||
|
login(request, user)
|
||||||
|
messages.success(request, "Registration successful.")
|
||||||
|
return redirect("dashboard")
|
||||||
|
else:
|
||||||
|
messages.error(request, "Registration failed. Please correct the errors.")
|
||||||
|
else:
|
||||||
|
form = CustomUserCreationForm()
|
||||||
|
|
||||||
|
return render(request, "accounts/register.html", {"form": form})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def profile_view(request):
|
||||||
|
"""View for user profile"""
|
||||||
|
user = request.user
|
||||||
|
company = user.company
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"user": user,
|
||||||
|
"company": company,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "accounts/profile.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@staff_member_required
|
||||||
|
def company_create_view(request):
|
||||||
|
"""View for creating companies (admin only)"""
|
||||||
|
if request.method == "POST":
|
||||||
|
form = CompanyForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
company = form.save()
|
||||||
|
messages.success(request, f"Company '{company.name}' created successfully.")
|
||||||
|
return redirect("admin:accounts_company_changelist")
|
||||||
|
else:
|
||||||
|
messages.error(request, "Failed to create company. Please correct the errors.")
|
||||||
|
else:
|
||||||
|
form = CompanyForm()
|
||||||
|
|
||||||
|
return render(request, "admin/accounts/company/create.html", {"form": form})
|
||||||
|
|
||||||
|
|
||||||
|
@staff_member_required
|
||||||
|
def company_users_view(request, company_id):
|
||||||
|
"""View for managing users in a company (admin only)"""
|
||||||
|
company = Company.objects.get(pk=company_id)
|
||||||
|
users = CustomUser.objects.filter(company=company)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"company": company,
|
||||||
|
"users": users,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "admin/accounts/company/users.html", context)
|
||||||
16
dashboard_project/asgi.py
Normal file
16
dashboard_project/asgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for dashboard_project project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
1
dashboard_project/dashboard/__init__.py
Normal file
1
dashboard_project/dashboard/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# This file is intentionally left empty to mark the directory as a Python package
|
||||||
65
dashboard_project/dashboard/admin.py
Normal file
65
dashboard_project/dashboard/admin.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# dashboard/admin.py
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import ChatSession, Dashboard, DataSource
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "company", "uploaded_at", "get_session_count")
|
||||||
|
list_filter = ("company", "uploaded_at")
|
||||||
|
search_fields = ("name", "description", "company__name")
|
||||||
|
ordering = ("-uploaded_at",)
|
||||||
|
|
||||||
|
def get_session_count(self, obj):
|
||||||
|
return obj.chat_sessions.count()
|
||||||
|
|
||||||
|
get_session_count.short_description = "Sessions"
|
||||||
|
|
||||||
|
|
||||||
|
class ChatSessionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"session_id",
|
||||||
|
"get_company",
|
||||||
|
"start_time",
|
||||||
|
"end_time",
|
||||||
|
"country",
|
||||||
|
"language",
|
||||||
|
"sentiment",
|
||||||
|
)
|
||||||
|
list_filter = (
|
||||||
|
"data_source__company",
|
||||||
|
"start_time",
|
||||||
|
"country",
|
||||||
|
"language",
|
||||||
|
"sentiment",
|
||||||
|
"escalated",
|
||||||
|
"forwarded_hr",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"session_id",
|
||||||
|
"country",
|
||||||
|
"language",
|
||||||
|
"initial_msg",
|
||||||
|
"full_transcript",
|
||||||
|
)
|
||||||
|
ordering = ("-start_time",)
|
||||||
|
|
||||||
|
def get_company(self, obj):
|
||||||
|
return obj.data_source.company.name
|
||||||
|
|
||||||
|
get_company.short_description = "Company"
|
||||||
|
get_company.admin_order_field = "data_source__company__name"
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "company", "created_at", "updated_at")
|
||||||
|
list_filter = ("company", "created_at")
|
||||||
|
search_fields = ("name", "description", "company__name")
|
||||||
|
filter_horizontal = ("data_sources",)
|
||||||
|
ordering = ("-updated_at",)
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(DataSource, DataSourceAdmin)
|
||||||
|
admin.site.register(ChatSession, ChatSessionAdmin)
|
||||||
|
admin.site.register(Dashboard, DashboardAdmin)
|
||||||
8
dashboard_project/dashboard/apps.py
Normal file
8
dashboard_project/dashboard/apps.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# dashboard/apps.py
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "dashboard"
|
||||||
49
dashboard_project/dashboard/forms.py
Normal file
49
dashboard_project/dashboard/forms.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# dashboard/forms.py
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from .models import Dashboard, DataSource
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceUploadForm(forms.ModelForm):
|
||||||
|
"""Form for uploading CSV files"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DataSource
|
||||||
|
fields = ["name", "description", "file"]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.company = kwargs.pop("company", None)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
instance = super().save(commit=False)
|
||||||
|
if self.company:
|
||||||
|
instance.company = self.company
|
||||||
|
if commit:
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardForm(forms.ModelForm):
|
||||||
|
"""Form for creating and editing dashboards"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Dashboard
|
||||||
|
fields = ["name", "description", "data_sources"]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.company = kwargs.pop("company", None)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if self.company:
|
||||||
|
self.fields["data_sources"].queryset = DataSource.objects.filter(company=self.company)
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
instance = super().save(commit=False)
|
||||||
|
if self.company:
|
||||||
|
instance.company = self.company
|
||||||
|
if commit:
|
||||||
|
instance.save()
|
||||||
|
self.save_m2m()
|
||||||
|
return instance
|
||||||
2
dashboard_project/dashboard/management/__init__.py
Normal file
2
dashboard_project/dashboard/management/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# dashboard/management/__init__.py
|
||||||
|
# This file is intentionally left empty to mark the directory as a Python package
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
# dashboard/management/commands/__init__.py
|
||||||
|
# This file is intentionally left empty to mark the directory as a Python package
|
||||||
@ -0,0 +1,277 @@
|
|||||||
|
# dashboard/management/commands/create_sample_data.py
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import random
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from accounts.models import Company
|
||||||
|
from dashboard.models import ChatSession, Dashboard, DataSource
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Create sample data for testing"
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
self.stdout.write("Creating sample data...")
|
||||||
|
|
||||||
|
# Create admin user if it doesn't exist
|
||||||
|
if not User.objects.filter(username="admin").exists():
|
||||||
|
admin_user = User.objects.create_superuser(username="admin", email="admin@example.com", password="admin123")
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Created admin user: {admin_user.username}"))
|
||||||
|
else:
|
||||||
|
admin_user = User.objects.get(username="admin")
|
||||||
|
self.stdout.write(f"Admin user already exists: {admin_user.username}")
|
||||||
|
|
||||||
|
# Create companies
|
||||||
|
companies = []
|
||||||
|
company_names = ["Acme Inc.", "TechCorp", "GlobalServices"]
|
||||||
|
|
||||||
|
for name in company_names:
|
||||||
|
company, created = Company.objects.get_or_create(
|
||||||
|
name=name, defaults={"description": f"Sample company: {name}"}
|
||||||
|
)
|
||||||
|
companies.append(company)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Created company: {company.name}"))
|
||||||
|
else:
|
||||||
|
self.stdout.write(f"Company already exists: {company.name}")
|
||||||
|
|
||||||
|
# Create users for each company
|
||||||
|
for i, company in enumerate(companies):
|
||||||
|
# Company admin
|
||||||
|
username = f"admin_{company.name.lower().replace(' ', '_')}"
|
||||||
|
if not User.objects.filter(username=username).exists():
|
||||||
|
user = User.objects.create_user(
|
||||||
|
username=username,
|
||||||
|
email=f"{username}@example.com",
|
||||||
|
password="password123",
|
||||||
|
company=company,
|
||||||
|
is_company_admin=True,
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Created company admin: {user.username}"))
|
||||||
|
|
||||||
|
# Regular users
|
||||||
|
for j in range(2):
|
||||||
|
username = f"user_{company.name.lower().replace(' ', '_')}_{j + 1}"
|
||||||
|
if not User.objects.filter(username=username).exists():
|
||||||
|
user = User.objects.create_user(
|
||||||
|
username=username,
|
||||||
|
email=f"{username}@example.com",
|
||||||
|
password="password123",
|
||||||
|
company=company,
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Created user: {user.username}"))
|
||||||
|
|
||||||
|
# Create sample data for each company
|
||||||
|
for company in companies:
|
||||||
|
self._create_sample_data_for_company(company)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS("Sample data created successfully!"))
|
||||||
|
|
||||||
|
def _create_sample_data_for_company(self, company):
|
||||||
|
# Create sample CSV data
|
||||||
|
csv_data = self._generate_sample_csv_data(company.name)
|
||||||
|
|
||||||
|
# Create data source
|
||||||
|
data_source_name = f"{company.name} Chat Data"
|
||||||
|
try:
|
||||||
|
data_source = DataSource.objects.get(name=data_source_name, company=company)
|
||||||
|
self.stdout.write(f"Data source already exists: {data_source.name}")
|
||||||
|
except DataSource.DoesNotExist:
|
||||||
|
# Create file from CSV data
|
||||||
|
csv_file = ContentFile(csv_data.encode("utf-8"))
|
||||||
|
data_source = DataSource.objects.create(
|
||||||
|
name=data_source_name,
|
||||||
|
description=f"Sample chat data for {company.name}",
|
||||||
|
company=company,
|
||||||
|
)
|
||||||
|
data_source.file.save(f"{company.name.lower().replace(' ', '_')}_chat_data.csv", csv_file)
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Created data source: {data_source.name}"))
|
||||||
|
|
||||||
|
# Parse CSV data and create chat sessions
|
||||||
|
reader = csv.DictReader(io.StringIO(csv_data))
|
||||||
|
for row in reader:
|
||||||
|
# Convert datetime strings to datetime objects
|
||||||
|
start_time = datetime.strptime(row["start_time"], "%Y-%m-%d %H:%M:%S")
|
||||||
|
end_time = datetime.strptime(row["end_time"], "%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
# Convert boolean strings to actual booleans
|
||||||
|
escalated = row["escalated"].lower() in ["true", "yes", "1", "t", "y"]
|
||||||
|
forwarded_hr = row["forwarded_hr"].lower() in [
|
||||||
|
"true",
|
||||||
|
"yes",
|
||||||
|
"1",
|
||||||
|
"t",
|
||||||
|
"y",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create chat session
|
||||||
|
ChatSession.objects.create(
|
||||||
|
data_source=data_source,
|
||||||
|
session_id=row["session_id"],
|
||||||
|
start_time=timezone.make_aware(start_time),
|
||||||
|
end_time=timezone.make_aware(end_time),
|
||||||
|
ip_address=row["ip_address"],
|
||||||
|
country=row["country"],
|
||||||
|
language=row["language"],
|
||||||
|
messages_sent=int(row["messages_sent"]),
|
||||||
|
sentiment=row["sentiment"],
|
||||||
|
escalated=escalated,
|
||||||
|
forwarded_hr=forwarded_hr,
|
||||||
|
full_transcript=row["full_transcript"],
|
||||||
|
avg_response_time=float(row["avg_response_time"]),
|
||||||
|
tokens=int(row["tokens"]),
|
||||||
|
tokens_eur=float(row["tokens_eur"]),
|
||||||
|
category=row["category"],
|
||||||
|
initial_msg=row["initial_msg"],
|
||||||
|
user_rating=row["user_rating"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Created {reader.line_num} chat sessions"))
|
||||||
|
|
||||||
|
# Create default dashboard
|
||||||
|
dashboard_name = f"{company.name} Dashboard"
|
||||||
|
try:
|
||||||
|
dashboard = Dashboard.objects.get(name=dashboard_name, company=company)
|
||||||
|
self.stdout.write(f"Dashboard already exists: {dashboard.name}")
|
||||||
|
except Dashboard.DoesNotExist:
|
||||||
|
dashboard = Dashboard.objects.create(
|
||||||
|
name=dashboard_name,
|
||||||
|
description=f"Default dashboard for {company.name}",
|
||||||
|
company=company,
|
||||||
|
)
|
||||||
|
dashboard.data_sources.add(data_source)
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Created dashboard: {dashboard.name}"))
|
||||||
|
|
||||||
|
def _generate_sample_csv_data(self, company_name):
|
||||||
|
"""Generate sample CSV data for a company"""
|
||||||
|
rows = []
|
||||||
|
headers = [
|
||||||
|
"session_id",
|
||||||
|
"start_time",
|
||||||
|
"end_time",
|
||||||
|
"ip_address",
|
||||||
|
"country",
|
||||||
|
"language",
|
||||||
|
"messages_sent",
|
||||||
|
"sentiment",
|
||||||
|
"escalated",
|
||||||
|
"forwarded_hr",
|
||||||
|
"full_transcript",
|
||||||
|
"avg_response_time",
|
||||||
|
"tokens",
|
||||||
|
"tokens_eur",
|
||||||
|
"category",
|
||||||
|
"initial_msg",
|
||||||
|
"user_rating",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Sample data for generating random values
|
||||||
|
countries = [
|
||||||
|
"USA",
|
||||||
|
"UK",
|
||||||
|
"Germany",
|
||||||
|
"France",
|
||||||
|
"Spain",
|
||||||
|
"Italy",
|
||||||
|
"Japan",
|
||||||
|
"Australia",
|
||||||
|
"Canada",
|
||||||
|
"Brazil",
|
||||||
|
]
|
||||||
|
languages = ["English", "Spanish", "German", "French", "Japanese", "Portuguese"]
|
||||||
|
sentiments = [
|
||||||
|
"Positive",
|
||||||
|
"Negative",
|
||||||
|
"Neutral",
|
||||||
|
"Very Positive",
|
||||||
|
"Very Negative",
|
||||||
|
]
|
||||||
|
categories = ["Support", "Sales", "Technical", "Billing", "General"]
|
||||||
|
ratings = ["Excellent", "Good", "Average", "Poor", "Terrible", ""]
|
||||||
|
|
||||||
|
# Generate rows
|
||||||
|
num_rows = random.randint(50, 100)
|
||||||
|
|
||||||
|
for i in range(num_rows):
|
||||||
|
# Generate random dates in the last 30 days
|
||||||
|
end_date = datetime.now() - timedelta(days=random.randint(0, 30))
|
||||||
|
start_date = end_date - timedelta(minutes=random.randint(5, 60))
|
||||||
|
|
||||||
|
# Generate random IP address
|
||||||
|
ip = ".".join(str(random.randint(0, 255)) for _ in range(4))
|
||||||
|
|
||||||
|
# Random country and language
|
||||||
|
country = random.choice(countries)
|
||||||
|
language = random.choice(languages)
|
||||||
|
|
||||||
|
# Random message count
|
||||||
|
messages_sent = random.randint(3, 20)
|
||||||
|
|
||||||
|
# Random sentiment
|
||||||
|
sentiment = random.choice(sentiments)
|
||||||
|
|
||||||
|
# Random escalation and forwarding
|
||||||
|
escalated = random.random() < 0.2 # 20% chance of escalation
|
||||||
|
forwarded_hr = random.random() < 0.1 # 10% chance of forwarding to HR
|
||||||
|
|
||||||
|
# Generate a sample transcript
|
||||||
|
transcript = (
|
||||||
|
"User: Hello, I need help with my account.\n"
|
||||||
|
"Agent: Hello! I'd be happy to help. What seems to be the issue?\n"
|
||||||
|
"User: I can't log in to my account.\n"
|
||||||
|
"Agent: I understand. Let me help you reset your password."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Random response time, tokens, and cost
|
||||||
|
avg_response_time = round(random.uniform(0.5, 10.0), 2)
|
||||||
|
tokens = random.randint(100, 2000)
|
||||||
|
tokens_eur = round(tokens * 0.00002, 4) # Example rate: €0.00002 per token
|
||||||
|
|
||||||
|
# Random category
|
||||||
|
category = random.choice(categories)
|
||||||
|
|
||||||
|
# Initial message
|
||||||
|
initial_msg = "Hello, I need help with my account."
|
||||||
|
|
||||||
|
# Random rating
|
||||||
|
user_rating = random.choice(ratings)
|
||||||
|
|
||||||
|
# Create row
|
||||||
|
row = {
|
||||||
|
"session_id": f"{company_name.lower().replace(' ', '_')}_{i + 1}",
|
||||||
|
"start_time": start_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"end_time": end_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"ip_address": ip,
|
||||||
|
"country": country,
|
||||||
|
"language": language,
|
||||||
|
"messages_sent": str(messages_sent),
|
||||||
|
"sentiment": sentiment,
|
||||||
|
"escalated": str(escalated),
|
||||||
|
"forwarded_hr": str(forwarded_hr),
|
||||||
|
"full_transcript": transcript,
|
||||||
|
"avg_response_time": str(avg_response_time),
|
||||||
|
"tokens": str(tokens),
|
||||||
|
"tokens_eur": str(tokens_eur),
|
||||||
|
"category": category,
|
||||||
|
"initial_msg": initial_msg,
|
||||||
|
"user_rating": user_rating,
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
# Write to CSV string
|
||||||
|
output = io.StringIO()
|
||||||
|
writer = csv.DictWriter(output, fieldnames=headers)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(rows)
|
||||||
|
|
||||||
|
return output.getvalue()
|
||||||
1
dashboard_project/dashboard/migrations/__init__.py
Normal file
1
dashboard_project/dashboard/migrations/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# This file is intentionally left empty to mark the directory as a Python package
|
||||||
57
dashboard_project/dashboard/models.py
Normal file
57
dashboard_project/dashboard/models.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# dashboard/models.py
|
||||||
|
|
||||||
|
from accounts.models import Company
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class DataSource(models.Model):
|
||||||
|
"""Model for uploaded data sources (CSV files)"""
|
||||||
|
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
file = models.FileField(upload_to="data_sources/")
|
||||||
|
uploaded_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name="data_sources")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class ChatSession(models.Model):
|
||||||
|
"""Model to store parsed chat session data from CSV"""
|
||||||
|
|
||||||
|
data_source = models.ForeignKey(DataSource, on_delete=models.CASCADE, related_name="chat_sessions")
|
||||||
|
session_id = models.CharField(max_length=255)
|
||||||
|
start_time = models.DateTimeField(null=True, blank=True)
|
||||||
|
end_time = models.DateTimeField(null=True, blank=True)
|
||||||
|
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||||
|
country = models.CharField(max_length=100, blank=True)
|
||||||
|
language = models.CharField(max_length=50, blank=True)
|
||||||
|
messages_sent = models.IntegerField(default=0)
|
||||||
|
sentiment = models.CharField(max_length=50, blank=True)
|
||||||
|
escalated = models.BooleanField(default=False)
|
||||||
|
forwarded_hr = models.BooleanField(default=False)
|
||||||
|
full_transcript = models.TextField(blank=True)
|
||||||
|
avg_response_time = models.FloatField(null=True, blank=True)
|
||||||
|
tokens = models.IntegerField(default=0)
|
||||||
|
tokens_eur = models.FloatField(null=True, blank=True)
|
||||||
|
category = models.CharField(max_length=100, blank=True)
|
||||||
|
initial_msg = models.TextField(blank=True)
|
||||||
|
user_rating = models.CharField(max_length=50, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Session {self.session_id}"
|
||||||
|
|
||||||
|
|
||||||
|
class Dashboard(models.Model):
|
||||||
|
"""Model for custom dashboards that can be created by users"""
|
||||||
|
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name="dashboards")
|
||||||
|
data_sources = models.ManyToManyField(DataSource, related_name="dashboards")
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
2
dashboard_project/dashboard/templatetags/__init__.py
Normal file
2
dashboard_project/dashboard/templatetags/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# dashboard/templatetags/__init__.py
|
||||||
|
# This file is intentionally left empty to mark the directory as a Python package
|
||||||
56
dashboard_project/dashboard/templatetags/dashboard_extras.py
Normal file
56
dashboard_project/dashboard/templatetags/dashboard_extras.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# dashboard/templatetags/dashboard_extras.py
|
||||||
|
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def split(value, delimiter):
|
||||||
|
"""Split a string into a list based on the delimiter"""
|
||||||
|
return value.split(delimiter)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def get_item(dictionary, key):
|
||||||
|
"""Get an item from a dictionary using the key"""
|
||||||
|
return dictionary.get(key)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def truncate_middle(value, max_length):
|
||||||
|
"""Truncate a string in the middle, keeping the beginning and end"""
|
||||||
|
if len(value) <= max_length:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# Calculate how many characters to keep at the start and end
|
||||||
|
half_max = max_length // 2
|
||||||
|
start = value[:half_max]
|
||||||
|
end = value[-half_max:]
|
||||||
|
|
||||||
|
return f"{start}...{end}"
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def format_duration(seconds):
|
||||||
|
"""Format seconds into a human-readable duration"""
|
||||||
|
if not seconds:
|
||||||
|
return "0s"
|
||||||
|
|
||||||
|
minutes, seconds = divmod(int(seconds), 60)
|
||||||
|
hours, minutes = divmod(minutes, 60)
|
||||||
|
|
||||||
|
if hours > 0:
|
||||||
|
return f"{hours}h {minutes}m {seconds}s"
|
||||||
|
elif minutes > 0:
|
||||||
|
return f"{minutes}m {seconds}s"
|
||||||
|
else:
|
||||||
|
return f"{seconds}s"
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def url_replace(request, field, value):
|
||||||
|
"""Replace a GET parameter in the current URL"""
|
||||||
|
dict_ = request.GET.copy()
|
||||||
|
dict_[field] = value
|
||||||
|
return dict_.urlencode()
|
||||||
0
dashboard_project/dashboard/tests.py
Normal file
0
dashboard_project/dashboard/tests.py
Normal file
43
dashboard_project/dashboard/urls.py
Normal file
43
dashboard_project/dashboard/urls.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# dashboard/urls.py
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", views.dashboard_view, name="dashboard"),
|
||||||
|
path("upload/", views.upload_data_view, name="upload_data"),
|
||||||
|
path(
|
||||||
|
"data-source/<int:data_source_id>/",
|
||||||
|
views.data_source_detail_view,
|
||||||
|
name="data_source_detail",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"chat-session/<str:session_id>/",
|
||||||
|
views.chat_session_detail_view,
|
||||||
|
name="chat_session_detail",
|
||||||
|
),
|
||||||
|
path("dashboard/create/", views.create_dashboard_view, name="create_dashboard"),
|
||||||
|
path(
|
||||||
|
"dashboard/<int:dashboard_id>/edit/",
|
||||||
|
views.edit_dashboard_view,
|
||||||
|
name="edit_dashboard",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"dashboard/<int:dashboard_id>/delete/",
|
||||||
|
views.delete_dashboard_view,
|
||||||
|
name="delete_dashboard",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"data-source/<int:data_source_id>/delete/",
|
||||||
|
views.delete_data_source_view,
|
||||||
|
name="delete_data_source",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"api/dashboard/<int:dashboard_id>/data/",
|
||||||
|
views.dashboard_data_api,
|
||||||
|
name="dashboard_data_api",
|
||||||
|
),
|
||||||
|
path("search/", views.search_chat_sessions, name="search_chat_sessions"),
|
||||||
|
path("data-view/", views.data_view, name="data_view"),
|
||||||
|
]
|
||||||
161
dashboard_project/dashboard/utils.py
Normal file
161
dashboard_project/dashboard/utils.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
# dashboard/utils.py
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.timezone import make_aware
|
||||||
|
|
||||||
|
from .models import ChatSession
|
||||||
|
|
||||||
|
|
||||||
|
def process_csv_file(data_source):
|
||||||
|
"""
|
||||||
|
Process the uploaded CSV file and create ChatSession objects
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_source: DataSource model instance containing the CSV file
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Read the CSV file
|
||||||
|
file_path = data_source.file.path
|
||||||
|
df = pd.read_csv(file_path)
|
||||||
|
|
||||||
|
# Process each row and create ChatSession objects
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
# Handle datetime fields
|
||||||
|
start_time = None
|
||||||
|
end_time = None
|
||||||
|
|
||||||
|
if "start_time" in row and pd.notna(row["start_time"]):
|
||||||
|
try:
|
||||||
|
start_time = make_aware(pd.to_datetime(row["start_time"]))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if "end_time" in row and pd.notna(row["end_time"]):
|
||||||
|
try:
|
||||||
|
end_time = make_aware(pd.to_datetime(row["end_time"]))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Convert boolean fields
|
||||||
|
escalated = str(row.get("escalated", "")).lower() in [
|
||||||
|
"true",
|
||||||
|
"yes",
|
||||||
|
"1",
|
||||||
|
"t",
|
||||||
|
"y",
|
||||||
|
]
|
||||||
|
forwarded_hr = str(row.get("forwarded_hr", "")).lower() in [
|
||||||
|
"true",
|
||||||
|
"yes",
|
||||||
|
"1",
|
||||||
|
"t",
|
||||||
|
"y",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create ChatSession object
|
||||||
|
session = ChatSession(
|
||||||
|
data_source=data_source,
|
||||||
|
session_id=str(row.get("session_id", "")),
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
ip_address=row.get("ip_address") if pd.notna(row.get("ip_address", np.nan)) else None,
|
||||||
|
country=str(row.get("country", "")),
|
||||||
|
language=str(row.get("language", "")),
|
||||||
|
messages_sent=int(row.get("messages_sent", 0)) if pd.notna(row.get("messages_sent", np.nan)) else 0,
|
||||||
|
sentiment=str(row.get("sentiment", "")),
|
||||||
|
escalated=escalated,
|
||||||
|
forwarded_hr=forwarded_hr,
|
||||||
|
full_transcript=str(row.get("full_transcript", "")),
|
||||||
|
avg_response_time=float(row.get("avg_response_time", 0))
|
||||||
|
if pd.notna(row.get("avg_response_time", np.nan))
|
||||||
|
else None,
|
||||||
|
tokens=int(row.get("tokens", 0)) if pd.notna(row.get("tokens", np.nan)) else 0,
|
||||||
|
tokens_eur=float(row.get("tokens_eur", 0)) if pd.notna(row.get("tokens_eur", np.nan)) else None,
|
||||||
|
category=str(row.get("category", "")),
|
||||||
|
initial_msg=str(row.get("initial_msg", "")),
|
||||||
|
user_rating=str(row.get("user_rating", "")),
|
||||||
|
)
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
return True, f"Successfully processed {len(df)} records."
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Error processing CSV file: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
def generate_dashboard_data(data_sources):
|
||||||
|
"""
|
||||||
|
Generate aggregated data for dashboard visualization
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_sources: QuerySet of DataSource objects
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary containing aggregated data for various charts
|
||||||
|
"""
|
||||||
|
# Get all chat sessions for the selected data sources
|
||||||
|
chat_sessions = ChatSession.objects.filter(data_source__in=data_sources)
|
||||||
|
|
||||||
|
if not chat_sessions.exists():
|
||||||
|
return {
|
||||||
|
"total_sessions": 0,
|
||||||
|
"avg_response_time": 0,
|
||||||
|
"total_tokens": 0,
|
||||||
|
"total_cost": 0,
|
||||||
|
"sentiment_data": [],
|
||||||
|
"country_data": [],
|
||||||
|
"category_data": [],
|
||||||
|
"time_series_data": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Basic statistics
|
||||||
|
total_sessions = chat_sessions.count()
|
||||||
|
avg_response_time = (
|
||||||
|
chat_sessions.filter(avg_response_time__isnull=False).aggregate(avg=models.Avg("avg_response_time"))["avg"] or 0
|
||||||
|
)
|
||||||
|
total_tokens = chat_sessions.aggregate(sum=models.Sum("tokens"))["sum"] or 0
|
||||||
|
total_cost = chat_sessions.filter(tokens_eur__isnull=False).aggregate(sum=models.Sum("tokens_eur"))["sum"] or 0
|
||||||
|
|
||||||
|
# Sentiment distribution
|
||||||
|
sentiment_data = (
|
||||||
|
chat_sessions.exclude(sentiment="").values("sentiment").annotate(count=models.Count("id")).order_by("-count")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Country distribution
|
||||||
|
country_data = (
|
||||||
|
chat_sessions.exclude(country="")
|
||||||
|
.values("country")
|
||||||
|
.annotate(count=models.Count("id"))
|
||||||
|
.order_by("-count")[:10] # Top 10 countries
|
||||||
|
)
|
||||||
|
|
||||||
|
# Category distribution
|
||||||
|
category_data = (
|
||||||
|
chat_sessions.exclude(category="").values("category").annotate(count=models.Count("id")).order_by("-count")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Time series data (sessions per day)
|
||||||
|
time_series_query = (
|
||||||
|
chat_sessions.filter(start_time__isnull=False)
|
||||||
|
.annotate(date=models.functions.TruncDate("start_time"))
|
||||||
|
.values("date")
|
||||||
|
.annotate(count=models.Count("id"))
|
||||||
|
.order_by("date")
|
||||||
|
)
|
||||||
|
|
||||||
|
time_series_data = [
|
||||||
|
{"date": entry["date"].strftime("%Y-%m-%d"), "count": entry["count"]} for entry in time_series_query
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_sessions": total_sessions,
|
||||||
|
"avg_response_time": round(avg_response_time, 2),
|
||||||
|
"total_tokens": total_tokens,
|
||||||
|
"total_cost": round(total_cost, 2),
|
||||||
|
"sentiment_data": list(sentiment_data),
|
||||||
|
"country_data": list(country_data),
|
||||||
|
"category_data": list(category_data),
|
||||||
|
"time_series_data": time_series_data,
|
||||||
|
}
|
||||||
452
dashboard_project/dashboard/views.py
Normal file
452
dashboard_project/dashboard/views.py
Normal file
@ -0,0 +1,452 @@
|
|||||||
|
# dashboard/views.py
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db.models import Avg, Q
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from .forms import DashboardForm, DataSourceUploadForm
|
||||||
|
from .models import ChatSession, Dashboard, DataSource
|
||||||
|
from .utils import generate_dashboard_data, process_csv_file
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def dashboard_view(request):
|
||||||
|
"""Main dashboard view"""
|
||||||
|
user = request.user
|
||||||
|
company = user.company
|
||||||
|
|
||||||
|
if not company:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
"You are not associated with any company. Please contact an administrator.",
|
||||||
|
)
|
||||||
|
return render(request, "dashboard/no_company.html")
|
||||||
|
|
||||||
|
# Get the user's dashboards or create a default one
|
||||||
|
dashboards = Dashboard.objects.filter(company=company)
|
||||||
|
|
||||||
|
if not dashboards.exists():
|
||||||
|
# Create a default dashboard if none exists
|
||||||
|
data_sources = DataSource.objects.filter(company=company)
|
||||||
|
if data_sources.exists():
|
||||||
|
default_dashboard = Dashboard.objects.create(
|
||||||
|
name="Default Dashboard",
|
||||||
|
description="Automatically created dashboard",
|
||||||
|
company=company,
|
||||||
|
)
|
||||||
|
default_dashboard.data_sources.set(data_sources)
|
||||||
|
dashboards = [default_dashboard]
|
||||||
|
else:
|
||||||
|
# No data sources available
|
||||||
|
return redirect("upload_data")
|
||||||
|
|
||||||
|
# Use the first dashboard by default or the one specified in the request
|
||||||
|
selected_dashboard_id = request.GET.get("dashboard_id")
|
||||||
|
if selected_dashboard_id:
|
||||||
|
selected_dashboard = get_object_or_404(Dashboard, id=selected_dashboard_id, company=company)
|
||||||
|
else:
|
||||||
|
selected_dashboard = dashboards.first()
|
||||||
|
|
||||||
|
# Generate dashboard data
|
||||||
|
dashboard_data = generate_dashboard_data(selected_dashboard.data_sources.all())
|
||||||
|
|
||||||
|
# Convert dashboard data to JSON for use in JavaScript
|
||||||
|
dashboard_data_json = json.dumps(
|
||||||
|
{
|
||||||
|
"sentiment_data": dashboard_data["sentiment_data"],
|
||||||
|
"country_data": dashboard_data["country_data"],
|
||||||
|
"category_data": dashboard_data["category_data"],
|
||||||
|
"time_series_data": dashboard_data["time_series_data"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"dashboards": dashboards,
|
||||||
|
"selected_dashboard": selected_dashboard,
|
||||||
|
"dashboard_data": dashboard_data,
|
||||||
|
"dashboard_data_json": dashboard_data_json,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "dashboard/dashboard.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def upload_data_view(request):
|
||||||
|
"""View for uploading CSV files"""
|
||||||
|
user = request.user
|
||||||
|
company = user.company
|
||||||
|
|
||||||
|
if not company:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
"You are not associated with any company. Please contact an administrator.",
|
||||||
|
)
|
||||||
|
return redirect("dashboard")
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = DataSourceUploadForm(request.POST, request.FILES, company=company)
|
||||||
|
if form.is_valid():
|
||||||
|
data_source = form.save()
|
||||||
|
|
||||||
|
# Process the uploaded CSV file
|
||||||
|
success, message = process_csv_file(data_source)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
messages.success(request, f"File uploaded successfully. {message}")
|
||||||
|
|
||||||
|
# Add the new data source to all existing dashboards
|
||||||
|
dashboards = Dashboard.objects.filter(company=company)
|
||||||
|
for dashboard in dashboards:
|
||||||
|
dashboard.data_sources.add(data_source)
|
||||||
|
|
||||||
|
return redirect("dashboard")
|
||||||
|
else:
|
||||||
|
# If processing failed, delete the data source
|
||||||
|
data_source.delete()
|
||||||
|
messages.error(request, message)
|
||||||
|
else:
|
||||||
|
messages.error(request, "Form is invalid. Please correct the errors.")
|
||||||
|
else:
|
||||||
|
form = DataSourceUploadForm()
|
||||||
|
|
||||||
|
# List existing data sources
|
||||||
|
data_sources = DataSource.objects.filter(company=company).order_by("-uploaded_at")
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"form": form,
|
||||||
|
"data_sources": data_sources,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "dashboard/upload.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def data_source_detail_view(request, data_source_id):
|
||||||
|
"""View for viewing details of a data source"""
|
||||||
|
user = request.user
|
||||||
|
company = user.company
|
||||||
|
|
||||||
|
if not company:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
"You are not associated with any company. Please contact an administrator.",
|
||||||
|
)
|
||||||
|
return redirect("dashboard")
|
||||||
|
|
||||||
|
data_source = get_object_or_404(DataSource, id=data_source_id, company=company)
|
||||||
|
|
||||||
|
# Get all chat sessions for this data source
|
||||||
|
chat_sessions = ChatSession.objects.filter(data_source=data_source).order_by("-start_time")
|
||||||
|
|
||||||
|
# Pagination
|
||||||
|
paginator = Paginator(chat_sessions, 20) # Show 20 records per page
|
||||||
|
page_number = request.GET.get("page")
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"data_source": data_source,
|
||||||
|
"page_obj": page_obj,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "dashboard/data_source_detail.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def chat_session_detail_view(request, session_id):
|
||||||
|
"""View for viewing details of a chat session"""
|
||||||
|
user = request.user
|
||||||
|
company = user.company
|
||||||
|
|
||||||
|
if not company:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
"You are not associated with any company. Please contact an administrator.",
|
||||||
|
)
|
||||||
|
return redirect("dashboard")
|
||||||
|
|
||||||
|
chat_session = get_object_or_404(ChatSession, session_id=session_id, data_source__company=company)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"session": chat_session,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "dashboard/chat_session_detail.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def create_dashboard_view(request):
|
||||||
|
"""View for creating a custom dashboard"""
|
||||||
|
user = request.user
|
||||||
|
company = user.company
|
||||||
|
|
||||||
|
if not company:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
"You are not associated with any company. Please contact an administrator.",
|
||||||
|
)
|
||||||
|
return redirect("dashboard")
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = DashboardForm(request.POST, company=company)
|
||||||
|
if form.is_valid():
|
||||||
|
dashboard = form.save()
|
||||||
|
messages.success(request, f"Dashboard '{dashboard.name}' created successfully.")
|
||||||
|
return redirect("dashboard")
|
||||||
|
else:
|
||||||
|
messages.error(request, "Failed to create dashboard. Please correct the errors.")
|
||||||
|
else:
|
||||||
|
form = DashboardForm(company=company)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"form": form,
|
||||||
|
"is_create": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "dashboard/dashboard_form.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def edit_dashboard_view(request, dashboard_id):
|
||||||
|
"""View for editing a dashboard"""
|
||||||
|
user = request.user
|
||||||
|
company = user.company
|
||||||
|
|
||||||
|
if not company:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
"You are not associated with any company. Please contact an administrator.",
|
||||||
|
)
|
||||||
|
return redirect("dashboard")
|
||||||
|
|
||||||
|
dashboard = get_object_or_404(Dashboard, id=dashboard_id, company=company)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = DashboardForm(request.POST, instance=dashboard, company=company)
|
||||||
|
if form.is_valid():
|
||||||
|
dashboard = form.save()
|
||||||
|
messages.success(request, f"Dashboard '{dashboard.name}' updated successfully.")
|
||||||
|
return redirect("dashboard")
|
||||||
|
else:
|
||||||
|
messages.error(request, "Failed to update dashboard. Please correct the errors.")
|
||||||
|
else:
|
||||||
|
form = DashboardForm(instance=dashboard, company=company)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"form": form,
|
||||||
|
"dashboard": dashboard,
|
||||||
|
"is_create": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "dashboard/dashboard_form.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def delete_dashboard_view(request, dashboard_id):
|
||||||
|
"""View for deleting a dashboard"""
|
||||||
|
user = request.user
|
||||||
|
company = user.company
|
||||||
|
|
||||||
|
if not company:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
"You are not associated with any company. Please contact an administrator.",
|
||||||
|
)
|
||||||
|
return redirect("dashboard")
|
||||||
|
|
||||||
|
dashboard = get_object_or_404(Dashboard, id=dashboard_id, company=company)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
dashboard_name = dashboard.name
|
||||||
|
dashboard.delete()
|
||||||
|
messages.success(request, f"Dashboard '{dashboard_name}' deleted successfully.")
|
||||||
|
return redirect("dashboard")
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"dashboard": dashboard,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "dashboard/dashboard_confirm_delete.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def delete_data_source_view(request, data_source_id):
|
||||||
|
"""View for deleting a data source"""
|
||||||
|
user = request.user
|
||||||
|
company = user.company
|
||||||
|
|
||||||
|
if not company:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
"You are not associated with any company. Please contact an administrator.",
|
||||||
|
)
|
||||||
|
return redirect("dashboard")
|
||||||
|
|
||||||
|
data_source = get_object_or_404(DataSource, id=data_source_id, company=company)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
data_source_name = data_source.name
|
||||||
|
data_source.delete()
|
||||||
|
messages.success(request, f"Data source '{data_source_name}' deleted successfully.")
|
||||||
|
return redirect("upload_data")
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"data_source": data_source,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "dashboard/data_source_confirm_delete.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
# API views for dashboard data
|
||||||
|
@login_required
|
||||||
|
def dashboard_data_api(request, dashboard_id):
|
||||||
|
"""API endpoint for dashboard data"""
|
||||||
|
user = request.user
|
||||||
|
company = user.company
|
||||||
|
|
||||||
|
if not company:
|
||||||
|
return JsonResponse({"error": "User not associated with a company"}, status=403)
|
||||||
|
|
||||||
|
dashboard = get_object_or_404(Dashboard, id=dashboard_id, company=company)
|
||||||
|
dashboard_data = generate_dashboard_data(dashboard.data_sources.all())
|
||||||
|
|
||||||
|
return JsonResponse(dashboard_data)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def search_chat_sessions(request):
|
||||||
|
"""View for searching chat sessions"""
|
||||||
|
user = request.user
|
||||||
|
company = user.company
|
||||||
|
|
||||||
|
if not company:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
"You are not associated with any company. Please contact an administrator.",
|
||||||
|
)
|
||||||
|
return redirect("dashboard")
|
||||||
|
|
||||||
|
query = request.GET.get("q", "")
|
||||||
|
data_source_id = request.GET.get("data_source_id")
|
||||||
|
|
||||||
|
# Base queryset
|
||||||
|
chat_sessions = ChatSession.objects.filter(data_source__company=company)
|
||||||
|
|
||||||
|
# Filter by data source if provided
|
||||||
|
if data_source_id:
|
||||||
|
chat_sessions = chat_sessions.filter(data_source_id=data_source_id)
|
||||||
|
|
||||||
|
# Apply search query if provided
|
||||||
|
if query:
|
||||||
|
chat_sessions = chat_sessions.filter(
|
||||||
|
Q(session_id__icontains=query)
|
||||||
|
| Q(country__icontains=query)
|
||||||
|
| Q(language__icontains=query)
|
||||||
|
| Q(sentiment__icontains=query)
|
||||||
|
| Q(category__icontains=query)
|
||||||
|
| Q(initial_msg__icontains=query)
|
||||||
|
| Q(full_transcript__icontains=query)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Order by most recent first
|
||||||
|
chat_sessions = chat_sessions.order_by("-start_time")
|
||||||
|
|
||||||
|
# Pagination
|
||||||
|
paginator = Paginator(chat_sessions, 20) # Show 20 records per page
|
||||||
|
page_number = request.GET.get("page")
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
# Get data source for context if filtered by data source
|
||||||
|
data_source = None
|
||||||
|
if data_source_id:
|
||||||
|
data_source = get_object_or_404(DataSource, id=data_source_id, company=company)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"query": query,
|
||||||
|
"page_obj": page_obj,
|
||||||
|
"data_source": data_source,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "dashboard/search_results.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def data_view(request):
|
||||||
|
"""View for viewing all data with filtering options"""
|
||||||
|
user = request.user
|
||||||
|
company = user.company
|
||||||
|
|
||||||
|
if not company:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
"You are not associated with any company. Please contact an administrator.",
|
||||||
|
)
|
||||||
|
return redirect("dashboard")
|
||||||
|
|
||||||
|
# Get available data sources
|
||||||
|
data_sources = DataSource.objects.filter(company=company)
|
||||||
|
|
||||||
|
# Get selected data source if any
|
||||||
|
data_source_id = request.GET.get("data_source_id")
|
||||||
|
selected_data_source = None
|
||||||
|
if data_source_id:
|
||||||
|
selected_data_source = get_object_or_404(DataSource, id=data_source_id, company=company)
|
||||||
|
|
||||||
|
# Base queryset
|
||||||
|
chat_sessions = ChatSession.objects.filter(data_source__company=company)
|
||||||
|
|
||||||
|
# Apply data source filter if selected
|
||||||
|
if selected_data_source:
|
||||||
|
chat_sessions = chat_sessions.filter(data_source=selected_data_source)
|
||||||
|
|
||||||
|
# Apply view filter if any
|
||||||
|
view = request.GET.get("view", "all")
|
||||||
|
|
||||||
|
if view == "recent":
|
||||||
|
# Sessions from the last 7 days
|
||||||
|
seven_days_ago = timezone.now() - timedelta(days=7)
|
||||||
|
chat_sessions = chat_sessions.filter(start_time__gte=seven_days_ago)
|
||||||
|
elif view == "positive":
|
||||||
|
# Sessions with positive sentiment
|
||||||
|
chat_sessions = chat_sessions.filter(Q(sentiment__icontains="positive"))
|
||||||
|
elif view == "negative":
|
||||||
|
# Sessions with negative sentiment
|
||||||
|
chat_sessions = chat_sessions.filter(Q(sentiment__icontains="negative"))
|
||||||
|
elif view == "escalated":
|
||||||
|
# Escalated sessions
|
||||||
|
chat_sessions = chat_sessions.filter(escalated=True)
|
||||||
|
|
||||||
|
# Order by most recent first
|
||||||
|
chat_sessions = chat_sessions.order_by("-start_time")
|
||||||
|
|
||||||
|
# Calculate some statistics
|
||||||
|
total_sessions = chat_sessions.count()
|
||||||
|
avg_response_time = (
|
||||||
|
chat_sessions.filter(avg_response_time__isnull=False).aggregate(avg=Avg("avg_response_time"))["avg"] or 0
|
||||||
|
)
|
||||||
|
avg_messages = chat_sessions.filter(messages_sent__gt=0).aggregate(avg=Avg("messages_sent"))["avg"] or 0
|
||||||
|
escalated_count = chat_sessions.filter(escalated=True).count()
|
||||||
|
escalation_rate = (escalated_count / total_sessions * 100) if total_sessions > 0 else 0
|
||||||
|
|
||||||
|
# Pagination
|
||||||
|
paginator = Paginator(chat_sessions, 20) # Show 20 records per page
|
||||||
|
page_number = request.GET.get("page")
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"data_sources": data_sources,
|
||||||
|
"selected_data_source": selected_data_source,
|
||||||
|
"page_obj": page_obj,
|
||||||
|
"view": view,
|
||||||
|
"avg_response_time": avg_response_time,
|
||||||
|
"avg_messages": avg_messages,
|
||||||
|
"escalation_rate": escalation_rate,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "dashboard/data_view.html", context)
|
||||||
1
dashboard_project/dashboard_project/__init__.py
Normal file
1
dashboard_project/dashboard_project/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# This file is intentionally left empty to mark the directory as a Python package
|
||||||
16
dashboard_project/dashboard_project/asgi.py
Normal file
16
dashboard_project/dashboard_project/asgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for dashboard_project project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
130
dashboard_project/dashboard_project/settings.py
Normal file
130
dashboard_project/dashboard_project/settings.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# dashboard_project/settings.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = "django-insecure-your-secret-key-here"
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
"django.contrib.sites",
|
||||||
|
# Third-party apps
|
||||||
|
"allauth",
|
||||||
|
"allauth.account",
|
||||||
|
"allauth.socialaccount",
|
||||||
|
"crispy_forms",
|
||||||
|
"crispy_bootstrap5",
|
||||||
|
# Custom apps
|
||||||
|
"dashboard.apps.DashboardConfig",
|
||||||
|
"accounts.apps.AccountsConfig",
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
"allauth.account.middleware.AccountMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "dashboard_project.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [os.path.join(BASE_DIR, "templates")],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "dashboard_project.wsgi.application"
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": BASE_DIR / "db.sqlite3",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
LANGUAGE_CODE = "en-us"
|
||||||
|
TIME_ZONE = "UTC"
|
||||||
|
USE_I18N = True
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
STATIC_URL = "static/"
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
os.path.join(BASE_DIR, "static"),
|
||||||
|
]
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
|
||||||
|
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||||
|
|
||||||
|
# Media files
|
||||||
|
MEDIA_URL = "/media/"
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
# Crispy Forms
|
||||||
|
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||||
|
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
AUTH_USER_MODEL = "accounts.CustomUser"
|
||||||
|
LOGIN_REDIRECT_URL = "dashboard"
|
||||||
|
LOGOUT_REDIRECT_URL = "login"
|
||||||
|
ACCOUNT_LOGOUT_ON_GET = True
|
||||||
|
|
||||||
|
# django-allauth
|
||||||
|
AUTHENTICATION_BACKENDS = [
|
||||||
|
"django.contrib.auth.backends.ModelBackend",
|
||||||
|
"allauth.account.auth_backends.AuthenticationBackend",
|
||||||
|
]
|
||||||
|
SITE_ID = 1
|
||||||
|
ACCOUNT_EMAIL_VERIFICATION = "none"
|
||||||
17
dashboard_project/dashboard_project/urls.py
Normal file
17
dashboard_project/dashboard_project/urls.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# dashboard_project/urls.py
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import include, path
|
||||||
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
path("accounts/", include("accounts.urls")),
|
||||||
|
path("dashboard/", include("dashboard.urls")),
|
||||||
|
path("", RedirectView.as_view(url="dashboard/", permanent=False)),
|
||||||
|
]
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
16
dashboard_project/dashboard_project/wsgi.py
Normal file
16
dashboard_project/dashboard_project/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for dashboard_project project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
19
dashboard_project/manage.py
Normal file
19
dashboard_project/manage.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError("Couldn't import Django. Are you sure it's installed?") from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
262
dashboard_project/static/css/dashboard.css
Normal file
262
dashboard_project/static/css/dashboard.css
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
/**
|
||||||
|
* dashboard.css - Styles specific to dashboard functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Dashboard grid layout */
|
||||||
|
.dashboard-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
/* Slightly larger minmax for widgets */
|
||||||
|
gap: 1.5rem;
|
||||||
|
/* Increased gap */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard widget cards */
|
||||||
|
.dashboard-widget {
|
||||||
|
display: flex;
|
||||||
|
/* Allow flex for content alignment */
|
||||||
|
flex-direction: column;
|
||||||
|
/* Stack header, body, footer vertically */
|
||||||
|
height: 100%;
|
||||||
|
/* Ensure widgets fill grid cell height */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget .card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget .card-header .widget-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
/* Slightly larger widget titles */
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget .card-header .widget-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget .card-header .widget-actions .btn {
|
||||||
|
width: 32px;
|
||||||
|
/* Slightly larger action buttons */
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget .card-header .widget-actions .btn:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-color: #e0e0e0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget .card-body {
|
||||||
|
flex-grow: 1;
|
||||||
|
/* Allow card body to take available space */
|
||||||
|
padding: 1.25rem;
|
||||||
|
/* Consistent padding */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chart widgets */
|
||||||
|
.chart-widget .card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-widget .chart-container {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 250px;
|
||||||
|
/* Adjusted min-height */
|
||||||
|
width: 100%;
|
||||||
|
/* Ensure it takes full width of card body */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stat widgets / Stat Cards */
|
||||||
|
.stat-card {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
/* Generous padding */
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-icon {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
/* Larger icon */
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: inline-block;
|
||||||
|
width: 4.5rem;
|
||||||
|
height: 4.5rem;
|
||||||
|
line-height: 4.5rem;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #e9f2ff;
|
||||||
|
/* Light blue background for icon */
|
||||||
|
color: #007bff;
|
||||||
|
/* Primary color for icon */
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-value {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
/* Larger stat value */
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
/* Reduced margin */
|
||||||
|
line-height: 1.1;
|
||||||
|
color: #212529;
|
||||||
|
/* Darker color for value */
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
/* Slightly larger label */
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard theme variations */
|
||||||
|
.dashboard-theme-light .card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-theme-dark {
|
||||||
|
background-color: #212529;
|
||||||
|
color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-theme-dark .card {
|
||||||
|
background-color: #343a40;
|
||||||
|
color: #f8f9fa;
|
||||||
|
border-color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-theme-dark .card-header {
|
||||||
|
background-color: #495057;
|
||||||
|
border-bottom-color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-theme-dark .stat-card .stat-label {
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Time period selector */
|
||||||
|
.time-period-selector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
/* Increased gap */
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
/* Increased margin */
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-period-selector .btn-group {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-period-selector .btn {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
/* Bootstrap-like padding */
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom metric selector */
|
||||||
|
.metric-selector {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-selector .nav-link {
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-selector .nav-link.active {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard loading states */
|
||||||
|
.widget-placeholder {
|
||||||
|
min-height: 300px;
|
||||||
|
background: linear-gradient(90deg, #e9ecef 25%, #f8f9fa 50%, #e9ecef 75%);
|
||||||
|
/* Lighter gradient */
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: loading 1.8s infinite ease-in-out;
|
||||||
|
/* Smoother animation */
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
/* Consistent with cards */
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard empty states */
|
||||||
|
.empty-state {
|
||||||
|
padding: 2.5rem;
|
||||||
|
/* Increased padding */
|
||||||
|
text-align: center;
|
||||||
|
color: #6c757d;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
/* Light background for empty state */
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px dashed #ced4da;
|
||||||
|
/* Dashed border */
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state .empty-state-icon {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
/* Larger icon */
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state .empty-state-message {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
/* Slightly larger message */
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state .btn {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.dashboard-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
line-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-value {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
328
dashboard_project/static/css/style.css
Normal file
328
dashboard_project/static/css/style.css
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
/**
|
||||||
|
* style.css - Global styles for the application
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* General Styles */
|
||||||
|
body {
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif,
|
||||||
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
|
background-color: #f4f7f9;
|
||||||
|
/* Lighter, cleaner background */
|
||||||
|
color: #333;
|
||||||
|
/* Darker text for better contrast */
|
||||||
|
line-height: 1.6;
|
||||||
|
display: flex;
|
||||||
|
/* Added for sticky footer */
|
||||||
|
flex-direction: column;
|
||||||
|
/* Added for sticky footer */
|
||||||
|
min-height: 100vh;
|
||||||
|
/* Ensures body takes at least full viewport height */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar adjustments (if needed, Bootstrap usually handles this well) */
|
||||||
|
.navbar {
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
/* Subtle shadow for depth */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helper Classes */
|
||||||
|
.text-truncate-2 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.min-w-150 {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card styles */
|
||||||
|
.card {
|
||||||
|
border: 1px solid #e0e5e9;
|
||||||
|
/* Lighter border */
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
/* Slightly more rounded corners */
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
/* Softer, more modern shadow */
|
||||||
|
transition:
|
||||||
|
transform 0.2s ease-in-out,
|
||||||
|
box-shadow 0.2s ease-in-out;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
/* Consistent margin */
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background-color: #ffffff;
|
||||||
|
/* Clean white header */
|
||||||
|
border-bottom: 1px solid #e0e5e9;
|
||||||
|
font-weight: 500;
|
||||||
|
/* Slightly bolder header text */
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
/* Adjusted card title size */
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar enhancements */
|
||||||
|
.sidebar {
|
||||||
|
background-color: #ffffff;
|
||||||
|
/* White sidebar for a cleaner look */
|
||||||
|
border-right: 1px solid #e0e5e9;
|
||||||
|
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.03);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-sticky {
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-link {
|
||||||
|
color: #4a5568;
|
||||||
|
/* Softer link color */
|
||||||
|
padding: 0.65rem 1.25rem;
|
||||||
|
/* Adjusted padding */
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
/* Bootstrap-like rounded corners for links */
|
||||||
|
margin: 0.1rem 0.5rem;
|
||||||
|
/* Margin around links */
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-link:hover {
|
||||||
|
color: #007bff;
|
||||||
|
/* Primary color on hover */
|
||||||
|
background-color: #e9f2ff;
|
||||||
|
/* Light blue background on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-link.active {
|
||||||
|
color: #007bff;
|
||||||
|
background-color: #d6e4ff;
|
||||||
|
/* Slightly darker blue for active */
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-link i.me-2 {
|
||||||
|
width: 20px;
|
||||||
|
/* Ensure icons align well */
|
||||||
|
text-align: center;
|
||||||
|
margin-right: 0.75rem !important;
|
||||||
|
/* Consistent icon spacing */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-header {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #718096;
|
||||||
|
/* Softer header color */
|
||||||
|
padding: 0.5rem 1.25rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard stats cards */
|
||||||
|
.stats-card {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card h3 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chart containers */
|
||||||
|
.chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading overlay */
|
||||||
|
.loading-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table enhancements */
|
||||||
|
.table {
|
||||||
|
border-color: #e0e5e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
font-weight: 600;
|
||||||
|
/* Bolder table headers */
|
||||||
|
color: #4a5568;
|
||||||
|
background-color: #f8f9fc;
|
||||||
|
/* Light background for headers */
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-striped tbody tr:nth-of-type(odd) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
/* Very subtle striping */
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover tbody tr:hover {
|
||||||
|
background-color: #e9f2ff;
|
||||||
|
/* Consistent hover with sidebar */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form improvements */
|
||||||
|
.form-control,
|
||||||
|
.form-select {
|
||||||
|
border-color: #ced4da;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
/* Consistent border radius */
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
/* Adjusted padding */
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus,
|
||||||
|
.form-select:focus {
|
||||||
|
border-color: #86b7fe;
|
||||||
|
/* Bootstrap focus color */
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||||
|
/* Bootstrap focus shadow */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button styling */
|
||||||
|
.btn {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
/* Consistent border radius */
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
/* Standard button padding */
|
||||||
|
font-weight: 500;
|
||||||
|
transition:
|
||||||
|
background-color 0.15s ease-in-out,
|
||||||
|
border-color 0.15s ease-in-out,
|
||||||
|
box-shadow 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #007bff;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #0069d9;
|
||||||
|
border-color: #0062cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #6c757d;
|
||||||
|
border-color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
border-color: #545b62;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert styling */
|
||||||
|
.alert {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.9rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat transcript styling */
|
||||||
|
.chat-transcript {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 1rem;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-transcript pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: inherit;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer styling */
|
||||||
|
footer {
|
||||||
|
background-color: #ffffff;
|
||||||
|
/* White footer */
|
||||||
|
border-top: 1px solid #e0e5e9;
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: auto;
|
||||||
|
/* Added for sticky footer */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print styles */
|
||||||
|
@media print {
|
||||||
|
.sidebar,
|
||||||
|
.navbar,
|
||||||
|
.btn,
|
||||||
|
footer {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
break-inside: avoid;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
break-inside: avoid;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
253
dashboard_project/static/js/dashboard.js
Normal file
253
dashboard_project/static/js/dashboard.js
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
/**
|
||||||
|
* dashboard.js - JavaScript for the dashboard functionality
|
||||||
|
*
|
||||||
|
* This file handles the interactive features of the dashboard,
|
||||||
|
* including chart refreshing, dashboard filtering, and dashboard
|
||||||
|
* customization.
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
// Chart responsiveness
|
||||||
|
function resizeCharts() {
|
||||||
|
const charts = document.querySelectorAll(".chart-container");
|
||||||
|
charts.forEach((chart) => {
|
||||||
|
if (chart.id && window.Plotly) {
|
||||||
|
Plotly.relayout(chart.id, {
|
||||||
|
"xaxis.automargin": true,
|
||||||
|
"yaxis.automargin": true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle window resize
|
||||||
|
window.addEventListener("resize", function () {
|
||||||
|
if (window.Plotly) {
|
||||||
|
resizeCharts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Time range filtering
|
||||||
|
const timeRangeDropdown = document.getElementById("timeRangeDropdown");
|
||||||
|
if (timeRangeDropdown) {
|
||||||
|
const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item");
|
||||||
|
timeRangeLinks.forEach((link) => {
|
||||||
|
link.addEventListener("click", function (e) {
|
||||||
|
const url = new URL(this.href);
|
||||||
|
const dashboardId = url.searchParams.get("dashboard_id");
|
||||||
|
const timeRange = url.searchParams.get("time_range");
|
||||||
|
|
||||||
|
// Fetch updated data via AJAX
|
||||||
|
if (dashboardId) {
|
||||||
|
fetchDashboardData(dashboardId, timeRange);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to fetch dashboard data
|
||||||
|
function fetchDashboardData(dashboardId, timeRange) {
|
||||||
|
const loadingOverlay = document.createElement("div");
|
||||||
|
loadingOverlay.className = "loading-overlay";
|
||||||
|
loadingOverlay.innerHTML =
|
||||||
|
'<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>';
|
||||||
|
document.querySelector("main").appendChild(loadingOverlay);
|
||||||
|
|
||||||
|
fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
updateDashboardStats(data);
|
||||||
|
updateDashboardCharts(data);
|
||||||
|
|
||||||
|
// Update URL without page reload
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set("dashboard_id", dashboardId);
|
||||||
|
if (timeRange) {
|
||||||
|
url.searchParams.set("time_range", timeRange);
|
||||||
|
}
|
||||||
|
window.history.pushState({}, "", url);
|
||||||
|
|
||||||
|
document.querySelector(".loading-overlay").remove();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching dashboard data:", error);
|
||||||
|
document.querySelector(".loading-overlay").remove();
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
const alertElement = document.createElement("div");
|
||||||
|
alertElement.className = "alert alert-danger alert-dismissible fade show";
|
||||||
|
alertElement.setAttribute("role", "alert");
|
||||||
|
alertElement.innerHTML = `
|
||||||
|
Error loading dashboard data. Please try again.
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
`;
|
||||||
|
document.querySelector("main").prepend(alertElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to update dashboard statistics
|
||||||
|
function updateDashboardStats(data) {
|
||||||
|
// Update total sessions
|
||||||
|
const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3");
|
||||||
|
if (totalSessionsElement) {
|
||||||
|
totalSessionsElement.textContent = data.total_sessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update average response time
|
||||||
|
const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3");
|
||||||
|
if (avgResponseTimeElement) {
|
||||||
|
avgResponseTimeElement.textContent = data.avg_response_time + "s";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update total tokens
|
||||||
|
const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3");
|
||||||
|
if (totalTokensElement) {
|
||||||
|
totalTokensElement.textContent = data.total_tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update total cost
|
||||||
|
const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3");
|
||||||
|
if (totalCostElement) {
|
||||||
|
totalCostElement.textContent = "€" + data.total_cost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to update dashboard charts
|
||||||
|
function updateDashboardCharts(data) {
|
||||||
|
// Update sessions over time chart
|
||||||
|
const timeSeriesData = data.time_series_data;
|
||||||
|
if (timeSeriesData && timeSeriesData.length > 0 && window.Plotly) {
|
||||||
|
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
||||||
|
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
||||||
|
|
||||||
|
Plotly.react(
|
||||||
|
"sessions-time-chart",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
x: timeSeriesX,
|
||||||
|
y: timeSeriesY,
|
||||||
|
type: "scatter",
|
||||||
|
mode: "lines+markers",
|
||||||
|
line: {
|
||||||
|
color: "rgb(75, 192, 192)",
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
marker: {
|
||||||
|
color: "rgb(75, 192, 192)",
|
||||||
|
size: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
margin: { t: 10, r: 10, b: 40, l: 40 },
|
||||||
|
xaxis: {
|
||||||
|
title: "Date",
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
title: "Number of Sessions",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update sentiment chart
|
||||||
|
const sentimentData = data.sentiment_data;
|
||||||
|
if (sentimentData && sentimentData.length > 0 && window.Plotly) {
|
||||||
|
const sentimentLabels = sentimentData.map((item) => item.sentiment);
|
||||||
|
const sentimentValues = sentimentData.map((item) => item.count);
|
||||||
|
const sentimentColors = sentimentLabels.map((sentiment) => {
|
||||||
|
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
|
||||||
|
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
|
||||||
|
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
|
||||||
|
return "rgb(201, 203, 207)";
|
||||||
|
});
|
||||||
|
|
||||||
|
Plotly.react(
|
||||||
|
"sentiment-chart",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
values: sentimentValues,
|
||||||
|
labels: sentimentLabels,
|
||||||
|
type: "pie",
|
||||||
|
marker: {
|
||||||
|
colors: sentimentColors,
|
||||||
|
},
|
||||||
|
hole: 0.4,
|
||||||
|
textinfo: "label+percent",
|
||||||
|
insidetextorientation: "radial",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update country chart
|
||||||
|
const countryData = data.country_data;
|
||||||
|
if (countryData && countryData.length > 0 && window.Plotly) {
|
||||||
|
const countryLabels = countryData.map((item) => item.country);
|
||||||
|
const countryValues = countryData.map((item) => item.count);
|
||||||
|
|
||||||
|
Plotly.react(
|
||||||
|
"country-chart",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
x: countryValues,
|
||||||
|
y: countryLabels,
|
||||||
|
type: "bar",
|
||||||
|
orientation: "h",
|
||||||
|
marker: {
|
||||||
|
color: "rgb(54, 162, 235)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
margin: { t: 10, r: 10, b: 40, l: 100 },
|
||||||
|
xaxis: {
|
||||||
|
title: "Number of Sessions",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update category chart
|
||||||
|
const categoryData = data.category_data;
|
||||||
|
if (categoryData && categoryData.length > 0 && window.Plotly) {
|
||||||
|
const categoryLabels = categoryData.map((item) => item.category);
|
||||||
|
const categoryValues = categoryData.map((item) => item.count);
|
||||||
|
|
||||||
|
Plotly.react(
|
||||||
|
"category-chart",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
labels: categoryLabels,
|
||||||
|
values: categoryValues,
|
||||||
|
type: "pie",
|
||||||
|
textinfo: "label+percent",
|
||||||
|
insidetextorientation: "radial",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dashboard selector
|
||||||
|
const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]');
|
||||||
|
dashboardSelector.forEach((link) => {
|
||||||
|
link.addEventListener("click", function (e) {
|
||||||
|
const url = new URL(this.href);
|
||||||
|
const dashboardId = url.searchParams.get("dashboard_id");
|
||||||
|
|
||||||
|
// Fetch updated data via AJAX
|
||||||
|
if (dashboardId) {
|
||||||
|
fetchDashboardData(dashboardId);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
152
dashboard_project/static/js/main.js
Normal file
152
dashboard_project/static/js/main.js
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* main.js - Global JavaScript functionality
|
||||||
|
*
|
||||||
|
* This file contains general JavaScript functionality used across
|
||||||
|
* the entire application, including navigation, forms, and UI interactions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
// Initialize tooltips
|
||||||
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize popovers
|
||||||
|
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
||||||
|
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
|
||||||
|
return new bootstrap.Popover(popoverTriggerEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle sidebar on mobile
|
||||||
|
const sidebarToggle = document.querySelector("#sidebarToggle");
|
||||||
|
if (sidebarToggle) {
|
||||||
|
sidebarToggle.addEventListener("click", function () {
|
||||||
|
document.querySelector(".sidebar").classList.toggle("show");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-dismiss alerts after 5 seconds
|
||||||
|
setTimeout(function () {
|
||||||
|
var alerts = document.querySelectorAll(".alert:not(.alert-important)");
|
||||||
|
alerts.forEach(function (alert) {
|
||||||
|
if (alert && bootstrap.Alert.getInstance(alert)) {
|
||||||
|
bootstrap.Alert.getInstance(alert).close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
const forms = document.querySelectorAll(".needs-validation");
|
||||||
|
forms.forEach(function (form) {
|
||||||
|
form.addEventListener(
|
||||||
|
"submit",
|
||||||
|
function (event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
form.classList.add("was-validated");
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Confirm dialogs
|
||||||
|
const confirmButtons = document.querySelectorAll("[data-confirm]");
|
||||||
|
confirmButtons.forEach(function (button) {
|
||||||
|
button.addEventListener("click", function (event) {
|
||||||
|
if (!confirm(this.dataset.confirm || "Are you sure?")) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
const backButtons = document.querySelectorAll(".btn-back");
|
||||||
|
backButtons.forEach(function (button) {
|
||||||
|
button.addEventListener("click", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
window.history.back();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// File input customization
|
||||||
|
const fileInputs = document.querySelectorAll(".custom-file-input");
|
||||||
|
fileInputs.forEach(function (input) {
|
||||||
|
input.addEventListener("change", function (e) {
|
||||||
|
const fileName = this.files[0]?.name || "Choose file";
|
||||||
|
const nextSibling = this.nextElementSibling;
|
||||||
|
if (nextSibling) {
|
||||||
|
nextSibling.innerText = fileName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search form submit on enter
|
||||||
|
const searchInputs = document.querySelectorAll(".search-input");
|
||||||
|
searchInputs.forEach(function (input) {
|
||||||
|
input.addEventListener("keypress", function (e) {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.closest("form").submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle password visibility
|
||||||
|
const togglePasswordButtons = document.querySelectorAll(".toggle-password");
|
||||||
|
togglePasswordButtons.forEach(function (button) {
|
||||||
|
button.addEventListener("click", function () {
|
||||||
|
const target = document.querySelector(this.dataset.target);
|
||||||
|
if (target) {
|
||||||
|
const type = target.getAttribute("type") === "password" ? "text" : "password";
|
||||||
|
target.setAttribute("type", type);
|
||||||
|
this.querySelector("i").classList.toggle("fa-eye");
|
||||||
|
this.querySelector("i").classList.toggle("fa-eye-slash");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dropdown menu positioning
|
||||||
|
const dropdowns = document.querySelectorAll(".dropdown-menu");
|
||||||
|
dropdowns.forEach(function (dropdown) {
|
||||||
|
dropdown.addEventListener("click", function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Responsive table handling
|
||||||
|
const tables = document.querySelectorAll(".table-responsive");
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
tables.forEach(function (table) {
|
||||||
|
table.classList.add("table-responsive-force");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle special links (printable views, exports)
|
||||||
|
const printLinks = document.querySelectorAll(".print-link");
|
||||||
|
printLinks.forEach(function (link) {
|
||||||
|
link.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
window.print();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const exportLinks = document.querySelectorAll("[data-export]");
|
||||||
|
exportLinks.forEach(function (link) {
|
||||||
|
link.addEventListener("click", function (e) {
|
||||||
|
// Handle export functionality if needed
|
||||||
|
console.log("Export requested:", this.dataset.export);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle sidebar collapse on small screens
|
||||||
|
function handleSidebarOnResize() {
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
document.querySelector(".sidebar")?.classList.remove("show");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleSidebarOnResize);
|
||||||
|
});
|
||||||
29
dashboard_project/templates/accounts/login.html
Normal file
29
dashboard_project/templates/accounts/login.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<!-- templates/accounts/login.html -->
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block title %}Login | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="card-title mb-0">Login</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
<p class="mb-0">Don't have an account? <a href="{% url 'register' %}">Register</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
37
dashboard_project/templates/accounts/password_change.html
Normal file
37
dashboard_project/templates/accounts/password_change.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<!-- templates/accounts/password_change.html -->
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block title %}Change Password | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div
|
||||||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
|
>
|
||||||
|
<h1 class="h2">Change Password</h1>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<a href="{% url 'profile' %}" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Profile
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Change Your Password</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Change Password</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
<!-- templates/accounts/password_change_done.html -->
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Password Changed | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div
|
||||||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
|
>
|
||||||
|
<h1 class="h2">Password Changed</h1>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<a href="{% url 'profile' %}" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Profile
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h5 class="card-title mb-0">Password Changed Successfully</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="mb-4">
|
||||||
|
<i class="fas fa-check-circle fa-4x text-success mb-3"></i>
|
||||||
|
<h4>Your password has been changed successfully!</h4>
|
||||||
|
<p>Your new password is now active. You can use it the next time you log in.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{% url 'profile' %}" class="btn btn-primary">Return to Profile</a>
|
||||||
|
<a href="{% url 'dashboard' %}" class="btn btn-outline-secondary ms-2"
|
||||||
|
>Go to Dashboard</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
189
dashboard_project/templates/accounts/profile.html
Normal file
189
dashboard_project/templates/accounts/profile.html
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
<!-- templates/accounts/profile.html -->
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}My Profile | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div
|
||||||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
|
>
|
||||||
|
<h1 class="h2">My Profile</h1>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Account Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4 fw-bold">Username:</div>
|
||||||
|
<div class="col-md-8">{{ user.username }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4 fw-bold">Email:</div>
|
||||||
|
<div class="col-md-8">{{ user.email }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4 fw-bold">Company:</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
{% if user.company %}
|
||||||
|
{{ user.company.name }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Not assigned to a company</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4 fw-bold">Role:</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<span class="badge bg-danger">Admin</span>
|
||||||
|
{% elif user.is_company_admin %}
|
||||||
|
<span class="badge bg-primary">Company Admin</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">User</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4 fw-bold">Last Login:</div>
|
||||||
|
<div class="col-md-8">{{ user.last_login|date:"F d, Y H:i" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4 fw-bold">Date Joined:</div>
|
||||||
|
<div class="col-md-8">{{ user.date_joined|date:"F d, Y H:i" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<a href="{% url 'password_change' %}" class="btn btn-primary">Change Password</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if user.company %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Company Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4 fw-bold">Company Name:</div>
|
||||||
|
<div class="col-md-8">{{ user.company.name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4 fw-bold">Description:</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
{{ user.company.description|default:"No description available." }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4 fw-bold">Created:</div>
|
||||||
|
<div class="col-md-8">{{ user.company.created_at|date:"F d, Y" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4 fw-bold">Total Employees:</div>
|
||||||
|
<div class="col-md-8">{{ user.company.employees.count }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4 fw-bold">Data Sources:</div>
|
||||||
|
<div class="col-md-8">{{ user.company.data_sources.count }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if user.is_company_admin or user.is_staff %}
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Admin Actions</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title">Manage Users</h5>
|
||||||
|
<p class="card-text">Manage users and assign them to companies.</p>
|
||||||
|
<a
|
||||||
|
href="{% url 'admin:accounts_customuser_changelist' %}"
|
||||||
|
class="btn btn-primary"
|
||||||
|
>Manage Users</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title">Manage Companies</h5>
|
||||||
|
<p class="card-text">Create and edit companies in the system.</p>
|
||||||
|
<a
|
||||||
|
href="{% url 'admin:accounts_company_changelist' %}"
|
||||||
|
class="btn btn-primary"
|
||||||
|
>Manage Companies</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title">Admin Dashboard</h5>
|
||||||
|
<p class="card-text">Go to the full admin dashboard.</p>
|
||||||
|
<a href="{% url 'admin:index' %}" class="btn btn-primary">Admin Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% elif user.is_company_admin %}
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title">Manage Dashboards</h5>
|
||||||
|
<p class="card-text">Create and edit dashboards for your company.</p>
|
||||||
|
<a href="{% url 'create_dashboard' %}" class="btn btn-primary"
|
||||||
|
>Manage Dashboards</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title">Upload Data</h5>
|
||||||
|
<p class="card-text">Upload and manage data sources for analysis.</p>
|
||||||
|
<a href="{% url 'upload_data' %}" class="btn btn-primary">Upload Data</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title">Search Sessions</h5>
|
||||||
|
<p class="card-text">Search and analyze chat sessions.</p>
|
||||||
|
<a href="{% url 'search_chat_sessions' %}" class="btn btn-primary"
|
||||||
|
>Search Sessions</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
29
dashboard_project/templates/accounts/register.html
Normal file
29
dashboard_project/templates/accounts/register.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<!-- templates/accounts/register.html -->
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block title %}Register | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="card-title mb-0">Register</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Register</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
<p class="mb-0">Already have an account? <a href="{% url 'login' %}">Login</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
312
dashboard_project/templates/base.html
Normal file
312
dashboard_project/templates/base.html
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
<!-- templates/base.html -->
|
||||||
|
{% load static %}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{% block title %}Chat Analytics Dashboard{% endblock %}</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@latest/css/all.min.css"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Plotly.js -->
|
||||||
|
<script
|
||||||
|
src="https://cdn.jsdelivr.net/npm/plotly.js@latest/dist/plotly.min.js"
|
||||||
|
charset="utf-8"
|
||||||
|
></script>
|
||||||
|
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<link rel="stylesheet" href="{% static 'css/style.css' %}" />
|
||||||
|
<link rel="stylesheet" href="{% static 'css/dashboard.css' %}" />
|
||||||
|
|
||||||
|
{% block extra_css %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- Navbar -->
|
||||||
|
<nav class="navbar navbar-expand-md navbar-dark bg-dark absolute-top">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="{% url 'dashboard' %}">Chat Analytics</a>
|
||||||
|
<button
|
||||||
|
class="navbar-toggler"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#navbarCollapse"
|
||||||
|
>
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-md-0">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
class="nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
|
||||||
|
href="{% url 'dashboard' %}"
|
||||||
|
>Dashboard</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
class="nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}"
|
||||||
|
href="{% url 'upload_data' %}"
|
||||||
|
>Upload Data</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
class="nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}"
|
||||||
|
href="{% url 'search_chat_sessions' %}"
|
||||||
|
>Search</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="d-flex">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<div class="dropdown">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-light dropdown-toggle"
|
||||||
|
type="button"
|
||||||
|
id="userDropdown"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
{% if user.company %}
|
||||||
|
<span class="badge bg-info me-1">{{ user.company.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{{ user.username }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
|
||||||
|
<li><a class="dropdown-item" href="{% url 'profile' %}">Profile</a></li>
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li>
|
||||||
|
<hr class="dropdown-divider" />
|
||||||
|
</li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'logout' %}">Logout</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'login' %}" class="btn btn-outline-light me-2">Login</a>
|
||||||
|
<a href="{% url 'register' %}" class="btn btn-light">Register</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav
|
||||||
|
id="sidebarMenu"
|
||||||
|
class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse sticky-top h-100 p-0"
|
||||||
|
>
|
||||||
|
<div class="sidebar-sticky pt-3">
|
||||||
|
{% block sidebar %}
|
||||||
|
<ul class="nav flex-column">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
class="nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
|
||||||
|
href="{% url 'dashboard' %}"
|
||||||
|
>
|
||||||
|
<i class="fas fa-tachometer-alt me-2"></i>
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
class="nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}"
|
||||||
|
href="{% url 'upload_data' %}"
|
||||||
|
>
|
||||||
|
<i class="fas fa-upload me-2"></i>
|
||||||
|
Upload Data
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
class="nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}"
|
||||||
|
href="{% url 'search_chat_sessions' %}"
|
||||||
|
>
|
||||||
|
<i class="fas fa-search me-2"></i>
|
||||||
|
Search
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
class="nav-link {% if request.resolver_match.url_name == 'data_view' %}active{% endif %}"
|
||||||
|
href="{% url 'data_view' %}"
|
||||||
|
>
|
||||||
|
<i class="fas fa-table me-2"></i>
|
||||||
|
Data View
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% if user.is_authenticated and user.company %}
|
||||||
|
{% if dashboards %}
|
||||||
|
<li class="nav-header"><strong>Dashboards</strong></li>
|
||||||
|
{% for dashboard in dashboards %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
class="nav-link {% if selected_dashboard.id == dashboard.id %}active{% endif %}"
|
||||||
|
href="{% url 'dashboard' %}?dashboard_id={{ dashboard.id }}"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chart-line me-2"></i>
|
||||||
|
{{ dashboard.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'create_dashboard' %}">
|
||||||
|
<i class="fas fa-plus-circle me-2"></i>
|
||||||
|
New Dashboard
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if data_sources %}
|
||||||
|
<li class="nav-header"><strong>Data Sources</strong></li>
|
||||||
|
{% for data_source in data_sources %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'data_source_detail' data_source.id %}">
|
||||||
|
<i class="fas fa-database me-2"></i>
|
||||||
|
{{ data_source.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
|
||||||
|
{# {% if messages %} #}
|
||||||
|
{# <div class="messages mt-3"> #}
|
||||||
|
{# {% for message in messages %} #}
|
||||||
|
{# <div class="alert {% if message.tags == 'error' %}alert-danger{% elif message.tags == 'success' %}alert-success{% elif message.tags == 'warning' %}alert-warning{% else %}alert-info{% endif %} alert-dismissible fade show" role="alert"> #}
|
||||||
|
{# {{ message }} #}
|
||||||
|
{# <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> #}
|
||||||
|
{# </div> #}
|
||||||
|
{# {% endfor %} #}
|
||||||
|
{# </div> #}
|
||||||
|
{# {% endif %} #}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="container">
|
||||||
|
<p>© {% now "Y" %} Chat Analytics Dashboard. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
<!-- jQuery (for Ajax) -->
|
||||||
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1100;">
|
||||||
|
<!-- Toasts will be appended here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for message in messages %}
|
||||||
|
<!-- Pre-render message data that will be used by JavaScript -->
|
||||||
|
<script type="application/json" id="message-data-{{ forloop.counter }}">
|
||||||
|
{
|
||||||
|
"message": "{{ message|escapejs }}",
|
||||||
|
"tags": "{{ message.tags|default:'' }}"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const toastContainer = document.querySelector(".toast-container");
|
||||||
|
if (!toastContainer) return;
|
||||||
|
|
||||||
|
// Find all message data elements
|
||||||
|
const messageDataElements = document.querySelectorAll('script[id^="message-data-"]');
|
||||||
|
messageDataElements.forEach(function (dataElement) {
|
||||||
|
try {
|
||||||
|
const messageData = JSON.parse(dataElement.textContent);
|
||||||
|
createToast(messageData.message, messageData.tags);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parsing message data:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function createToast(messageText, messageTags) {
|
||||||
|
let toastClass = "";
|
||||||
|
let autohide = true;
|
||||||
|
let delay = 5000;
|
||||||
|
|
||||||
|
if (messageTags.includes("debug")) {
|
||||||
|
toastClass = "bg-secondary text-white";
|
||||||
|
} else if (messageTags.includes("info")) {
|
||||||
|
toastClass = "bg-info text-dark";
|
||||||
|
} else if (messageTags.includes("success")) {
|
||||||
|
toastClass = "bg-success text-white";
|
||||||
|
} else if (messageTags.includes("warning")) {
|
||||||
|
toastClass = "bg-warning text-dark";
|
||||||
|
autohide = false;
|
||||||
|
} else if (messageTags.includes("error")) {
|
||||||
|
toastClass = "bg-danger text-white";
|
||||||
|
autohide = false;
|
||||||
|
} else {
|
||||||
|
toastClass = "bg-light text-dark";
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastId =
|
||||||
|
"toast-" + Date.now() + "-" + Math.random().toString(36).substring(2, 11);
|
||||||
|
const toastHtml = `
|
||||||
|
<div id="${toastId}" class="toast ${toastClass}" role="alert" aria-live="assertive" aria-atomic="true">
|
||||||
|
<div class="toast-header">
|
||||||
|
<strong class="me-auto">Notification</strong>
|
||||||
|
<small class="${toastClass.includes("text-white") ? "" : "text-muted"} me-2">Just now</small>
|
||||||
|
<button type="button" class="btn-close ${toastClass.includes("text-white") ? "btn-close-white" : ""}" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="toast-body">
|
||||||
|
${messageText}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
toastContainer.insertAdjacentHTML("beforeend", toastHtml);
|
||||||
|
|
||||||
|
const toastElement = document.getElementById(toastId);
|
||||||
|
if (toastElement) {
|
||||||
|
const toast = new bootstrap.Toast(toastElement, {
|
||||||
|
autohide: autohide,
|
||||||
|
delay: autohide ? delay : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
toastElement.addEventListener("hidden.bs.toast", function () {
|
||||||
|
toastElement.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
130
dashboard_project/templates/dashboard/chat_session_detail.html
Normal file
130
dashboard_project/templates/dashboard/chat_session_detail.html
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Chat Session {{ session.session_id }} | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div
|
||||||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
|
>
|
||||||
|
<h1 class="h2">Chat Session: {{ session.session_id }}</h1>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<a
|
||||||
|
href="{% url 'data_source_detail' session.data_source.id %}"
|
||||||
|
class="btn btn-sm btn-outline-secondary"
|
||||||
|
>
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Data Source
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Session Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p><strong>Session ID:</strong> {{ session.session_id }}</p>
|
||||||
|
<p><strong>Start Time:</strong> {{ session.start_time|date:"F d, Y H:i" }}</p>
|
||||||
|
<p><strong>End Time:</strong> {{ session.end_time|date:"F d, Y H:i" }}</p>
|
||||||
|
<p><strong>IP Address:</strong> {{ session.ip_address|default:"N/A" }}</p>
|
||||||
|
<p><strong>Country:</strong> {{ session.country|default:"N/A" }}</p>
|
||||||
|
<p><strong>Language:</strong> {{ session.language|default:"N/A" }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p><strong>Messages Sent:</strong> {{ session.messages_sent }}</p>
|
||||||
|
<p>
|
||||||
|
<strong>Average Response Time:</strong>
|
||||||
|
{{ session.avg_response_time|floatformat:2 }}s
|
||||||
|
</p>
|
||||||
|
<p><strong>Tokens:</strong> {{ session.tokens }}</p>
|
||||||
|
<p><strong>Token Cost:</strong> €{{ session.tokens_eur|floatformat:2 }}</p>
|
||||||
|
<p><strong>Category:</strong> {{ session.category|default:"N/A" }}</p>
|
||||||
|
<p>
|
||||||
|
<strong>Sentiment:</strong>
|
||||||
|
{% if session.sentiment %}
|
||||||
|
{% if 'positive' in session.sentiment|lower %}
|
||||||
|
<span class="badge bg-success">{{ session.sentiment }}</span>
|
||||||
|
{% elif 'negative' in session.sentiment|lower %}
|
||||||
|
<span class="badge bg-danger">{{ session.sentiment }}</span>
|
||||||
|
{% elif 'neutral' in session.sentiment|lower %}
|
||||||
|
<span class="badge bg-warning">{{ session.sentiment }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{{ session.sentiment }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<p class="mb-2"><strong>Initial Message:</strong></p>
|
||||||
|
<div class="card bg-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="mb-0">{{ session.initial_msg|default:"N/A" }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Additional Info</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>
|
||||||
|
<strong>Escalated:</strong> {% if session.escalated %}
|
||||||
|
<span class="badge bg-danger">Yes</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-success">No</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Forwarded to HR:</strong> {% if session.forwarded_hr %}
|
||||||
|
<span class="badge bg-danger">Yes</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-success">No</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<p><strong>User Rating:</strong> {{ session.user_rating|default:"N/A" }}</p>
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
<strong>Data Source:</strong>
|
||||||
|
<a href="{% url 'data_source_detail' session.data_source.id %}"
|
||||||
|
>{{ session.data_source.name }}</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p><strong>Company:</strong> {{ session.data_source.company.name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Full Transcript</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if session.full_transcript %}
|
||||||
|
<div class="chat-transcript" style="max-height: 500px; overflow-y: auto;">
|
||||||
|
<pre style="white-space: pre-wrap; font-family: inherit;">
|
||||||
|
{{ session.full_transcript }}</pre
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-center text-muted">No transcript available.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
286
dashboard_project/templates/dashboard/dashboard.html
Normal file
286
dashboard_project/templates/dashboard/dashboard.html
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
<!-- templates/dashboard/dashboard.html -->
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Dashboard | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div
|
||||||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
|
>
|
||||||
|
<h1 class="h2">{{ selected_dashboard.name }}</h1>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<div class="btn-group me-2">
|
||||||
|
<a
|
||||||
|
href="{% url 'edit_dashboard' selected_dashboard.id %}"
|
||||||
|
class="btn btn-sm btn-outline-secondary"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="{% url 'delete_dashboard' selected_dashboard.id %}"
|
||||||
|
class="btn btn-sm btn-outline-danger"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash"></i> Delete
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-outline-primary dropdown-toggle"
|
||||||
|
type="button"
|
||||||
|
id="timeRangeDropdown"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<i class="fas fa-calendar"></i> Time Range
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="timeRangeDropdown">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=7"
|
||||||
|
>Last 7 days</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=30"
|
||||||
|
>Last 30 days</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=90"
|
||||||
|
>Last 90 days</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=all"
|
||||||
|
>All time</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stats-card bg-primary text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">Total Sessions</h6>
|
||||||
|
<h3>{{ dashboard_data.total_sessions }}</h3>
|
||||||
|
<p>Chat conversations</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stats-card bg-success text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">Avg Response Time</h6>
|
||||||
|
<h3>{{ dashboard_data.avg_response_time }}s</h3>
|
||||||
|
<p>Average response</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stats-card bg-info text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">Total Tokens</h6>
|
||||||
|
<h3>{{ dashboard_data.total_tokens }}</h3>
|
||||||
|
<p>Total usage</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stats-card bg-warning text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">Total Cost</h6>
|
||||||
|
<h3>€{{ dashboard_data.total_cost }}</h3>
|
||||||
|
<p>Token cost</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Sessions Over Time</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="sessions-time-chart" class="chart-container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Sentiment Analysis</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="sentiment-chart" class="chart-container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Top Countries</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="country-chart" class="chart-container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Categories</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="category-chart" class="chart-container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
// Parse the dashboard data from JSON
|
||||||
|
const dashboardData = JSON.parse("{{ dashboard_data_json|safe }}");
|
||||||
|
|
||||||
|
// Sessions over time chart
|
||||||
|
const timeSeriesData = dashboardData.time_series_data;
|
||||||
|
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
||||||
|
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
||||||
|
|
||||||
|
Plotly.newPlot(
|
||||||
|
"sessions-time-chart",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
x: timeSeriesX,
|
||||||
|
y: timeSeriesY,
|
||||||
|
type: "scatter",
|
||||||
|
mode: "lines+markers",
|
||||||
|
line: {
|
||||||
|
color: "rgb(75, 192, 192)",
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
marker: {
|
||||||
|
color: "rgb(75, 192, 192)",
|
||||||
|
size: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
margin: { t: 10, r: 10, b: 40, l: 40 },
|
||||||
|
xaxis: {
|
||||||
|
title: "Date",
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
title: "Number of Sessions",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sentiment analysis chart
|
||||||
|
const sentimentData = dashboardData.sentiment_data;
|
||||||
|
|
||||||
|
if (sentimentData.length > 0) {
|
||||||
|
const sentimentLabels = sentimentData.map((item) => item.sentiment);
|
||||||
|
const sentimentValues = sentimentData.map((item) => item.count);
|
||||||
|
const sentimentColors = sentimentLabels.map((sentiment) => {
|
||||||
|
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
|
||||||
|
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
|
||||||
|
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
|
||||||
|
return "rgb(201, 203, 207)";
|
||||||
|
});
|
||||||
|
|
||||||
|
Plotly.newPlot(
|
||||||
|
"sentiment-chart",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
values: sentimentValues,
|
||||||
|
labels: sentimentLabels,
|
||||||
|
type: "pie",
|
||||||
|
marker: {
|
||||||
|
colors: sentimentColors,
|
||||||
|
},
|
||||||
|
hole: 0.4,
|
||||||
|
textinfo: "label+percent",
|
||||||
|
insidetextorientation: "radial",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
document.getElementById("sentiment-chart").innerHTML =
|
||||||
|
'<div class="text-center py-5"><p class="text-muted">No sentiment data available</p></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Country chart
|
||||||
|
const countryData = dashboardData.country_data;
|
||||||
|
|
||||||
|
if (countryData.length > 0) {
|
||||||
|
const countryLabels = countryData.map((item) => item.country);
|
||||||
|
const countryValues = countryData.map((item) => item.count);
|
||||||
|
|
||||||
|
Plotly.newPlot(
|
||||||
|
"country-chart",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
x: countryValues,
|
||||||
|
y: countryLabels,
|
||||||
|
type: "bar",
|
||||||
|
orientation: "h",
|
||||||
|
marker: {
|
||||||
|
color: "rgb(54, 162, 235)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
margin: { t: 10, r: 10, b: 40, l: 100 },
|
||||||
|
xaxis: {
|
||||||
|
title: "Number of Sessions",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
document.getElementById("country-chart").innerHTML =
|
||||||
|
'<div class="text-center py-5"><p class="text-muted">No country data available</p></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category chart
|
||||||
|
const categoryData = dashboardData.category_data;
|
||||||
|
|
||||||
|
if (categoryData.length > 0) {
|
||||||
|
const categoryLabels = categoryData.map((item) => item.category);
|
||||||
|
const categoryValues = categoryData.map((item) => item.count);
|
||||||
|
|
||||||
|
Plotly.newPlot(
|
||||||
|
"category-chart",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
labels: categoryLabels,
|
||||||
|
values: categoryValues,
|
||||||
|
type: "pie",
|
||||||
|
textinfo: "label+percent",
|
||||||
|
insidetextorientation: "radial",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
document.getElementById("category-chart").innerHTML =
|
||||||
|
'<div class="text-center py-5"><p class="text-muted">No category data available</p></div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Delete Dashboard | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div
|
||||||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
|
>
|
||||||
|
<h1 class="h2">Delete Dashboard</h1>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card border-danger">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<h5 class="card-title mb-0">Confirm Deletion</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="lead">
|
||||||
|
Are you sure you want to delete the dashboard "<strong>{{ dashboard.name }}</strong>"?
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This action cannot be undone. The dashboard will be permanently deleted, but the
|
||||||
|
underlying data sources will remain intact.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="d-flex justify-content-between mt-4">
|
||||||
|
<a href="{% url 'dashboard' %}" class="btn btn-secondary">Cancel</a>
|
||||||
|
<button type="submit" class="btn btn-danger">Delete Dashboard</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
43
dashboard_project/templates/dashboard/dashboard_form.html
Normal file
43
dashboard_project/templates/dashboard/dashboard_form.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}
|
||||||
|
| Chat Analytics
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div
|
||||||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
|
>
|
||||||
|
<h1 class="h2">{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}</h1>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
{% if is_create %}Create Dashboard{% else %}Update Dashboard{% endif %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
<!-- templates/dashboard/data_source_confirm_delete.html -->
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Delete Data Source | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div
|
||||||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
|
>
|
||||||
|
<h1 class="h2">Delete Data Source</h1>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<a
|
||||||
|
href="{% url 'data_source_detail' data_source.id %}"
|
||||||
|
class="btn btn-sm btn-outline-secondary"
|
||||||
|
>
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Data Source
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card border-danger">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<h5 class="card-title mb-0">Confirm Deletion</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="lead">
|
||||||
|
Are you sure you want to delete the data source
|
||||||
|
"<strong>{{ data_source.name }}</strong>"?
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This action cannot be undone. The data source and all associated chat sessions
|
||||||
|
({{ data_source.chat_sessions.count }} sessions) will be permanently deleted.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="d-flex justify-content-between mt-4">
|
||||||
|
<a href="{% url 'data_source_detail' data_source.id %}" class="btn btn-secondary"
|
||||||
|
>Cancel</a
|
||||||
|
>
|
||||||
|
<button type="submit" class="btn btn-danger">Delete Data Source</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
216
dashboard_project/templates/dashboard/data_source_detail.html
Normal file
216
dashboard_project/templates/dashboard/data_source_detail.html
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
<!-- templates/dashboard/data_source_detail.html -->
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load dashboard_extras %}
|
||||||
|
|
||||||
|
{% block title %}{{ data_source.name }} | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div
|
||||||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
|
>
|
||||||
|
<h1 class="h2">{{ data_source.name }}</h1>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<a href="{% url 'upload_data' %}" class="btn btn-sm btn-outline-secondary me-2">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Data Sources
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'delete_data_source' data_source.id %}" class="btn btn-sm btn-outline-danger">
|
||||||
|
<i class="fas fa-trash"></i> Delete
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Data Source Details</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p><strong>Name:</strong> {{ data_source.name }}</p>
|
||||||
|
<p><strong>Uploaded At:</strong> {{ data_source.uploaded_at|date:"F d, Y H:i" }}</p>
|
||||||
|
<p><strong>File:</strong> {{ data_source.file.name|split:"/"|last }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p><strong>Company:</strong> {{ data_source.company.name }}</p>
|
||||||
|
<p><strong>Total Sessions:</strong> {{ page_obj.paginator.count }}</p>
|
||||||
|
<p><strong>Description:</strong> {{ data_source.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Filter Sessions</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="get" action="{% url 'search_chat_sessions' %}">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="q"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Search sessions..."
|
||||||
|
aria-label="Search sessions"
|
||||||
|
/>
|
||||||
|
<input type="hidden" name="data_source_id" value="{{ data_source.id }}" />
|
||||||
|
<button class="btn btn-outline-primary" type="submit">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Chat Sessions ({{ page_obj.paginator.count }})</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Session ID</th>
|
||||||
|
<th>Start Time</th>
|
||||||
|
<th>Country</th>
|
||||||
|
<th>Language</th>
|
||||||
|
<th>Sentiment</th>
|
||||||
|
<th>Messages</th>
|
||||||
|
<th>Tokens</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for session in page_obj %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ session.session_id|truncatechars:10 }}</td>
|
||||||
|
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
|
||||||
|
<td>{{ session.country }}</td>
|
||||||
|
<td>{{ session.language }}</td>
|
||||||
|
<td>
|
||||||
|
{% if session.sentiment %}
|
||||||
|
{% if 'positive' in session.sentiment|lower %}
|
||||||
|
<span class="badge bg-success">{{ session.sentiment }}</span>
|
||||||
|
{% elif 'negative' in session.sentiment|lower %}
|
||||||
|
<span class="badge bg-danger">{{ session.sentiment }}</span>
|
||||||
|
{% elif 'neutral' in session.sentiment|lower %}
|
||||||
|
<span class="badge bg-warning">{{ session.sentiment }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{{ session.sentiment }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ session.messages_sent }}</td>
|
||||||
|
<td>{{ session.tokens }}</td>
|
||||||
|
<td>{{ session.category|default:"N/A" }}</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href="{% url 'chat_session_detail' session.session_id %}"
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
>
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="9" class="text-center">No chat sessions found.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if page_obj.paginator.num_pages > 1 %}
|
||||||
|
<nav aria-label="Page navigation" class="mt-4">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page=1" aria-label="First">
|
||||||
|
<span aria-hidden="true">««</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="?page={{ page_obj.previous_page_number }}"
|
||||||
|
aria-label="Previous"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">«</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" aria-label="First">
|
||||||
|
<span aria-hidden="true">««</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" aria-label="Previous">
|
||||||
|
<span aria-hidden="true">«</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for num in page_obj.paginator.page_range %}
|
||||||
|
{% if page_obj.number == num %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
|
||||||
|
</li>
|
||||||
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="?page={{ page_obj.next_page_number }}"
|
||||||
|
aria-label="Next"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="?page={{ page_obj.paginator.num_pages }}"
|
||||||
|
aria-label="Last"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">»»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" aria-label="Next">
|
||||||
|
<span aria-hidden="true">»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" aria-label="Last">
|
||||||
|
<span aria-hidden="true">»»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
316
dashboard_project/templates/dashboard/data_view.html
Normal file
316
dashboard_project/templates/dashboard/data_view.html
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
<!-- templates/dashboard/data_view.html -->
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load dashboard_extras %}
|
||||||
|
|
||||||
|
{% block title %}Data View | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div
|
||||||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
|
>
|
||||||
|
<h1 class="h2">Data View</h1>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<div class="btn-group me-2">
|
||||||
|
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||||
|
</a>
|
||||||
|
{% if selected_data_source %}
|
||||||
|
<a
|
||||||
|
href="{% url 'data_source_detail' selected_data_source.id %}"
|
||||||
|
class="btn btn-sm btn-outline-secondary"
|
||||||
|
>
|
||||||
|
<i class="fas fa-database"></i> View Source
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-outline-primary dropdown-toggle"
|
||||||
|
type="button"
|
||||||
|
id="dataViewDropdown"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<i class="fas fa-filter"></i> Filter
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="dataViewDropdown">
|
||||||
|
<li><a class="dropdown-item" href="?view=all">All Sessions</a></li>
|
||||||
|
<li><a class="dropdown-item" href="?view=recent">Recent Sessions</a></li>
|
||||||
|
<li><a class="dropdown-item" href="?view=positive">Positive Sentiment</a></li>
|
||||||
|
<li><a class="dropdown-item" href="?view=negative">Negative Sentiment</a></li>
|
||||||
|
<li><a class="dropdown-item" href="?view=escalated">Escalated Sessions</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data Source Selection -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Data Source Selection</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="get" class="row g-3 align-items-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<select name="data_source_id" class="form-select" aria-label="Select Data Source">
|
||||||
|
<option value="">All Data Sources</option>
|
||||||
|
{% for ds in data_sources %}
|
||||||
|
<option
|
||||||
|
value="{{ ds.id }}"
|
||||||
|
{% if selected_data_source.id == ds.id %}selected{% endif %}
|
||||||
|
>
|
||||||
|
{{ ds.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<select name="view" class="form-select" aria-label="Select View">
|
||||||
|
<option value="all" {% if view == 'all' %}selected{% endif %}>All Sessions</option>
|
||||||
|
<option value="recent" {% if view == 'recent' %}selected{% endif %}>
|
||||||
|
Recent Sessions
|
||||||
|
</option>
|
||||||
|
<option value="positive" {% if view == 'positive' %}selected{% endif %}>
|
||||||
|
Positive Sentiment
|
||||||
|
</option>
|
||||||
|
<option value="negative" {% if view == 'negative' %}selected{% endif %}>
|
||||||
|
Negative Sentiment
|
||||||
|
</option>
|
||||||
|
<option value="escalated" {% if view == 'escalated' %}selected{% endif %}>
|
||||||
|
Escalated Sessions
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Apply</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data Table -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
Chat Sessions
|
||||||
|
{% if selected_data_source %}
|
||||||
|
for {{ selected_data_source.name }}
|
||||||
|
{% endif %}
|
||||||
|
{% if view != 'all' %}
|
||||||
|
({{ view|title }})
|
||||||
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
<span class="badge bg-primary">{{ page_obj.paginator.count }} sessions</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Session ID</th>
|
||||||
|
<th>Start Time</th>
|
||||||
|
<th>Country</th>
|
||||||
|
<th>Language</th>
|
||||||
|
<th>Messages</th>
|
||||||
|
<th>Sentiment</th>
|
||||||
|
<th>Response Time</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for session in page_obj %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ session.session_id|truncatechars:10 }}</td>
|
||||||
|
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
|
||||||
|
<td>{{ session.country|default:"N/A" }}</td>
|
||||||
|
<td>{{ session.language|default:"N/A" }}</td>
|
||||||
|
<td>{{ session.messages_sent }}</td>
|
||||||
|
<td>
|
||||||
|
{% if session.sentiment %}
|
||||||
|
{% if 'positive' in session.sentiment|lower %}
|
||||||
|
<span class="badge bg-success">{{ session.sentiment }}</span>
|
||||||
|
{% elif 'negative' in session.sentiment|lower %}
|
||||||
|
<span class="badge bg-danger">{{ session.sentiment }}</span>
|
||||||
|
{% elif 'neutral' in session.sentiment|lower %}
|
||||||
|
<span class="badge bg-warning">{{ session.sentiment }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{{ session.sentiment }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ session.avg_response_time|floatformat:2 }}s</td>
|
||||||
|
<td>{{ session.category|default:"N/A" }}</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href="{% url 'chat_session_detail' session.session_id %}"
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
>
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="9" class="text-center">No chat sessions found.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if page_obj.paginator.num_pages > 1 %}
|
||||||
|
<nav aria-label="Page navigation" class="mt-4">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page=1"
|
||||||
|
aria-label="First"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">««</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.previous_page_number }}"
|
||||||
|
aria-label="Previous"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">«</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" aria-label="First">
|
||||||
|
<span aria-hidden="true">««</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" aria-label="Previous">
|
||||||
|
<span aria-hidden="true">«</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for num in page_obj.paginator.page_range %}
|
||||||
|
{% if page_obj.number == num %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
|
||||||
|
>{{ num }}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
|
||||||
|
>{{ num }}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.next_page_number }}"
|
||||||
|
aria-label="Next"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
|
||||||
|
aria-label="Last"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">»»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" aria-label="Next">
|
||||||
|
<span aria-hidden="true">»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" aria-label="Last">
|
||||||
|
<span aria-hidden="true">»»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data Summary -->
|
||||||
|
{% if page_obj %}
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Summary</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stats-card bg-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">Total Sessions</h6>
|
||||||
|
<h3>{{ page_obj.paginator.count }}</h3>
|
||||||
|
<p>Chat conversations</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stats-card bg-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">Avg Response Time</h6>
|
||||||
|
<h3>{{ avg_response_time|floatformat:2 }}s</h3>
|
||||||
|
<p>Average response</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stats-card bg-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">Avg Messages</h6>
|
||||||
|
<h3>{{ avg_messages|floatformat:1 }}</h3>
|
||||||
|
<p>Per conversation</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stats-card bg-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">Escalation Rate</h6>
|
||||||
|
<h3>{{ escalation_rate|floatformat:1 }}%</h3>
|
||||||
|
<p>Escalated sessions</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
40
dashboard_project/templates/dashboard/no_company.html
Normal file
40
dashboard_project/templates/dashboard/no_company.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<!-- templates/dashboard/no_company.html -->
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}No Company | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div
|
||||||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
|
>
|
||||||
|
<h1 class="h2">No Company Association</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<h5 class="card-title mb-0">Account Not Associated with a Company</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="mb-4">
|
||||||
|
<i class="fas fa-building fa-4x text-warning mb-3"></i>
|
||||||
|
<h4>You are not currently associated with any company</h4>
|
||||||
|
<p class="lead">You need to be associated with a company to access the dashboard.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Please contact an administrator to have your account assigned to a company. Once your
|
||||||
|
account is associated with a company, you'll be able to access the dashboard and its
|
||||||
|
features.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{% url 'profile' %}" class="btn btn-primary">View Your Profile</a>
|
||||||
|
<a href="{% url 'logout' %}" class="btn btn-outline-secondary ms-2">Logout</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
222
dashboard_project/templates/dashboard/search_results.html
Normal file
222
dashboard_project/templates/dashboard/search_results.html
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
<!-- templates/dashboard/search_results.html -->
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Search Results | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div
|
||||||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
|
>
|
||||||
|
<h1 class="h2">Search Results</h1>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Search Chat Sessions</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="get" action="{% url 'search_chat_sessions' %}">
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="q"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Search sessions..."
|
||||||
|
value="{{ query }}"
|
||||||
|
aria-label="Search sessions"
|
||||||
|
/>
|
||||||
|
{% if data_source %}
|
||||||
|
<input type="hidden" name="data_source_id" value="{{ data_source.id }}" />
|
||||||
|
{% endif %}
|
||||||
|
<button class="btn btn-outline-primary" type="submit">
|
||||||
|
<i class="fas fa-search"></i> Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-muted">
|
||||||
|
<small
|
||||||
|
>Search by session ID, country, language, sentiment, category, or message
|
||||||
|
content.</small
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
Results {% if query %}for "{{ query }}"{% endif %}
|
||||||
|
{% if data_source %}in {{ data_source.name }}{% endif %}
|
||||||
|
({{ page_obj.paginator.count }})
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Session ID</th>
|
||||||
|
<th>Start Time</th>
|
||||||
|
<th>Data Source</th>
|
||||||
|
<th>Country</th>
|
||||||
|
<th>Language</th>
|
||||||
|
<th>Sentiment</th>
|
||||||
|
<th>Messages</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for session in page_obj %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ session.session_id|truncatechars:10 }}</td>
|
||||||
|
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'data_source_detail' session.data_source.id %}"
|
||||||
|
>{{ session.data_source.name|truncatechars:15 }}</a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td>{{ session.country }}</td>
|
||||||
|
<td>{{ session.language }}</td>
|
||||||
|
<td>
|
||||||
|
{% if session.sentiment %}
|
||||||
|
{% if 'positive' in session.sentiment|lower %}
|
||||||
|
<span class="badge bg-success">{{ session.sentiment }}</span>
|
||||||
|
{% elif 'negative' in session.sentiment|lower %}
|
||||||
|
<span class="badge bg-danger">{{ session.sentiment }}</span>
|
||||||
|
{% elif 'neutral' in session.sentiment|lower %}
|
||||||
|
<span class="badge bg-warning">{{ session.sentiment }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{{ session.sentiment }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ session.messages_sent }}</td>
|
||||||
|
<td>{{ session.category|default:"N/A" }}</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href="{% url 'chat_session_detail' session.session_id %}"
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
>
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="9" class="text-center">
|
||||||
|
No chat sessions found matching your criteria.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if page_obj.paginator.num_pages > 1 %}
|
||||||
|
<nav aria-label="Page navigation" class="mt-4">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page=1"
|
||||||
|
aria-label="First"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">««</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.previous_page_number }}"
|
||||||
|
aria-label="Previous"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">«</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" aria-label="First">
|
||||||
|
<span aria-hidden="true">««</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" aria-label="Previous">
|
||||||
|
<span aria-hidden="true">«</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for num in page_obj.paginator.page_range %}
|
||||||
|
{% if page_obj.number == num %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
|
||||||
|
>{{ num }}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
|
||||||
|
>{{ num }}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.next_page_number }}"
|
||||||
|
aria-label="Next"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
|
||||||
|
aria-label="Last"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">»»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" aria-label="Next">
|
||||||
|
<span aria-hidden="true">»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" aria-label="Last">
|
||||||
|
<span aria-hidden="true">»»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
197
dashboard_project/templates/dashboard/upload.html
Normal file
197
dashboard_project/templates/dashboard/upload.html
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
<!-- templates/dashboard/upload.html -->
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
{% load dashboard_extras %}
|
||||||
|
|
||||||
|
{% block title %}Upload Data | Chat Analytics{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div
|
||||||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
|
||||||
|
>
|
||||||
|
<h1 class="h2">Upload Data</h1>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Upload CSV File</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %} {{ form|crispy }}
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Upload</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">CSV File Format</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>The CSV file should contain the following columns:</p>
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Column</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Type</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>session_id</td>
|
||||||
|
<td>Unique identifier for the chat session</td>
|
||||||
|
<td>String</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>start_time</td>
|
||||||
|
<td>When the session started</td>
|
||||||
|
<td>Datetime</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>end_time</td>
|
||||||
|
<td>When the session ended</td>
|
||||||
|
<td>Datetime</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ip_address</td>
|
||||||
|
<td>IP address of the user</td>
|
||||||
|
<td>String</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>country</td>
|
||||||
|
<td>Country of the user</td>
|
||||||
|
<td>String</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>language</td>
|
||||||
|
<td>Language used in the conversation</td>
|
||||||
|
<td>String</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>messages_sent</td>
|
||||||
|
<td>Number of messages in the conversation</td>
|
||||||
|
<td>Integer</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>sentiment</td>
|
||||||
|
<td>Sentiment analysis of the conversation</td>
|
||||||
|
<td>String</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>escalated</td>
|
||||||
|
<td>Whether the conversation was escalated</td>
|
||||||
|
<td>Boolean</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>forwarded_hr</td>
|
||||||
|
<td>Whether the conversation was forwarded to HR</td>
|
||||||
|
<td>Boolean</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>full_transcript</td>
|
||||||
|
<td>Full transcript of the conversation</td>
|
||||||
|
<td>Text</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>avg_response_time</td>
|
||||||
|
<td>Average response time in seconds</td>
|
||||||
|
<td>Float</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>tokens</td>
|
||||||
|
<td>Total number of tokens used</td>
|
||||||
|
<td>Integer</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>tokens_eur</td>
|
||||||
|
<td>Cost of tokens in EUR</td>
|
||||||
|
<td>Float</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>category</td>
|
||||||
|
<td>Category of the conversation</td>
|
||||||
|
<td>String</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>initial_msg</td>
|
||||||
|
<td>First message from the user</td>
|
||||||
|
<td>Text</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>user_rating</td>
|
||||||
|
<td>User rating of the conversation</td>
|
||||||
|
<td>String</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if data_sources %}
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Uploaded Data Sources</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Uploaded</th>
|
||||||
|
<th>File</th>
|
||||||
|
<th>Sessions</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for data_source in data_sources %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ data_source.name }}</td>
|
||||||
|
<td>{{ data_source.description|truncatechars:50 }}</td>
|
||||||
|
<td>{{ data_source.uploaded_at|date:"M d, Y H:i" }}</td>
|
||||||
|
<td>{{ data_source.file.name|split:"/"|last }}</td>
|
||||||
|
<td>{{ data_source.chat_sessions.count }}</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href="{% url 'data_source_detail' data_source.id %}"
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
>
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="{% url 'delete_data_source' data_source.id %}"
|
||||||
|
class="btn btn-sm btn-outline-danger"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
16
dashboard_project/wsgi.py
Normal file
16
dashboard_project/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for dashboard_project project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
45
docker-compose.yml
Normal file
45
docker-compose.yml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# docker-compose.yml
|
||||||
|
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
command: gunicorn dashboard_project.wsgi:application --bind 0.0.0.0:8000
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- static_volume:/app/staticfiles
|
||||||
|
- media_volume:/app/media
|
||||||
|
ports:
|
||||||
|
- '8000:8000'
|
||||||
|
environment:
|
||||||
|
- DEBUG=0
|
||||||
|
- SECRET_KEY=your_secret_key_here
|
||||||
|
- ALLOWED_HOSTS=localhost,127.0.0.1
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:13
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data/
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=postgres
|
||||||
|
- POSTGRES_PASSWORD=postgres
|
||||||
|
- POSTGRES_DB=dashboard_db
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:latest
|
||||||
|
ports:
|
||||||
|
- '80:80'
|
||||||
|
volumes:
|
||||||
|
- ./nginx/conf.d:/etc/nginx/conf.d
|
||||||
|
- static_volume:/app/staticfiles
|
||||||
|
- media_volume:/app/media
|
||||||
|
depends_on:
|
||||||
|
- web
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
static_volume:
|
||||||
|
media_volume:
|
||||||
25
nginx/conf.d/default.conf
Normal file
25
nginx/conf.d/default.conf
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
upstream dashboard {
|
||||||
|
server web:8000;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://dashboard;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_redirect off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
alias /app/staticfiles/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /media/ {
|
||||||
|
alias /app/media/;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
package-lock.json
generated
Normal file
39
package-lock.json
generated
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "LiveGraphsDjango",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"prettier-plugin-jinja-template": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||||
|
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prettier-plugin-jinja-template": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier-plugin-jinja-template/-/prettier-plugin-jinja-template-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-mzoCp2Oy9BDSug80fw3B3J4n4KQj1hRvoQOL1akqcDKBb5nvYxrik9zUEDs4AEJ6nK7QDTGoH0y9rx7AlnQ78Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"prettier": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
package.json
Normal file
6
package.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"prettier-plugin-jinja-template": "^2.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
67
pyproject.toml
Normal file
67
pyproject.toml
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
[project]
|
||||||
|
name = "livegraphsdjango"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"crispy-bootstrap5>=2025.4",
|
||||||
|
"django>=5.2.1",
|
||||||
|
"django-allauth>=65.8.0",
|
||||||
|
"django-crispy-forms>=2.4",
|
||||||
|
"gunicorn>=23.0.0",
|
||||||
|
"numpy>=2.2.5",
|
||||||
|
"pandas>=2.2.3",
|
||||||
|
"plotly>=6.1.0",
|
||||||
|
"python-dotenv>=1.1.0",
|
||||||
|
"whitenoise>=6.9.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
# Exclude a variety of commonly ignored directories.
|
||||||
|
exclude = [
|
||||||
|
".bzr",
|
||||||
|
".direnv",
|
||||||
|
".eggs",
|
||||||
|
".git",
|
||||||
|
".git-rewrite",
|
||||||
|
".hg",
|
||||||
|
".ipynb_checkpoints",
|
||||||
|
".mypy_cache",
|
||||||
|
".nox",
|
||||||
|
".pants.d",
|
||||||
|
".pyenv",
|
||||||
|
".pytest_cache",
|
||||||
|
".pytype",
|
||||||
|
".ruff_cache",
|
||||||
|
".svn",
|
||||||
|
".tox",
|
||||||
|
".venv",
|
||||||
|
".vscode",
|
||||||
|
"__pypackages__",
|
||||||
|
"_build",
|
||||||
|
"buck-out",
|
||||||
|
"build",
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
"site-packages",
|
||||||
|
"venv",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Same as Black.
|
||||||
|
line-length = 120
|
||||||
|
indent-width = 4
|
||||||
|
|
||||||
|
# Assume Python 3.13
|
||||||
|
target-version = "py313"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I"]
|
||||||
|
ignore = ["E501"]
|
||||||
|
fixable = ["ALL"]
|
||||||
|
unfixable = []
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "double"
|
||||||
|
indent-style = "space"
|
||||||
|
line-ending = "lf"
|
||||||
256
uv.lock
generated
Normal file
256
uv.lock
generated
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 2
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asgiref"
|
||||||
|
version = "3.8.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186, upload-time = "2024-03-22T14:39:36.863Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crispy-bootstrap5"
|
||||||
|
version = "2025.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
{ name = "django-crispy-forms" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/59/65/d4406d4c914aedea8e2de9cf44d70bd1f6e339c87ef2d5791ff10fa223d2/crispy_bootstrap5-2025.4.tar.gz", hash = "sha256:d675ea7e245048905077dfe16bf1fa1ee16842f52fe88164ccc8a5e2d11119b3", size = 23913, upload-time = "2025-04-02T12:33:16.019Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/9a/4f1166cc82c9f777cf9a5bc2a75171d63301ac317c5de8f59bd44bfe2b7a/crispy_bootstrap5-2025.4-py3-none-any.whl", hash = "sha256:51efa19c7d40e339774a6fe23407e83b95b7634cad6de70fd1f1093131bea1d9", size = 24772, upload-time = "2025-04-02T12:33:14.904Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django"
|
||||||
|
version = "5.2.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "asgiref" },
|
||||||
|
{ name = "sqlparse" },
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ac/10/0d546258772b8f31398e67c85e52c66ebc2b13a647193c3eef8ee433f1a8/django-5.2.1.tar.gz", hash = "sha256:57fe1f1b59462caed092c80b3dd324fd92161b620d59a9ba9181c34746c97284", size = 10818735, upload-time = "2025-05-07T14:06:17.543Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/92/7448697b5838b3a1c6e1d2d6a673e908d0398e84dc4f803a2ce11e7ffc0f/django-5.2.1-py3-none-any.whl", hash = "sha256:a9b680e84f9a0e71da83e399f1e922e1ab37b2173ced046b541c72e1589a5961", size = 8301833, upload-time = "2025-05-07T14:06:10.955Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-allauth"
|
||||||
|
version = "65.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "asgiref" },
|
||||||
|
{ name = "django" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/09/50/4fa3a907be1a49f5ad3b7cd67944d4b91186ef6743fe0fd401c160ba6341/django_allauth-65.8.0.tar.gz", hash = "sha256:9da589d99d412740629333a01865a90c95c97e0fae0cde789aa45a8fda90e83b", size = 1679978, upload-time = "2025-05-08T19:31:27.975Z" }
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-crispy-forms"
|
||||||
|
version = "2.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/88/a1/ffd7b0e160296121d88e3e173165370000ee4de7328f5c4f4b266638dcd9/django_crispy_forms-2.4.tar.gz", hash = "sha256:915e1ffdeb2987d78b33fabfeff8e5203c8776aa910a3a659a2c514ca125f3bd", size = 278932, upload-time = "2025-04-13T07:25:00.176Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/ec/a25f81e56a674e63cf6c3dd8e36b1b3fecc238fecd6098504adc0cc61402/django_crispy_forms-2.4-py3-none-any.whl", hash = "sha256:5a4b99876cfb1bdd3e47727731b6d4197c51c0da502befbfbec6a93010b02030", size = 31446, upload-time = "2025-04-13T07:24:58.516Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gunicorn"
|
||||||
|
version = "23.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "packaging" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "livegraphsdjango"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "crispy-bootstrap5" },
|
||||||
|
{ name = "django" },
|
||||||
|
{ name = "django-allauth" },
|
||||||
|
{ name = "django-crispy-forms" },
|
||||||
|
{ name = "gunicorn" },
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "pandas" },
|
||||||
|
{ name = "plotly" },
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "whitenoise" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "crispy-bootstrap5", specifier = ">=2025.4" },
|
||||||
|
{ name = "django", specifier = ">=5.2.1" },
|
||||||
|
{ name = "django-allauth", specifier = ">=65.8.0" },
|
||||||
|
{ name = "django-crispy-forms", specifier = ">=2.4" },
|
||||||
|
{ name = "gunicorn", specifier = ">=23.0.0" },
|
||||||
|
{ name = "numpy", specifier = ">=2.2.5" },
|
||||||
|
{ name = "pandas", specifier = ">=2.2.3" },
|
||||||
|
{ name = "plotly", specifier = ">=6.1.0" },
|
||||||
|
{ name = "python-dotenv", specifier = ">=1.1.0" },
|
||||||
|
{ name = "whitenoise", specifier = ">=6.9.0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "narwhals"
|
||||||
|
version = "1.39.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/56/e6/66682dd5ffe656358e07194fa9551b1d41e33952831a40553e791dd98dfc/narwhals-1.39.1.tar.gz", hash = "sha256:cf15389e6f8c5321e8cd0ca8b5bace3b1aea5f5622fa59dfd64821998741d836", size = 484444, upload-time = "2025-05-15T17:45:10.967Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/c4/b83520ecc27840a4d58bd585ae0ec0f8a4f2b0c5a965b66749254a54de0e/narwhals-1.39.1-py3-none-any.whl", hash = "sha256:68d0f29c760f1a9419ada537f35f21ff202b0be1419e6d22135a0352c6d96deb", size = 355009, upload-time = "2025-05-15T17:45:07.954Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.2.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/dc/b2/ce4b867d8cd9c0ee84938ae1e6a6f7926ebf928c9090d036fc3c6a04f946/numpy-2.2.5.tar.gz", hash = "sha256:a9c0d994680cd991b1cb772e8b297340085466a6fe964bc9d4e80f5e2f43c291", size = 20273920, upload-time = "2025-04-19T23:27:42.561Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/a0/0aa7f0f4509a2e07bd7a509042967c2fab635690d4f48c6c7b3afd4f448c/numpy-2.2.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:059b51b658f4414fff78c6d7b1b4e18283ab5fa56d270ff212d5ba0c561846f4", size = 20935102, upload-time = "2025-04-19T22:41:16.234Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/e4/a6a9f4537542912ec513185396fce52cdd45bdcf3e9d921ab02a93ca5aa9/numpy-2.2.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:47f9ed103af0bc63182609044b0490747e03bd20a67e391192dde119bf43d52f", size = 14191709, upload-time = "2025-04-19T22:41:38.472Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/65/72f3186b6050bbfe9c43cb81f9df59ae63603491d36179cf7a7c8d216758/numpy-2.2.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:261a1ef047751bb02f29dfe337230b5882b54521ca121fc7f62668133cb119c9", size = 5149173, upload-time = "2025-04-19T22:41:47.823Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/e9/83e7a9432378dde5802651307ae5e9ea07bb72b416728202218cd4da2801/numpy-2.2.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4520caa3807c1ceb005d125a75e715567806fed67e315cea619d5ec6e75a4191", size = 6684502, upload-time = "2025-04-19T22:41:58.689Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/27/b80da6c762394c8ee516b74c1f686fcd16c8f23b14de57ba0cad7349d1d2/numpy-2.2.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d14b17b9be5f9c9301f43d2e2a4886a33b53f4e6fdf9ca2f4cc60aeeee76372", size = 14084417, upload-time = "2025-04-19T22:42:19.897Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/fc/ebfd32c3e124e6a1043e19c0ab0769818aa69050ce5589b63d05ff185526/numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba321813a00e508d5421104464510cc962a6f791aa2fca1c97b1e65027da80d", size = 16133807, upload-time = "2025-04-19T22:42:44.433Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/9b/4cc171a0acbe4666f7775cfd21d4eb6bb1d36d3a0431f48a73e9212d2278/numpy-2.2.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4cbdef3ddf777423060c6f81b5694bad2dc9675f110c4b2a60dc0181543fac7", size = 15575611, upload-time = "2025-04-19T22:43:09.928Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/45/40f4135341850df48f8edcf949cf47b523c404b712774f8855a64c96ef29/numpy-2.2.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54088a5a147ab71a8e7fdfd8c3601972751ded0739c6b696ad9cb0343e21ab73", size = 17895747, upload-time = "2025-04-19T22:43:36.983Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/4c/b32a17a46f0ffbde8cc82df6d3daeaf4f552e346df143e1b188a701a8f09/numpy-2.2.5-cp313-cp313-win32.whl", hash = "sha256:c8b82a55ef86a2d8e81b63da85e55f5537d2157165be1cb2ce7cfa57b6aef38b", size = 6309594, upload-time = "2025-04-19T22:47:10.523Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/ae/72e6276feb9ef06787365b05915bfdb057d01fceb4a43cb80978e518d79b/numpy-2.2.5-cp313-cp313-win_amd64.whl", hash = "sha256:d8882a829fd779f0f43998e931c466802a77ca1ee0fe25a3abe50278616b1471", size = 12638356, upload-time = "2025-04-19T22:47:30.253Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/56/be8b85a9f2adb688e7ded6324e20149a03541d2b3297c3ffc1a73f46dedb/numpy-2.2.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8b025c351b9f0e8b5436cf28a07fa4ac0204d67b38f01433ac7f9b870fa38c6", size = 20963778, upload-time = "2025-04-19T22:44:09.251Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/77/19c5e62d55bff507a18c3cdff82e94fe174957bad25860a991cac719d3ab/numpy-2.2.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dfa94b6a4374e7851bbb6f35e6ded2120b752b063e6acdd3157e4d2bb922eba", size = 14207279, upload-time = "2025-04-19T22:44:31.383Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/22/aa11f22dc11ff4ffe4e849d9b63bbe8d4ac6d5fae85ddaa67dfe43be3e76/numpy-2.2.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:97c8425d4e26437e65e1d189d22dff4a079b747ff9c2788057bfb8114ce1e133", size = 5199247, upload-time = "2025-04-19T22:44:40.361Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/6c/12d5e760fc62c08eded0394f62039f5a9857f758312bf01632a81d841459/numpy-2.2.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:352d330048c055ea6db701130abc48a21bec690a8d38f8284e00fab256dc1376", size = 6711087, upload-time = "2025-04-19T22:44:51.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/94/ece8280cf4218b2bee5cec9567629e61e51b4be501e5c6840ceb593db945/numpy-2.2.5-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b4c0773b6ada798f51f0f8e30c054d32304ccc6e9c5d93d46cb26f3d385ab19", size = 14059964, upload-time = "2025-04-19T22:45:12.451Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/41/c5377dac0514aaeec69115830a39d905b1882819c8e65d97fc60e177e19e/numpy-2.2.5-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55f09e00d4dccd76b179c0f18a44f041e5332fd0e022886ba1c0bbf3ea4a18d0", size = 16121214, upload-time = "2025-04-19T22:45:37.734Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/54/3b9f89a943257bc8e187145c6bc0eb8e3d615655f7b14e9b490b053e8149/numpy-2.2.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02f226baeefa68f7d579e213d0f3493496397d8f1cff5e2b222af274c86a552a", size = 15575788, upload-time = "2025-04-19T22:46:01.908Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/c4/2e407e85df35b29f79945751b8f8e671057a13a376497d7fb2151ba0d290/numpy-2.2.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c26843fd58f65da9491165072da2cccc372530681de481ef670dcc8e27cfb066", size = 17893672, upload-time = "2025-04-19T22:46:28.585Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/7e/d0b44e129d038dba453f00d0e29ebd6eaf2f06055d72b95b9947998aca14/numpy-2.2.5-cp313-cp313t-win32.whl", hash = "sha256:1a161c2c79ab30fe4501d5a2bbfe8b162490757cf90b7f05be8b80bc02f7bb8e", size = 6377102, upload-time = "2025-04-19T22:46:39.949Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/be/b85e4aa4bf42c6502851b971f1c326d583fcc68227385f92089cf50a7b45/numpy-2.2.5-cp313-cp313t-win_amd64.whl", hash = "sha256:d403c84991b5ad291d3809bace5e85f4bbf44a04bdc9a88ed2bb1807b3360bb8", size = 12750096, upload-time = "2025-04-19T22:47:00.147Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "25.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pandas"
|
||||||
|
version = "2.2.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "pytz" },
|
||||||
|
{ name = "tzdata" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643, upload-time = "2024-09-20T13:09:25.522Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573, upload-time = "2024-09-20T13:09:28.012Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085, upload-time = "2024-09-20T19:02:10.451Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809, upload-time = "2024-09-20T13:09:30.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316, upload-time = "2024-09-20T19:02:13.825Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055, upload-time = "2024-09-20T13:09:33.462Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175, upload-time = "2024-09-20T13:09:35.871Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650, upload-time = "2024-09-20T13:09:38.685Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177, upload-time = "2024-09-20T13:09:41.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526, upload-time = "2024-09-20T19:02:16.905Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013, upload-time = "2024-09-20T13:09:44.39Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620, upload-time = "2024-09-20T19:02:20.639Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "plotly"
|
||||||
|
version = "6.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "narwhals" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a9/e3/66eabba0b35095027e1ae5cb2e091cd168d44362242b5496baac9a460697/plotly-6.1.0.tar.gz", hash = "sha256:f13f497ccc2d97f06f771a30b27fab0cbd220f2975865f4ecbc75057135521de", size = 7545417, upload-time = "2025-05-15T16:04:39.532Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/11/83ae52318353f9da4a88cc23e7f9dbc3d449b3f0fd6158fba15eb3c3b816/plotly-6.1.0-py3-none-any.whl", hash = "sha256:a29d3ed523c9d7960095693af1ee52689830df0f9c6bae3e5e92c20c4f5684c3", size = 16118476, upload-time = "2025-05-15T16:04:30.81Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.9.0.post0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytz"
|
||||||
|
version = "2025.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlparse"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2025.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "whitenoise"
|
||||||
|
version = "6.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b9/cf/c15c2f21aee6b22a9f6fc9be3f7e477e2442ec22848273db7f4eb73d6162/whitenoise-6.9.0.tar.gz", hash = "sha256:8c4a7c9d384694990c26f3047e118c691557481d624f069b7f7752a2f735d609", size = 25920, upload-time = "2025-02-06T22:16:34.957Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/b2/2ce9263149fbde9701d352bda24ea1362c154e196d2fda2201f18fc585d7/whitenoise-6.9.0-py3-none-any.whl", hash = "sha256:c8a489049b7ee9889617bb4c274a153f3d979e8f51d2efd0f5b403caf41c57df", size = 20161, upload-time = "2025-02-06T22:16:32.589Z" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user