Spaces:
Running
Running
upload file
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env +7 -0
- .gitattributes +3 -35
- .gitignore +2 -0
- Dockerfile +32 -0
- app.py +1069 -0
- config.yaml +36 -0
- embedding_data/embeddings.pkl +3 -0
- embedding_data/faiss_index_23_06.index +3 -0
- gemini_handler.py +559 -0
- readme.md +1 -0
- requirements.txt +21 -0
- static/assets/01-dark.png +0 -0
- static/assets/01-light.png +3 -0
- static/assets/01.jpg +0 -0
- static/assets/02-dark.png +0 -0
- static/assets/02-light.png +0 -0
- static/assets/02.jpg +0 -0
- static/assets/03-dark.png +0 -0
- static/assets/03-light.png +0 -0
- static/assets/48.jpg +0 -0
- static/assets/49.jpg +0 -0
- static/assets/50.jpg +0 -0
- static/assets/awwwards.png +0 -0
- static/assets/boxicons.min.css +1 -0
- static/assets/clutch-rating.png +0 -0
- static/assets/clutch.png +0 -0
- static/assets/good-firms.png +0 -0
- static/assets/jarallax.min.js +6 -0
- static/assets/java.png +0 -0
- static/assets/landings.jpg +0 -0
- static/assets/logo.svg +75 -0
- static/assets/node-dark.png +0 -0
- static/assets/node-light.png +0 -0
- static/assets/product-hunt.png +0 -0
- static/assets/react.png +0 -0
- static/assets/rellax.min.js +14 -0
- static/assets/swiper-bundle.min.css +13 -0
- static/assets/swiper-bundle.min.js +0 -0
- static/assets/theme-switcher.js +68 -0
- static/assets/theme.min.css +0 -0
- static/assets/theme.min.js +23 -0
- static/assets/vue-dark.png +0 -0
- static/assets/vue-light.png +0 -0
- static/css/styles.css +39 -0
- static/script.js +506 -0
- static/style.css +1036 -0
- static/translations.js +134 -0
- templates/admin_dashboard.html +453 -0
- templates/change_password.html +443 -0
- templates/forgot_password.html +613 -0
.env
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
GEMINI_API_KEYS=AIzaSyBZy7wnuLbRpngTyuplRz1FNjpP8uxttVw,AIzaSyCvlZ63Nkt5NpjdmxYPAsG8Qskex6usCFw,AIzaSyB4BlhbVDHupKtExi59btmX5Y5Nkm0eN7g,AIzaSyA2RKWDRHuSVm8X5ez30-5NWbF0F4QdJGo,AIzaSyCya9OpC1EgO_j3ARee7OrBPJqjlj4_xso
|
| 2 |
+
MONGO_URI=mongodb+srv://legalmind:<db_password>@cluster0.xdzfv.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
|
| 3 |
+
|
| 4 |
+
MYSQL_HOST=localhost
|
| 5 |
+
MYSQL_USER=root
|
| 6 |
+
MYSQL_PASSWORD=
|
| 7 |
+
MYSQL_DATABASE=legal_query_db
|
.gitattributes
CHANGED
|
@@ -1,35 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
embedding_data/embeddings.pkl filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
embedding_data/faiss_index_23_06.index filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
static/assets/01-light.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.log
|
| 2 |
+
*.tmp
|
Dockerfile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use the official Python 3.11 slim image as the base
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# Set the working directory inside the container
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy the requirements file into the container
|
| 8 |
+
COPY requirements.txt .
|
| 9 |
+
|
| 10 |
+
# Install dependencies
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
# Install additional system dependencies for MongoDB and other libraries
|
| 14 |
+
RUN apt-get update && apt-get install -y \
|
| 15 |
+
gcc \
|
| 16 |
+
g++ \
|
| 17 |
+
libffi-dev \
|
| 18 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 19 |
+
|
| 20 |
+
# Copy the entire application code into the container
|
| 21 |
+
COPY . .
|
| 22 |
+
|
| 23 |
+
# Expose the port Hugging Face Spaces expects (7860)
|
| 24 |
+
EXPOSE 7860
|
| 25 |
+
|
| 26 |
+
# Set environment variables
|
| 27 |
+
ENV FLASK_ENV=production
|
| 28 |
+
ENV PYTHONUNBUFFERED=1
|
| 29 |
+
ENV PORT=7860
|
| 30 |
+
|
| 31 |
+
# Command to run the application with Flask-SocketIO
|
| 32 |
+
CMD ["python", "-m", "flask", "run", "--host=0.0.0.0", "--port=7860"]
|
app.py
ADDED
|
@@ -0,0 +1,1069 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, request, jsonify, session, render_template, redirect, url_for
|
| 2 |
+
from flask_socketio import SocketIO, emit, disconnect # Add SocketIO
|
| 3 |
+
from pymongo import MongoClient
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
from gemini_handler import GeminiHandler, GenerationConfig, Strategy, KeyRotationStrategy
|
| 6 |
+
from langchain.memory import ConversationBufferMemory
|
| 7 |
+
import json
|
| 8 |
+
from typing import List, Dict
|
| 9 |
+
import re
|
| 10 |
+
import logging
|
| 11 |
+
import pickle
|
| 12 |
+
import faiss
|
| 13 |
+
import torch
|
| 14 |
+
import numpy as np
|
| 15 |
+
from sentence_transformers import SentenceTransformer
|
| 16 |
+
from bson import ObjectId
|
| 17 |
+
import hashlib
|
| 18 |
+
import os
|
| 19 |
+
import smtplib
|
| 20 |
+
from email.mime.text import MIMEText
|
| 21 |
+
import random
|
| 22 |
+
import string
|
| 23 |
+
from functools import wraps
|
| 24 |
+
|
| 25 |
+
# Cấu hình logging
|
| 26 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 27 |
+
|
| 28 |
+
# Initialize Flask app and SocketIO
|
| 29 |
+
app = Flask(__name__)
|
| 30 |
+
app.config['SECRET_KEY'] = 'your-secret-key' # Replace with a secure key
|
| 31 |
+
app.config['MONGO_URI'] = 'mongodb+srv://itdatit12:[email protected]/?retryWrites=true&w=majority&appName=Cluster0'
|
| 32 |
+
app.config['SMTP_SERVER'] = 'smtp.gmail.com'
|
| 33 |
+
app.config['SMTP_PORT'] = 587
|
| 34 |
+
app.config['EMAIL_ADDRESS'] = '[email protected]' # Replace with your email
|
| 35 |
+
app.config['EMAIL_PASSWORD'] = 'hihj vpcb ayjk gaex' # Replace with your app password
|
| 36 |
+
socketio = SocketIO(app, cors_allowed_origins="*") # Initialize SocketIO
|
| 37 |
+
|
| 38 |
+
# Initialize MongoDB client
|
| 39 |
+
mongo = MongoClient(app.config['MONGO_URI'])
|
| 40 |
+
db = mongo.get_database('legal_assistant')
|
| 41 |
+
|
| 42 |
+
# Store WebSocket clients by user_id
|
| 43 |
+
connected_clients = {}
|
| 44 |
+
|
| 45 |
+
# Password hashing function
|
| 46 |
+
def hash_password(password: str) -> str:
|
| 47 |
+
salt = os.urandom(32)
|
| 48 |
+
hashed = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
|
| 49 |
+
password_hash = (salt + hashed).hex()
|
| 50 |
+
logging.info(f"Generated password hash: {password_hash}")
|
| 51 |
+
return password_hash
|
| 52 |
+
|
| 53 |
+
# Password verification function
|
| 54 |
+
def verify_password(stored_password: str, provided_password: str) -> bool:
|
| 55 |
+
if not stored_password or not all(c in '0123456789abcdefABCDEF' for c in stored_password):
|
| 56 |
+
logging.error(f"Invalid stored password format: {stored_password}")
|
| 57 |
+
return False
|
| 58 |
+
try:
|
| 59 |
+
stored_bytes = bytes.fromhex(stored_password)
|
| 60 |
+
salt = stored_bytes[:32]
|
| 61 |
+
stored_hash = stored_bytes[32:]
|
| 62 |
+
provided_hash = hashlib.pbkdf2_hmac('sha256', provided_password.encode('utf-8'), salt, 100000)
|
| 63 |
+
return stored_hash == provided_hash
|
| 64 |
+
except ValueError as e:
|
| 65 |
+
logging.error(f"Error in verify_password: {e}, stored_password: {stored_password}")
|
| 66 |
+
return False
|
| 67 |
+
|
| 68 |
+
# Generate OTP
|
| 69 |
+
def generate_otp(length=6):
|
| 70 |
+
return ''.join(random.choices(string.digits, k=length))
|
| 71 |
+
|
| 72 |
+
# Send email with OTP or password
|
| 73 |
+
def send_email(to_email, subject, body):
|
| 74 |
+
msg = MIMEText(body)
|
| 75 |
+
msg['Subject'] = subject
|
| 76 |
+
msg['From'] = app.config['EMAIL_ADDRESS']
|
| 77 |
+
msg['To'] = to_email
|
| 78 |
+
try:
|
| 79 |
+
with smtplib.SMTP(app.config['SMTP_SERVER'], app.config['SMTP_PORT']) as server:
|
| 80 |
+
server.starttls()
|
| 81 |
+
server.login(app.config['EMAIL_ADDRESS'], app.config['EMAIL_PASSWORD'])
|
| 82 |
+
server.send_message(msg)
|
| 83 |
+
logging.info(f"Email sent to {to_email}")
|
| 84 |
+
return True
|
| 85 |
+
except Exception as e:
|
| 86 |
+
logging.error(f"Error sending email: {e}")
|
| 87 |
+
return False
|
| 88 |
+
|
| 89 |
+
# Khởi tạo model embedding
|
| 90 |
+
model = SentenceTransformer('hiieu/halong_embedding', device='cuda' if torch.cuda.is_available() else 'cpu')
|
| 91 |
+
|
| 92 |
+
# Đường dẫn đến FAISS index và embeddings data
|
| 93 |
+
INDEX_PATH = "embedding_data/faiss_index_23_06.index"
|
| 94 |
+
EMBEDDINGS_DATA_PATH = "embedding_data/embeddings.pkl"
|
| 95 |
+
|
| 96 |
+
# Khởi tạo ConversationBufferMemory
|
| 97 |
+
memory = ConversationBufferMemory(
|
| 98 |
+
memory_key="chat_history",
|
| 99 |
+
return_messages=True,
|
| 100 |
+
max_message_limit=10,
|
| 101 |
+
max_token_limit=1000
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
# Load FAISS index
|
| 105 |
+
def load_faiss_index(index_path):
|
| 106 |
+
try:
|
| 107 |
+
index = faiss.read_index(index_path)
|
| 108 |
+
logging.info(f"Loaded FAISS index from {index_path}")
|
| 109 |
+
return index
|
| 110 |
+
except Exception as e:
|
| 111 |
+
logging.error(f"Error loading FAISS index: {e}")
|
| 112 |
+
return None
|
| 113 |
+
|
| 114 |
+
# Load embeddings data
|
| 115 |
+
def load_embeddings_data(data_path):
|
| 116 |
+
try:
|
| 117 |
+
with open(data_path, 'rb') as f:
|
| 118 |
+
embeddings_data = pickle.load(f)
|
| 119 |
+
logging.info(f"Loaded embeddings data from {data_path}")
|
| 120 |
+
return embeddings_data
|
| 121 |
+
except Exception as e:
|
| 122 |
+
logging.error(f"Error loading embeddings data: {e}")
|
| 123 |
+
return None
|
| 124 |
+
|
| 125 |
+
# Hàm truy xuất
|
| 126 |
+
def retrieve(query, index, embeddings_data, k=10):
|
| 127 |
+
try:
|
| 128 |
+
query_embedding = model.encode([query], convert_to_numpy=True)
|
| 129 |
+
distances, indices = index.search(query_embedding, k)
|
| 130 |
+
results = []
|
| 131 |
+
for idx, distance in zip(indices[0], distances[0]):
|
| 132 |
+
results.append({
|
| 133 |
+
'file': embeddings_data[idx]['file'],
|
| 134 |
+
'folder': embeddings_data[idx]['folder'],
|
| 135 |
+
'text_path': embeddings_data[idx]['text_path'],
|
| 136 |
+
'text': embeddings_data[idx]['text'],
|
| 137 |
+
'distance': float(distance)
|
| 138 |
+
})
|
| 139 |
+
return results
|
| 140 |
+
except Exception as e:
|
| 141 |
+
logging.error(f"Error during retrieval: {e}")
|
| 142 |
+
return []
|
| 143 |
+
|
| 144 |
+
# Load FAISS index và embeddings data
|
| 145 |
+
index = load_faiss_index(INDEX_PATH)
|
| 146 |
+
embeddings_data = load_embeddings_data(EMBEDDINGS_DATA_PATH)
|
| 147 |
+
if index is None or embeddings_data is None:
|
| 148 |
+
logging.error("Failed to load FAISS index or embeddings data. Application cannot start.")
|
| 149 |
+
exit(1)
|
| 150 |
+
|
| 151 |
+
# Reset query count for limited accounts (daily reset)
|
| 152 |
+
def reset_query_count(user_id):
|
| 153 |
+
user = db.users.find_one({'_id': ObjectId(user_id)})
|
| 154 |
+
if not user or user.get('account_type') == 'unlimited':
|
| 155 |
+
return
|
| 156 |
+
last_reset = user.get('last_reset')
|
| 157 |
+
if last_reset and datetime.utcnow() - last_reset > timedelta(days=1):
|
| 158 |
+
db.users.update_one(
|
| 159 |
+
{'_id': ObjectId(user_id)},
|
| 160 |
+
{'$set': {'query_count': 0, 'last_reset': datetime.utcnow()}}
|
| 161 |
+
)
|
| 162 |
+
logging.info(f"Query count reset for user {user_id}")
|
| 163 |
+
|
| 164 |
+
# Check if user can make a query
|
| 165 |
+
def can_make_query(user_id):
|
| 166 |
+
user = db.users.find_one({'_id': ObjectId(user_id)})
|
| 167 |
+
if not user:
|
| 168 |
+
return False, "Người dùng không tồn tại", None, None
|
| 169 |
+
if user.get('is_admin') or user.get('account_type') == 'unlimited':
|
| 170 |
+
return True, None, None, None
|
| 171 |
+
reset_query_count(user_id)
|
| 172 |
+
user = db.users.find_one({'_id': ObjectId(user_id)})
|
| 173 |
+
query_limit = user.get('query_limit', 10)
|
| 174 |
+
query_count = user.get('query_count', 0)
|
| 175 |
+
if query_count >= query_limit:
|
| 176 |
+
return False, f"Bạn đã sử dụng hết {query_limit} lượt hỏi đáp hôm nay", query_count, query_limit
|
| 177 |
+
# Warn if user is close to the limit
|
| 178 |
+
if query_count + 1 == query_limit:
|
| 179 |
+
return True, "Cảnh báo: Đây là lượt hỏi cuối cùng của bạn hôm nay", query_count, query_limit
|
| 180 |
+
return True, None, query_count, query_limit
|
| 181 |
+
|
| 182 |
+
# Admin required decorator
|
| 183 |
+
def admin_required(f):
|
| 184 |
+
@wraps(f)
|
| 185 |
+
def decorated_function(*args, **kwargs):
|
| 186 |
+
if 'user_id' not in session:
|
| 187 |
+
return jsonify({'error': 'Vui lòng đăng nhập'}), 401
|
| 188 |
+
user = db.users.find_one({'_id': ObjectId(session['user_id'])})
|
| 189 |
+
if not user or not user.get('is_admin'):
|
| 190 |
+
return jsonify({'error': 'Quyền truy cập bị từ chối. Chỉ admin được phép.'}), 403
|
| 191 |
+
return f(*args, **kwargs)
|
| 192 |
+
return decorated_function
|
| 193 |
+
|
| 194 |
+
# Preprocess related questions
|
| 195 |
+
def preprocess_related_questions(related_questions_input: str | List[Dict[str, str]]) -> List[Dict[str, str]]:
|
| 196 |
+
fallback_questions = [
|
| 197 |
+
{"question": "Quy định pháp luật Việt Nam hiện hành về xử lý tranh chấp hợp đồng dân sự được quy định trong văn bản nào?"},
|
| 198 |
+
{"question": "Trường hợp nào thì một bản án có thể được sử dụng làm án lệ theo quy định của pháp luật Việt Nam?"},
|
| 199 |
+
{"question": "Các nguyên tắc cơ bản của Bộ luật Dân sự Việt Nam năm 2015 được quy định tại điều khoản nào?"},
|
| 200 |
+
{"question": "Nghị định nào quy định về xử phạt vi phạm hành chính trong lĩnh vực hôn nhân và gia đình tại Việt Nam?"},
|
| 201 |
+
{"question": "Quy trình áp dụng pháp luật trong trường hợp không có bản án tương đồng được thực hiện như thế nào?"}
|
| 202 |
+
]
|
| 203 |
+
if isinstance(related_questions_input, str):
|
| 204 |
+
cleaned_input = re.sub(r'^```json\s*|\s*```$', '', related_questions_input).strip()
|
| 205 |
+
try:
|
| 206 |
+
related_questions = json.loads(cleaned_input)
|
| 207 |
+
except json.JSONDecodeError:
|
| 208 |
+
return fallback_questions[:5]
|
| 209 |
+
else:
|
| 210 |
+
related_questions = related_questions_input
|
| 211 |
+
if not isinstance(related_questions, list):
|
| 212 |
+
return fallback_questions[:5]
|
| 213 |
+
valid_questions = [
|
| 214 |
+
q for q in related_questions
|
| 215 |
+
if isinstance(q, dict) and "question" in q and isinstance(q["question"], str) and q["question"].strip()
|
| 216 |
+
]
|
| 217 |
+
seen = set()
|
| 218 |
+
unique_questions = []
|
| 219 |
+
for q in valid_questions:
|
| 220 |
+
question_text = q["question"].strip()
|
| 221 |
+
if question_text not in seen:
|
| 222 |
+
seen.add(question_text)
|
| 223 |
+
unique_questions.append({"question": question_text})
|
| 224 |
+
legal_keywords = r"(Luật|Bộ luật|Nghị định|Thông tư|Quy định|án lệ|Việt Nam|tòa án|pháp luật|điều luật|Bảo hiểm xã hội)"
|
| 225 |
+
filtered_questions = [
|
| 226 |
+
q for q in unique_questions
|
| 227 |
+
if re.search(legal_keywords, q["question"], re.IGNORECASE)
|
| 228 |
+
]
|
| 229 |
+
if len(filtered_questions) < 5:
|
| 230 |
+
remaining = 5 - len(filtered_questions)
|
| 231 |
+
for fq in fallback_questions:
|
| 232 |
+
if len(filtered_questions) >= 5:
|
| 233 |
+
break
|
| 234 |
+
if fq["question"] not in seen:
|
| 235 |
+
filtered_questions.append(fq)
|
| 236 |
+
seen.add(fq["question"])
|
| 237 |
+
return filtered_questions[:5]
|
| 238 |
+
|
| 239 |
+
def format_chat_history(memory):
|
| 240 |
+
messages = memory.chat_memory.messages
|
| 241 |
+
if not messages:
|
| 242 |
+
return "Không có lịch sử hội thoại trước."
|
| 243 |
+
formatted = []
|
| 244 |
+
for m in messages:
|
| 245 |
+
role = getattr(m, "type", None) or m.get("role", "User")
|
| 246 |
+
content = getattr(m, "content", None) or m.get("content", "")
|
| 247 |
+
formatted.append(f"{role.capitalize()}: {content}")
|
| 248 |
+
return "\n".join(formatted)
|
| 249 |
+
|
| 250 |
+
# WebSocket handlers
|
| 251 |
+
@socketio.on('connect')
|
| 252 |
+
def handle_connect():
|
| 253 |
+
user_id = session.get('user_id')
|
| 254 |
+
if user_id:
|
| 255 |
+
connected_clients[user_id] = request.sid
|
| 256 |
+
logging.info(f"User {user_id} connected via WebSocket with SID {request.sid}")
|
| 257 |
+
else:
|
| 258 |
+
disconnect() # Disconnect unauthorized clients
|
| 259 |
+
logging.warning("Unauthorized WebSocket connection attempt")
|
| 260 |
+
|
| 261 |
+
@socketio.on('disconnect')
|
| 262 |
+
def handle_disconnect():
|
| 263 |
+
user_id = session.get('user_id')
|
| 264 |
+
if user_id in connected_clients and connected_clients[user_id] == request.sid:
|
| 265 |
+
del connected_clients[user_id]
|
| 266 |
+
logging.info(f"User {user_id} disconnected from WebSocket")
|
| 267 |
+
|
| 268 |
+
# Đăng ký
|
| 269 |
+
@app.route('/register', methods=['GET', 'POST'])
|
| 270 |
+
def register():
|
| 271 |
+
if request.method == 'GET':
|
| 272 |
+
return render_template('register.html')
|
| 273 |
+
data = request.get_json(silent=True) or {}
|
| 274 |
+
username = data.get('username', '').strip()
|
| 275 |
+
email = data.get('email', '').strip()
|
| 276 |
+
password = data.get('password', '').strip()
|
| 277 |
+
phone = data.get('phone', '').strip()
|
| 278 |
+
account_type = data.get('account_type', 'limited').strip()
|
| 279 |
+
if not username or not email or not password or not phone:
|
| 280 |
+
return jsonify({'error': 'Thiếu thông tin bắt buộc'}), 400
|
| 281 |
+
if not re.match(r'^\+84\d{9}$|^0\d{9}$', phone):
|
| 282 |
+
return jsonify({'error': 'Số điện thoại không hợp lệ'}), 400
|
| 283 |
+
if account_type not in ['limited', 'unlimited']:
|
| 284 |
+
return jsonify({'error': 'Loại tài khoản không hợp lệ'}), 400
|
| 285 |
+
if db.users.find_one({'$or': [{'email': email}, {'phone': phone}]}):
|
| 286 |
+
return jsonify({'error': 'Email hoặc số điện thoại đã tồn tại'}), 400
|
| 287 |
+
otp = generate_otp()
|
| 288 |
+
password_hash = hash_password(password)
|
| 289 |
+
user = {
|
| 290 |
+
'username': username,
|
| 291 |
+
'email': email,
|
| 292 |
+
'phone': phone,
|
| 293 |
+
'password_hash': password_hash,
|
| 294 |
+
'otp': otp,
|
| 295 |
+
'is_active': False,
|
| 296 |
+
'created_at': datetime.utcnow(),
|
| 297 |
+
'is_admin': False,
|
| 298 |
+
'account_type': account_type,
|
| 299 |
+
'query_limit': 3 if account_type == 'limited' else None,
|
| 300 |
+
'query_count': 0,
|
| 301 |
+
'last_reset': datetime.utcnow()
|
| 302 |
+
}
|
| 303 |
+
result = db.users.insert_one(user)
|
| 304 |
+
if send_email(
|
| 305 |
+
email,
|
| 306 |
+
'Mã OTP xác thực tài khoản',
|
| 307 |
+
f'Mã OTP của bạn là: {otp}. Vui lòng sử dụng mã này để xác thực tài khoản.'
|
| 308 |
+
):
|
| 309 |
+
return jsonify({
|
| 310 |
+
'message': 'Đăng ký thành công, vui lòng kiểm tra email để lấy mã OTP',
|
| 311 |
+
'user_id': str(result.inserted_id)
|
| 312 |
+
}), 201
|
| 313 |
+
else:
|
| 314 |
+
db.users.delete_one({'_id': result.inserted_id})
|
| 315 |
+
return jsonify({'error': 'Lỗi khi gửi OTP, vui lòng thử lại'}), 500
|
| 316 |
+
|
| 317 |
+
# Xác thực OTP
|
| 318 |
+
@app.route('/verify_otp', methods=['GET', 'POST'])
|
| 319 |
+
def verify_otp():
|
| 320 |
+
if request.method == 'GET':
|
| 321 |
+
user_id = request.args.get('user_id')
|
| 322 |
+
if not user_id:
|
| 323 |
+
return jsonify({'error': 'Thiếu user_id'}), 400
|
| 324 |
+
try:
|
| 325 |
+
user = db.users.find_one({'_id': ObjectId(user_id)})
|
| 326 |
+
if not user:
|
| 327 |
+
return jsonify({'error': 'Người dùng không tồn tại'}), 404
|
| 328 |
+
return render_template('verify_otp.html', user_id=user_id)
|
| 329 |
+
except Exception as e:
|
| 330 |
+
logging.error(f"Invalid user_id: {e}")
|
| 331 |
+
return jsonify({'error': 'user_id không hợp lệ'}), 400
|
| 332 |
+
elif request.method == 'POST':
|
| 333 |
+
data = request.get_json(silent=True) or {}
|
| 334 |
+
user_id = data.get('user_id', '').strip()
|
| 335 |
+
otp = data.get('otp', '').strip()
|
| 336 |
+
if not user_id or not otp:
|
| 337 |
+
return jsonify({'error': 'Thiếu user_id hoặc OTP'}), 400
|
| 338 |
+
try:
|
| 339 |
+
user = db.users.find_one({'_id': ObjectId(user_id)})
|
| 340 |
+
if not user:
|
| 341 |
+
return jsonify({'error': 'Người dùng không tồn tại'}), 404
|
| 342 |
+
if user.get('otp') != otp:
|
| 343 |
+
return jsonify({'error': 'Mã OTP không đúng'}), 400
|
| 344 |
+
db.users.update_one(
|
| 345 |
+
{'_id': ObjectId(user_id)},
|
| 346 |
+
{'$set': {'is_active': True, 'otp': None}}
|
| 347 |
+
)
|
| 348 |
+
return jsonify({'message': 'Xác thực tài khoản thành công'}), 200
|
| 349 |
+
except Exception as e:
|
| 350 |
+
logging.error(f"Error verifying OTP: {e}")
|
| 351 |
+
return jsonify({'error': 'Lỗi hệ thống, vui lòng thử lại'}), 500
|
| 352 |
+
|
| 353 |
+
# Get masked phone number
|
| 354 |
+
@app.route('/get_masked_phone', methods=['POST'])
|
| 355 |
+
def get_masked_phone():
|
| 356 |
+
data = request.get_json(silent=True) or {}
|
| 357 |
+
email = data.get('email', '').strip()
|
| 358 |
+
if not email:
|
| 359 |
+
return jsonify({'error': 'Thiếu email'}), 400
|
| 360 |
+
user = db.users.find_one({'email': email})
|
| 361 |
+
if not user:
|
| 362 |
+
return jsonify({'error': 'Email không tồn tại'}), 404
|
| 363 |
+
phone = user.get('phone', '')
|
| 364 |
+
masked_phone = phone[:-4] + '****'
|
| 365 |
+
return jsonify({'masked_phone': masked_phone}), 200
|
| 366 |
+
|
| 367 |
+
# Quên mật khẩu
|
| 368 |
+
@app.route('/forgot_password', methods=['GET', 'POST'])
|
| 369 |
+
def forgot_password():
|
| 370 |
+
if request.method == 'GET':
|
| 371 |
+
return render_template('forgot_password.html')
|
| 372 |
+
elif request.method == 'POST':
|
| 373 |
+
data = request.get_json(silent=True) or {}
|
| 374 |
+
email = data.get('email', '').strip()
|
| 375 |
+
last_four_digits = data.get('last_four_digits', '').strip()
|
| 376 |
+
if not email or not last_four_digits:
|
| 377 |
+
return jsonify({'error': 'Thiếu email hoặc 4 số cuối của số điện thoại'}), 400
|
| 378 |
+
user = db.users.find_one({'email': email})
|
| 379 |
+
if not user:
|
| 380 |
+
return jsonify({'error': 'Email không tồn tại'}), 404
|
| 381 |
+
phone = user.get('phone', '')
|
| 382 |
+
if not phone[-4:] == last_four_digits:
|
| 383 |
+
return jsonify({'error': '4 số cuối của số điện thoại không khớp'}), 400
|
| 384 |
+
new_password = ''.join(random.choices(string.ascii_letters + string.digits, k=12))
|
| 385 |
+
new_password_hash = hash_password(new_password)
|
| 386 |
+
db.users.update_one(
|
| 387 |
+
{'_id': user['_id']},
|
| 388 |
+
{'$set': {'password_hash': new_password_hash}}
|
| 389 |
+
)
|
| 390 |
+
if send_email(
|
| 391 |
+
email,
|
| 392 |
+
'Mật khẩu mới',
|
| 393 |
+
f'Mật khẩu mới của bạn là: {new_password}. Vui lòng đổi mật khẩu sau khi đăng nhập.'
|
| 394 |
+
):
|
| 395 |
+
return jsonify({'message': 'Mật khẩu mới đã được gửi qua email'}), 200
|
| 396 |
+
else:
|
| 397 |
+
return jsonify({'error': 'Lỗi khi gửi mật khẩu mới'}), 500
|
| 398 |
+
|
| 399 |
+
@app.route('/change_password', methods=['GET'])
|
| 400 |
+
def change_password_get():
|
| 401 |
+
if 'user_id' not in session:
|
| 402 |
+
return redirect(url_for('login_page'))
|
| 403 |
+
return render_template('change_password.html')
|
| 404 |
+
|
| 405 |
+
# Đổi mật khẩu
|
| 406 |
+
@app.route('/change_password', methods=['POST'])
|
| 407 |
+
def change_password():
|
| 408 |
+
if 'user_id' not in session:
|
| 409 |
+
return jsonify({'error': 'Vui lòng đăng nhập để đổi mật khẩu'}), 401
|
| 410 |
+
|
| 411 |
+
data = request.get_json(silent=True) or {}
|
| 412 |
+
current_password = data.get('current_password', '').strip()
|
| 413 |
+
new_password = data.get('new_password', '').strip()
|
| 414 |
+
|
| 415 |
+
if not current_password or not new_password:
|
| 416 |
+
return jsonify({'error': 'Thiếu mật khẩu hiện tại hoặc mật khẩu mới'}), 400
|
| 417 |
+
|
| 418 |
+
user_id = session['user_id']
|
| 419 |
+
user = db.users.find_one({'_id': ObjectId(user_id)})
|
| 420 |
+
if not user:
|
| 421 |
+
return jsonify({'error': 'Người dùng không tồn tại'}), 404
|
| 422 |
+
|
| 423 |
+
if not verify_password(user['password_hash'], current_password):
|
| 424 |
+
return jsonify({'error': 'Mật khẩu hiện tại không đúng'}), 401
|
| 425 |
+
|
| 426 |
+
new_password_hash = hash_password(new_password)
|
| 427 |
+
db.users.update_one(
|
| 428 |
+
{'_id': ObjectId(user_id)},
|
| 429 |
+
{'$set': {'password_hash': new_password_hash}}
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
# Send confirmation email
|
| 433 |
+
if send_email(
|
| 434 |
+
user['email'],
|
| 435 |
+
'Xác nhận đổi mật khẩu',
|
| 436 |
+
f'Mật khẩu của bạn đã được thay đổi thành công vào lúc {datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")}.'
|
| 437 |
+
):
|
| 438 |
+
return jsonify({'message': 'Đổi mật khẩu thành công, email xác nhận đã được gửi'}), 200
|
| 439 |
+
else:
|
| 440 |
+
return jsonify({'error': 'Đổi mật khẩu thành công nhưng lỗi khi gửi email xác nhận'}), 200
|
| 441 |
+
|
| 442 |
+
# # Đăng nhập
|
| 443 |
+
@app.route('/logins', methods=['POST'])
|
| 444 |
+
def login():
|
| 445 |
+
data = request.get_json(silent=True) or {}
|
| 446 |
+
email = data.get('email', '').strip()
|
| 447 |
+
password = data.get('password', '').strip()
|
| 448 |
+
user = db.users.find_one({'email': email})
|
| 449 |
+
if not user:
|
| 450 |
+
logging.error(f"No user found for email: {email}")
|
| 451 |
+
return jsonify({'error': 'Email hoặc mật khẩu không đúng'}), 401
|
| 452 |
+
if not user.get('is_active', False):
|
| 453 |
+
return jsonify({'error': 'Tài khoản chưa được kích hoạt. Vui lòng xác thực OTP.'}), 401
|
| 454 |
+
if not verify_password(user['password_hash'], password):
|
| 455 |
+
return jsonify({'error': 'Email hoặc mật khẩu không đúng'}), 401
|
| 456 |
+
session['user_id'] = str(user['_id'])
|
| 457 |
+
session['username'] = user['username']
|
| 458 |
+
session['is_admin'] = user.get('is_admin', False)
|
| 459 |
+
session['query_limit'] = user.get('query_limit', 3)
|
| 460 |
+
session['query_count'] = user.get('query_count', 0)
|
| 461 |
+
session['account_type'] = user.get('account_type', 'limited')
|
| 462 |
+
|
| 463 |
+
# Broadcast initial query count and limit
|
| 464 |
+
if str(user['_id']) in connected_clients:
|
| 465 |
+
socketio.emit('query_update', {
|
| 466 |
+
'query_count': user.get('query_count', 0),
|
| 467 |
+
'query_limit': user.get('query_limit', 3 if user.get('account_type') == 'limited' else None)
|
| 468 |
+
}, room=connected_clients[str(user['_id'])])
|
| 469 |
+
logging.info(f"Broadcasted initial query update to user {user['_id']}")
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
return jsonify({
|
| 473 |
+
'message': 'Đăng nhập thành công',
|
| 474 |
+
'username': user['username'],
|
| 475 |
+
'is_admin': user.get('is_admin', False),
|
| 476 |
+
'account_type': user.get('account_type', 'limited'),
|
| 477 |
+
'query_limit': user.get('query_limit', 3),
|
| 478 |
+
'query_count': user.get('query_count', 0),
|
| 479 |
+
}), 200
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
# @app.route('/logins', methods=['POST'])
|
| 483 |
+
# def login():
|
| 484 |
+
# data = request.get_json(silent=True) or {}
|
| 485 |
+
# email = data.get('email', '').strip()
|
| 486 |
+
# password = data.get('password', '').strip()
|
| 487 |
+
# user = db.users.find_one({'email': email})
|
| 488 |
+
# if not user:
|
| 489 |
+
# logging.error(f"No user found for email: {email}")
|
| 490 |
+
# return jsonify({'error': 'Email hoặc mật khẩu không đúng'}), 401
|
| 491 |
+
# if not user.get('is_active', False):
|
| 492 |
+
# return jsonify({'error': 'Tài khoản chưa được kích hoạt. Vui lòng xác thực OTP.'}), 401
|
| 493 |
+
# if not verify_password(user['password_hash'], password):
|
| 494 |
+
# return jsonify({'error': 'Email hoặc mật khẩu không đúng'}), 401
|
| 495 |
+
# session['user_id'] = str(user['_id'])
|
| 496 |
+
# session['username'] = user['username']
|
| 497 |
+
# session['is_admin'] = user.get('is_admin', False)
|
| 498 |
+
# session['query_limit'] = user.get('query_limit', 3)
|
| 499 |
+
# session['query_count'] = user.get('query_count', 0)
|
| 500 |
+
# session['account_type'] = user.get('account_type', 'limited')
|
| 501 |
+
# return jsonify({
|
| 502 |
+
# 'message': 'Đăng nhập thành công',
|
| 503 |
+
# 'username': user['username'],
|
| 504 |
+
# 'is_admin': user.get('is_admin', False),
|
| 505 |
+
# 'account_type': user.get('account_type', 'limited'),
|
| 506 |
+
# 'query_limit': user.get('query_limit', 3),
|
| 507 |
+
# 'query_count': user.get('query_count', 0)
|
| 508 |
+
# }), 200
|
| 509 |
+
|
| 510 |
+
# Đăng xuất
|
| 511 |
+
@app.route('/logout', methods=['POST'])
|
| 512 |
+
def logout():
|
| 513 |
+
user_id = session.get('user_id')
|
| 514 |
+
if user_id in connected_clients:
|
| 515 |
+
del connected_clients[user_id] # Remove from connected clients
|
| 516 |
+
logging.info(f"User {user_id} removed from connected clients on logout")
|
| 517 |
+
session.pop('user_id', None)
|
| 518 |
+
session.pop('username', None)
|
| 519 |
+
session.pop('is_admin', None)
|
| 520 |
+
session.pop('account_type', None)
|
| 521 |
+
return jsonify({'message': 'Đăng xuất thành công'}), 200
|
| 522 |
+
|
| 523 |
+
# Kiểm tra session
|
| 524 |
+
@app.route('/check_session', methods=['GET'])
|
| 525 |
+
def check_session():
|
| 526 |
+
if 'user_id' in session:
|
| 527 |
+
user = db.users.find_one({'_id': ObjectId(session['user_id'])})
|
| 528 |
+
if user:
|
| 529 |
+
# Update session with latest values
|
| 530 |
+
session['query_limit'] = user.get('query_limit', 3)
|
| 531 |
+
session['query_count'] = user.get('query_count', 0)
|
| 532 |
+
session['account_type'] = user.get('account_type', 'limited')
|
| 533 |
+
# Broadcast current query count and limit
|
| 534 |
+
if session['user_id'] in connected_clients:
|
| 535 |
+
socketio.emit('query_update', {
|
| 536 |
+
'query_count': user.get('query_count', 0),
|
| 537 |
+
'query_limit': user.get('query_limit', 3 if user.get('account_type') == 'limited' else None)
|
| 538 |
+
}, room=connected_clients[session['user_id']])
|
| 539 |
+
logging.info(f"Broadcasted query update to user {session['user_id']} on session check")
|
| 540 |
+
return jsonify({
|
| 541 |
+
'logged_in': True,
|
| 542 |
+
'username': session['username'],
|
| 543 |
+
'query_limit': session['query_limit'],
|
| 544 |
+
'query_count': session['query_count'],
|
| 545 |
+
'is_admin': session.get('is_admin', False),
|
| 546 |
+
'account_type': session['account_type']
|
| 547 |
+
}), 200
|
| 548 |
+
return jsonify({'logged_in': False}), 200
|
| 549 |
+
# @app.route('/check_session', methods=['GET'])
|
| 550 |
+
# def check_session():
|
| 551 |
+
# if 'user_id' in session:
|
| 552 |
+
# return jsonify({
|
| 553 |
+
# 'logged_in': True,
|
| 554 |
+
# 'username': session['username'],
|
| 555 |
+
# 'query_limit': session.get('query_limit', 3),
|
| 556 |
+
# 'query_count': session.get('query_count', 0),
|
| 557 |
+
# 'is_admin': session.get('is_admin', False),
|
| 558 |
+
# 'account_type': session.get('account_type', 'limited')
|
| 559 |
+
# }), 200
|
| 560 |
+
# return jsonify({'logged_in': False}), 200
|
| 561 |
+
|
| 562 |
+
|
| 563 |
+
@socketio.on('connect')
|
| 564 |
+
def handle_connect():
|
| 565 |
+
user_id = session.get('user_id')
|
| 566 |
+
if user_id:
|
| 567 |
+
connected_clients[user_id] = request.sid
|
| 568 |
+
user = db.users.find_one({'_id': ObjectId(user_id)})
|
| 569 |
+
if user:
|
| 570 |
+
socketio.emit('query_update', {
|
| 571 |
+
'query_count': user.get('query_count', 0),
|
| 572 |
+
'query_limit': user.get('query_limit', 3 if user.get('account_type') == 'limited' else None)
|
| 573 |
+
}, room=request.sid)
|
| 574 |
+
logging.info(f"Emitted initial query_update to user {user_id} on connect")
|
| 575 |
+
|
| 576 |
+
# Lấy danh sách hội thoại
|
| 577 |
+
@app.route('/conversations', methods=['GET'])
|
| 578 |
+
def get_conversations():
|
| 579 |
+
if 'user_id' not in session:
|
| 580 |
+
return jsonify({'error': 'Vui lòng đăng nhập'}), 401
|
| 581 |
+
user_id = session['user_id']
|
| 582 |
+
conversations = db.conversations.find({'user_id': user_id}).sort('timestamp', -1)
|
| 583 |
+
return jsonify([{
|
| 584 |
+
'id': str(conv['_id']),
|
| 585 |
+
'title': conv['title'],
|
| 586 |
+
'timestamp': conv['timestamp'].isoformat(),
|
| 587 |
+
'message_count': db.messages.count_documents({'conversation_id': str(conv['_id'])})
|
| 588 |
+
} for conv in conversations])
|
| 589 |
+
|
| 590 |
+
# Lấy chi tiết hội thoại
|
| 591 |
+
@app.route('/conversation/<conversation_id>', methods=['GET'])
|
| 592 |
+
def get_conversation(conversation_id):
|
| 593 |
+
if 'user_id' not in session:
|
| 594 |
+
return jsonify({'error': 'Vui lòng đăng nhập'}), 401
|
| 595 |
+
user_id = session['user_id']
|
| 596 |
+
conversation = db.conversations.find_one({'_id': ObjectId(conversation_id), 'user_id': user_id})
|
| 597 |
+
if not conversation:
|
| 598 |
+
return jsonify({'error': 'Hội thoại không tồn tại'}), 404
|
| 599 |
+
messages = db.messages.find({'conversation_id': conversation_id})
|
| 600 |
+
messages_list = [{
|
| 601 |
+
'id': str(msg['_id']),
|
| 602 |
+
'type': msg['type'],
|
| 603 |
+
'content': msg['content'],
|
| 604 |
+
'timestamp': msg['timestamp'].isoformat(),
|
| 605 |
+
'sources': msg.get('sources'),
|
| 606 |
+
'related_questions': msg.get('related_questions')
|
| 607 |
+
} for msg in messages]
|
| 608 |
+
return jsonify({
|
| 609 |
+
'id': str(conversation['_id']),
|
| 610 |
+
'title': conversation['title'],
|
| 611 |
+
'timestamp': conversation['timestamp'].isoformat(),
|
| 612 |
+
'messages': messages_list
|
| 613 |
+
})
|
| 614 |
+
|
| 615 |
+
# Xử lý truy vấn
|
| 616 |
+
@app.route("/query", methods=["POST"])
|
| 617 |
+
def query():
|
| 618 |
+
if 'user_id' not in session:
|
| 619 |
+
return jsonify({
|
| 620 |
+
'error': 'Vui lòng đăng nhập để sử dụng tính năng này',
|
| 621 |
+
'error_code': 'UNAUTHENTICATED'
|
| 622 |
+
}), 401
|
| 623 |
+
|
| 624 |
+
user_id = session['user_id']
|
| 625 |
+
|
| 626 |
+
# Validate user_id format
|
| 627 |
+
try:
|
| 628 |
+
ObjectId(user_id)
|
| 629 |
+
except Exception:
|
| 630 |
+
return jsonify({
|
| 631 |
+
'error': 'ID người dùng không hợp lệ',
|
| 632 |
+
'error_code': 'INVALID_USER_ID'
|
| 633 |
+
}), 400
|
| 634 |
+
|
| 635 |
+
# Check query permission
|
| 636 |
+
can_query, error_message, query_count, query_limit = can_make_query(user_id)
|
| 637 |
+
if not can_query:
|
| 638 |
+
return jsonify({
|
| 639 |
+
'error': error_message, # e.g., "Bạn đã sử dụng hết 10 lượt hỏi đáp hôm nay"
|
| 640 |
+
'error_code': 'QUERY_LIMIT_EXCEEDED',
|
| 641 |
+
'query_count': query_count,
|
| 642 |
+
'query_limit': query_limit,
|
| 643 |
+
'upgrade_url': 'https://legal.loca.lt' # Redirect to upgrade page
|
| 644 |
+
}), 403
|
| 645 |
+
elif error_message: # Warning for last query
|
| 646 |
+
logging.info(f"User {user_id} received warning: {error_message}")
|
| 647 |
+
|
| 648 |
+
# Parse JSON input
|
| 649 |
+
data = request.get_json(silent=True) or {}
|
| 650 |
+
question = data.get('question', '').strip()
|
| 651 |
+
if not question:
|
| 652 |
+
return jsonify({
|
| 653 |
+
'error': 'Câu hỏi không hợp lệ',
|
| 654 |
+
'error_code': 'INVALID_QUESTION'
|
| 655 |
+
}), 400
|
| 656 |
+
|
| 657 |
+
# Update query count for limited accounts
|
| 658 |
+
user = db.users.find_one({'_id': ObjectId(user_id)})
|
| 659 |
+
if not user:
|
| 660 |
+
return jsonify({
|
| 661 |
+
'error': 'Người dùng không tồn tại',
|
| 662 |
+
'error_code': 'USER_NOT_FOUND'
|
| 663 |
+
}), 404
|
| 664 |
+
|
| 665 |
+
if user.get('account_type') == 'limited':
|
| 666 |
+
try:
|
| 667 |
+
db.users.update_one(
|
| 668 |
+
{'_id': ObjectId(user_id)},
|
| 669 |
+
{'$inc': {'query_count': 1}}
|
| 670 |
+
)
|
| 671 |
+
# Fetch updated user data
|
| 672 |
+
user = db.users.find_one({'_id': ObjectId(user_id)})
|
| 673 |
+
query_count = user.get('query_count', 0)
|
| 674 |
+
query_limit = user.get('query_limit', 10)
|
| 675 |
+
user_type = user.get('account_type', 'limited')
|
| 676 |
+
# Broadcast updated query count to the user
|
| 677 |
+
if user_id in connected_clients:
|
| 678 |
+
socketio.emit('query_update', {
|
| 679 |
+
'query_count': query_count,
|
| 680 |
+
'query_limit': query_limit,
|
| 681 |
+
'user_type': user_type
|
| 682 |
+
}, room=connected_clients[user_id])
|
| 683 |
+
logging.info(f"Broadcasted query update to user {user_id}: {query_count}/{query_limit}")
|
| 684 |
+
except Exception as e:
|
| 685 |
+
logging.error(f"Error updating query count for user {user_id}: {e}")
|
| 686 |
+
return jsonify({
|
| 687 |
+
'error': f"Bạn đã sử dụng hết {query_limit} lượt hỏi đáp hôm nay", # Use specific message
|
| 688 |
+
'error_code': 'DATABASE_ERROR',
|
| 689 |
+
'query_count': query_count,
|
| 690 |
+
'query_limit': query_limit,
|
| 691 |
+
'user_type': user_type,
|
| 692 |
+
'upgrade_url': 'https://legal.loca.lt/'
|
| 693 |
+
}), 500
|
| 694 |
+
|
| 695 |
+
# Handle conversation
|
| 696 |
+
conversation_id = data.get('conversation_id')
|
| 697 |
+
if conversation_id:
|
| 698 |
+
try:
|
| 699 |
+
conversation = db.conversations.find_one({'_id': ObjectId(conversation_id), 'user_id': user_id})
|
| 700 |
+
if not conversation:
|
| 701 |
+
return jsonify({
|
| 702 |
+
'error': 'Hội thoại không tồn tại hoặc không thuộc về người dùng',
|
| 703 |
+
'error_code': 'CONVERSATION_NOT_FOUND'
|
| 704 |
+
}), 404
|
| 705 |
+
except Exception:
|
| 706 |
+
return jsonify({
|
| 707 |
+
'error': 'ID hội thoại không hợp lệ',
|
| 708 |
+
'error_code': 'INVALID_CONVERSATION_ID'
|
| 709 |
+
}), 400
|
| 710 |
+
else:
|
| 711 |
+
conversation = {
|
| 712 |
+
'user_id': user_id,
|
| 713 |
+
'title': question[:50],
|
| 714 |
+
'timestamp': datetime.utcnow(),
|
| 715 |
+
'messages': []
|
| 716 |
+
}
|
| 717 |
+
try:
|
| 718 |
+
result = db.conversations.insert_one(conversation)
|
| 719 |
+
conversation_id = str(result.inserted_id)
|
| 720 |
+
except Exception as e:
|
| 721 |
+
logging.error(f"Error creating conversation for user {user_id}: {e}")
|
| 722 |
+
return jsonify({
|
| 723 |
+
'error': f"Bạn đã sử dụng hết {query_limit} lượt hỏi đáp hôm nay", # Use specific message
|
| 724 |
+
'error_code': 'DATABASE_ERROR',
|
| 725 |
+
'query_count': query_count,
|
| 726 |
+
'query_limit': query_limit,
|
| 727 |
+
'upgrade_url': 'https://legal.loca.lt/'
|
| 728 |
+
}), 500
|
| 729 |
+
|
| 730 |
+
# Save user message
|
| 731 |
+
user_message = {
|
| 732 |
+
'conversation_id': conversation_id,
|
| 733 |
+
'type': 'user',
|
| 734 |
+
'content': question,
|
| 735 |
+
'timestamp': datetime.utcnow()
|
| 736 |
+
}
|
| 737 |
+
try:
|
| 738 |
+
db.messages.insert_one(user_message)
|
| 739 |
+
except Exception as e:
|
| 740 |
+
logging.error(f"Error saving user message for conversation {conversation_id}: {e}")
|
| 741 |
+
return jsonify({
|
| 742 |
+
'error': f"Bạn đã sử dụng hết {query_limit} lượt hỏi đáp hôm nay", # Use specific message
|
| 743 |
+
'error_code': 'DATABASE_ERROR',
|
| 744 |
+
'query_count': query_count,
|
| 745 |
+
'query_limit': query_limit,
|
| 746 |
+
'upgrade_url': 'https://legal.loca.lt/'
|
| 747 |
+
}), 500
|
| 748 |
+
|
| 749 |
+
# Retrieve relevant legal documents
|
| 750 |
+
try:
|
| 751 |
+
banan_results = retrieve(question, index, embeddings_data, k=5)
|
| 752 |
+
except Exception as e:
|
| 753 |
+
logging.error(f"Error retrieving documents: {e}")
|
| 754 |
+
banan_results = []
|
| 755 |
+
|
| 756 |
+
# Format chat history
|
| 757 |
+
chat_history_str = format_chat_history(memory)
|
| 758 |
+
|
| 759 |
+
# Define main prompt
|
| 760 |
+
main_prompt = f"""
|
| 761 |
+
Dưới đây là lịch sử hội thoại trước đó:
|
| 762 |
+
{chat_history_str}
|
| 763 |
+
|
| 764 |
+
**Câu hỏi:**
|
| 765 |
+
{question}
|
| 766 |
+
|
| 767 |
+
**Thông tin tham khảo (bản án tương đồng):**
|
| 768 |
+
{banan_results if banan_results else "Không tìm thấy bản án phù hợp. Phân tích dựa trên các quy định pháp luật hiện hành và nguyên tắc pháp lý chung."}
|
| 769 |
+
|
| 770 |
+
**Hướng dẫn trả lời chi tiết:**
|
| 771 |
+
1. **Tổng quan về bản án, án lệ tương đồng:**
|
| 772 |
+
- Trình bày rõ thông tin tham khảo nếu có.
|
| 773 |
+
- Nhớ đề cập đến tên file bản án, không dùng từ ví dụ, giả sử, giả định.
|
| 774 |
+
- Nếu không có bản án, nêu rõ sẽ phân tích trên cơ sở các điều luật hiện hành tại Việt Nam.
|
| 775 |
+
|
| 776 |
+
2. **Nội dung chi tiết của bản án, án lệ:**
|
| 777 |
+
- Nếu có thông tin cụ thể, trình bày rõ vấn đề pháp lý và lập luận của tòa án trong bản án, án lệ liên quan.
|
| 778 |
+
- Nếu không có thông tin đầy đủ, phân tích dựa vào các nguyên tắc pháp lý chung, điều luật, nghị định hiện hành.
|
| 779 |
+
|
| 780 |
+
3. **Phân tích tình huống pháp lý:**
|
| 781 |
+
- Phân tích rõ các vấn đề pháp lý chính.
|
| 782 |
+
- Làm nổi bật các quy định cụ thể trong Bộ luật Dân sự, Luật Thương mại hoặc các luật chuyên ngành, nghị định, nghị quyết và các văn bản pháp luật liên quan.
|
| 783 |
+
|
| 784 |
+
4. **Lập luận pháp lý:**
|
| 785 |
+
- Nêu rõ căn cứ pháp lý chính xác, trích dẫn cụ thể các điều khoản, nghị định, nghị quyết, thông tư, văn bản hướng dẫn thi hành liên quan.
|
| 786 |
+
- Giải thích rõ cách thức áp dụng các điều khoản pháp luật vào tình huống thực tế, bảo đảm chính xác và khả thi trong thực tiễn.
|
| 787 |
+
|
| 788 |
+
5. **Kết luận và khuyến nghị:**
|
| 789 |
+
- Kết luận rõ quyền và nghĩa vụ các bên theo quy định của pháp luật.
|
| 790 |
+
- Chỉ ra những hậu quả pháp lý cụ thể, kèm theo lưu ý khi áp dụng vào các tình huống tương tự trong thực tế.
|
| 791 |
+
|
| 792 |
+
6. **Nguồn tham khảo:**
|
| 793 |
+
- Nguồn trích d���n pháp luật bao gồm các điều luật, nghị định, nghị quyết, thông tư, văn bản hướng dẫn thi hành liên quan đến vụ án nằm bên trong button <a href="">Tên nội dung tham khảo như là Khoản, điều, luật, nghị định...</a>.
|
| 794 |
+
**Ví dụ:**
|
| 795 |
+
<a href="">[Luật Hôn nhân và Gia đình 2014, số 52/2014/QH13]</a> (Đặc biệt Điều 3, Điều 5, Điều 8, Điều 10, Điều 11, Điều 12)
|
| 796 |
+
<a href="">[Nghị định 115/2015/NĐ-CP]</a> (Đặc biệt Điều 58)
|
| 797 |
+
<a href="">[Bộ luật Hình sự năm 2015]</a> (Đặc biệt Điều 184)
|
| 798 |
+
|
| 799 |
+
**Lưu ý quan trọng:**
|
| 800 |
+
- Có trả về nguồn trích dẫn điều luật, nghị định, nghị quyết, thông tư, văn bản hướng dẫn thi hành liên quan đến vụ án, dưới dạng <a>Tên nội dung tham khảo</a>.
|
| 801 |
+
- Nếu câu hỏi không thuộc lĩnh vực pháp lý hoặc không có thông tin pháp lý phù hợp, hãy trả lời: "Câu trả lời không nằm trong kiến thức của tôi."
|
| 802 |
+
- Trả lời ngắn gọn, súc tích, rõ ràng, đúng trọng tâm.
|
| 803 |
+
- Tuyệt đối không dùng từ "giả sử", "ví dụ".
|
| 804 |
+
- Không giới thiệu bản thân, không đề cập đến kinh nghiệm tư vấn.
|
| 805 |
+
- Không cần mô tả quy trình phân tích.
|
| 806 |
+
- Nếu không có thông tin bản án, án lệ phù hợp, hãy bỏ qua, tập trung hoàn toàn vào phân tích pháp luật hiện hành.
|
| 807 |
+
- Phân tích phải luôn kết hợp chặt chẽ giữa lý thuyết pháp lý và văn bản pháp luật Việt Nam hiện hành.
|
| 808 |
+
- Trình bày rõ ràng, ngắn gọn, sử dụng ngôn ngữ pháp lý chuẩn xác, dễ áp dụng vào thực tế.
|
| 809 |
+
"""
|
| 810 |
+
|
| 811 |
+
# Call Gemini for main response
|
| 812 |
+
try:
|
| 813 |
+
handler = GeminiHandler(
|
| 814 |
+
config_path="config.yaml",
|
| 815 |
+
content_strategy=Strategy.ROUND_ROBIN,
|
| 816 |
+
key_strategy=KeyRotationStrategy.SMART_COOLDOWN
|
| 817 |
+
)
|
| 818 |
+
gen = handler.generate_content(
|
| 819 |
+
prompt=main_prompt,
|
| 820 |
+
model_name="gemini-2.0-flash-thinking-exp-01-21",
|
| 821 |
+
return_stats=False
|
| 822 |
+
)
|
| 823 |
+
answer = gen.get("text", "Không có phản hồi từ mô hình.")
|
| 824 |
+
except Exception as e:
|
| 825 |
+
logging.error(f"Error calling Gemini for main prompt: {e}")
|
| 826 |
+
return jsonify({
|
| 827 |
+
'error': f"Bạn đã sử dụng hết {query_limit} lượt hỏi đáp hôm nay", # Use specific message
|
| 828 |
+
'error_code': 'GEMINI_ERROR',
|
| 829 |
+
'query_count': query_count,
|
| 830 |
+
'query_limit': query_limit,
|
| 831 |
+
'upgrade_url': 'https://legal.loca.lt/'
|
| 832 |
+
}), 500
|
| 833 |
+
|
| 834 |
+
# Define related questions prompt
|
| 835 |
+
related_questions_prompt = f"""
|
| 836 |
+
Bạn là chuyên gia tư vấn pháp luật Việt Nam. Dựa trên câu hỏi pháp lý được cung cấp, hãy sinh ra 5 câu hỏi liên quan, đảm bảo các câu hỏi:
|
| 837 |
+
- Liên quan chặt chẽ đến chủ đề pháp lý của câu hỏi gốc.
|
| 838 |
+
- Phù hợp với hệ thống pháp luật Việt Nam hiện hành.
|
| 839 |
+
- Ngắn gọn, rõ ràng, và mang tính ứng dụng thực tế.
|
| 840 |
+
- Tập trung vào các khía cạnh pháp lý như quy định, điều luật, nghị định, án lệ, hoặc thủ tục pháp lý.
|
| 841 |
+
- Được trình bày dưới dạng danh sách JSON, mỗi câu hỏi là một đối tượng với key `question`.
|
| 842 |
+
|
| 843 |
+
**Câu hỏi gốc:**
|
| 844 |
+
{question}
|
| 845 |
+
|
| 846 |
+
**Hướng dẫn thêm:**
|
| 847 |
+
- Nếu câu hỏi gốc thuộc một lĩnh vực pháp lý cụ thể (ví dụ: dân sự, hình sự, thương mại, hôn nhân và gia đình), hãy sinh ra các câu hỏi liên quan đến lĩnh vực đó.
|
| 848 |
+
- Nếu câu hỏi không rõ lĩnh vực, sinh ra các câu hỏi liên quan đến các khía cạnh pháp lý chung như Bộ luật Dân sự, Bộ luật Hình sự, hoặc các nghị định liên quan.
|
| 849 |
+
- Không sử dụng từ "giả sử" hoặc "ví dụ".
|
| 850 |
+
- Không lặp lại câu hỏi gốc.
|
| 851 |
+
- Đảm bảo các câu hỏi có tính liên quan và không trùng lặp nội dung.
|
| 852 |
+
|
| 853 |
+
**Định dạng đầu ra (JSON):**
|
| 854 |
+
[
|
| 855 |
+
{{"question": "Câu hỏi 1"}},
|
| 856 |
+
{{"question": "Câu hỏi 2"}},
|
| 857 |
+
{{"question": "Câu hỏi 3"}},
|
| 858 |
+
{{"question": "Câu hỏi 4"}},
|
| 859 |
+
{{"question": "Câu hỏi 5"}}
|
| 860 |
+
]
|
| 861 |
+
"""
|
| 862 |
+
try:
|
| 863 |
+
handler = GeminiHandler(
|
| 864 |
+
config_path="config.yaml",
|
| 865 |
+
content_strategy=Strategy.ROUND_ROBIN,
|
| 866 |
+
key_strategy=KeyRotationStrategy.SMART_COOLDOWN
|
| 867 |
+
)
|
| 868 |
+
gen = handler.generate_content(
|
| 869 |
+
prompt=related_questions_prompt,
|
| 870 |
+
model_name="gemini-2.0-flash-thinking-exp-01-21",
|
| 871 |
+
return_stats=False
|
| 872 |
+
)
|
| 873 |
+
related_questions = gen.get("text", "Không có phản hồi từ mô hình.")
|
| 874 |
+
except Exception as e:
|
| 875 |
+
logging.error(f"Error calling Gemini for related questions: {e}")
|
| 876 |
+
return jsonify({
|
| 877 |
+
'error': f"Bạn đã sử dụng hết {query_limit} lượt hỏi đáp hôm nay", # Use specific message
|
| 878 |
+
'error_code': 'GEMINI_ERROR',
|
| 879 |
+
'query_count': query_count,
|
| 880 |
+
'query_limit': query_limit,
|
| 881 |
+
'upgrade_url': 'https://legal.loca.lt/'
|
| 882 |
+
}), 500
|
| 883 |
+
|
| 884 |
+
related_questions = preprocess_related_questions(related_questions)
|
| 885 |
+
assistant_message = {
|
| 886 |
+
'conversation_id': conversation_id,
|
| 887 |
+
'type': 'assistant',
|
| 888 |
+
'content': answer,
|
| 889 |
+
'timestamp': datetime.utcnow(),
|
| 890 |
+
'sources': banan_results,
|
| 891 |
+
'related_questions': related_questions
|
| 892 |
+
}
|
| 893 |
+
try:
|
| 894 |
+
db.messages.insert_one(assistant_message)
|
| 895 |
+
except Exception as e:
|
| 896 |
+
logging.error(f"Error saving assistant message for conversation {conversation_id}: {e}")
|
| 897 |
+
return jsonify({
|
| 898 |
+
'error': f"Bạn đã sử dụng hết {query_limit} lượt hỏi đáp hôm nay", # Use specific message
|
| 899 |
+
'error_code': 'DATABASE_ERROR',
|
| 900 |
+
'query_count': query_count,
|
| 901 |
+
'query_limit': query_limit,
|
| 902 |
+
'upgrade_url': 'https://legal.loca.lt/'
|
| 903 |
+
}), 500
|
| 904 |
+
|
| 905 |
+
try:
|
| 906 |
+
db.conversations.update_one(
|
| 907 |
+
{'_id': ObjectId(conversation_id)},
|
| 908 |
+
{'$set': {'title': question[:50], 'timestamp': datetime.utcnow()}}
|
| 909 |
+
)
|
| 910 |
+
except Exception as e:
|
| 911 |
+
logging.error(f"Error updating conversation {conversation_id}: {e}")
|
| 912 |
+
return jsonify({
|
| 913 |
+
'error': f"Bạn đã sử dụng hết {query_limit} lượt hỏi đáp hôm nay", # Use specific message
|
| 914 |
+
'error_code': 'DATABASE_ERROR',
|
| 915 |
+
'query_count': query_count,
|
| 916 |
+
'query_limit': query_limit,
|
| 917 |
+
'upgrade_url': 'https://legal.loca.lt/'
|
| 918 |
+
}), 500
|
| 919 |
+
|
| 920 |
+
memory.save_context({'question': question}, {'answer': answer})
|
| 921 |
+
return jsonify({
|
| 922 |
+
'final_response': answer,
|
| 923 |
+
'top_banan_documents': banan_results,
|
| 924 |
+
'chat_history': chat_history_str,
|
| 925 |
+
'related_questions': related_questions,
|
| 926 |
+
'conversation_id': conversation_id,
|
| 927 |
+
'query_count': query_count,
|
| 928 |
+
'query_limit': query_limit
|
| 929 |
+
}), 200
|
| 930 |
+
|
| 931 |
+
# Soạn thảo bản án
|
| 932 |
+
@app.route("/draft_judgment", methods=["POST"])
|
| 933 |
+
def draft_judgment():
|
| 934 |
+
if 'user_id' not in session:
|
| 935 |
+
return jsonify({'error': 'Vui lòng đăng nhập'}), 401
|
| 936 |
+
data = request.get_json(silent=True) or {}
|
| 937 |
+
case_details = data.get('case_details', '').strip()
|
| 938 |
+
if not case_details:
|
| 939 |
+
return jsonify({'error': 'Chi tiết vụ án không hợp lệ'}), 400
|
| 940 |
+
banan_results = retrieve(case_details, index, embeddings_data, k=2)
|
| 941 |
+
top_banan_docs = [{'source': r['file'], **r} for r in banan_results]
|
| 942 |
+
chat_history_str = format_chat_history(memory)
|
| 943 |
+
judgment = "Placeholder judgment: Drafted legal document based on case details."
|
| 944 |
+
memory.save_context({'case_details': case_details}, {'judgment': judgment})
|
| 945 |
+
return jsonify({
|
| 946 |
+
'judgment': judgment,
|
| 947 |
+
'top_banan_documents': top_banan_docs,
|
| 948 |
+
'chat_history': chat_history_str
|
| 949 |
+
})
|
| 950 |
+
|
| 951 |
+
# Xóa hội thoại
|
| 952 |
+
@app.route('/conversation/<conversation_id>', methods=['DELETE'])
|
| 953 |
+
def delete_conversation(conversation_id):
|
| 954 |
+
if 'user_id' not in session:
|
| 955 |
+
return jsonify({'error': 'Vui lòng đăng nhập'}), 401
|
| 956 |
+
user_id = session['user_id']
|
| 957 |
+
result = db.conversations.delete_one({'_id': ObjectId(conversation_id), 'user_id': user_id})
|
| 958 |
+
if result.deleted_count == 0:
|
| 959 |
+
return jsonify({'error': 'Hội thoại không tồn tại'}), 404
|
| 960 |
+
db.messages.delete_many({'conversation_id': conversation_id})
|
| 961 |
+
return jsonify({'message': 'Xóa hội thoại thành công'}), 200
|
| 962 |
+
|
| 963 |
+
# Admin routes
|
| 964 |
+
@app.route('/admin/dashboard')
|
| 965 |
+
@admin_required
|
| 966 |
+
def admin_dashboard():
|
| 967 |
+
users = db.users.find()
|
| 968 |
+
user_name = session.get('username')
|
| 969 |
+
users_list = [{
|
| 970 |
+
'id': str(user['_id']),
|
| 971 |
+
'username': user['username'],
|
| 972 |
+
'email': user['email'],
|
| 973 |
+
'phone': user['phone'],
|
| 974 |
+
'is_active': user.get('is_active', False),
|
| 975 |
+
'is_admin': user.get('is_admin', False),
|
| 976 |
+
'account_type': user.get('account_type', 'limited'),
|
| 977 |
+
'query_limit': user.get('query_limit', None),
|
| 978 |
+
'query_count': user.get('query_count', 0),
|
| 979 |
+
'last_reset': user.get('last_reset', None).isoformat() if user.get('last_reset') else None
|
| 980 |
+
} for user in users]
|
| 981 |
+
return render_template('admin_dashboard.html', users=users_list, user_name = user_name)
|
| 982 |
+
|
| 983 |
+
@app.route('/admin/users', methods=['GET'])
|
| 984 |
+
@admin_required
|
| 985 |
+
def get_all_users():
|
| 986 |
+
users = db.users.find()
|
| 987 |
+
return jsonify([{
|
| 988 |
+
'id': str(user['_id']),
|
| 989 |
+
'username': user['username'],
|
| 990 |
+
'email': user['email'],
|
| 991 |
+
'phone': user['phone'],
|
| 992 |
+
'is_active': user.get('is_active', False),
|
| 993 |
+
'is_admin': user.get('is_admin', False),
|
| 994 |
+
'account_type': user.get('account_type', 'limited'),
|
| 995 |
+
'query_limit': user.get('query_limit', None),
|
| 996 |
+
'query_count': user.get('query_count', 0),
|
| 997 |
+
'last_reset': user.get('last_reset', None).isoformat() if user.get('last_reset') else None
|
| 998 |
+
} for user in users]), 200
|
| 999 |
+
|
| 1000 |
+
@app.route('/admin/user/<user_id>', methods=['PUT'])
|
| 1001 |
+
@admin_required
|
| 1002 |
+
def update_user(user_id):
|
| 1003 |
+
data = request.get_json(silent=True) or {}
|
| 1004 |
+
updates = {}
|
| 1005 |
+
if 'account_type' in data and data['account_type'] in ['limited', 'unlimited']:
|
| 1006 |
+
updates['account_type'] = data['account_type']
|
| 1007 |
+
updates['query_limit'] = 10 if data['account_type'] == 'limited' else None
|
| 1008 |
+
updates['query_count'] = 0
|
| 1009 |
+
updates['last_reset'] = datetime.utcnow()
|
| 1010 |
+
if 'is_admin' in data and isinstance(data['is_admin'], bool):
|
| 1011 |
+
updates['is_admin'] = data['is_admin']
|
| 1012 |
+
if 'query_limit' in data and isinstance(data['query_limit'], int) and data.get('account_type') == 'limited':
|
| 1013 |
+
updates['query_limit'] = data['query_limit']
|
| 1014 |
+
if not updates:
|
| 1015 |
+
return jsonify({'error': 'Không có thông tin cập nhật hợp lệ'}), 400
|
| 1016 |
+
result = db.users.update_one(
|
| 1017 |
+
{'_id': ObjectId(user_id)},
|
| 1018 |
+
{'$set': updates}
|
| 1019 |
+
)
|
| 1020 |
+
if result.modified_count == 0:
|
| 1021 |
+
return jsonify({'error': 'Không tìm thấy người dùng hoặc không có thay đổi'}), 404
|
| 1022 |
+
logging.info(f"Admin updated user {user_id}: {updates}")
|
| 1023 |
+
# Broadcast updated query count to the user
|
| 1024 |
+
if user_id in connected_clients:
|
| 1025 |
+
user = db.users.find_one({'_id': ObjectId(user_id)})
|
| 1026 |
+
socketio.emit('query_update', {
|
| 1027 |
+
'query_count': user.get('query_count', 0),
|
| 1028 |
+
'query_limit': user.get('query_limit', 10)
|
| 1029 |
+
}, room=connected_clients[user_id])
|
| 1030 |
+
logging.info(f"Broadcasted query update to user {user_id} after admin update")
|
| 1031 |
+
return jsonify({'message': 'Cập nhật người dùng thành công'}), 200
|
| 1032 |
+
|
| 1033 |
+
@app.route('/admin/user/<user_id>/reset_query', methods=['POST'])
|
| 1034 |
+
@admin_required
|
| 1035 |
+
def reset_user_query_count(user_id):
|
| 1036 |
+
user = db.users.find_one({'_id': ObjectId(user_id)})
|
| 1037 |
+
if not user:
|
| 1038 |
+
return jsonify({'error': 'Người dùng không tồn tại'}), 404
|
| 1039 |
+
if user.get('account_type') == 'unlimited':
|
| 1040 |
+
return jsonify({'error': 'Tài khoản không giới hạn không cần reset'}), 400
|
| 1041 |
+
db.users.update_one(
|
| 1042 |
+
{'_id': ObjectId(user_id)},
|
| 1043 |
+
{'$set': {'query_count': 0, 'last_reset': datetime.utcnow()}}
|
| 1044 |
+
)
|
| 1045 |
+
logging.info(f"Admin reset query count for user {user_id}")
|
| 1046 |
+
# Broadcast updated query count to the user
|
| 1047 |
+
if user_id in connected_clients:
|
| 1048 |
+
socketio.emit('query_update', {
|
| 1049 |
+
'query_count': 0,
|
| 1050 |
+
'query_limit': user.get('query_limit', 10)
|
| 1051 |
+
}, room=connected_clients[user_id])
|
| 1052 |
+
logging.info(f"Broadcasted query update to user {user_id} after reset")
|
| 1053 |
+
return jsonify({'message': 'Reset lượt hỏi đáp thành công'}), 200
|
| 1054 |
+
|
| 1055 |
+
# Page routes
|
| 1056 |
+
@app.route('/')
|
| 1057 |
+
def page_index():
|
| 1058 |
+
return render_template('index.html')
|
| 1059 |
+
|
| 1060 |
+
@app.route('/home')
|
| 1061 |
+
def page_home():
|
| 1062 |
+
return render_template('home.html')
|
| 1063 |
+
|
| 1064 |
+
@app.route('/login')
|
| 1065 |
+
def login_page():
|
| 1066 |
+
return render_template('login.html')
|
| 1067 |
+
|
| 1068 |
+
if __name__ == '__main__':
|
| 1069 |
+
socketio.run(app, debug=True) # Use socketio.run instead of app.run
|
config.yaml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gemini:
|
| 2 |
+
# Required: API Keys
|
| 3 |
+
api_keys:
|
| 4 |
+
- "AIzaSyBZy7wnuLbRpngTyuplRz1FNjpP8uxttVw"
|
| 5 |
+
- "AIzaSyCvlZ63Nkt5NpjdmxYPAsG8Qskex6usCFw"
|
| 6 |
+
- "AIzaSyB4BlhbVDHupKtExi59btmX5Y5Nkm0eN7g"
|
| 7 |
+
- "AIzaSyA2RKWDRHuSVm8X5ez30-5NWbF0F4QdJGo"
|
| 8 |
+
- "AIzaSyCya9OpC1EgO_j3ARee7OrBPJqjlj4_xso"
|
| 9 |
+
|
| 10 |
+
# Optional: Generation Settings
|
| 11 |
+
generation:
|
| 12 |
+
temperature: 0.5
|
| 13 |
+
top_p: 1.0
|
| 14 |
+
top_k: 40
|
| 15 |
+
max_output_tokens: 8192
|
| 16 |
+
stop_sequences: []
|
| 17 |
+
response_mime_type: "text/plain"
|
| 18 |
+
|
| 19 |
+
# Optional: Rate Limiting
|
| 20 |
+
rate_limits:
|
| 21 |
+
requests_per_minute: 60
|
| 22 |
+
reset_window: 60 # seconds
|
| 23 |
+
|
| 24 |
+
# Optional: Strategies
|
| 25 |
+
strategies:
|
| 26 |
+
content: "fallback"
|
| 27 |
+
key_rotation: "smart_cooldown"
|
| 28 |
+
|
| 29 |
+
# Optional: Retry Settings
|
| 30 |
+
retry:
|
| 31 |
+
max_attempts: 3
|
| 32 |
+
delay: 15 # seconds
|
| 33 |
+
|
| 34 |
+
# Optional: Model Settings
|
| 35 |
+
default_model: "gemma-3n-e2b-it"
|
| 36 |
+
system_instruction: null
|
embedding_data/embeddings.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:57f4d29d1a9da5eecb05c3052c986d35f7a67609c9166fca5fe741e4fc729069
|
| 3 |
+
size 110171889
|
embedding_data/faiss_index_23_06.index
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:adee95b3eef649e0832368c752b999e99efb4fefb68bbadac328cb960b2175d8
|
| 3 |
+
size 13793325
|
gemini_handler.py
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from abc import ABC, abstractmethod
|
| 2 |
+
import google.generativeai as genai
|
| 3 |
+
import time
|
| 4 |
+
import os
|
| 5 |
+
import yaml
|
| 6 |
+
from typing import List, Dict, Any, Optional, Tuple, Union
|
| 7 |
+
from enum import Enum
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from itertools import cycle
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
@dataclass
|
| 13 |
+
class GenerationConfig:
|
| 14 |
+
"""Configuration for model generation parameters."""
|
| 15 |
+
temperature: float = 1.0
|
| 16 |
+
top_p: float = 1.0
|
| 17 |
+
top_k: int = 40
|
| 18 |
+
max_output_tokens: int = 8192
|
| 19 |
+
stop_sequences: Optional[List[str]] = None
|
| 20 |
+
response_mime_type: str = "text/plain"
|
| 21 |
+
|
| 22 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 23 |
+
"""Convert config to dictionary, excluding None values."""
|
| 24 |
+
return {k: v for k, v in self.__dict__.items() if v is not None}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass
|
| 28 |
+
class ModelResponse:
|
| 29 |
+
"""Represents a standardized response from any model."""
|
| 30 |
+
success: bool
|
| 31 |
+
model: str
|
| 32 |
+
text: str = ""
|
| 33 |
+
error: str = ""
|
| 34 |
+
time: float = 0.0
|
| 35 |
+
attempts: int = 1
|
| 36 |
+
api_key_index: int = 0
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class Strategy(Enum):
|
| 40 |
+
"""Available content generation strategies."""
|
| 41 |
+
ROUND_ROBIN = "round_robin"
|
| 42 |
+
FALLBACK = "fallback"
|
| 43 |
+
RETRY = "retry"
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class KeyRotationStrategy(Enum):
|
| 47 |
+
"""Available key rotation strategies."""
|
| 48 |
+
SEQUENTIAL = "sequential"
|
| 49 |
+
ROUND_ROBIN = "round_robin"
|
| 50 |
+
LEAST_USED = "least_used"
|
| 51 |
+
SMART_COOLDOWN = "smart_cooldown"
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@dataclass
|
| 55 |
+
class KeyStats:
|
| 56 |
+
"""Track usage statistics for each API key."""
|
| 57 |
+
uses: int = 0
|
| 58 |
+
last_used: float = 0
|
| 59 |
+
failures: int = 0
|
| 60 |
+
rate_limited_until: float = 0
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class ConfigLoader:
|
| 64 |
+
"""Handles loading configuration from various sources."""
|
| 65 |
+
|
| 66 |
+
@staticmethod
|
| 67 |
+
def load_api_keys(config_path: Optional[Union[str, Path]] = None) -> List[str]:
|
| 68 |
+
"""
|
| 69 |
+
Load API keys from multiple sources in priority order:
|
| 70 |
+
1. YAML config file if provided
|
| 71 |
+
2. Environment variables (GEMINI_API_KEYS as comma-separated string)
|
| 72 |
+
3. Single GEMINI_API_KEY environment variable
|
| 73 |
+
"""
|
| 74 |
+
# Try loading from YAML config
|
| 75 |
+
if config_path:
|
| 76 |
+
try:
|
| 77 |
+
with open(config_path, 'r') as f:
|
| 78 |
+
config = yaml.safe_load(f)
|
| 79 |
+
if config and 'gemini' in config and 'api_keys' in config['gemini']:
|
| 80 |
+
keys = config['gemini']['api_keys']
|
| 81 |
+
if isinstance(keys, list) and all(isinstance(k, str) for k in keys):
|
| 82 |
+
return keys
|
| 83 |
+
except Exception as e:
|
| 84 |
+
print(f"Warning: Failed to load config from {config_path}: {e}")
|
| 85 |
+
|
| 86 |
+
# Try loading from GEMINI_API_KEYS environment variable
|
| 87 |
+
api_keys_str = os.getenv('GEMINI_API_KEYS')
|
| 88 |
+
if api_keys_str:
|
| 89 |
+
keys = [k.strip() for k in api_keys_str.split(',') if k.strip()]
|
| 90 |
+
if keys:
|
| 91 |
+
return keys
|
| 92 |
+
|
| 93 |
+
# Try loading single API key
|
| 94 |
+
single_key = os.getenv('GEMINI_API_KEY')
|
| 95 |
+
if single_key:
|
| 96 |
+
return [single_key]
|
| 97 |
+
|
| 98 |
+
raise ValueError(
|
| 99 |
+
"No API keys found. Please provide keys via config file, "
|
| 100 |
+
"GEMINI_API_KEYS environment variable (comma-separated), "
|
| 101 |
+
"or GEMINI_API_KEY environment variable."
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class ModelConfig:
|
| 106 |
+
"""Configuration for model settings."""
|
| 107 |
+
def __init__(self):
|
| 108 |
+
self.models = [
|
| 109 |
+
"gemini-2.0-flash-exp",
|
| 110 |
+
"gemini-1.5-pro",
|
| 111 |
+
"learnlm-1.5-pro-experimental",
|
| 112 |
+
"gemini-exp-1206",
|
| 113 |
+
"gemini-exp-1121",
|
| 114 |
+
"gemini-exp-1114",
|
| 115 |
+
"gemini-2.0-flash-thinking-exp-1219",
|
| 116 |
+
"gemini-1.5-flash"
|
| 117 |
+
]
|
| 118 |
+
self.max_retries = 3
|
| 119 |
+
self.retry_delay = 30
|
| 120 |
+
self.default_model = "gemini-2.0-flash-exp"
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
class KeyRotationManager:
|
| 124 |
+
"""Enhanced key rotation manager with multiple strategies."""
|
| 125 |
+
def __init__(
|
| 126 |
+
self,
|
| 127 |
+
api_keys: List[str],
|
| 128 |
+
strategy: KeyRotationStrategy = KeyRotationStrategy.ROUND_ROBIN,
|
| 129 |
+
rate_limit: int = 60,
|
| 130 |
+
reset_window: int = 60
|
| 131 |
+
):
|
| 132 |
+
if not api_keys:
|
| 133 |
+
raise ValueError("At least one API key must be provided")
|
| 134 |
+
|
| 135 |
+
self.api_keys = api_keys
|
| 136 |
+
self.strategy = strategy
|
| 137 |
+
self.rate_limit = rate_limit
|
| 138 |
+
self.reset_window = reset_window
|
| 139 |
+
|
| 140 |
+
# Initialize tracking
|
| 141 |
+
self.key_stats = {i: KeyStats() for i in range(len(api_keys))}
|
| 142 |
+
self._key_cycle = cycle(range(len(api_keys)))
|
| 143 |
+
self.current_index = 0
|
| 144 |
+
|
| 145 |
+
def _is_key_available(self, key_index: int) -> bool:
|
| 146 |
+
"""Check if a key is available based on rate limits and cooldown."""
|
| 147 |
+
stats = self.key_stats[key_index]
|
| 148 |
+
current_time = time.time()
|
| 149 |
+
|
| 150 |
+
if current_time < stats.rate_limited_until:
|
| 151 |
+
return False
|
| 152 |
+
|
| 153 |
+
if current_time - stats.last_used > self.reset_window:
|
| 154 |
+
stats.uses = 0
|
| 155 |
+
|
| 156 |
+
return stats.uses < self.rate_limit
|
| 157 |
+
|
| 158 |
+
def _get_sequential_key(self) -> Tuple[str, int]:
|
| 159 |
+
"""Get next key using sequential strategy."""
|
| 160 |
+
start_index = self.current_index
|
| 161 |
+
|
| 162 |
+
while True:
|
| 163 |
+
if self._is_key_available(self.current_index):
|
| 164 |
+
key_index = self.current_index
|
| 165 |
+
self.current_index = (self.current_index + 1) % len(self.api_keys)
|
| 166 |
+
return self.api_keys[key_index], key_index
|
| 167 |
+
|
| 168 |
+
self.current_index = (self.current_index + 1) % len(self.api_keys)
|
| 169 |
+
if self.current_index == start_index:
|
| 170 |
+
self._handle_all_keys_busy()
|
| 171 |
+
|
| 172 |
+
def _get_round_robin_key(self) -> Tuple[str, int]:
|
| 173 |
+
"""Get next key using round-robin strategy."""
|
| 174 |
+
start_index = next(self._key_cycle)
|
| 175 |
+
current_index = start_index
|
| 176 |
+
|
| 177 |
+
while True:
|
| 178 |
+
if self._is_key_available(current_index):
|
| 179 |
+
return self.api_keys[current_index], current_index
|
| 180 |
+
|
| 181 |
+
current_index = next(self._key_cycle)
|
| 182 |
+
if current_index == start_index:
|
| 183 |
+
self._handle_all_keys_busy()
|
| 184 |
+
|
| 185 |
+
def _get_least_used_key(self) -> Tuple[str, int]:
|
| 186 |
+
"""Get key with lowest usage count."""
|
| 187 |
+
while True:
|
| 188 |
+
available_keys = [
|
| 189 |
+
(idx, stats) for idx, stats in self.key_stats.items()
|
| 190 |
+
if self._is_key_available(idx)
|
| 191 |
+
]
|
| 192 |
+
|
| 193 |
+
if available_keys:
|
| 194 |
+
key_index, _ = min(available_keys, key=lambda x: x[1].uses)
|
| 195 |
+
return self.api_keys[key_index], key_index
|
| 196 |
+
|
| 197 |
+
self._handle_all_keys_busy()
|
| 198 |
+
|
| 199 |
+
def _get_smart_cooldown_key(self) -> Tuple[str, int]:
|
| 200 |
+
"""Get key using smart cooldown strategy."""
|
| 201 |
+
while True:
|
| 202 |
+
current_time = time.time()
|
| 203 |
+
available_keys = [
|
| 204 |
+
(idx, stats) for idx, stats in self.key_stats.items()
|
| 205 |
+
if current_time >= stats.rate_limited_until and self._is_key_available(idx)
|
| 206 |
+
]
|
| 207 |
+
|
| 208 |
+
if available_keys:
|
| 209 |
+
key_index, _ = min(
|
| 210 |
+
available_keys,
|
| 211 |
+
key=lambda x: (x[1].failures, -(current_time - x[1].last_used))
|
| 212 |
+
)
|
| 213 |
+
return self.api_keys[key_index], key_index
|
| 214 |
+
|
| 215 |
+
self._handle_all_keys_busy()
|
| 216 |
+
|
| 217 |
+
def _handle_all_keys_busy(self) -> None:
|
| 218 |
+
"""Handle situation when all keys are busy."""
|
| 219 |
+
current_time = time.time()
|
| 220 |
+
any_reset = False
|
| 221 |
+
|
| 222 |
+
for idx, stats in self.key_stats.items():
|
| 223 |
+
if current_time - stats.last_used > self.reset_window:
|
| 224 |
+
stats.uses = 0
|
| 225 |
+
any_reset = True
|
| 226 |
+
|
| 227 |
+
if not any_reset:
|
| 228 |
+
time.sleep(1)
|
| 229 |
+
|
| 230 |
+
def get_next_key(self) -> Tuple[str, int]:
|
| 231 |
+
"""Get next available API key based on selected strategy."""
|
| 232 |
+
strategy_methods = {
|
| 233 |
+
KeyRotationStrategy.SEQUENTIAL: self._get_sequential_key,
|
| 234 |
+
KeyRotationStrategy.ROUND_ROBIN: self._get_round_robin_key,
|
| 235 |
+
KeyRotationStrategy.LEAST_USED: self._get_least_used_key,
|
| 236 |
+
KeyRotationStrategy.SMART_COOLDOWN: self._get_smart_cooldown_key
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
method = strategy_methods.get(self.strategy)
|
| 240 |
+
if not method:
|
| 241 |
+
raise ValueError(f"Unknown strategy: {self.strategy}")
|
| 242 |
+
|
| 243 |
+
api_key, key_index = method()
|
| 244 |
+
|
| 245 |
+
stats = self.key_stats[key_index]
|
| 246 |
+
stats.uses += 1
|
| 247 |
+
stats.last_used = time.time()
|
| 248 |
+
|
| 249 |
+
return api_key, key_index
|
| 250 |
+
|
| 251 |
+
def mark_success(self, key_index: int) -> None:
|
| 252 |
+
"""Mark successful API call."""
|
| 253 |
+
if 0 <= key_index < len(self.api_keys):
|
| 254 |
+
self.key_stats[key_index].failures = 0
|
| 255 |
+
|
| 256 |
+
def mark_rate_limited(self, key_index: int) -> None:
|
| 257 |
+
"""Mark API key as rate limited."""
|
| 258 |
+
if 0 <= key_index < len(self.api_keys):
|
| 259 |
+
stats = self.key_stats[key_index]
|
| 260 |
+
stats.failures += 1
|
| 261 |
+
stats.rate_limited_until = time.time() + self.reset_window
|
| 262 |
+
stats.uses = self.rate_limit
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
class ResponseHandler:
|
| 266 |
+
"""Handles and processes model responses."""
|
| 267 |
+
@staticmethod
|
| 268 |
+
def process_response(
|
| 269 |
+
response: Any,
|
| 270 |
+
model_name: str,
|
| 271 |
+
start_time: float,
|
| 272 |
+
key_index: int
|
| 273 |
+
) -> ModelResponse:
|
| 274 |
+
"""Process and validate model response."""
|
| 275 |
+
try:
|
| 276 |
+
if hasattr(response, 'candidates') and response.candidates:
|
| 277 |
+
finish_reason = response.candidates[0].finish_reason
|
| 278 |
+
if finish_reason == 4: # Copyright material
|
| 279 |
+
return ModelResponse(
|
| 280 |
+
success=False,
|
| 281 |
+
model=model_name,
|
| 282 |
+
error='Copyright material detected in response',
|
| 283 |
+
time=time.time() - start_time,
|
| 284 |
+
api_key_index=key_index
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
return ModelResponse(
|
| 288 |
+
success=True,
|
| 289 |
+
model=model_name,
|
| 290 |
+
text=response.text,
|
| 291 |
+
time=time.time() - start_time,
|
| 292 |
+
api_key_index=key_index
|
| 293 |
+
)
|
| 294 |
+
except Exception as e:
|
| 295 |
+
if "The `response.text` quick accessor requires the response to contain a valid `Part`" in str(e):
|
| 296 |
+
return ModelResponse(
|
| 297 |
+
success=False,
|
| 298 |
+
model=model_name,
|
| 299 |
+
error='No valid response parts available',
|
| 300 |
+
time=time.time() - start_time,
|
| 301 |
+
api_key_index=key_index
|
| 302 |
+
)
|
| 303 |
+
raise
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
class ContentStrategy(ABC):
|
| 307 |
+
"""Abstract base class for content generation strategies."""
|
| 308 |
+
def __init__(
|
| 309 |
+
self,
|
| 310 |
+
config: ModelConfig,
|
| 311 |
+
key_manager: KeyRotationManager,
|
| 312 |
+
system_instruction: Optional[str] = None,
|
| 313 |
+
generation_config: Optional[GenerationConfig] = None
|
| 314 |
+
):
|
| 315 |
+
self.config = config
|
| 316 |
+
self.key_manager = key_manager
|
| 317 |
+
self.system_instruction = system_instruction
|
| 318 |
+
self.generation_config = generation_config or GenerationConfig()
|
| 319 |
+
|
| 320 |
+
@abstractmethod
|
| 321 |
+
def generate(self, prompt: str, model_name: str) -> ModelResponse:
|
| 322 |
+
"""Generate content using the specific strategy."""
|
| 323 |
+
pass
|
| 324 |
+
|
| 325 |
+
def _try_generate(self, model_name: str, prompt: str, start_time: float) -> ModelResponse:
|
| 326 |
+
"""Helper method for generating content with key rotation."""
|
| 327 |
+
api_key, key_index = self.key_manager.get_next_key()
|
| 328 |
+
try:
|
| 329 |
+
genai.configure(api_key=api_key)
|
| 330 |
+
model = genai.GenerativeModel(
|
| 331 |
+
model_name=model_name,
|
| 332 |
+
generation_config=self.generation_config.to_dict(),
|
| 333 |
+
system_instruction=self.system_instruction
|
| 334 |
+
)
|
| 335 |
+
response = model.generate_content(prompt)
|
| 336 |
+
|
| 337 |
+
result = ResponseHandler.process_response(response, model_name, start_time, key_index)
|
| 338 |
+
if result.success:
|
| 339 |
+
self.key_manager.mark_success(key_index)
|
| 340 |
+
return result
|
| 341 |
+
|
| 342 |
+
except Exception as e:
|
| 343 |
+
if "429" in str(e):
|
| 344 |
+
self.key_manager.mark_rate_limited(key_index)
|
| 345 |
+
return ModelResponse(
|
| 346 |
+
success=False,
|
| 347 |
+
model=model_name,
|
| 348 |
+
error=str(e),
|
| 349 |
+
time=time.time() - start_time,
|
| 350 |
+
api_key_index=key_index
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
class RoundRobinStrategy(ContentStrategy):
|
| 355 |
+
"""Round robin implementation of content generation."""
|
| 356 |
+
def __init__(self, *args, **kwargs):
|
| 357 |
+
super().__init__(*args, **kwargs)
|
| 358 |
+
self._current_index = 0
|
| 359 |
+
|
| 360 |
+
def _get_next_model(self) -> str:
|
| 361 |
+
"""Get next model in round-robin fashion."""
|
| 362 |
+
model = self.config.models[self._current_index]
|
| 363 |
+
self._current_index = (self._current_index + 1) % len(self.config.models)
|
| 364 |
+
return model
|
| 365 |
+
|
| 366 |
+
def generate(self, prompt: str, _: str) -> ModelResponse:
|
| 367 |
+
start_time = time.time()
|
| 368 |
+
|
| 369 |
+
for _ in range(len(self.config.models)):
|
| 370 |
+
model_name = self._get_next_model()
|
| 371 |
+
result = self._try_generate(model_name, prompt, start_time)
|
| 372 |
+
if result.success or 'Copyright' in result.error:
|
| 373 |
+
return result
|
| 374 |
+
|
| 375 |
+
return ModelResponse(
|
| 376 |
+
success=False,
|
| 377 |
+
model='all_models_failed',
|
| 378 |
+
error='All models failed (rate limited or copyright issues)',
|
| 379 |
+
time=time.time() - start_time
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
class FallbackStrategy(ContentStrategy):
|
| 384 |
+
"""Fallback implementation of content generation."""
|
| 385 |
+
def generate(self, prompt: str, start_model: str) -> ModelResponse:
|
| 386 |
+
start_time = time.time()
|
| 387 |
+
|
| 388 |
+
try:
|
| 389 |
+
start_index = self.config.models.index(start_model)
|
| 390 |
+
except ValueError:
|
| 391 |
+
return ModelResponse(
|
| 392 |
+
success=False,
|
| 393 |
+
model=start_model,
|
| 394 |
+
error=f"Model {start_model} not found in available models",
|
| 395 |
+
time=time.time() - start_time
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
for model_name in self.config.models[start_index:]:
|
| 399 |
+
result = self._try_generate(model_name, prompt, start_time)
|
| 400 |
+
if result.success or 'Copyright' in result.error:
|
| 401 |
+
return result
|
| 402 |
+
|
| 403 |
+
return ModelResponse(
|
| 404 |
+
success=False,
|
| 405 |
+
model='all_models_failed',
|
| 406 |
+
error='All models failed (rate limited or copyright issues)',
|
| 407 |
+
time=time.time() - start_time
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
class RetryStrategy(ContentStrategy):
|
| 412 |
+
"""Retry implementation of content generation."""
|
| 413 |
+
def generate(self, prompt: str, model_name: str) -> ModelResponse:
|
| 414 |
+
start_time = time.time()
|
| 415 |
+
|
| 416 |
+
for attempt in range(self.config.max_retries):
|
| 417 |
+
result = self._try_generate(model_name, prompt, start_time)
|
| 418 |
+
result.attempts = attempt + 1
|
| 419 |
+
|
| 420 |
+
if result.success or 'Copyright' in result.error:
|
| 421 |
+
return result
|
| 422 |
+
|
| 423 |
+
if attempt < self.config.max_retries - 1:
|
| 424 |
+
print(f"Error encountered. Waiting {self.config.retry_delay}s... "
|
| 425 |
+
f"(Attempt {attempt + 1}/{self.config.max_retries})")
|
| 426 |
+
time.sleep(self.config.retry_delay)
|
| 427 |
+
|
| 428 |
+
return ModelResponse(
|
| 429 |
+
success=False,
|
| 430 |
+
model=model_name,
|
| 431 |
+
error='Max retries exceeded',
|
| 432 |
+
time=time.time() - start_time,
|
| 433 |
+
attempts=self.config.max_retries
|
| 434 |
+
)
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
class GeminiHandler:
|
| 438 |
+
"""Main handler class for Gemini API interactions."""
|
| 439 |
+
def __init__(
|
| 440 |
+
self,
|
| 441 |
+
api_keys: Optional[List[str]] = None,
|
| 442 |
+
config_path: Optional[Union[str, Path]] = None,
|
| 443 |
+
content_strategy: Strategy = Strategy.ROUND_ROBIN,
|
| 444 |
+
key_strategy: KeyRotationStrategy = KeyRotationStrategy.ROUND_ROBIN,
|
| 445 |
+
system_instruction: Optional[str] = None,
|
| 446 |
+
generation_config: Optional[GenerationConfig] = None
|
| 447 |
+
):
|
| 448 |
+
"""
|
| 449 |
+
Initialize GeminiHandler with flexible configuration options.
|
| 450 |
+
|
| 451 |
+
Args:
|
| 452 |
+
api_keys: Optional list of API keys
|
| 453 |
+
config_path: Optional path to YAML config file
|
| 454 |
+
content_strategy: Strategy for content generation
|
| 455 |
+
key_strategy: Strategy for key rotation
|
| 456 |
+
system_instruction: Optional system instruction
|
| 457 |
+
generation_config: Optional generation configuration
|
| 458 |
+
"""
|
| 459 |
+
# Load API keys from provided list or config sources
|
| 460 |
+
self.api_keys = api_keys or ConfigLoader.load_api_keys(config_path)
|
| 461 |
+
|
| 462 |
+
self.config = ModelConfig()
|
| 463 |
+
self.key_manager = KeyRotationManager(
|
| 464 |
+
api_keys=self.api_keys,
|
| 465 |
+
strategy=key_strategy,
|
| 466 |
+
rate_limit=60,
|
| 467 |
+
reset_window=60
|
| 468 |
+
)
|
| 469 |
+
self.system_instruction = system_instruction
|
| 470 |
+
self.generation_config = generation_config
|
| 471 |
+
self._strategy = self._create_strategy(content_strategy)
|
| 472 |
+
|
| 473 |
+
def _create_strategy(self, strategy: Strategy) -> ContentStrategy:
|
| 474 |
+
"""Factory method to create appropriate strategy."""
|
| 475 |
+
strategies = {
|
| 476 |
+
Strategy.ROUND_ROBIN: RoundRobinStrategy,
|
| 477 |
+
Strategy.FALLBACK: FallbackStrategy,
|
| 478 |
+
Strategy.RETRY: RetryStrategy
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
strategy_class = strategies.get(strategy)
|
| 482 |
+
if not strategy_class:
|
| 483 |
+
raise ValueError(f"Unknown strategy: {strategy}")
|
| 484 |
+
|
| 485 |
+
return strategy_class(
|
| 486 |
+
config=self.config,
|
| 487 |
+
key_manager=self.key_manager,
|
| 488 |
+
system_instruction=self.system_instruction,
|
| 489 |
+
generation_config=self.generation_config
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
def generate_content(
|
| 493 |
+
self,
|
| 494 |
+
prompt: str,
|
| 495 |
+
model_name: Optional[str] = None,
|
| 496 |
+
return_stats: bool = False
|
| 497 |
+
) -> Dict[str, Any]:
|
| 498 |
+
"""
|
| 499 |
+
Generate content using the selected strategies.
|
| 500 |
+
|
| 501 |
+
Args:
|
| 502 |
+
prompt: The input prompt for content generation
|
| 503 |
+
model_name: Optional specific model to use (default: None)
|
| 504 |
+
return_stats: Whether to include key usage statistics (default: False)
|
| 505 |
+
|
| 506 |
+
Returns:
|
| 507 |
+
Dictionary containing generation results and optionally key statistics
|
| 508 |
+
"""
|
| 509 |
+
if not model_name:
|
| 510 |
+
model_name = self.config.default_model
|
| 511 |
+
|
| 512 |
+
response = self._strategy.generate(prompt, model_name)
|
| 513 |
+
result = response.__dict__
|
| 514 |
+
|
| 515 |
+
if return_stats:
|
| 516 |
+
result["key_stats"] = {
|
| 517 |
+
idx: {
|
| 518 |
+
"uses": stats.uses,
|
| 519 |
+
"last_used": stats.last_used,
|
| 520 |
+
"failures": stats.failures,
|
| 521 |
+
"rate_limited_until": stats.rate_limited_until
|
| 522 |
+
}
|
| 523 |
+
for idx, stats in self.key_manager.key_stats.items()
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
return result
|
| 527 |
+
|
| 528 |
+
def get_key_stats(self, key_index: Optional[int] = None) -> Dict[int, Dict[str, Any]]:
|
| 529 |
+
"""
|
| 530 |
+
Get current key usage statistics.
|
| 531 |
+
|
| 532 |
+
Args:
|
| 533 |
+
key_index: Optional specific key index to get stats for
|
| 534 |
+
|
| 535 |
+
Returns:
|
| 536 |
+
Dictionary of key statistics
|
| 537 |
+
"""
|
| 538 |
+
if key_index is not None:
|
| 539 |
+
if 0 <= key_index < len(self.key_manager.api_keys):
|
| 540 |
+
stats = self.key_manager.key_stats[key_index]
|
| 541 |
+
return {
|
| 542 |
+
key_index: {
|
| 543 |
+
"uses": stats.uses,
|
| 544 |
+
"last_used": stats.last_used,
|
| 545 |
+
"failures": stats.failures,
|
| 546 |
+
"rate_limited_until": stats.rate_limited_until
|
| 547 |
+
}
|
| 548 |
+
}
|
| 549 |
+
raise ValueError(f"Invalid key index: {key_index}")
|
| 550 |
+
|
| 551 |
+
return {
|
| 552 |
+
idx: {
|
| 553 |
+
"uses": stats.uses,
|
| 554 |
+
"last_used": stats.last_used,
|
| 555 |
+
"failures": stats.failures,
|
| 556 |
+
"rate_limited_until": stats.rate_limited_until
|
| 557 |
+
}
|
| 558 |
+
for idx, stats in self.key_manager.key_stats.items()
|
| 559 |
+
}
|
readme.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
lt --port 5000 --subdomain legal
|
requirements.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask
|
| 2 |
+
pymongo
|
| 3 |
+
google-generativeai
|
| 4 |
+
pyyaml
|
| 5 |
+
sentence-transformers
|
| 6 |
+
torch
|
| 7 |
+
numpy
|
| 8 |
+
boto3
|
| 9 |
+
Flask-SocketIO
|
| 10 |
+
pymongo
|
| 11 |
+
python-dateutil==2.9.0.post0
|
| 12 |
+
sentence-transformers==3.1.0
|
| 13 |
+
torch==2.4.0
|
| 14 |
+
numpy==1.26.4
|
| 15 |
+
faiss-cpu==1.8.0
|
| 16 |
+
langchain==0.2.15
|
| 17 |
+
langchain-community==0.2.15
|
| 18 |
+
PyYAML==6.0.1
|
| 19 |
+
requests==2.32.3
|
| 20 |
+
gunicorn==22.0.0
|
| 21 |
+
eventlet==0.36.0
|
static/assets/01-dark.png
ADDED
|
static/assets/01-light.png
ADDED
|
Git LFS Details
|
static/assets/01.jpg
ADDED
|
static/assets/02-dark.png
ADDED
|
static/assets/02-light.png
ADDED
|
static/assets/02.jpg
ADDED
|
static/assets/03-dark.png
ADDED
|
static/assets/03-light.png
ADDED
|
static/assets/48.jpg
ADDED
|
static/assets/49.jpg
ADDED
|
static/assets/50.jpg
ADDED
|
static/assets/awwwards.png
ADDED
|
static/assets/boxicons.min.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@font-face{font-family:boxicons;font-weight:400;font-style:normal;src:url(../fonts/boxicons.eot);src:url(../fonts/boxicons.eot) format('embedded-opentype'),url(../fonts/boxicons.woff2) format('woff2'),url(../fonts/boxicons.woff) format('woff'),url(../fonts/boxicons.ttf) format('truetype'),url(../fonts/boxicons.svg?#boxicons) format('svg')}.bx{font-family:boxicons!important;font-weight:400;font-style:normal;font-variant:normal;line-height:1;text-rendering:auto;display:inline-block;text-transform:none;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.bx-ul{margin-left:2em;padding-left:0;list-style:none}.bx-ul>li{position:relative}.bx-ul .bx{font-size:inherit;line-height:inherit;position:absolute;left:-2em;width:2em;text-align:center}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@-webkit-keyframes burst{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}90%{-webkit-transform:scale(1.5);transform:scale(1.5);opacity:0}}@keyframes burst{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}90%{-webkit-transform:scale(1.5);transform:scale(1.5);opacity:0}}@-webkit-keyframes flashing{0%{opacity:1}45%{opacity:0}90%{opacity:1}}@keyframes flashing{0%{opacity:1}45%{opacity:0}90%{opacity:1}}@-webkit-keyframes fade-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}75%{-webkit-transform:translateX(-20px);transform:translateX(-20px);opacity:0}}@keyframes fade-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}75%{-webkit-transform:translateX(-20px);transform:translateX(-20px);opacity:0}}@-webkit-keyframes fade-right{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}75%{-webkit-transform:translateX(20px);transform:translateX(20px);opacity:0}}@keyframes fade-right{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}75%{-webkit-transform:translateX(20px);transform:translateX(20px);opacity:0}}@-webkit-keyframes fade-up{0%{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}75%{-webkit-transform:translateY(-20px);transform:translateY(-20px);opacity:0}}@keyframes fade-up{0%{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}75%{-webkit-transform:translateY(-20px);transform:translateY(-20px);opacity:0}}@-webkit-keyframes fade-down{0%{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}75%{-webkit-transform:translateY(20px);transform:translateY(20px);opacity:0}}@keyframes fade-down{0%{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}75%{-webkit-transform:translateY(20px);transform:translateY(20px);opacity:0}}@-webkit-keyframes tada{from{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}10%,20%{-webkit-transform:scale3d(.95,.95,.95) rotate3d(0,0,1,-10deg);transform:scale3d(.95,.95,.95) rotate3d(0,0,1,-10deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1,1,1) rotate3d(0,0,1,10deg);transform:scale3d(1,1,1) rotate3d(0,0,1,10deg)}40%,60%,80%{-webkit-transform:scale3d(1,1,1) rotate3d(0,0,1,-10deg);transform:scale3d(1,1,1) rotate3d(0,0,1,-10deg)}to{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes tada{from{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}10%,20%{-webkit-transform:scale3d(.95,.95,.95) rotate3d(0,0,1,-10deg);transform:scale3d(.95,.95,.95) rotate3d(0,0,1,-10deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1,1,1) rotate3d(0,0,1,10deg);transform:scale3d(1,1,1) rotate3d(0,0,1,10deg)}40%,60%,80%{-webkit-transform:rotate3d(0,0,1,-10deg);transform:rotate3d(0,0,1,-10deg)}to{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}.bx-spin{-webkit-animation:spin 2s linear infinite;animation:spin 2s linear infinite}.bx-spin-hover:hover{-webkit-animation:spin 2s linear infinite;animation:spin 2s linear infinite}.bx-tada{-webkit-animation:tada 1.5s ease infinite;animation:tada 1.5s ease infinite}.bx-tada-hover:hover{-webkit-animation:tada 1.5s ease infinite;animation:tada 1.5s ease infinite}.bx-flashing{-webkit-animation:flashing 1.5s infinite linear;animation:flashing 1.5s infinite linear}.bx-flashing-hover:hover{-webkit-animation:flashing 1.5s infinite linear;animation:flashing 1.5s infinite linear}.bx-burst{-webkit-animation:burst 1.5s infinite linear;animation:burst 1.5s infinite linear}.bx-burst-hover:hover{-webkit-animation:burst 1.5s infinite linear;animation:burst 1.5s infinite linear}.bx-fade-up{-webkit-animation:fade-up 1.5s infinite linear;animation:fade-up 1.5s infinite linear}.bx-fade-up-hover:hover{-webkit-animation:fade-up 1.5s infinite linear;animation:fade-up 1.5s infinite linear}.bx-fade-down{-webkit-animation:fade-down 1.5s infinite linear;animation:fade-down 1.5s infinite linear}.bx-fade-down-hover:hover{-webkit-animation:fade-down 1.5s infinite linear;animation:fade-down 1.5s infinite linear}.bx-fade-left{-webkit-animation:fade-left 1.5s infinite linear;animation:fade-left 1.5s infinite linear}.bx-fade-left-hover:hover{-webkit-animation:fade-left 1.5s infinite linear;animation:fade-left 1.5s infinite linear}.bx-fade-right{-webkit-animation:fade-right 1.5s infinite linear;animation:fade-right 1.5s infinite linear}.bx-fade-right-hover:hover{-webkit-animation:fade-right 1.5s infinite linear;animation:fade-right 1.5s infinite linear}.bx-xs{font-size:1rem!important}.bx-sm{font-size:1.55rem!important}.bx-md{font-size:2.25rem!important}.bx-lg{font-size:3rem!important}.bx-fw{font-size:1.2857142857em;line-height:.8em;width:1.2857142857em;height:.8em;margin-top:-.2em!important;vertical-align:middle}.bx-pull-left{float:left;margin-right:.3em!important}.bx-pull-right{float:right;margin-left:.3em!important}.bx-rotate-90{transform:rotate(90deg)}.bx-rotate-180{transform:rotate(180deg)}.bx-rotate-270{transform:rotate(270deg)}.bx-flip-horizontal{transform:scaleX(-1)}.bx-flip-vertical{transform:scaleY(-1)}.bx-border{padding:.25em;border:.07em solid rgba(0,0,0,.1);border-radius:.25em}.bx-border-circle{padding:.25em;border:.07em solid rgba(0,0,0,.1);border-radius:50%}.bxs-balloon:before{content:"\eb60"}.bxs-castle:before{content:"\eb79"}.bxs-coffee-bean:before{content:"\eb92"}.bxs-objects-horizontal-center:before{content:"\ebab"}.bxs-objects-horizontal-left:before{content:"\ebc4"}.bxs-objects-horizontal-right:before{content:"\ebdd"}.bxs-objects-vertical-bottom:before{content:"\ebf6"}.bxs-objects-vertical-center:before{content:"\ef40"}.bxs-objects-vertical-top:before{content:"\ef41"}.bxs-pear:before{content:"\ef42"}.bxs-shield-minus:before{content:"\ef43"}.bxs-shield-plus:before{content:"\ef44"}.bxs-shower:before{content:"\ef45"}.bxs-sushi:before{content:"\ef46"}.bxs-universal-access:before{content:"\ef47"}.bx-child:before{content:"\ef48"}.bx-horizontal-left:before{content:"\ef49"}.bx-horizontal-right:before{content:"\ef4a"}.bx-objects-horizontal-center:before{content:"\ef4b"}.bx-objects-horizontal-left:before{content:"\ef4c"}.bx-objects-horizontal-right:before{content:"\ef4d"}.bx-objects-vertical-bottom:before{content:"\ef4e"}.bx-objects-vertical-center:before{content:"\ef4f"}.bx-objects-vertical-top:before{content:"\ef50"}.bx-rfid:before{content:"\ef51"}.bx-shield-minus:before{content:"\ef52"}.bx-shield-plus:before{content:"\ef53"}.bx-shower:before{content:"\ef54"}.bx-sushi:before{content:"\ef55"}.bx-universal-access:before{content:"\ef56"}.bx-vertical-bottom:before{content:"\ef57"}.bx-vertical-top:before{content:"\ef58"}.bxl-graphql:before{content:"\ef59"}.bxl-typescript:before{content:"\ef5a"}.bxs-color:before{content:"\ef39"}.bx-reflect-horizontal:before{content:"\ef3a"}.bx-reflect-vertical:before{content:"\ef3b"}.bx-color:before{content:"\ef3c"}.bxl-mongodb:before{content:"\ef3d"}.bxl-postgresql:before{content:"\ef3e"}.bxl-deezer:before{content:"\ef3f"}.bxs-hard-hat:before{content:"\ef2a"}.bxs-home-alt-2:before{content:"\ef2b"}.bxs-cheese:before{content:"\ef2c"}.bx-home-alt-2:before{content:"\ef2d"}.bx-hard-hat:before{content:"\ef2e"}.bx-cheese:before{content:"\ef2f"}.bx-cart-add:before{content:"\ef30"}.bx-cart-download:before{content:"\ef31"}.bx-no-signal:before{content:"\ef32"}.bx-signal-1:before{content:"\ef33"}.bx-signal-2:before{content:"\ef34"}.bx-signal-3:before{content:"\ef35"}.bx-signal-4:before{content:"\ef36"}.bx-signal-5:before{content:"\ef37"}.bxl-xing:before{content:"\ef38"}.bxl-meta:before{content:"\ef27"}.bx-lemon:before{content:"\ef28"}.bxs-lemon:before{content:"\ef29"}.bx-cricket-ball:before{content:"\ef0c"}.bx-baguette:before{content:"\ef0d"}.bx-bowl-hot:before{content:"\ef0e"}.bx-bowl-rice:before{content:"\ef0f"}.bx-cable-car:before{content:"\ef10"}.bx-candles:before{content:"\ef11"}.bx-circle-half:before{content:"\ef12"}.bx-circle-quarter:before{content:"\ef13"}.bx-circle-three-quarter:before{content:"\ef14"}.bx-cross:before{content:"\ef15"}.bx-fork:before{content:"\ef16"}.bx-knife:before{content:"\ef17"}.bx-money-withdraw:before{content:"\ef18"}.bx-popsicle:before{content:"\ef19"}.bx-scatter-chart:before{content:"\ef1a"}.bxs-baguette:before{content:"\ef1b"}.bxs-bowl-hot:before{content:"\ef1c"}.bxs-bowl-rice:before{content:"\ef1d"}.bxs-cable-car:before{content:"\ef1e"}.bxs-circle-half:before{content:"\ef1f"}.bxs-circle-quarter:before{content:"\ef20"}.bxs-circle-three-quarter:before{content:"\ef21"}.bxs-cricket-ball:before{content:"\ef22"}.bxs-invader:before{content:"\ef23"}.bx-male-female:before{content:"\ef24"}.bxs-popsicle:before{content:"\ef25"}.bxs-tree-alt:before{content:"\ef26"}.bxl-venmo:before{content:"\e900"}.bxl-upwork:before{content:"\e901"}.bxl-netlify:before{content:"\e902"}.bxl-java:before{content:"\e903"}.bxl-heroku:before{content:"\e904"}.bxl-go-lang:before{content:"\e905"}.bxl-gmail:before{content:"\e906"}.bxl-flask:before{content:"\e907"}.bxl-99designs:before{content:"\e908"}.bxl-500px:before{content:"\e909"}.bxl-adobe:before{content:"\e90a"}.bxl-airbnb:before{content:"\e90b"}.bxl-algolia:before{content:"\e90c"}.bxl-amazon:before{content:"\e90d"}.bxl-android:before{content:"\e90e"}.bxl-angular:before{content:"\e90f"}.bxl-apple:before{content:"\e910"}.bxl-audible:before{content:"\e911"}.bxl-aws:before{content:"\e912"}.bxl-baidu:before{content:"\e913"}.bxl-behance:before{content:"\e914"}.bxl-bing:before{content:"\e915"}.bxl-bitcoin:before{content:"\e916"}.bxl-blender:before{content:"\e917"}.bxl-blogger:before{content:"\e918"}.bxl-bootstrap:before{content:"\e919"}.bxl-chrome:before{content:"\e91a"}.bxl-codepen:before{content:"\e91b"}.bxl-c-plus-plus:before{content:"\e91c"}.bxl-creative-commons:before{content:"\e91d"}.bxl-css3:before{content:"\e91e"}.bxl-dailymotion:before{content:"\e91f"}.bxl-deviantart:before{content:"\e920"}.bxl-dev-to:before{content:"\e921"}.bxl-digg:before{content:"\e922"}.bxl-digitalocean:before{content:"\e923"}.bxl-discord:before{content:"\e924"}.bxl-discord-alt:before{content:"\e925"}.bxl-discourse:before{content:"\e926"}.bxl-django:before{content:"\e927"}.bxl-docker:before{content:"\e928"}.bxl-dribbble:before{content:"\e929"}.bxl-dropbox:before{content:"\e92a"}.bxl-drupal:before{content:"\e92b"}.bxl-ebay:before{content:"\e92c"}.bxl-edge:before{content:"\e92d"}.bxl-etsy:before{content:"\e92e"}.bxl-facebook:before{content:"\e92f"}.bxl-facebook-circle:before{content:"\e930"}.bxl-facebook-square:before{content:"\e931"}.bxl-figma:before{content:"\e932"}.bxl-firebase:before{content:"\e933"}.bxl-firefox:before{content:"\e934"}.bxl-flickr:before{content:"\e935"}.bxl-flickr-square:before{content:"\e936"}.bxl-flutter:before{content:"\e937"}.bxl-foursquare:before{content:"\e938"}.bxl-git:before{content:"\e939"}.bxl-github:before{content:"\e93a"}.bxl-gitlab:before{content:"\e93b"}.bxl-google:before{content:"\e93c"}.bxl-google-cloud:before{content:"\e93d"}.bxl-google-plus:before{content:"\e93e"}.bxl-google-plus-circle:before{content:"\e93f"}.bxl-html5:before{content:"\e940"}.bxl-imdb:before{content:"\e941"}.bxl-instagram:before{content:"\e942"}.bxl-instagram-alt:before{content:"\e943"}.bxl-internet-explorer:before{content:"\e944"}.bxl-invision:before{content:"\e945"}.bxl-javascript:before{content:"\e946"}.bxl-joomla:before{content:"\e947"}.bxl-jquery:before{content:"\e948"}.bxl-jsfiddle:before{content:"\e949"}.bxl-kickstarter:before{content:"\e94a"}.bxl-kubernetes:before{content:"\e94b"}.bxl-less:before{content:"\e94c"}.bxl-linkedin:before{content:"\e94d"}.bxl-linkedin-square:before{content:"\e94e"}.bxl-magento:before{content:"\e94f"}.bxl-mailchimp:before{content:"\e950"}.bxl-markdown:before{content:"\e951"}.bxl-mastercard:before{content:"\e952"}.bxl-mastodon:before{content:"\e953"}.bxl-medium:before{content:"\e954"}.bxl-medium-old:before{content:"\e955"}.bxl-medium-square:before{content:"\e956"}.bxl-messenger:before{content:"\e957"}.bxl-microsoft:before{content:"\e958"}.bxl-microsoft-teams:before{content:"\e959"}.bxl-nodejs:before{content:"\e95a"}.bxl-ok-ru:before{content:"\e95b"}.bxl-opera:before{content:"\e95c"}.bxl-patreon:before{content:"\e95d"}.bxl-paypal:before{content:"\e95e"}.bxl-periscope:before{content:"\e95f"}.bxl-php:before{content:"\e960"}.bxl-pinterest:before{content:"\e961"}.bxl-pinterest-alt:before{content:"\e962"}.bxl-play-store:before{content:"\e963"}.bxl-pocket:before{content:"\e964"}.bxl-product-hunt:before{content:"\e965"}.bxl-python:before{content:"\e966"}.bxl-quora:before{content:"\e967"}.bxl-react:before{content:"\e968"}.bxl-redbubble:before{content:"\e969"}.bxl-reddit:before{content:"\e96a"}.bxl-redux:before{content:"\e96b"}.bxl-sass:before{content:"\e96c"}.bxl-shopify:before{content:"\e96d"}.bxl-sketch:before{content:"\e96e"}.bxl-skype:before{content:"\e96f"}.bxl-slack:before{content:"\e970"}.bxl-slack-old:before{content:"\e971"}.bxl-snapchat:before{content:"\e972"}.bxl-soundcloud:before{content:"\e973"}.bxl-spotify:before{content:"\e974"}.bxl-spring-boot:before{content:"\e975"}.bxl-squarespace:before{content:"\e976"}.bxl-stack-overflow:before{content:"\e977"}.bxl-steam:before{content:"\e978"}.bxl-stripe:before{content:"\e979"}.bxl-tailwind-css:before{content:"\e97a"}.bxl-telegram:before{content:"\e97b"}.bxl-tiktok:before{content:"\e97c"}.bxl-trello:before{content:"\e97d"}.bxl-trip-advisor:before{content:"\e97e"}.bxl-tumblr:before{content:"\e97f"}.bxl-tux:before{content:"\e980"}.bxl-twitch:before{content:"\e981"}.bxl-twitter:before{content:"\e982"}.bxl-unity:before{content:"\e983"}.bxl-unsplash:before{content:"\e984"}.bxl-vimeo:before{content:"\e985"}.bxl-visa:before{content:"\e986"}.bxl-visual-studio:before{content:"\e987"}.bxl-vk:before{content:"\e988"}.bxl-vuejs:before{content:"\e989"}.bxl-whatsapp:before{content:"\e98a"}.bxl-whatsapp-square:before{content:"\e98b"}.bxl-wikipedia:before{content:"\e98c"}.bxl-windows:before{content:"\e98d"}.bxl-wix:before{content:"\e98e"}.bxl-wordpress:before{content:"\e98f"}.bxl-yahoo:before{content:"\e990"}.bxl-yelp:before{content:"\e991"}.bxl-youtube:before{content:"\e992"}.bxl-zoom:before{content:"\e993"}.bx-collapse-alt:before{content:"\e994"}.bx-collapse-horizontal:before{content:"\e995"}.bx-collapse-vertical:before{content:"\e996"}.bx-expand-horizontal:before{content:"\e997"}.bx-expand-vertical:before{content:"\e998"}.bx-injection:before{content:"\e999"}.bx-leaf:before{content:"\e99a"}.bx-math:before{content:"\e99b"}.bx-party:before{content:"\e99c"}.bx-abacus:before{content:"\e99d"}.bx-accessibility:before{content:"\e99e"}.bx-add-to-queue:before{content:"\e99f"}.bx-adjust:before{content:"\e9a0"}.bx-alarm:before{content:"\e9a1"}.bx-alarm-add:before{content:"\e9a2"}.bx-alarm-exclamation:before{content:"\e9a3"}.bx-alarm-off:before{content:"\e9a4"}.bx-alarm-snooze:before{content:"\e9a5"}.bx-album:before{content:"\e9a6"}.bx-align-justify:before{content:"\e9a7"}.bx-align-left:before{content:"\e9a8"}.bx-align-middle:before{content:"\e9a9"}.bx-align-right:before{content:"\e9aa"}.bx-analyse:before{content:"\e9ab"}.bx-anchor:before{content:"\e9ac"}.bx-angry:before{content:"\e9ad"}.bx-aperture:before{content:"\e9ae"}.bx-arch:before{content:"\e9af"}.bx-archive:before{content:"\e9b0"}.bx-archive-in:before{content:"\e9b1"}.bx-archive-out:before{content:"\e9b2"}.bx-area:before{content:"\e9b3"}.bx-arrow-back:before{content:"\e9b4"}.bx-arrow-from-bottom:before{content:"\e9b5"}.bx-arrow-from-left:before{content:"\e9b6"}.bx-arrow-from-right:before{content:"\e9b7"}.bx-arrow-from-top:before{content:"\e9b8"}.bx-arrow-to-bottom:before{content:"\e9b9"}.bx-arrow-to-left:before{content:"\e9ba"}.bx-arrow-to-right:before{content:"\e9bb"}.bx-arrow-to-top:before{content:"\e9bc"}.bx-at:before{content:"\e9bd"}.bx-atom:before{content:"\e9be"}.bx-award:before{content:"\e9bf"}.bx-badge:before{content:"\e9c0"}.bx-badge-check:before{content:"\e9c1"}.bx-ball:before{content:"\e9c2"}.bx-band-aid:before{content:"\e9c3"}.bx-bar-chart:before{content:"\e9c4"}.bx-bar-chart-alt:before{content:"\e9c5"}.bx-bar-chart-alt-2:before{content:"\e9c6"}.bx-bar-chart-square:before{content:"\e9c7"}.bx-barcode:before{content:"\e9c8"}.bx-barcode-reader:before{content:"\e9c9"}.bx-baseball:before{content:"\e9ca"}.bx-basket:before{content:"\e9cb"}.bx-basketball:before{content:"\e9cc"}.bx-bath:before{content:"\e9cd"}.bx-battery:before{content:"\e9ce"}.bx-bed:before{content:"\e9cf"}.bx-been-here:before{content:"\e9d0"}.bx-beer:before{content:"\e9d1"}.bx-bell:before{content:"\e9d2"}.bx-bell-minus:before{content:"\e9d3"}.bx-bell-off:before{content:"\e9d4"}.bx-bell-plus:before{content:"\e9d5"}.bx-bible:before{content:"\e9d6"}.bx-bitcoin:before{content:"\e9d7"}.bx-blanket:before{content:"\e9d8"}.bx-block:before{content:"\e9d9"}.bx-bluetooth:before{content:"\e9da"}.bx-body:before{content:"\e9db"}.bx-bold:before{content:"\e9dc"}.bx-bolt-circle:before{content:"\e9dd"}.bx-bomb:before{content:"\e9de"}.bx-bone:before{content:"\e9df"}.bx-bong:before{content:"\e9e0"}.bx-book:before{content:"\e9e1"}.bx-book-add:before{content:"\e9e2"}.bx-book-alt:before{content:"\e9e3"}.bx-book-bookmark:before{content:"\e9e4"}.bx-book-content:before{content:"\e9e5"}.bx-book-heart:before{content:"\e9e6"}.bx-bookmark:before{content:"\e9e7"}.bx-bookmark-alt:before{content:"\e9e8"}.bx-bookmark-alt-minus:before{content:"\e9e9"}.bx-bookmark-alt-plus:before{content:"\e9ea"}.bx-bookmark-heart:before{content:"\e9eb"}.bx-bookmark-minus:before{content:"\e9ec"}.bx-bookmark-plus:before{content:"\e9ed"}.bx-bookmarks:before{content:"\e9ee"}.bx-book-open:before{content:"\e9ef"}.bx-book-reader:before{content:"\e9f0"}.bx-border-all:before{content:"\e9f1"}.bx-border-bottom:before{content:"\e9f2"}.bx-border-inner:before{content:"\e9f3"}.bx-border-left:before{content:"\e9f4"}.bx-border-none:before{content:"\e9f5"}.bx-border-outer:before{content:"\e9f6"}.bx-border-radius:before{content:"\e9f7"}.bx-border-right:before{content:"\e9f8"}.bx-border-top:before{content:"\e9f9"}.bx-bot:before{content:"\e9fa"}.bx-bowling-ball:before{content:"\e9fb"}.bx-box:before{content:"\e9fc"}.bx-bracket:before{content:"\e9fd"}.bx-braille:before{content:"\e9fe"}.bx-brain:before{content:"\e9ff"}.bx-briefcase:before{content:"\ea00"}.bx-briefcase-alt:before{content:"\ea01"}.bx-briefcase-alt-2:before{content:"\ea02"}.bx-brightness:before{content:"\ea03"}.bx-brightness-half:before{content:"\ea04"}.bx-broadcast:before{content:"\ea05"}.bx-brush:before{content:"\ea06"}.bx-brush-alt:before{content:"\ea07"}.bx-bug:before{content:"\ea08"}.bx-bug-alt:before{content:"\ea09"}.bx-building:before{content:"\ea0a"}.bx-building-house:before{content:"\ea0b"}.bx-buildings:before{content:"\ea0c"}.bx-bulb:before{content:"\ea0d"}.bx-bullseye:before{content:"\ea0e"}.bx-buoy:before{content:"\ea0f"}.bx-bus:before{content:"\ea10"}.bx-bus-school:before{content:"\ea11"}.bx-cabinet:before{content:"\ea12"}.bx-cake:before{content:"\ea13"}.bx-calculator:before{content:"\ea14"}.bx-calendar:before{content:"\ea15"}.bx-calendar-alt:before{content:"\ea16"}.bx-calendar-check:before{content:"\ea17"}.bx-calendar-edit:before{content:"\ea18"}.bx-calendar-event:before{content:"\ea19"}.bx-calendar-exclamation:before{content:"\ea1a"}.bx-calendar-heart:before{content:"\ea1b"}.bx-calendar-minus:before{content:"\ea1c"}.bx-calendar-plus:before{content:"\ea1d"}.bx-calendar-star:before{content:"\ea1e"}.bx-calendar-week:before{content:"\ea1f"}.bx-calendar-x:before{content:"\ea20"}.bx-camera:before{content:"\ea21"}.bx-camera-home:before{content:"\ea22"}.bx-camera-movie:before{content:"\ea23"}.bx-camera-off:before{content:"\ea24"}.bx-capsule:before{content:"\ea25"}.bx-captions:before{content:"\ea26"}.bx-car:before{content:"\ea27"}.bx-card:before{content:"\ea28"}.bx-caret-down:before{content:"\ea29"}.bx-caret-down-circle:before{content:"\ea2a"}.bx-caret-down-square:before{content:"\ea2b"}.bx-caret-left:before{content:"\ea2c"}.bx-caret-left-circle:before{content:"\ea2d"}.bx-caret-left-square:before{content:"\ea2e"}.bx-caret-right:before{content:"\ea2f"}.bx-caret-right-circle:before{content:"\ea30"}.bx-caret-right-square:before{content:"\ea31"}.bx-caret-up:before{content:"\ea32"}.bx-caret-up-circle:before{content:"\ea33"}.bx-caret-up-square:before{content:"\ea34"}.bx-carousel:before{content:"\ea35"}.bx-cart:before{content:"\ea36"}.bx-cart-alt:before{content:"\ea37"}.bx-cast:before{content:"\ea38"}.bx-category:before{content:"\ea39"}.bx-category-alt:before{content:"\ea3a"}.bx-cctv:before{content:"\ea3b"}.bx-certification:before{content:"\ea3c"}.bx-chair:before{content:"\ea3d"}.bx-chalkboard:before{content:"\ea3e"}.bx-chart:before{content:"\ea3f"}.bx-chat:before{content:"\ea40"}.bx-check:before{content:"\ea41"}.bx-checkbox:before{content:"\ea42"}.bx-checkbox-checked:before{content:"\ea43"}.bx-checkbox-minus:before{content:"\ea44"}.bx-checkbox-square:before{content:"\ea45"}.bx-check-circle:before{content:"\ea46"}.bx-check-double:before{content:"\ea47"}.bx-check-shield:before{content:"\ea48"}.bx-check-square:before{content:"\ea49"}.bx-chevron-down:before{content:"\ea4a"}.bx-chevron-down-circle:before{content:"\ea4b"}.bx-chevron-down-square:before{content:"\ea4c"}.bx-chevron-left:before{content:"\ea4d"}.bx-chevron-left-circle:before{content:"\ea4e"}.bx-chevron-left-square:before{content:"\ea4f"}.bx-chevron-right:before{content:"\ea50"}.bx-chevron-right-circle:before{content:"\ea51"}.bx-chevron-right-square:before{content:"\ea52"}.bx-chevrons-down:before{content:"\ea53"}.bx-chevrons-left:before{content:"\ea54"}.bx-chevrons-right:before{content:"\ea55"}.bx-chevrons-up:before{content:"\ea56"}.bx-chevron-up:before{content:"\ea57"}.bx-chevron-up-circle:before{content:"\ea58"}.bx-chevron-up-square:before{content:"\ea59"}.bx-chip:before{content:"\ea5a"}.bx-church:before{content:"\ea5b"}.bx-circle:before{content:"\ea5c"}.bx-clinic:before{content:"\ea5d"}.bx-clipboard:before{content:"\ea5e"}.bx-closet:before{content:"\ea5f"}.bx-cloud:before{content:"\ea60"}.bx-cloud-download:before{content:"\ea61"}.bx-cloud-drizzle:before{content:"\ea62"}.bx-cloud-lightning:before{content:"\ea63"}.bx-cloud-light-rain:before{content:"\ea64"}.bx-cloud-rain:before{content:"\ea65"}.bx-cloud-snow:before{content:"\ea66"}.bx-cloud-upload:before{content:"\ea67"}.bx-code:before{content:"\ea68"}.bx-code-alt:before{content:"\ea69"}.bx-code-block:before{content:"\ea6a"}.bx-code-curly:before{content:"\ea6b"}.bx-coffee:before{content:"\ea6c"}.bx-coffee-togo:before{content:"\ea6d"}.bx-cog:before{content:"\ea6e"}.bx-coin:before{content:"\ea6f"}.bx-coin-stack:before{content:"\ea70"}.bx-collapse:before{content:"\ea71"}.bx-collection:before{content:"\ea72"}.bx-color-fill:before{content:"\ea73"}.bx-columns:before{content:"\ea74"}.bx-command:before{content:"\ea75"}.bx-comment:before{content:"\ea76"}.bx-comment-add:before{content:"\ea77"}.bx-comment-check:before{content:"\ea78"}.bx-comment-detail:before{content:"\ea79"}.bx-comment-dots:before{content:"\ea7a"}.bx-comment-edit:before{content:"\ea7b"}.bx-comment-error:before{content:"\ea7c"}.bx-comment-minus:before{content:"\ea7d"}.bx-comment-x:before{content:"\ea7e"}.bx-compass:before{content:"\ea7f"}.bx-confused:before{content:"\ea80"}.bx-conversation:before{content:"\ea81"}.bx-cookie:before{content:"\ea82"}.bx-cool:before{content:"\ea83"}.bx-copy:before{content:"\ea84"}.bx-copy-alt:before{content:"\ea85"}.bx-copyright:before{content:"\ea86"}.bx-credit-card:before{content:"\ea87"}.bx-credit-card-alt:before{content:"\ea88"}.bx-credit-card-front:before{content:"\ea89"}.bx-crop:before{content:"\ea8a"}.bx-crosshair:before{content:"\ea8b"}.bx-crown:before{content:"\ea8c"}.bx-cube:before{content:"\ea8d"}.bx-cube-alt:before{content:"\ea8e"}.bx-cuboid:before{content:"\ea8f"}.bx-current-location:before{content:"\ea90"}.bx-customize:before{content:"\ea91"}.bx-cut:before{content:"\ea92"}.bx-cycling:before{content:"\ea93"}.bx-cylinder:before{content:"\ea94"}.bx-data:before{content:"\ea95"}.bx-desktop:before{content:"\ea96"}.bx-detail:before{content:"\ea97"}.bx-devices:before{content:"\ea98"}.bx-dialpad:before{content:"\ea99"}.bx-dialpad-alt:before{content:"\ea9a"}.bx-diamond:before{content:"\ea9b"}.bx-dice-1:before{content:"\ea9c"}.bx-dice-2:before{content:"\ea9d"}.bx-dice-3:before{content:"\ea9e"}.bx-dice-4:before{content:"\ea9f"}.bx-dice-5:before{content:"\eaa0"}.bx-dice-6:before{content:"\eaa1"}.bx-directions:before{content:"\eaa2"}.bx-disc:before{content:"\eaa3"}.bx-dish:before{content:"\eaa4"}.bx-dislike:before{content:"\eaa5"}.bx-dizzy:before{content:"\eaa6"}.bx-dna:before{content:"\eaa7"}.bx-dock-bottom:before{content:"\eaa8"}.bx-dock-left:before{content:"\eaa9"}.bx-dock-right:before{content:"\eaaa"}.bx-dock-top:before{content:"\eaab"}.bx-dollar:before{content:"\eaac"}.bx-dollar-circle:before{content:"\eaad"}.bx-donate-blood:before{content:"\eaae"}.bx-donate-heart:before{content:"\eaaf"}.bx-door-open:before{content:"\eab0"}.bx-dots-horizontal:before{content:"\eab1"}.bx-dots-horizontal-rounded:before{content:"\eab2"}.bx-dots-vertical:before{content:"\eab3"}.bx-dots-vertical-rounded:before{content:"\eab4"}.bx-doughnut-chart:before{content:"\eab5"}.bx-down-arrow:before{content:"\eab6"}.bx-down-arrow-alt:before{content:"\eab7"}.bx-down-arrow-circle:before{content:"\eab8"}.bx-download:before{content:"\eab9"}.bx-downvote:before{content:"\eaba"}.bx-drink:before{content:"\eabb"}.bx-droplet:before{content:"\eabc"}.bx-dumbbell:before{content:"\eabd"}.bx-duplicate:before{content:"\eabe"}.bx-edit:before{content:"\eabf"}.bx-edit-alt:before{content:"\eac0"}.bx-envelope:before{content:"\eac1"}.bx-envelope-open:before{content:"\eac2"}.bx-equalizer:before{content:"\eac3"}.bx-eraser:before{content:"\eac4"}.bx-error:before{content:"\eac5"}.bx-error-alt:before{content:"\eac6"}.bx-error-circle:before{content:"\eac7"}.bx-euro:before{content:"\eac8"}.bx-exclude:before{content:"\eac9"}.bx-exit:before{content:"\eaca"}.bx-exit-fullscreen:before{content:"\eacb"}.bx-expand:before{content:"\eacc"}.bx-expand-alt:before{content:"\eacd"}.bx-export:before{content:"\eace"}.bx-extension:before{content:"\eacf"}.bx-face:before{content:"\ead0"}.bx-fast-forward:before{content:"\ead1"}.bx-fast-forward-circle:before{content:"\ead2"}.bx-female:before{content:"\ead3"}.bx-female-sign:before{content:"\ead4"}.bx-file:before{content:"\ead5"}.bx-file-blank:before{content:"\ead6"}.bx-file-find:before{content:"\ead7"}.bx-film:before{content:"\ead8"}.bx-filter:before{content:"\ead9"}.bx-filter-alt:before{content:"\eada"}.bx-fingerprint:before{content:"\eadb"}.bx-first-aid:before{content:"\eadc"}.bx-first-page:before{content:"\eadd"}.bx-flag:before{content:"\eade"}.bx-folder:before{content:"\eadf"}.bx-folder-minus:before{content:"\eae0"}.bx-folder-open:before{content:"\eae1"}.bx-folder-plus:before{content:"\eae2"}.bx-font:before{content:"\eae3"}.bx-font-color:before{content:"\eae4"}.bx-font-family:before{content:"\eae5"}.bx-font-size:before{content:"\eae6"}.bx-food-menu:before{content:"\eae7"}.bx-food-tag:before{content:"\eae8"}.bx-football:before{content:"\eae9"}.bx-fridge:before{content:"\eaea"}.bx-fullscreen:before{content:"\eaeb"}.bx-game:before{content:"\eaec"}.bx-gas-pump:before{content:"\eaed"}.bx-ghost:before{content:"\eaee"}.bx-gift:before{content:"\eaef"}.bx-git-branch:before{content:"\eaf0"}.bx-git-commit:before{content:"\eaf1"}.bx-git-compare:before{content:"\eaf2"}.bx-git-merge:before{content:"\eaf3"}.bx-git-pull-request:before{content:"\eaf4"}.bx-git-repo-forked:before{content:"\eaf5"}.bx-glasses:before{content:"\eaf6"}.bx-glasses-alt:before{content:"\eaf7"}.bx-globe:before{content:"\eaf8"}.bx-globe-alt:before{content:"\eaf9"}.bx-grid:before{content:"\eafa"}.bx-grid-alt:before{content:"\eafb"}.bx-grid-horizontal:before{content:"\eafc"}.bx-grid-small:before{content:"\eafd"}.bx-grid-vertical:before{content:"\eafe"}.bx-group:before{content:"\eaff"}.bx-handicap:before{content:"\eb00"}.bx-happy:before{content:"\eb01"}.bx-happy-alt:before{content:"\eb02"}.bx-happy-beaming:before{content:"\eb03"}.bx-happy-heart-eyes:before{content:"\eb04"}.bx-hash:before{content:"\eb05"}.bx-hdd:before{content:"\eb06"}.bx-heading:before{content:"\eb07"}.bx-headphone:before{content:"\eb08"}.bx-health:before{content:"\eb09"}.bx-heart:before{content:"\eb0a"}.bx-heart-circle:before{content:"\eb0b"}.bx-heart-square:before{content:"\eb0c"}.bx-help-circle:before{content:"\eb0d"}.bx-hide:before{content:"\eb0e"}.bx-highlight:before{content:"\eb0f"}.bx-history:before{content:"\eb10"}.bx-hive:before{content:"\eb11"}.bx-home:before{content:"\eb12"}.bx-home-alt:before{content:"\eb13"}.bx-home-circle:before{content:"\eb14"}.bx-home-heart:before{content:"\eb15"}.bx-home-smile:before{content:"\eb16"}.bx-horizontal-center:before{content:"\eb17"}.bx-hotel:before{content:"\eb18"}.bx-hourglass:before{content:"\eb19"}.bx-id-card:before{content:"\eb1a"}.bx-image:before{content:"\eb1b"}.bx-image-add:before{content:"\eb1c"}.bx-image-alt:before{content:"\eb1d"}.bx-images:before{content:"\eb1e"}.bx-import:before{content:"\eb1f"}.bx-infinite:before{content:"\eb20"}.bx-info-circle:before{content:"\eb21"}.bx-info-square:before{content:"\eb22"}.bx-intersect:before{content:"\eb23"}.bx-italic:before{content:"\eb24"}.bx-joystick:before{content:"\eb25"}.bx-joystick-alt:before{content:"\eb26"}.bx-joystick-button:before{content:"\eb27"}.bx-key:before{content:"\eb28"}.bx-label:before{content:"\eb29"}.bx-landscape:before{content:"\eb2a"}.bx-laptop:before{content:"\eb2b"}.bx-last-page:before{content:"\eb2c"}.bx-laugh:before{content:"\eb2d"}.bx-layer:before{content:"\eb2e"}.bx-layer-minus:before{content:"\eb2f"}.bx-layer-plus:before{content:"\eb30"}.bx-layout:before{content:"\eb31"}.bx-left-arrow:before{content:"\eb32"}.bx-left-arrow-alt:before{content:"\eb33"}.bx-left-arrow-circle:before{content:"\eb34"}.bx-left-down-arrow-circle:before{content:"\eb35"}.bx-left-indent:before{content:"\eb36"}.bx-left-top-arrow-circle:before{content:"\eb37"}.bx-library:before{content:"\eb38"}.bx-like:before{content:"\eb39"}.bx-line-chart:before{content:"\eb3a"}.bx-line-chart-down:before{content:"\eb3b"}.bx-link:before{content:"\eb3c"}.bx-link-alt:before{content:"\eb3d"}.bx-link-external:before{content:"\eb3e"}.bx-lira:before{content:"\eb3f"}.bx-list-check:before{content:"\eb40"}.bx-list-minus:before{content:"\eb41"}.bx-list-ol:before{content:"\eb42"}.bx-list-plus:before{content:"\eb43"}.bx-list-ul:before{content:"\eb44"}.bx-loader:before{content:"\eb45"}.bx-loader-alt:before{content:"\eb46"}.bx-loader-circle:before{content:"\eb47"}.bx-location-plus:before{content:"\eb48"}.bx-lock:before{content:"\eb49"}.bx-lock-alt:before{content:"\eb4a"}.bx-lock-open:before{content:"\eb4b"}.bx-lock-open-alt:before{content:"\eb4c"}.bx-log-in:before{content:"\eb4d"}.bx-log-in-circle:before{content:"\eb4e"}.bx-log-out:before{content:"\eb4f"}.bx-log-out-circle:before{content:"\eb50"}.bx-low-vision:before{content:"\eb51"}.bx-magnet:before{content:"\eb52"}.bx-mail-send:before{content:"\eb53"}.bx-male:before{content:"\eb54"}.bx-male-sign:before{content:"\eb55"}.bx-map:before{content:"\eb56"}.bx-map-alt:before{content:"\eb57"}.bx-map-pin:before{content:"\eb58"}.bx-mask:before{content:"\eb59"}.bx-medal:before{content:"\eb5a"}.bx-meh:before{content:"\eb5b"}.bx-meh-alt:before{content:"\eb5c"}.bx-meh-blank:before{content:"\eb5d"}.bx-memory-card:before{content:"\eb5e"}.bx-menu:before{content:"\eb5f"}.bx-menu-alt-left:before{content:"\ef5b"}.bx-menu-alt-right:before{content:"\eb61"}.bx-merge:before{content:"\eb62"}.bx-message:before{content:"\eb63"}.bx-message-add:before{content:"\eb64"}.bx-message-alt:before{content:"\eb65"}.bx-message-alt-add:before{content:"\eb66"}.bx-message-alt-check:before{content:"\eb67"}.bx-message-alt-detail:before{content:"\eb68"}.bx-message-alt-dots:before{content:"\eb69"}.bx-message-alt-edit:before{content:"\eb6a"}.bx-message-alt-error:before{content:"\eb6b"}.bx-message-alt-minus:before{content:"\eb6c"}.bx-message-alt-x:before{content:"\eb6d"}.bx-message-check:before{content:"\eb6e"}.bx-message-detail:before{content:"\eb6f"}.bx-message-dots:before{content:"\eb70"}.bx-message-edit:before{content:"\eb71"}.bx-message-error:before{content:"\eb72"}.bx-message-minus:before{content:"\eb73"}.bx-message-rounded:before{content:"\eb74"}.bx-message-rounded-add:before{content:"\eb75"}.bx-message-rounded-check:before{content:"\eb76"}.bx-message-rounded-detail:before{content:"\eb77"}.bx-message-rounded-dots:before{content:"\eb78"}.bx-message-rounded-edit:before{content:"\ef5c"}.bx-message-rounded-error:before{content:"\eb7a"}.bx-message-rounded-minus:before{content:"\eb7b"}.bx-message-rounded-x:before{content:"\eb7c"}.bx-message-square:before{content:"\eb7d"}.bx-message-square-add:before{content:"\eb7e"}.bx-message-square-check:before{content:"\eb7f"}.bx-message-square-detail:before{content:"\eb80"}.bx-message-square-dots:before{content:"\eb81"}.bx-message-square-edit:before{content:"\eb82"}.bx-message-square-error:before{content:"\eb83"}.bx-message-square-minus:before{content:"\eb84"}.bx-message-square-x:before{content:"\eb85"}.bx-message-x:before{content:"\eb86"}.bx-meteor:before{content:"\eb87"}.bx-microchip:before{content:"\eb88"}.bx-microphone:before{content:"\eb89"}.bx-microphone-off:before{content:"\eb8a"}.bx-minus:before{content:"\eb8b"}.bx-minus-back:before{content:"\eb8c"}.bx-minus-circle:before{content:"\eb8d"}.bx-minus-front:before{content:"\eb8e"}.bx-mobile:before{content:"\eb8f"}.bx-mobile-alt:before{content:"\eb90"}.bx-mobile-landscape:before{content:"\eb91"}.bx-mobile-vibration:before{content:"\ef5d"}.bx-money:before{content:"\eb93"}.bx-moon:before{content:"\eb94"}.bx-mouse:before{content:"\eb95"}.bx-mouse-alt:before{content:"\eb96"}.bx-move:before{content:"\eb97"}.bx-move-horizontal:before{content:"\eb98"}.bx-move-vertical:before{content:"\eb99"}.bx-movie:before{content:"\eb9a"}.bx-movie-play:before{content:"\eb9b"}.bx-music:before{content:"\eb9c"}.bx-navigation:before{content:"\eb9d"}.bx-network-chart:before{content:"\eb9e"}.bx-news:before{content:"\eb9f"}.bx-no-entry:before{content:"\eba0"}.bx-note:before{content:"\eba1"}.bx-notepad:before{content:"\eba2"}.bx-notification:before{content:"\eba3"}.bx-notification-off:before{content:"\eba4"}.bx-outline:before{content:"\eba5"}.bx-package:before{content:"\eba6"}.bx-paint:before{content:"\eba7"}.bx-paint-roll:before{content:"\eba8"}.bx-palette:before{content:"\eba9"}.bx-paperclip:before{content:"\ebaa"}.bx-paper-plane:before{content:"\ef61"}.bx-paragraph:before{content:"\ebac"}.bx-paste:before{content:"\ebad"}.bx-pause:before{content:"\ebae"}.bx-pause-circle:before{content:"\ebaf"}.bx-pen:before{content:"\ebb0"}.bx-pencil:before{content:"\ebb1"}.bx-phone:before{content:"\ebb2"}.bx-phone-call:before{content:"\ebb3"}.bx-phone-incoming:before{content:"\ebb4"}.bx-phone-off:before{content:"\ebb5"}.bx-phone-outgoing:before{content:"\ebb6"}.bx-photo-album:before{content:"\ebb7"}.bx-pie-chart:before{content:"\ebb8"}.bx-pie-chart-alt:before{content:"\ebb9"}.bx-pie-chart-alt-2:before{content:"\ebba"}.bx-pin:before{content:"\ebbb"}.bx-planet:before{content:"\ebbc"}.bx-play:before{content:"\ebbd"}.bx-play-circle:before{content:"\ebbe"}.bx-plug:before{content:"\ebbf"}.bx-plus:before{content:"\ebc0"}.bx-plus-circle:before{content:"\ebc1"}.bx-plus-medical:before{content:"\ebc2"}.bx-podcast:before{content:"\ebc3"}.bx-pointer:before{content:"\ef5e"}.bx-poll:before{content:"\ebc5"}.bx-polygon:before{content:"\ebc6"}.bx-pound:before{content:"\ebc7"}.bx-power-off:before{content:"\ebc8"}.bx-printer:before{content:"\ebc9"}.bx-pulse:before{content:"\ebca"}.bx-purchase-tag:before{content:"\ebcb"}.bx-purchase-tag-alt:before{content:"\ebcc"}.bx-pyramid:before{content:"\ebcd"}.bx-qr:before{content:"\ebce"}.bx-qr-scan:before{content:"\ebcf"}.bx-question-mark:before{content:"\ebd0"}.bx-radar:before{content:"\ebd1"}.bx-radio:before{content:"\ebd2"}.bx-radio-circle:before{content:"\ebd3"}.bx-radio-circle-marked:before{content:"\ebd4"}.bx-receipt:before{content:"\ebd5"}.bx-rectangle:before{content:"\ebd6"}.bx-recycle:before{content:"\ebd7"}.bx-redo:before{content:"\ebd8"}.bx-refresh:before{content:"\ebd9"}.bx-registered:before{content:"\ebda"}.bx-rename:before{content:"\ebdb"}.bx-repeat:before{content:"\ebdc"}.bx-reply:before{content:"\ef5f"}.bx-reply-all:before{content:"\ebde"}.bx-repost:before{content:"\ebdf"}.bx-reset:before{content:"\ebe0"}.bx-restaurant:before{content:"\ebe1"}.bx-revision:before{content:"\ebe2"}.bx-rewind:before{content:"\ebe3"}.bx-rewind-circle:before{content:"\ebe4"}.bx-right-arrow:before{content:"\ebe5"}.bx-right-arrow-alt:before{content:"\ebe6"}.bx-right-arrow-circle:before{content:"\ebe7"}.bx-right-down-arrow-circle:before{content:"\ebe8"}.bx-right-indent:before{content:"\ebe9"}.bx-right-top-arrow-circle:before{content:"\ebea"}.bx-rocket:before{content:"\ebeb"}.bx-rotate-left:before{content:"\ebec"}.bx-rotate-right:before{content:"\ebed"}.bx-rss:before{content:"\ebee"}.bx-ruble:before{content:"\ebef"}.bx-ruler:before{content:"\ebf0"}.bx-run:before{content:"\ebf1"}.bx-rupee:before{content:"\ebf2"}.bx-sad:before{content:"\ebf3"}.bx-save:before{content:"\ebf4"}.bx-scan:before{content:"\ebf5"}.bx-screenshot:before{content:"\ef60"}.bx-search:before{content:"\ebf7"}.bx-search-alt:before{content:"\ebf8"}.bx-search-alt-2:before{content:"\ebf9"}.bx-selection:before{content:"\ebfa"}.bx-select-multiple:before{content:"\ebfb"}.bx-send:before{content:"\ebfc"}.bx-server:before{content:"\ebfd"}.bx-shape-circle:before{content:"\ebfe"}.bx-shape-polygon:before{content:"\ebff"}.bx-shape-square:before{content:"\ec00"}.bx-shape-triangle:before{content:"\ec01"}.bx-share:before{content:"\ec02"}.bx-share-alt:before{content:"\ec03"}.bx-shekel:before{content:"\ec04"}.bx-shield:before{content:"\ec05"}.bx-shield-alt:before{content:"\ec06"}.bx-shield-alt-2:before{content:"\ec07"}.bx-shield-quarter:before{content:"\ec08"}.bx-shield-x:before{content:"\ec09"}.bx-shocked:before{content:"\ec0a"}.bx-shopping-bag:before{content:"\ec0b"}.bx-show:before{content:"\ec0c"}.bx-show-alt:before{content:"\ec0d"}.bx-shuffle:before{content:"\ec0e"}.bx-sidebar:before{content:"\ec0f"}.bx-sitemap:before{content:"\ec10"}.bx-skip-next:before{content:"\ec11"}.bx-skip-next-circle:before{content:"\ec12"}.bx-skip-previous:before{content:"\ec13"}.bx-skip-previous-circle:before{content:"\ec14"}.bx-sleepy:before{content:"\ec15"}.bx-slider:before{content:"\ec16"}.bx-slider-alt:before{content:"\ec17"}.bx-slideshow:before{content:"\ec18"}.bx-smile:before{content:"\ec19"}.bx-sort:before{content:"\ec1a"}.bx-sort-alt-2:before{content:"\ec1b"}.bx-sort-a-z:before{content:"\ec1c"}.bx-sort-down:before{content:"\ec1d"}.bx-sort-up:before{content:"\ec1e"}.bx-sort-z-a:before{content:"\ec1f"}.bx-spa:before{content:"\ec20"}.bx-space-bar:before{content:"\ec21"}.bx-speaker:before{content:"\ec22"}.bx-spray-can:before{content:"\ec23"}.bx-spreadsheet:before{content:"\ec24"}.bx-square:before{content:"\ec25"}.bx-square-rounded:before{content:"\ec26"}.bx-star:before{content:"\ec27"}.bx-station:before{content:"\ec28"}.bx-stats:before{content:"\ec29"}.bx-sticker:before{content:"\ec2a"}.bx-stop:before{content:"\ec2b"}.bx-stop-circle:before{content:"\ec2c"}.bx-stopwatch:before{content:"\ec2d"}.bx-store:before{content:"\ec2e"}.bx-store-alt:before{content:"\ec2f"}.bx-street-view:before{content:"\ec30"}.bx-strikethrough:before{content:"\ec31"}.bx-subdirectory-left:before{content:"\ec32"}.bx-subdirectory-right:before{content:"\ec33"}.bx-sun:before{content:"\ec34"}.bx-support:before{content:"\ec35"}.bx-swim:before{content:"\ec36"}.bx-sync:before{content:"\ec37"}.bx-tab:before{content:"\ec38"}.bx-table:before{content:"\ec39"}.bx-tachometer:before{content:"\ec3a"}.bx-tag:before{content:"\ec3b"}.bx-tag-alt:before{content:"\ec3c"}.bx-target-lock:before{content:"\ec3d"}.bx-task:before{content:"\ec3e"}.bx-task-x:before{content:"\ec3f"}.bx-taxi:before{content:"\ec40"}.bx-tennis-ball:before{content:"\ec41"}.bx-terminal:before{content:"\ec42"}.bx-test-tube:before{content:"\ec43"}.bx-text:before{content:"\ec44"}.bx-time:before{content:"\ec45"}.bx-time-five:before{content:"\ec46"}.bx-timer:before{content:"\ec47"}.bx-tired:before{content:"\ec48"}.bx-toggle-left:before{content:"\ec49"}.bx-toggle-right:before{content:"\ec4a"}.bx-tone:before{content:"\ec4b"}.bx-traffic-cone:before{content:"\ec4c"}.bx-train:before{content:"\ec4d"}.bx-transfer:before{content:"\ec4e"}.bx-transfer-alt:before{content:"\ec4f"}.bx-trash:before{content:"\ec50"}.bx-trash-alt:before{content:"\ec51"}.bx-trending-down:before{content:"\ec52"}.bx-trending-up:before{content:"\ec53"}.bx-trim:before{content:"\ec54"}.bx-trip:before{content:"\ec55"}.bx-trophy:before{content:"\ec56"}.bx-tv:before{content:"\ec57"}.bx-underline:before{content:"\ec58"}.bx-undo:before{content:"\ec59"}.bx-unite:before{content:"\ec5a"}.bx-unlink:before{content:"\ec5b"}.bx-up-arrow:before{content:"\ec5c"}.bx-up-arrow-alt:before{content:"\ec5d"}.bx-up-arrow-circle:before{content:"\ec5e"}.bx-upload:before{content:"\ec5f"}.bx-upside-down:before{content:"\ec60"}.bx-upvote:before{content:"\ec61"}.bx-usb:before{content:"\ec62"}.bx-user:before{content:"\ec63"}.bx-user-check:before{content:"\ec64"}.bx-user-circle:before{content:"\ec65"}.bx-user-minus:before{content:"\ec66"}.bx-user-pin:before{content:"\ec67"}.bx-user-plus:before{content:"\ec68"}.bx-user-voice:before{content:"\ec69"}.bx-user-x:before{content:"\ec6a"}.bx-vector:before{content:"\ec6b"}.bx-vertical-center:before{content:"\ec6c"}.bx-vial:before{content:"\ec6d"}.bx-video:before{content:"\ec6e"}.bx-video-off:before{content:"\ec6f"}.bx-video-plus:before{content:"\ec70"}.bx-video-recording:before{content:"\ec71"}.bx-voicemail:before{content:"\ec72"}.bx-volume:before{content:"\ec73"}.bx-volume-full:before{content:"\ec74"}.bx-volume-low:before{content:"\ec75"}.bx-volume-mute:before{content:"\ec76"}.bx-walk:before{content:"\ec77"}.bx-wallet:before{content:"\ec78"}.bx-wallet-alt:before{content:"\ec79"}.bx-water:before{content:"\ec7a"}.bx-webcam:before{content:"\ec7b"}.bx-wifi:before{content:"\ec7c"}.bx-wifi-0:before{content:"\ec7d"}.bx-wifi-1:before{content:"\ec7e"}.bx-wifi-2:before{content:"\ec7f"}.bx-wifi-off:before{content:"\ec80"}.bx-wind:before{content:"\ec81"}.bx-window:before{content:"\ec82"}.bx-window-alt:before{content:"\ec83"}.bx-window-close:before{content:"\ec84"}.bx-window-open:before{content:"\ec85"}.bx-windows:before{content:"\ec86"}.bx-wine:before{content:"\ec87"}.bx-wink-smile:before{content:"\ec88"}.bx-wink-tongue:before{content:"\ec89"}.bx-won:before{content:"\ec8a"}.bx-world:before{content:"\ec8b"}.bx-wrench:before{content:"\ec8c"}.bx-x:before{content:"\ec8d"}.bx-x-circle:before{content:"\ec8e"}.bx-yen:before{content:"\ec8f"}.bx-zoom-in:before{content:"\ec90"}.bx-zoom-out:before{content:"\ec91"}.bxs-party:before{content:"\ec92"}.bxs-hot:before{content:"\ec93"}.bxs-droplet:before{content:"\ec94"}.bxs-cat:before{content:"\ec95"}.bxs-dog:before{content:"\ec96"}.bxs-injection:before{content:"\ec97"}.bxs-leaf:before{content:"\ec98"}.bxs-add-to-queue:before{content:"\ec99"}.bxs-adjust:before{content:"\ec9a"}.bxs-adjust-alt:before{content:"\ec9b"}.bxs-alarm:before{content:"\ec9c"}.bxs-alarm-add:before{content:"\ec9d"}.bxs-alarm-exclamation:before{content:"\ec9e"}.bxs-alarm-off:before{content:"\ec9f"}.bxs-alarm-snooze:before{content:"\eca0"}.bxs-album:before{content:"\eca1"}.bxs-ambulance:before{content:"\eca2"}.bxs-analyse:before{content:"\eca3"}.bxs-angry:before{content:"\eca4"}.bxs-arch:before{content:"\eca5"}.bxs-archive:before{content:"\eca6"}.bxs-archive-in:before{content:"\eca7"}.bxs-archive-out:before{content:"\eca8"}.bxs-area:before{content:"\eca9"}.bxs-arrow-from-bottom:before{content:"\ecaa"}.bxs-arrow-from-left:before{content:"\ecab"}.bxs-arrow-from-right:before{content:"\ecac"}.bxs-arrow-from-top:before{content:"\ecad"}.bxs-arrow-to-bottom:before{content:"\ecae"}.bxs-arrow-to-left:before{content:"\ecaf"}.bxs-arrow-to-right:before{content:"\ecb0"}.bxs-arrow-to-top:before{content:"\ecb1"}.bxs-award:before{content:"\ecb2"}.bxs-baby-carriage:before{content:"\ecb3"}.bxs-backpack:before{content:"\ecb4"}.bxs-badge:before{content:"\ecb5"}.bxs-badge-check:before{content:"\ecb6"}.bxs-badge-dollar:before{content:"\ecb7"}.bxs-ball:before{content:"\ecb8"}.bxs-band-aid:before{content:"\ecb9"}.bxs-bank:before{content:"\ecba"}.bxs-bar-chart-alt-2:before{content:"\ecbb"}.bxs-bar-chart-square:before{content:"\ecbc"}.bxs-barcode:before{content:"\ecbd"}.bxs-baseball:before{content:"\ecbe"}.bxs-basket:before{content:"\ecbf"}.bxs-basketball:before{content:"\ecc0"}.bxs-bath:before{content:"\ecc1"}.bxs-battery:before{content:"\ecc2"}.bxs-battery-charging:before{content:"\ecc3"}.bxs-battery-full:before{content:"\ecc4"}.bxs-battery-low:before{content:"\ecc5"}.bxs-bed:before{content:"\ecc6"}.bxs-been-here:before{content:"\ecc7"}.bxs-beer:before{content:"\ecc8"}.bxs-bell:before{content:"\ecc9"}.bxs-bell-minus:before{content:"\ecca"}.bxs-bell-off:before{content:"\eccb"}.bxs-bell-plus:before{content:"\eccc"}.bxs-bell-ring:before{content:"\eccd"}.bxs-bible:before{content:"\ecce"}.bxs-binoculars:before{content:"\eccf"}.bxs-blanket:before{content:"\ecd0"}.bxs-bolt:before{content:"\ecd1"}.bxs-bolt-circle:before{content:"\ecd2"}.bxs-bomb:before{content:"\ecd3"}.bxs-bone:before{content:"\ecd4"}.bxs-bong:before{content:"\ecd5"}.bxs-book:before{content:"\ecd6"}.bxs-book-add:before{content:"\ecd7"}.bxs-book-alt:before{content:"\ecd8"}.bxs-book-bookmark:before{content:"\ecd9"}.bxs-book-content:before{content:"\ecda"}.bxs-book-heart:before{content:"\ecdb"}.bxs-bookmark:before{content:"\ecdc"}.bxs-bookmark-alt:before{content:"\ecdd"}.bxs-bookmark-alt-minus:before{content:"\ecde"}.bxs-bookmark-alt-plus:before{content:"\ecdf"}.bxs-bookmark-heart:before{content:"\ece0"}.bxs-bookmark-minus:before{content:"\ece1"}.bxs-bookmark-plus:before{content:"\ece2"}.bxs-bookmarks:before{content:"\ece3"}.bxs-bookmark-star:before{content:"\ece4"}.bxs-book-open:before{content:"\ece5"}.bxs-book-reader:before{content:"\ece6"}.bxs-bot:before{content:"\ece7"}.bxs-bowling-ball:before{content:"\ece8"}.bxs-box:before{content:"\ece9"}.bxs-brain:before{content:"\ecea"}.bxs-briefcase:before{content:"\eceb"}.bxs-briefcase-alt:before{content:"\ecec"}.bxs-briefcase-alt-2:before{content:"\eced"}.bxs-brightness:before{content:"\ecee"}.bxs-brightness-half:before{content:"\ecef"}.bxs-brush:before{content:"\ecf0"}.bxs-brush-alt:before{content:"\ecf1"}.bxs-bug:before{content:"\ecf2"}.bxs-bug-alt:before{content:"\ecf3"}.bxs-building:before{content:"\ecf4"}.bxs-building-house:before{content:"\ecf5"}.bxs-buildings:before{content:"\ecf6"}.bxs-bulb:before{content:"\ecf7"}.bxs-bullseye:before{content:"\ecf8"}.bxs-buoy:before{content:"\ecf9"}.bxs-bus:before{content:"\ecfa"}.bxs-business:before{content:"\ecfb"}.bxs-bus-school:before{content:"\ecfc"}.bxs-cabinet:before{content:"\ecfd"}.bxs-cake:before{content:"\ecfe"}.bxs-calculator:before{content:"\ecff"}.bxs-calendar:before{content:"\ed00"}.bxs-calendar-alt:before{content:"\ed01"}.bxs-calendar-check:before{content:"\ed02"}.bxs-calendar-edit:before{content:"\ed03"}.bxs-calendar-event:before{content:"\ed04"}.bxs-calendar-exclamation:before{content:"\ed05"}.bxs-calendar-heart:before{content:"\ed06"}.bxs-calendar-minus:before{content:"\ed07"}.bxs-calendar-plus:before{content:"\ed08"}.bxs-calendar-star:before{content:"\ed09"}.bxs-calendar-week:before{content:"\ed0a"}.bxs-calendar-x:before{content:"\ed0b"}.bxs-camera:before{content:"\ed0c"}.bxs-camera-home:before{content:"\ed0d"}.bxs-camera-movie:before{content:"\ed0e"}.bxs-camera-off:before{content:"\ed0f"}.bxs-camera-plus:before{content:"\ed10"}.bxs-capsule:before{content:"\ed11"}.bxs-captions:before{content:"\ed12"}.bxs-car:before{content:"\ed13"}.bxs-car-battery:before{content:"\ed14"}.bxs-car-crash:before{content:"\ed15"}.bxs-card:before{content:"\ed16"}.bxs-caret-down-circle:before{content:"\ed17"}.bxs-caret-down-square:before{content:"\ed18"}.bxs-caret-left-circle:before{content:"\ed19"}.bxs-caret-left-square:before{content:"\ed1a"}.bxs-caret-right-circle:before{content:"\ed1b"}.bxs-caret-right-square:before{content:"\ed1c"}.bxs-caret-up-circle:before{content:"\ed1d"}.bxs-caret-up-square:before{content:"\ed1e"}.bxs-car-garage:before{content:"\ed1f"}.bxs-car-mechanic:before{content:"\ed20"}.bxs-carousel:before{content:"\ed21"}.bxs-cart:before{content:"\ed22"}.bxs-cart-add:before{content:"\ed23"}.bxs-cart-alt:before{content:"\ed24"}.bxs-cart-download:before{content:"\ed25"}.bxs-car-wash:before{content:"\ed26"}.bxs-category:before{content:"\ed27"}.bxs-category-alt:before{content:"\ed28"}.bxs-cctv:before{content:"\ed29"}.bxs-certification:before{content:"\ed2a"}.bxs-chalkboard:before{content:"\ed2b"}.bxs-chart:before{content:"\ed2c"}.bxs-chat:before{content:"\ed2d"}.bxs-checkbox:before{content:"\ed2e"}.bxs-checkbox-checked:before{content:"\ed2f"}.bxs-checkbox-minus:before{content:"\ed30"}.bxs-check-circle:before{content:"\ed31"}.bxs-check-shield:before{content:"\ed32"}.bxs-check-square:before{content:"\ed33"}.bxs-chess:before{content:"\ed34"}.bxs-chevron-down:before{content:"\ed35"}.bxs-chevron-down-circle:before{content:"\ed36"}.bxs-chevron-down-square:before{content:"\ed37"}.bxs-chevron-left:before{content:"\ed38"}.bxs-chevron-left-circle:before{content:"\ed39"}.bxs-chevron-left-square:before{content:"\ed3a"}.bxs-chevron-right:before{content:"\ed3b"}.bxs-chevron-right-circle:before{content:"\ed3c"}.bxs-chevron-right-square:before{content:"\ed3d"}.bxs-chevrons-down:before{content:"\ed3e"}.bxs-chevrons-left:before{content:"\ed3f"}.bxs-chevrons-right:before{content:"\ed40"}.bxs-chevrons-up:before{content:"\ed41"}.bxs-chevron-up:before{content:"\ed42"}.bxs-chevron-up-circle:before{content:"\ed43"}.bxs-chevron-up-square:before{content:"\ed44"}.bxs-chip:before{content:"\ed45"}.bxs-church:before{content:"\ed46"}.bxs-circle:before{content:"\ed47"}.bxs-city:before{content:"\ed48"}.bxs-clinic:before{content:"\ed49"}.bxs-cloud:before{content:"\ed4a"}.bxs-cloud-download:before{content:"\ed4b"}.bxs-cloud-lightning:before{content:"\ed4c"}.bxs-cloud-rain:before{content:"\ed4d"}.bxs-cloud-upload:before{content:"\ed4e"}.bxs-coffee:before{content:"\ed4f"}.bxs-coffee-alt:before{content:"\ed50"}.bxs-coffee-togo:before{content:"\ed51"}.bxs-cog:before{content:"\ed52"}.bxs-coin:before{content:"\ed53"}.bxs-coin-stack:before{content:"\ed54"}.bxs-collection:before{content:"\ed55"}.bxs-color-fill:before{content:"\ed56"}.bxs-comment:before{content:"\ed57"}.bxs-comment-add:before{content:"\ed58"}.bxs-comment-check:before{content:"\ed59"}.bxs-comment-detail:before{content:"\ed5a"}.bxs-comment-dots:before{content:"\ed5b"}.bxs-comment-edit:before{content:"\ed5c"}.bxs-comment-error:before{content:"\ed5d"}.bxs-comment-minus:before{content:"\ed5e"}.bxs-comment-x:before{content:"\ed5f"}.bxs-compass:before{content:"\ed60"}.bxs-component:before{content:"\ed61"}.bxs-confused:before{content:"\ed62"}.bxs-contact:before{content:"\ed63"}.bxs-conversation:before{content:"\ed64"}.bxs-cookie:before{content:"\ed65"}.bxs-cool:before{content:"\ed66"}.bxs-copy:before{content:"\ed67"}.bxs-copy-alt:before{content:"\ed68"}.bxs-copyright:before{content:"\ed69"}.bxs-coupon:before{content:"\ed6a"}.bxs-credit-card:before{content:"\ed6b"}.bxs-credit-card-alt:before{content:"\ed6c"}.bxs-credit-card-front:before{content:"\ed6d"}.bxs-crop:before{content:"\ed6e"}.bxs-crown:before{content:"\ed6f"}.bxs-cube:before{content:"\ed70"}.bxs-cube-alt:before{content:"\ed71"}.bxs-cuboid:before{content:"\ed72"}.bxs-customize:before{content:"\ed73"}.bxs-cylinder:before{content:"\ed74"}.bxs-dashboard:before{content:"\ed75"}.bxs-data:before{content:"\ed76"}.bxs-detail:before{content:"\ed77"}.bxs-devices:before{content:"\ed78"}.bxs-diamond:before{content:"\ed79"}.bxs-dice-1:before{content:"\ed7a"}.bxs-dice-2:before{content:"\ed7b"}.bxs-dice-3:before{content:"\ed7c"}.bxs-dice-4:before{content:"\ed7d"}.bxs-dice-5:before{content:"\ed7e"}.bxs-dice-6:before{content:"\ed7f"}.bxs-direction-left:before{content:"\ed80"}.bxs-direction-right:before{content:"\ed81"}.bxs-directions:before{content:"\ed82"}.bxs-disc:before{content:"\ed83"}.bxs-discount:before{content:"\ed84"}.bxs-dish:before{content:"\ed85"}.bxs-dislike:before{content:"\ed86"}.bxs-dizzy:before{content:"\ed87"}.bxs-dock-bottom:before{content:"\ed88"}.bxs-dock-left:before{content:"\ed89"}.bxs-dock-right:before{content:"\ed8a"}.bxs-dock-top:before{content:"\ed8b"}.bxs-dollar-circle:before{content:"\ed8c"}.bxs-donate-blood:before{content:"\ed8d"}.bxs-donate-heart:before{content:"\ed8e"}.bxs-door-open:before{content:"\ed8f"}.bxs-doughnut-chart:before{content:"\ed90"}.bxs-down-arrow:before{content:"\ed91"}.bxs-down-arrow-alt:before{content:"\ed92"}.bxs-down-arrow-circle:before{content:"\ed93"}.bxs-down-arrow-square:before{content:"\ed94"}.bxs-download:before{content:"\ed95"}.bxs-downvote:before{content:"\ed96"}.bxs-drink:before{content:"\ed97"}.bxs-droplet-half:before{content:"\ed98"}.bxs-dryer:before{content:"\ed99"}.bxs-duplicate:before{content:"\ed9a"}.bxs-edit:before{content:"\ed9b"}.bxs-edit-alt:before{content:"\ed9c"}.bxs-edit-location:before{content:"\ed9d"}.bxs-eject:before{content:"\ed9e"}.bxs-envelope:before{content:"\ed9f"}.bxs-envelope-open:before{content:"\eda0"}.bxs-eraser:before{content:"\eda1"}.bxs-error:before{content:"\eda2"}.bxs-error-alt:before{content:"\eda3"}.bxs-error-circle:before{content:"\eda4"}.bxs-ev-station:before{content:"\eda5"}.bxs-exit:before{content:"\eda6"}.bxs-extension:before{content:"\eda7"}.bxs-eyedropper:before{content:"\eda8"}.bxs-face:before{content:"\eda9"}.bxs-face-mask:before{content:"\edaa"}.bxs-factory:before{content:"\edab"}.bxs-fast-forward-circle:before{content:"\edac"}.bxs-file:before{content:"\edad"}.bxs-file-archive:before{content:"\edae"}.bxs-file-blank:before{content:"\edaf"}.bxs-file-css:before{content:"\edb0"}.bxs-file-doc:before{content:"\edb1"}.bxs-file-export:before{content:"\edb2"}.bxs-file-find:before{content:"\edb3"}.bxs-file-gif:before{content:"\edb4"}.bxs-file-html:before{content:"\edb5"}.bxs-file-image:before{content:"\edb6"}.bxs-file-import:before{content:"\edb7"}.bxs-file-jpg:before{content:"\edb8"}.bxs-file-js:before{content:"\edb9"}.bxs-file-json:before{content:"\edba"}.bxs-file-md:before{content:"\edbb"}.bxs-file-pdf:before{content:"\edbc"}.bxs-file-plus:before{content:"\edbd"}.bxs-file-png:before{content:"\edbe"}.bxs-file-txt:before{content:"\edbf"}.bxs-film:before{content:"\edc0"}.bxs-filter-alt:before{content:"\edc1"}.bxs-first-aid:before{content:"\edc2"}.bxs-flag:before{content:"\edc3"}.bxs-flag-alt:before{content:"\edc4"}.bxs-flag-checkered:before{content:"\edc5"}.bxs-flame:before{content:"\edc6"}.bxs-flask:before{content:"\edc7"}.bxs-florist:before{content:"\edc8"}.bxs-folder:before{content:"\edc9"}.bxs-folder-minus:before{content:"\edca"}.bxs-folder-open:before{content:"\edcb"}.bxs-folder-plus:before{content:"\edcc"}.bxs-food-menu:before{content:"\edcd"}.bxs-fridge:before{content:"\edce"}.bxs-game:before{content:"\edcf"}.bxs-gas-pump:before{content:"\edd0"}.bxs-ghost:before{content:"\edd1"}.bxs-gift:before{content:"\edd2"}.bxs-graduation:before{content:"\edd3"}.bxs-grid:before{content:"\edd4"}.bxs-grid-alt:before{content:"\edd5"}.bxs-group:before{content:"\edd6"}.bxs-guitar-amp:before{content:"\edd7"}.bxs-hand:before{content:"\edd8"}.bxs-hand-down:before{content:"\edd9"}.bxs-hand-left:before{content:"\edda"}.bxs-hand-right:before{content:"\eddb"}.bxs-hand-up:before{content:"\eddc"}.bxs-happy:before{content:"\eddd"}.bxs-happy-alt:before{content:"\edde"}.bxs-happy-beaming:before{content:"\eddf"}.bxs-happy-heart-eyes:before{content:"\ede0"}.bxs-hdd:before{content:"\ede1"}.bxs-heart:before{content:"\ede2"}.bxs-heart-circle:before{content:"\ede3"}.bxs-heart-square:before{content:"\ede4"}.bxs-help-circle:before{content:"\ede5"}.bxs-hide:before{content:"\ede6"}.bxs-home:before{content:"\ede7"}.bxs-home-circle:before{content:"\ede8"}.bxs-home-heart:before{content:"\ede9"}.bxs-home-smile:before{content:"\edea"}.bxs-hotel:before{content:"\edeb"}.bxs-hourglass:before{content:"\edec"}.bxs-hourglass-bottom:before{content:"\eded"}.bxs-hourglass-top:before{content:"\edee"}.bxs-id-card:before{content:"\edef"}.bxs-image:before{content:"\edf0"}.bxs-image-add:before{content:"\edf1"}.bxs-image-alt:before{content:"\edf2"}.bxs-inbox:before{content:"\edf3"}.bxs-info-circle:before{content:"\edf4"}.bxs-info-square:before{content:"\edf5"}.bxs-institution:before{content:"\edf6"}.bxs-joystick:before{content:"\edf7"}.bxs-joystick-alt:before{content:"\edf8"}.bxs-joystick-button:before{content:"\edf9"}.bxs-key:before{content:"\edfa"}.bxs-keyboard:before{content:"\edfb"}.bxs-label:before{content:"\edfc"}.bxs-landmark:before{content:"\edfd"}.bxs-landscape:before{content:"\edfe"}.bxs-laugh:before{content:"\edff"}.bxs-layer:before{content:"\ee00"}.bxs-layer-minus:before{content:"\ee01"}.bxs-layer-plus:before{content:"\ee02"}.bxs-layout:before{content:"\ee03"}.bxs-left-arrow:before{content:"\ee04"}.bxs-left-arrow-alt:before{content:"\ee05"}.bxs-left-arrow-circle:before{content:"\ee06"}.bxs-left-arrow-square:before{content:"\ee07"}.bxs-left-down-arrow-circle:before{content:"\ee08"}.bxs-left-top-arrow-circle:before{content:"\ee09"}.bxs-like:before{content:"\ee0a"}.bxs-location-plus:before{content:"\ee0b"}.bxs-lock:before{content:"\ee0c"}.bxs-lock-alt:before{content:"\ee0d"}.bxs-lock-open:before{content:"\ee0e"}.bxs-lock-open-alt:before{content:"\ee0f"}.bxs-log-in:before{content:"\ee10"}.bxs-log-in-circle:before{content:"\ee11"}.bxs-log-out:before{content:"\ee12"}.bxs-log-out-circle:before{content:"\ee13"}.bxs-low-vision:before{content:"\ee14"}.bxs-magic-wand:before{content:"\ee15"}.bxs-magnet:before{content:"\ee16"}.bxs-map:before{content:"\ee17"}.bxs-map-alt:before{content:"\ee18"}.bxs-map-pin:before{content:"\ee19"}.bxs-mask:before{content:"\ee1a"}.bxs-medal:before{content:"\ee1b"}.bxs-megaphone:before{content:"\ee1c"}.bxs-meh:before{content:"\ee1d"}.bxs-meh-alt:before{content:"\ee1e"}.bxs-meh-blank:before{content:"\ee1f"}.bxs-memory-card:before{content:"\ee20"}.bxs-message:before{content:"\ee21"}.bxs-message-add:before{content:"\ee22"}.bxs-message-alt:before{content:"\ee23"}.bxs-message-alt-add:before{content:"\ee24"}.bxs-message-alt-check:before{content:"\ee25"}.bxs-message-alt-detail:before{content:"\ee26"}.bxs-message-alt-dots:before{content:"\ee27"}.bxs-message-alt-edit:before{content:"\ee28"}.bxs-message-alt-error:before{content:"\ee29"}.bxs-message-alt-minus:before{content:"\ee2a"}.bxs-message-alt-x:before{content:"\ee2b"}.bxs-message-check:before{content:"\ee2c"}.bxs-message-detail:before{content:"\ee2d"}.bxs-message-dots:before{content:"\ee2e"}.bxs-message-edit:before{content:"\ee2f"}.bxs-message-error:before{content:"\ee30"}.bxs-message-minus:before{content:"\ee31"}.bxs-message-rounded:before{content:"\ee32"}.bxs-message-rounded-add:before{content:"\ee33"}.bxs-message-rounded-check:before{content:"\ee34"}.bxs-message-rounded-detail:before{content:"\ee35"}.bxs-message-rounded-dots:before{content:"\ee36"}.bxs-message-rounded-edit:before{content:"\ee37"}.bxs-message-rounded-error:before{content:"\ee38"}.bxs-message-rounded-minus:before{content:"\ee39"}.bxs-message-rounded-x:before{content:"\ee3a"}.bxs-message-square:before{content:"\ee3b"}.bxs-message-square-add:before{content:"\ee3c"}.bxs-message-square-check:before{content:"\ee3d"}.bxs-message-square-detail:before{content:"\ee3e"}.bxs-message-square-dots:before{content:"\ee3f"}.bxs-message-square-edit:before{content:"\ee40"}.bxs-message-square-error:before{content:"\ee41"}.bxs-message-square-minus:before{content:"\ee42"}.bxs-message-square-x:before{content:"\ee43"}.bxs-message-x:before{content:"\ee44"}.bxs-meteor:before{content:"\ee45"}.bxs-microchip:before{content:"\ee46"}.bxs-microphone:before{content:"\ee47"}.bxs-microphone-alt:before{content:"\ee48"}.bxs-microphone-off:before{content:"\ee49"}.bxs-minus-circle:before{content:"\ee4a"}.bxs-minus-square:before{content:"\ee4b"}.bxs-mobile:before{content:"\ee4c"}.bxs-mobile-vibration:before{content:"\ee4d"}.bxs-moon:before{content:"\ee4e"}.bxs-mouse:before{content:"\ee4f"}.bxs-mouse-alt:before{content:"\ee50"}.bxs-movie:before{content:"\ee51"}.bxs-movie-play:before{content:"\ee52"}.bxs-music:before{content:"\ee53"}.bxs-navigation:before{content:"\ee54"}.bxs-network-chart:before{content:"\ee55"}.bxs-news:before{content:"\ee56"}.bxs-no-entry:before{content:"\ee57"}.bxs-note:before{content:"\ee58"}.bxs-notepad:before{content:"\ee59"}.bxs-notification:before{content:"\ee5a"}.bxs-notification-off:before{content:"\ee5b"}.bxs-offer:before{content:"\ee5c"}.bxs-package:before{content:"\ee5d"}.bxs-paint:before{content:"\ee5e"}.bxs-paint-roll:before{content:"\ee5f"}.bxs-palette:before{content:"\ee60"}.bxs-paper-plane:before{content:"\ee61"}.bxs-parking:before{content:"\ee62"}.bxs-paste:before{content:"\ee63"}.bxs-pen:before{content:"\ee64"}.bxs-pencil:before{content:"\ee65"}.bxs-phone:before{content:"\ee66"}.bxs-phone-call:before{content:"\ee67"}.bxs-phone-incoming:before{content:"\ee68"}.bxs-phone-off:before{content:"\ee69"}.bxs-phone-outgoing:before{content:"\ee6a"}.bxs-photo-album:before{content:"\ee6b"}.bxs-piano:before{content:"\ee6c"}.bxs-pie-chart:before{content:"\ee6d"}.bxs-pie-chart-alt:before{content:"\ee6e"}.bxs-pie-chart-alt-2:before{content:"\ee6f"}.bxs-pin:before{content:"\ee70"}.bxs-pizza:before{content:"\ee71"}.bxs-plane:before{content:"\ee72"}.bxs-plane-alt:before{content:"\ee73"}.bxs-plane-land:before{content:"\ee74"}.bxs-planet:before{content:"\ee75"}.bxs-plane-take-off:before{content:"\ee76"}.bxs-playlist:before{content:"\ee77"}.bxs-plug:before{content:"\ee78"}.bxs-plus-circle:before{content:"\ee79"}.bxs-plus-square:before{content:"\ee7a"}.bxs-pointer:before{content:"\ee7b"}.bxs-polygon:before{content:"\ee7c"}.bxs-printer:before{content:"\ee7d"}.bxs-purchase-tag:before{content:"\ee7e"}.bxs-purchase-tag-alt:before{content:"\ee7f"}.bxs-pyramid:before{content:"\ee80"}.bxs-quote-alt-left:before{content:"\ee81"}.bxs-quote-alt-right:before{content:"\ee82"}.bxs-quote-left:before{content:"\ee83"}.bxs-quote-right:before{content:"\ee84"}.bxs-quote-single-left:before{content:"\ee85"}.bxs-quote-single-right:before{content:"\ee86"}.bxs-radiation:before{content:"\ee87"}.bxs-radio:before{content:"\ee88"}.bxs-receipt:before{content:"\ee89"}.bxs-rectangle:before{content:"\ee8a"}.bxs-registered:before{content:"\ee8b"}.bxs-rename:before{content:"\ee8c"}.bxs-report:before{content:"\ee8d"}.bxs-rewind-circle:before{content:"\ee8e"}.bxs-right-arrow:before{content:"\ee8f"}.bxs-right-arrow-alt:before{content:"\ee90"}.bxs-right-arrow-circle:before{content:"\ee91"}.bxs-right-arrow-square:before{content:"\ee92"}.bxs-right-down-arrow-circle:before{content:"\ee93"}.bxs-right-top-arrow-circle:before{content:"\ee94"}.bxs-rocket:before{content:"\ee95"}.bxs-ruler:before{content:"\ee96"}.bxs-sad:before{content:"\ee97"}.bxs-save:before{content:"\ee98"}.bxs-school:before{content:"\ee99"}.bxs-search:before{content:"\ee9a"}.bxs-search-alt-2:before{content:"\ee9b"}.bxs-select-multiple:before{content:"\ee9c"}.bxs-send:before{content:"\ee9d"}.bxs-server:before{content:"\ee9e"}.bxs-shapes:before{content:"\ee9f"}.bxs-share:before{content:"\eea0"}.bxs-share-alt:before{content:"\eea1"}.bxs-shield:before{content:"\eea2"}.bxs-shield-alt-2:before{content:"\eea3"}.bxs-shield-x:before{content:"\eea4"}.bxs-ship:before{content:"\eea5"}.bxs-shocked:before{content:"\eea6"}.bxs-shopping-bag:before{content:"\eea7"}.bxs-shopping-bag-alt:before{content:"\eea8"}.bxs-shopping-bags:before{content:"\eea9"}.bxs-show:before{content:"\eeaa"}.bxs-skip-next-circle:before{content:"\eeab"}.bxs-skip-previous-circle:before{content:"\eeac"}.bxs-skull:before{content:"\eead"}.bxs-sleepy:before{content:"\eeae"}.bxs-slideshow:before{content:"\eeaf"}.bxs-smile:before{content:"\eeb0"}.bxs-sort-alt:before{content:"\eeb1"}.bxs-spa:before{content:"\eeb2"}.bxs-speaker:before{content:"\eeb3"}.bxs-spray-can:before{content:"\eeb4"}.bxs-spreadsheet:before{content:"\eeb5"}.bxs-square:before{content:"\eeb6"}.bxs-square-rounded:before{content:"\eeb7"}.bxs-star:before{content:"\eeb8"}.bxs-star-half:before{content:"\eeb9"}.bxs-sticker:before{content:"\eeba"}.bxs-stopwatch:before{content:"\eebb"}.bxs-store:before{content:"\eebc"}.bxs-store-alt:before{content:"\eebd"}.bxs-sun:before{content:"\eebe"}.bxs-tachometer:before{content:"\eebf"}.bxs-tag:before{content:"\eec0"}.bxs-tag-alt:before{content:"\eec1"}.bxs-tag-x:before{content:"\eec2"}.bxs-taxi:before{content:"\eec3"}.bxs-tennis-ball:before{content:"\eec4"}.bxs-terminal:before{content:"\eec5"}.bxs-thermometer:before{content:"\eec6"}.bxs-time:before{content:"\eec7"}.bxs-time-five:before{content:"\eec8"}.bxs-timer:before{content:"\eec9"}.bxs-tired:before{content:"\eeca"}.bxs-toggle-left:before{content:"\eecb"}.bxs-toggle-right:before{content:"\eecc"}.bxs-tone:before{content:"\eecd"}.bxs-torch:before{content:"\eece"}.bxs-to-top:before{content:"\eecf"}.bxs-traffic:before{content:"\eed0"}.bxs-traffic-barrier:before{content:"\eed1"}.bxs-traffic-cone:before{content:"\eed2"}.bxs-train:before{content:"\eed3"}.bxs-trash:before{content:"\eed4"}.bxs-trash-alt:before{content:"\eed5"}.bxs-tree:before{content:"\eed6"}.bxs-trophy:before{content:"\eed7"}.bxs-truck:before{content:"\eed8"}.bxs-t-shirt:before{content:"\eed9"}.bxs-tv:before{content:"\eeda"}.bxs-up-arrow:before{content:"\eedb"}.bxs-up-arrow-alt:before{content:"\eedc"}.bxs-up-arrow-circle:before{content:"\eedd"}.bxs-up-arrow-square:before{content:"\eede"}.bxs-upside-down:before{content:"\eedf"}.bxs-upvote:before{content:"\eee0"}.bxs-user:before{content:"\eee1"}.bxs-user-account:before{content:"\eee2"}.bxs-user-badge:before{content:"\eee3"}.bxs-user-check:before{content:"\eee4"}.bxs-user-circle:before{content:"\eee5"}.bxs-user-detail:before{content:"\eee6"}.bxs-user-minus:before{content:"\eee7"}.bxs-user-pin:before{content:"\eee8"}.bxs-user-plus:before{content:"\eee9"}.bxs-user-rectangle:before{content:"\eeea"}.bxs-user-voice:before{content:"\eeeb"}.bxs-user-x:before{content:"\eeec"}.bxs-vector:before{content:"\eeed"}.bxs-vial:before{content:"\eeee"}.bxs-video:before{content:"\eeef"}.bxs-video-off:before{content:"\eef0"}.bxs-video-plus:before{content:"\eef1"}.bxs-video-recording:before{content:"\eef2"}.bxs-videos:before{content:"\eef3"}.bxs-virus:before{content:"\eef4"}.bxs-virus-block:before{content:"\eef5"}.bxs-volume:before{content:"\eef6"}.bxs-volume-full:before{content:"\eef7"}.bxs-volume-low:before{content:"\eef8"}.bxs-volume-mute:before{content:"\eef9"}.bxs-wallet:before{content:"\eefa"}.bxs-wallet-alt:before{content:"\eefb"}.bxs-washer:before{content:"\eefc"}.bxs-watch:before{content:"\eefd"}.bxs-watch-alt:before{content:"\eefe"}.bxs-webcam:before{content:"\eeff"}.bxs-widget:before{content:"\ef00"}.bxs-window-alt:before{content:"\ef01"}.bxs-wine:before{content:"\ef02"}.bxs-wink-smile:before{content:"\ef03"}.bxs-wink-tongue:before{content:"\ef04"}.bxs-wrench:before{content:"\ef05"}.bxs-x-circle:before{content:"\ef06"}.bxs-x-square:before{content:"\ef07"}.bxs-yin-yang:before{content:"\ef08"}.bxs-zap:before{content:"\ef09"}.bxs-zoom-in:before{content:"\ef0a"}.bxs-zoom-out:before{content:"\ef0b"}
|
static/assets/clutch-rating.png
ADDED
|
static/assets/clutch.png
ADDED
|
static/assets/good-firms.png
ADDED
|
static/assets/jarallax.min.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*!
|
| 2 |
+
* Jarallax v2.2.1 (https://github.com/nk-o/jarallax)
|
| 3 |
+
* Copyright 2024 nK <https://nkdev.info>
|
| 4 |
+
* Licensed under MIT (https://github.com/nk-o/jarallax/blob/master/LICENSE)
|
| 5 |
+
*/
|
| 6 |
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).jarallax=t()}(this,(function(){"use strict";function e(e){"complete"===document.readyState||"interactive"===document.readyState?e():document.addEventListener("DOMContentLoaded",e,{capture:!0,once:!0,passive:!0})}let t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};var i=t,o={type:"scroll",speed:.5,containerClass:"jarallax-container",imgSrc:null,imgElement:".jarallax-img",imgSize:"cover",imgPosition:"50% 50%",imgRepeat:"no-repeat",keepImg:!1,elementInViewport:null,zIndex:-100,disableParallax:!1,onScroll:null,onInit:null,onDestroy:null,onCoverImage:null,videoClass:"jarallax-video",videoSrc:null,videoStartTime:0,videoEndTime:0,videoVolume:0,videoLoop:!0,videoPlayOnlyVisible:!0,videoLazyLoading:!0,disableVideo:!1,onVideoInsert:null,onVideoWorkerInit:null};const{navigator:n}=i,a=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(n.userAgent);let s,l,r;function c(){s=i.innerWidth||document.documentElement.clientWidth,a?(!r&&document.body&&(r=document.createElement("div"),r.style.cssText="position: fixed; top: -9999px; left: 0; height: 100vh; width: 0;",document.body.appendChild(r)),l=(r?r.clientHeight:0)||i.innerHeight||document.documentElement.clientHeight):l=i.innerHeight||document.documentElement.clientHeight}function p(){return{width:s,height:l}}c(),i.addEventListener("resize",c),i.addEventListener("orientationchange",c),i.addEventListener("load",c),e((()=>{c()}));const m=[];function d(){if(!m.length)return;const{width:e,height:t}=p();m.forEach(((i,o)=>{const{instance:n,oldData:a}=i;if(!n.isVisible())return;const s=n.$item.getBoundingClientRect(),l={width:s.width,height:s.height,top:s.top,bottom:s.bottom,wndW:e,wndH:t},r=!a||a.wndW!==l.wndW||a.wndH!==l.wndH||a.width!==l.width||a.height!==l.height,c=r||!a||a.top!==l.top||a.bottom!==l.bottom;m[o].oldData=l,r&&n.onResize(),c&&n.onScroll()})),i.requestAnimationFrame(d)}const g=new i.IntersectionObserver((e=>{e.forEach((e=>{e.target.jarallax.isElementInViewport=e.isIntersecting}))}),{rootMargin:"50px"});const{navigator:u}=i;let f=0;class h{constructor(e,t){const i=this;i.instanceID=f,f+=1,i.$item=e,i.defaults={...o};const n=i.$item.dataset||{},a={};if(Object.keys(n).forEach((e=>{const t=e.substr(0,1).toLowerCase()+e.substr(1);t&&void 0!==i.defaults[t]&&(a[t]=n[e])})),i.options=i.extend({},i.defaults,a,t),i.pureOptions=i.extend({},i.options),Object.keys(i.options).forEach((e=>{"true"===i.options[e]?i.options[e]=!0:"false"===i.options[e]&&(i.options[e]=!1)})),i.options.speed=Math.min(2,Math.max(-1,parseFloat(i.options.speed))),"string"==typeof i.options.disableParallax&&(i.options.disableParallax=new RegExp(i.options.disableParallax)),i.options.disableParallax instanceof RegExp){const e=i.options.disableParallax;i.options.disableParallax=()=>e.test(u.userAgent)}if("function"!=typeof i.options.disableParallax){const e=i.options.disableParallax;i.options.disableParallax=()=>!0===e}if("string"==typeof i.options.disableVideo&&(i.options.disableVideo=new RegExp(i.options.disableVideo)),i.options.disableVideo instanceof RegExp){const e=i.options.disableVideo;i.options.disableVideo=()=>e.test(u.userAgent)}if("function"!=typeof i.options.disableVideo){const e=i.options.disableVideo;i.options.disableVideo=()=>!0===e}let s=i.options.elementInViewport;s&&"object"==typeof s&&void 0!==s.length&&([s]=s),s instanceof Element||(s=null),i.options.elementInViewport=s,i.image={src:i.options.imgSrc||null,$container:null,useImgTag:!1,position:"fixed"},i.initImg()&&i.canInitParallax()&&i.init()}css(e,t){return function(e,t){return"string"==typeof t?i.getComputedStyle(e).getPropertyValue(t):(Object.keys(t).forEach((i=>{e.style[i]=t[i]})),e)}(e,t)}extend(e,...t){return function(e,...t){return e=e||{},Object.keys(t).forEach((i=>{t[i]&&Object.keys(t[i]).forEach((o=>{e[o]=t[i][o]}))})),e}(e,...t)}getWindowData(){const{width:e,height:t}=p();return{width:e,height:t,y:document.documentElement.scrollTop}}initImg(){const e=this;let t=e.options.imgElement;return t&&"string"==typeof t&&(t=e.$item.querySelector(t)),t instanceof Element||(e.options.imgSrc?(t=new Image,t.src=e.options.imgSrc):t=null),t&&(e.options.keepImg?e.image.$item=t.cloneNode(!0):(e.image.$item=t,e.image.$itemParent=t.parentNode),e.image.useImgTag=!0),!!e.image.$item||(null===e.image.src&&(e.image.src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",e.image.bgImage=e.css(e.$item,"background-image")),!(!e.image.bgImage||"none"===e.image.bgImage))}canInitParallax(){return!this.options.disableParallax()}init(){const e=this,t={position:"absolute",top:0,left:0,width:"100%",height:"100%",overflow:"hidden"};let o={pointerEvents:"none",transformStyle:"preserve-3d",backfaceVisibility:"hidden"};if(!e.options.keepImg){const t=e.$item.getAttribute("style");if(t&&e.$item.setAttribute("data-jarallax-original-styles",t),e.image.useImgTag){const t=e.image.$item.getAttribute("style");t&&e.image.$item.setAttribute("data-jarallax-original-styles",t)}}if("static"===e.css(e.$item,"position")&&e.css(e.$item,{position:"relative"}),"auto"===e.css(e.$item,"z-index")&&e.css(e.$item,{zIndex:0}),e.image.$container=document.createElement("div"),e.css(e.image.$container,t),e.css(e.image.$container,{"z-index":e.options.zIndex}),"fixed"===this.image.position&&e.css(e.image.$container,{"-webkit-clip-path":"polygon(0 0, 100% 0, 100% 100%, 0 100%)","clip-path":"polygon(0 0, 100% 0, 100% 100%, 0 100%)"}),e.image.$container.setAttribute("id",`jarallax-container-${e.instanceID}`),e.options.containerClass&&e.image.$container.setAttribute("class",e.options.containerClass),e.$item.appendChild(e.image.$container),e.image.useImgTag?o=e.extend({"object-fit":e.options.imgSize,"object-position":e.options.imgPosition,"max-width":"none"},t,o):(e.image.$item=document.createElement("div"),e.image.src&&(o=e.extend({"background-position":e.options.imgPosition,"background-size":e.options.imgSize,"background-repeat":e.options.imgRepeat,"background-image":e.image.bgImage||`url("${e.image.src}")`},t,o))),"opacity"!==e.options.type&&"scale"!==e.options.type&&"scale-opacity"!==e.options.type&&1!==e.options.speed||(e.image.position="absolute"),"fixed"===e.image.position){const t=function(e){const t=[];for(;null!==e.parentElement;)1===(e=e.parentElement).nodeType&&t.push(e);return t}(e.$item).filter((e=>{const t=i.getComputedStyle(e),o=t["-webkit-transform"]||t["-moz-transform"]||t.transform;return o&&"none"!==o||/(auto|scroll)/.test(t.overflow+t["overflow-y"]+t["overflow-x"])}));e.image.position=t.length?"absolute":"fixed"}var n;o.position=e.image.position,e.css(e.image.$item,o),e.image.$container.appendChild(e.image.$item),e.onResize(),e.onScroll(!0),e.options.onInit&&e.options.onInit.call(e),"none"!==e.css(e.$item,"background-image")&&e.css(e.$item,{"background-image":"none"}),n=e,m.push({instance:n}),1===m.length&&i.requestAnimationFrame(d),g.observe(n.options.elementInViewport||n.$item)}destroy(){const e=this;var t;t=e,m.forEach(((e,i)=>{e.instance.instanceID===t.instanceID&&m.splice(i,1)})),g.unobserve(t.options.elementInViewport||t.$item);const i=e.$item.getAttribute("data-jarallax-original-styles");if(e.$item.removeAttribute("data-jarallax-original-styles"),i?e.$item.setAttribute("style",i):e.$item.removeAttribute("style"),e.image.useImgTag){const t=e.image.$item.getAttribute("data-jarallax-original-styles");e.image.$item.removeAttribute("data-jarallax-original-styles"),t?e.image.$item.setAttribute("style",i):e.image.$item.removeAttribute("style"),e.image.$itemParent&&e.image.$itemParent.appendChild(e.image.$item)}e.image.$container&&e.image.$container.parentNode.removeChild(e.image.$container),e.options.onDestroy&&e.options.onDestroy.call(e),delete e.$item.jarallax}coverImage(){const e=this,{height:t}=p(),i=e.image.$container.getBoundingClientRect(),o=i.height,{speed:n}=e.options,a="scroll"===e.options.type||"scroll-opacity"===e.options.type;let s=0,l=o,r=0;return a&&(n<0?(s=n*Math.max(o,t),t<o&&(s-=n*(o-t))):s=n*(o+t),n>1?l=Math.abs(s-t):n<0?l=s/n+Math.abs(s):l+=(t-o)*(1-n),s/=2),e.parallaxScrollDistance=s,r=a?(t-l)/2:(o-l)/2,e.css(e.image.$item,{height:`${l}px`,marginTop:`${r}px`,left:"fixed"===e.image.position?`${i.left}px`:"0",width:`${i.width}px`}),e.options.onCoverImage&&e.options.onCoverImage.call(e),{image:{height:l,marginTop:r},container:i}}isVisible(){return this.isElementInViewport||!1}onScroll(e){const t=this;if(!e&&!t.isVisible())return;const{height:i}=p(),o=t.$item.getBoundingClientRect(),n=o.top,a=o.height,s={},l=Math.max(0,n),r=Math.max(0,a+n),c=Math.max(0,-n),m=Math.max(0,n+a-i),d=Math.max(0,a-(n+a-i)),g=Math.max(0,-n+i-a),u=1-(i-n)/(i+a)*2;let f=1;if(a<i?f=1-(c||m)/a:r<=i?f=r/i:d<=i&&(f=d/i),"opacity"!==t.options.type&&"scale-opacity"!==t.options.type&&"scroll-opacity"!==t.options.type||(s.transform="translate3d(0,0,0)",s.opacity=f),"scale"===t.options.type||"scale-opacity"===t.options.type){let e=1;t.options.speed<0?e-=t.options.speed*f:e+=t.options.speed*(1-f),s.transform=`scale(${e}) translate3d(0,0,0)`}if("scroll"===t.options.type||"scroll-opacity"===t.options.type){let e=t.parallaxScrollDistance*u;"absolute"===t.image.position&&(e-=n),s.transform=`translate3d(0,${e}px,0)`}t.css(t.image.$item,s),t.options.onScroll&&t.options.onScroll.call(t,{section:o,beforeTop:l,beforeTopEnd:r,afterTop:c,beforeBottom:m,beforeBottomEnd:d,afterBottom:g,visiblePercent:f,fromViewportCenter:u})}onResize(){this.coverImage()}}const b=function(e,t,...i){("object"==typeof HTMLElement?e instanceof HTMLElement:e&&"object"==typeof e&&null!==e&&1===e.nodeType&&"string"==typeof e.nodeName)&&(e=[e]);const o=e.length;let n,a=0;for(;a<o;a+=1)if("object"==typeof t||void 0===t?e[a].jarallax||(e[a].jarallax=new h(e[a],t)):e[a].jarallax&&(n=e[a].jarallax[t].apply(e[a].jarallax,i)),void 0!==n)return n;return e};b.constructor=h;const y=i.jQuery;if(void 0!==y){const e=function(...e){Array.prototype.unshift.call(e,this);const t=b.apply(i,e);return"object"!=typeof t?t:this};e.constructor=b.constructor;const t=y.fn.jarallax;y.fn.jarallax=e,y.fn.jarallax.noConflict=function(){return y.fn.jarallax=t,this}}return e((()=>{b(document.querySelectorAll("[data-jarallax]"))})),b}));//# sourceMappingURL=jarallax.min.js.map
|
static/assets/java.png
ADDED
|
static/assets/landings.jpg
ADDED
|
static/assets/logo.svg
ADDED
|
|
static/assets/node-dark.png
ADDED
|
static/assets/node-light.png
ADDED
|
static/assets/product-hunt.png
ADDED
|
static/assets/react.png
ADDED
|
static/assets/rellax.min.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
(function(q,g){"function"===typeof define&&define.amd?define([],g):"object"===typeof module&&module.exports?module.exports=g():q.Rellax=g()})("undefined"!==typeof window?window:global,function(){var q=function(g,u){function C(){if(3===a.options.breakpoints.length&&Array.isArray(a.options.breakpoints)){var f=!0,c=!0,b;a.options.breakpoints.forEach(function(a){"number"!==typeof a&&(c=!1);null!==b&&a<b&&(f=!1);b=a});if(f&&c)return}a.options.breakpoints=[576,768,1201];console.warn("Rellax: You must pass an array of 3 numbers in ascending order to the breakpoints option. Defaults reverted")}
|
| 2 |
+
var a=Object.create(q.prototype),l=0,v=0,m=0,n=0,d=[],w=!0,A=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame||window.oRequestAnimationFrame||function(a){return setTimeout(a,1E3/60)},p=null,x=!1;try{var k=Object.defineProperty({},"passive",{get:function(){x=!0}});window.addEventListener("testPassive",null,k);window.removeEventListener("testPassive",null,k)}catch(f){}var D=window.cancelAnimationFrame||window.mozCancelAnimationFrame||
|
| 3 |
+
clearTimeout,E=window.transformProp||function(){var a=document.createElement("div");if(null===a.style.transform){var c=["Webkit","Moz","ms"],b;for(b in c)if(void 0!==a.style[c[b]+"Transform"])return c[b]+"Transform"}return"transform"}();a.options={speed:-2,verticalSpeed:null,horizontalSpeed:null,breakpoints:[576,768,1201],center:!1,wrapper:null,relativeToWrapper:!1,round:!0,vertical:!0,horizontal:!1,verticalScrollAxis:"y",horizontalScrollAxis:"x",callback:function(){}};u&&Object.keys(u).forEach(function(d){a.options[d]=
|
| 4 |
+
u[d]});u&&u.breakpoints&&C();g||(g=".rellax");k="string"===typeof g?document.querySelectorAll(g):[g];if(0<k.length){a.elems=k;if(a.options.wrapper&&!a.options.wrapper.nodeType)if(k=document.querySelector(a.options.wrapper))a.options.wrapper=k;else{console.warn("Rellax: The wrapper you're trying to use doesn't exist.");return}var F,B=function(){for(var f=0;f<d.length;f++)a.elems[f].style.cssText=d[f].style;d=[];v=window.innerHeight;n=window.innerWidth;f=a.options.breakpoints;F=n<f[0]?"xs":n>=f[0]&&n<
|
| 5 |
+
f[1]?"sm":n>=f[1]&&n<f[2]?"md":"lg";H();for(f=0;f<a.elems.length;f++){var c=void 0,b=a.elems[f],e=b.getAttribute("data-rellax-percentage"),y=b.getAttribute("data-rellax-speed"),t=b.getAttribute("data-rellax-xs-speed"),g=b.getAttribute("data-rellax-mobile-speed"),h=b.getAttribute("data-rellax-tablet-speed"),k=b.getAttribute("data-rellax-desktop-speed"),l=b.getAttribute("data-rellax-vertical-speed"),m=b.getAttribute("data-rellax-horizontal-speed"),p=b.getAttribute("data-rellax-vertical-scroll-axis"),
|
| 6 |
+
q=b.getAttribute("data-rellax-horizontal-scroll-axis"),u=b.getAttribute("data-rellax-zindex")||0,x=b.getAttribute("data-rellax-min"),A=b.getAttribute("data-rellax-max"),C=b.getAttribute("data-rellax-min-x"),D=b.getAttribute("data-rellax-max-x"),E=b.getAttribute("data-rellax-min-y"),L=b.getAttribute("data-rellax-max-y"),r=!0;t||g||h||k?c={xs:t,sm:g,md:h,lg:k}:r=!1;t=a.options.wrapper?a.options.wrapper.scrollTop:window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop;a.options.relativeToWrapper&&
|
| 7 |
+
(t=(window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop)-a.options.wrapper.offsetTop);var z=a.options.vertical?e||a.options.center?t:0:0,I=a.options.horizontal?e||a.options.center?a.options.wrapper?a.options.wrapper.scrollLeft:window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft:0:0;t=z+b.getBoundingClientRect().top;g=b.clientHeight||b.offsetHeight||b.scrollHeight;h=I+b.getBoundingClientRect().left;k=b.clientWidth||b.offsetWidth||b.scrollWidth;
|
| 8 |
+
z=e?e:(z-t+v)/(g+v);e=e?e:(I-h+n)/(k+n);a.options.center&&(z=e=.5);c=r&&null!==c[F]?Number(c[F]):y?y:a.options.speed;l=l?l:a.options.verticalSpeed;m=m?m:a.options.horizontalSpeed;p=p?p:a.options.verticalScrollAxis;q=q?q:a.options.horizontalScrollAxis;y=J(e,z,c,l,m);b=b.style.cssText;r="";if(e=/transform\s*:/i.exec(b))r=b.slice(e.index),r=(e=r.indexOf(";"))?" "+r.slice(11,e).replace(/\s/g,""):" "+r.slice(11).replace(/\s/g,"");d.push({baseX:y.x,baseY:y.y,top:t,left:h,height:g,width:k,speed:c,verticalSpeed:l,
|
| 9 |
+
horizontalSpeed:m,verticalScrollAxis:p,horizontalScrollAxis:q,style:b,transform:r,zindex:u,min:x,max:A,minX:C,maxX:D,minY:E,maxY:L})}K();w&&(window.addEventListener("resize",B),w=!1,G())},H=function(){var d=l,c=m;l=a.options.wrapper?a.options.wrapper.scrollTop:(document.documentElement||document.body.parentNode||document.body).scrollTop||window.pageYOffset;m=a.options.wrapper?a.options.wrapper.scrollLeft:(document.documentElement||document.body.parentNode||document.body).scrollLeft||window.pageXOffset;
|
| 10 |
+
a.options.relativeToWrapper&&(l=((document.documentElement||document.body.parentNode||document.body).scrollTop||window.pageYOffset)-a.options.wrapper.offsetTop);return d!=l&&a.options.vertical||c!=m&&a.options.horizontal?!0:!1},J=function(d,c,b,e,g){var f={};d=100*(g?g:b)*(1-d);c=100*(e?e:b)*(1-c);f.x=a.options.round?Math.round(d):Math.round(100*d)/100;f.y=a.options.round?Math.round(c):Math.round(100*c)/100;return f},h=function(){window.removeEventListener("resize",h);window.removeEventListener("orientationchange",
|
| 11 |
+
h);(a.options.wrapper?a.options.wrapper:window).removeEventListener("scroll",h);(a.options.wrapper?a.options.wrapper:document).removeEventListener("touchmove",h);p=A(G)},G=function(){H()&&!1===w?(K(),p=A(G)):(p=null,window.addEventListener("resize",h),window.addEventListener("orientationchange",h),(a.options.wrapper?a.options.wrapper:window).addEventListener("scroll",h,x?{passive:!0}:!1),(a.options.wrapper?a.options.wrapper:document).addEventListener("touchmove",h,x?{passive:!0}:!1))},K=function(){for(var f,
|
| 12 |
+
c=0;c<a.elems.length;c++){var b=d[c].verticalScrollAxis.toLowerCase(),e=d[c].horizontalScrollAxis.toLowerCase();f=-1!=b.indexOf("x")?l:0;b=-1!=b.indexOf("y")?l:0;var g=-1!=e.indexOf("x")?m:0;e=-1!=e.indexOf("y")?m:0;f=J((f+g-d[c].left+n)/(d[c].width+n),(b+e-d[c].top+v)/(d[c].height+v),d[c].speed,d[c].verticalSpeed,d[c].horizontalSpeed);e=f.y-d[c].baseY;b=f.x-d[c].baseX;null!==d[c].min&&(a.options.vertical&&!a.options.horizontal&&(e=e<=d[c].min?d[c].min:e),a.options.horizontal&&!a.options.vertical&&
|
| 13 |
+
(b=b<=d[c].min?d[c].min:b));null!=d[c].minY&&(e=e<=d[c].minY?d[c].minY:e);null!=d[c].minX&&(b=b<=d[c].minX?d[c].minX:b);null!==d[c].max&&(a.options.vertical&&!a.options.horizontal&&(e=e>=d[c].max?d[c].max:e),a.options.horizontal&&!a.options.vertical&&(b=b>=d[c].max?d[c].max:b));null!=d[c].maxY&&(e=e>=d[c].maxY?d[c].maxY:e);null!=d[c].maxX&&(b=b>=d[c].maxX?d[c].maxX:b);a.elems[c].style[E]="translate3d("+(a.options.horizontal?b:"0")+"px,"+(a.options.vertical?e:"0")+"px,"+d[c].zindex+"px) "+d[c].transform}a.options.callback(f)};
|
| 14 |
+
a.destroy=function(){for(var f=0;f<a.elems.length;f++)a.elems[f].style.cssText=d[f].style;w||(window.removeEventListener("resize",B),w=!0);D(p);p=null};B();a.refresh=B;return a}console.warn("Rellax: The elements you're trying to select don't exist.")};return q});
|
static/assets/swiper-bundle.min.css
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Swiper 11.2.6
|
| 3 |
+
* Most modern mobile touch slider and framework with hardware accelerated transitions
|
| 4 |
+
* https://swiperjs.com
|
| 5 |
+
*
|
| 6 |
+
* Copyright 2014-2025 Vladimir Kharlampidi
|
| 7 |
+
*
|
| 8 |
+
* Released under the MIT License
|
| 9 |
+
*
|
| 10 |
+
* Released on: March 19, 2025
|
| 11 |
+
*/
|
| 12 |
+
|
| 13 |
+
@font-face{font-family:swiper-icons;src:url('data:application/font-woff;charset=utf-8;base64, d09GRgABAAAAAAZgABAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAGRAAAABoAAAAci6qHkUdERUYAAAWgAAAAIwAAACQAYABXR1BPUwAABhQAAAAuAAAANuAY7+xHU1VCAAAFxAAAAFAAAABm2fPczU9TLzIAAAHcAAAASgAAAGBP9V5RY21hcAAAAkQAAACIAAABYt6F0cBjdnQgAAACzAAAAAQAAAAEABEBRGdhc3AAAAWYAAAACAAAAAj//wADZ2x5ZgAAAywAAADMAAAD2MHtryVoZWFkAAABbAAAADAAAAA2E2+eoWhoZWEAAAGcAAAAHwAAACQC9gDzaG10eAAAAigAAAAZAAAArgJkABFsb2NhAAAC0AAAAFoAAABaFQAUGG1heHAAAAG8AAAAHwAAACAAcABAbmFtZQAAA/gAAAE5AAACXvFdBwlwb3N0AAAFNAAAAGIAAACE5s74hXjaY2BkYGAAYpf5Hu/j+W2+MnAzMYDAzaX6QjD6/4//Bxj5GA8AuRwMYGkAPywL13jaY2BkYGA88P8Agx4j+/8fQDYfA1AEBWgDAIB2BOoAeNpjYGRgYNBh4GdgYgABEMnIABJzYNADCQAACWgAsQB42mNgYfzCOIGBlYGB0YcxjYGBwR1Kf2WQZGhhYGBiYGVmgAFGBiQQkOaawtDAoMBQxXjg/wEGPcYDDA4wNUA2CCgwsAAAO4EL6gAAeNpj2M0gyAACqxgGNWBkZ2D4/wMA+xkDdgAAAHjaY2BgYGaAYBkGRgYQiAHyGMF8FgYHIM3DwMHABGQrMOgyWDLEM1T9/w8UBfEMgLzE////P/5//f/V/xv+r4eaAAeMbAxwIUYmIMHEgKYAYjUcsDAwsLKxc3BycfPw8jEQA/gZBASFhEVExcQlJKWkZWTl5BUUlZRVVNXUNTQZBgMAAMR+E+gAEQFEAAAAKgAqACoANAA+AEgAUgBcAGYAcAB6AIQAjgCYAKIArAC2AMAAygDUAN4A6ADyAPwBBgEQARoBJAEuATgBQgFMAVYBYAFqAXQBfgGIAZIBnAGmAbIBzgHsAAB42u2NMQ6CUAyGW568x9AneYYgm4MJbhKFaExIOAVX8ApewSt4Bic4AfeAid3VOBixDxfPYEza5O+Xfi04YADggiUIULCuEJK8VhO4bSvpdnktHI5QCYtdi2sl8ZnXaHlqUrNKzdKcT8cjlq+rwZSvIVczNiezsfnP/uznmfPFBNODM2K7MTQ45YEAZqGP81AmGGcF3iPqOop0r1SPTaTbVkfUe4HXj97wYE+yNwWYxwWu4v1ugWHgo3S1XdZEVqWM7ET0cfnLGxWfkgR42o2PvWrDMBSFj/IHLaF0zKjRgdiVMwScNRAoWUoH78Y2icB/yIY09An6AH2Bdu/UB+yxopYshQiEvnvu0dURgDt8QeC8PDw7Fpji3fEA4z/PEJ6YOB5hKh4dj3EvXhxPqH/SKUY3rJ7srZ4FZnh1PMAtPhwP6fl2PMJMPDgeQ4rY8YT6Gzao0eAEA409DuggmTnFnOcSCiEiLMgxCiTI6Cq5DZUd3Qmp10vO0LaLTd2cjN4fOumlc7lUYbSQcZFkutRG7g6JKZKy0RmdLY680CDnEJ+UMkpFFe1RN7nxdVpXrC4aTtnaurOnYercZg2YVmLN/d/gczfEimrE/fs/bOuq29Zmn8tloORaXgZgGa78yO9/cnXm2BpaGvq25Dv9S4E9+5SIc9PqupJKhYFSSl47+Qcr1mYNAAAAeNptw0cKwkAAAMDZJA8Q7OUJvkLsPfZ6zFVERPy8qHh2YER+3i/BP83vIBLLySsoKimrqKqpa2hp6+jq6RsYGhmbmJqZSy0sraxtbO3sHRydnEMU4uR6yx7JJXveP7WrDycAAAAAAAH//wACeNpjYGRgYOABYhkgZgJCZgZNBkYGLQZtIJsFLMYAAAw3ALgAeNolizEKgDAQBCchRbC2sFER0YD6qVQiBCv/H9ezGI6Z5XBAw8CBK/m5iQQVauVbXLnOrMZv2oLdKFa8Pjuru2hJzGabmOSLzNMzvutpB3N42mNgZGBg4GKQYzBhYMxJLMlj4GBgAYow/P/PAJJhLM6sSoWKfWCAAwDAjgbRAAB42mNgYGBkAIIbCZo5IPrmUn0hGA0AO8EFTQAA');font-weight:400;font-style:normal}:root{--swiper-theme-color:#007aff}:host{position:relative;display:block;margin-left:auto;margin-right:auto;z-index:1}.swiper{margin-left:auto;margin-right:auto;position:relative;overflow:hidden;list-style:none;padding:0;z-index:1;display:block}.swiper-vertical>.swiper-wrapper{flex-direction:column}.swiper-wrapper{position:relative;width:100%;height:100%;z-index:1;display:flex;transition-property:transform;transition-timing-function:var(--swiper-wrapper-transition-timing-function,initial);box-sizing:content-box}.swiper-android .swiper-slide,.swiper-ios .swiper-slide,.swiper-wrapper{transform:translate3d(0px,0,0)}.swiper-horizontal{touch-action:pan-y}.swiper-vertical{touch-action:pan-x}.swiper-slide{flex-shrink:0;width:100%;height:100%;position:relative;transition-property:transform;display:block}.swiper-slide-invisible-blank{visibility:hidden}.swiper-autoheight,.swiper-autoheight .swiper-slide{height:auto}.swiper-autoheight .swiper-wrapper{align-items:flex-start;transition-property:transform,height}.swiper-backface-hidden .swiper-slide{transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper-3d.swiper-css-mode .swiper-wrapper{perspective:1200px}.swiper-3d .swiper-wrapper{transform-style:preserve-3d}.swiper-3d{perspective:1200px}.swiper-3d .swiper-cube-shadow,.swiper-3d .swiper-slide{transform-style:preserve-3d}.swiper-css-mode>.swiper-wrapper{overflow:auto;scrollbar-width:none;-ms-overflow-style:none}.swiper-css-mode>.swiper-wrapper::-webkit-scrollbar{display:none}.swiper-css-mode>.swiper-wrapper>.swiper-slide{scroll-snap-align:start start}.swiper-css-mode.swiper-horizontal>.swiper-wrapper{scroll-snap-type:x mandatory}.swiper-css-mode.swiper-vertical>.swiper-wrapper{scroll-snap-type:y mandatory}.swiper-css-mode.swiper-free-mode>.swiper-wrapper{scroll-snap-type:none}.swiper-css-mode.swiper-free-mode>.swiper-wrapper>.swiper-slide{scroll-snap-align:none}.swiper-css-mode.swiper-centered>.swiper-wrapper::before{content:'';flex-shrink:0;order:9999}.swiper-css-mode.swiper-centered>.swiper-wrapper>.swiper-slide{scroll-snap-align:center center;scroll-snap-stop:always}.swiper-css-mode.swiper-centered.swiper-horizontal>.swiper-wrapper>.swiper-slide:first-child{margin-inline-start:var(--swiper-centered-offset-before)}.swiper-css-mode.swiper-centered.swiper-horizontal>.swiper-wrapper::before{height:100%;min-height:1px;width:var(--swiper-centered-offset-after)}.swiper-css-mode.swiper-centered.swiper-vertical>.swiper-wrapper>.swiper-slide:first-child{margin-block-start:var(--swiper-centered-offset-before)}.swiper-css-mode.swiper-centered.swiper-vertical>.swiper-wrapper::before{width:100%;min-width:1px;height:var(--swiper-centered-offset-after)}.swiper-3d .swiper-slide-shadow,.swiper-3d .swiper-slide-shadow-bottom,.swiper-3d .swiper-slide-shadow-left,.swiper-3d .swiper-slide-shadow-right,.swiper-3d .swiper-slide-shadow-top{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;z-index:10}.swiper-3d .swiper-slide-shadow{background:rgba(0,0,0,.15)}.swiper-3d .swiper-slide-shadow-left{background-image:linear-gradient(to left,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-right{background-image:linear-gradient(to right,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-top{background-image:linear-gradient(to top,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-bottom{background-image:linear-gradient(to bottom,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-lazy-preloader{width:42px;height:42px;position:absolute;left:50%;top:50%;margin-left:-21px;margin-top:-21px;z-index:10;transform-origin:50%;box-sizing:border-box;border:4px solid var(--swiper-preloader-color,var(--swiper-theme-color));border-radius:50%;border-top-color:transparent}.swiper-watch-progress .swiper-slide-visible .swiper-lazy-preloader,.swiper:not(.swiper-watch-progress) .swiper-lazy-preloader{animation:swiper-preloader-spin 1s infinite linear}.swiper-lazy-preloader-white{--swiper-preloader-color:#fff}.swiper-lazy-preloader-black{--swiper-preloader-color:#000}@keyframes swiper-preloader-spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.swiper-virtual .swiper-slide{-webkit-backface-visibility:hidden;transform:translateZ(0)}.swiper-virtual.swiper-css-mode .swiper-wrapper::after{content:'';position:absolute;left:0;top:0;pointer-events:none}.swiper-virtual.swiper-css-mode.swiper-horizontal .swiper-wrapper::after{height:1px;width:var(--swiper-virtual-size)}.swiper-virtual.swiper-css-mode.swiper-vertical .swiper-wrapper::after{width:1px;height:var(--swiper-virtual-size)}:root{--swiper-navigation-size:44px}.swiper-button-next,.swiper-button-prev{position:absolute;top:var(--swiper-navigation-top-offset,50%);width:calc(var(--swiper-navigation-size)/ 44 * 27);height:var(--swiper-navigation-size);margin-top:calc(0px - (var(--swiper-navigation-size)/ 2));z-index:10;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--swiper-navigation-color,var(--swiper-theme-color))}.swiper-button-next.swiper-button-disabled,.swiper-button-prev.swiper-button-disabled{opacity:.35;cursor:auto;pointer-events:none}.swiper-button-next.swiper-button-hidden,.swiper-button-prev.swiper-button-hidden{opacity:0;cursor:auto;pointer-events:none}.swiper-navigation-disabled .swiper-button-next,.swiper-navigation-disabled .swiper-button-prev{display:none!important}.swiper-button-next svg,.swiper-button-prev svg{width:100%;height:100%;object-fit:contain;transform-origin:center}.swiper-rtl .swiper-button-next svg,.swiper-rtl .swiper-button-prev svg{transform:rotate(180deg)}.swiper-button-prev,.swiper-rtl .swiper-button-next{left:var(--swiper-navigation-sides-offset,10px);right:auto}.swiper-button-next,.swiper-rtl .swiper-button-prev{right:var(--swiper-navigation-sides-offset,10px);left:auto}.swiper-button-lock{display:none}.swiper-button-next:after,.swiper-button-prev:after{font-family:swiper-icons;font-size:var(--swiper-navigation-size);text-transform:none!important;letter-spacing:0;font-variant:initial;line-height:1}.swiper-button-prev:after,.swiper-rtl .swiper-button-next:after{content:'prev'}.swiper-button-next,.swiper-rtl .swiper-button-prev{right:var(--swiper-navigation-sides-offset,10px);left:auto}.swiper-button-next:after,.swiper-rtl .swiper-button-prev:after{content:'next'}.swiper-pagination{position:absolute;text-align:center;transition:.3s opacity;transform:translate3d(0,0,0);z-index:10}.swiper-pagination.swiper-pagination-hidden{opacity:0}.swiper-pagination-disabled>.swiper-pagination,.swiper-pagination.swiper-pagination-disabled{display:none!important}.swiper-horizontal>.swiper-pagination-bullets,.swiper-pagination-bullets.swiper-pagination-horizontal,.swiper-pagination-custom,.swiper-pagination-fraction{bottom:var(--swiper-pagination-bottom,8px);top:var(--swiper-pagination-top,auto);left:0;width:100%}.swiper-pagination-bullets-dynamic{overflow:hidden;font-size:0}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transform:scale(.33);position:relative}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active{transform:scale(1)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-main{transform:scale(1)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev{transform:scale(.66)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev-prev{transform:scale(.33)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next{transform:scale(.66)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next-next{transform:scale(.33)}.swiper-pagination-bullet{width:var(--swiper-pagination-bullet-width,var(--swiper-pagination-bullet-size,8px));height:var(--swiper-pagination-bullet-height,var(--swiper-pagination-bullet-size,8px));display:inline-block;border-radius:var(--swiper-pagination-bullet-border-radius,50%);background:var(--swiper-pagination-bullet-inactive-color,#000);opacity:var(--swiper-pagination-bullet-inactive-opacity, .2)}button.swiper-pagination-bullet{border:none;margin:0;padding:0;box-shadow:none;-webkit-appearance:none;appearance:none}.swiper-pagination-clickable .swiper-pagination-bullet{cursor:pointer}.swiper-pagination-bullet:only-child{display:none!important}.swiper-pagination-bullet-active{opacity:var(--swiper-pagination-bullet-opacity, 1);background:var(--swiper-pagination-color,var(--swiper-theme-color))}.swiper-pagination-vertical.swiper-pagination-bullets,.swiper-vertical>.swiper-pagination-bullets{right:var(--swiper-pagination-right,8px);left:var(--swiper-pagination-left,auto);top:50%;transform:translate3d(0px,-50%,0)}.swiper-pagination-vertical.swiper-pagination-bullets .swiper-pagination-bullet,.swiper-vertical>.swiper-pagination-bullets .swiper-pagination-bullet{margin:var(--swiper-pagination-bullet-vertical-gap,6px) 0;display:block}.swiper-pagination-vertical.swiper-pagination-bullets.swiper-pagination-bullets-dynamic,.swiper-vertical>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic{top:50%;transform:translateY(-50%);width:8px}.swiper-pagination-vertical.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet,.swiper-vertical>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{display:inline-block;transition:.2s transform,.2s top}.swiper-horizontal>.swiper-pagination-bullets .swiper-pagination-bullet,.swiper-pagination-horizontal.swiper-pagination-bullets .swiper-pagination-bullet{margin:0 var(--swiper-pagination-bullet-horizontal-gap,4px)}.swiper-horizontal>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic,.swiper-pagination-horizontal.swiper-pagination-bullets.swiper-pagination-bullets-dynamic{left:50%;transform:translateX(-50%);white-space:nowrap}.swiper-horizontal>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet,.swiper-pagination-horizontal.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transition:.2s transform,.2s left}.swiper-horizontal.swiper-rtl>.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transition:.2s transform,.2s right}.swiper-pagination-fraction{color:var(--swiper-pagination-fraction-color,inherit)}.swiper-pagination-progressbar{background:var(--swiper-pagination-progressbar-bg-color,rgba(0,0,0,.25));position:absolute}.swiper-pagination-progressbar .swiper-pagination-progressbar-fill{background:var(--swiper-pagination-color,var(--swiper-theme-color));position:absolute;left:0;top:0;width:100%;height:100%;transform:scale(0);transform-origin:left top}.swiper-rtl .swiper-pagination-progressbar .swiper-pagination-progressbar-fill{transform-origin:right top}.swiper-horizontal>.swiper-pagination-progressbar,.swiper-pagination-progressbar.swiper-pagination-horizontal,.swiper-pagination-progressbar.swiper-pagination-vertical.swiper-pagination-progressbar-opposite,.swiper-vertical>.swiper-pagination-progressbar.swiper-pagination-progressbar-opposite{width:100%;height:var(--swiper-pagination-progressbar-size,4px);left:0;top:0}.swiper-horizontal>.swiper-pagination-progressbar.swiper-pagination-progressbar-opposite,.swiper-pagination-progressbar.swiper-pagination-horizontal.swiper-pagination-progressbar-opposite,.swiper-pagination-progressbar.swiper-pagination-vertical,.swiper-vertical>.swiper-pagination-progressbar{width:var(--swiper-pagination-progressbar-size,4px);height:100%;left:0;top:0}.swiper-pagination-lock{display:none}.swiper-scrollbar{border-radius:var(--swiper-scrollbar-border-radius,10px);position:relative;touch-action:none;background:var(--swiper-scrollbar-bg-color,rgba(0,0,0,.1))}.swiper-scrollbar-disabled>.swiper-scrollbar,.swiper-scrollbar.swiper-scrollbar-disabled{display:none!important}.swiper-horizontal>.swiper-scrollbar,.swiper-scrollbar.swiper-scrollbar-horizontal{position:absolute;left:var(--swiper-scrollbar-sides-offset,1%);bottom:var(--swiper-scrollbar-bottom,4px);top:var(--swiper-scrollbar-top,auto);z-index:50;height:var(--swiper-scrollbar-size,4px);width:calc(100% - 2 * var(--swiper-scrollbar-sides-offset,1%))}.swiper-scrollbar.swiper-scrollbar-vertical,.swiper-vertical>.swiper-scrollbar{position:absolute;left:var(--swiper-scrollbar-left,auto);right:var(--swiper-scrollbar-right,4px);top:var(--swiper-scrollbar-sides-offset,1%);z-index:50;width:var(--swiper-scrollbar-size,4px);height:calc(100% - 2 * var(--swiper-scrollbar-sides-offset,1%))}.swiper-scrollbar-drag{height:100%;width:100%;position:relative;background:var(--swiper-scrollbar-drag-bg-color,rgba(0,0,0,.5));border-radius:var(--swiper-scrollbar-border-radius,10px);left:0;top:0}.swiper-scrollbar-cursor-drag{cursor:move}.swiper-scrollbar-lock{display:none}.swiper-zoom-container{width:100%;height:100%;display:flex;justify-content:center;align-items:center;text-align:center}.swiper-zoom-container>canvas,.swiper-zoom-container>img,.swiper-zoom-container>svg{max-width:100%;max-height:100%;object-fit:contain}.swiper-slide-zoomed{cursor:move;touch-action:none}.swiper .swiper-notification{position:absolute;left:0;top:0;pointer-events:none;opacity:0;z-index:-1000}.swiper-free-mode>.swiper-wrapper{transition-timing-function:ease-out;margin:0 auto}.swiper-grid>.swiper-wrapper{flex-wrap:wrap}.swiper-grid-column>.swiper-wrapper{flex-wrap:wrap;flex-direction:column}.swiper-fade.swiper-free-mode .swiper-slide{transition-timing-function:ease-out}.swiper-fade .swiper-slide{pointer-events:none;transition-property:opacity}.swiper-fade .swiper-slide .swiper-slide{pointer-events:none}.swiper-fade .swiper-slide-active{pointer-events:auto}.swiper-fade .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper.swiper-cube{overflow:visible}.swiper-cube .swiper-slide{pointer-events:none;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1;visibility:hidden;transform-origin:0 0;width:100%;height:100%}.swiper-cube .swiper-slide .swiper-slide{pointer-events:none}.swiper-cube.swiper-rtl .swiper-slide{transform-origin:100% 0}.swiper-cube .swiper-slide-active,.swiper-cube .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-cube .swiper-slide-active,.swiper-cube .swiper-slide-next,.swiper-cube .swiper-slide-prev{pointer-events:auto;visibility:visible}.swiper-cube .swiper-cube-shadow{position:absolute;left:0;bottom:0px;width:100%;height:100%;opacity:.6;z-index:0}.swiper-cube .swiper-cube-shadow:before{content:'';background:#000;position:absolute;left:0;top:0;bottom:0;right:0;filter:blur(50px)}.swiper-cube .swiper-slide-next+.swiper-slide{pointer-events:auto;visibility:visible}.swiper-cube .swiper-slide-shadow-cube.swiper-slide-shadow-bottom,.swiper-cube .swiper-slide-shadow-cube.swiper-slide-shadow-left,.swiper-cube .swiper-slide-shadow-cube.swiper-slide-shadow-right,.swiper-cube .swiper-slide-shadow-cube.swiper-slide-shadow-top{z-index:0;-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper.swiper-flip{overflow:visible}.swiper-flip .swiper-slide{pointer-events:none;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1}.swiper-flip .swiper-slide .swiper-slide{pointer-events:none}.swiper-flip .swiper-slide-active,.swiper-flip .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-flip .swiper-slide-shadow-flip.swiper-slide-shadow-bottom,.swiper-flip .swiper-slide-shadow-flip.swiper-slide-shadow-left,.swiper-flip .swiper-slide-shadow-flip.swiper-slide-shadow-right,.swiper-flip .swiper-slide-shadow-flip.swiper-slide-shadow-top{z-index:0;-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper-creative .swiper-slide{-webkit-backface-visibility:hidden;backface-visibility:hidden;overflow:hidden;transition-property:transform,opacity,height}.swiper.swiper-cards{overflow:visible}.swiper-cards .swiper-slide{transform-origin:center bottom;-webkit-backface-visibility:hidden;backface-visibility:hidden;overflow:hidden}
|
static/assets/swiper-bundle.min.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/assets/theme-switcher.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Switch between light and dark themes (color modes)
|
| 3 |
+
* Copyright 2025 Createx Studio
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
(() => {
|
| 7 |
+
'use strict'
|
| 8 |
+
|
| 9 |
+
const getStoredTheme = () => localStorage.getItem('theme')
|
| 10 |
+
const setStoredTheme = theme => localStorage.setItem('theme', theme)
|
| 11 |
+
|
| 12 |
+
const getPreferredTheme = () => {
|
| 13 |
+
const storedTheme = getStoredTheme()
|
| 14 |
+
if (storedTheme) {
|
| 15 |
+
return storedTheme
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// Set default theme to 'light'.
|
| 19 |
+
// Possible options: 'dark' or system color mode (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
| 20 |
+
return 'light'
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const setTheme = theme => {
|
| 24 |
+
if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
| 25 |
+
document.documentElement.setAttribute('data-bs-theme', 'dark')
|
| 26 |
+
} else {
|
| 27 |
+
document.documentElement.setAttribute('data-bs-theme', theme)
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
setTheme(getPreferredTheme())
|
| 32 |
+
|
| 33 |
+
const showActiveTheme = (theme) => {
|
| 34 |
+
const themeSwitcher = document.querySelector('[data-bs-toggle="mode"]')
|
| 35 |
+
const themeSwitcherCheck = themeSwitcher.querySelector('input[type="checkbox"]')
|
| 36 |
+
|
| 37 |
+
if (!themeSwitcher) {
|
| 38 |
+
return
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
if (theme === 'dark') {
|
| 42 |
+
themeSwitcherCheck.checked = 'checked'
|
| 43 |
+
} else {
|
| 44 |
+
themeSwitcherCheck.checked = false
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
| 49 |
+
const storedTheme = getStoredTheme()
|
| 50 |
+
if (storedTheme !== 'light' && storedTheme !== 'dark') {
|
| 51 |
+
setTheme(getPreferredTheme())
|
| 52 |
+
}
|
| 53 |
+
})
|
| 54 |
+
|
| 55 |
+
window.addEventListener('DOMContentLoaded', () => {
|
| 56 |
+
showActiveTheme(getPreferredTheme())
|
| 57 |
+
|
| 58 |
+
document.querySelectorAll('[data-bs-toggle="mode"]')
|
| 59 |
+
.forEach(toggle => {
|
| 60 |
+
toggle.addEventListener('click', () => {
|
| 61 |
+
const theme = toggle.querySelector('input[type="checkbox"]').checked === true ? 'dark' : 'light'
|
| 62 |
+
setStoredTheme(theme)
|
| 63 |
+
setTheme(theme)
|
| 64 |
+
showActiveTheme(theme, true)
|
| 65 |
+
})
|
| 66 |
+
})
|
| 67 |
+
})
|
| 68 |
+
})()
|
static/assets/theme.min.css
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/assets/theme.min.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*!
|
| 2 |
+
* Silicon | Multipurpose Bootstrap Template & UI Kit
|
| 3 |
+
* Copyright 2025 Createx Studio
|
| 4 |
+
* Theme scripts
|
| 5 |
+
*
|
| 6 |
+
* @copyright Createx Studio
|
| 7 |
+
* @version 1.7.0
|
| 8 |
+
*/
|
| 9 |
+
!function(){"use strict";
|
| 10 |
+
/*!
|
| 11 |
+
* Bootstrap v5.3.5 (https://getbootstrap.com/)
|
| 12 |
+
* Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
| 13 |
+
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
| 14 |
+
*/var e,t;!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).bootstrap=t()}(void 0,(function(){const e=new Map,t={set(t,n,i){e.has(t)||e.set(t,new Map);const s=e.get(t);s.has(n)||0===s.size?s.set(n,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(t,n)=>e.has(t)&&e.get(t).get(n)||null,remove(t,n){if(!e.has(t))return;const i=e.get(t);i.delete(n),0===i.size&&e.delete(t)}},n="transitionend",i=e=>(e&&window.CSS&&window.CSS.escape&&(e=e.replace(/#([^\s"#']+)/g,((e,t)=>`#${CSS.escape(t)}`))),e),s=e=>{e.dispatchEvent(new Event(n))},o=e=>!(!e||"object"!=typeof e)&&(void 0!==e.jquery&&(e=e[0]),void 0!==e.nodeType),r=e=>o(e)?e.jquery?e[0]:e:"string"==typeof e&&e.length>0?document.querySelector(i(e)):null,a=e=>{if(!o(e)||0===e.getClientRects().length)return!1;const t="visible"===getComputedStyle(e).getPropertyValue("visibility"),n=e.closest("details:not([open])");if(!n)return t;if(n!==e){const t=e.closest("summary");if(t&&t.parentNode!==n)return!1;if(null===t)return!1}return t},l=e=>!e||e.nodeType!==Node.ELEMENT_NODE||!!e.classList.contains("disabled")||(void 0!==e.disabled?e.disabled:e.hasAttribute("disabled")&&"false"!==e.getAttribute("disabled")),c=e=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof e.getRootNode){const t=e.getRootNode();return t instanceof ShadowRoot?t:null}return e instanceof ShadowRoot?e:e.parentNode?c(e.parentNode):null},u=()=>{},d=e=>{e.offsetHeight},h=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=e=>{var t;t=()=>{const t=h();if(t){const n=e.NAME,i=t.fn[n];t.fn[n]=e.jQueryInterface,t.fn[n].Constructor=e,t.fn[n].noConflict=()=>(t.fn[n]=i,e.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const e of f)e()})),f.push(t)):t()},g=(e,t=[],n=e)=>"function"==typeof e?e.call(...t):n,_=(e,t,i=!0)=>{if(!i)return void g(e);const o=(e=>{if(!e)return 0;let{transitionDuration:t,transitionDelay:n}=window.getComputedStyle(e);const i=Number.parseFloat(t),s=Number.parseFloat(n);return i||s?(t=t.split(",")[0],n=n.split(",")[0],1e3*(Number.parseFloat(t)+Number.parseFloat(n))):0})(t)+5;let r=!1;const a=({target:i})=>{i===t&&(r=!0,t.removeEventListener(n,a),g(e))};t.addEventListener(n,a),setTimeout((()=>{r||s(t)}),o)},v=(e,t,n,i)=>{const s=e.length;let o=e.indexOf(t);return-1===o?!n&&i?e[s-1]:e[0]:(o+=n?1:-1,i&&(o=(o+s)%s),e[Math.max(0,Math.min(o,s-1))])},b=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,E={};let A=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function S(e,t){return t&&`${t}::${A++}`||e.uidEvent||A++}function x(e){const t=S(e);return e.uidEvent=t,E[t]=E[t]||{},E[t]}function O(e,t,n=null){return Object.values(e).find((e=>e.callable===t&&e.delegationSelector===n))}function L(e,t,n){const i="string"==typeof t,s=i?n:t||n;let o=M(e);return C.has(o)||(o=e),[i,s,o]}function k(e,t,n,i,s){if("string"!=typeof t||!e)return;let[o,r,a]=L(t,n,i);if(t in T){const e=e=>function(t){if(!t.relatedTarget||t.relatedTarget!==t.delegateTarget&&!t.delegateTarget.contains(t.relatedTarget))return e.call(this,t)};r=e(r)}const l=x(e),c=l[a]||(l[a]={}),u=O(c,r,o?n:null);if(u)return void(u.oneOff=u.oneOff&&s);const d=S(r,t.replace(b,"")),h=o?function(e,t,n){return function i(s){const o=e.querySelectorAll(t);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),i.oneOff&&$.off(e,s.type,t,n),n.apply(r,[s])}}(e,n,r):function(e,t){return function n(i){return P(i,{delegateTarget:e}),n.oneOff&&$.off(e,i.type,t),t.apply(e,[i])}}(e,r);h.delegationSelector=o?n:null,h.callable=r,h.oneOff=s,h.uidEvent=d,c[d]=h,e.addEventListener(a,h,o)}function D(e,t,n,i,s){const o=O(t[n],i,s);o&&(e.removeEventListener(n,o,Boolean(s)),delete t[n][o.uidEvent])}function I(e,t,n,i){const s=t[n]||{};for(const[o,r]of Object.entries(s))o.includes(i)&&D(e,t,n,r.callable,r.delegationSelector)}function M(e){return e=e.replace(y,""),T[e]||e}const $={on(e,t,n,i){k(e,t,n,i,!1)},one(e,t,n,i){k(e,t,n,i,!0)},off(e,t,n,i){if("string"!=typeof t||!e)return;const[s,o,r]=L(t,n,i),a=r!==t,l=x(e),c=l[r]||{},u=t.startsWith(".");if(void 0===o){if(u)for(const n of Object.keys(l))I(e,l,n,t.slice(1));for(const[n,i]of Object.entries(c)){const s=n.replace(w,"");a&&!t.includes(s)||D(e,l,r,i.callable,i.delegationSelector)}}else{if(!Object.keys(c).length)return;D(e,l,r,o,s?n:null)}},trigger(e,t,n){if("string"!=typeof t||!e)return null;const i=h();let s=null,o=!0,r=!0,a=!1;t!==M(t)&&i&&(s=i.Event(t,n),i(e).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(t,{bubbles:o,cancelable:!0}),n);return a&&l.preventDefault(),r&&e.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(e,t={}){for(const[n,i]of Object.entries(t))try{e[n]=i}catch(t){Object.defineProperty(e,n,{configurable:!0,get:()=>i})}return e}function N(e){if("true"===e)return!0;if("false"===e)return!1;if(e===Number(e).toString())return Number(e);if(""===e||"null"===e)return null;if("string"!=typeof e)return e;try{return JSON.parse(decodeURIComponent(e))}catch(t){return e}}function j(e){return e.replace(/[A-Z]/g,(e=>`-${e.toLowerCase()}`))}const q={setDataAttribute(e,t,n){e.setAttribute(`data-bs-${j(t)}`,n)},removeDataAttribute(e,t){e.removeAttribute(`data-bs-${j(t)}`)},getDataAttributes(e){if(!e)return{};const t={},n=Object.keys(e.dataset).filter((e=>e.startsWith("bs")&&!e.startsWith("bsConfig")));for(const i of n){let n=i.replace(/^bs/,"");n=n.charAt(0).toLowerCase()+n.slice(1),t[n]=N(e.dataset[i])}return t},getDataAttribute:(e,t)=>N(e.getAttribute(`data-bs-${j(t)}`))};class F{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(e){return e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e}_mergeConfigObj(e,t){const n=o(t)?q.getDataAttribute(t,"config"):{};return{...this.constructor.Default,..."object"==typeof n?n:{},...o(t)?q.getDataAttributes(t):{},..."object"==typeof e?e:{}}}_typeCheckConfig(e,t=this.constructor.DefaultType){for(const[i,s]of Object.entries(t)){const t=e[i],r=o(t)?"element":null==(n=t)?`${n}`:Object.prototype.toString.call(n).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${i}" provided type "${r}" but expected type "${s}".`)}var n}}class H extends F{constructor(e,n){super(),(e=r(e))&&(this._element=e,this._config=this._getConfig(n),t.set(this._element,this.constructor.DATA_KEY,this))}dispose(){t.remove(this._element,this.constructor.DATA_KEY),$.off(this._element,this.constructor.EVENT_KEY);for(const e of Object.getOwnPropertyNames(this))this[e]=null}_queueCallback(e,t,n=!0){_(e,t,n)}_getConfig(e){return e=this._mergeConfigObj(e,this._element),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}static getInstance(e){return t.get(r(e),this.DATA_KEY)}static getOrCreateInstance(e,t={}){return this.getInstance(e)||new this(e,"object"==typeof t?t:null)}static get VERSION(){return"5.3.5"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(e){return`${e}${this.EVENT_KEY}`}}const R=e=>{let t=e.getAttribute("data-bs-target");if(!t||"#"===t){let n=e.getAttribute("href");if(!n||!n.includes("#")&&!n.startsWith("."))return null;n.includes("#")&&!n.startsWith("#")&&(n=`#${n.split("#")[1]}`),t=n&&"#"!==n?n.trim():null}return t?t.split(",").map((e=>i(e))).join(","):null},B={find:(e,t=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(t,e)),findOne:(e,t=document.documentElement)=>Element.prototype.querySelector.call(t,e),children:(e,t)=>[].concat(...e.children).filter((e=>e.matches(t))),parents(e,t){const n=[];let i=e.parentNode.closest(t);for(;i;)n.push(i),i=i.parentNode.closest(t);return n},prev(e,t){let n=e.previousElementSibling;for(;n;){if(n.matches(t))return[n];n=n.previousElementSibling}return[]},next(e,t){let n=e.nextElementSibling;for(;n;){if(n.matches(t))return[n];n=n.nextElementSibling}return[]},focusableChildren(e){const t=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((e=>`${e}:not([tabindex^="-"])`)).join(",");return this.find(t,e).filter((e=>!l(e)&&a(e)))},getSelectorFromElement(e){const t=R(e);return t&&B.findOne(t)?t:null},getElementFromSelector(e){const t=R(e);return t?B.findOne(t):null},getMultipleElementsFromSelector(e){const t=R(e);return t?B.find(t):[]}},W=(e,t="hide")=>{const n=`click.dismiss${e.EVENT_KEY}`,i=e.NAME;$.on(document,n,`[data-bs-dismiss="${i}"]`,(function(n){if(["A","AREA"].includes(this.tagName)&&n.preventDefault(),l(this))return;const s=B.getElementFromSelector(this)||this.closest(`.${i}`);e.getOrCreateInstance(s)[t]()}))},z=".bs.alert",V=`close${z}`,U=`closed${z}`;class Q extends H{static get NAME(){return"alert"}close(){if($.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove("show");const e=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,e)}_destroyElement(){this._element.remove(),$.trigger(this._element,U),this.dispose()}static jQueryInterface(e){return this.each((function(){const t=Q.getOrCreateInstance(this);if("string"==typeof e){if(void 0===t[e]||e.startsWith("_")||"constructor"===e)throw new TypeError(`No method named "${e}"`);t[e](this)}}))}}W(Q,"close"),m(Q);const K='[data-bs-toggle="button"]';class Y extends H{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(e){return this.each((function(){const t=Y.getOrCreateInstance(this);"toggle"===e&&t[e]()}))}}$.on(document,"click.bs.button.data-api",K,(e=>{e.preventDefault();const t=e.target.closest(K);Y.getOrCreateInstance(t).toggle()})),m(Y);const X=".bs.swipe",J=`touchstart${X}`,G=`touchmove${X}`,Z=`touchend${X}`,ee=`pointerdown${X}`,te=`pointerup${X}`,ne={endCallback:null,leftCallback:null,rightCallback:null},ie={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class se extends F{constructor(e,t){super(),this._element=e,e&&se.isSupported()&&(this._config=this._getConfig(t),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return ne}static get DefaultType(){return ie}static get NAME(){return"swipe"}dispose(){$.off(this._element,X)}_start(e){this._supportPointerEvents?this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX):this._deltaX=e.touches[0].clientX}_end(e){this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(e){this._deltaX=e.touches&&e.touches.length>1?0:e.touches[0].clientX-this._deltaX}_handleSwipe(){const e=Math.abs(this._deltaX);if(e<=40)return;const t=e/this._deltaX;this._deltaX=0,t&&g(t>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?($.on(this._element,ee,(e=>this._start(e))),$.on(this._element,te,(e=>this._end(e))),this._element.classList.add("pointer-event")):($.on(this._element,J,(e=>this._start(e))),$.on(this._element,G,(e=>this._move(e))),$.on(this._element,Z,(e=>this._end(e))))}_eventIsPointerPenTouch(e){return this._supportPointerEvents&&("pen"===e.pointerType||"touch"===e.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const oe=".bs.carousel",re=".data-api",ae="ArrowLeft",le="ArrowRight",ce="next",ue="prev",de="left",he="right",fe=`slide${oe}`,pe=`slid${oe}`,me=`keydown${oe}`,ge=`mouseenter${oe}`,_e=`mouseleave${oe}`,ve=`dragstart${oe}`,be=`load${oe}${re}`,ye=`click${oe}${re}`,we="carousel",Ee="active",Ae=".active",Te=".carousel-item",Ce=Ae+Te,Se={[ae]:he,[le]:de},xe={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Oe={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class Le extends H{constructor(e,t){super(e,t),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=B.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===we&&this.cycle()}static get Default(){return xe}static get DefaultType(){return Oe}static get NAME(){return"carousel"}next(){this._slide(ce)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(ue)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?$.one(this._element,pe,(()=>this.cycle())):this.cycle())}to(e){const t=this._getItems();if(e>t.length-1||e<0)return;if(this._isSliding)return void $.one(this._element,pe,(()=>this.to(e)));const n=this._getItemIndex(this._getActive());if(n===e)return;const i=e>n?ce:ue;this._slide(i,t[e])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(e){return e.defaultInterval=e.interval,e}_addEventListeners(){this._config.keyboard&&$.on(this._element,me,(e=>this._keydown(e))),"hover"===this._config.pause&&($.on(this._element,ge,(()=>this.pause())),$.on(this._element,_e,(()=>this._maybeEnableCycle()))),this._config.touch&&se.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const e of B.find(".carousel-item img",this._element))$.on(e,ve,(e=>e.preventDefault()));const e={leftCallback:()=>this._slide(this._directionToOrder(de)),rightCallback:()=>this._slide(this._directionToOrder(he)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new se(this._element,e)}_keydown(e){if(/input|textarea/i.test(e.target.tagName))return;const t=Se[e.key];t&&(e.preventDefault(),this._slide(this._directionToOrder(t)))}_getItemIndex(e){return this._getItems().indexOf(e)}_setActiveIndicatorElement(e){if(!this._indicatorsElement)return;const t=B.findOne(Ae,this._indicatorsElement);t.classList.remove(Ee),t.removeAttribute("aria-current");const n=B.findOne(`[data-bs-slide-to="${e}"]`,this._indicatorsElement);n&&(n.classList.add(Ee),n.setAttribute("aria-current","true"))}_updateInterval(){const e=this._activeElement||this._getActive();if(!e)return;const t=Number.parseInt(e.getAttribute("data-bs-interval"),10);this._config.interval=t||this._config.defaultInterval}_slide(e,t=null){if(this._isSliding)return;const n=this._getActive(),i=e===ce,s=t||v(this._getItems(),n,i,this._config.wrap);if(s===n)return;const o=this._getItemIndex(s),r=t=>$.trigger(this._element,t,{relatedTarget:s,direction:this._orderToDirection(e),from:this._getItemIndex(n),to:o});if(r(fe).defaultPrevented)return;if(!n||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=i?"carousel-item-start":"carousel-item-end",c=i?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),n.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(Ee),n.classList.remove(Ee,c,l),this._isSliding=!1,r(pe)}),n,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return B.findOne(Ce,this._element)}_getItems(){return B.find(Te,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(e){return p()?e===de?ue:ce:e===de?ce:ue}_orderToDirection(e){return p()?e===ue?de:he:e===ue?he:de}static jQueryInterface(e){return this.each((function(){const t=Le.getOrCreateInstance(this,e);if("number"!=typeof e){if("string"==typeof e){if(void 0===t[e]||e.startsWith("_")||"constructor"===e)throw new TypeError(`No method named "${e}"`);t[e]()}}else t.to(e)}))}}$.on(document,ye,"[data-bs-slide], [data-bs-slide-to]",(function(e){const t=B.getElementFromSelector(this);if(!t||!t.classList.contains(we))return;e.preventDefault();const n=Le.getOrCreateInstance(t),i=this.getAttribute("data-bs-slide-to");return i?(n.to(i),void n._maybeEnableCycle()):"next"===q.getDataAttribute(this,"slide")?(n.next(),void n._maybeEnableCycle()):(n.prev(),void n._maybeEnableCycle())})),$.on(window,be,(()=>{const e=B.find('[data-bs-ride="carousel"]');for(const t of e)Le.getOrCreateInstance(t)})),m(Le);const ke=".bs.collapse",De=`show${ke}`,Ie=`shown${ke}`,Me=`hide${ke}`,$e=`hidden${ke}`,Pe=`click${ke}.data-api`,Ne="show",je="collapse",qe="collapsing",Fe=`:scope .${je} .${je}`,He='[data-bs-toggle="collapse"]',Re={parent:null,toggle:!0},Be={parent:"(null|element)",toggle:"boolean"};class We extends H{constructor(e,t){super(e,t),this._isTransitioning=!1,this._triggerArray=[];const n=B.find(He);for(const e of n){const t=B.getSelectorFromElement(e),n=B.find(t).filter((e=>e===this._element));null!==t&&n.length&&this._triggerArray.push(e)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Re}static get DefaultType(){return Be}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let e=[];if(this._config.parent&&(e=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((e=>e!==this._element)).map((e=>We.getOrCreateInstance(e,{toggle:!1})))),e.length&&e[0]._isTransitioning)return;if($.trigger(this._element,De).defaultPrevented)return;for(const t of e)t.hide();const t=this._getDimension();this._element.classList.remove(je),this._element.classList.add(qe),this._element.style[t]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const n=`scroll${t[0].toUpperCase()+t.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(qe),this._element.classList.add(je,Ne),this._element.style[t]="",$.trigger(this._element,Ie)}),this._element,!0),this._element.style[t]=`${this._element[n]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if($.trigger(this._element,Me).defaultPrevented)return;const e=this._getDimension();this._element.style[e]=`${this._element.getBoundingClientRect()[e]}px`,d(this._element),this._element.classList.add(qe),this._element.classList.remove(je,Ne);for(const e of this._triggerArray){const t=B.getElementFromSelector(e);t&&!this._isShown(t)&&this._addAriaAndCollapsedClass([e],!1)}this._isTransitioning=!0,this._element.style[e]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(qe),this._element.classList.add(je),$.trigger(this._element,$e)}),this._element,!0)}_isShown(e=this._element){return e.classList.contains(Ne)}_configAfterMerge(e){return e.toggle=Boolean(e.toggle),e.parent=r(e.parent),e}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const e=this._getFirstLevelChildren(He);for(const t of e){const e=B.getElementFromSelector(t);e&&this._addAriaAndCollapsedClass([t],this._isShown(e))}}_getFirstLevelChildren(e){const t=B.find(Fe,this._config.parent);return B.find(e,this._config.parent).filter((e=>!t.includes(e)))}_addAriaAndCollapsedClass(e,t){if(e.length)for(const n of e)n.classList.toggle("collapsed",!t),n.setAttribute("aria-expanded",t)}static jQueryInterface(e){const t={};return"string"==typeof e&&/show|hide/.test(e)&&(t.toggle=!1),this.each((function(){const n=We.getOrCreateInstance(this,t);if("string"==typeof e){if(void 0===n[e])throw new TypeError(`No method named "${e}"`);n[e]()}}))}}$.on(document,Pe,He,(function(e){("A"===e.target.tagName||e.delegateTarget&&"A"===e.delegateTarget.tagName)&&e.preventDefault();for(const e of B.getMultipleElementsFromSelector(this))We.getOrCreateInstance(e,{toggle:!1}).toggle()})),m(We);var ze="top",Ve="bottom",Ue="right",Qe="left",Ke="auto",Ye=[ze,Ve,Ue,Qe],Xe="start",Je="end",Ge="clippingParents",Ze="viewport",et="popper",tt="reference",nt=Ye.reduce((function(e,t){return e.concat([t+"-"+Xe,t+"-"+Je])}),[]),it=[].concat(Ye,[Ke]).reduce((function(e,t){return e.concat([t,t+"-"+Xe,t+"-"+Je])}),[]),st="beforeRead",ot="read",rt="afterRead",at="beforeMain",lt="main",ct="afterMain",ut="beforeWrite",dt="write",ht="afterWrite",ft=[st,ot,rt,at,lt,ct,ut,dt,ht];function pt(e){return e?(e.nodeName||"").toLowerCase():null}function mt(e){if(null==e)return window;if("[object Window]"!==e.toString()){var t=e.ownerDocument;return t&&t.defaultView||window}return e}function gt(e){return e instanceof mt(e).Element||e instanceof Element}function _t(e){return e instanceof mt(e).HTMLElement||e instanceof HTMLElement}function vt(e){return"undefined"!=typeof ShadowRoot&&(e instanceof mt(e).ShadowRoot||e instanceof ShadowRoot)}const bt={name:"applyStyles",enabled:!0,phase:"write",fn:function(e){var t=e.state;Object.keys(t.elements).forEach((function(e){var n=t.styles[e]||{},i=t.attributes[e]||{},s=t.elements[e];_t(s)&&pt(s)&&(Object.assign(s.style,n),Object.keys(i).forEach((function(e){var t=i[e];!1===t?s.removeAttribute(e):s.setAttribute(e,!0===t?"":t)})))}))},effect:function(e){var t=e.state,n={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(t.elements.popper.style,n.popper),t.styles=n,t.elements.arrow&&Object.assign(t.elements.arrow.style,n.arrow),function(){Object.keys(t.elements).forEach((function(e){var i=t.elements[e],s=t.attributes[e]||{},o=Object.keys(t.styles.hasOwnProperty(e)?t.styles[e]:n[e]).reduce((function(e,t){return e[t]="",e}),{});_t(i)&&pt(i)&&(Object.assign(i.style,o),Object.keys(s).forEach((function(e){i.removeAttribute(e)})))}))}},requires:["computeStyles"]};function yt(e){return e.split("-")[0]}var wt=Math.max,Et=Math.min,At=Math.round;function Tt(){var e=navigator.userAgentData;return null!=e&&e.brands&&Array.isArray(e.brands)?e.brands.map((function(e){return e.brand+"/"+e.version})).join(" "):navigator.userAgent}function Ct(){return!/^((?!chrome|android).)*safari/i.test(Tt())}function St(e,t,n){void 0===t&&(t=!1),void 0===n&&(n=!1);var i=e.getBoundingClientRect(),s=1,o=1;t&&_t(e)&&(s=e.offsetWidth>0&&At(i.width)/e.offsetWidth||1,o=e.offsetHeight>0&&At(i.height)/e.offsetHeight||1);var r=(gt(e)?mt(e):window).visualViewport,a=!Ct()&&n,l=(i.left+(a&&r?r.offsetLeft:0))/s,c=(i.top+(a&&r?r.offsetTop:0))/o,u=i.width/s,d=i.height/o;return{width:u,height:d,top:c,right:l+u,bottom:c+d,left:l,x:l,y:c}}function xt(e){var t=St(e),n=e.offsetWidth,i=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-i)<=1&&(i=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:i}}function Ot(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&vt(n)){var i=t;do{if(i&&e.isSameNode(i))return!0;i=i.parentNode||i.host}while(i)}return!1}function Lt(e){return mt(e).getComputedStyle(e)}function kt(e){return["table","td","th"].indexOf(pt(e))>=0}function Dt(e){return((gt(e)?e.ownerDocument:e.document)||window.document).documentElement}function It(e){return"html"===pt(e)?e:e.assignedSlot||e.parentNode||(vt(e)?e.host:null)||Dt(e)}function Mt(e){return _t(e)&&"fixed"!==Lt(e).position?e.offsetParent:null}function $t(e){for(var t=mt(e),n=Mt(e);n&&kt(n)&&"static"===Lt(n).position;)n=Mt(n);return n&&("html"===pt(n)||"body"===pt(n)&&"static"===Lt(n).position)?t:n||function(e){var t=/firefox/i.test(Tt());if(/Trident/i.test(Tt())&&_t(e)&&"fixed"===Lt(e).position)return null;var n=It(e);for(vt(n)&&(n=n.host);_t(n)&&["html","body"].indexOf(pt(n))<0;){var i=Lt(n);if("none"!==i.transform||"none"!==i.perspective||"paint"===i.contain||-1!==["transform","perspective"].indexOf(i.willChange)||t&&"filter"===i.willChange||t&&i.filter&&"none"!==i.filter)return n;n=n.parentNode}return null}(e)||t}function Pt(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}function Nt(e,t,n){return wt(e,Et(t,n))}function jt(e){return Object.assign({},{top:0,right:0,bottom:0,left:0},e)}function qt(e,t){return t.reduce((function(t,n){return t[n]=e,t}),{})}const Ft={name:"arrow",enabled:!0,phase:"main",fn:function(e){var t,n=e.state,i=e.name,s=e.options,o=n.elements.arrow,r=n.modifiersData.popperOffsets,a=yt(n.placement),l=Pt(a),c=[Qe,Ue].indexOf(a)>=0?"height":"width";if(o&&r){var u=function(e,t){return jt("number"!=typeof(e="function"==typeof e?e(Object.assign({},t.rects,{placement:t.placement})):e)?e:qt(e,Ye))}(s.padding,n),d=xt(o),h="y"===l?ze:Qe,f="y"===l?Ve:Ue,p=n.rects.reference[c]+n.rects.reference[l]-r[l]-n.rects.popper[c],m=r[l]-n.rects.reference[l],g=$t(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,v=p/2-m/2,b=u[h],y=_-d[c]-u[f],w=_/2-d[c]/2+v,E=Nt(b,w,y),A=l;n.modifiersData[i]=((t={})[A]=E,t.centerOffset=E-w,t)}},effect:function(e){var t=e.state,n=e.options.element,i=void 0===n?"[data-popper-arrow]":n;null!=i&&("string"!=typeof i||(i=t.elements.popper.querySelector(i)))&&Ot(t.elements.popper,i)&&(t.elements.arrow=i)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Ht(e){return e.split("-")[1]}var Rt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function Bt(e){var t,n=e.popper,i=e.popperRect,s=e.placement,o=e.variation,r=e.offsets,a=e.position,l=e.gpuAcceleration,c=e.adaptive,u=e.roundOffsets,d=e.isFixed,h=r.x,f=void 0===h?0:h,p=r.y,m=void 0===p?0:p,g="function"==typeof u?u({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),v=r.hasOwnProperty("y"),b=Qe,y=ze,w=window;if(c){var E=$t(n),A="clientHeight",T="clientWidth";E===mt(n)&&"static"!==Lt(E=Dt(n)).position&&"absolute"===a&&(A="scrollHeight",T="scrollWidth"),(s===ze||(s===Qe||s===Ue)&&o===Je)&&(y=Ve,m-=(d&&E===w&&w.visualViewport?w.visualViewport.height:E[A])-i.height,m*=l?1:-1),s!==Qe&&(s!==ze&&s!==Ve||o!==Je)||(b=Ue,f-=(d&&E===w&&w.visualViewport?w.visualViewport.width:E[T])-i.width,f*=l?1:-1)}var C,S=Object.assign({position:a},c&&Rt),x=!0===u?function(e,t){var n=e.x,i=e.y,s=t.devicePixelRatio||1;return{x:At(n*s)/s||0,y:At(i*s)/s||0}}({x:f,y:m},mt(n)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},S,((C={})[y]=v?"0":"",C[b]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},S,((t={})[y]=v?m+"px":"",t[b]=_?f+"px":"",t.transform="",t))}const Wt={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(e){var t=e.state,n=e.options,i=n.gpuAcceleration,s=void 0===i||i,o=n.adaptive,r=void 0===o||o,a=n.roundOffsets,l=void 0===a||a,c={placement:yt(t.placement),variation:Ht(t.placement),popper:t.elements.popper,popperRect:t.rects.popper,gpuAcceleration:s,isFixed:"fixed"===t.options.strategy};null!=t.modifiersData.popperOffsets&&(t.styles.popper=Object.assign({},t.styles.popper,Bt(Object.assign({},c,{offsets:t.modifiersData.popperOffsets,position:t.options.strategy,adaptive:r,roundOffsets:l})))),null!=t.modifiersData.arrow&&(t.styles.arrow=Object.assign({},t.styles.arrow,Bt(Object.assign({},c,{offsets:t.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-placement":t.placement})},data:{}};var zt={passive:!0};const Vt={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(e){var t=e.state,n=e.instance,i=e.options,s=i.scroll,o=void 0===s||s,r=i.resize,a=void 0===r||r,l=mt(t.elements.popper),c=[].concat(t.scrollParents.reference,t.scrollParents.popper);return o&&c.forEach((function(e){e.addEventListener("scroll",n.update,zt)})),a&&l.addEventListener("resize",n.update,zt),function(){o&&c.forEach((function(e){e.removeEventListener("scroll",n.update,zt)})),a&&l.removeEventListener("resize",n.update,zt)}},data:{}};var Ut={left:"right",right:"left",bottom:"top",top:"bottom"};function Qt(e){return e.replace(/left|right|bottom|top/g,(function(e){return Ut[e]}))}var Kt={start:"end",end:"start"};function Yt(e){return e.replace(/start|end/g,(function(e){return Kt[e]}))}function Xt(e){var t=mt(e);return{scrollLeft:t.pageXOffset,scrollTop:t.pageYOffset}}function Jt(e){return St(Dt(e)).left+Xt(e).scrollLeft}function Gt(e){var t=Lt(e),n=t.overflow,i=t.overflowX,s=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+s+i)}function Zt(e){return["html","body","#document"].indexOf(pt(e))>=0?e.ownerDocument.body:_t(e)&&Gt(e)?e:Zt(It(e))}function en(e,t){var n;void 0===t&&(t=[]);var i=Zt(e),s=i===(null==(n=e.ownerDocument)?void 0:n.body),o=mt(i),r=s?[o].concat(o.visualViewport||[],Gt(i)?i:[]):i,a=t.concat(r);return s?a:a.concat(en(It(r)))}function tn(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function nn(e,t,n){return t===Ze?tn(function(e,t){var n=mt(e),i=Dt(e),s=n.visualViewport,o=i.clientWidth,r=i.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ct();(c||!c&&"fixed"===t)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Jt(e),y:l}}(e,n)):gt(t)?function(e,t){var n=St(e,!1,"fixed"===t);return n.top=n.top+e.clientTop,n.left=n.left+e.clientLeft,n.bottom=n.top+e.clientHeight,n.right=n.left+e.clientWidth,n.width=e.clientWidth,n.height=e.clientHeight,n.x=n.left,n.y=n.top,n}(t,n):tn(function(e){var t,n=Dt(e),i=Xt(e),s=null==(t=e.ownerDocument)?void 0:t.body,o=wt(n.scrollWidth,n.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=wt(n.scrollHeight,n.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-i.scrollLeft+Jt(e),l=-i.scrollTop;return"rtl"===Lt(s||n).direction&&(a+=wt(n.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Dt(e)))}function sn(e){var t,n=e.reference,i=e.element,s=e.placement,o=s?yt(s):null,r=s?Ht(s):null,a=n.x+n.width/2-i.width/2,l=n.y+n.height/2-i.height/2;switch(o){case ze:t={x:a,y:n.y-i.height};break;case Ve:t={x:a,y:n.y+n.height};break;case Ue:t={x:n.x+n.width,y:l};break;case Qe:t={x:n.x-i.width,y:l};break;default:t={x:n.x,y:n.y}}var c=o?Pt(o):null;if(null!=c){var u="y"===c?"height":"width";switch(r){case Xe:t[c]=t[c]-(n[u]/2-i[u]/2);break;case Je:t[c]=t[c]+(n[u]/2-i[u]/2)}}return t}function on(e,t){void 0===t&&(t={});var n=t,i=n.placement,s=void 0===i?e.placement:i,o=n.strategy,r=void 0===o?e.strategy:o,a=n.boundary,l=void 0===a?Ge:a,c=n.rootBoundary,u=void 0===c?Ze:c,d=n.elementContext,h=void 0===d?et:d,f=n.altBoundary,p=void 0!==f&&f,m=n.padding,g=void 0===m?0:m,_=jt("number"!=typeof g?g:qt(g,Ye)),v=h===et?tt:et,b=e.rects.popper,y=e.elements[p?v:h],w=function(e,t,n,i){var s="clippingParents"===t?function(e){var t=en(It(e)),n=["absolute","fixed"].indexOf(Lt(e).position)>=0&&_t(e)?$t(e):e;return gt(n)?t.filter((function(e){return gt(e)&&Ot(e,n)&&"body"!==pt(e)})):[]}(e):[].concat(t),o=[].concat(s,[n]),r=o[0],a=o.reduce((function(t,n){var s=nn(e,n,i);return t.top=wt(s.top,t.top),t.right=Et(s.right,t.right),t.bottom=Et(s.bottom,t.bottom),t.left=wt(s.left,t.left),t}),nn(e,r,i));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(gt(y)?y:y.contextElement||Dt(e.elements.popper),l,u,r),E=St(e.elements.reference),A=sn({reference:E,element:b,placement:s}),T=tn(Object.assign({},b,A)),C=h===et?T:E,S={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=e.modifiersData.offset;if(h===et&&x){var O=x[s];Object.keys(S).forEach((function(e){var t=[Ue,Ve].indexOf(e)>=0?1:-1,n=[ze,Ve].indexOf(e)>=0?"y":"x";S[e]+=O[n]*t}))}return S}function rn(e,t){void 0===t&&(t={});var n=t,i=n.placement,s=n.boundary,o=n.rootBoundary,r=n.padding,a=n.flipVariations,l=n.allowedAutoPlacements,c=void 0===l?it:l,u=Ht(i),d=u?a?nt:nt.filter((function(e){return Ht(e)===u})):Ye,h=d.filter((function(e){return c.indexOf(e)>=0}));0===h.length&&(h=d);var f=h.reduce((function(t,n){return t[n]=on(e,{placement:n,boundary:s,rootBoundary:o,padding:r})[yt(n)],t}),{});return Object.keys(f).sort((function(e,t){return f[e]-f[t]}))}const an={name:"flip",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options,i=e.name;if(!t.modifiersData[i]._skip){for(var s=n.mainAxis,o=void 0===s||s,r=n.altAxis,a=void 0===r||r,l=n.fallbackPlacements,c=n.padding,u=n.boundary,d=n.rootBoundary,h=n.altBoundary,f=n.flipVariations,p=void 0===f||f,m=n.allowedAutoPlacements,g=t.options.placement,_=yt(g),v=l||(_!==g&&p?function(e){if(yt(e)===Ke)return[];var t=Qt(e);return[Yt(e),t,Yt(t)]}(g):[Qt(g)]),b=[g].concat(v).reduce((function(e,n){return e.concat(yt(n)===Ke?rn(t,{placement:n,boundary:u,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):n)}),[]),y=t.rects.reference,w=t.rects.popper,E=new Map,A=!0,T=b[0],C=0;C<b.length;C++){var S=b[C],x=yt(S),O=Ht(S)===Xe,L=[ze,Ve].indexOf(x)>=0,k=L?"width":"height",D=on(t,{placement:S,boundary:u,rootBoundary:d,altBoundary:h,padding:c}),I=L?O?Ue:Qe:O?Ve:ze;y[k]>w[k]&&(I=Qt(I));var M=Qt(I),$=[];if(o&&$.push(D[x]<=0),a&&$.push(D[I]<=0,D[M]<=0),$.every((function(e){return e}))){T=S,A=!1;break}E.set(S,$)}if(A)for(var P=function(e){var t=b.find((function(t){var n=E.get(t);if(n)return n.slice(0,e).every((function(e){return e}))}));if(t)return T=t,"break"},N=p?3:1;N>0&&"break"!==P(N);N--);t.placement!==T&&(t.modifiersData[i]._skip=!0,t.placement=T,t.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function ln(e,t,n){return void 0===n&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function cn(e){return[ze,Ue,Ve,Qe].some((function(t){return e[t]>=0}))}const un={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(e){var t=e.state,n=e.name,i=t.rects.reference,s=t.rects.popper,o=t.modifiersData.preventOverflow,r=on(t,{elementContext:"reference"}),a=on(t,{altBoundary:!0}),l=ln(r,i),c=ln(a,s,o),u=cn(l),d=cn(c);t.modifiersData[n]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:u,hasPopperEscaped:d},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":u,"data-popper-escaped":d})}},dn={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(e){var t=e.state,n=e.options,i=e.name,s=n.offset,o=void 0===s?[0,0]:s,r=it.reduce((function(e,n){return e[n]=function(e,t,n){var i=yt(e),s=[Qe,ze].indexOf(i)>=0?-1:1,o="function"==typeof n?n(Object.assign({},t,{placement:e})):n,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Qe,Ue].indexOf(i)>=0?{x:a,y:r}:{x:r,y:a}}(n,t.rects,o),e}),{}),a=r[t.placement],l=a.x,c=a.y;null!=t.modifiersData.popperOffsets&&(t.modifiersData.popperOffsets.x+=l,t.modifiersData.popperOffsets.y+=c),t.modifiersData[i]=r}},hn={name:"popperOffsets",enabled:!0,phase:"read",fn:function(e){var t=e.state,n=e.name;t.modifiersData[n]=sn({reference:t.rects.reference,element:t.rects.popper,placement:t.placement})},data:{}},fn={name:"preventOverflow",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options,i=e.name,s=n.mainAxis,o=void 0===s||s,r=n.altAxis,a=void 0!==r&&r,l=n.boundary,c=n.rootBoundary,u=n.altBoundary,d=n.padding,h=n.tether,f=void 0===h||h,p=n.tetherOffset,m=void 0===p?0:p,g=on(t,{boundary:l,rootBoundary:c,padding:d,altBoundary:u}),_=yt(t.placement),v=Ht(t.placement),b=!v,y=Pt(_),w="x"===y?"y":"x",E=t.modifiersData.popperOffsets,A=t.rects.reference,T=t.rects.popper,C="function"==typeof m?m(Object.assign({},t.rects,{placement:t.placement})):m,S="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,O={x:0,y:0};if(E){if(o){var L,k="y"===y?ze:Qe,D="y"===y?Ve:Ue,I="y"===y?"height":"width",M=E[y],$=M+g[k],P=M-g[D],N=f?-T[I]/2:0,j=v===Xe?A[I]:T[I],q=v===Xe?-T[I]:-A[I],F=t.elements.arrow,H=f&&F?xt(F):{width:0,height:0},R=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},B=R[k],W=R[D],z=Nt(0,A[I],H[I]),V=b?A[I]/2-N-z-B-S.mainAxis:j-z-B-S.mainAxis,U=b?-A[I]/2+N+z+W+S.mainAxis:q+z+W+S.mainAxis,Q=t.elements.arrow&&$t(t.elements.arrow),K=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,X=M+U-Y,J=Nt(f?Et($,M+V-Y-K):$,M,f?wt(P,X):P);E[y]=J,O[y]=J-M}if(a){var G,Z="x"===y?ze:Qe,ee="x"===y?Ve:Ue,te=E[w],ne="y"===w?"height":"width",ie=te+g[Z],se=te-g[ee],oe=-1!==[ze,Qe].indexOf(_),re=null!=(G=null==x?void 0:x[w])?G:0,ae=oe?ie:te-A[ne]-T[ne]-re+S.altAxis,le=oe?te+A[ne]+T[ne]-re-S.altAxis:se,ce=f&&oe?function(e,t,n){var i=Nt(e,t,n);return i>n?n:i}(ae,te,le):Nt(f?ae:ie,te,f?le:se);E[w]=ce,O[w]=ce-te}t.modifiersData[i]=O}},requiresIfExists:["offset"]};function pn(e,t,n){void 0===n&&(n=!1);var i,s,o=_t(t),r=_t(t)&&function(e){var t=e.getBoundingClientRect(),n=At(t.width)/e.offsetWidth||1,i=At(t.height)/e.offsetHeight||1;return 1!==n||1!==i}(t),a=Dt(t),l=St(e,r,n),c={scrollLeft:0,scrollTop:0},u={x:0,y:0};return(o||!o&&!n)&&(("body"!==pt(t)||Gt(a))&&(c=(i=t)!==mt(i)&&_t(i)?{scrollLeft:(s=i).scrollLeft,scrollTop:s.scrollTop}:Xt(i)),_t(t)?((u=St(t,!0)).x+=t.clientLeft,u.y+=t.clientTop):a&&(u.x=Jt(a))),{x:l.left+c.scrollLeft-u.x,y:l.top+c.scrollTop-u.y,width:l.width,height:l.height}}function mn(e){var t=new Map,n=new Set,i=[];function s(e){n.add(e.name),[].concat(e.requires||[],e.requiresIfExists||[]).forEach((function(e){if(!n.has(e)){var i=t.get(e);i&&s(i)}})),i.push(e)}return e.forEach((function(e){t.set(e.name,e)})),e.forEach((function(e){n.has(e.name)||s(e)})),i}var gn={placement:"bottom",modifiers:[],strategy:"absolute"};function _n(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return!t.some((function(e){return!(e&&"function"==typeof e.getBoundingClientRect)}))}function vn(e){void 0===e&&(e={});var t=e,n=t.defaultModifiers,i=void 0===n?[]:n,s=t.defaultOptions,o=void 0===s?gn:s;return function(e,t,n){void 0===n&&(n=o);var s,r,a={placement:"bottom",orderedModifiers:[],options:Object.assign({},gn,o),modifiersData:{},elements:{reference:e,popper:t},attributes:{},styles:{}},l=[],c=!1,u={state:a,setOptions:function(n){var s="function"==typeof n?n(a.options):n;d(),a.options=Object.assign({},o,a.options,s),a.scrollParents={reference:gt(e)?en(e):e.contextElement?en(e.contextElement):[],popper:en(t)};var r,c,h=function(e){var t=mn(e);return ft.reduce((function(e,n){return e.concat(t.filter((function(e){return e.phase===n})))}),[])}((r=[].concat(i,a.options.modifiers),c=r.reduce((function(e,t){var n=e[t.name];return e[t.name]=n?Object.assign({},n,t,{options:Object.assign({},n.options,t.options),data:Object.assign({},n.data,t.data)}):t,e}),{}),Object.keys(c).map((function(e){return c[e]}))));return a.orderedModifiers=h.filter((function(e){return e.enabled})),a.orderedModifiers.forEach((function(e){var t=e.name,n=e.options,i=void 0===n?{}:n,s=e.effect;if("function"==typeof s){var o=s({state:a,name:t,instance:u,options:i});l.push(o||function(){})}})),u.update()},forceUpdate:function(){if(!c){var e=a.elements,t=e.reference,n=e.popper;if(_n(t,n)){a.rects={reference:pn(t,$t(n),"fixed"===a.options.strategy),popper:xt(n)},a.reset=!1,a.placement=a.options.placement,a.orderedModifiers.forEach((function(e){return a.modifiersData[e.name]=Object.assign({},e.data)}));for(var i=0;i<a.orderedModifiers.length;i++)if(!0!==a.reset){var s=a.orderedModifiers[i],o=s.fn,r=s.options,l=void 0===r?{}:r,d=s.name;"function"==typeof o&&(a=o({state:a,options:l,name:d,instance:u})||a)}else a.reset=!1,i=-1}}},update:(s=function(){return new Promise((function(e){u.forceUpdate(),e(a)}))},function(){return r||(r=new Promise((function(e){Promise.resolve().then((function(){r=void 0,e(s())}))}))),r}),destroy:function(){d(),c=!0}};if(!_n(e,t))return u;function d(){l.forEach((function(e){return e()})),l=[]}return u.setOptions(n).then((function(e){!c&&n.onFirstUpdate&&n.onFirstUpdate(e)})),u}}var bn=vn(),yn=vn({defaultModifiers:[Vt,hn,Wt,bt]}),wn=vn({defaultModifiers:[Vt,hn,Wt,bt,dn,an,fn,Ft,un]});const En=Object.freeze(Object.defineProperty({__proto__:null,afterMain:ct,afterRead:rt,afterWrite:ht,applyStyles:bt,arrow:Ft,auto:Ke,basePlacements:Ye,beforeMain:at,beforeRead:st,beforeWrite:ut,bottom:Ve,clippingParents:Ge,computeStyles:Wt,createPopper:wn,createPopperBase:bn,createPopperLite:yn,detectOverflow:on,end:Je,eventListeners:Vt,flip:an,hide:un,left:Qe,main:lt,modifierPhases:ft,offset:dn,placements:it,popper:et,popperGenerator:vn,popperOffsets:hn,preventOverflow:fn,read:ot,reference:tt,right:Ue,start:Xe,top:ze,variationPlacements:nt,viewport:Ze,write:dt},Symbol.toStringTag,{value:"Module"})),An="dropdown",Tn=".bs.dropdown",Cn=".data-api",Sn="ArrowUp",xn="ArrowDown",On=`hide${Tn}`,Ln=`hidden${Tn}`,kn=`show${Tn}`,Dn=`shown${Tn}`,In=`click${Tn}${Cn}`,Mn=`keydown${Tn}${Cn}`,$n=`keyup${Tn}${Cn}`,Pn="show",Nn='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',jn=`${Nn}.${Pn}`,qn=".dropdown-menu",Fn=p()?"top-end":"top-start",Hn=p()?"top-start":"top-end",Rn=p()?"bottom-end":"bottom-start",Bn=p()?"bottom-start":"bottom-end",Wn=p()?"left-start":"right-start",zn=p()?"right-start":"left-start",Vn={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},Un={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class Qn extends H{constructor(e,t){super(e,t),this._popper=null,this._parent=this._element.parentNode,this._menu=B.next(this._element,qn)[0]||B.prev(this._element,qn)[0]||B.findOne(qn,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return Vn}static get DefaultType(){return Un}static get NAME(){return An}toggle(){return this._isShown()?this.hide():this.show()}show(){if(l(this._element)||this._isShown())return;const e={relatedTarget:this._element};if(!$.trigger(this._element,kn,e).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const e of[].concat(...document.body.children))$.on(e,"mouseover",u);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Pn),this._element.classList.add(Pn),$.trigger(this._element,Dn,e)}}hide(){if(l(this._element)||!this._isShown())return;const e={relatedTarget:this._element};this._completeHide(e)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(e){if(!$.trigger(this._element,On,e).defaultPrevented){if("ontouchstart"in document.documentElement)for(const e of[].concat(...document.body.children))$.off(e,"mouseover",u);this._popper&&this._popper.destroy(),this._menu.classList.remove(Pn),this._element.classList.remove(Pn),this._element.setAttribute("aria-expanded","false"),q.removeDataAttribute(this._menu,"popper"),$.trigger(this._element,Ln,e)}}_getConfig(e){if("object"==typeof(e=super._getConfig(e)).reference&&!o(e.reference)&&"function"!=typeof e.reference.getBoundingClientRect)throw new TypeError(`${An.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return e}_createPopper(){if(void 0===En)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org/docs/v2/)");let e=this._element;"parent"===this._config.reference?e=this._parent:o(this._config.reference)?e=r(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference);const t=this._getPopperConfig();this._popper=wn(e,this._menu,t)}_isShown(){return this._menu.classList.contains(Pn)}_getPlacement(){const e=this._parent;if(e.classList.contains("dropend"))return Wn;if(e.classList.contains("dropstart"))return zn;if(e.classList.contains("dropup-center"))return"top";if(e.classList.contains("dropdown-center"))return"bottom";const t="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return e.classList.contains("dropup")?t?Hn:Fn:t?Bn:Rn}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:e}=this._config;return"string"==typeof e?e.split(",").map((e=>Number.parseInt(e,10))):"function"==typeof e?t=>e(t,this._element):e}_getPopperConfig(){const e={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(q.setDataAttribute(this._menu,"popper","static"),e.modifiers=[{name:"applyStyles",enabled:!1}]),{...e,...g(this._config.popperConfig,[void 0,e])}}_selectMenuItem({key:e,target:t}){const n=B.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((e=>a(e)));n.length&&v(n,t,e===xn,!n.includes(t)).focus()}static jQueryInterface(e){return this.each((function(){const t=Qn.getOrCreateInstance(this,e);if("string"==typeof e){if(void 0===t[e])throw new TypeError(`No method named "${e}"`);t[e]()}}))}static clearMenus(e){if(2===e.button||"keyup"===e.type&&"Tab"!==e.key)return;const t=B.find(jn);for(const n of t){const t=Qn.getInstance(n);if(!t||!1===t._config.autoClose)continue;const i=e.composedPath(),s=i.includes(t._menu);if(i.includes(t._element)||"inside"===t._config.autoClose&&!s||"outside"===t._config.autoClose&&s)continue;if(t._menu.contains(e.target)&&("keyup"===e.type&&"Tab"===e.key||/input|select|option|textarea|form/i.test(e.target.tagName)))continue;const o={relatedTarget:t._element};"click"===e.type&&(o.clickEvent=e),t._completeHide(o)}}static dataApiKeydownHandler(e){const t=/input|textarea/i.test(e.target.tagName),n="Escape"===e.key,i=[Sn,xn].includes(e.key);if(!i&&!n)return;if(t&&!n)return;e.preventDefault();const s=this.matches(Nn)?this:B.prev(this,Nn)[0]||B.next(this,Nn)[0]||B.findOne(Nn,e.delegateTarget.parentNode),o=Qn.getOrCreateInstance(s);if(i)return e.stopPropagation(),o.show(),void o._selectMenuItem(e);o._isShown()&&(e.stopPropagation(),o.hide(),s.focus())}}$.on(document,Mn,Nn,Qn.dataApiKeydownHandler),$.on(document,Mn,qn,Qn.dataApiKeydownHandler),$.on(document,In,Qn.clearMenus),$.on(document,$n,Qn.clearMenus),$.on(document,In,Nn,(function(e){e.preventDefault(),Qn.getOrCreateInstance(this).toggle()})),m(Qn);const Kn="backdrop",Yn="show",Xn=`mousedown.bs.${Kn}`,Jn={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Gn={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Zn extends F{constructor(e){super(),this._config=this._getConfig(e),this._isAppended=!1,this._element=null}static get Default(){return Jn}static get DefaultType(){return Gn}static get NAME(){return Kn}show(e){if(!this._config.isVisible)return void g(e);this._append();const t=this._getElement();this._config.isAnimated&&d(t),t.classList.add(Yn),this._emulateAnimation((()=>{g(e)}))}hide(e){this._config.isVisible?(this._getElement().classList.remove(Yn),this._emulateAnimation((()=>{this.dispose(),g(e)}))):g(e)}dispose(){this._isAppended&&($.off(this._element,Xn),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const e=document.createElement("div");e.className=this._config.className,this._config.isAnimated&&e.classList.add("fade"),this._element=e}return this._element}_configAfterMerge(e){return e.rootElement=r(e.rootElement),e}_append(){if(this._isAppended)return;const e=this._getElement();this._config.rootElement.append(e),$.on(e,Xn,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(e){_(e,this._getElement(),this._config.isAnimated)}}const ei=".bs.focustrap",ti=`focusin${ei}`,ni=`keydown.tab${ei}`,ii="backward",si={autofocus:!0,trapElement:null},oi={autofocus:"boolean",trapElement:"element"};class ri extends F{constructor(e){super(),this._config=this._getConfig(e),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return si}static get DefaultType(){return oi}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),$.off(document,ei),$.on(document,ti,(e=>this._handleFocusin(e))),$.on(document,ni,(e=>this._handleKeydown(e))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,$.off(document,ei))}_handleFocusin(e){const{trapElement:t}=this._config;if(e.target===document||e.target===t||t.contains(e.target))return;const n=B.focusableChildren(t);0===n.length?t.focus():this._lastTabNavDirection===ii?n[n.length-1].focus():n[0].focus()}_handleKeydown(e){"Tab"===e.key&&(this._lastTabNavDirection=e.shiftKey?ii:"forward")}}const ai=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",li=".sticky-top",ci="padding-right",ui="margin-right";class di{constructor(){this._element=document.body}getWidth(){const e=document.documentElement.clientWidth;return Math.abs(window.innerWidth-e)}hide(){const e=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,ci,(t=>t+e)),this._setElementAttributes(ai,ci,(t=>t+e)),this._setElementAttributes(li,ui,(t=>t-e))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,ci),this._resetElementAttributes(ai,ci),this._resetElementAttributes(li,ui)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(e,t,n){const i=this.getWidth();this._applyManipulationCallback(e,(e=>{if(e!==this._element&&window.innerWidth>e.clientWidth+i)return;this._saveInitialAttribute(e,t);const s=window.getComputedStyle(e).getPropertyValue(t);e.style.setProperty(t,`${n(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(e,t){const n=e.style.getPropertyValue(t);n&&q.setDataAttribute(e,t,n)}_resetElementAttributes(e,t){this._applyManipulationCallback(e,(e=>{const n=q.getDataAttribute(e,t);null!==n?(q.removeDataAttribute(e,t),e.style.setProperty(t,n)):e.style.removeProperty(t)}))}_applyManipulationCallback(e,t){if(o(e))t(e);else for(const n of B.find(e,this._element))t(n)}}const hi=".bs.modal",fi=`hide${hi}`,pi=`hidePrevented${hi}`,mi=`hidden${hi}`,gi=`show${hi}`,_i=`shown${hi}`,vi=`resize${hi}`,bi=`click.dismiss${hi}`,yi=`mousedown.dismiss${hi}`,wi=`keydown.dismiss${hi}`,Ei=`click${hi}.data-api`,Ai="modal-open",Ti="show",Ci="modal-static",Si={backdrop:!0,focus:!0,keyboard:!0},xi={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Oi extends H{constructor(e,t){super(e,t),this._dialog=B.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new di,this._addEventListeners()}static get Default(){return Si}static get DefaultType(){return xi}static get NAME(){return"modal"}toggle(e){return this._isShown?this.hide():this.show(e)}show(e){this._isShown||this._isTransitioning||$.trigger(this._element,gi,{relatedTarget:e}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Ai),this._adjustDialog(),this._backdrop.show((()=>this._showElement(e))))}hide(){this._isShown&&!this._isTransitioning&&($.trigger(this._element,fi).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Ti),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){$.off(window,hi),$.off(this._dialog,hi),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Zn({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new ri({trapElement:this._element})}_showElement(e){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const t=B.findOne(".modal-body",this._dialog);t&&(t.scrollTop=0),d(this._element),this._element.classList.add(Ti),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,$.trigger(this._element,_i,{relatedTarget:e})}),this._dialog,this._isAnimated())}_addEventListeners(){$.on(this._element,wi,(e=>{"Escape"===e.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),$.on(window,vi,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),$.on(this._element,yi,(e=>{$.one(this._element,bi,(t=>{this._element===e.target&&this._element===t.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Ai),this._resetAdjustments(),this._scrollBar.reset(),$.trigger(this._element,mi)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if($.trigger(this._element,pi).defaultPrevented)return;const e=this._element.scrollHeight>document.documentElement.clientHeight,t=this._element.style.overflowY;"hidden"===t||this._element.classList.contains(Ci)||(e||(this._element.style.overflowY="hidden"),this._element.classList.add(Ci),this._queueCallback((()=>{this._element.classList.remove(Ci),this._queueCallback((()=>{this._element.style.overflowY=t}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const e=this._element.scrollHeight>document.documentElement.clientHeight,t=this._scrollBar.getWidth(),n=t>0;if(n&&!e){const e=p()?"paddingLeft":"paddingRight";this._element.style[e]=`${t}px`}if(!n&&e){const e=p()?"paddingRight":"paddingLeft";this._element.style[e]=`${t}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(e,t){return this.each((function(){const n=Oi.getOrCreateInstance(this,e);if("string"==typeof e){if(void 0===n[e])throw new TypeError(`No method named "${e}"`);n[e](t)}}))}}$.on(document,Ei,'[data-bs-toggle="modal"]',(function(e){const t=B.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&e.preventDefault(),$.one(t,gi,(e=>{e.defaultPrevented||$.one(t,mi,(()=>{a(this)&&this.focus()}))}));const n=B.findOne(".modal.show");n&&Oi.getInstance(n).hide(),Oi.getOrCreateInstance(t).toggle(this)})),W(Oi),m(Oi);const Li=".bs.offcanvas",ki=".data-api",Di=`load${Li}${ki}`,Ii="show",Mi="showing",$i="hiding",Pi=".offcanvas.show",Ni=`show${Li}`,ji=`shown${Li}`,qi=`hide${Li}`,Fi=`hidePrevented${Li}`,Hi=`hidden${Li}`,Ri=`resize${Li}`,Bi=`click${Li}${ki}`,Wi=`keydown.dismiss${Li}`,zi={backdrop:!0,keyboard:!0,scroll:!1},Vi={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Ui extends H{constructor(e,t){super(e,t),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return zi}static get DefaultType(){return Vi}static get NAME(){return"offcanvas"}toggle(e){return this._isShown?this.hide():this.show(e)}show(e){this._isShown||$.trigger(this._element,Ni,{relatedTarget:e}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new di).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Mi),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Ii),this._element.classList.remove(Mi),$.trigger(this._element,ji,{relatedTarget:e})}),this._element,!0))}hide(){this._isShown&&($.trigger(this._element,qi).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add($i),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Ii,$i),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new di).reset(),$.trigger(this._element,Hi)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const e=Boolean(this._config.backdrop);return new Zn({className:"offcanvas-backdrop",isVisible:e,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:e?()=>{"static"!==this._config.backdrop?this.hide():$.trigger(this._element,Fi)}:null})}_initializeFocusTrap(){return new ri({trapElement:this._element})}_addEventListeners(){$.on(this._element,Wi,(e=>{"Escape"===e.key&&(this._config.keyboard?this.hide():$.trigger(this._element,Fi))}))}static jQueryInterface(e){return this.each((function(){const t=Ui.getOrCreateInstance(this,e);if("string"==typeof e){if(void 0===t[e]||e.startsWith("_")||"constructor"===e)throw new TypeError(`No method named "${e}"`);t[e](this)}}))}}$.on(document,Bi,'[data-bs-toggle="offcanvas"]',(function(e){const t=B.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&e.preventDefault(),l(this))return;$.one(t,Hi,(()=>{a(this)&&this.focus()}));const n=B.findOne(Pi);n&&n!==t&&Ui.getInstance(n).hide(),Ui.getOrCreateInstance(t).toggle(this)})),$.on(window,Di,(()=>{for(const e of B.find(Pi))Ui.getOrCreateInstance(e).show()})),$.on(window,Ri,(()=>{for(const e of B.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(e).position&&Ui.getOrCreateInstance(e).hide()})),W(Ui),m(Ui);const Qi={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Ki=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Yi=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xi=(e,t)=>{const n=e.nodeName.toLowerCase();return t.includes(n)?!Ki.has(n)||Boolean(Yi.test(e.nodeValue)):t.filter((e=>e instanceof RegExp)).some((e=>e.test(n)))},Ji={allowList:Qi,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"<div></div>"},Gi={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Zi={entry:"(string|element|function|null)",selector:"(string|element)"};class es extends F{constructor(e){super(),this._config=this._getConfig(e)}static get Default(){return Ji}static get DefaultType(){return Gi}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((e=>this._resolvePossibleFunction(e))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(e){return this._checkContent(e),this._config.content={...this._config.content,...e},this}toHtml(){const e=document.createElement("div");e.innerHTML=this._maybeSanitize(this._config.template);for(const[t,n]of Object.entries(this._config.content))this._setContent(e,n,t);const t=e.children[0],n=this._resolvePossibleFunction(this._config.extraClass);return n&&t.classList.add(...n.split(" ")),t}_typeCheckConfig(e){super._typeCheckConfig(e),this._checkContent(e.content)}_checkContent(e){for(const[t,n]of Object.entries(e))super._typeCheckConfig({selector:t,entry:n},Zi)}_setContent(e,t,n){const i=B.findOne(n,e);i&&((t=this._resolvePossibleFunction(t))?o(t)?this._putElementInTemplate(r(t),i):this._config.html?i.innerHTML=this._maybeSanitize(t):i.textContent=t:i.remove())}_maybeSanitize(e){return this._config.sanitize?function(e,t,n){if(!e.length)return e;if(n&&"function"==typeof n)return n(e);const i=(new window.DOMParser).parseFromString(e,"text/html"),s=[].concat(...i.body.querySelectorAll("*"));for(const e of s){const n=e.nodeName.toLowerCase();if(!Object.keys(t).includes(n)){e.remove();continue}const i=[].concat(...e.attributes),s=[].concat(t["*"]||[],t[n]||[]);for(const t of i)Xi(t,s)||e.removeAttribute(t.nodeName)}return i.body.innerHTML}(e,this._config.allowList,this._config.sanitizeFn):e}_resolvePossibleFunction(e){return g(e,[void 0,this])}_putElementInTemplate(e,t){if(this._config.html)return t.innerHTML="",void t.append(e);t.textContent=e.textContent}}const ts=new Set(["sanitize","allowList","sanitizeFn"]),ns="fade",is="show",ss=".tooltip-inner",os=".modal",rs="hide.bs.modal",as="hover",ls="focus",cs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},us={allowList:Qi,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',title:"",trigger:"hover focus"},ds={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class hs extends H{constructor(e,t){if(void 0===En)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)");super(e,t),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return us}static get DefaultType(){return ds}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),$.off(this._element.closest(os),rs,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const e=$.trigger(this._element,this.constructor.eventName("show")),t=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(e.defaultPrevented||!t)return;this._disposePopper();const n=this._getTipElement();this._element.setAttribute("aria-describedby",n.getAttribute("id"));const{container:i}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(i.append(n),$.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(n),n.classList.add(is),"ontouchstart"in document.documentElement)for(const e of[].concat(...document.body.children))$.on(e,"mouseover",u);this._queueCallback((()=>{$.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!$.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(is),"ontouchstart"in document.documentElement)for(const e of[].concat(...document.body.children))$.off(e,"mouseover",u);this._activeTrigger.click=!1,this._activeTrigger[ls]=!1,this._activeTrigger[as]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),$.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(e){const t=this._getTemplateFactory(e).toHtml();if(!t)return null;t.classList.remove(ns,is),t.classList.add(`bs-${this.constructor.NAME}-auto`);const n=(e=>{do{e+=Math.floor(1e6*Math.random())}while(document.getElementById(e));return e})(this.constructor.NAME).toString();return t.setAttribute("id",n),this._isAnimated()&&t.classList.add(ns),t}setContent(e){this._newContent=e,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(e){return this._templateFactory?this._templateFactory.changeContent(e):this._templateFactory=new es({...this._config,content:e,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[ss]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(e){return this.constructor.getOrCreateInstance(e.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ns)}_isShown(){return this.tip&&this.tip.classList.contains(is)}_createPopper(e){const t=g(this._config.placement,[this,e,this._element]),n=cs[t.toUpperCase()];return wn(this._element,e,this._getPopperConfig(n))}_getOffset(){const{offset:e}=this._config;return"string"==typeof e?e.split(",").map((e=>Number.parseInt(e,10))):"function"==typeof e?t=>e(t,this._element):e}_resolvePossibleFunction(e){return g(e,[this._element,this._element])}_getPopperConfig(e){const t={placement:e,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:e=>{this._getTipElement().setAttribute("data-popper-placement",e.state.placement)}}]};return{...t,...g(this._config.popperConfig,[void 0,t])}}_setListeners(){const e=this._config.trigger.split(" ");for(const t of e)if("click"===t)$.on(this._element,this.constructor.eventName("click"),this._config.selector,(e=>{this._initializeOnDelegatedTarget(e).toggle()}));else if("manual"!==t){const e=t===as?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),n=t===as?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");$.on(this._element,e,this._config.selector,(e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger["focusin"===e.type?ls:as]=!0,t._enter()})),$.on(this._element,n,this._config.selector,(e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger["focusout"===e.type?ls:as]=t._element.contains(e.relatedTarget),t._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},$.on(this._element.closest(os),rs,this._hideModalHandler)}_fixTitle(){const e=this._element.getAttribute("title");e&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",e),this._element.setAttribute("data-bs-original-title",e),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(e,t){clearTimeout(this._timeout),this._timeout=setTimeout(e,t)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(e){const t=q.getDataAttributes(this._element);for(const e of Object.keys(t))ts.has(e)&&delete t[e];return e={...t,..."object"==typeof e&&e?e:{}},e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e.container=!1===e.container?document.body:r(e.container),"number"==typeof e.delay&&(e.delay={show:e.delay,hide:e.delay}),"number"==typeof e.title&&(e.title=e.title.toString()),"number"==typeof e.content&&(e.content=e.content.toString()),e}_getDelegateConfig(){const e={};for(const[t,n]of Object.entries(this._config))this.constructor.Default[t]!==n&&(e[t]=n);return e.selector=!1,e.trigger="manual",e}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(e){return this.each((function(){const t=hs.getOrCreateInstance(this,e);if("string"==typeof e){if(void 0===t[e])throw new TypeError(`No method named "${e}"`);t[e]()}}))}}m(hs);const fs=".popover-header",ps=".popover-body",ms={...hs.Default,content:"",offset:[0,8],placement:"right",template:'<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>',trigger:"click"},gs={...hs.DefaultType,content:"(null|string|element|function)"};class _s extends hs{static get Default(){return ms}static get DefaultType(){return gs}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[fs]:this._getTitle(),[ps]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(e){return this.each((function(){const t=_s.getOrCreateInstance(this,e);if("string"==typeof e){if(void 0===t[e])throw new TypeError(`No method named "${e}"`);t[e]()}}))}}m(_s);const vs=".bs.scrollspy",bs=`activate${vs}`,ys=`click${vs}`,ws=`load${vs}.data-api`,Es="active",As="[href]",Ts=".nav-link",Cs=`${Ts}, .nav-item > ${Ts}, .list-group-item`,Ss={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},xs={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Os extends H{constructor(e,t){super(e,t),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return Ss}static get DefaultType(){return xs}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const e of this._observableSections.values())this._observer.observe(e)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(e){return e.target=r(e.target)||document.body,e.rootMargin=e.offset?`${e.offset}px 0px -30%`:e.rootMargin,"string"==typeof e.threshold&&(e.threshold=e.threshold.split(",").map((e=>Number.parseFloat(e)))),e}_maybeEnableSmoothScroll(){this._config.smoothScroll&&($.off(this._config.target,ys),$.on(this._config.target,ys,As,(e=>{const t=this._observableSections.get(e.target.hash);if(t){e.preventDefault();const n=this._rootElement||window,i=t.offsetTop-this._element.offsetTop;if(n.scrollTo)return void n.scrollTo({top:i,behavior:"smooth"});n.scrollTop=i}})))}_getNewObserver(){const e={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((e=>this._observerCallback(e)),e)}_observerCallback(e){const t=e=>this._targetLinks.get(`#${e.target.id}`),n=e=>{this._previousScrollData.visibleEntryTop=e.target.offsetTop,this._process(t(e))},i=(this._rootElement||document.documentElement).scrollTop,s=i>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=i;for(const o of e){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(t(o));continue}const e=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&e){if(n(o),!i)return}else s||e||n(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const e=B.find(As,this._config.target);for(const t of e){if(!t.hash||l(t))continue;const e=B.findOne(decodeURI(t.hash),this._element);a(e)&&(this._targetLinks.set(decodeURI(t.hash),t),this._observableSections.set(t.hash,e))}}_process(e){this._activeTarget!==e&&(this._clearActiveClass(this._config.target),this._activeTarget=e,e.classList.add(Es),this._activateParents(e),$.trigger(this._element,bs,{relatedTarget:e}))}_activateParents(e){if(e.classList.contains("dropdown-item"))B.findOne(".dropdown-toggle",e.closest(".dropdown")).classList.add(Es);else for(const t of B.parents(e,".nav, .list-group"))for(const e of B.prev(t,Cs))e.classList.add(Es)}_clearActiveClass(e){e.classList.remove(Es);const t=B.find(`${As}.${Es}`,e);for(const e of t)e.classList.remove(Es)}static jQueryInterface(e){return this.each((function(){const t=Os.getOrCreateInstance(this,e);if("string"==typeof e){if(void 0===t[e]||e.startsWith("_")||"constructor"===e)throw new TypeError(`No method named "${e}"`);t[e]()}}))}}$.on(window,ws,(()=>{for(const e of B.find('[data-bs-spy="scroll"]'))Os.getOrCreateInstance(e)})),m(Os);const Ls=".bs.tab",ks=`hide${Ls}`,Ds=`hidden${Ls}`,Is=`show${Ls}`,Ms=`shown${Ls}`,$s=`click${Ls}`,Ps=`keydown${Ls}`,Ns=`load${Ls}`,js="ArrowLeft",qs="ArrowRight",Fs="ArrowUp",Hs="ArrowDown",Rs="Home",Bs="End",Ws="active",zs="fade",Vs="show",Us=".dropdown-toggle",Qs=`:not(${Us})`,Ks='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Ys=`.nav-link${Qs}, .list-group-item${Qs}, [role="tab"]${Qs}, ${Ks}`,Xs=`.${Ws}[data-bs-toggle="tab"], .${Ws}[data-bs-toggle="pill"], .${Ws}[data-bs-toggle="list"]`;class Js extends H{constructor(e){super(e),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),$.on(this._element,Ps,(e=>this._keydown(e))))}static get NAME(){return"tab"}show(){const e=this._element;if(this._elemIsActive(e))return;const t=this._getActiveElem(),n=t?$.trigger(t,ks,{relatedTarget:e}):null;$.trigger(e,Is,{relatedTarget:t}).defaultPrevented||n&&n.defaultPrevented||(this._deactivate(t,e),this._activate(e,t))}_activate(e,t){e&&(e.classList.add(Ws),this._activate(B.getElementFromSelector(e)),this._queueCallback((()=>{"tab"===e.getAttribute("role")?(e.removeAttribute("tabindex"),e.setAttribute("aria-selected",!0),this._toggleDropDown(e,!0),$.trigger(e,Ms,{relatedTarget:t})):e.classList.add(Vs)}),e,e.classList.contains(zs)))}_deactivate(e,t){e&&(e.classList.remove(Ws),e.blur(),this._deactivate(B.getElementFromSelector(e)),this._queueCallback((()=>{"tab"===e.getAttribute("role")?(e.setAttribute("aria-selected",!1),e.setAttribute("tabindex","-1"),this._toggleDropDown(e,!1),$.trigger(e,Ds,{relatedTarget:t})):e.classList.remove(Vs)}),e,e.classList.contains(zs)))}_keydown(e){if(![js,qs,Fs,Hs,Rs,Bs].includes(e.key))return;e.stopPropagation(),e.preventDefault();const t=this._getChildren().filter((e=>!l(e)));let n;if([Rs,Bs].includes(e.key))n=t[e.key===Rs?0:t.length-1];else{const i=[qs,Hs].includes(e.key);n=v(t,e.target,i,!0)}n&&(n.focus({preventScroll:!0}),Js.getOrCreateInstance(n).show())}_getChildren(){return B.find(Ys,this._parent)}_getActiveElem(){return this._getChildren().find((e=>this._elemIsActive(e)))||null}_setInitialAttributes(e,t){this._setAttributeIfNotExists(e,"role","tablist");for(const e of t)this._setInitialAttributesOnChild(e)}_setInitialAttributesOnChild(e){e=this._getInnerElement(e);const t=this._elemIsActive(e),n=this._getOuterElement(e);e.setAttribute("aria-selected",t),n!==e&&this._setAttributeIfNotExists(n,"role","presentation"),t||e.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(e,"role","tab"),this._setInitialAttributesOnTargetPanel(e)}_setInitialAttributesOnTargetPanel(e){const t=B.getElementFromSelector(e);t&&(this._setAttributeIfNotExists(t,"role","tabpanel"),e.id&&this._setAttributeIfNotExists(t,"aria-labelledby",`${e.id}`))}_toggleDropDown(e,t){const n=this._getOuterElement(e);if(!n.classList.contains("dropdown"))return;const i=(e,i)=>{const s=B.findOne(e,n);s&&s.classList.toggle(i,t)};i(Us,Ws),i(".dropdown-menu",Vs),n.setAttribute("aria-expanded",t)}_setAttributeIfNotExists(e,t,n){e.hasAttribute(t)||e.setAttribute(t,n)}_elemIsActive(e){return e.classList.contains(Ws)}_getInnerElement(e){return e.matches(Ys)?e:B.findOne(Ys,e)}_getOuterElement(e){return e.closest(".nav-item, .list-group-item")||e}static jQueryInterface(e){return this.each((function(){const t=Js.getOrCreateInstance(this);if("string"==typeof e){if(void 0===t[e]||e.startsWith("_")||"constructor"===e)throw new TypeError(`No method named "${e}"`);t[e]()}}))}}$.on(document,$s,Ks,(function(e){["A","AREA"].includes(this.tagName)&&e.preventDefault(),l(this)||Js.getOrCreateInstance(this).show()})),$.on(window,Ns,(()=>{for(const e of B.find(Xs))Js.getOrCreateInstance(e)})),m(Js);const Gs=".bs.toast",Zs=`mouseover${Gs}`,eo=`mouseout${Gs}`,to=`focusin${Gs}`,no=`focusout${Gs}`,io=`hide${Gs}`,so=`hidden${Gs}`,oo=`show${Gs}`,ro=`shown${Gs}`,ao="hide",lo="show",co="showing",uo={animation:"boolean",autohide:"boolean",delay:"number"},ho={animation:!0,autohide:!0,delay:5e3};class fo extends H{constructor(e,t){super(e,t),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return ho}static get DefaultType(){return uo}static get NAME(){return"toast"}show(){$.trigger(this._element,oo).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(ao),d(this._element),this._element.classList.add(lo,co),this._queueCallback((()=>{this._element.classList.remove(co),$.trigger(this._element,ro),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&($.trigger(this._element,io).defaultPrevented||(this._element.classList.add(co),this._queueCallback((()=>{this._element.classList.add(ao),this._element.classList.remove(co,lo),$.trigger(this._element,so)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(lo),super.dispose()}isShown(){return this._element.classList.contains(lo)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(e,t){switch(e.type){case"mouseover":case"mouseout":this._hasMouseInteraction=t;break;case"focusin":case"focusout":this._hasKeyboardInteraction=t}if(t)return void this._clearTimeout();const n=e.relatedTarget;this._element===n||this._element.contains(n)||this._maybeScheduleHide()}_setListeners(){$.on(this._element,Zs,(e=>this._onInteraction(e,!0))),$.on(this._element,eo,(e=>this._onInteraction(e,!1))),$.on(this._element,to,(e=>this._onInteraction(e,!0))),$.on(this._element,no,(e=>this._onInteraction(e,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(e){return this.each((function(){const t=fo.getOrCreateInstance(this,e);if("string"==typeof e){if(void 0===t[e])throw new TypeError(`No method named "${e}"`);t[e](this)}}))}}return W(fo),m(fo),{Alert:Q,Button:Y,Carousel:Le,Collapse:We,Dropdown:Qn,Modal:Oi,Offcanvas:Ui,Popover:_s,ScrollSpy:Os,Tab:Js,Toast:fo,Tooltip:hs}})),
|
| 15 |
+
/*!
|
| 16 |
+
* smooth-scroll v16.1.3
|
| 17 |
+
* Animate scrolling to anchor links
|
| 18 |
+
* (c) 2020 Chris Ferdinandi
|
| 19 |
+
* MIT License
|
| 20 |
+
* http://github.com/cferdinandi/smooth-scroll
|
| 21 |
+
*/
|
| 22 |
+
window.Element&&!Element.prototype.closest&&(Element.prototype.closest=function(e){var t,n=(this.document||this.ownerDocument).querySelectorAll(e),i=this;do{for(t=n.length;--t>=0&&n.item(t)!==i;);}while(t<0&&(i=i.parentElement));return i}),function(){if("function"==typeof window.CustomEvent)return!1;function e(e,t){t=t||{bubbles:!1,cancelable:!1,detail:void 0};var n=document.createEvent("CustomEvent");return n.initCustomEvent(e,t.bubbles,t.cancelable,t.detail),n}e.prototype=window.Event.prototype,window.CustomEvent=e}(),function(){for(var e=0,t=["ms","moz","webkit","o"],n=0;n<t.length&&!window.requestAnimationFrame;++n)window.requestAnimationFrame=window[t[n]+"RequestAnimationFrame"],window.cancelAnimationFrame=window[t[n]+"CancelAnimationFrame"]||window[t[n]+"CancelRequestAnimationFrame"];window.requestAnimationFrame||(window.requestAnimationFrame=function(t,n){var i=(new Date).getTime(),s=Math.max(0,16-(i-e)),o=window.setTimeout((function(){t(i+s)}),s);return e=i+s,o}),window.cancelAnimationFrame||(window.cancelAnimationFrame=function(e){clearTimeout(e)})}(),e="undefined"!=typeof global?global:"undefined"!=typeof window?window:void 0,t=function(e){var t={ignore:"[data-scroll-ignore]",header:null,topOnEmptyHash:!0,speed:500,speedAsDuration:!1,durationMax:null,durationMin:null,clip:!0,offset:0,easing:"easeInOutCubic",customEasing:null,updateURL:!0,popstate:!0,emitEvents:!0},n=function(){var e={};return Array.prototype.forEach.call(arguments,(function(t){for(var n in t){if(!t.hasOwnProperty(n))return;e[n]=t[n]}})),e},i=function(e){"#"===e.charAt(0)&&(e=e.substr(1));for(var t,n=String(e),i=n.length,s=-1,o="",r=n.charCodeAt(0);++s<i;){if(0===(t=n.charCodeAt(s)))throw new InvalidCharacterError("Invalid character: the input contains U+0000.");o+=t>=1&&t<=31||127==t||0===s&&t>=48&&t<=57||1===s&&t>=48&&t<=57&&45===r?"\\"+t.toString(16)+" ":t>=128||45===t||95===t||t>=48&&t<=57||t>=65&&t<=90||t>=97&&t<=122?n.charAt(s):"\\"+n.charAt(s)}return"#"+o},s=function(){return Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,document.body.offsetHeight,document.documentElement.offsetHeight,document.body.clientHeight,document.documentElement.clientHeight)},o=function(t,n,i){0===t&&document.body.focus(),i||(t.focus(),document.activeElement!==t&&(t.setAttribute("tabindex","-1"),t.focus(),t.style.outline="none"),e.scrollTo(0,n))},r=function(t,n,i,s){if(n.emitEvents&&"function"==typeof e.CustomEvent){var o=new CustomEvent(t,{bubbles:!0,detail:{anchor:i,toggle:s}});document.dispatchEvent(o)}};return function(a,l){var c,u,d,h,f={cancelScroll:function(e){cancelAnimationFrame(h),h=null,e||r("scrollCancel",c)},animateScroll:function(i,a,l){f.cancelScroll();var u,p,m=n(c||t,l||{}),g="[object Number]"===Object.prototype.toString.call(i),_=g||!i.tagName?null:i;if(g||_){var v=e.pageYOffset;m.header&&!d&&(d=document.querySelector(m.header));var b,y,w,E=(u=d)?(p=u,parseInt(e.getComputedStyle(p).height,10)+u.offsetTop):0,A=g?i:function(t,n,i,o){var r=0;if(t.offsetParent)do{r+=t.offsetTop,t=t.offsetParent}while(t);return r=Math.max(r-n-i,0),o&&(r=Math.min(r,s()-e.innerHeight)),r}(_,E,parseInt("function"==typeof m.offset?m.offset(i,a):m.offset,10),m.clip),T=A-v,C=s(),S=0,x=function(e,t){var n=t.speedAsDuration?t.speed:Math.abs(e/1e3*t.speed);return t.durationMax&&n>t.durationMax?t.durationMax:t.durationMin&&n<t.durationMin?t.durationMin:parseInt(n,10)}(T,m),O=function(t){b||(b=t),S+=t-b,w=v+T*function(e,t){var n;return"easeInQuad"===e.easing&&(n=t*t),"easeOutQuad"===e.easing&&(n=t*(2-t)),"easeInOutQuad"===e.easing&&(n=t<.5?2*t*t:(4-2*t)*t-1),"easeInCubic"===e.easing&&(n=t*t*t),"easeOutCubic"===e.easing&&(n=--t*t*t+1),"easeInOutCubic"===e.easing&&(n=t<.5?4*t*t*t:(t-1)*(2*t-2)*(2*t-2)+1),"easeInQuart"===e.easing&&(n=t*t*t*t),"easeOutQuart"===e.easing&&(n=1- --t*t*t*t),"easeInOutQuart"===e.easing&&(n=t<.5?8*t*t*t*t:1-8*--t*t*t*t),"easeInQuint"===e.easing&&(n=t*t*t*t*t),"easeOutQuint"===e.easing&&(n=1+--t*t*t*t*t),"easeInOutQuint"===e.easing&&(n=t<.5?16*t*t*t*t*t:1+16*--t*t*t*t*t),e.customEasing&&(n=e.customEasing(t)),n||t}(m,y=(y=0===x?0:S/x)>1?1:y),e.scrollTo(0,Math.floor(w)),function(t,n){var s=e.pageYOffset;if(t==n||s==n||(v<n&&e.innerHeight+s)>=C)return f.cancelScroll(!0),o(i,n,g),r("scrollStop",m,i,a),b=null,h=null,!0}(w,A)||(h=e.requestAnimationFrame(O),b=t)};0===e.pageYOffset&&e.scrollTo(0,0),function(e,t,n){t||history.pushState&&n.updateURL&&history.pushState({smoothScroll:JSON.stringify(n),anchor:e.id},document.title,e===document.documentElement?"#top":"#"+e.id)}(i,g,m),"matchMedia"in e&&e.matchMedia("(prefers-reduced-motion)").matches?o(i,Math.floor(A),!1):(r("scrollStart",m,i,a),f.cancelScroll(!0),e.requestAnimationFrame(O))}}},p=function(t){if(!t.defaultPrevented&&!(0!==t.button||t.metaKey||t.ctrlKey||t.shiftKey)&&"closest"in t.target&&(u=t.target.closest(a))&&"a"===u.tagName.toLowerCase()&&!t.target.closest(c.ignore)&&u.hostname===e.location.hostname&&u.pathname===e.location.pathname&&/#/.test(u.href)){var n,s;try{n=i(decodeURIComponent(u.hash))}catch(e){n=i(u.hash)}if("#"===n){if(!c.topOnEmptyHash)return;s=document.documentElement}else s=document.querySelector(n);(s=s||"#top"!==n?s:document.documentElement)&&(t.preventDefault(),function(t){if(history.replaceState&&t.updateURL&&!history.state){var n=e.location.hash;n=n||"",history.replaceState({smoothScroll:JSON.stringify(t),anchor:n||e.pageYOffset},document.title,n||e.location.href)}}(c),f.animateScroll(s,u))}},m=function(e){if(null!==history.state&&history.state.smoothScroll&&history.state.smoothScroll===JSON.stringify(c)){var t=history.state.anchor;"string"==typeof t&&t&&!(t=document.querySelector(i(history.state.anchor)))||f.animateScroll(t,null,{updateURL:!1})}};return f.destroy=function(){c&&(document.removeEventListener("click",p,!1),e.removeEventListener("popstate",m,!1),f.cancelScroll(),c=null,u=null,d=null,h=null)},function(){if(!("querySelector"in document&&"addEventListener"in e&&"requestAnimationFrame"in e&&"closest"in e.Element.prototype))throw"Smooth Scroll: This browser does not support the required JavaScript methods and browser APIs.";f.destroy(),c=n(t,l||{}),d=c.header?document.querySelector(c.header):null,document.addEventListener("click",p,!1),c.updateURL&&c.popstate&&e.addEventListener("popstate",m,!1)}(),f}},"function"==typeof define&&define.amd?define([],(function(){return t(e)})):"object"==typeof exports?module.exports=t(e):e.SmoothScroll=t(e),(()=>{let e=document.querySelector(".navbar-sticky");if(null==e)return;let t=e.classList,n=e.offsetHeight;t.contains("position-absolute")?window.addEventListener("scroll",(t=>{t.currentTarget.pageYOffset>500?e.classList.add("navbar-stuck"):e.classList.remove("navbar-stuck")})):window.addEventListener("scroll",(t=>{t.currentTarget.pageYOffset>500?(document.body.style.paddingTop=n+"px",e.classList.add("navbar-stuck")):(document.body.style.paddingTop="",e.classList.remove("navbar-stuck"))}))})(),new SmoothScroll("[data-scroll]",{speed:800,speedAsDuration:!0,offset:(e,t)=>t.dataset.scrollOffset||40,header:"[data-scroll-header]",updateURL:!1}),(()=>{const e=document.querySelector(".btn-scroll-top");if(null==e)return;let t=parseInt(600,10);window.addEventListener("scroll",(n=>{n.currentTarget.pageYOffset>t?e.classList.add("show"):e.classList.remove("show")}))})(),(()=>{let e=document.querySelectorAll(".password-toggle");for(let t=0;t<e.length;t++){let n=e[t].querySelector(".form-control");e[t].querySelector(".password-toggle-btn").addEventListener("click",(e=>{"checkbox"===e.target.type&&(e.target.checked?n.type="text":n.type="password")}),!1)}})(),null!==document.querySelector(".rellax")&&new Rellax(".rellax",{horizontal:!0}),(()=>{const e=document.querySelectorAll(".parallax");for(let t=0;t<e.length;t++)new Parallax(e[t])})(),((e,t)=>{for(let n=0;n<e.length;n++)t.call(undefined,n,e[n])})(document.querySelectorAll(".swiper"),((e,t)=>{let n,i;null!=t.dataset.swiperOptions&&(n=JSON.parse(t.dataset.swiperOptions)),n.pager&&(i={pagination:{el:".pagination .list-unstyled",clickable:!0,bulletActiveClass:"active",bulletClass:"page-item",renderBullet:function(e,t){return'<li class="'+t+'"><a href="#" class="page-link btn-icon btn-sm">'+(e+1)+"</a></li>"}}});const s={...n,...i},o=new Swiper(t,s);n.tabs&&o.on("activeIndexChange",(e=>{let t=document.querySelector(e.slides[e.activeIndex].dataset.swiperTab);document.querySelector(e.slides[e.previousIndex].dataset.swiperTab).classList.remove("active"),t.classList.add("active")}))})),(()=>{const e=document.querySelectorAll(".gallery");if(e.length)for(let t=0;t<e.length;t++){const n=!!e[t].dataset.thumbnails,i=!!e[t].dataset.video,s=[lgZoom,lgFullscreen,...i?[lgVideo]:[],...n?[lgThumbnail]:[]];lightGallery(e[t],{selector:".gallery-item",plugins:s,licenseKey:"D4194FDD-48924833-A54AECA3-D6F8E646",download:!1,autoplayVideoOnSlide:!0,zoomFromOrigin:!1,youtubePlayerParams:{modestbranding:1,showinfo:0,rel:0},vimeoPlayerParams:{byline:0,portrait:0,color:"6366f1"}})}})(),(()=>{let e=document.querySelectorAll(".range-slider");for(let t=0;t<e.length;t++){let n=e[t].querySelector(".range-slider-ui"),i=e[t].querySelector(".range-slider-value-min"),s=e[t].querySelector(".range-slider-value-max"),o={dataStartMin:parseInt(e[t].dataset.startMin,10),dataStartMax:parseInt(e[t].dataset.startMax,10),dataMin:parseInt(e[t].dataset.min,10),dataMax:parseInt(e[t].dataset.max,10),dataStep:parseInt(e[t].dataset.step,10),dataPips:e[t].dataset.pips,dataTooltips:!e[t].dataset.tooltips||"true"===e[t].dataset.tooltips,dataTooltipPrefix:e[t].dataset.tooltipPrefix||"",dataTooltipSuffix:e[t].dataset.tooltipSuffix||""},r=o.dataStartMax?[o.dataStartMin,o.dataStartMax]:[o.dataStartMin],a=!!o.dataStartMax||"lower";noUiSlider.create(n,{start:r,connect:a,step:o.dataStep,pips:!!o.dataPips&&{mode:"count",values:5},tooltips:o.dataTooltips,range:{min:o.dataMin,max:o.dataMax},format:{to:function(e){return o.dataTooltipPrefix+parseInt(e,10)+o.dataTooltipSuffix},from:function(e){return Number(e)}}}),n.noUiSlider.on("update",((e,t)=>{let n=e[t];n=n.replace(/\D/g,""),t?s&&(s.value=Math.round(n)):i&&(i.value=Math.round(n))})),i&&i.addEventListener("change",(function(){n.noUiSlider.set([this.value,null])})),s&&s.addEventListener("change",(function(){n.noUiSlider.set([null,this.value])}))}})(),window.addEventListener("load",(()=>{const e=document.getElementsByClassName("needs-validation");Array.prototype.filter.call(e,(e=>{e.addEventListener("submit",(t=>{!1===e.checkValidity()&&(t.preventDefault(),t.stopPropagation()),e.classList.add("was-validated")}),!1)}))}),!1),(()=>{const e=document.querySelectorAll("[data-format]");if(0!==e.length)for(let t=0;t<e.length;t++){let n,i=e[t],s=i.parentNode.querySelector(".credit-card-icon");null!=i.dataset.format&&(n=JSON.parse(i.dataset.format)),s?new Cleave(i,{...n,onCreditCardTypeChanged:e=>{s.className="credit-card-icon "+e}}):new Cleave(i,n)}})(),[].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')).map((e=>new bootstrap.Tooltip(e,{trigger:"hover"}))),[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map((e=>new bootstrap.Popover(e))),[].slice.call(document.querySelectorAll(".toast")).map((e=>new bootstrap.Toast(e))),(()=>{let e=document.querySelectorAll('[data-bs-toggle="video"]');if(e.length)for(let t=0;t<e.length;t++)lightGallery(e[t],{selector:"this",plugins:[lgVideo],licenseKey:"D4194FDD-48924833-A54AECA3-D6F8E646",download:!1,youtubePlayerParams:{modestbranding:1,showinfo:0,rel:0},vimeoPlayerParams:{byline:0,portrait:0,color:"6366f1"}})})(),(()=>{let e=document.querySelectorAll(".price-switch-wrapper");if(!(e.length<=0))for(let t=0;t<e.length;t++)e[t].querySelector('[data-bs-toggle="price"]').addEventListener("change",(e=>{let t=e.currentTarget.querySelector('input[type="checkbox"]'),n=e.currentTarget.closest(".price-switch-wrapper").querySelectorAll("[data-monthly-price]"),i=e.currentTarget.closest(".price-switch-wrapper").querySelectorAll("[data-annual-price]");for(let e=0;e<n.length;e++)1==t.checked?n[e].classList.add("d-none"):n[e].classList.remove("d-none");for(let e=0;e<n.length;e++)1==t.checked?i[e].classList.remove("d-none"):i[e].classList.add("d-none")}))})(),(()=>{let e,t=document.querySelectorAll(".masonry-grid");if(null!==t)for(let n=0;n<t.length;n++){e=new Shuffle(t[n],{itemSelector:".masonry-grid-item",sizer:".masonry-grid-item"}),imagesLoaded(t[n]).on("progress",(()=>{e.layout()}));let i=t[n].closest(".masonry-filterable");if(null===i)return;let s=i.querySelectorAll(".masonry-filters [data-group]");for(let t=0;t<s.length;t++)s[t].addEventListener("click",(function(t){let n=i.querySelector(".masonry-filters .active"),s=this.dataset.group;null!==n&&n.classList.remove("active"),this.classList.add("active"),e.filter(s),t.preventDefault()}))}})(),(()=>{const e=document.querySelectorAll(".subscription-form");if(null===e)return;for(let n=0;n<e.length;n++){let i=e[n].querySelector('button[type="submit"]'),s=i.innerHTML,o=e[n].querySelector(".form-control"),r=e[n].querySelector(".subscription-form-antispam"),a=e[n].querySelector(".subscription-status");e[n].addEventListener("submit",(function(e){e&&e.preventDefault(),""===r.value&&t(this,i,o,s,a)}))}let t=(e,t,n,i,s)=>{t.innerHTML="Sending...";let o=e.action.replace("/post?","/post-json?"),r="&"+n.name+"="+encodeURIComponent(n.value),a=document.createElement("script");a.src=o+"&c=callback"+r,document.body.appendChild(a);let l="callback";window[l]=e=>{delete window[l],document.body.removeChild(a),t.innerHTML=i,"success"==e.result?(n.classList.remove("is-invalid"),n.classList.add("is-valid"),s.classList.remove("status-error"),s.classList.add("status-success"),s.innerHTML=e.msg,setTimeout((()=>{n.classList.remove("is-valid"),s.innerHTML="",s.classList.remove("status-success")}),6e3)):(n.classList.remove("is-valid"),n.classList.add("is-invalid"),s.classList.remove("status-success"),s.classList.add("status-error"),s.innerHTML=e.msg.substring(4),setTimeout((()=>{n.classList.remove("is-invalid"),s.innerHTML="",s.classList.remove("status-error")}),6e3))}}})(),document.querySelectorAll(".animation-on-hover").forEach((e=>{e.addEventListener("mouseover",(()=>{e.querySelectorAll("lottie-player").forEach((e=>{e.setDirection(1),e.play()}))})),e.addEventListener("mouseleave",(()=>{e.querySelectorAll("lottie-player").forEach((e=>{e.setDirection(-1),e.play()}))}))})),(()=>{const e=document.querySelectorAll(".audio-player");if(0!==e.length)for(let t=0;t<e.length;t++){const n=e[t],i=n.querySelector("audio"),s=n.querySelector(".ap-play-button"),o=n.querySelector(".ap-seek-slider"),r=n.querySelector(".ap-volume-slider"),a=n.querySelector(".ap-duration"),l=n.querySelector(".ap-current-time");let c="play",u=null;s.addEventListener("click",(e=>{"play"===c?(e.currentTarget.classList.add("ap-pause"),i.play(),requestAnimationFrame(g),c="pause"):(e.currentTarget.classList.remove("ap-pause"),i.pause(),cancelAnimationFrame(u),c="play")}));const d=e=>{e===o?n.style.setProperty("--seek-before-width",e.value/e.max*100+"%"):n.style.setProperty("--volume-before-width",e.value/e.max*100+"%")};o.addEventListener("input",(e=>{d(e.target)})),r.addEventListener("input",(e=>{d(e.target)}));const h=e=>{const t=Math.floor(e/60),n=Math.floor(e%60);return`${t}:${n<10?`0${n}`:`${n}`}`},f=()=>{a.textContent=h(i.duration)},p=()=>{o.max=Math.floor(i.duration)},m=()=>{if(i.buffered.length>0){const e=Math.floor(i.buffered.end(i.buffered.length-1));n.style.setProperty("--buffered-width",e/o.max*100+"%")}},g=()=>{o.value=Math.floor(i.currentTime),l.textContent=h(o.value),n.style.setProperty("--seek-before-width",o.value/o.max*100+"%"),u=requestAnimationFrame(g)};i.readyState>0?(f(),p(),m()):i.addEventListener("loadedmetadata",(()=>{f(),p(),m()})),i.addEventListener("progress",m),o.addEventListener("input",(()=>{l.textContent=h(o.value),i.paused||cancelAnimationFrame(u)})),o.addEventListener("change",(()=>{i.currentTime=o.value,i.paused||requestAnimationFrame(g)})),r.addEventListener("input",(e=>{const t=e.target.value;i.volume=t/100}))}})()}();
|
| 23 |
+
//# sourceMappingURL=theme.min.js.map
|
static/assets/vue-dark.png
ADDED
|
static/assets/vue-light.png
ADDED
|
static/css/styles.css
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.bg-primary {
|
| 2 |
+
background-color: #2E8B57 !important;
|
| 3 |
+
;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
:root {
|
| 7 |
+
--bs-primary-rgb: #2E8B57 !important;
|
| 8 |
+
/* Màu nguyên thủy */
|
| 9 |
+
--custom-primary-rgb: #2E8B57 !important;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
/* Giữ nguyên màu khi đổi Light/Dark Mode */
|
| 13 |
+
[data-bs-theme="light"],
|
| 14 |
+
[data-bs-theme="dark"] {
|
| 15 |
+
--bs-primary-rgb: var(--custom-primary-rgb) !important;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
@keyframes rotate {
|
| 19 |
+
from {
|
| 20 |
+
transform: rotate(0deg);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
to {
|
| 24 |
+
transform: rotate(360deg);
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.fa-gear1 {
|
| 29 |
+
animation: rotate 2s linear infinite;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.ctext-content {
|
| 33 |
+
text-align: left;
|
| 34 |
+
/* Căn lề trái */
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.card-header {
|
| 38 |
+
background-color: #f8f9fa !important;
|
| 39 |
+
}
|
static/script.js
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Application state
|
| 2 |
+
let conversations = [];
|
| 3 |
+
let currentConversationId = null;
|
| 4 |
+
let messages = [];
|
| 5 |
+
let isLoading = false;
|
| 6 |
+
|
| 7 |
+
// Translation dictionary
|
| 8 |
+
const translations = {
|
| 9 |
+
en: {
|
| 10 |
+
newConversation: 'New Conversation',
|
| 11 |
+
noConversations: 'No conversations yet',
|
| 12 |
+
startNewChat: 'Start a new chat to begin',
|
| 13 |
+
message: 'message',
|
| 14 |
+
messages: 'messages',
|
| 15 |
+
analyzing: 'Analyzing...',
|
| 16 |
+
legalReferences: 'Legal References',
|
| 17 |
+
pageNumber: 'Page',
|
| 18 |
+
matchScore: 'Match Score',
|
| 19 |
+
relatedQuestions: 'Related Questions',
|
| 20 |
+
errorMessage: 'I apologize, but I encountered an error while processing your request. Please try again.',
|
| 21 |
+
},
|
| 22 |
+
vi: {
|
| 23 |
+
newConversation: 'Hội thoại mới',
|
| 24 |
+
noConversations: 'Chưa có hội thoại nào',
|
| 25 |
+
startNewChat: 'Bắt đầu một cuộc trò chuyện mới để bắt đầu',
|
| 26 |
+
message: 'tin nhắn',
|
| 27 |
+
messages: 'tin nhắn',
|
| 28 |
+
analyzing: 'Đang phân tích...',
|
| 29 |
+
legalReferences: 'Tài liệu pháp lý',
|
| 30 |
+
pageNumber: 'Trang',
|
| 31 |
+
matchScore: 'Độ khớp',
|
| 32 |
+
relatedQuestions: 'Câu hỏi liên quan',
|
| 33 |
+
errorMessage: 'Tôi xin lỗi, đã xảy ra lỗi khi xử lý yêu cầu của bạn. Vui lòng thử lại.',
|
| 34 |
+
},
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
// Mock suggested questions for initial screen
|
| 38 |
+
const suggestedQuestions = {
|
| 39 |
+
en: [
|
| 40 |
+
'What are the essential elements of a valid contract?',
|
| 41 |
+
'How does civil law differ from criminal law?',
|
| 42 |
+
'What protections does intellectual property law provide?',
|
| 43 |
+
'What are the ways a contract can be terminated?',
|
| 44 |
+
],
|
| 45 |
+
vi: [
|
| 46 |
+
'Các yếu tố thiết yếu của một hợp đồng hợp lệ là gì?',
|
| 47 |
+
'Luật dân sự khác với luật hình sự như thế nào?',
|
| 48 |
+
'Luật sở hữu trí tuệ cung cấp những bảo vệ gì?',
|
| 49 |
+
'Hợp đồng có thể được chấm dứt bằng những cách nào?',
|
| 50 |
+
],
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
// DOM elements
|
| 54 |
+
const sidebar = document.getElementById('sidebar');
|
| 55 |
+
const mobileOverlay = document.getElementById('mobileOverlay');
|
| 56 |
+
const menuBtn = document.getElementById('menuBtn');
|
| 57 |
+
const closeSidebarBtn = document.getElementById('closeSidebarBtn');
|
| 58 |
+
const newChatBtn = document.getElementById('newChatBtn');
|
| 59 |
+
const conversationsList = document.getElementById('conversationsList');
|
| 60 |
+
const messagesContainer = document.getElementById('messagesContainer');
|
| 61 |
+
const messages_div = document.getElementById('messages');
|
| 62 |
+
const welcomeSection = document.getElementById('welcomeSection');
|
| 63 |
+
const suggestedQuestionsElement = document.getElementById('suggestedQuestions');
|
| 64 |
+
const inputForm = document.getElementById('inputForm');
|
| 65 |
+
const messageInput = document.getElementById('messageInput');
|
| 66 |
+
const sendBtn = document.getElementById('sendBtn');
|
| 67 |
+
const themeToggle = document.getElementById('themeToggle');
|
| 68 |
+
const languageSelect = document.getElementById('languageSelect');
|
| 69 |
+
|
| 70 |
+
// Theme management
|
| 71 |
+
function initializeTheme() {
|
| 72 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
| 73 |
+
document.documentElement.setAttribute('data-theme', savedTheme);
|
| 74 |
+
updateThemeIcon(savedTheme);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
function toggleTheme() {
|
| 78 |
+
const currentTheme = document.documentElement.getAttribute('data-theme');
|
| 79 |
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
| 80 |
+
document.documentElement.setAttribute('data-theme', newTheme);
|
| 81 |
+
localStorage.setItem('theme', newTheme);
|
| 82 |
+
updateThemeIcon(newTheme);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
function updateThemeIcon(theme) {
|
| 86 |
+
const icon = themeToggle.querySelector('i');
|
| 87 |
+
icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// Sidebar management
|
| 91 |
+
function openSidebar() {
|
| 92 |
+
sidebar.classList.add('open');
|
| 93 |
+
mobileOverlay.classList.add('show');
|
| 94 |
+
document.body.style.overflow = 'hidden';
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
function closeSidebar() {
|
| 98 |
+
sidebar.classList.remove('open');
|
| 99 |
+
mobileOverlay.classList.remove('show');
|
| 100 |
+
document.body.style.overflow = '';
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Conversation management
|
| 104 |
+
function createNewConversation() {
|
| 105 |
+
const newConversation = {
|
| 106 |
+
id: Date.now().toString(),
|
| 107 |
+
title: translate('newConversation'),
|
| 108 |
+
timestamp: new Date().toISOString(),
|
| 109 |
+
messageCount: 0,
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
+
conversations.unshift(newConversation);
|
| 113 |
+
currentConversationId = newConversation.id;
|
| 114 |
+
messages = [];
|
| 115 |
+
|
| 116 |
+
renderConversations();
|
| 117 |
+
renderMessages();
|
| 118 |
+
closeSidebar();
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
function selectConversation(conversationId) {
|
| 122 |
+
currentConversationId = conversationId;
|
| 123 |
+
messages = [];
|
| 124 |
+
renderMessages();
|
| 125 |
+
closeSidebar();
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
function deleteConversation(conversationId) {
|
| 129 |
+
conversations = conversations.filter(conv => conv.id !== conversationId);
|
| 130 |
+
if (currentConversationId === conversationId) {
|
| 131 |
+
currentConversationId = null;
|
| 132 |
+
messages = [];
|
| 133 |
+
renderMessages();
|
| 134 |
+
}
|
| 135 |
+
renderConversations();
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
function updateConversationTitle(conversationId, newTitle, messageCount) {
|
| 139 |
+
const conversation = conversations.find(conv => conv.id === conversationId);
|
| 140 |
+
if (conversation) {
|
| 141 |
+
if (conversation.messageCount === 0) {
|
| 142 |
+
conversation.title = newTitle.slice(0, 50) + (newTitle.length > 50 ? '...' : '');
|
| 143 |
+
}
|
| 144 |
+
conversation.messageCount = messageCount;
|
| 145 |
+
conversation.timestamp = new Date().toISOString();
|
| 146 |
+
renderConversations();
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
function renderConversations() {
|
| 151 |
+
if (conversations.length === 0) {
|
| 152 |
+
conversationsList.innerHTML = `
|
| 153 |
+
<div class="empty-conversations">
|
| 154 |
+
<i class="fas fa-comments"></i>
|
| 155 |
+
<p data-translate="noConversations">${translate('noConversations')}</p>
|
| 156 |
+
<small data-translate="startNewChat">${translate('startNewChat')}</small>
|
| 157 |
+
</div>
|
| 158 |
+
`;
|
| 159 |
+
return;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
conversationsList.innerHTML = conversations.map(conversation => `
|
| 163 |
+
<div class="conversation-item ${conversation.id === currentConversationId ? 'active' : ''}"
|
| 164 |
+
data-conversation-id="${conversation.id}">
|
| 165 |
+
<div class="conversation-title">${conversation.title}</div>
|
| 166 |
+
<div class="conversation-meta">
|
| 167 |
+
<span>${conversation.messageCount} ${conversation.messageCount === 1 ? translate('message') : translate('messages')}</span>
|
| 168 |
+
<span>•</span>
|
| 169 |
+
<span>${formatTimestamp(conversation.timestamp)}</span>
|
| 170 |
+
</div>
|
| 171 |
+
<button class="delete-conversation" data-conversation-id="${conversation.id}">
|
| 172 |
+
<i class="fas fa-trash"></i>
|
| 173 |
+
</button>
|
| 174 |
+
</div>
|
| 175 |
+
`).join('');
|
| 176 |
+
|
| 177 |
+
conversationsList.querySelectorAll('.conversation-item').forEach(item => {
|
| 178 |
+
item.addEventListener('click', (e) => {
|
| 179 |
+
if (!e.target.closest('.delete-conversation')) {
|
| 180 |
+
selectConversation(item.dataset.conversationId);
|
| 181 |
+
}
|
| 182 |
+
});
|
| 183 |
+
});
|
| 184 |
+
|
| 185 |
+
conversationsList.querySelectorAll('.delete-conversation').forEach(btn => {
|
| 186 |
+
btn.addEventListener('click', (e) => {
|
| 187 |
+
e.stopPropagation();
|
| 188 |
+
deleteConversation(btn.dataset.conversationId);
|
| 189 |
+
});
|
| 190 |
+
});
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
// Message management
|
| 194 |
+
function addMessage(type, content, sources = null, relatedQuestions = null, isTyping = false) {
|
| 195 |
+
const message = {
|
| 196 |
+
id: Date.now().toString() + (isTyping ? '-typing' : ''),
|
| 197 |
+
type,
|
| 198 |
+
content,
|
| 199 |
+
timestamp: new Date().toISOString(),
|
| 200 |
+
sources,
|
| 201 |
+
relatedQuestions,
|
| 202 |
+
isTyping,
|
| 203 |
+
};
|
| 204 |
+
|
| 205 |
+
if (isTyping) {
|
| 206 |
+
messages = messages.filter(msg => !msg.isTyping);
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
messages.push(message);
|
| 210 |
+
renderMessages();
|
| 211 |
+
|
| 212 |
+
return message.id;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
function removeMessage(messageId) {
|
| 216 |
+
messages = messages.filter(msg => msg.id !== messageId);
|
| 217 |
+
renderMessages();
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
function renderMessages() {
|
| 221 |
+
const hasMessages = messages.length > 0;
|
| 222 |
+
const hasConversation = currentConversationId !== null;
|
| 223 |
+
|
| 224 |
+
welcomeSection.style.display = hasMessages ? 'none' : 'block';
|
| 225 |
+
|
| 226 |
+
if (!hasMessages && !hasConversation) {
|
| 227 |
+
suggestedQuestionsElement.style.display = 'block';
|
| 228 |
+
const currentLang = getCurrentLanguage();
|
| 229 |
+
suggestedQuestionsElement.innerHTML = `
|
| 230 |
+
<h3>${translate('relatedQuestions')}</h3>
|
| 231 |
+
<div class="questions-grid">
|
| 232 |
+
${suggestedQuestions[currentLang].map(question => `
|
| 233 |
+
<button class="question-btn" data-question="${question}">
|
| 234 |
+
<i class="fas fa-question-circle"></i>
|
| 235 |
+
<span>${question}</span>
|
| 236 |
+
</button>
|
| 237 |
+
`).join('')}
|
| 238 |
+
</div>
|
| 239 |
+
`;
|
| 240 |
+
} else {
|
| 241 |
+
suggestedQuestionsElement.style.display = 'none';
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
if (!hasMessages) {
|
| 245 |
+
messages_div.innerHTML = '';
|
| 246 |
+
return;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
messages_div.innerHTML = messages.map(message => {
|
| 250 |
+
if (message.isTyping) {
|
| 251 |
+
return `
|
| 252 |
+
<div class="message assistant fade-in">
|
| 253 |
+
<div class="message-content">
|
| 254 |
+
<div class="message-wrapper">
|
| 255 |
+
<div class="message-avatar">
|
| 256 |
+
<i class="fas fa-robot"></i>
|
| 257 |
+
</div>
|
| 258 |
+
<div class="message-bubble">
|
| 259 |
+
<div class="typing-indicator">
|
| 260 |
+
<div class="typing-dots">
|
| 261 |
+
<div class="typing-dot"></div>
|
| 262 |
+
<div class="typing-dot"></div>
|
| 263 |
+
<div class="typing-dot"></div>
|
| 264 |
+
</div>
|
| 265 |
+
<span>${translate('analyzing')}</span>
|
| 266 |
+
</div>
|
| 267 |
+
</div>
|
| 268 |
+
</div>
|
| 269 |
+
</div>
|
| 270 |
+
</div>
|
| 271 |
+
`;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
const sourcesHtml = message.sources ? `
|
| 275 |
+
<div class="message-sources">
|
| 276 |
+
<div class="sources-title">${translate('legalReferences')}</div>
|
| 277 |
+
${message.sources.map(source => `
|
| 278 |
+
<div class="source-item">
|
| 279 |
+
<div class="source-header">
|
| 280 |
+
<div class="source-info">
|
| 281 |
+
<i class="fas fa-file-text"></i>
|
| 282 |
+
<div class="source-details">
|
| 283 |
+
<div class="source-name">${source.documentName}</div>
|
| 284 |
+
<div class="source-excerpt">"${source.excerpt}"</div>
|
| 285 |
+
${source.pageNumber ? `<div class="source-page">${translate('pageNumber')} ${source.pageNumber}</div>` : ''}
|
| 286 |
+
</div>
|
| 287 |
+
</div>
|
| 288 |
+
<div class="source-meta">
|
| 289 |
+
<div class="source-score">${Math.round(source.relevanceScore * 100)}% ${translate('matchScore')}</div>
|
| 290 |
+
<i class="fas fa-external-link-alt"></i>
|
| 291 |
+
</div>
|
| 292 |
+
</div>
|
| 293 |
+
</div>
|
| 294 |
+
`).join('')}
|
| 295 |
+
</div>
|
| 296 |
+
` : '';
|
| 297 |
+
|
| 298 |
+
const relatedQuestionsHtml = message.relatedQuestions ? `
|
| 299 |
+
<div class="related-questions">
|
| 300 |
+
<div class="sources-title">${translate('relatedQuestions')}</div>
|
| 301 |
+
<div class="questions-grid">
|
| 302 |
+
${message.relatedQuestions.map(question => `
|
| 303 |
+
<button class="question-btn" data-question="${question}">
|
| 304 |
+
<i class="fas fa-question-circle"></i>
|
| 305 |
+
<span>${question}</span>
|
| 306 |
+
</button>
|
| 307 |
+
`).join('')}
|
| 308 |
+
</div>
|
| 309 |
+
</div>
|
| 310 |
+
` : '';
|
| 311 |
+
|
| 312 |
+
return `
|
| 313 |
+
<div class="message ${message.type} ${message.type === 'user' ? 'slide-in-right' : 'slide-in-left'}">
|
| 314 |
+
<div class="message-content">
|
| 315 |
+
<div class="message-wrapper">
|
| 316 |
+
<div class="message-avatar">
|
| 317 |
+
<i class="fas fa-${message.type === 'user' ? 'user' : 'robot'}"></i>
|
| 318 |
+
</div>
|
| 319 |
+
<div class="message-bubble">
|
| 320 |
+
<div class="message-text">${message.content}</div>
|
| 321 |
+
${sourcesHtml}
|
| 322 |
+
${relatedQuestionsHtml}
|
| 323 |
+
<div class="message-timestamp">
|
| 324 |
+
<i class="fas fa-clock"></i>
|
| 325 |
+
<span>${formatMessageTime(message.timestamp)}</span>
|
| 326 |
+
</div>
|
| 327 |
+
</div>
|
| 328 |
+
</div>
|
| 329 |
+
</div>
|
| 330 |
+
</div>
|
| 331 |
+
`;
|
| 332 |
+
}).join('');
|
| 333 |
+
|
| 334 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
// Message sending
|
| 338 |
+
async function sendMessage(content) {
|
| 339 |
+
if (!content.trim() || isLoading) return;
|
| 340 |
+
|
| 341 |
+
if (!currentConversationId) {
|
| 342 |
+
createNewConversation();
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
addMessage('user', content);
|
| 346 |
+
|
| 347 |
+
const messageCount = messages.filter(msg => !msg.isTyping).length;
|
| 348 |
+
updateConversationTitle(currentConversationId, content, messageCount);
|
| 349 |
+
|
| 350 |
+
isLoading = true;
|
| 351 |
+
updateSendButton();
|
| 352 |
+
|
| 353 |
+
const typingId = addMessage('assistant', '', null, null, true);
|
| 354 |
+
|
| 355 |
+
try {
|
| 356 |
+
const { response, sources, related_questions } = await generateResponse(content);
|
| 357 |
+
|
| 358 |
+
removeMessage(typingId);
|
| 359 |
+
|
| 360 |
+
addMessage('assistant', response, sources, related_questions);
|
| 361 |
+
|
| 362 |
+
} catch (error) {
|
| 363 |
+
removeMessage(typingId);
|
| 364 |
+
addMessage('assistant', translate('errorMessage'));
|
| 365 |
+
} finally {
|
| 366 |
+
isLoading = false;
|
| 367 |
+
updateSendButton();
|
| 368 |
+
}
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
async function generateResponse(query) {
|
| 372 |
+
try {
|
| 373 |
+
const response = await fetch('http://127.0.0.1:5000/api/query', {
|
| 374 |
+
method: 'POST',
|
| 375 |
+
headers: {
|
| 376 |
+
'Content-Type': 'application/json',
|
| 377 |
+
},
|
| 378 |
+
body: JSON.stringify({ question: query }),
|
| 379 |
+
});
|
| 380 |
+
|
| 381 |
+
if (!response.ok) {
|
| 382 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
const data = await response.json();
|
| 386 |
+
|
| 387 |
+
const formattedResponse = {
|
| 388 |
+
response: data.final_response,
|
| 389 |
+
sources: data.top_banan_documents.map(doc => ({
|
| 390 |
+
documentId: doc.file,
|
| 391 |
+
documentName: doc.file,
|
| 392 |
+
excerpt: doc.text.slice(0, 150) + '...',
|
| 393 |
+
relevanceScore: doc.distance,
|
| 394 |
+
pageNumber: doc.pageNumber || null,
|
| 395 |
+
})),
|
| 396 |
+
related_questions: data.related_questions.map(q => q.question),
|
| 397 |
+
};
|
| 398 |
+
|
| 399 |
+
return formattedResponse;
|
| 400 |
+
} catch (error) {
|
| 401 |
+
console.error('Error fetching response from API:', error);
|
| 402 |
+
throw error;
|
| 403 |
+
}
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
// Utility functions
|
| 407 |
+
function formatTimestamp(timestamp) {
|
| 408 |
+
const date = new Date(timestamp);
|
| 409 |
+
const now = new Date();
|
| 410 |
+
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
|
| 411 |
+
|
| 412 |
+
if (diffInHours < 24) {
|
| 413 |
+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
| 414 |
+
} else if (diffInHours < 168) {
|
| 415 |
+
return date.toLocaleDateString([], { weekday: 'short' });
|
| 416 |
+
} else {
|
| 417 |
+
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
function formatMessageTime(timestamp) {
|
| 422 |
+
return new Date(timestamp).toLocaleTimeString([], {
|
| 423 |
+
hour: '2-digit',
|
| 424 |
+
minute: '2-digit'
|
| 425 |
+
});
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
function updateSendButton() {
|
| 429 |
+
const hasText = messageInput.value.trim().length > 0;
|
| 430 |
+
sendBtn.disabled = !hasText || isLoading;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
function adjustTextareaHeight() {
|
| 434 |
+
messageInput.style.height = 'auto';
|
| 435 |
+
messageInput.style.height = Math.min(messageInput.scrollHeight, 120) + 'px';
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
function getCurrentLanguage() {
|
| 439 |
+
return languageSelect.value || 'vi';
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
function setLanguage(lang) {
|
| 443 |
+
languageSelect.value = lang;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
function translate(key) {
|
| 447 |
+
const currentLang = getCurrentLanguage();
|
| 448 |
+
return translations[currentLang][key] || translations.en[key] || key;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
// Event listeners
|
| 452 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 453 |
+
initializeTheme();
|
| 454 |
+
renderConversations();
|
| 455 |
+
renderMessages();
|
| 456 |
+
updateSendButton();
|
| 457 |
+
});
|
| 458 |
+
|
| 459 |
+
menuBtn.addEventListener('click', openSidebar);
|
| 460 |
+
closeSidebarBtn.addEventListener('click', closeSidebar);
|
| 461 |
+
mobileOverlay.addEventListener('click', closeSidebar);
|
| 462 |
+
newChatBtn.addEventListener('click', createNewConversation);
|
| 463 |
+
|
| 464 |
+
themeToggle.addEventListener('click', toggleTheme);
|
| 465 |
+
|
| 466 |
+
languageSelect.addEventListener('change', (e) => {
|
| 467 |
+
setLanguage(e.target.value);
|
| 468 |
+
renderConversations();
|
| 469 |
+
renderMessages();
|
| 470 |
+
});
|
| 471 |
+
|
| 472 |
+
messageInput.addEventListener('input', () => {
|
| 473 |
+
updateSendButton();
|
| 474 |
+
adjustTextareaHeight();
|
| 475 |
+
});
|
| 476 |
+
|
| 477 |
+
messageInput.addEventListener('keydown', (e) => {
|
| 478 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 479 |
+
e.preventDefault();
|
| 480 |
+
inputForm.dispatchEvent(new Event('submit'));
|
| 481 |
+
}
|
| 482 |
+
});
|
| 483 |
+
|
| 484 |
+
inputForm.addEventListener('submit', (e) => {
|
| 485 |
+
e.preventDefault();
|
| 486 |
+
const content = messageInput.value.trim();
|
| 487 |
+
if (content) {
|
| 488 |
+
sendMessage(content);
|
| 489 |
+
messageInput.value = '';
|
| 490 |
+
messageInput.style.height = 'auto';
|
| 491 |
+
updateSendButton();
|
| 492 |
+
}
|
| 493 |
+
});
|
| 494 |
+
|
| 495 |
+
document.addEventListener('click', (e) => {
|
| 496 |
+
if (e.target.closest('.question-btn')) {
|
| 497 |
+
const question = e.target.closest('.question-btn').dataset.question;
|
| 498 |
+
sendMessage(question);
|
| 499 |
+
}
|
| 500 |
+
});
|
| 501 |
+
|
| 502 |
+
window.addEventListener('resize', () => {
|
| 503 |
+
if (window.innerWidth >= 1024) {
|
| 504 |
+
closeSidebar();
|
| 505 |
+
}
|
| 506 |
+
});
|
static/style.css
ADDED
|
@@ -0,0 +1,1036 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* CSS Variables for Theming */
|
| 2 |
+
:root {
|
| 3 |
+
--bg-primary: #f9fafb;
|
| 4 |
+
--bg-secondary: #ffffff;
|
| 5 |
+
--bg-tertiary: #f3f4f6;
|
| 6 |
+
--text-primary: #111827;
|
| 7 |
+
--text-secondary: #6b7280;
|
| 8 |
+
--text-tertiary: #ffffff;
|
| 9 |
+
--border-color: #e5e7eb;
|
| 10 |
+
--accent-color: #3b82f6;
|
| 11 |
+
--accent-colors:#ffffff;
|
| 12 |
+
--accent-hover: #2563eb;
|
| 13 |
+
--success-color: #10b981;
|
| 14 |
+
--warning-color: #f59e0b;
|
| 15 |
+
--error-color: #ef4444;
|
| 16 |
+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
| 17 |
+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
| 18 |
+
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
| 19 |
+
--gradient-primary: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
| 20 |
+
--gradient-secondary: linear-gradient(135deg, #06b6d4, #3b82f6);
|
| 21 |
+
--border-radius-sm: 0.375rem;
|
| 22 |
+
--border-radius-md: 0.5rem;
|
| 23 |
+
--border-radius-lg: 0.75rem;
|
| 24 |
+
--transition-ease: all 0.2s ease-in-out;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
[data-theme="dark"] {
|
| 28 |
+
--bg-primary: #0f172a;
|
| 29 |
+
--bg-secondary: #1e293b;
|
| 30 |
+
--bg-tertiary: #334155;
|
| 31 |
+
--text-primary: #f8fafc;
|
| 32 |
+
--text-secondary: #cbd5e1;
|
| 33 |
+
--text-tertiary: #ffffff;
|
| 34 |
+
--border-color: #475569;
|
| 35 |
+
--accent-color: #3b82f6;
|
| 36 |
+
--accent-hover: #60a5fa;
|
| 37 |
+
--success-color: #22c55e;
|
| 38 |
+
--warning-color: #fbbf24;
|
| 39 |
+
--error-color: #f87171;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.message.user .message-text {
|
| 43 |
+
color: var(--text-tertiary);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.fa-file-text {
|
| 47 |
+
color: #3b82f6 !important;
|
| 48 |
+
}
|
| 49 |
+
/* Reset and Base Styles */
|
| 50 |
+
* {
|
| 51 |
+
margin: 0;
|
| 52 |
+
padding: 0;
|
| 53 |
+
box-sizing: border-box;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
body {
|
| 57 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 58 |
+
background-color: var(--bg-primary);
|
| 59 |
+
color: var(--text-primary);
|
| 60 |
+
line-height: 1.6;
|
| 61 |
+
font-size: 16px;
|
| 62 |
+
transition: var(--transition-ease);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.app {
|
| 66 |
+
display: flex;
|
| 67 |
+
min-height: 100vh;
|
| 68 |
+
overflow: hidden;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/* Sidebar Styles */
|
| 72 |
+
.sidebar {
|
| 73 |
+
width: 320px;
|
| 74 |
+
background-color: var(--bg-secondary);
|
| 75 |
+
border-right: 1px solid var(--border-color);
|
| 76 |
+
display: flex;
|
| 77 |
+
flex-direction: column;
|
| 78 |
+
transition: transform 0.3s ease;
|
| 79 |
+
box-shadow: var(--shadow-sm);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.sidebar.open {
|
| 83 |
+
transform: translateX(0);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.sidebar-header {
|
| 87 |
+
padding: 1.5rem;
|
| 88 |
+
border-bottom: 1px solid var(--border-color);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.sidebar-title-row {
|
| 92 |
+
display: flex;
|
| 93 |
+
justify-content: space-between;
|
| 94 |
+
align-items: center;
|
| 95 |
+
margin-bottom: 1rem;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.sidebar-title-row h2 {
|
| 99 |
+
font-size: 1.25rem;
|
| 100 |
+
font-weight: 600;
|
| 101 |
+
color: var(--text-primary);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.close-sidebar-btn {
|
| 105 |
+
display: none;
|
| 106 |
+
background: none;
|
| 107 |
+
border: none;
|
| 108 |
+
color: var(--text-secondary);
|
| 109 |
+
cursor: pointer;
|
| 110 |
+
padding: 0.5rem;
|
| 111 |
+
border-radius: var(--border-radius-sm);
|
| 112 |
+
transition: var(--transition-ease);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.close-sidebar-btn:hover {
|
| 116 |
+
background-color: var(--bg-tertiary);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.new-chat-btn {
|
| 120 |
+
width: 100%;
|
| 121 |
+
display: flex;
|
| 122 |
+
align-items: center;
|
| 123 |
+
justify-content: center;
|
| 124 |
+
gap: 0.5rem;
|
| 125 |
+
padding: 0.75rem 1rem;
|
| 126 |
+
background: var(--gradient-primary);
|
| 127 |
+
color: white;
|
| 128 |
+
border: none;
|
| 129 |
+
border-radius: var(--border-radius-md);
|
| 130 |
+
cursor: pointer;
|
| 131 |
+
font-weight: 500;
|
| 132 |
+
font-size: 0.875rem;
|
| 133 |
+
transition: var(--transition-ease);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
.btn-upgrade {
|
| 138 |
+
display: flex;
|
| 139 |
+
align-items: center;
|
| 140 |
+
justify-content: center;
|
| 141 |
+
gap: 0.5rem;
|
| 142 |
+
padding: 0.75rem 1rem;
|
| 143 |
+
background: var(--gradient-primary);
|
| 144 |
+
color: white;
|
| 145 |
+
border: none;
|
| 146 |
+
border-radius: var(--border-radius-md);
|
| 147 |
+
cursor: pointer;
|
| 148 |
+
font-weight: 500;
|
| 149 |
+
font-size: 0.875rem;
|
| 150 |
+
transition: var(--transition-ease);
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.new-chat-btn:hover {
|
| 154 |
+
opacity: 0.9;
|
| 155 |
+
transform: translateY(-2px);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.conversations-list {
|
| 159 |
+
flex: 1;
|
| 160 |
+
max-height: 67vh;
|
| 161 |
+
overflow-y: auto;
|
| 162 |
+
padding: 1rem;
|
| 163 |
+
scroll-behavior: smooth;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.empty-conversations {
|
| 167 |
+
text-align: center;
|
| 168 |
+
padding: 2rem 0;
|
| 169 |
+
color: var(--text-secondary);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.empty-conversations i {
|
| 173 |
+
font-size: 3rem;
|
| 174 |
+
color: var(--text-tertiary);
|
| 175 |
+
margin-bottom: 1rem;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.conversation-item {
|
| 179 |
+
padding: 0.75rem;
|
| 180 |
+
border-radius: var(--border-radius-md);
|
| 181 |
+
cursor: pointer;
|
| 182 |
+
margin-bottom: 0.5rem;
|
| 183 |
+
border: 1px solid transparent;
|
| 184 |
+
transition: var(--transition-ease);
|
| 185 |
+
position: relative;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.conversation-item:hover {
|
| 189 |
+
background-color: var(--bg-tertiary);
|
| 190 |
+
transform: translateX(4px);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.conversation-item.active {
|
| 194 |
+
background-color: var(--bg-tertiary);
|
| 195 |
+
border-color: var(--accent-color);
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.conversation-title {
|
| 199 |
+
font-weight: 500;
|
| 200 |
+
font-size: 0.875rem;
|
| 201 |
+
color: var(--text-primary);
|
| 202 |
+
margin-bottom: 0.25rem;
|
| 203 |
+
white-space: nowrap;
|
| 204 |
+
max-width: 250px;
|
| 205 |
+
overflow: hidden;
|
| 206 |
+
text-overflow: ellipsis;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.conversation-meta {
|
| 210 |
+
display: flex;
|
| 211 |
+
align-items: center;
|
| 212 |
+
gap: 0.5rem;
|
| 213 |
+
font-size: 0.75rem;
|
| 214 |
+
color: var(--text-secondary);
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.delete-conversation {
|
| 218 |
+
position: absolute;
|
| 219 |
+
top: 0.5rem;
|
| 220 |
+
right: 0.5rem;
|
| 221 |
+
background: none;
|
| 222 |
+
border: none;
|
| 223 |
+
color: var(--error-color);
|
| 224 |
+
cursor: pointer;
|
| 225 |
+
padding: 0.25rem;
|
| 226 |
+
border-radius: var(--border-radius-sm);
|
| 227 |
+
opacity: 0;
|
| 228 |
+
transition: var(--transition-ease);
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.conversation-item:hover .delete-conversation {
|
| 232 |
+
opacity: 1;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.delete-conversation:hover {
|
| 236 |
+
background-color: var(--bg-tertiary);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.sidebar-footer {
|
| 240 |
+
padding: 1rem;
|
| 241 |
+
border-top: 1px solid var(--border-color);
|
| 242 |
+
background-color: var(--bg-secondary);
|
| 243 |
+
height: auto;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.controls {
|
| 247 |
+
display: flex;
|
| 248 |
+
align-items: center;
|
| 249 |
+
gap: 0.5rem;
|
| 250 |
+
margin-bottom: 1rem;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.control-btn {
|
| 254 |
+
font-size: small;
|
| 255 |
+
background: none;
|
| 256 |
+
border: 1px solid var(--border-color);
|
| 257 |
+
color: var(--text-secondary);
|
| 258 |
+
cursor: pointer;
|
| 259 |
+
padding: 0.5rem;
|
| 260 |
+
width: 35px;
|
| 261 |
+
border-radius: var(--border-radius-sm);
|
| 262 |
+
transition: var(--transition-ease);
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.control-btn-off {
|
| 266 |
+
background: none;
|
| 267 |
+
border: none;
|
| 268 |
+
color: var(--text-secondary);
|
| 269 |
+
cursor: pointer;
|
| 270 |
+
padding: 0.5rem;
|
| 271 |
+
border-radius: var(--border-radius-sm);
|
| 272 |
+
transition: var(--transition-ease);
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.control-btn:hover,
|
| 276 |
+
.control-btn-off:hover {
|
| 277 |
+
background-color: var(--bg-tertiary);
|
| 278 |
+
color: var(--text-primary);
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.language-select {
|
| 282 |
+
flex: 1;
|
| 283 |
+
background-color: var(--bg-secondary);
|
| 284 |
+
border: 1px solid var(--border-color);
|
| 285 |
+
color: var(--text-primary);
|
| 286 |
+
padding: 0.5rem;
|
| 287 |
+
border-radius: var(--border-radius-sm);
|
| 288 |
+
font-size: 0.875rem;
|
| 289 |
+
transition: var(--transition-ease);
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.footer-text {
|
| 293 |
+
text-align: center;
|
| 294 |
+
font-size: 0.75rem;
|
| 295 |
+
color: var(--text-secondary);
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.footer-text-image {
|
| 299 |
+
text-align: left;
|
| 300 |
+
font-size: 0.75rem;
|
| 301 |
+
color: var(--text-secondary);
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
/* Chat Area Styles */
|
| 305 |
+
.chat-area {
|
| 306 |
+
flex: 1;
|
| 307 |
+
display: flex;
|
| 308 |
+
flex-direction: column;
|
| 309 |
+
min-width: 0;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.chat-header {
|
| 313 |
+
background-color: var(--bg-secondary);
|
| 314 |
+
border-bottom: 1px solid var(--border-color);
|
| 315 |
+
padding: 1rem;
|
| 316 |
+
display: flex;
|
| 317 |
+
align-items: center;
|
| 318 |
+
gap: 0.75rem;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.menu-btn {
|
| 322 |
+
display: none;
|
| 323 |
+
background: none;
|
| 324 |
+
border: none;
|
| 325 |
+
color: var(--text-secondary);
|
| 326 |
+
cursor: pointer;
|
| 327 |
+
padding: 0.5rem;
|
| 328 |
+
border-radius: var(--border-radius-sm);
|
| 329 |
+
transition: var(--transition-ease);
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.menu-btn:hover {
|
| 333 |
+
background-color: var(--bg-tertiary);
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
.header-info {
|
| 337 |
+
display: flex;
|
| 338 |
+
justify-content: space-between;
|
| 339 |
+
align-items: center;
|
| 340 |
+
flex: 1;
|
| 341 |
+
min-width: 0;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.bot-avatar {
|
| 345 |
+
width: 2.5rem;
|
| 346 |
+
height: 2.5rem;
|
| 347 |
+
background: var(--gradient-primary);
|
| 348 |
+
border-radius: var(--border-radius-md);
|
| 349 |
+
display: flex;
|
| 350 |
+
align-items: center;
|
| 351 |
+
justify-content: center;
|
| 352 |
+
color: white;
|
| 353 |
+
font-size: 1.25rem;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.header-text h1 {
|
| 357 |
+
font-size: 1.25rem;
|
| 358 |
+
font-weight: 600;
|
| 359 |
+
color: var(--text-primary);
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.header-text p {
|
| 363 |
+
font-size: 0.875rem;
|
| 364 |
+
color: var(--text-secondary);
|
| 365 |
+
overflow: hidden;
|
| 366 |
+
text-overflow: ellipsis;
|
| 367 |
+
white-space: nowrap;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.user-info {
|
| 371 |
+
margin-left: auto;
|
| 372 |
+
display: flex;
|
| 373 |
+
align-items: center;
|
| 374 |
+
gap: 0.5rem;
|
| 375 |
+
color: var(--text-primary);
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
.user-info span {
|
| 379 |
+
font-size: 0.875rem;
|
| 380 |
+
font-weight: 500;
|
| 381 |
+
color: var(--text-primary);
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.user-info .control-btn-off {
|
| 385 |
+
color: var(--text-secondary);
|
| 386 |
+
font-size: small;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.user-info .control-btn-off:hover {
|
| 390 |
+
color: var(--text-primary);
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
.messages-container {
|
| 394 |
+
flex: 1;
|
| 395 |
+
max-height: 77vh;
|
| 396 |
+
overflow-y: auto;
|
| 397 |
+
padding: 1rem;
|
| 398 |
+
background-color: var(--bg-primary);
|
| 399 |
+
scroll-behavior: smooth;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.messages-container::-webkit-scrollbar {
|
| 403 |
+
width: 6px;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.messages-container::-webkit-scrollbar-track {
|
| 407 |
+
background: var(--bg-tertiary);
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
.messages-container::-webkit-scrollbar-thumb {
|
| 411 |
+
background: var(--text-tertiary);
|
| 412 |
+
border-radius: 3px;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.messages-container::-webkit-scrollbar-thumb:hover {
|
| 416 |
+
background: var(--text-secondary);
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
.welcome-section {
|
| 420 |
+
max-width: 48rem;
|
| 421 |
+
margin: 0 auto;
|
| 422 |
+
text-align: center;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.welcome-icon {
|
| 426 |
+
width: 4rem;
|
| 427 |
+
height: 4rem;
|
| 428 |
+
background: var(--gradient-primary);
|
| 429 |
+
border-radius: var(--border-radius-lg);
|
| 430 |
+
display: flex;
|
| 431 |
+
align-items: center;
|
| 432 |
+
justify-content: center;
|
| 433 |
+
margin: 0 auto 1.5rem;
|
| 434 |
+
color: white;
|
| 435 |
+
font-size: 2rem;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
.welcome-section h2 {
|
| 439 |
+
font-size: 1.5rem;
|
| 440 |
+
font-weight: 700;
|
| 441 |
+
color: var(--text-primary);
|
| 442 |
+
margin-bottom: 1rem;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
.welcome-section>p {
|
| 446 |
+
color: var(--text-secondary);
|
| 447 |
+
margin-bottom: 2rem;
|
| 448 |
+
max-width: 32rem;
|
| 449 |
+
margin-left: auto;
|
| 450 |
+
margin-right: auto;
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
.suggested-questions {
|
| 454 |
+
max-width: 48rem;
|
| 455 |
+
margin: 0 auto;
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
.suggested-questions h3 {
|
| 459 |
+
font-size: 1.125rem;
|
| 460 |
+
font-weight: 600;
|
| 461 |
+
color: var(--text-primary);
|
| 462 |
+
margin-bottom: 1rem;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
.questions-grid {
|
| 466 |
+
display: grid;
|
| 467 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 468 |
+
gap: 1rem;
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
.question-btn {
|
| 472 |
+
background-color: var(--bg-secondary);
|
| 473 |
+
border: 1px solid var(--border-color);
|
| 474 |
+
border-radius: var(--border-radius-md);
|
| 475 |
+
padding: 1rem;
|
| 476 |
+
text-align: left;
|
| 477 |
+
cursor: pointer;
|
| 478 |
+
transition: var(--transition-ease);
|
| 479 |
+
display: flex;
|
| 480 |
+
align-items: flex-start;
|
| 481 |
+
gap: 0.75rem;
|
| 482 |
+
box-shadow: var(--shadow-sm);
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
.question-btn:hover {
|
| 486 |
+
border-color: var(--accent-color);
|
| 487 |
+
background-color: var(--bg-tertiary);
|
| 488 |
+
transform: translateY(-2px);
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
.question-btn i {
|
| 492 |
+
color: var(--accent-color);
|
| 493 |
+
margin-top: 0.125rem;
|
| 494 |
+
flex-shrink: 0;
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
.question-btn span {
|
| 498 |
+
font-size: 0.875rem;
|
| 499 |
+
color: var(--text-primary);
|
| 500 |
+
line-height: 1.4;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
.messages {
|
| 504 |
+
max-width: 48rem;
|
| 505 |
+
margin: 0 auto;
|
| 506 |
+
padding: 1rem 0;
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
.message {
|
| 510 |
+
display: flex;
|
| 511 |
+
margin-bottom: 1.5rem;
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
.message.user {
|
| 515 |
+
justify-content: flex-end;
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
.message.assistant {
|
| 519 |
+
justify-content: flex-start;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
.message-content {
|
| 523 |
+
max-width: 48rem;
|
| 524 |
+
width: 100%;
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
.message.user .message-content {
|
| 528 |
+
margin-left: 3rem;
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
.message.assistant .message-content {
|
| 532 |
+
margin-right: 3rem;
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
.message-wrapper {
|
| 536 |
+
display: flex;
|
| 537 |
+
align-items: flex-start;
|
| 538 |
+
gap: 0.75rem;
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
.message.user .message-wrapper {
|
| 542 |
+
flex-direction: row-reverse;
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
.message-avatar {
|
| 546 |
+
width: 2rem;
|
| 547 |
+
height: 2rem;
|
| 548 |
+
border-radius: var(--border-radius-md);
|
| 549 |
+
display: flex;
|
| 550 |
+
align-items: center;
|
| 551 |
+
justify-content: center;
|
| 552 |
+
flex-shrink: 0;
|
| 553 |
+
color: white;
|
| 554 |
+
font-size: 0.875rem;
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
.message.user .message-avatar {
|
| 558 |
+
background-color: var(--accent-color);
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
.message.assistant .message-avatar {
|
| 562 |
+
background: var(--gradient-primary);
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
.message-bubble {
|
| 566 |
+
border-radius: var(--border-radius-lg);
|
| 567 |
+
padding: 1rem;
|
| 568 |
+
position: relative;
|
| 569 |
+
box-shadow: var(--shadow-sm);
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
.message.user .message-bubble {
|
| 573 |
+
background-color: var(--accent-color);
|
| 574 |
+
margin-left: auto;
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
.message.assistant .message-bubble {
|
| 578 |
+
background-color: var(--bg-secondary);
|
| 579 |
+
border: 1px solid var(--border-color);
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
.message-text {
|
| 583 |
+
font-size: 0.875rem;
|
| 584 |
+
line-height: 1.5;
|
| 585 |
+
white-space: pre-wrap;
|
| 586 |
+
color: var(--text-primary);
|
| 587 |
+
}
|
| 588 |
+
.typing-indicator {
|
| 589 |
+
display: flex;
|
| 590 |
+
align-items: center;
|
| 591 |
+
gap: 0.5rem;
|
| 592 |
+
padding: 0.75rem 1rem; /* Đồng bộ với padding của .message-bubble */
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
.typing-dots {
|
| 596 |
+
display: flex;
|
| 597 |
+
gap: 0.25rem;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
.typing-dot {
|
| 601 |
+
width: 0.5rem;
|
| 602 |
+
height: 0.5rem;
|
| 603 |
+
background-color: var(--text-secondary); /* Sử dụng màu accent để nổi bật hơn */
|
| 604 |
+
border-radius: 50%;
|
| 605 |
+
animation: typing 1.4s infinite ease-in-out;
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
.typing-dot:nth-child(2) {
|
| 609 |
+
animation-delay: 0.2s;
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
.typing-dot:nth-child(3) {
|
| 613 |
+
animation-delay: 0.4s;
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
@keyframes typing {
|
| 617 |
+
0%, 60%, 100% {
|
| 618 |
+
transform: translateY(0);
|
| 619 |
+
}
|
| 620 |
+
30% {
|
| 621 |
+
transform: translateY(-10px);
|
| 622 |
+
}
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
.typing-indicator span {
|
| 626 |
+
color: var(--text-primary); /* Sử dụng text-primary để tương phản tốt hơn */
|
| 627 |
+
font-size: 0.875rem;
|
| 628 |
+
font-weight: 500;
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
.message-sources {
|
| 632 |
+
margin-top: 0.75rem;
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
.sources-title {
|
| 636 |
+
font-size: 0.75rem;
|
| 637 |
+
font-weight: 500;
|
| 638 |
+
color: var(--text-secondary);
|
| 639 |
+
text-transform: uppercase;
|
| 640 |
+
letter-spacing: 0.05em;
|
| 641 |
+
margin-bottom: 0.5rem;
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
.source-item {
|
| 645 |
+
background-color: var(--bg-tertiary);
|
| 646 |
+
border: 1px solid var(--border-color);
|
| 647 |
+
border-radius: var(--border-radius-md);
|
| 648 |
+
padding: 0.75rem;
|
| 649 |
+
margin-bottom: 0.5rem;
|
| 650 |
+
cursor: pointer;
|
| 651 |
+
transition: var(--transition-ease);
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
.source-item:hover {
|
| 655 |
+
background-color: var(--bg-secondary);
|
| 656 |
+
transform: translateY(-2px);
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
.source-header {
|
| 660 |
+
display: flex;
|
| 661 |
+
align-items: flex-start;
|
| 662 |
+
justify-content: space-between;
|
| 663 |
+
gap: 0.5rem;
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
.source-info {
|
| 667 |
+
display: flex;
|
| 668 |
+
align-items: flex-start;
|
| 669 |
+
gap: 0.5rem;
|
| 670 |
+
flex: 1;
|
| 671 |
+
min-width: 0;
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
+
.source-info i {
|
| 675 |
+
color: var(--text-tertiary);
|
| 676 |
+
margin-top: 0.125rem;
|
| 677 |
+
flex-shrink: 0;
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
.source-details {
|
| 681 |
+
min-width: 0;
|
| 682 |
+
flex: 1;
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
.source-name {
|
| 686 |
+
font-size: 0.875rem;
|
| 687 |
+
font-weight: 500;
|
| 688 |
+
color: var(--text-primary);
|
| 689 |
+
margin-bottom: 0.25rem;
|
| 690 |
+
overflow: hidden;
|
| 691 |
+
text-overflow: ellipsis;
|
| 692 |
+
white-space: nowrap;
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
.source-excerpt {
|
| 696 |
+
font-size: 0.75rem;
|
| 697 |
+
color: var(--text-secondary);
|
| 698 |
+
line-height: 1.4;
|
| 699 |
+
display: -webkit-box;
|
| 700 |
+
-webkit-line-clamp: 2;
|
| 701 |
+
-webkit-box-orient: vertical;
|
| 702 |
+
overflow: hidden;
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
.source-score {
|
| 706 |
+
font-size: 0.75rem;
|
| 707 |
+
color: var(--text-secondary);
|
| 708 |
+
}
|
| 709 |
+
|
| 710 |
+
.message-timestamp {
|
| 711 |
+
display: flex;
|
| 712 |
+
align-items: center;
|
| 713 |
+
gap: 0.25rem;
|
| 714 |
+
margin-top: 0.5rem;
|
| 715 |
+
font-size: 0.75rem;
|
| 716 |
+
color: var(--text-secondary);
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
.message-timestamp i {
|
| 720 |
+
font-size: 0.625rem;
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
.input-area {
|
| 724 |
+
background-color: var(--bg-secondary);
|
| 725 |
+
border-top: 1px solid var(--border-color);
|
| 726 |
+
padding: 1.5rem;
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
.input-form {
|
| 730 |
+
max-width: 48rem;
|
| 731 |
+
margin: 0 auto;
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
.input-wrapper {
|
| 735 |
+
display: flex;
|
| 736 |
+
gap: 1rem;
|
| 737 |
+
align-items: flex-end;
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
#messageInput {
|
| 741 |
+
flex: 1;
|
| 742 |
+
background-color: var(--bg-secondary);
|
| 743 |
+
border: 1px solid var(--border-color);
|
| 744 |
+
border-radius: var(--border-radius-lg);
|
| 745 |
+
padding: 0.75rem 1rem;
|
| 746 |
+
font-size: 0.875rem;
|
| 747 |
+
line-height: 1.5;
|
| 748 |
+
resize: none;
|
| 749 |
+
min-height: 2.75rem;
|
| 750 |
+
max-height: 7.5rem;
|
| 751 |
+
color: var(--text-primary);
|
| 752 |
+
transition: var(--transition-ease);
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
#messageInput:focus {
|
| 756 |
+
outline: none;
|
| 757 |
+
border-color: var(--accent-color);
|
| 758 |
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
#messageInput::placeholder {
|
| 762 |
+
color: var(--text-tertiary);
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
#sendBtn {
|
| 766 |
+
background-color: var(--accent-color);
|
| 767 |
+
color: white;
|
| 768 |
+
border: none;
|
| 769 |
+
border-radius: var(--border-radius-lg);
|
| 770 |
+
padding: 0.75rem 1.5rem;
|
| 771 |
+
cursor: pointer;
|
| 772 |
+
transition: var(--transition-ease);
|
| 773 |
+
flex-shrink: 0;
|
| 774 |
+
height: 2.75rem;
|
| 775 |
+
display: flex;
|
| 776 |
+
align-items: center;
|
| 777 |
+
justify-content: center;
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
#sendBtn:hover:not(:disabled) {
|
| 781 |
+
background-color: var(--accent-hover);
|
| 782 |
+
transform: translateY(-2px);
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
#sendBtn:disabled {
|
| 786 |
+
opacity: 0.5;
|
| 787 |
+
cursor: not-allowed;
|
| 788 |
+
}
|
| 789 |
+
|
| 790 |
+
.input-hint {
|
| 791 |
+
text-align: center;
|
| 792 |
+
font-size: 0.75rem;
|
| 793 |
+
color: var(--text-secondary);
|
| 794 |
+
margin-top: 0.5rem;
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
.mobile-overlay {
|
| 798 |
+
display: none;
|
| 799 |
+
position: fixed;
|
| 800 |
+
inset: 0;
|
| 801 |
+
background-color: rgba(0, 0, 0, 0.5);
|
| 802 |
+
z-index: 40;
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
.related-questions {
|
| 806 |
+
margin-top: 1rem;
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
.related-questions .questions-grid {
|
| 810 |
+
display: grid;
|
| 811 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 812 |
+
gap: 1rem;
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
.related-questions .question-btn {
|
| 816 |
+
background-color: var(--bg-secondary);
|
| 817 |
+
border: 1px solid var(--border-color);
|
| 818 |
+
border-radius: var(--border-radius-md);
|
| 819 |
+
padding: 1rem;
|
| 820 |
+
text-align: left;
|
| 821 |
+
cursor: pointer;
|
| 822 |
+
transition: var(--transition-ease);
|
| 823 |
+
display: flex;
|
| 824 |
+
align-items: flex-start;
|
| 825 |
+
gap: 0.75rem;
|
| 826 |
+
box-shadow: var(--shadow-sm);
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
.related-questions .question-btn:hover {
|
| 830 |
+
border-color: var(--accent-color);
|
| 831 |
+
background-color: var(--bg-tertiary);
|
| 832 |
+
transform: translateY(-2px);
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
|
| 836 |
+
.message .message.assistant .message-text{
|
| 837 |
+
color: var(--text-tertiary);
|
| 838 |
+
}
|
| 839 |
+
.message-timestamp{
|
| 840 |
+
color: var(--text-tertiary);
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
.related-questions .question-btn i {
|
| 844 |
+
color: var(--accent-color);
|
| 845 |
+
margin-top: 0.125rem;
|
| 846 |
+
flex-shrink: 0;
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
+
.related-questions .question-btn span {
|
| 850 |
+
font-size: 0.875rem;
|
| 851 |
+
color: var(--text-primary);
|
| 852 |
+
line-height: 1.4;
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
/* Responsive Design */
|
| 856 |
+
@media (max-width: 1024px) {
|
| 857 |
+
.sidebar {
|
| 858 |
+
position: fixed;
|
| 859 |
+
top: 0;
|
| 860 |
+
left: 0;
|
| 861 |
+
height: 100vh;
|
| 862 |
+
z-index: 50;
|
| 863 |
+
transform: translateX(-100%);
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
.sidebar.open {
|
| 867 |
+
transform: translateX(0);
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
.mobile-overlay.show {
|
| 871 |
+
display: block;
|
| 872 |
+
}
|
| 873 |
+
|
| 874 |
+
.close-sidebar-btn {
|
| 875 |
+
display: block;
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
.menu-btn {
|
| 879 |
+
display: block;
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
.chat-area {
|
| 883 |
+
width: 100%;
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
.questions-grid {
|
| 887 |
+
grid-template-columns: 1fr;
|
| 888 |
+
}
|
| 889 |
+
|
| 890 |
+
.message.user .message-content {
|
| 891 |
+
margin-left: 1rem;
|
| 892 |
+
}
|
| 893 |
+
|
| 894 |
+
|
| 895 |
+
.message.assistant .message-content {
|
| 896 |
+
margin-right: 1rem;
|
| 897 |
+
}
|
| 898 |
+
}
|
| 899 |
+
|
| 900 |
+
@media (max-width: 640px) {
|
| 901 |
+
.sidebar {
|
| 902 |
+
width: 100vw;
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
+
.header-info {
|
| 906 |
+
flex-wrap: wrap;
|
| 907 |
+
gap: 0.5rem;
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
.user-info {
|
| 911 |
+
margin-left: 0;
|
| 912 |
+
gap: 0.5rem;
|
| 913 |
+
flex-wrap: nowrap;
|
| 914 |
+
}
|
| 915 |
+
|
| 916 |
+
.bot-avatar {
|
| 917 |
+
width: 2rem;
|
| 918 |
+
height: 2rem;
|
| 919 |
+
font-size: 1rem;
|
| 920 |
+
}
|
| 921 |
+
|
| 922 |
+
.header-text h1 {
|
| 923 |
+
font-size: 1rem;
|
| 924 |
+
}
|
| 925 |
+
|
| 926 |
+
.header-text p {
|
| 927 |
+
font-size: 0.75rem;
|
| 928 |
+
}
|
| 929 |
+
|
| 930 |
+
.welcome-section {
|
| 931 |
+
padding: 1rem;
|
| 932 |
+
}
|
| 933 |
+
|
| 934 |
+
.welcome-section h2 {
|
| 935 |
+
font-size: 1.25rem;
|
| 936 |
+
}
|
| 937 |
+
|
| 938 |
+
.messages {
|
| 939 |
+
padding: 0.5rem 0;
|
| 940 |
+
}
|
| 941 |
+
|
| 942 |
+
.message-content {
|
| 943 |
+
max-width: 100%;
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
.message.user .message-content,
|
| 947 |
+
.message.assistant .message-content {
|
| 948 |
+
margin-left: 0.5rem;
|
| 949 |
+
margin-right: 0.5rem;
|
| 950 |
+
}
|
| 951 |
+
|
| 952 |
+
.message-bubble {
|
| 953 |
+
width: 85%;
|
| 954 |
+
}
|
| 955 |
+
|
| 956 |
+
.input-wrapper {
|
| 957 |
+
gap: 0.5rem;
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
#sendBtn {
|
| 961 |
+
padding: 0.75rem;
|
| 962 |
+
width: 2.75rem;
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
.header-text {
|
| 966 |
+
display: none;
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
.conversation-title {
|
| 970 |
+
max-width: 325px;
|
| 971 |
+
}
|
| 972 |
+
|
| 973 |
+
.suggested-questions h3 {
|
| 974 |
+
font-size: 1rem;
|
| 975 |
+
}
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
/* Scrollbar Styling */
|
| 979 |
+
::-webkit-scrollbar {
|
| 980 |
+
width: 6px;
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
::-webkit-scrollbar-track {
|
| 984 |
+
background: var(--bg-tertiary);
|
| 985 |
+
}
|
| 986 |
+
|
| 987 |
+
::-webkit-scrollbar-thumb {
|
| 988 |
+
background: var(--text-tertiary);
|
| 989 |
+
border-radius: 3px;
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
::-webkit-scrollbar-thumb:hover {
|
| 993 |
+
background: var(--text-secondary);
|
| 994 |
+
}
|
| 995 |
+
|
| 996 |
+
/* Animation Classes */
|
| 997 |
+
.fade-in {
|
| 998 |
+
animation: fadeIn 0.3s ease-in-out;
|
| 999 |
+
}
|
| 1000 |
+
|
| 1001 |
+
@keyframes fadeIn {
|
| 1002 |
+
from {
|
| 1003 |
+
opacity: 0;
|
| 1004 |
+
transform: translateY(10px);
|
| 1005 |
+
}
|
| 1006 |
+
to {
|
| 1007 |
+
opacity: 1;
|
| 1008 |
+
transform: translateY(0);
|
| 1009 |
+
}
|
| 1010 |
+
}
|
| 1011 |
+
|
| 1012 |
+
.user-info {
|
| 1013 |
+
display: flex;
|
| 1014 |
+
align-items: center;
|
| 1015 |
+
gap: 10px;
|
| 1016 |
+
}
|
| 1017 |
+
|
| 1018 |
+
#query-count-display {
|
| 1019 |
+
font-size: 14px;
|
| 1020 |
+
font-weight: bold;
|
| 1021 |
+
padding: 5px 10px;
|
| 1022 |
+
border-radius: 5px;
|
| 1023 |
+
transition: color 0.3s ease;
|
| 1024 |
+
}
|
| 1025 |
+
|
| 1026 |
+
#query-count-display.red {
|
| 1027 |
+
color: red;
|
| 1028 |
+
}
|
| 1029 |
+
|
| 1030 |
+
#query-count-display.orange {
|
| 1031 |
+
color: orange;
|
| 1032 |
+
}
|
| 1033 |
+
|
| 1034 |
+
#query-count-display.blue {
|
| 1035 |
+
color: #3b82f6;
|
| 1036 |
+
}
|
static/translations.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Translation system
|
| 2 |
+
const translations = {
|
| 3 |
+
en: {
|
| 4 |
+
// Sidebar
|
| 5 |
+
chatHistory: "Chat History",
|
| 6 |
+
newConversation: "New Conversation",
|
| 7 |
+
noConversations: "No conversations yet",
|
| 8 |
+
startNewChat: "Start a new chat to begin",
|
| 9 |
+
appName: "Legal AI Assistant",
|
| 10 |
+
poweredBy: "Powered by RAG Technology",
|
| 11 |
+
|
| 12 |
+
// Header
|
| 13 |
+
appTitle: "Legal AI Assistant",
|
| 14 |
+
appSubtitle: "Ask questions about legal topics and get expert insights",
|
| 15 |
+
|
| 16 |
+
// Welcome
|
| 17 |
+
welcomeTitle: "Welcome to Legal AI Assistant",
|
| 18 |
+
welcomeText: "I'm here to help you understand legal concepts, analyze contracts, and provide insights on various areas of law. Ask me anything about legal topics and I'll provide detailed, professional guidance.",
|
| 19 |
+
popularQuestions: "Popular Questions",
|
| 20 |
+
|
| 21 |
+
// Questions
|
| 22 |
+
question1: "What are the key elements of a valid contract?",
|
| 23 |
+
question2: "Explain the difference between civil and criminal law",
|
| 24 |
+
question3: "What is intellectual property law?",
|
| 25 |
+
question4: "How does contract termination work?",
|
| 26 |
+
question5: "What are fiduciary duties in corporate law?",
|
| 27 |
+
question6: "Explain force majeure clauses",
|
| 28 |
+
|
| 29 |
+
// Input
|
| 30 |
+
inputPlaceholder: "Ask a question about legal topics...",
|
| 31 |
+
inputHint: "Press Enter to send, Shift+Enter for new line",
|
| 32 |
+
|
| 33 |
+
// Messages
|
| 34 |
+
legalReferences: "Legal References",
|
| 35 |
+
analyzing: "Analyzing your question...",
|
| 36 |
+
pageNumber: "Page",
|
| 37 |
+
matchScore: "match",
|
| 38 |
+
|
| 39 |
+
// Time
|
| 40 |
+
today: "Today",
|
| 41 |
+
yesterday: "Yesterday",
|
| 42 |
+
thisWeek: "This week",
|
| 43 |
+
|
| 44 |
+
// Conversation meta
|
| 45 |
+
messages: "messages",
|
| 46 |
+
message: "message"
|
| 47 |
+
},
|
| 48 |
+
|
| 49 |
+
vi: {
|
| 50 |
+
// Sidebar
|
| 51 |
+
chatHistory: "Lịch sử trò chuyện",
|
| 52 |
+
newConversation: "Cuộc trò chuyện mới",
|
| 53 |
+
noConversations: "Chưa có cuộc trò chuyện nào",
|
| 54 |
+
startNewChat: "Bắt đầu trò chuyện mới",
|
| 55 |
+
appName: "Trợ lý AI Pháp lý",
|
| 56 |
+
poweredBy: "Được hỗ trợ bởi công nghệ RAG",
|
| 57 |
+
|
| 58 |
+
// Header
|
| 59 |
+
appTitle: "Trợ lý AI Pháp lý",
|
| 60 |
+
appSubtitle: "Đặt câu hỏi về các chủ đề pháp lý và nhận được thông tin chuyên sâu",
|
| 61 |
+
|
| 62 |
+
// Welcome
|
| 63 |
+
welcomeTitle: "Chào mừng đến với Trợ lý AI Pháp lý",
|
| 64 |
+
welcomeText: "Tôi ở đây để giúp bạn hiểu các khái niệm pháp lý, phân tích hợp đồng và cung cấp thông tin chi tiết về các lĩnh vực khác nhau của pháp luật. Hãy hỏi tôi bất cứ điều gì về các chủ đề pháp lý và tôi sẽ cung cấp hướng dẫn chi tiết, chuyên nghiệp.",
|
| 65 |
+
popularQuestions: "Câu hỏi phổ biến",
|
| 66 |
+
|
| 67 |
+
// Questions
|
| 68 |
+
question1: "Các yếu tố chính của một hợp đồng hợp lệ là gì?",
|
| 69 |
+
question2: "Giải thích sự khác biệt giữa luật dân sự và luật hình sự",
|
| 70 |
+
question3: "Luật sở hữu trí tuệ là gì?",
|
| 71 |
+
question4: "Việc chấm dứt hợp đồng hoạt động như thế nào?",
|
| 72 |
+
question5: "Nghĩa vụ tín thác trong luật doanh nghiệp là gì?",
|
| 73 |
+
question6: "Giải thích các điều khoản bất khả kháng",
|
| 74 |
+
|
| 75 |
+
// Input
|
| 76 |
+
inputPlaceholder: "Đặt câu hỏi về các chủ đề pháp lý...",
|
| 77 |
+
inputHint: "Nhấn Enter để gửi, Shift+Enter để xuống dòng",
|
| 78 |
+
|
| 79 |
+
// Messages
|
| 80 |
+
legalReferences: "Tài liệu tham khảo pháp lý",
|
| 81 |
+
analyzing: "Đang phân tích câu hỏi của bạn...",
|
| 82 |
+
pageNumber: "Trang",
|
| 83 |
+
matchScore: "khớp",
|
| 84 |
+
|
| 85 |
+
// Time
|
| 86 |
+
today: "Hôm nay",
|
| 87 |
+
yesterday: "Hôm qua",
|
| 88 |
+
thisWeek: "Tuần này",
|
| 89 |
+
|
| 90 |
+
// Conversation meta
|
| 91 |
+
messages: "tin nhắn",
|
| 92 |
+
message: "tin nhắn"
|
| 93 |
+
}
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
// Translation utility functions
|
| 97 |
+
function getCurrentLanguage() {
|
| 98 |
+
return localStorage.getItem('language') || 'vi';
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
function setLanguage(lang) {
|
| 102 |
+
localStorage.setItem('language', lang);
|
| 103 |
+
updateTranslations();
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
function translate(key) {
|
| 107 |
+
const lang = getCurrentLanguage();
|
| 108 |
+
return translations[lang][key] || translations.en[key] || key;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
function updateTranslations() {
|
| 112 |
+
const elements = document.querySelectorAll('[data-translate]');
|
| 113 |
+
elements.forEach(element => {
|
| 114 |
+
const key = element.getAttribute('data-translate');
|
| 115 |
+
element.textContent = translate(key);
|
| 116 |
+
});
|
| 117 |
+
|
| 118 |
+
// Update placeholders
|
| 119 |
+
const placeholderElements = document.querySelectorAll('[data-translate-placeholder]');
|
| 120 |
+
placeholderElements.forEach(element => {
|
| 121 |
+
const key = element.getAttribute('data-translate-placeholder');
|
| 122 |
+
element.placeholder = translate(key);
|
| 123 |
+
});
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
// Initialize translations when DOM is loaded
|
| 127 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 128 |
+
const savedLanguage = getCurrentLanguage();
|
| 129 |
+
const languageSelect = document.getElementById('languageSelect');
|
| 130 |
+
if (languageSelect) {
|
| 131 |
+
languageSelect.value = savedLanguage;
|
| 132 |
+
}
|
| 133 |
+
updateTranslations();
|
| 134 |
+
});
|
templates/admin_dashboard.html
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="vi">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Quản lý người dùng</title>
|
| 8 |
+
<!-- Bootstrap CSS -->
|
| 9 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 10 |
+
<!-- Font Awesome for icons -->
|
| 11 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
| 12 |
+
<!-- Google Fonts -->
|
| 13 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
| 14 |
+
<!-- DataTables CSS -->
|
| 15 |
+
<link href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css" rel="stylesheet">
|
| 16 |
+
<style>
|
| 17 |
+
:root {
|
| 18 |
+
--primary-color: #0c1a28;
|
| 19 |
+
--sidebar-bg: #2c3e50;
|
| 20 |
+
--text-color: #212529;
|
| 21 |
+
--bg-color: #f8f9fa;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
[data-theme="dark"] {
|
| 25 |
+
--primary-color: #0c1a28;
|
| 26 |
+
--sidebar-bg: #1a252f;
|
| 27 |
+
--text-color: #f8f9fa;
|
| 28 |
+
--bg-color: #212529;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
body {
|
| 32 |
+
background-color: var(--bg-color);
|
| 33 |
+
color: var(--text-color);
|
| 34 |
+
font-family: 'Inter', sans-serif;
|
| 35 |
+
transition: all 0.3s;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.sidebar {
|
| 39 |
+
width: 250px;
|
| 40 |
+
position: fixed;
|
| 41 |
+
top: 0;
|
| 42 |
+
bottom: 0;
|
| 43 |
+
left: 0;
|
| 44 |
+
background-color: var(--sidebar-bg);
|
| 45 |
+
color: #fff;
|
| 46 |
+
padding-top: 20px;
|
| 47 |
+
transition: all 0.3s;
|
| 48 |
+
z-index: 1000;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.sidebar.collapsed {
|
| 52 |
+
margin-left: -250px;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.sidebar .nav-link {
|
| 56 |
+
color: #fff;
|
| 57 |
+
padding: 12px 20px;
|
| 58 |
+
border-radius: 5px;
|
| 59 |
+
margin: 0 10px;
|
| 60 |
+
transition: all 0.2s;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.sidebar .nav-link:hover {
|
| 64 |
+
background-color: rgba(255, 255, 255, 0.1);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.sidebar .nav-link i {
|
| 68 |
+
margin-right: 10px;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.main-content {
|
| 72 |
+
margin-left: 250px;
|
| 73 |
+
padding: 30px;
|
| 74 |
+
transition: all 0.3s;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.main-content.shifted {
|
| 78 |
+
margin-left: 0;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.navbar {
|
| 82 |
+
background-color: var(--primary-color);
|
| 83 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.navbar-brand,
|
| 87 |
+
.welcome-text {
|
| 88 |
+
color: #fff !important;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.table {
|
| 92 |
+
background-color: #fff;
|
| 93 |
+
border-radius: 8px;
|
| 94 |
+
overflow: hidden;
|
| 95 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
[data-theme="dark"] .table {
|
| 99 |
+
background-color: #343a40;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.table-image img {
|
| 103 |
+
width: 40px;
|
| 104 |
+
height: 40px;
|
| 105 |
+
border-radius: 50%;
|
| 106 |
+
object-fit: cover;
|
| 107 |
+
border: 2px solid #fff;
|
| 108 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.action-btn {
|
| 112 |
+
padding: 6px 12px;
|
| 113 |
+
font-size: 0.9rem;
|
| 114 |
+
border-radius: 5px;
|
| 115 |
+
margin-right: 5px;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.modal-content {
|
| 119 |
+
border-radius: 10px;
|
| 120 |
+
animation: slideIn 0.3s ease-in-out;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
@keyframes slideIn {
|
| 124 |
+
from {
|
| 125 |
+
transform: translateY(-50px);
|
| 126 |
+
opacity: 0;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
to {
|
| 130 |
+
transform: translateY(0);
|
| 131 |
+
opacity: 1;
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.form-control,
|
| 136 |
+
.form-select {
|
| 137 |
+
border-radius: 6px;
|
| 138 |
+
transition: border-color 0.2s;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.form-control:focus,
|
| 142 |
+
.form-select:focus {
|
| 143 |
+
border-color: var(--primary-color);
|
| 144 |
+
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.btn-primary {
|
| 148 |
+
background-color: var(--primary-color);
|
| 149 |
+
border-color: var(--primary-color);
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.btn-primary:hover {
|
| 153 |
+
background-color: #218838;
|
| 154 |
+
border-color: #218838;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
@media (max-width: 768px) {
|
| 158 |
+
.sidebar {
|
| 159 |
+
margin-left: -250px;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.main-content {
|
| 163 |
+
margin-left: 0;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.sidebar.collapsed {
|
| 167 |
+
margin-left: 0;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.table-responsive {
|
| 171 |
+
overflow-x: auto;
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
</style>
|
| 175 |
+
</head>
|
| 176 |
+
|
| 177 |
+
<body>
|
| 178 |
+
<!-- Navbar -->
|
| 179 |
+
<nav class="navbar navbar-expand-lg navbar-dark">
|
| 180 |
+
<div class="container-fluid">
|
| 181 |
+
<button class="navbar-toggler me-3" id="sidebarToggle" aria-label="Toggle sidebar">
|
| 182 |
+
<span class="navbar-toggler-icon"></span>
|
| 183 |
+
</button>
|
| 184 |
+
<a class="navbar-brand" href="#">Quản lý người dùng</a>
|
| 185 |
+
<div class="ms-auto d-flex align-items-center">
|
| 186 |
+
<span class="welcome-text me-3">Xin chào, {{ user_name | default('Admin') | e }}!</span>
|
| 187 |
+
<!-- <i class="fas fa-moon theme-toggle me-3" id="themeToggle" data-bs-toggle="tooltip" title="Chuyển đổi giao diện"></i> -->
|
| 188 |
+
<button class="btn btn-outline-light btn-sm" onclick="logout()">Đăng xuất</button>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
</nav>
|
| 192 |
+
|
| 193 |
+
<!-- Sidebar -->
|
| 194 |
+
<div class="sidebar" id="sidebar">
|
| 195 |
+
<h5 class="text-center mb-4">Quản lý</h5>
|
| 196 |
+
<nav class="nav flex-column">
|
| 197 |
+
<a class="nav-link" href="#"><i class="fas fa-tachometer-alt"></i> Dashboard</a>
|
| 198 |
+
<a class="nav-link active" href="#"><i class="fas fa-users"></i> Người dùng</a>
|
| 199 |
+
<a class="nav-link" href="#"><i class="fas fa-cog"></i> Cài đặt</a>
|
| 200 |
+
<a class="nav-link" href="#"><i class="fas fa-chart-bar"></i> Báo cáo</a>
|
| 201 |
+
<a class="nav-link" href="#"><i class="fas fa-user"></i> Hồ sơ</a>
|
| 202 |
+
<a class="nav-link" href="#" onclick="logout()"><i class="fas fa-sign-out-alt"></i> Đăng xuất</a>
|
| 203 |
+
</nav>
|
| 204 |
+
</div>
|
| 205 |
+
|
| 206 |
+
<!-- Main Content -->
|
| 207 |
+
<div class="main-content" id="mainContent">
|
| 208 |
+
<div class="container table-container">
|
| 209 |
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
| 210 |
+
<h1>Danh sách người dùng</h1>
|
| 211 |
+
<button class="btn btn-primary"><i class="fas fa-plus me-2"></i>Thêm mới</button>
|
| 212 |
+
</div>
|
| 213 |
+
<div class="card">
|
| 214 |
+
<div class="card-body">
|
| 215 |
+
<div class="table-responsive">
|
| 216 |
+
<table class="table table-striped table-bordered">
|
| 217 |
+
<thead class="table-success">
|
| 218 |
+
<tr>
|
| 219 |
+
<th>ID</th>
|
| 220 |
+
<th>Tên người dùng</th>
|
| 221 |
+
<th>Email</th>
|
| 222 |
+
<th>Số điện thoại</th>
|
| 223 |
+
<th>Loại tài khoản</th>
|
| 224 |
+
<th>Lượt hỏi đáp</th>
|
| 225 |
+
<th>Hành động</th>
|
| 226 |
+
</tr>
|
| 227 |
+
</thead>
|
| 228 |
+
<tbody>
|
| 229 |
+
{% for user in users %}
|
| 230 |
+
<tr>
|
| 231 |
+
<td>{{ user.id }}</td>
|
| 232 |
+
<td>{{ user.username | e }}</td>
|
| 233 |
+
<td>{{ user.email | e }}</td>
|
| 234 |
+
<td>{{ user.phone | e }}</td>
|
| 235 |
+
<td>{{ user.account_type | e }}</td>
|
| 236 |
+
<td>{{ user.query_count }}/{{ user.query_limit or 'Không giới hạn' }}</td>
|
| 237 |
+
<td>
|
| 238 |
+
<button class="btn btn-primary btn-sm update-btn"
|
| 239 |
+
onclick="openUpdateModal('{{ user.id | e }}', '{{ user.account_type | e }}', {{ 'true' if user.is_admin else 'false' }}, '{{ user.query_limit | default('') | e }}')">Cập
|
| 240 |
+
nhật</button>
|
| 241 |
+
<button class="btn btn-danger btn-sm delete-btn"
|
| 242 |
+
onclick="deleteUser('{{ user.id | e }}')">Xóa</button>
|
| 243 |
+
<button class="btn btn-warning btn-sm reset-btn"
|
| 244 |
+
onclick="resetQuery('{{ user.id | e }}')">Reset Lượt</button>
|
| 245 |
+
</td>
|
| 246 |
+
</tr>
|
| 247 |
+
{% endfor %}
|
| 248 |
+
</tbody>
|
| 249 |
+
</table>
|
| 250 |
+
</div>
|
| 251 |
+
</div>
|
| 252 |
+
</div>
|
| 253 |
+
</div>
|
| 254 |
+
</div>
|
| 255 |
+
|
| 256 |
+
<!-- Update Modal -->
|
| 257 |
+
<div class="modal fade" id="updateModal" tabindex="-1" aria-labelledby="updateModalLabel" aria-hidden="true">
|
| 258 |
+
<div class="modal-dialog">
|
| 259 |
+
<div class="modal-content">
|
| 260 |
+
<div class="modal-header">
|
| 261 |
+
<h5 class="modal-title" id="updateModalLabel">Cập nhật người dùng</h5>
|
| 262 |
+
<button type="button" class="btn-close" onclick="closeUpdateModal()" aria-label="Close"></button>
|
| 263 |
+
</div>
|
| 264 |
+
<div class="modal-body">
|
| 265 |
+
<form id="updateForm">
|
| 266 |
+
<input type="hidden" id="updateUserId">
|
| 267 |
+
<div class="mb-3">
|
| 268 |
+
<label for="updateAccountType" class="form-label">Loại tài khoản:</label>
|
| 269 |
+
<select id="updateAccountType" class="form-select" onchange="toggleQueryLimit()">
|
| 270 |
+
<option value="limited">Limited</option>
|
| 271 |
+
<option value="unlimited">Unlimited</option>
|
| 272 |
+
</select>
|
| 273 |
+
</div>
|
| 274 |
+
<div class="mb-3 form-check">
|
| 275 |
+
<input type="checkbox" class="form-check-input" id="updateIsAdmin">
|
| 276 |
+
<label class="form-check-label" for="updateIsAdmin">Admin</label>
|
| 277 |
+
</div>
|
| 278 |
+
<div class="mb-3">
|
| 279 |
+
<label for="updateQueryLimit" class="form-label">Giới hạn hỏi đáp (cho tài khoản
|
| 280 |
+
limited):</label>
|
| 281 |
+
<input type="number" class="form-control" id="updateQueryLimit" min="1"
|
| 282 |
+
placeholder="Nhập giới hạn (ví dụ: 10)">
|
| 283 |
+
</div>
|
| 284 |
+
<div class="d-flex justify-content-end">
|
| 285 |
+
<button type="submit" class="btn btn-success me-2">Lưu</button>
|
| 286 |
+
<button type="button" class="btn btn-secondary cancel-btn"
|
| 287 |
+
onclick="closeUpdateModal()">Hủy</button>
|
| 288 |
+
</div>
|
| 289 |
+
</form>
|
| 290 |
+
</div>
|
| 291 |
+
</div>
|
| 292 |
+
</div>
|
| 293 |
+
</div>
|
| 294 |
+
|
| 295 |
+
<!-- jQuery (required for DataTables) -->
|
| 296 |
+
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
| 297 |
+
<!-- Bootstrap JS -->
|
| 298 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
| 299 |
+
<!-- DataTables JS -->
|
| 300 |
+
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
| 301 |
+
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
| 302 |
+
<script>
|
| 303 |
+
// Sidebar toggle
|
| 304 |
+
document.getElementById('sidebarToggle').addEventListener('click', () => {
|
| 305 |
+
const sidebar = document.getElementById('sidebar');
|
| 306 |
+
const mainContent = document.getElementById('mainContent');
|
| 307 |
+
sidebar.classList.toggle('collapsed');
|
| 308 |
+
mainContent.classList.toggle('shifted');
|
| 309 |
+
});
|
| 310 |
+
|
| 311 |
+
// Theme toggle
|
| 312 |
+
const themeToggle = document.getElementById('themeToggle');
|
| 313 |
+
themeToggle.addEventListener('click', () => {
|
| 314 |
+
const currentTheme = document.documentElement.getAttribute('data-theme');
|
| 315 |
+
if (currentTheme === 'dark') {
|
| 316 |
+
document.documentElement.removeAttribute('data-theme');
|
| 317 |
+
themeToggle.classList.remove('fa-sun');
|
| 318 |
+
themeToggle.classList.add('fa-moon');
|
| 319 |
+
} else {
|
| 320 |
+
document.documentElement.setAttribute('data-theme', 'dark');
|
| 321 |
+
themeToggle.classList.remove('fa-moon');
|
| 322 |
+
themeToggle.classList.add('fa-sun');
|
| 323 |
+
}
|
| 324 |
+
localStorage.setItem('theme', document.documentElement.getAttribute('data-theme') || 'light');
|
| 325 |
+
});
|
| 326 |
+
|
| 327 |
+
// Load saved theme
|
| 328 |
+
const savedTheme = localStorage.getItem('theme');
|
| 329 |
+
if (savedTheme === 'dark') {
|
| 330 |
+
document.documentElement.setAttribute('data-theme', 'dark');
|
| 331 |
+
themeToggle.classList.remove('fa-moon');
|
| 332 |
+
themeToggle.classList.add('fa-sun');
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
// Initialize DataTables
|
| 336 |
+
$(document).ready(function () {
|
| 337 |
+
$('#userTable').DataTable({
|
| 338 |
+
language: {
|
| 339 |
+
url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/vi.json' // Vietnamese translation
|
| 340 |
+
},
|
| 341 |
+
pageLength: 10,
|
| 342 |
+
responsive: true,
|
| 343 |
+
columnDefs: [
|
| 344 |
+
{ orderable: false, targets: 6 } // Disable sorting for action column
|
| 345 |
+
]
|
| 346 |
+
});
|
| 347 |
+
});
|
| 348 |
+
|
| 349 |
+
// Initialize tooltips
|
| 350 |
+
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
| 351 |
+
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
| 352 |
+
|
| 353 |
+
</script>
|
| 354 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
| 355 |
+
<script>
|
| 356 |
+
async function deleteUser(userId) {
|
| 357 |
+
if (confirm('Bạn có chắc muốn xóa người dùng này?')) {
|
| 358 |
+
const response = await fetch(`/admin/user/${userId}`, {
|
| 359 |
+
method: 'DELETE',
|
| 360 |
+
headers: { 'Content-Type': 'application/json' }
|
| 361 |
+
});
|
| 362 |
+
const result = await response.json();
|
| 363 |
+
alert(result.message || result.error);
|
| 364 |
+
location.reload();
|
| 365 |
+
}
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
async function resetQuery(userId) {
|
| 369 |
+
const response = await fetch(`/admin/user/${userId}/reset_query`, {
|
| 370 |
+
method: 'POST',
|
| 371 |
+
headers: { 'Content-Type': 'application/json' }
|
| 372 |
+
});
|
| 373 |
+
const result = await response.json();
|
| 374 |
+
alert(result.message || result.error);
|
| 375 |
+
location.reload();
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
function openUpdateModal(userId, accountType, isAdmin, queryLimit) {
|
| 379 |
+
document.getElementById('updateUserId').value = userId;
|
| 380 |
+
document.getElementById('updateAccountType').value = accountType;
|
| 381 |
+
document.getElementById('updateIsAdmin').checked = isAdmin;
|
| 382 |
+
document.getElementById('updateQueryLimit').value = queryLimit || '';
|
| 383 |
+
toggleQueryLimit();
|
| 384 |
+
const modal = new bootstrap.Modal(document.getElementById('updateModal'));
|
| 385 |
+
modal.show();
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
function closeUpdateModal() {
|
| 389 |
+
const modal = bootstrap.Modal.getInstance(document.getElementById('updateModal'));
|
| 390 |
+
modal.hide();
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
function toggleQueryLimit() {
|
| 394 |
+
const accountType = document.getElementById('updateAccountType').value;
|
| 395 |
+
const queryLimitInput = document.getElementById('updateQueryLimit');
|
| 396 |
+
queryLimitInput.disabled = accountType === 'unlimited';
|
| 397 |
+
if (accountType === 'unlimited') {
|
| 398 |
+
queryLimitInput.value = '';
|
| 399 |
+
}
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
async function logout() {
|
| 403 |
+
try {
|
| 404 |
+
const response = await fetch('/logout', {
|
| 405 |
+
method: 'POST',
|
| 406 |
+
headers: { 'Content-Type': 'application/json' }
|
| 407 |
+
});
|
| 408 |
+
if (response.ok) {
|
| 409 |
+
// Clear client-side state (adapted for dashboard context)
|
| 410 |
+
// Variables like currentUser, conversations, etc., may not exist here
|
| 411 |
+
// If needed, define them or remove irrelevant ones
|
| 412 |
+
alert('Đăng xuất thành công');
|
| 413 |
+
window.location.href = '/';
|
| 414 |
+
} else {
|
| 415 |
+
alert('Lỗi khi đăng xuất');
|
| 416 |
+
}
|
| 417 |
+
} catch (error) {
|
| 418 |
+
alert('Lỗi khi đăng xuất: ' + error.message);
|
| 419 |
+
}
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
document.getElementById('updateForm').addEventListener('submit', async (e) => {
|
| 423 |
+
e.preventDefault();
|
| 424 |
+
const userId = document.getElementById('updateUserId').value;
|
| 425 |
+
const accountType = document.getElementById('updateAccountType').value;
|
| 426 |
+
const isAdmin = document.getElementById('updateIsAdmin').checked;
|
| 427 |
+
const queryLimit = document.getElementById('updateQueryLimit').value;
|
| 428 |
+
if (accountType === 'limited' && queryLimit && !/^\d+$/.test(queryLimit)) {
|
| 429 |
+
alert('Giới hạn hỏi đáp phải là một số nguyên dương.');
|
| 430 |
+
return;
|
| 431 |
+
}
|
| 432 |
+
const updates = { account_type: accountType, is_admin: isAdmin };
|
| 433 |
+
if (accountType === 'limited' && queryLimit) {
|
| 434 |
+
updates.query_limit = parseInt(queryLimit);
|
| 435 |
+
}
|
| 436 |
+
try {
|
| 437 |
+
const response = await fetch(`/admin/user/${userId}`, {
|
| 438 |
+
method: 'PUT',
|
| 439 |
+
headers: { 'Content-Type': 'application/json' },
|
| 440 |
+
body: JSON.stringify(updates)
|
| 441 |
+
});
|
| 442 |
+
const result = await response.json();
|
| 443 |
+
alert(result.message || result.error);
|
| 444 |
+
closeUpdateModal();
|
| 445 |
+
location.reload();
|
| 446 |
+
} catch (error) {
|
| 447 |
+
alert('Lỗi khi cập nhật người dùng: ' + error.message);
|
| 448 |
+
}
|
| 449 |
+
});
|
| 450 |
+
</script>
|
| 451 |
+
</body>
|
| 452 |
+
|
| 453 |
+
</html>
|
templates/change_password.html
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="vi">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Quên mật khẩu</title>
|
| 8 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 9 |
+
<style>
|
| 10 |
+
:root {
|
| 11 |
+
--primary-color: #6366F1;
|
| 12 |
+
--bg-dark: #0d1b2a;
|
| 13 |
+
--bg-light: #1e2a44;
|
| 14 |
+
--card-bg: rgba(13, 27, 42, 0.95);
|
| 15 |
+
--text-muted: #adb5bd;
|
| 16 |
+
--error-color: #dc3545;
|
| 17 |
+
--success-color: #28a745;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
body {
|
| 21 |
+
background: linear-gradient(135deg, var(--bg-light), var(--bg-dark));
|
| 22 |
+
min-height: 100vh;
|
| 23 |
+
display: flex;
|
| 24 |
+
justify-content: center;
|
| 25 |
+
align-items: center;
|
| 26 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 27 |
+
color: #fff;
|
| 28 |
+
margin: 0;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.forgot-password-container {
|
| 32 |
+
background: var(--card-bg);
|
| 33 |
+
padding: 2.5rem;
|
| 34 |
+
border-radius: 12px;
|
| 35 |
+
width: 100%;
|
| 36 |
+
max-width: 450px;
|
| 37 |
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
| 38 |
+
backdrop-filter: blur(5px);
|
| 39 |
+
transition: transform 0.3s ease;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.forgot-password-container:hover {
|
| 43 |
+
transform: translateY(-2px);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.forgot-password-container h2 {
|
| 47 |
+
font-size: 1.75rem;
|
| 48 |
+
font-weight: 600;
|
| 49 |
+
margin-bottom: 1rem;
|
| 50 |
+
text-align: center;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.description {
|
| 54 |
+
color: var(--text-muted);
|
| 55 |
+
font-size: 0.9rem;
|
| 56 |
+
text-align: center;
|
| 57 |
+
margin-bottom: 2rem;
|
| 58 |
+
line-height: 1.5;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.form-label {
|
| 62 |
+
color: var(--text-muted);
|
| 63 |
+
font-size: 0.85rem;
|
| 64 |
+
font-weight: 500;
|
| 65 |
+
margin-bottom: 0.5rem;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.form-control {
|
| 69 |
+
background-color: #2c3e50;
|
| 70 |
+
border: 1px solid #3b4a5e;
|
| 71 |
+
color: #ffffff !important;
|
| 72 |
+
border-radius: 6px;
|
| 73 |
+
padding: 0.75rem;
|
| 74 |
+
transition: all 0.2s ease;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.form-control:focus {
|
| 78 |
+
background-color: #34495e;
|
| 79 |
+
border-color: var(--primary-color);
|
| 80 |
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
|
| 81 |
+
outline: none;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.form-control::placeholder {
|
| 85 |
+
color: #6c757d;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.form-control{
|
| 89 |
+
color: white !important; /* Ensure input text is white in dark mode */
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.form-control[readonly] {
|
| 93 |
+
background-color: #1b263b;
|
| 94 |
+
opacity: 0.9;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.form-control.is-invalid {
|
| 98 |
+
border-color: var(--error-color);
|
| 99 |
+
background-image: none;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.invalid-feedback {
|
| 103 |
+
font-size: 0.8rem;
|
| 104 |
+
color: var(--error-color);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.btn-submit {
|
| 108 |
+
background: var(--primary-color);
|
| 109 |
+
color: #fff;
|
| 110 |
+
border: none;
|
| 111 |
+
padding: 0.85rem;
|
| 112 |
+
border-radius: 6px;
|
| 113 |
+
width: 100%;
|
| 114 |
+
font-weight: 500;
|
| 115 |
+
transition: all 0.2s ease;
|
| 116 |
+
position: relative;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.btn-submit:hover:not(:disabled) {
|
| 120 |
+
background: #4f46e5;
|
| 121 |
+
transform: translateY(-1px);
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.btn-submit:disabled {
|
| 125 |
+
opacity: 0.6;
|
| 126 |
+
cursor: not-allowed;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.text-muted {
|
| 130 |
+
color: #6c757d !important;
|
| 131 |
+
font-size: 0.85rem;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.toast-container {
|
| 135 |
+
z-index: 1100;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.loading-spinner {
|
| 139 |
+
display: none;
|
| 140 |
+
border: 3px solid #f3f3f3;
|
| 141 |
+
border-top: 3px solid var(--primary-color);
|
| 142 |
+
border-radius: 50%;
|
| 143 |
+
width: 20px;
|
| 144 |
+
height: 20px;
|
| 145 |
+
animation: spin 1s linear infinite;
|
| 146 |
+
position: absolute;
|
| 147 |
+
right: 10px;
|
| 148 |
+
top: 50%;
|
| 149 |
+
transform: translateY(-50%);
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
@keyframes spin {
|
| 153 |
+
0% {
|
| 154 |
+
transform: translateY(-50%) rotate(0deg);
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
100% {
|
| 158 |
+
transform: translateY(-50%) rotate(360deg);
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
@media (max-width: 576px) {
|
| 163 |
+
.forgot-password-container {
|
| 164 |
+
margin: 1.5rem;
|
| 165 |
+
padding: 1.5rem;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.forgot-password-container h2 {
|
| 169 |
+
font-size: 1.5rem;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.description {
|
| 173 |
+
font-size: 0.85rem;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.btn-submit {
|
| 177 |
+
padding: 0.75rem;
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
/* Accessibility improvements */
|
| 182 |
+
.sr-only {
|
| 183 |
+
position: absolute;
|
| 184 |
+
width: 1px;
|
| 185 |
+
height: 1px;
|
| 186 |
+
padding: 0;
|
| 187 |
+
margin: -1px;
|
| 188 |
+
overflow: hidden;
|
| 189 |
+
clip: rect(0, 0, 0, 0);
|
| 190 |
+
border: 0;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.btn-google {
|
| 194 |
+
background-color: #6366F1;
|
| 195 |
+
color: #fff;
|
| 196 |
+
border: none;
|
| 197 |
+
padding: 0.5rem;
|
| 198 |
+
width: 100%;
|
| 199 |
+
margin-bottom: 1rem;
|
| 200 |
+
display: flex;
|
| 201 |
+
align-items: center;
|
| 202 |
+
justify-content: center;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.btn-google img {
|
| 206 |
+
width: 20px;
|
| 207 |
+
margin-right: 0.5rem;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.btn-login {
|
| 211 |
+
background-color: #6366F1;
|
| 212 |
+
color: #fff;
|
| 213 |
+
border: none;
|
| 214 |
+
padding: 0.75rem;
|
| 215 |
+
width: 100%;
|
| 216 |
+
margin-top: 1rem;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.btn-login:hover {
|
| 220 |
+
background: #4f46e5;
|
| 221 |
+
transform: translateY(-1px);
|
| 222 |
+
}
|
| 223 |
+
</style>
|
| 224 |
+
</head>
|
| 225 |
+
|
| 226 |
+
<body onload="checkSession()">
|
| 227 |
+
|
| 228 |
+
<div class="forgot-password-container">
|
| 229 |
+
<h2>Đổi mật khẩu</h2>
|
| 230 |
+
<p class="description">Nhập mật khẩu hiện tại và mật khẩu mới (ít nhất 8 ký tự, bao gồm chữ cái và số).</p>
|
| 231 |
+
<div id="change-password-message"></div>
|
| 232 |
+
<form onsubmit="event.preventDefault(); changePassword();">
|
| 233 |
+
<div class="form-group">
|
| 234 |
+
<label for="current_password">Mật khẩu hiện tại</label>
|
| 235 |
+
<input type="password" class="form-control" id="current_password" required>
|
| 236 |
+
</div>
|
| 237 |
+
<div class="form-group">
|
| 238 |
+
<label for="new_password">Mật khẩu mới</label>
|
| 239 |
+
<input type="password" class="form-control" id="new_password" required>
|
| 240 |
+
</div>
|
| 241 |
+
<div class="mb-3 form-group ">
|
| 242 |
+
<a href="/register" class="float-start text-muted">Đăng ký ngay</a>
|
| 243 |
+
<a href="/login" class="float-end text-muted">Đăng nhập ngay</a>
|
| 244 |
+
</div>
|
| 245 |
+
<button type="submit" class="btn btn-login">Đổi mật khẩu</button>
|
| 246 |
+
</form>
|
| 247 |
+
<p class="text-center text-muted mt-3"><a href="/register" style="color: #6366F1;">Đăng
|
| 248 |
+
ký ngay</a> <a href="/login" style="color: #6366F1;">Đăng nhập ngay</a></p>
|
| 249 |
+
|
| 250 |
+
</div>
|
| 251 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
| 252 |
+
<script>
|
| 253 |
+
// Function to display messages
|
| 254 |
+
function showMessage(elementId, message, type = 'success') {
|
| 255 |
+
const messageDiv = document.getElementById(elementId);
|
| 256 |
+
messageDiv.innerHTML = `<div class="alert alert-${type}">${message}</div>`;
|
| 257 |
+
setTimeout(() => messageDiv.innerHTML = '', 5000);
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
// Check session on page load
|
| 261 |
+
function checkSession() {
|
| 262 |
+
fetch('/check_session', {
|
| 263 |
+
method: 'GET',
|
| 264 |
+
headers: { 'Content-Type': 'application/json' }
|
| 265 |
+
})
|
| 266 |
+
.then(response => response.json())
|
| 267 |
+
.then(data => {
|
| 268 |
+
const authNav = document.getElementById('auth-nav');
|
| 269 |
+
if (data.logged_in) {
|
| 270 |
+
authNav.innerHTML = `
|
| 271 |
+
<li class="nav-item">
|
| 272 |
+
<span class="nav-link">Xin chào, ${data.username}</span>
|
| 273 |
+
</li>
|
| 274 |
+
<li class="nav-item">
|
| 275 |
+
<a class="nav-link" href="/change_password">Đổi mật khẩu</a>
|
| 276 |
+
</li>
|
| 277 |
+
<li class="nav-item">
|
| 278 |
+
<a class="nav-link" href="#" onclick="logout()">Đăng xuất</a>
|
| 279 |
+
</li>
|
| 280 |
+
`;
|
| 281 |
+
} else {
|
| 282 |
+
authNav.innerHTML = `
|
| 283 |
+
<li class="nav-item">
|
| 284 |
+
<a class="nav-link" href="/login">Đăng nhập</a>
|
| 285 |
+
</li>
|
| 286 |
+
<li class="nav-item">
|
| 287 |
+
<a class="nav-link" href="/register">Đăng ký</a>
|
| 288 |
+
</li>
|
| 289 |
+
`;
|
| 290 |
+
}
|
| 291 |
+
})
|
| 292 |
+
.catch(error => console.error('Error checking session:', error));
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
// Register
|
| 296 |
+
function register() {
|
| 297 |
+
const username = document.getElementById('username').value;
|
| 298 |
+
const email = document.getElementById('email').value;
|
| 299 |
+
const password = document.getElementById('password').value;
|
| 300 |
+
const phone = document.getElementById('phone').value;
|
| 301 |
+
|
| 302 |
+
fetch('/register', {
|
| 303 |
+
method: 'POST',
|
| 304 |
+
headers: { 'Content-Type': 'application/json' },
|
| 305 |
+
body: JSON.stringify({ username, email, password, phone })
|
| 306 |
+
})
|
| 307 |
+
.then(response => response.json())
|
| 308 |
+
.then(data => {
|
| 309 |
+
if (data.error) {
|
| 310 |
+
showMessage('register-message', data.error, 'danger');
|
| 311 |
+
} else {
|
| 312 |
+
showMessage('register-message', data.message, 'success');
|
| 313 |
+
setTimeout(() => {
|
| 314 |
+
window.location.href = `/verify_otp?user_id=${data.user_id}`;
|
| 315 |
+
}, 2000);
|
| 316 |
+
}
|
| 317 |
+
})
|
| 318 |
+
.catch(error => {
|
| 319 |
+
showMessage('register-message', 'Lỗi hệ thống, vui lòng thử lại', 'danger');
|
| 320 |
+
console.error('Error:', error);
|
| 321 |
+
});
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
// Verify OTP
|
| 325 |
+
function verifyOtp() {
|
| 326 |
+
const user_id = new URLSearchParams(window.location.search).get('user_id');
|
| 327 |
+
const otp = document.getElementById('otp').value;
|
| 328 |
+
|
| 329 |
+
fetch('/verify_otp', {
|
| 330 |
+
method: 'POST',
|
| 331 |
+
headers: { 'Content-Type': 'application/json' },
|
| 332 |
+
body: JSON.stringify({ user_id, otp })
|
| 333 |
+
})
|
| 334 |
+
.then(response => response.json())
|
| 335 |
+
.then(data => {
|
| 336 |
+
if (data.error) {
|
| 337 |
+
showMessage('otp-message', data.error, 'danger');
|
| 338 |
+
} else {
|
| 339 |
+
showMessage('otp-message', data.message, 'success');
|
| 340 |
+
setTimeout(() => window.location.href = '/login', 2000);
|
| 341 |
+
}
|
| 342 |
+
})
|
| 343 |
+
.catch(error => {
|
| 344 |
+
showMessage('otp-message', 'Lỗi hệ thống, vui lòng thử lại', 'danger');
|
| 345 |
+
console.error('Error:', error);
|
| 346 |
+
});
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
// Login
|
| 350 |
+
function login() {
|
| 351 |
+
const email = document.getElementById('email').value;
|
| 352 |
+
const password = document.getElementById('password').value;
|
| 353 |
+
|
| 354 |
+
fetch('/logins', {
|
| 355 |
+
method: 'POST',
|
| 356 |
+
headers: { 'Content-Type': 'application/json' },
|
| 357 |
+
body: JSON.stringify({ email, password })
|
| 358 |
+
})
|
| 359 |
+
.then(response => response.json())
|
| 360 |
+
.then(data => {
|
| 361 |
+
if (data.error) {
|
| 362 |
+
showMessage('login-message', data.error, 'danger');
|
| 363 |
+
} else {
|
| 364 |
+
showMessage('login-message', data.message, 'success');
|
| 365 |
+
setTimeout(() => window.location.href = '/home', 2000);
|
| 366 |
+
}
|
| 367 |
+
})
|
| 368 |
+
.catch(error => {
|
| 369 |
+
showMessage('login-message', 'Lỗi hệ thống, vui lòng thử lại', 'danger');
|
| 370 |
+
console.error('Error:', error);
|
| 371 |
+
});
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
// Forgot Password
|
| 375 |
+
function forgotPassword() {
|
| 376 |
+
const email = document.getElementById('email').value;
|
| 377 |
+
const last_three_digits = document.getElementById('last_three_digits').value;
|
| 378 |
+
|
| 379 |
+
fetch('/forgot_password', {
|
| 380 |
+
method: 'POST',
|
| 381 |
+
headers: { 'Content-Type': 'application/json' },
|
| 382 |
+
body: JSON.stringify({ email, last_three_digits })
|
| 383 |
+
})
|
| 384 |
+
.then(response => response.json())
|
| 385 |
+
.then(data => {
|
| 386 |
+
if (data.error) {
|
| 387 |
+
showMessage('forgot-password-message', data.error, 'danger');
|
| 388 |
+
} else {
|
| 389 |
+
showMessage('forgot-password-message', data.message, 'success');
|
| 390 |
+
setTimeout(() => window.location.href = '/login', 2000);
|
| 391 |
+
}
|
| 392 |
+
})
|
| 393 |
+
.catch(error => {
|
| 394 |
+
showMessage('forgot-password-message', 'Lỗi hệ thống, vui lòng thử lại', 'danger');
|
| 395 |
+
console.error('Error:', error);
|
| 396 |
+
});
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
// Change Password
|
| 400 |
+
function changePassword() {
|
| 401 |
+
const current_password = document.getElementById('current_password').value;
|
| 402 |
+
const new_password = document.getElementById('new_password').value;
|
| 403 |
+
|
| 404 |
+
fetch('/change_password', {
|
| 405 |
+
method: 'POST',
|
| 406 |
+
headers: { 'Content-Type': 'application/json' },
|
| 407 |
+
body: JSON.stringify({ current_password, new_password })
|
| 408 |
+
})
|
| 409 |
+
.then(response => response.json())
|
| 410 |
+
.then(data => {
|
| 411 |
+
if (data.error) {
|
| 412 |
+
showMessage('change-password-message', data.error, 'danger');
|
| 413 |
+
} else {
|
| 414 |
+
showMessage('change-password-message', data.message, 'success');
|
| 415 |
+
setTimeout(() => window.location.href = '/home', 2000);
|
| 416 |
+
}
|
| 417 |
+
})
|
| 418 |
+
.catch(error => {
|
| 419 |
+
showMessage('change-password-message', 'Lỗi hệ thống, vui lòng thử lại', 'danger');
|
| 420 |
+
console.error('Error:', error);
|
| 421 |
+
});
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
// Logout
|
| 425 |
+
function logout() {
|
| 426 |
+
fetch('/logout', {
|
| 427 |
+
method: 'POST',
|
| 428 |
+
headers: { 'Content-Type': 'application/json' }
|
| 429 |
+
})
|
| 430 |
+
.then(response => response.json())
|
| 431 |
+
.then(data => {
|
| 432 |
+
showMessage('auth-message', data.message, 'success');
|
| 433 |
+
setTimeout(() => window.location.href = '/', 1000);
|
| 434 |
+
})
|
| 435 |
+
.catch(error => {
|
| 436 |
+
showMessage('auth-message', 'Lỗi hệ thống, vui lòng thử lại', 'danger');
|
| 437 |
+
console.error('Error:', error);
|
| 438 |
+
});
|
| 439 |
+
}
|
| 440 |
+
</script>
|
| 441 |
+
</body>
|
| 442 |
+
|
| 443 |
+
</html>
|
templates/forgot_password.html
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="vi" data-theme="dark">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<meta name="description" content="Đăng nhập tài khoản của bạn một cách dễ dàng và an toàn">
|
| 8 |
+
<title>Đăng Nhập</title>
|
| 9 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
| 11 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
| 12 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/jarallax/2.2.1/jarallax.min.css" rel="stylesheet">
|
| 13 |
+
<style>
|
| 14 |
+
:root {
|
| 15 |
+
--primary-color: #6366F1;
|
| 16 |
+
--error-color: #dc3545;
|
| 17 |
+
--success-color: #28a745;
|
| 18 |
+
--text-muted: #adb5bd;
|
| 19 |
+
--input-text: #000000; /* Default input text color */
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
[data-theme="dark"] {
|
| 23 |
+
--card-bg: rgba(13, 27, 42, 0.95);
|
| 24 |
+
--input-bg: #2c3e50;
|
| 25 |
+
--input-border: #3b4a5e;
|
| 26 |
+
--input-focus-bg: #34495e;
|
| 27 |
+
--alert-bg: #1b263b;
|
| 28 |
+
--input-text: #ffffff; /* White text for dark mode */
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
[data-theme="light"] {
|
| 32 |
+
--card-bg: rgba(255, 255, 255, 0.95);
|
| 33 |
+
--input-bg: #ffffff;
|
| 34 |
+
--input-border: #ced4da;
|
| 35 |
+
--input-focus-bg: #f8f9fa;
|
| 36 |
+
--alert-bg: #e9ecef;
|
| 37 |
+
--text-muted: #6c757d;
|
| 38 |
+
--input-text: #000000; /* Black text for light mode */
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
body {
|
| 42 |
+
background-color: #000000;
|
| 43 |
+
min-height: 100vh;
|
| 44 |
+
margin: 0;
|
| 45 |
+
display: flex;
|
| 46 |
+
justify-content: center;
|
| 47 |
+
align-items: center;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.form-label {
|
| 51 |
+
color: var(--text-muted);
|
| 52 |
+
font-size: 0.85rem;
|
| 53 |
+
font-weight: 500;
|
| 54 |
+
margin-bottom: 0.5rem;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.jarallax {
|
| 58 |
+
position: relative;
|
| 59 |
+
z-index: 0;
|
| 60 |
+
background-color: #000000;
|
| 61 |
+
min-height: 100vh;
|
| 62 |
+
width: 100%;
|
| 63 |
+
display: flex;
|
| 64 |
+
justify-content: center;
|
| 65 |
+
align-items: center;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.text-muted {
|
| 69 |
+
color: #cbd2d7 !important;
|
| 70 |
+
font-size: 0.85rem;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.text-muted, .form-check-label {
|
| 74 |
+
color: #cbd2d7 !important;
|
| 75 |
+
font-size: 0.85rem;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.btn-google {
|
| 79 |
+
background-color: #6366F1;
|
| 80 |
+
color: #fff;
|
| 81 |
+
border: none;
|
| 82 |
+
padding: 0.5rem;
|
| 83 |
+
width: 100%;
|
| 84 |
+
margin-bottom: 1rem;
|
| 85 |
+
display: flex;
|
| 86 |
+
align-items: center;
|
| 87 |
+
justify-content: center;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.btn-google img {
|
| 91 |
+
width: 20px;
|
| 92 |
+
margin-right: 0.5rem;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.jarallax-img {
|
| 96 |
+
position: absolute;
|
| 97 |
+
object-fit: cover;
|
| 98 |
+
top: 0;
|
| 99 |
+
left: 0;
|
| 100 |
+
width: 100%;
|
| 101 |
+
height: 100%;
|
| 102 |
+
z-index: -1;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.login-container {
|
| 106 |
+
background: var(--card-bg);
|
| 107 |
+
padding: 2.5rem;
|
| 108 |
+
border-radius: 12px;
|
| 109 |
+
width: 100%;
|
| 110 |
+
max-width: 450px;
|
| 111 |
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
| 112 |
+
backdrop-filter: blur(5px);
|
| 113 |
+
transition: transform 0.3s ease;
|
| 114 |
+
position: relative;
|
| 115 |
+
z-index: 1;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.login-container:hover {
|
| 119 |
+
transform: translateY(-2px);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.login-container h2 {
|
| 123 |
+
font-size: 1.75rem;
|
| 124 |
+
font-weight: 600;
|
| 125 |
+
margin-bottom: 1rem;
|
| 126 |
+
text-align: center;
|
| 127 |
+
color: #fff;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.description {
|
| 131 |
+
font-size: 0.9rem;
|
| 132 |
+
text-align: center;
|
| 133 |
+
margin-bottom: 2rem;
|
| 134 |
+
line-height: 1.5;
|
| 135 |
+
color: white;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.form-label {
|
| 139 |
+
font-size: 0.85rem;
|
| 140 |
+
font-weight: 500;
|
| 141 |
+
margin-bottom: 0.5rem;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.form-control {
|
| 145 |
+
background-color: var(--input-bg);
|
| 146 |
+
border: 1px solid var(--input-border);
|
| 147 |
+
color: var(--input-text); /* Use theme-based text color */
|
| 148 |
+
border-radius: 6px;
|
| 149 |
+
padding: 0.75rem;
|
| 150 |
+
transition: all 0.2s ease;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.form-control:focus {
|
| 154 |
+
background-color: var(--input-focus-bg);
|
| 155 |
+
border-color: var(--primary-color);
|
| 156 |
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
|
| 157 |
+
outline: none;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.form-control::placeholder {
|
| 161 |
+
color: var(--input-text); /* White placeholder text in dark mode */
|
| 162 |
+
opacity: 0.7; /* Slightly reduce opacity for better UX */
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.form-control.is-invalid {
|
| 166 |
+
border-color: var(--error-color);
|
| 167 |
+
background-image: none;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.form-control[readonly] {
|
| 171 |
+
color: var(--input-text); /* Ensure readonly input text is white in dark mode */
|
| 172 |
+
opacity: 0.9; /* Slightly reduce opacity for readonly fields */
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.invalid-feedback {
|
| 176 |
+
font-size: 0.8rem;
|
| 177 |
+
color: var(--error-color);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.alert {
|
| 181 |
+
background-color: var(--alert-bg);
|
| 182 |
+
color: var(--text-muted);
|
| 183 |
+
border: none;
|
| 184 |
+
margin-bottom: 1rem;
|
| 185 |
+
font-size: 0.9rem;
|
| 186 |
+
border-radius: 6px;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.alert-success {
|
| 190 |
+
background-color: var(--success-color);
|
| 191 |
+
color: #fff;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.alert-danger {
|
| 195 |
+
background-color: var(--error-color);
|
| 196 |
+
color: #fff;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.btn-submit,
|
| 200 |
+
.btn-login,
|
| 201 |
+
.btn-google {
|
| 202 |
+
background: var(--primary-color);
|
| 203 |
+
color: #fff;
|
| 204 |
+
border: none;
|
| 205 |
+
padding: 0.85rem;
|
| 206 |
+
border-radius: 6px;
|
| 207 |
+
width: 100%;
|
| 208 |
+
font-weight: 500;
|
| 209 |
+
transition: all 0.2s ease;
|
| 210 |
+
position: relative;
|
| 211 |
+
display: flex;
|
| 212 |
+
align-items: center;
|
| 213 |
+
justify-content: center;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.btn-submit:hover:not(:disabled),
|
| 217 |
+
.btn-login:hover:not(:disabled),
|
| 218 |
+
.btn-google:hover:not(:disabled) {
|
| 219 |
+
background: #4f46e5;
|
| 220 |
+
transform: translateY(-1px);
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.btn-login:hover {
|
| 224 |
+
color: #fff;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.btn-submit:disabled,
|
| 228 |
+
.btn-login:disabled,
|
| 229 |
+
.btn-google:disabled {
|
| 230 |
+
opacity: 0.6;
|
| 231 |
+
cursor: not-allowed;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.btn-google img {
|
| 235 |
+
width: 20px;
|
| 236 |
+
margin-right: 0.5rem;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.text-muted {
|
| 240 |
+
font-size: 0.85rem;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.theme-toggle-btn {
|
| 244 |
+
position: absolute;
|
| 245 |
+
top: 1rem;
|
| 246 |
+
right: 1rem;
|
| 247 |
+
background: transparent;
|
| 248 |
+
border: none;
|
| 249 |
+
color: var(--text-muted);
|
| 250 |
+
font-size: 1.2rem;
|
| 251 |
+
cursor: pointer;
|
| 252 |
+
transition: color 0.2s ease;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.theme-toggle-btn:hover {
|
| 256 |
+
color: var(--primary-color);
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.loading-spinner {
|
| 260 |
+
display: none;
|
| 261 |
+
border: 3px solid #f3f3f3;
|
| 262 |
+
border-top: 3px solid var(--primary-color);
|
| 263 |
+
border-radius: 50%;
|
| 264 |
+
width: 20px;
|
| 265 |
+
height: 20px;
|
| 266 |
+
animation: spin 1s linear infinite;
|
| 267 |
+
position: absolute;
|
| 268 |
+
right: 10px;
|
| 269 |
+
top: 50%;
|
| 270 |
+
transform: translateY(-50%);
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
@keyframes spin {
|
| 274 |
+
0% {
|
| 275 |
+
transform: translateY(-50%) rotate(0deg);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
100% {
|
| 279 |
+
transform: translateY(-50%) rotate(360deg);
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
@media (max-width: 576px) {
|
| 284 |
+
.login-container {
|
| 285 |
+
margin: 1.5rem;
|
| 286 |
+
padding: 1.5rem;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.login-container h2 {
|
| 290 |
+
font-size: 1.5rem;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.description {
|
| 294 |
+
font-size: 0.85rem;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.btn-submit,
|
| 298 |
+
.btn-login,
|
| 299 |
+
.btn-google {
|
| 300 |
+
padding: 0.75rem;
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.sr-only {
|
| 305 |
+
position: absolute;
|
| 306 |
+
width: 1px;
|
| 307 |
+
height: 1px;
|
| 308 |
+
padding: 0;
|
| 309 |
+
margin: -1px;
|
| 310 |
+
overflow: hidden;
|
| 311 |
+
clip: rect(0, 0, 0, 0);
|
| 312 |
+
border: 0;
|
| 313 |
+
}
|
| 314 |
+
.form-control{
|
| 315 |
+
color: white !important; /* Ensure input text is white in dark mode */
|
| 316 |
+
}
|
| 317 |
+
</style>
|
| 318 |
+
</head>
|
| 319 |
+
|
| 320 |
+
<body>
|
| 321 |
+
<div class="jarallax" data-jarallax data-speed="0.2">
|
| 322 |
+
<img class="jarallax-img" src="https://silicon.createx.studio/assets/img/landing/saas-5/hero-bg-pattern.png"
|
| 323 |
+
alt="Background">
|
| 324 |
+
<div class="login-container">
|
| 325 |
+
<h2>Quên mật khẩu</h2>
|
| 326 |
+
<p class="description">Nhập email và 4 số cuối của số điện thoại để nhận mật khẩu mới.</p>
|
| 327 |
+
<div id="forgot-password-message"></div>
|
| 328 |
+
<form onsubmit="event.preventDefault(); forgotPassword();">
|
| 329 |
+
<div class="form-group mb-3">
|
| 330 |
+
<label for="email" class="form-label">Email</label>
|
| 331 |
+
<input type="email" class="form-control" id="email" required>
|
| 332 |
+
</div>
|
| 333 |
+
<div class="form-group mb-3">
|
| 334 |
+
<label for="masked_phone" class="form-label">Số điện thoại đã đăng k�� (Tự động)</label>
|
| 335 |
+
<input type="text" class="form-control" id="masked_phone" readonly
|
| 336 |
+
aria-describedby="maskedPhoneFeedback">
|
| 337 |
+
<div id="maskedPhoneFeedback" class="invalid-feedback"></div>
|
| 338 |
+
</div>
|
| 339 |
+
<div class="form-group mb-3">
|
| 340 |
+
<label for="last_four_digits" class="form-label">4 số cuối của số điện thoại</label>
|
| 341 |
+
<input type="text" class="form-control" id="last_four_digits" maxlength="4"
|
| 342 |
+
aria-describedby="lastThreeDigitsFeedback">
|
| 343 |
+
<div id="lastThreeDigitsFeedback" class="invalid-feedback">Vui lòng nhập đúng 3 số cuối của số điện
|
| 344 |
+
thoại.</div>
|
| 345 |
+
</div>
|
| 346 |
+
<div class="mb-3 form-group ">
|
| 347 |
+
<a href="/register" class="float-start text-muted">Đăng ký ngay</a>
|
| 348 |
+
<a href="/login" class="float-end text-muted">Đăng nhập ngay</a>
|
| 349 |
+
</div>
|
| 350 |
+
<br>
|
| 351 |
+
<button type="submit" class="btn btn-login" id="submit-btn">
|
| 352 |
+
Gửi yêu cầu
|
| 353 |
+
<span class="loading-spinner"></span>
|
| 354 |
+
</button>
|
| 355 |
+
</form>
|
| 356 |
+
|
| 357 |
+
</div>
|
| 358 |
+
</div>
|
| 359 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
| 360 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jarallax/2.2.1/jarallax.min.js"></script>
|
| 361 |
+
<script>
|
| 362 |
+
// Show messages
|
| 363 |
+
function showMessage(elementId, message, type = 'success') {
|
| 364 |
+
const messageDiv = document.getElementById(elementId);
|
| 365 |
+
messageDiv.innerHTML = `<div class="alert alert-${type}">${message}</div>`;
|
| 366 |
+
setTimeout(() => messageDiv.innerHTML = '', 5000);
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
// Fetch masked phone number
|
| 370 |
+
async function fetchMaskedPhone(emailInput, maskedPhoneInput) {
|
| 371 |
+
const email = emailInput.value.trim();
|
| 372 |
+
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
| 373 |
+
maskedPhoneInput.value = '';
|
| 374 |
+
document.getElementById('maskedPhoneFeedback').textContent = 'Vui lòng nhập email hợp lệ';
|
| 375 |
+
maskedPhoneInput.classList.add('is-invalid');
|
| 376 |
+
return;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
try {
|
| 380 |
+
document.getElementById('submit-btn').disabled = true;
|
| 381 |
+
document.querySelector('.loading-spinner').classList.add('active');
|
| 382 |
+
const response = await fetch('/get_masked_phone', {
|
| 383 |
+
method: 'POST',
|
| 384 |
+
headers: { 'Content-Type': 'application/json' },
|
| 385 |
+
body: JSON.stringify({ email })
|
| 386 |
+
});
|
| 387 |
+
const data = await response.json();
|
| 388 |
+
|
| 389 |
+
if (data.error) {
|
| 390 |
+
maskedPhoneInput.value = '';
|
| 391 |
+
document.getElementById('maskedPhoneFeedback').textContent = data.error;
|
| 392 |
+
maskedPhoneInput.classList.add('is-invalid');
|
| 393 |
+
} else {
|
| 394 |
+
maskedPhoneInput.classList.remove('is-invalid');
|
| 395 |
+
document.getElementById('maskedPhoneFeedback').textContent = '';
|
| 396 |
+
maskedPhoneInput.value = data.masked_phone;
|
| 397 |
+
}
|
| 398 |
+
} catch (error) {
|
| 399 |
+
maskedPhoneInput.value = '';
|
| 400 |
+
document.getElementById('maskedPhoneFeedback').textContent = _action
|
| 401 |
+
'Lỗi hệ thống, vui lòng thử lại';
|
| 402 |
+
maskedPhoneInput.classList.add('is-invalid');
|
| 403 |
+
console.error('Error:', error);
|
| 404 |
+
} finally {
|
| 405 |
+
document.getElementById('submit-btn').disabled = false;
|
| 406 |
+
document.querySelector('.loading-spinner').classList.remove('active');
|
| 407 |
+
}
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
// Check session on page load
|
| 411 |
+
function checkSession() {
|
| 412 |
+
fetch('/check_session', {
|
| 413 |
+
method: 'GET',
|
| 414 |
+
headers: { 'Content-Type': 'application/json' }
|
| 415 |
+
})
|
| 416 |
+
.then(response => response.json())
|
| 417 |
+
.then(data => {
|
| 418 |
+
const authNav = document.getElementById('auth-nav');
|
| 419 |
+
if (data.logged_in) {
|
| 420 |
+
authNav.innerHTML = `
|
| 421 |
+
<li class="nav-item">
|
| 422 |
+
<span class="nav-link">Xin chào, ${data.username}</span>
|
| 423 |
+
</li>
|
| 424 |
+
<li class="nav-item">
|
| 425 |
+
<a class="nav-link" href="/change_password">Đổi mật khẩu</a>
|
| 426 |
+
</li>
|
| 427 |
+
<li class="nav-item">
|
| 428 |
+
<a class="nav-link" href="#" onclick="logout()">Đăng xuất</a>
|
| 429 |
+
</li>
|
| 430 |
+
`;
|
| 431 |
+
} else {
|
| 432 |
+
authNav.innerHTML = `
|
| 433 |
+
<li class="nav-item">
|
| 434 |
+
<a class="nav-link" href="/login">Đăng nhập</a>
|
| 435 |
+
</li>
|
| 436 |
+
<li class="nav-item">
|
| 437 |
+
<a class="nav-link" href="/register">Đăng ký</a>
|
| 438 |
+
</li>
|
| 439 |
+
`;
|
| 440 |
+
}
|
| 441 |
+
})
|
| 442 |
+
.catch(error => console.error('Error checking session:', error));
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
// Register
|
| 446 |
+
function register() {
|
| 447 |
+
const username = document.getElementById('username')?.value;
|
| 448 |
+
const email = document.getElementById('email')?.value;
|
| 449 |
+
const password = document.getElementById('password')?.value;
|
| 450 |
+
const phone = document.getElementById('phone')?.value;
|
| 451 |
+
|
| 452 |
+
fetch('/register', {
|
| 453 |
+
method: 'POST',
|
| 454 |
+
headers: { 'Content-Type': 'application/json' },
|
| 455 |
+
body: JSON.stringify({ username, email, password, phone })
|
| 456 |
+
})
|
| 457 |
+
.then(response => response.json())
|
| 458 |
+
.then(data => {
|
| 459 |
+
if (data.error) {
|
| 460 |
+
showMessage('register-message', data.error, 'danger');
|
| 461 |
+
} else {
|
| 462 |
+
showMessage('register-message', data.message, 'success');
|
| 463 |
+
setTimeout(() => {
|
| 464 |
+
window.location.href = `/verify_otp?user_id=${data.user_id}`;
|
| 465 |
+
}, 2000);
|
| 466 |
+
}
|
| 467 |
+
})
|
| 468 |
+
.catch(error => {
|
| 469 |
+
showMessage('register-message', 'Lỗi hệ thống, vui lòng thử lại', 'danger');
|
| 470 |
+
console.error('Error:', error);
|
| 471 |
+
});
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
// Verify OTP
|
| 475 |
+
function verifyOtp() {
|
| 476 |
+
const user_id = new URLSearchParams(window.location.search).get('user_id');
|
| 477 |
+
const otp = document.getElementById('otp')?.value;
|
| 478 |
+
|
| 479 |
+
fetch('/verify_otp', {
|
| 480 |
+
method: 'POST',
|
| 481 |
+
headers: { 'Content-Type': 'application/json' },
|
| 482 |
+
body: JSON.stringify({ user_id, otp })
|
| 483 |
+
})
|
| 484 |
+
.then(response => response.json())
|
| 485 |
+
.then(data => {
|
| 486 |
+
if (data.error) {
|
| 487 |
+
showMessage('otp-message', data.error, 'danger');
|
| 488 |
+
} else {
|
| 489 |
+
showMessage('otp-message', 'Xác minh thành công', 'success');
|
| 490 |
+
setTimeout(() => window.location.href = '/login', 2000);
|
| 491 |
+
}
|
| 492 |
+
})
|
| 493 |
+
.catch(error => {
|
| 494 |
+
showMessage('otp-message', 'Lỗi hệ thống, vui lòng thử lại', ' Hambackend');
|
| 495 |
+
console.error('Error:', error);
|
| 496 |
+
});
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
// Login
|
| 500 |
+
function login() {
|
| 501 |
+
const email = document.getElementById('email')?.value;
|
| 502 |
+
const password = document.getElementById('password')?.value;
|
| 503 |
+
|
| 504 |
+
fetch('/login', {
|
| 505 |
+
method: 'POST',
|
| 506 |
+
headers: { 'Content-Type': 'application/json' },
|
| 507 |
+
body: JSON.stringify({ email, password })
|
| 508 |
+
})
|
| 509 |
+
.then(response => response.json())
|
| 510 |
+
.then(data => {
|
| 511 |
+
if (data.error) {
|
| 512 |
+
showMessage('login-message', data.error, 'danger');
|
| 513 |
+
} else {
|
| 514 |
+
showMessage('login-message', 'Đăng nhập thành công', 'success');
|
| 515 |
+
setTimeout(() => window.location.href = '/home', 2000);
|
| 516 |
+
}
|
| 517 |
+
})
|
| 518 |
+
.catch(error => {
|
| 519 |
+
showMessage('login-message', 'Lỗi hệ thống, vui lòng thử lại', 'danger');
|
| 520 |
+
console.error('Error:', error);
|
| 521 |
+
});
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
// Forgot Password
|
| 525 |
+
function forgotPassword() {
|
| 526 |
+
const email = document.getElementById('email').value;
|
| 527 |
+
const last_four_digits = document.getElementById('last_four_digits').value;
|
| 528 |
+
|
| 529 |
+
if (!/^[0-9]{4}$/.test(last_four_digits)) {
|
| 530 |
+
document.getElementById('last_four_digits').classList.add('is-invalid');
|
| 531 |
+
return;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
document.getElementById('submit-btn').disabled = true;
|
| 535 |
+
document.querySelector('.loading-spinner').classList.add('active');
|
| 536 |
+
|
| 537 |
+
fetch('/forgot_password', {
|
| 538 |
+
method: 'POST',
|
| 539 |
+
headers: { 'Content-Type': 'application/json' },
|
| 540 |
+
body: JSON.stringify({ email, last_four_digits })
|
| 541 |
+
})
|
| 542 |
+
.then(response => response.json())
|
| 543 |
+
.then(data => {
|
| 544 |
+
if (data.error) {
|
| 545 |
+
showMessage('forgot-password-message', data.error, 'danger');
|
| 546 |
+
} else {
|
| 547 |
+
showMessage('forgot-password-message', data.message, 'success');
|
| 548 |
+
setTimeout(() => window.location.href = '/login', 2000);
|
| 549 |
+
}
|
| 550 |
+
})
|
| 551 |
+
.catch(error => {
|
| 552 |
+
showMessage('forgot-password-message', 'Lỗi hệ thống, vui lòng thử lại', 'danger');
|
| 553 |
+
console.error('Error:', error);
|
| 554 |
+
})
|
| 555 |
+
.finally(() => {
|
| 556 |
+
document.getElementById('submit-btn').disabled = false;
|
| 557 |
+
document.querySelector('.loading-spinner').classList.remove('active');
|
| 558 |
+
});
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
// Change Password
|
| 562 |
+
function changePassword() {
|
| 563 |
+
const current_password = document.getElementById('current_password').value;
|
| 564 |
+
const new_password = document.getElementById('new_password').value;
|
| 565 |
+
|
| 566 |
+
fetch('/change_password', {
|
| 567 |
+
method: 'POST',
|
| 568 |
+
headers: { 'Content-Type': 'application/json' },
|
| 569 |
+
body: JSON.stringify({ current_password, new_password })
|
| 570 |
+
})
|
| 571 |
+
.then(response => response.json())
|
| 572 |
+
.then(data => {
|
| 573 |
+
if (data.error) {
|
| 574 |
+
showMessage('change-password-message', data.error, 'danger');
|
| 575 |
+
} else {
|
| 576 |
+
showMessage('change-password-message', data.message, 'success');
|
| 577 |
+
setTimeout(() => window.location.href = '/home', 2000);
|
| 578 |
+
}
|
| 579 |
+
})
|
| 580 |
+
.catch(error => {
|
| 581 |
+
showMessage('change-password-message', 'Lỗi hệ thống, vui lòng thử lại', 'danger');
|
| 582 |
+
console.error('Error:', error);
|
| 583 |
+
});
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
|
| 587 |
+
// Logout
|
| 588 |
+
function logout() {
|
| 589 |
+
fetch('/logout', {
|
| 590 |
+
method: 'POST',
|
| 591 |
+
headers: { 'Content-Type': 'application/json' }
|
| 592 |
+
})
|
| 593 |
+
.then(response => response.json())
|
| 594 |
+
.then(data => {
|
| 595 |
+
showMessage('auth-message', data.message, 'success');
|
| 596 |
+
setTimeout(() => window.location.href = '/', 1000);
|
| 597 |
+
})
|
| 598 |
+
.catch(error => {
|
| 599 |
+
showMessage('auth-message', 'Lỗi hệ thống, vui lòng thử lại', 'danger');
|
| 600 |
+
console.error('Error:', error);
|
| 601 |
+
});
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
// Event listener for email input
|
| 605 |
+
document.getElementById('email').addEventListener('input', () => {
|
| 606 |
+
const emailInput = document.getElementById('email');
|
| 607 |
+
const maskedPhoneInput = document.getElementById('masked_phone');
|
| 608 |
+
fetchMaskedPhone(emailInput, maskedPhoneInput);
|
| 609 |
+
});
|
| 610 |
+
</script>
|
| 611 |
+
</body>
|
| 612 |
+
|
| 613 |
+
</html>
|