Nguyendat92929 commited on
Commit
8a15958
·
verified ·
1 Parent(s): 44dff95

upload file

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +7 -0
  2. .gitattributes +3 -35
  3. .gitignore +2 -0
  4. Dockerfile +32 -0
  5. app.py +1069 -0
  6. config.yaml +36 -0
  7. embedding_data/embeddings.pkl +3 -0
  8. embedding_data/faiss_index_23_06.index +3 -0
  9. gemini_handler.py +559 -0
  10. readme.md +1 -0
  11. requirements.txt +21 -0
  12. static/assets/01-dark.png +0 -0
  13. static/assets/01-light.png +3 -0
  14. static/assets/01.jpg +0 -0
  15. static/assets/02-dark.png +0 -0
  16. static/assets/02-light.png +0 -0
  17. static/assets/02.jpg +0 -0
  18. static/assets/03-dark.png +0 -0
  19. static/assets/03-light.png +0 -0
  20. static/assets/48.jpg +0 -0
  21. static/assets/49.jpg +0 -0
  22. static/assets/50.jpg +0 -0
  23. static/assets/awwwards.png +0 -0
  24. static/assets/boxicons.min.css +1 -0
  25. static/assets/clutch-rating.png +0 -0
  26. static/assets/clutch.png +0 -0
  27. static/assets/good-firms.png +0 -0
  28. static/assets/jarallax.min.js +6 -0
  29. static/assets/java.png +0 -0
  30. static/assets/landings.jpg +0 -0
  31. static/assets/logo.svg +75 -0
  32. static/assets/node-dark.png +0 -0
  33. static/assets/node-light.png +0 -0
  34. static/assets/product-hunt.png +0 -0
  35. static/assets/react.png +0 -0
  36. static/assets/rellax.min.js +14 -0
  37. static/assets/swiper-bundle.min.css +13 -0
  38. static/assets/swiper-bundle.min.js +0 -0
  39. static/assets/theme-switcher.js +68 -0
  40. static/assets/theme.min.css +0 -0
  41. static/assets/theme.min.js +23 -0
  42. static/assets/vue-dark.png +0 -0
  43. static/assets/vue-light.png +0 -0
  44. static/css/styles.css +39 -0
  45. static/script.js +506 -0
  46. static/style.css +1036 -0
  47. static/translations.js +134 -0
  48. templates/admin_dashboard.html +453 -0
  49. templates/change_password.html +443 -0
  50. 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
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
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

  • SHA256: b3f34001a3ec7e01d2fba733105d990636eca9436582918f49e605574194c6f1
  • Pointer size: 131 Bytes
  • Size of remote file: 113 kB
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>