This view is limited to 50 files because it contains too many changes.  See the raw diff here.
Files changed (50) hide show
  1. .env +0 -7
  2. .gitattributes +35 -3
  3. .gitignore +0 -2
  4. Dockerfile +0 -46
  5. app.py +0 -1149
  6. config.yaml +0 -36
  7. embedding_data/embeddings.pkl +0 -3
  8. embedding_data/faiss_index_23_06.index +0 -3
  9. gemini_handler.py +0 -559
  10. readme.md +0 -1
  11. requirements.txt +0 -20
  12. static/assets/01-dark.png +0 -0
  13. static/assets/01-light.png +0 -3
  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 +0 -1
  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 +0 -6
  29. static/assets/java.png +0 -0
  30. static/assets/landings.jpg +0 -0
  31. static/assets/logo.svg +0 -75
  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 +0 -14
  37. static/assets/swiper-bundle.min.css +0 -13
  38. static/assets/swiper-bundle.min.js +0 -0
  39. static/assets/theme-switcher.js +0 -68
  40. static/assets/theme.min.css +0 -0
  41. static/assets/theme.min.js +0 -23
  42. static/assets/vue-dark.png +0 -0
  43. static/assets/vue-light.png +0 -0
  44. static/css/styles.css +0 -39
  45. static/script.js +0 -506
  46. static/style.css +0 -1036
  47. static/style_admin.css +0 -616
  48. static/translations.js +0 -134
  49. templates/admin_dashboard.html +0 -365
  50. templates/change_password.html +0 -443
.env DELETED
@@ -1,7 +0,0 @@
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,3 +1,35 @@
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
.gitignore DELETED
@@ -1,2 +0,0 @@
1
- *.log
2
- *.tmp
 
 
 
Dockerfile DELETED
@@ -1,46 +0,0 @@
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
- # Install system dependencies for MongoDB, FAISS, and other libraries
8
- RUN apt-get update && apt-get install -y \
9
- gcc \
10
- g++ \
11
- libffi-dev \
12
- wget \
13
- && rm -rf /var/lib/apt/lists/*
14
-
15
- # Copy the requirements file into the container
16
- COPY requirements.txt .
17
-
18
- # Install Python dependencies
19
- RUN pip install --no-cache-dir -r requirements.txt
20
- RUN pip install gunicorn certifi eventlet
21
-
22
- # Download Boxicons fonts to fix 404 error
23
- RUN mkdir -p /app/static/fonts && \
24
- wget -P /app/static/fonts https://unpkg.com/boxicons@2.1.4/fonts/boxicons.woff2 && \
25
- wget -P /app/static/fonts https://unpkg.com/boxicons@2.1.4/fonts/boxicons.woff && \
26
- wget -P /app/static/fonts https://unpkg.com/boxicons@2.1.4/fonts/boxicons.ttf
27
-
28
- # Set up cache, log, and session directories with proper permissions
29
- ENV HF_HOME=/app/.cache/huggingface
30
- ENV FLASK_SESSION_DIR=/app/sessions
31
- RUN mkdir -p /app/.cache/huggingface /app/logs /app/sessions /app/static/fonts /app/embedding_data && \
32
- chmod -R 777 /app/.cache/huggingface /app/logs /app/sessions /app/static/fonts /app/embedding_data
33
-
34
- # Copy the entire application code, including embedding_data
35
- COPY . .
36
-
37
- # Expose the port Hugging Face Spaces expects
38
- EXPOSE 7860
39
-
40
- # Set environment variables
41
- ENV FLASK_ENV=production
42
- ENV PYTHONUNBUFFERED=1
43
- ENV PORT=7860
44
-
45
- # Command to run the application with Gunicorn
46
- CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "4", "--timeout", "120", "--worker-class", "eventlet", "app:app"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py DELETED
@@ -1,1149 +0,0 @@
1
- import os
2
- import logging
3
- import certifi
4
- from flask import Flask, request, jsonify, session, render_template, redirect, url_for
5
- from flask_socketio import SocketIO, emit, disconnect
6
- from flask_cors import CORS
7
- from pymongo import MongoClient
8
- from datetime import datetime, timedelta
9
- from gemini_handler import GeminiHandler, GenerationConfig, Strategy, KeyRotationStrategy
10
- from langchain.memory import ConversationBufferMemory
11
- import json
12
- from typing import List, Dict
13
- import re
14
- import pickle
15
- import faiss
16
- import torch
17
- import numpy as np
18
- from sentence_transformers import SentenceTransformer
19
- from bson import ObjectId
20
- import hashlib
21
- import smtplib
22
- from email.mime.text import MIMEText
23
- import random
24
- import string
25
- from functools import wraps
26
- import threading
27
- import time
28
-
29
- # Cấu hình logging
30
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
31
-
32
- app = Flask(__name__)
33
- CORS(app) # Cho phép tất cả nguồn gốc
34
-
35
- app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'NQJWnj7YwbQML8yENQJWnj7YwbQML8yE')
36
- app.config['MONGO_URI'] = os.getenv('MONGO_URI', 'mongodb+srv://itdatit12:NQJWnj7YwbQML8yE@cluster0.pwv2g0y.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0')
37
- app.config['SMTP_SERVER'] = 'smtp.gmail.com'
38
- app.config['SMTP_PORT'] = 587
39
- app.config['EMAIL_ADDRESS'] = os.getenv('EMAIL_ADDRESS', 'legalmind2025@gmail.com')
40
- app.config['EMAIL_PASSWORD'] = os.getenv('EMAIL_PASSWORD', 'hihj vpcb ayjk gaex')
41
- app.config['SESSION_TYPE'] = 'filesystem'
42
- app.config['SESSION_FILE_DIR'] = os.getenv('FLASK_SESSION_DIR', '/app/sessions')
43
- app.config['SESSION_PERMANENT'] = True
44
- app.config['PERMANENT_SESSION_LIFETIME'] = 86400
45
-
46
- # Đảm bảo thư mục session tồn tại
47
- os.makedirs(app.config['SESSION_FILE_DIR'], exist_ok=True)
48
-
49
- socketio = SocketIO(app, cors_allowed_origins="*", async_mode='gevent', ping_timeout=120, ping_interval=30)
50
-
51
- # Khởi tạo MongoDB client với certifi
52
- mongo = MongoClient(app.config['MONGO_URI'], ssl_cert_reqs='CERT_REQUIRED', ssl_ca_certs=certifi.where())
53
- db = mongo.get_database('legal_assistant')
54
-
55
-
56
- # Lưu trữ WebSocket clients theo user_id
57
- connected_clients = {}
58
-
59
- # Hàm hash mật khẩu
60
- def hash_password(password: str) -> str:
61
- salt = os.urandom(32)
62
- hashed = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
63
- password_hash = (salt + hashed).hex()
64
- logging.info(f"Tạo hash mật khẩu")
65
- return password_hash
66
-
67
- # Hàm xác minh mật khẩu
68
- def verify_password(stored_password: str, provided_password: str) -> bool:
69
- if not stored_password or not all(c in '0123456789abcdefABCDEF' for c in stored_password):
70
- logging.error(f"Định dạng mật khẩu lưu trữ không hợp lệ")
71
- return False
72
- try:
73
- stored_bytes = bytes.fromhex(stored_password)
74
- salt = stored_bytes[:32]
75
- stored_hash = stored_bytes[32:]
76
- provided_hash = hashlib.pbkdf2_hmac('sha256', provided_password.encode('utf-8'), salt, 100000)
77
- return stored_hash == provided_hash
78
- except ValueError as e:
79
- logging.error(f"Lỗi trong verify_password: {e}")
80
- return False
81
-
82
- # Tạo OTP
83
- def generate_otp(length=6):
84
- return ''.join(random.choices(string.digits, k=length))
85
-
86
- # Gửi email với OTP hoặc mật khẩu
87
- def send_email(to_email, subject, body):
88
- msg = MIMEText(body)
89
- msg['Subject'] = subject
90
- msg['From'] = app.config['EMAIL_ADDRESS']
91
- msg['To'] = to_email
92
- try:
93
- with smtplib.SMTP(app.config['SMTP_SERVER'], app.config['SMTP_PORT']) as server:
94
- server.starttls()
95
- server.login(app.config['EMAIL_ADDRESS'], app.config['EMAIL_PASSWORD'])
96
- server.send_message(msg)
97
- logging.info(f"Email đã gửi tới {to_email}")
98
- return True
99
- except Exception as e:
100
- logging.error(f"Lỗi khi gửi email: {e}")
101
- return False
102
-
103
- # Khởi tạo model embedding
104
- model = SentenceTransformer('hiieu/halong_embedding', device='cuda' if torch.cuda.is_available() else 'cpu')
105
-
106
- # Đường dẫn đến FAISS index và dữ liệu embeddings
107
- INDEX_PATH = "embedding_data/faiss_index_23_06.index"
108
- EMBEDDINGS_DATA_PATH = "embedding_data/embeddings.pkl"
109
-
110
- # Khởi tạo ConversationBufferMemory
111
- memory = ConversationBufferMemory(
112
- memory_key="chat_history",
113
- return_messages=True,
114
- max_message_limit=10,
115
- max_token_limit=1000
116
- )
117
-
118
- # Tải FAISS index
119
- def load_faiss_index(index_path):
120
- try:
121
- index = faiss.read_index(index_path)
122
- logging.info(f"Đã tải FAISS index từ {index_path}")
123
- return index
124
- except Exception as e:
125
- logging.error(f"Lỗi khi tải FAISS index: {e}")
126
- return None
127
-
128
- # Tải dữ liệu embeddings
129
- def load_embeddings_data(data_path):
130
- try:
131
- with open(data_path, 'rb') as f:
132
- embeddings_data = pickle.load(f)
133
- logging.info(f"Đã tải dữ liệu embeddings từ {data_path}")
134
- return embeddings_data
135
- except Exception as e:
136
- logging.error(f"Lỗi khi tải dữ liệu embeddings: {e}")
137
- return None
138
-
139
- # Hàm truy xuất
140
- def retrieve(query, index, embeddings_data, k=10):
141
- try:
142
- query_embedding = model.encode([query], convert_to_numpy=True)
143
- distances, indices = index.search(query_embedding, k)
144
- results = []
145
- for idx, distance in zip(indices[0], distances[0]):
146
- results.append({
147
- 'file': embeddings_data[idx]['file'],
148
- 'folder': embeddings_data[idx]['folder'],
149
- 'text_path': embeddings_data[idx]['text_path'],
150
- 'text': embeddings_data[idx]['text'],
151
- 'distance': float(distance)
152
- })
153
- return results
154
- except Exception as e:
155
- logging.error(f"Lỗi trong quá trình truy xuất: {e}")
156
- return []
157
-
158
- # Tải FAISS index và dữ liệu embeddings
159
- index = load_faiss_index(INDEX_PATH)
160
- embeddings_data = load_embeddings_data(EMBEDDINGS_DATA_PATH)
161
- if index is None or embeddings_data is None:
162
- logging.error("Không thể tải FAISS index hoặc dữ liệu embeddings. Ứng dụng không thể khởi động.")
163
- exit(1)
164
-
165
- # Reset số lượt truy vấn cho tài khoản giới hạn
166
- def reset_query_count(user_id):
167
- user = db.users.find_one({'_id': ObjectId(user_id)})
168
- if not user or user.get('account_type') == 'unlimited':
169
- return
170
- last_reset = user.get('last_reset')
171
- if last_reset and datetime.utcnow() - last_reset > timedelta(days=1):
172
- db.users.update_one(
173
- {'_id': ObjectId(user_id)},
174
- {'$set': {'query_count': 0, 'last_reset': datetime.utcnow()}}
175
- )
176
- logging.info(f"Đã reset số lượt truy vấn cho người dùng {user_id}")
177
-
178
- # Kiểm tra quyền truy vấn
179
- def can_make_query(user_id):
180
- user = db.users.find_one({'_id': ObjectId(user_id)})
181
- if not user:
182
- return False, "Người dùng không tồn tại", None, None
183
- if user.get('is_admin') or user.get('account_type') == 'unlimited':
184
- return True, None, None, None
185
- reset_query_count(user_id)
186
- user = db.users.find_one({'_id': ObjectId(user_id)})
187
- query_limit = user.get('query_limit', 10)
188
- query_count = user.get('query_count', 0)
189
- if query_count >= query_limit:
190
- return False, f"Bạn đã sử dụng hết {query_limit} lượt hỏi đáp hôm nay", query_count, query_limit
191
- if query_count + 1 == query_limit:
192
- 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
193
- return True, None, query_count, query_limit
194
-
195
- # Decorator yêu cầu quyền admin
196
- def admin_required(f):
197
- @wraps(f)
198
- def decorated_function(*args, **kwargs):
199
- if 'user_id' not in session:
200
- return jsonify({'error': 'Vui lòng đăng nhập'}), 401
201
- user = db.users.find_one({'_id': ObjectId(session['user_id'])})
202
- if not user or not user.get('is_admin'):
203
- return jsonify({'error': 'Quyền truy cập bị từ chối. Chỉ admin được phép.'}), 403
204
- return f(*args, **kwargs)
205
- return decorated_function
206
-
207
-
208
- # Preprocess related questions
209
- def preprocess_related_questions(related_questions_input: str | List[Dict[str, str]]) -> List[Dict[str, str]]:
210
- fallback_questions = [
211
- {"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?"},
212
- {"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?"},
213
- {"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?"},
214
- {"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?"},
215
- {"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?"}
216
- ]
217
- if isinstance(related_questions_input, str):
218
- cleaned_input = re.sub(r'^```json\s*|\s*```$', '', related_questions_input).strip()
219
- try:
220
- related_questions = json.loads(cleaned_input)
221
- except json.JSONDecodeError:
222
- return fallback_questions[:5]
223
- else:
224
- related_questions = related_questions_input
225
- if not isinstance(related_questions, list):
226
- return fallback_questions[:5]
227
- valid_questions = [
228
- q for q in related_questions
229
- if isinstance(q, dict) and "question" in q and isinstance(q["question"], str) and q["question"].strip()
230
- ]
231
- seen = set()
232
- unique_questions = []
233
- for q in valid_questions:
234
- question_text = q["question"].strip()
235
- if question_text not in seen:
236
- seen.add(question_text)
237
- unique_questions.append({"question": question_text})
238
- 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)"
239
- filtered_questions = [
240
- q for q in unique_questions
241
- if re.search(legal_keywords, q["question"], re.IGNORECASE)
242
- ]
243
- if len(filtered_questions) < 5:
244
- remaining = 5 - len(filtered_questions)
245
- for fq in fallback_questions:
246
- if len(filtered_questions) >= 5:
247
- break
248
- if fq["question"] not in seen:
249
- filtered_questions.append(fq)
250
- seen.add(fq["question"])
251
- return filtered_questions[:5]
252
-
253
- def format_chat_history(memory):
254
- messages = memory.chat_memory.messages
255
- if not messages:
256
- return "Không có lịch sử hội thoại trước."
257
- formatted = []
258
- for m in messages:
259
- role = getattr(m, "type", None) or m.get("role", "User")
260
- content = getattr(m, "content", None) or m.get("content", "")
261
- formatted.append(f"{role.capitalize()}: {content}")
262
- return "\n".join(formatted)
263
-
264
- # WebSocket handlers
265
- @socketio.on('connect')
266
- def handle_connect():
267
- user_id = session.get('user_id')
268
- if user_id:
269
- connected_clients[user_id] = request.sid
270
- logging.info(f"User {user_id} connected via WebSocket with SID {request.sid}")
271
- else:
272
- disconnect() # Disconnect unauthorized clients
273
- logging.warning("Unauthorized WebSocket connection attempt")
274
-
275
- @socketio.on('disconnect')
276
- def handle_disconnect():
277
- user_id = session.get('user_id')
278
- if user_id in connected_clients and connected_clients[user_id] == request.sid:
279
- del connected_clients[user_id]
280
- logging.info(f"User {user_id} disconnected from WebSocket")
281
-
282
- # Đăng ký
283
- @app.route('/register', methods=['GET', 'POST'])
284
- def register():
285
- if request.method == 'GET':
286
- return render_template('register.html')
287
- data = request.get_json(silent=True) or {}
288
- username = data.get('username', '').strip()
289
- email = data.get('email', '').strip()
290
- password = data.get('password', '').strip()
291
- phone = data.get('phone', '').strip()
292
- account_type = data.get('account_type', 'limited').strip()
293
- if not username or not email or not password or not phone:
294
- return jsonify({'error': 'Thiếu thông tin bắt buộc'}), 400
295
- if not re.match(r'^\+84\d{9}$|^0\d{9}$', phone):
296
- return jsonify({'error': 'Số điện thoại không hợp lệ'}), 400
297
- if account_type not in ['limited', 'unlimited']:
298
- return jsonify({'error': 'Loại tài khoản không hợp lệ'}), 400
299
- if db.users.find_one({'$or': [{'email': email}, {'phone': phone}]}):
300
- return jsonify({'error': 'Email hoặc số điện thoại đã tồn tại'}), 400
301
- otp = generate_otp()
302
- password_hash = hash_password(password)
303
- user = {
304
- 'username': username,
305
- 'email': email,
306
- 'phone': phone,
307
- 'password_hash': password_hash,
308
- 'otp': otp,
309
- 'is_active': False,
310
- 'created_at': datetime.utcnow(),
311
- 'is_admin': False,
312
- 'account_type': account_type,
313
- 'query_limit': 3 if account_type == 'limited' else None,
314
- 'query_count': 0,
315
- 'last_reset': datetime.utcnow()
316
- }
317
- result = db.users.insert_one(user)
318
- if send_email(
319
- email,
320
- 'Mã OTP xác thực tài khoản',
321
- 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.'
322
- ):
323
- return jsonify({
324
- 'message': 'Đăng ký thành công, vui lòng kiểm tra email để lấy mã OTP',
325
- 'user_id': str(result.inserted_id)
326
- }), 201
327
- else:
328
- db.users.delete_one({'_id': result.inserted_id})
329
- return jsonify({'error': 'Lỗi khi gửi OTP, vui lòng thử lại'}), 500
330
-
331
-
332
- @app.route('/admin/user/<user_id>/verify', methods=['POST'])
333
- @admin_required
334
- def verify_user(user_id):
335
- data = request.get_json(silent=True) or {}
336
- action = data.get('action') # 'approve' or 'reject'
337
- if action not in ['approve', 'reaction']:
338
- return jsonify({'error': 'Hành động không hợp lệ'}), 400
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('is_active') != False:
343
- return jsonify({'error': 'Tài khoản không ở trạng thái chờ xác thực'}), 400
344
- if action == 'approve':
345
- db.users.update_one(
346
- {'_id': ObjectId(user_id)},
347
- {'$set': {'is_active': True, 'otp': None, 'created_at': datetime.utcnow()}}
348
- )
349
- send_email(
350
- user['email'],
351
- 'Tài khoản đã được xác thực',
352
- f'Tài khoản của bạn ({user["username"]}) đã được admin xác thực thành công. Bạn có thể đăng nhập tại https://legalmindver1.loca.lt/login.'
353
- )
354
- return jsonify({'message': 'Xác thực tài khoản thành công'}), 200
355
- else: # reject
356
- db.users.delete_one({'_id': ObjectId(user_id)})
357
- send_email(
358
- user['email'],
359
- 'Tài khoản bị từ chối',
360
- f'Tài khoản của bạn ({user["username"]}) đã bị từ chối bởi admin. Vui lòng liên hệ hỗ trợ nếu cần thêm thông tin.'
361
- )
362
- return jsonify({'message': 'Từ chối tài khoản thành công'}), 200
363
-
364
- @app.route('/admin/pending_users', methods=['GET'])
365
- @admin_required
366
- def get_pending_users():
367
- pending_users = db.users.find({'is_active': False})
368
- return jsonify([{
369
- 'id': str(user['_id']),
370
- 'username': user['username'],
371
- 'email': user['email'],
372
- 'phone': user['phone'],
373
- 'created_at': user['created_at'].isoformat(),
374
- 'account_type': user.get('account_type', 'limited')
375
- } for user in pending_users]), 200
376
-
377
- # Xác thực OTP
378
- @app.route('/verify_otp', methods=['GET', 'POST'])
379
- def verify_otp():
380
- if request.method == 'GET':
381
- user_id = request.args.get('user_id')
382
- if not user_id:
383
- return jsonify({'error': 'Thiếu user_id'}), 400
384
- try:
385
- user = db.users.find_one({'_id': ObjectId(user_id)})
386
- if not user:
387
- return jsonify({'error': 'Người dùng không tồn tại'}), 404
388
- return render_template('verify_otp.html', user_id=user_id)
389
- except Exception as e:
390
- logging.error(f"Invalid user_id: {e}")
391
- return jsonify({'error': 'user_id không hợp lệ'}), 400
392
- elif request.method == 'POST':
393
- data = request.get_json(silent=True) or {}
394
- user_id = data.get('user_id', '').strip()
395
- otp = data.get('otp', '').strip()
396
- if not user_id or not otp:
397
- return jsonify({'error': 'Thiếu user_id hoặc OTP'}), 400
398
- try:
399
- user = db.users.find_one({'_id': ObjectId(user_id)})
400
- if not user:
401
- return jsonify({'error': 'Người dùng không tồn tại'}), 404
402
- if user.get('otp') != otp:
403
- return jsonify({'error': 'Mã OTP không đúng'}), 400
404
- db.users.update_one(
405
- {'_id': ObjectId(user_id)},
406
- {'$set': {'is_active': True, 'otp': None}}
407
- )
408
- return jsonify({'message': 'Xác thực tài khoản thành công'}), 200
409
- except Exception as e:
410
- logging.error(f"Error verifying OTP: {e}")
411
- return jsonify({'error': 'Lỗi hệ thống, vui lòng thử lại'}), 500
412
-
413
- # Get masked phone number
414
- @app.route('/get_masked_phone', methods=['POST'])
415
- def get_masked_phone():
416
- data = request.get_json(silent=True) or {}
417
- email = data.get('email', '').strip()
418
- if not email:
419
- return jsonify({'error': 'Thiếu email'}), 400
420
- user = db.users.find_one({'email': email})
421
- if not user:
422
- return jsonify({'error': 'Email không tồn tại'}), 404
423
- phone = user.get('phone', '')
424
- masked_phone = phone[:-4] + '****'
425
- return jsonify({'masked_phone': masked_phone}), 200
426
-
427
- # Quên mật khẩu
428
- @app.route('/forgot_password', methods=['GET', 'POST'])
429
- def forgot_password():
430
- if request.method == 'GET':
431
- return render_template('forgot_password.html')
432
- elif request.method == 'POST':
433
- data = request.get_json(silent=True) or {}
434
- email = data.get('email', '').strip()
435
- last_four_digits = data.get('last_four_digits', '').strip()
436
- if not email or not last_four_digits:
437
- return jsonify({'error': 'Thiếu email hoặc 4 số cuối của số điện thoại'}), 400
438
- user = db.users.find_one({'email': email})
439
- if not user:
440
- return jsonify({'error': 'Email không tồn tại'}), 404
441
- phone = user.get('phone', '')
442
- if not phone[-4:] == last_four_digits:
443
- return jsonify({'error': '4 số cuối của số điện thoại không khớp'}), 400
444
- new_password = ''.join(random.choices(string.ascii_letters + string.digits, k=12))
445
- new_password_hash = hash_password(new_password)
446
- db.users.update_one(
447
- {'_id': user['_id']},
448
- {'$set': {'password_hash': new_password_hash}}
449
- )
450
- if send_email(
451
- email,
452
- 'Mật khẩu mới',
453
- 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.'
454
- ):
455
- return jsonify({'message': 'Mật khẩu mới đã được gửi qua email'}), 200
456
- else:
457
- return jsonify({'error': 'Lỗi khi gửi mật khẩu mới'}), 500
458
-
459
- @app.route('/change_password', methods=['GET'])
460
- def change_password_get():
461
- if 'user_id' not in session:
462
- return redirect(url_for('login_page'))
463
- return render_template('change_password.html')
464
-
465
- # Đổi mật khẩu
466
- @app.route('/change_password', methods=['POST'])
467
- def change_password():
468
- if 'user_id' not in session:
469
- return jsonify({'error': 'Vui lòng đăng nhập để đổi mật khẩu'}), 401
470
-
471
- data = request.get_json(silent=True) or {}
472
- current_password = data.get('current_password', '').strip()
473
- new_password = data.get('new_password', '').strip()
474
-
475
- if not current_password or not new_password:
476
- return jsonify({'error': 'Thiếu mật khẩu hiện tại hoặc mật khẩu mới'}), 400
477
-
478
- user_id = session['user_id']
479
- user = db.users.find_one({'_id': ObjectId(user_id)})
480
- if not user:
481
- return jsonify({'error': 'Người dùng không tồn tại'}), 404
482
-
483
- if not verify_password(user['password_hash'], current_password):
484
- return jsonify({'error': 'Mật khẩu hiện tại không đúng'}), 401
485
-
486
- new_password_hash = hash_password(new_password)
487
- db.users.update_one(
488
- {'_id': ObjectId(user_id)},
489
- {'$set': {'password_hash': new_password_hash}}
490
- )
491
-
492
- # Send confirmation email
493
- if send_email(
494
- user['email'],
495
- 'Xác nhận đổi mật khẩu',
496
- 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")}.'
497
- ):
498
- return jsonify({'message': 'Đổi mật khẩu thành công, email xác nhận đã được gửi'}), 200
499
- else:
500
- 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
501
-
502
- # # Đăng nhập
503
- @app.route('/logins', methods=['POST'])
504
- def login():
505
- data = request.get_json(silent=True) or {}
506
- email = data.get('email', '').strip()
507
- password = data.get('password', '').strip()
508
- user = db.users.find_one({'email': email})
509
- if not user:
510
- logging.error(f"No user found for email: {email}")
511
- return jsonify({'error': 'Email hoặc mật khẩu không đúng'}), 401
512
- if not user.get('is_active', False):
513
- return jsonify({'error': 'Tài khoản chưa được kích hoạt. Vui lòng xác thực OTP.','id_user':str(user['_id'])}), 401
514
- if not verify_password(user['password_hash'], password):
515
- return jsonify({'error': 'Email hoặc mật khẩu không đúng'}), 401
516
- session['user_id'] = str(user['_id'])
517
- session['username'] = user['username']
518
- session['is_admin'] = user.get('is_admin', False)
519
- session['query_limit'] = user.get('query_limit', 3)
520
- session['query_count'] = user.get('query_count', 0)
521
- session['account_type'] = user.get('account_type', 'limited')
522
-
523
- # Broadcast initial query count and limit
524
- if str(user['_id']) in connected_clients:
525
- socketio.emit('query_update', {
526
- 'query_count': user.get('query_count', 0),
527
- 'query_limit': user.get('query_limit', 3 if user.get('account_type') == 'limited' else None)
528
- }, room=connected_clients[str(user['_id'])])
529
- logging.info(f"Broadcasted initial query update to user {user['_id']}")
530
-
531
-
532
- return jsonify({
533
- 'message': 'Đăng nhập thành công',
534
- 'username': user['username'],
535
- 'is_admin': user.get('is_admin', False),
536
- 'account_type': user.get('account_type', 'limited'),
537
- 'query_limit': user.get('query_limit', 3),
538
- 'query_count': user.get('query_count', 0),
539
- }), 200
540
-
541
-
542
- # @app.route('/logins', methods=['POST'])
543
- # def login():
544
- # data = request.get_json(silent=True) or {}
545
- # email = data.get('email', '').strip()
546
- # password = data.get('password', '').strip()
547
- # user = db.users.find_one({'email': email})
548
- # if not user:
549
- # logging.error(f"No user found for email: {email}")
550
- # return jsonify({'error': 'Email hoặc mật khẩu không đúng'}), 401
551
- # if not user.get('is_active', False):
552
- # return jsonify({'error': 'Tài khoản chưa được kích hoạt. Vui lòng xác thực OTP.'}), 401
553
- # if not verify_password(user['password_hash'], password):
554
- # return jsonify({'error': 'Email hoặc mật khẩu không đúng'}), 401
555
- # session['user_id'] = str(user['_id'])
556
- # session['username'] = user['username']
557
- # session['is_admin'] = user.get('is_admin', False)
558
- # session['query_limit'] = user.get('query_limit', 3)
559
- # session['query_count'] = user.get('query_count', 0)
560
- # session['account_type'] = user.get('account_type', 'limited')
561
- # return jsonify({
562
- # 'message': 'Đăng nhập thành công',
563
- # 'username': user['username'],
564
- # 'is_admin': user.get('is_admin', False),
565
- # 'account_type': user.get('account_type', 'limited'),
566
- # 'query_limit': user.get('query_limit', 3),
567
- # 'query_count': user.get('query_count', 0)
568
- # }), 200
569
-
570
- # Đăng xuất
571
- @app.route('/logout', methods=['POST'])
572
- def logout():
573
- user_id = session.get('user_id')
574
- if user_id in connected_clients:
575
- del connected_clients[user_id] # Remove from connected clients
576
- logging.info(f"User {user_id} removed from connected clients on logout")
577
- session.pop('user_id', None)
578
- session.pop('username', None)
579
- session.pop('is_admin', None)
580
- session.pop('account_type', None)
581
- return jsonify({'message': 'Đăng xuất thành công'}), 200
582
-
583
- # Kiểm tra session
584
- @app.route('/check_session', methods=['GET'])
585
- def check_session():
586
- if 'user_id' in session:
587
- user = db.users.find_one({'_id': ObjectId(session['user_id'])})
588
- if user:
589
- # Update session with latest values
590
- session['query_limit'] = user.get('query_limit', 3)
591
- session['query_count'] = user.get('query_count', 0)
592
- session['account_type'] = user.get('account_type', 'limited')
593
- # Broadcast current query count and limit
594
- if session['user_id'] in connected_clients:
595
- socketio.emit('query_update', {
596
- 'query_count': user.get('query_count', 0),
597
- 'query_limit': user.get('query_limit', 3 if user.get('account_type') == 'limited' else None)
598
- }, room=connected_clients[session['user_id']])
599
- logging.info(f"Broadcasted query update to user {session['user_id']} on session check")
600
- return jsonify({
601
- 'logged_in': True,
602
- 'username': session['username'],
603
- 'query_limit': session['query_limit'],
604
- 'query_count': session['query_count'],
605
- 'is_admin': session.get('is_admin', False),
606
- 'account_type': session['account_type']
607
- }), 200
608
- return jsonify({'logged_in': False}), 200
609
- # @app.route('/check_session', methods=['GET'])
610
- # def check_session():
611
- # if 'user_id' in session:
612
- # return jsonify({
613
- # 'logged_in': True,
614
- # 'username': session['username'],
615
- # 'query_limit': session.get('query_limit', 3),
616
- # 'query_count': session.get('query_count', 0),
617
- # 'is_admin': session.get('is_admin', False),
618
- # 'account_type': session.get('account_type', 'limited')
619
- # }), 200
620
- # return jsonify({'logged_in': False}), 200
621
-
622
-
623
- @socketio.on('connect')
624
- def handle_connect():
625
- user_id = session.get('user_id')
626
- if user_id:
627
- connected_clients[user_id] = request.sid
628
- user = db.users.find_one({'_id': ObjectId(user_id)})
629
- if user:
630
- socketio.emit('query_update', {
631
- 'query_count': user.get('query_count', 0),
632
- 'query_limit': user.get('query_limit', 3 if user.get('account_type') == 'limited' else None)
633
- }, room=request.sid)
634
- logging.info(f"Emitted initial query_update to user {user_id} on connect")
635
-
636
- # Lấy danh sách hội thoại
637
- @app.route('/conversations', methods=['GET'])
638
- def get_conversations():
639
- if 'user_id' not in session:
640
- return jsonify({'error': 'Vui lòng đăng nhập'}), 401
641
- user_id = session['user_id']
642
- conversations = db.conversations.find({'user_id': user_id}).sort('timestamp', -1)
643
- return jsonify([{
644
- 'id': str(conv['_id']),
645
- 'title': conv['title'],
646
- 'timestamp': conv['timestamp'].isoformat(),
647
- 'message_count': db.messages.count_documents({'conversation_id': str(conv['_id'])})
648
- } for conv in conversations])
649
-
650
- # Lấy chi tiết hội thoại
651
- @app.route('/conversation/<conversation_id>', methods=['GET'])
652
- def get_conversation(conversation_id):
653
- if 'user_id' not in session:
654
- return jsonify({'error': 'Vui lòng đăng nhập'}), 401
655
- user_id = session['user_id']
656
- conversation = db.conversations.find_one({'_id': ObjectId(conversation_id), 'user_id': user_id})
657
- if not conversation:
658
- return jsonify({'error': 'Hội thoại không tồn tại'}), 404
659
- messages = db.messages.find({'conversation_id': conversation_id})
660
- messages_list = [{
661
- 'id': str(msg['_id']),
662
- 'type': msg['type'],
663
- 'content': msg['content'],
664
- 'timestamp': msg['timestamp'].isoformat(),
665
- 'sources': msg.get('sources'),
666
- 'related_questions': msg.get('related_questions')
667
- } for msg in messages]
668
- return jsonify({
669
- 'id': str(conversation['_id']),
670
- 'title': conversation['title'],
671
- 'timestamp': conversation['timestamp'].isoformat(),
672
- 'messages': messages_list
673
- })
674
-
675
- # Xử lý truy vấn
676
- @app.route("/query", methods=["POST"])
677
- def query():
678
- if 'user_id' not in session:
679
- return jsonify({
680
- 'error': 'Vui lòng đăng nhập để sử dụng tính năng này',
681
- 'error_code': 'UNAUTHENTICATED'
682
- }), 401
683
-
684
- user_id = session['user_id']
685
-
686
- # Validate user_id format
687
- try:
688
- ObjectId(user_id)
689
- except Exception:
690
- return jsonify({
691
- 'error': 'ID người dùng không hợp lệ',
692
- 'error_code': 'INVALID_USER_ID'
693
- }), 400
694
-
695
- # Check query permission
696
- can_query, error_message, query_count, query_limit = can_make_query(user_id)
697
- if not can_query:
698
- return jsonify({
699
- 'error': error_message, # e.g., "Bạn đã sử dụng hết 10 lượt hỏi đáp hôm nay"
700
- 'error_code': 'QUERY_LIMIT_EXCEEDED',
701
- 'query_count': query_count,
702
- 'query_limit': query_limit,
703
- 'upgrade_url': 'https://legalmindver1.loca.lt' # Redirect to upgrade page
704
- }), 403
705
- elif error_message: # Warning for last query
706
- logging.info(f"User {user_id} received warning: {error_message}")
707
-
708
- # Parse JSON input
709
- data = request.get_json(silent=True) or {}
710
- question = data.get('question', '').strip()
711
- if not question:
712
- return jsonify({
713
- 'error': 'Câu hỏi không hợp lệ',
714
- 'error_code': 'INVALID_QUESTION'
715
- }), 400
716
-
717
- # Update query count for limited accounts
718
- user = db.users.find_one({'_id': ObjectId(user_id)})
719
- if not user:
720
- return jsonify({
721
- 'error': 'Người dùng không tồn tại',
722
- 'error_code': 'USER_NOT_FOUND'
723
- }), 404
724
-
725
- if user.get('account_type') == 'limited':
726
- try:
727
- db.users.update_one(
728
- {'_id': ObjectId(user_id)},
729
- {'$inc': {'query_count': 1}}
730
- )
731
- # Fetch updated user data
732
- user = db.users.find_one({'_id': ObjectId(user_id)})
733
- query_count = user.get('query_count', 0)
734
- query_limit = user.get('query_limit', 10)
735
- user_type = user.get('account_type', 'limited')
736
- # Broadcast updated query count to the user
737
- if user_id in connected_clients:
738
- socketio.emit('query_update', {
739
- 'query_count': query_count,
740
- 'query_limit': query_limit,
741
- 'user_type': user_type
742
- }, room=connected_clients[user_id])
743
- logging.info(f"Broadcasted query update to user {user_id}: {query_count}/{query_limit}")
744
- except Exception as e:
745
- logging.error(f"Error updating query count for user {user_id}: {e}")
746
- return jsonify({
747
- 'error': f"Bạn đã sử dụng hết {query_limit} lượt hỏi đáp hôm nay", # Use specific message
748
- 'error_code': 'DATABASE_ERROR',
749
- 'query_count': query_count,
750
- 'query_limit': query_limit,
751
- 'user_type': user_type,
752
- 'upgrade_url': 'https://legalmindver1.loca.lt/'
753
- }), 500
754
-
755
- # Handle conversation
756
- conversation_id = data.get('conversation_id')
757
- if conversation_id:
758
- try:
759
- conversation = db.conversations.find_one({'_id': ObjectId(conversation_id), 'user_id': user_id})
760
- if not conversation:
761
- return jsonify({
762
- 'error': 'Hội thoại không tồn tại hoặc không thuộc về người dùng',
763
- 'error_code': 'CONVERSATION_NOT_FOUND'
764
- }), 404
765
- except Exception:
766
- return jsonify({
767
- 'error': 'ID hội thoại không hợp lệ',
768
- 'error_code': 'INVALID_CONVERSATION_ID'
769
- }), 400
770
- else:
771
- conversation = {
772
- 'user_id': user_id,
773
- 'title': question[:50],
774
- 'timestamp': datetime.utcnow(),
775
- 'messages': []
776
- }
777
- try:
778
- result = db.conversations.insert_one(conversation)
779
- conversation_id = str(result.inserted_id)
780
- except Exception as e:
781
- logging.error(f"Error creating conversation for user {user_id}: {e}")
782
- return jsonify({
783
- 'error': f"Bạn đã sử dụng hết {query_limit} lượt hỏi đáp hôm nay", # Use specific message
784
- 'error_code': 'DATABASE_ERROR',
785
- 'query_count': query_count,
786
- 'query_limit': query_limit,
787
- 'upgrade_url': 'https://legalmindver1.loca.lt/'
788
- }), 500
789
-
790
- # Save user message
791
- user_message = {
792
- 'conversation_id': conversation_id,
793
- 'type': 'user',
794
- 'content': question,
795
- 'timestamp': datetime.utcnow()
796
- }
797
- try:
798
- db.messages.insert_one(user_message)
799
- except Exception as e:
800
- logging.error(f"Error saving user message for conversation {conversation_id}: {e}")
801
- return jsonify({
802
- 'error': f"Bạn đã sử dụng hết {query_limit} lượt hỏi đáp hôm nay", # Use specific message
803
- 'error_code': 'DATABASE_ERROR',
804
- 'query_count': query_count,
805
- 'query_limit': query_limit,
806
- 'upgrade_url': 'https://legalmindver1.loca.lt/'
807
- }), 500
808
-
809
- # Retrieve relevant legal documents
810
- try:
811
- banan_results = retrieve(question, index, embeddings_data, k=5)
812
- except Exception as e:
813
- logging.error(f"Error retrieving documents: {e}")
814
- banan_results = []
815
-
816
- # Format chat history
817
- chat_history_str = format_chat_history(memory)
818
-
819
- # Define main prompt
820
- main_prompt = f"""
821
- Dưới đây là lịch sử hội thoại trước đó:
822
- {chat_history_str}
823
-
824
- **Câu hỏi:**
825
- {question}
826
-
827
- **Thông tin tham khảo (bản án tương đồng):**
828
- {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."}
829
-
830
- **Hướng dẫn trả lời chi tiết:**
831
- 1. **Tổng quan về bản án, án lệ tương đồng:**
832
- - Trình bày rõ thông tin tham khảo nếu có.
833
- - Nhớ đề cập đến tên file bản án, không dùng từ ví dụ, giả sử, giả định.
834
- - 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.
835
-
836
- 2. **Nội dung chi tiết của bản án, án lệ:**
837
- - 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.
838
- - 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.
839
-
840
- 3. **Phân tích tình huống pháp lý:**
841
- - Phân tích rõ các vấn đề pháp lý chính.
842
- - 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.
843
-
844
- 4. **Lập luận pháp lý:**
845
- - 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.
846
- - 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.
847
-
848
- 5. **Kết luận và khuyến nghị:**
849
- - Kết luận rõ quyền và nghĩa vụ các bên theo quy định của pháp luật.
850
- - 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ế.
851
-
852
- 6. **Nguồn tham khảo:**
853
- - 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>.
854
- **Ví dụ:**
855
- <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)
856
- <a href="">[Nghị định 115/2015/NĐ-CP]</a> (Đặc biệt Điều 58)
857
- <a href="">[Bộ luật Hình sự năm 2015]</a> (Đặc biệt Điều 184)
858
-
859
- **Lưu ý quan trọng:**
860
- - 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>.
861
- - 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."
862
- - Trả lời ngắn gọn, súc tích, rõ ràng, đúng trọng tâm.
863
- - Tuyệt đối không dùng từ "giả sử", "ví dụ".
864
- - Không giới thiệu bản thân, không đề cập đến kinh nghiệm tư vấn.
865
- - Không cần mô tả quy trình phân tích.
866
- - 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.
867
- - 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.
868
- - 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ế.
869
- """
870
-
871
- # Call Gemini for main response
872
- try:
873
- handler = GeminiHandler(
874
- config_path="config.yaml",
875
- content_strategy=Strategy.ROUND_ROBIN,
876
- key_strategy=KeyRotationStrategy.SMART_COOLDOWN
877
- )
878
- gen = handler.generate_content(
879
- prompt=main_prompt,
880
- model_name="gemini-2.0-flash-thinking-exp-01-21",
881
- return_stats=False
882
- )
883
- answer = gen.get("text", "Không có phản hồi từ mô hình.")
884
- except Exception as e:
885
- logging.error(f"Error calling Gemini for main prompt: {e}")
886
- return jsonify({
887
- 'error': f"Bạn đã sử dụng hết {query_limit} lượt hỏi đáp hôm nay", # Use specific message
888
- 'error_code': 'GEMINI_ERROR',
889
- 'query_count': query_count,
890
- 'query_limit': query_limit,
891
- 'upgrade_url': 'https://legalmindver1.loca.lt/'
892
- }), 500
893
-
894
- # Define related questions prompt
895
- related_questions_prompt = f"""
896
- 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:
897
- - Liên quan chặt chẽ đến chủ đề pháp lý của câu hỏi gốc.
898
- - Phù hợp với hệ thống pháp luật Việt Nam hiện hành.
899
- - Ngắn gọn, rõ ràng, và mang tính ứng dụng thực tế.
900
- - 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ý.
901
- - Đượ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`.
902
-
903
- **Câu hỏi gốc:**
904
- {question}
905
-
906
- **Hướng dẫn thêm:**
907
- - 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 đó.
908
- - 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.
909
- - Không sử dụng từ "giả sử" hoặc "ví dụ".
910
- - Không lặp lại câu hỏi gốc.
911
- - Đả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.
912
-
913
- **Định dạng đầu ra (JSON):**
914
- [
915
- {{"question": "Câu hỏi 1"}},
916
- {{"question": "Câu hỏi 2"}},
917
- {{"question": "Câu hỏi 3"}},
918
- {{"question": "Câu hỏi 4"}},
919
- {{"question": "Câu hỏi 5"}}
920
- ]
921
- """
922
- try:
923
- handler = GeminiHandler(
924
- config_path="config.yaml",
925
- content_strategy=Strategy.ROUND_ROBIN,
926
- key_strategy=KeyRotationStrategy.SMART_COOLDOWN
927
- )
928
- gen = handler.generate_content(
929
- prompt=related_questions_prompt,
930
- model_name="gemini-2.0-flash-thinking-exp-01-21",
931
- return_stats=False
932
- )
933
- related_questions = gen.get("text", "Không có phản hồi từ mô hình.")
934
- except Exception as e:
935
- logging.error(f"Error calling Gemini for related questions: {e}")
936
- return jsonify({
937
- 'error': f"Bạn đã sử dụng hết {query_limit} lượt hỏi đáp hôm nay", # Use specific message
938
- 'error_code': 'GEMINI_ERROR',
939
- 'query_count': query_count,
940
- 'query_limit': query_limit,
941
- 'upgrade_url': 'https://legalmindver1.loca.lt/'
942
- }), 500
943
-
944
- related_questions = preprocess_related_questions(related_questions)
945
- assistant_message = {
946
- 'conversation_id': conversation_id,
947
- 'type': 'assistant',
948
- 'content': answer,
949
- 'timestamp': datetime.utcnow(),
950
- 'sources': banan_results,
951
- 'related_questions': related_questions
952
- }
953
- try:
954
- db.messages.insert_one(assistant_message)
955
- except Exception as e:
956
- logging.error(f"Error saving assistant message for conversation {conversation_id}: {e}")
957
- return jsonify({
958
- 'error': f"Bạn đã sử dụng hết {query_limit} lượt hỏi đáp hôm nay", # Use specific message
959
- 'error_code': 'DATABASE_ERROR',
960
- 'query_count': query_count,
961
- 'query_limit': query_limit,
962
- 'upgrade_url': 'https://legalmindver1.loca.lt/'
963
- }), 500
964
-
965
- try:
966
- db.conversations.update_one(
967
- {'_id': ObjectId(conversation_id)},
968
- {'$set': {'title': question[:50], 'timestamp': datetime.utcnow()}}
969
- )
970
- except Exception as e:
971
- logging.error(f"Error updating conversation {conversation_id}: {e}")
972
- return jsonify({
973
- 'error': f"Bạn đã sử dụng hết {query_limit} lượt hỏi đáp hôm nay", # Use specific message
974
- 'error_code': 'DATABASE_ERROR',
975
- 'query_count': query_count,
976
- 'query_limit': query_limit,
977
- 'upgrade_url': 'https://legalmindver1.loca.lt/'
978
- }), 500
979
-
980
- memory.save_context({'question': question}, {'answer': answer})
981
- return jsonify({
982
- 'final_response': answer,
983
- 'top_banan_documents': banan_results,
984
- 'chat_history': chat_history_str,
985
- 'related_questions': related_questions,
986
- 'conversation_id': conversation_id,
987
- 'query_count': query_count,
988
- 'query_limit': query_limit
989
- }), 200
990
-
991
- # Soạn thảo bản án
992
- @app.route("/draft_judgment", methods=["POST"])
993
- def draft_judgment():
994
- if 'user_id' not in session:
995
- return jsonify({'error': 'Vui lòng đăng nhập'}), 401
996
- data = request.get_json(silent=True) or {}
997
- case_details = data.get('case_details', '').strip()
998
- if not case_details:
999
- return jsonify({'error': 'Chi tiết vụ án không hợp lệ'}), 400
1000
- banan_results = retrieve(case_details, index, embeddings_data, k=2)
1001
- top_banan_docs = [{'source': r['file'], **r} for r in banan_results]
1002
- chat_history_str = format_chat_history(memory)
1003
- judgment = "Placeholder judgment: Drafted legal document based on case details."
1004
- memory.save_context({'case_details': case_details}, {'judgment': judgment})
1005
- return jsonify({
1006
- 'judgment': judgment,
1007
- 'top_banan_documents': top_banan_docs,
1008
- 'chat_history': chat_history_str
1009
- })
1010
-
1011
- # Xóa hội thoại
1012
- @app.route('/conversation/<conversation_id>', methods=['DELETE'])
1013
- def delete_conversation(conversation_id):
1014
- if 'user_id' not in session:
1015
- return jsonify({'error': 'Vui lòng đăng nhập'}), 401
1016
- user_id = session['user_id']
1017
- result = db.conversations.delete_one({'_id': ObjectId(conversation_id), 'user_id': user_id})
1018
- if result.deleted_count == 0:
1019
- return jsonify({'error': 'Hội thoại không tồn tại'}), 404
1020
- db.messages.delete_many({'conversation_id': conversation_id})
1021
- return jsonify({'message': 'Xóa hội thoại thành công'}), 200
1022
-
1023
- @app.route('/admin/dashboard')
1024
- @admin_required
1025
- def admin_dashboard():
1026
- users = db.users.find({'is_active': True})
1027
- user_name = session.get('username')
1028
- users_list = [{
1029
- 'id': str(user['_id']),
1030
- 'username': user['username'],
1031
- 'email': user['email'],
1032
- 'phone': user['phone'],
1033
- 'is_active': user.get('is_active', False),
1034
- 'is_admin': user.get('is_admin', False),
1035
- 'account_type': user.get('account_type', 'limited'),
1036
- 'query_limit': user.get('query_limit', None),
1037
- 'query_count': user.get('query_count', 0),
1038
- 'last_reset': user.get('last_reset', None).isoformat() if user.get('last_reset') else None
1039
- } for user in users]
1040
- return render_template('admin_dashboard.html', users=users_list, user_name=user_name)
1041
-
1042
-
1043
- @app.route('/admin/user/<user_id>', methods=['DELETE'])
1044
- @admin_required
1045
- def delete_user(user_id):
1046
- try:
1047
- user = db.users.find_one({'_id': ObjectId(user_id)})
1048
- if not user:
1049
- return jsonify({'error': 'Người dùng không tồn tại', 'error_code': 'USER_NOT_FOUND'}), 404
1050
- if user.get('is_admin', False):
1051
- return jsonify({'error': 'Không thể xóa tài khoản admin', 'error_code': 'ADMIN_PROTECTED'}), 403
1052
- db.users.delete_one({'_id': ObjectId(user_id)})
1053
- send_email(
1054
- user['email'],
1055
- 'Tài khoản của bạn đã bị xóa',
1056
- f'Tài khoản của bạn ({user["username"]}) đã bị admin xóa. Vui lòng liên hệ hỗ trợ nếu cần thêm thông tin.'
1057
- )
1058
- return jsonify({'message': 'Xóa tài khoản thành công'}), 200
1059
- except Exception as e:
1060
- logging.error(f"Error deleting user {user_id}: {e}")
1061
- return jsonify({'error': 'Lỗi khi xóa tài khoản', 'error_code': 'SERVER_ERROR'}), 500
1062
-
1063
- @app.route('/admin/users', methods=['GET'])
1064
- @admin_required
1065
- def get_all_users():
1066
- users = db.users.find()
1067
- return jsonify([{
1068
- 'id': str(user['_id']),
1069
- 'username': user['username'],
1070
- 'email': user['email'],
1071
- 'phone': user['phone'],
1072
- 'is_active': user.get('is_active', False),
1073
- 'is_admin': user.get('is_admin', False),
1074
- 'account_type': user.get('account_type', 'limited'),
1075
- 'query_limit': user.get('query_limit', None),
1076
- 'query_count': user.get('query_count', 0),
1077
- 'last_reset': user.get('last_reset', None).isoformat() if user.get('last_reset') else None
1078
- } for user in users]), 200
1079
-
1080
- @app.route('/admin/user/<user_id>', methods=['PUT'])
1081
- @admin_required
1082
- def update_user(user_id):
1083
- data = request.get_json(silent=True) or {}
1084
- updates = {}
1085
- if 'account_type' in data and data['account_type'] in ['limited', 'unlimited']:
1086
- updates['account_type'] = data['account_type']
1087
- updates['query_limit'] = 10 if data['account_type'] == 'limited' else None
1088
- updates['query_count'] = 0
1089
- updates['last_reset'] = datetime.utcnow()
1090
- if 'is_admin' in data and isinstance(data['is_admin'], bool):
1091
- updates['is_admin'] = data['is_admin']
1092
- if 'query_limit' in data and isinstance(data['query_limit'], int) and data.get('account_type') == 'limited':
1093
- updates['query_limit'] = data['query_limit']
1094
- if not updates:
1095
- return jsonify({'error': 'Không có thông tin cập nhật hợp lệ'}), 400
1096
- result = db.users.update_one(
1097
- {'_id': ObjectId(user_id)},
1098
- {'$set': updates}
1099
- )
1100
- if result.modified_count == 0:
1101
- return jsonify({'error': 'Không tìm thấy người dùng hoặc không có thay đổi'}), 404
1102
- logging.info(f"Admin updated user {user_id}: {updates}")
1103
- # Broadcast updated query count to the user
1104
- if user_id in connected_clients:
1105
- user = db.users.find_one({'_id': ObjectId(user_id)})
1106
- socketio.emit('query_update', {
1107
- 'query_count': user.get('query_count', 0),
1108
- 'query_limit': user.get('query_limit', 10)
1109
- }, room=connected_clients[user_id])
1110
- logging.info(f"Broadcasted query update to user {user_id} after admin update")
1111
- return jsonify({'message': 'Cập nhật người dùng thành công'}), 200
1112
-
1113
- @app.route('/admin/user/<user_id>/reset_query', methods=['POST'])
1114
- @admin_required
1115
- def reset_user_query_count(user_id):
1116
- user = db.users.find_one({'_id': ObjectId(user_id)})
1117
- if not user:
1118
- return jsonify({'error': 'Người dùng không tồn tại'}), 404
1119
- if user.get('account_type') == 'unlimited':
1120
- return jsonify({'error': 'Tài khoản không giới hạn không cần reset!'}), 400
1121
- db.users.update_one(
1122
- {'_id': ObjectId(user_id)},
1123
- {'$set': {'query_count': 0, 'last_reset': datetime.utcnow()}}
1124
- )
1125
- logging.info(f"Admin reset query count for user {user_id}")
1126
- # Broadcast updated query count to the user
1127
- if user_id in connected_clients:
1128
- socketio.emit('query_update', {
1129
- 'query_count': 0,
1130
- 'query_limit': user.get('query_limit', 10)
1131
- }, room=connected_clients[user_id])
1132
- logging.info(f"Broadcasted query update to user {user_id} after reset")
1133
- return jsonify({'message': 'Reset lượt hỏi đáp thành công'}), 200
1134
-
1135
- # Page routes
1136
- @app.route('/')
1137
- def page_index():
1138
- return render_template('index.html')
1139
-
1140
- @app.route('/home')
1141
- def page_home():
1142
- return render_template('home.html')
1143
-
1144
- @app.route('/login')
1145
- def login_page():
1146
- return render_template('login.html')
1147
-
1148
- if __name__ == '__main__':
1149
- socketio.run(app, host='0.0.0.0', port=int(os.getenv('PORT', 7860)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
config.yaml DELETED
@@ -1,36 +0,0 @@
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 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:57f4d29d1a9da5eecb05c3052c986d35f7a67609c9166fca5fe741e4fc729069
3
- size 110171889
 
 
 
 
embedding_data/faiss_index_23_06.index DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:adee95b3eef649e0832368c752b999e99efb4fefb68bbadac328cb960b2175d8
3
- size 13793325
 
 
 
 
gemini_handler.py DELETED
@@ -1,559 +0,0 @@
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 DELETED
@@ -1 +0,0 @@
1
- lt --port 5000 --subdomain legal
 
 
requirements.txt DELETED
@@ -1,20 +0,0 @@
1
- Flask==3.0.3
2
- flask-cors
3
- Flask-SocketIO==5.3.6
4
- pymongo==3.13.0
5
- python-dateutil==2.9.0.post0
6
- sentence-transformers==3.1.0
7
- torch==2.4.0
8
- numpy==1.26.4
9
- faiss-cpu==1.8.0
10
- langchain==0.2.15
11
- langchain-community==0.2.15
12
- PyYAML==6.0.1
13
- requests==2.32.3
14
- gunicorn==22.0.0
15
- gevent==24.2.1 # Thêm gevent
16
- google-generativeai
17
- boto3
18
- python-dotenv==1.0.1
19
- flask-session==0.5.0
20
- certifi
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/assets/01-dark.png DELETED
Binary file (41.6 kB)
 
static/assets/01-light.png DELETED

Git LFS Details

  • SHA256: b3f34001a3ec7e01d2fba733105d990636eca9436582918f49e605574194c6f1
  • Pointer size: 131 Bytes
  • Size of remote file: 113 kB
static/assets/01.jpg DELETED
Binary file (87.7 kB)
 
static/assets/02-dark.png DELETED
Binary file (49.7 kB)
 
static/assets/02-light.png DELETED
Binary file (47.8 kB)
 
static/assets/02.jpg DELETED
Binary file (43.7 kB)
 
static/assets/03-dark.png DELETED
Binary file (48 kB)
 
static/assets/03-light.png DELETED
Binary file (48.2 kB)
 
static/assets/48.jpg DELETED
Binary file (6.18 kB)
 
static/assets/49.jpg DELETED
Binary file (5.72 kB)
 
static/assets/50.jpg DELETED
Binary file (4.4 kB)
 
static/assets/awwwards.png DELETED
Binary file (1.81 kB)
 
static/assets/boxicons.min.css DELETED
@@ -1 +0,0 @@
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 DELETED
Binary file (2.49 kB)
 
static/assets/clutch.png DELETED
Binary file (1.89 kB)
 
static/assets/good-firms.png DELETED
Binary file (3.2 kB)
 
static/assets/jarallax.min.js DELETED
@@ -1,6 +0,0 @@
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 DELETED
Binary file (8.82 kB)
 
static/assets/landings.jpg DELETED
Binary file (61.9 kB)
 
static/assets/logo.svg DELETED
static/assets/node-dark.png DELETED
Binary file (5.01 kB)
 
static/assets/node-light.png DELETED
Binary file (5.25 kB)
 
static/assets/product-hunt.png DELETED
Binary file (2.2 kB)
 
static/assets/react.png DELETED
Binary file (9.54 kB)
 
static/assets/rellax.min.js DELETED
@@ -1,14 +0,0 @@
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 DELETED
@@ -1,13 +0,0 @@
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 DELETED
The diff for this file is too large to render. See raw diff
 
static/assets/theme-switcher.js DELETED
@@ -1,68 +0,0 @@
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 DELETED
The diff for this file is too large to render. See raw diff
 
static/assets/theme.min.js DELETED
@@ -1,23 +0,0 @@
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 DELETED
Binary file (5.43 kB)
 
static/assets/vue-light.png DELETED
Binary file (6.17 kB)
 
static/css/styles.css DELETED
@@ -1,39 +0,0 @@
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 DELETED
@@ -1,506 +0,0 @@
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 DELETED
@@ -1,1036 +0,0 @@
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/style_admin.css DELETED
@@ -1,616 +0,0 @@
1
- @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap');
2
-
3
- :root {
4
- /* Màu sắc lấy từ ảnh */
5
- --sidebar-bg: #1e293b;
6
- --sidebar-text: #adb5bd;
7
- --sidebar-active-bg: #2563eb;
8
- --sidebar-active-border: #4d80e4;
9
- --topbar-bg: #0f172a;
10
- --text-dark: #343a40;
11
- --body-bg-change: #3b4b61;
12
- --text-light: #ffffff;
13
- --body-bg: #f8f9fa;
14
- --card-bg: #ffffff;
15
- --border-color: #dee2e6;
16
- --shadow-light: rgba(0, 0, 0, 0.05);
17
- --table-header-bg: #f1f3f5;
18
- --table-row-hover: #fcfcfc;
19
- --blue-primary: #4d80e4;
20
- --green-unlimited: #28a745;
21
- --yellow-limited: #ffc107;
22
- --red-delete: #dc3545;
23
- --orange-reset: #fd7e14;
24
- --body-bgs: #334155;
25
- }
26
-
27
- * {
28
- margin: 0;
29
- padding: 0;
30
- box-sizing: border-box;
31
- /* Sửa lỗi font-family ở đây */
32
- font-family: 'ui-sans-serif', 'system-ui', 'sans-serif', "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
33
- }
34
-
35
- body {
36
- display: flex;
37
- min-height: 100vh;
38
- background-color: var(--body-bg);
39
- color: var(--text-dark);
40
- font-size: 14px;
41
- }
42
-
43
- /* Sidebar */
44
- .sidebar {
45
- width: 260px;
46
- background-color: var(--sidebar-bg);
47
- color: var(--sidebar-text);
48
- padding: 25px 0 20px;
49
- display: flex;
50
- flex-direction: column;
51
- box-shadow: 2px 0 8px var(--shadow-light);
52
- flex-shrink: 0;
53
- transition: transform 0.3s ease-in-out, width 0.3s ease-in-out;
54
- }
55
-
56
- .sidebar-header {
57
- padding: 0 25px 10px;
58
- font-size: 20px;
59
- font-weight: 700;
60
- color: var(--text-light);
61
- }
62
-
63
- .sidebar-nav ul {
64
- list-style: none;
65
- }
66
-
67
- .sidebar-nav li {
68
- margin-bottom: 2px;
69
- }
70
-
71
- .sidebar-nav a {
72
- display: flex;
73
- align-items: center;
74
- padding: 12px 25px;
75
- color: var(--sidebar-text);
76
- text-decoration: none;
77
- transition: background-color 0.2s ease, color 0.2s ease;
78
- font-size: 15px;
79
- font-weight: 500;
80
- }
81
-
82
- .sidebar-nav a i {
83
- margin-right: 12px;
84
- font-size: 18px;
85
- }
86
-
87
- .sidebar-nav a:hover {
88
- background-color: rgba(255, 255, 255, 0.05);
89
- color: var(--text-light);
90
- }
91
-
92
- .sidebar-nav a.active {
93
- background-color: var(--sidebar-active-bg);
94
- color: var(--text-light);
95
- border-left: 4px solid var(--sidebar-active-border);
96
- padding-left: 21px;
97
- }
98
-
99
- /* Main content */
100
- .main-content {
101
- flex-grow: 1;
102
- display: flex;
103
- flex-direction: column;
104
- transition: margin-left 0.3s ease-in-out;
105
- }
106
-
107
- /* Top Bar */
108
- .top-bar {
109
- background-color: var(--topbar-bg);
110
- padding: 15px 30px;
111
- display: flex;
112
- justify-content: flex-end;
113
- align-items: center;
114
- box-shadow: 0 1px 3px var(--shadow-light);
115
- border-bottom: 1px solid var(--border-color);
116
- }
117
-
118
- .top-bar .menu-toggle {
119
- display: none;
120
- background: none;
121
- border: none;
122
- font-size: 20px;
123
- color: var(--text-light);
124
- cursor: pointer;
125
- margin-right: auto;
126
- }
127
-
128
- .top-bar span {
129
- margin-right: 20px;
130
- font-weight: 500;
131
- color: var(--text-light);
132
- font-size: 14px;
133
- }
134
-
135
- .top-bar button {
136
- background-color: var(--body-bgs);
137
- color: var(--text-light);
138
- border: 1px solid var(--body-bgs);
139
- padding: 8px 12px;
140
- border-radius: 6px;
141
- cursor: pointer;
142
- font-size: 14px;
143
- display: flex;
144
- align-items: center;
145
- font-weight: 500;
146
- transition: background-color 0.2s ease;
147
- }
148
-
149
- .top-bar button i {
150
- margin-right: 8px;
151
- }
152
-
153
- .top-bar button:hover {
154
- background-color: var(--body-bg-change);
155
- }
156
-
157
- /* Content Area */
158
- .content-area {
159
- flex-grow: 1;
160
- padding: 30px;
161
- background-color: var(--body-bg);
162
- }
163
-
164
- .content-header {
165
- display: flex;
166
- justify-content: space-between;
167
- align-items: center;
168
- margin-bottom: 25px;
169
- flex-wrap: wrap;
170
- }
171
-
172
- .content-header h1 {
173
- font-size: 24px;
174
- color: var(--text-dark);
175
- font-weight: 600;
176
- margin-bottom: 0;
177
- }
178
-
179
- .add-new-btn {
180
- background-color: var(--blue-primary);
181
- color: var(--text-light);
182
- border: none;
183
- padding: 10px 18px;
184
- border-radius: 8px;
185
- cursor: pointer;
186
- font-size: 14px;
187
- display: flex;
188
- align-items: center;
189
- font-weight: 500;
190
- transition: background-color 0.2s ease;
191
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
192
- }
193
-
194
- .add-new-btn:hover {
195
- background-color: #3b6ed3;
196
- }
197
-
198
- .add-new-btn i {
199
- margin-right: 8px;
200
- font-size: 16px;
201
- }
202
-
203
- .search-bar {
204
- margin-bottom: 25px;
205
- position: relative;
206
- }
207
-
208
- .search-bar input {
209
- width: 100%;
210
- padding: 10px 15px 10px 40px;
211
- border: 1px solid #ced4da;
212
- border-radius: 8px;
213
- font-size: 15px;
214
- outline: none;
215
- transition: border-color 0.2s ease, box-shadow 0.2s ease;
216
- }
217
-
218
- .search-bar input::placeholder {
219
- color: #6c757d;
220
- }
221
-
222
- .search-bar input:focus {
223
- border-color: var(--blue-primary);
224
- box-shadow: 0 0 0 0.2rem rgba(77, 128, 228, 0.25);
225
- }
226
-
227
- .search-bar i {
228
- position: absolute;
229
- left: 15px;
230
- top: 50%;
231
- transform: translateY(-50%);
232
- color: #6c757d;
233
- font-size: 16px;
234
- }
235
-
236
- /* Responsive Table Wrapper */
237
- .user-table-responsive {
238
- width: 100%;
239
- overflow-x: auto;
240
- -webkit-overflow-scrolling: touch;
241
- }
242
-
243
- /* Table Container */
244
- .user-table {
245
- background-color: var(--card-bg);
246
- border-radius: 8px;
247
- box-shadow: 0 2px 5px var(--shadow-light);
248
- overflow: hidden;
249
- min-width: 0;
250
- }
251
-
252
- /* Table */
253
- .user-table table {
254
- width: 100%;
255
- border-collapse: collapse;
256
- table-layout: auto;
257
- }
258
-
259
- /* Table Cells */
260
- .user-table th,
261
- .user-table td {
262
- text-align: left;
263
- padding: 15px 20px;
264
- border-bottom: 1px solid var(--border-color);
265
- font-size: 14px;
266
- vertical-align: middle;
267
- white-space: nowrap;
268
- }
269
-
270
- /* Specific column widths */
271
- .user-table th:nth-child(1),
272
- .user-table td:nth-child(1) {
273
- /* ID */
274
- width: 10%;
275
- }
276
-
277
- .user-table th:nth-child(2),
278
- .user-table td:nth-child(2) {
279
- /* Username */
280
- width: 20%;
281
- }
282
-
283
- .user-table th:nth-child(3),
284
- .user-table td:nth-child(3) {
285
- /* Email */
286
- width: 25%;
287
- }
288
-
289
- .user-table th:nth-child(4),
290
- .user-table td:nth-child(4) {
291
- /* Phone */
292
- width: 15%;
293
- }
294
-
295
- .user-table th:nth-child(5),
296
- .user-table td:nth-child(5) {
297
- /* Account Type */
298
- width: 15%;
299
- }
300
-
301
- .user-table th:nth-child(6),
302
- .user-table td:nth-child(6) {
303
- /* Query Count */
304
- width: 15%;
305
- }
306
-
307
- .user-table th:nth-child(7),
308
- .user-table td:nth-child(7) {
309
- /* Actions */
310
- width: 20%;
311
- }
312
-
313
- .user-table th {
314
- background-color: var(--table-header-bg);
315
- font-weight: 600;
316
- color: var(--text-dark);
317
- text-transform: uppercase;
318
- font-size: 12px;
319
- letter-spacing: 0.5px;
320
- position: sticky;
321
- top: 0;
322
- z-index: 1;
323
- }
324
-
325
- .user-table tbody tr:last-child td {
326
- border-bottom: none;
327
- }
328
-
329
- .user-table tbody tr:hover {
330
- background-color: var(--table-row-hover);
331
- }
332
-
333
- .status-badge {
334
- display: inline-flex;
335
- align-items: center;
336
- justify-content: center;
337
- padding: 6px 10px;
338
- border-radius: 5px;
339
- font-size: 12px;
340
- font-weight: 600;
341
- line-height: 1;
342
- }
343
-
344
- .status-badge.limit {
345
- background-color: var(--yellow-limited);
346
- color: #555;
347
- }
348
-
349
- .status-badge.unlimited {
350
- background-color: var(--green-unlimited);
351
- color: var(--text-light);
352
- }
353
-
354
- .action-buttons {
355
- display: flex;
356
- gap: 8px;
357
- flex-wrap: nowrap;
358
- }
359
-
360
- .action-buttons button {
361
- background: none;
362
- border: 1px solid;
363
- cursor: pointer;
364
- font-size: 14px;
365
- padding: 10px 10px;
366
- border-radius: 5px;
367
- display: flex;
368
- align-items: center;
369
- transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
370
- font-weight: 500;
371
- flex-shrink: 0;
372
- }
373
-
374
- .action-buttons button i {
375
- font-size: 13px;
376
- }
377
-
378
- .action-buttons .edit-btn {
379
- color: var(--blue-primary);
380
- border-color: var(--blue-primary);
381
- border-radius: 100%;
382
-
383
- }
384
-
385
- .action-buttons .edit-btn:hover {
386
- background-color: var(--blue-primary);
387
- color: var(--text-light);
388
- border-radius: 100%;
389
- }
390
-
391
- .action-buttons .delete-btn {
392
- color: var(--red-delete);
393
- border-color: var(--red-delete);
394
- border-radius: 100%;
395
- }
396
-
397
- .action-buttons .delete-btn:hover {
398
- background-color: var(--red-delete);
399
- color: var(--text-light);
400
-
401
- }
402
-
403
- .action-buttons .reset-btn {
404
- color: var(--orange-reset);
405
- border-color: var(--orange-reset);
406
- border-radius: 100%;
407
- }
408
-
409
- .action-buttons .reset-btn:hover {
410
- background-color: var(--orange-reset);
411
- color: var(--text-light);
412
-
413
- }
414
-
415
- /* Responsive Styles */
416
- @media (max-width: 768px) {
417
- body {
418
- flex-direction: column;
419
- }
420
-
421
- .sidebar {
422
- position: fixed;
423
- top: 0;
424
- left: 0;
425
- height: 100%;
426
- z-index: 1000;
427
- transform: translateX(-100%);
428
- box-shadow: 3px 0 8px rgba(0, 0, 0, 0.2);
429
- width: 250px;
430
- }
431
-
432
- .sidebar.active {
433
- transform: translateX(0);
434
- }
435
-
436
- .main-content {
437
- margin-left: 0;
438
- }
439
-
440
- .top-bar {
441
- justify-content: space-between;
442
- padding: 15px 20px;
443
- }
444
-
445
- .top-bar .menu-toggle {
446
- display: block;
447
- }
448
-
449
- .top-bar span {
450
- margin-right: auto;
451
- margin-left: 10px;
452
- }
453
-
454
- .content-area {
455
- padding: 20px;
456
- }
457
-
458
- .content-header {
459
- flex-direction: column;
460
- align-items: flex-start;
461
- margin-bottom: 20px;
462
- }
463
-
464
- .content-header h1 {
465
- margin-bottom: 15px;
466
- font-size: 22px;
467
- }
468
-
469
- .add-new-btn {
470
- width: 100%;
471
- justify-content: center;
472
- padding: 12px 18px;
473
- }
474
-
475
- .search-bar input {
476
- font-size: 14px;
477
- }
478
-
479
- .user-table-responsive {
480
- overflow-x: auto;
481
- }
482
-
483
- .user-table table {
484
- display: block;
485
- width: 100%;
486
- min-width: unset;
487
- }
488
-
489
- .user-table thead {
490
- display: none;
491
- }
492
-
493
- .user-table tbody,
494
- .user-table tr {
495
- display: block;
496
- }
497
-
498
- .user-table tr {
499
- margin-bottom: 20px;
500
- border: 1px solid var(--border-color);
501
- border-radius: 8px;
502
- padding: 10px;
503
- background-color: var(--card-bg);
504
- }
505
-
506
- .user-table td {
507
- display: flex;
508
- justify-content: space-between;
509
- align-items: center;
510
- padding: 10px 15px;
511
- border-bottom: none;
512
- text-align: right;
513
- }
514
-
515
- .user-table td:before {
516
- content: attr(data-label);
517
- font-weight: 600;
518
- color: var(--text-dark);
519
- text-transform: uppercase;
520
- font-size: 12px;
521
- flex: 1;
522
- text-align: left;
523
- }
524
-
525
- .user-table td:nth-child(1),
526
- .user-table td:nth-child(4) {
527
- display: none;
528
- }
529
-
530
- .action-buttons {
531
- flex-direction: row;
532
- flex-wrap: wrap;
533
- justify-content: center;
534
- gap: 10px;
535
- }
536
-
537
- .action-buttons button {
538
- width: auto;
539
- min-width: 120px;
540
- }
541
- }
542
-
543
- @media (min-width: 769px) and (max-width: 1024px) {
544
- .sidebar {
545
- width: 220px;
546
- }
547
-
548
- .sidebar-header {
549
- font-size: 18px;
550
- }
551
-
552
- .sidebar-nav a {
553
- font-size: 14px;
554
- padding: 10px 20px;
555
- }
556
-
557
- .sidebar-nav a i {
558
- font-size: 16px;
559
- }
560
-
561
- .content-area {
562
- padding: 25px;
563
- }
564
-
565
- .content-header h1 {
566
- font-size: 22px;
567
- }
568
-
569
- .add-new-btn {
570
- padding: 9px 16px;
571
- font-size: 13px;
572
- }
573
-
574
- .user-table th,
575
- .user-table td {
576
- padding: 12px 18px;
577
- font-size: 13.5px;
578
- }
579
- }
580
-
581
- ol,
582
- ul {
583
- padding-left: 0rem !important;
584
-
585
- }
586
- /* Custom badge styling */
587
- .badge {
588
- display: inline-block;
589
- padding: 0.25em 0.4em;
590
- font-size: 75%;
591
- font-weight: 700;
592
- line-height: 1;
593
- text-align: center;
594
- white-space: nowrap;
595
- vertical-align: baseline;
596
- border-radius: 0.25rem;
597
- }
598
-
599
- /* Pill-shaped badge */
600
- .badge-pill {
601
- border-radius: 10rem;
602
- }
603
-
604
- /* Warning badge (for limited account) */
605
- .badge-warning {
606
- color: #c0a167;
607
- background-color: #fef9c3; /* Yellow/orange background */
608
- }
609
-
610
- /* Success badge (for other accounts) */
611
- .badge-success {
612
- color: #4e9067;
613
- background-color: #dcfce7; /* Green background */
614
- }
615
-
616
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/translations.js DELETED
@@ -1,134 +0,0 @@
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 DELETED
@@ -1,365 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="vi">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Quản lý người dùng</title>
7
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
8
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
9
- <link rel="stylesheet" href="{{ url_for('static', filename='style_admin.css') }}">
10
- </head>
11
- <body>
12
- <div class="sidebar" id="sidebar">
13
- <div class="sidebar-header">Quản lý hệ thống</div>
14
- <hr>
15
- <nav class="sidebar-nav">
16
- <ul>
17
- <li><a href="#" id="dashboardLink"><i class="fas fa-tachometer-alt"></i> Dashboard</a></li>
18
- <li><a href="#" id="usersLink" class="active"><i class="fas fa-users"></i> Người dùng</a></li>
19
- <li><a href="#" id="pendingUsersLink"><i class="fas fa-user-clock"></i> Người dùng chờ xác thực</a></li>
20
- <li><a href="#"><i class="fas fa-cog"></i> Cài đặt</a></li>
21
- <li><a href="#"><i class="fas fa-chart-bar"></i> Báo cáo</a></li>
22
- <li><a href="#"><i class="fas fa-user-circle"></i> Hồ sơ</a></li>
23
- <li><a href="#" onclick="logout()"><i class="fas fa-sign-out-alt"></i> Đăng xuất</a></li>
24
- </ul>
25
- </nav>
26
- </div>
27
-
28
- <div class="main-content" id="mainContent">
29
- <div class="top-bar">
30
- <button class="menu-toggle" id="menuToggle"><i class="fas fa-bars"></i></button>
31
- <span>Xin chào, {{ user_name | default('Admin') | e }}!</span>
32
- <button onclick="logout()">Đăng xuất</button>
33
- </div>
34
-
35
- <div class="content-area" id="contentArea">
36
- <!-- Users Tab -->
37
- <div id="usersTab">
38
- <div class="content-header">
39
- <h1>Danh sách người dùng</h1>
40
- <button class="add-new-btn"><i class="fas fa-plus"></i> Thêm mới</button>
41
- </div>
42
- <div class="search-bar">
43
- <i class="fas fa-search"></i>
44
- <input type="text" placeholder="Tìm kiếm người dùng...">
45
- </div>
46
- <div class="table-responsive">
47
- <table class="table table-striped table-bordered" id="userTable">
48
- <thead class="table-success">
49
- <tr>
50
- <th>ID</th>
51
- <th>Tên người dùng</th>
52
- <th>Vai trò</th>
53
- <th>Email</th>
54
- <th>Số điện thoại</th>
55
- <th>Loại tài khoản</th>
56
- <th>Lượt hỏi đáp</th>
57
- <th>Hành động</th>
58
- </tr>
59
- </thead>
60
- <tbody>
61
- {% for user in users %}
62
- <tr>
63
- <td data-label="ID">{{ user.id }}</td>
64
- <td data-label="Tên người dùng">{{ user.username | e }}</td>
65
- <td data-label="Vai trò">{% if user.is_admin %}Admin{% else %}User{% endif %}</td>
66
- <td data-label="Email">{{ user.email | e }}</td>
67
- <td data-label="Số điện thoại">{{ user.phone | e }}</td>
68
- <td data-label="Loại tài khoản">
69
- {% if user.account_type == 'limited' %}
70
- <span class="badge badge-pill badge-warning">Giới hạn</span>
71
- {% else %}
72
- <span class="badge badge-pill badge-success">Không giới hạn</span>
73
- {% endif %}
74
- </td>
75
- <td data-label="Lượt hỏi đáp">
76
- {% if user.account_type == 'unlimited' %}
77
- <span class="query-normal">Không giới hạn</span>
78
- {% elif user.query_limit %}
79
- <span class="query-normal">{{ user.query_count }}/{{ user.query_limit | e }}</span>
80
- {% else %}
81
- <span class="query-normal">{{ user.query_count }}/{{ user.account_type | e }}</span>
82
- {% endif %}
83
- </td>
84
- <td data-label="Hành động" class="action-buttons">
85
- <button class="edit-btn update-btn" onclick="openUpdateModal('{{ user.id | e }}', '{{ user.account_type | e }}', {{ 'true' if user.is_admin else 'false' }}, '{{ user.query_limit | default('') | e }}')">
86
- <i class="fas fa-pencil-alt"></i>
87
- </button>
88
- <button class="delete-btn" onclick="deleteUser('{{ user.id | e }}')">
89
- <i class="fas fa-trash-alt"></i>
90
- </button>
91
- <button class="reset-btn" onclick="resetQuery('{{ user.id | e }}')">
92
- <i class="fas fa-redo-alt"></i>
93
- </button>
94
- </td>
95
- </tr>
96
- {% endfor %}
97
- </tbody>
98
- </table>
99
- </div>
100
- </div>
101
-
102
- <!-- Pending Users Tab -->
103
- <div id="pendingUsersTab" style="display: none;">
104
- <div class="content-header">
105
- <h1>Danh sách người dùng chờ xác thực</h1>
106
- </div>
107
- <div class="table-responsive">
108
- <table class="table table-striped table-bordered" id="pendingUserTable">
109
- <thead class="table-success">
110
- <tr>
111
- <th>ID</th>
112
- <th>Tên người dùng</th>
113
- <th>Email</th>
114
- <th>Số điện thoại</th>
115
- <th>Loại tài khoản</th>
116
- <th>Ngày đăng ký</th>
117
- <th>Hành động</th>
118
- </tr>
119
- </thead>
120
- <tbody id="pendingUsersBody">
121
- <!-- Populated by JavaScript -->
122
- </tbody>
123
- </table>
124
- </div>
125
- </div>
126
- </div>
127
- </div>
128
-
129
- <!-- Update Modal (Existing) -->
130
- <div class="modal fade" id="updateModal" tabindex="-1" aria-labelledby="updateModalLabel" aria-hidden="true">
131
- <div class="modal-dialog">
132
- <div class="modal-content">
133
- <div class="modal-header">
134
- <h5 class="modal-title" id="updateModalLabel">Cập nhật người dùng</h5>
135
- <button type="button" class="btn-close" onclick="closeUpdateModal()" aria-label="Close"></button>
136
- </div>
137
- <div class="modal-body">
138
- <form id="updateForm">
139
- <input type="hidden" id="updateUserId">
140
- <div class="mb-3">
141
- <label for="updateAccountType" class="form-label">Loại tài khoản:</label>
142
- <select id="updateAccountType" class="form-select" onchange="toggleQueryLimit()">
143
- <option value="limited">Limited</option>
144
- <option value="unlimited">Unlimited</option>
145
- </select>
146
- </div>
147
- <div class="mb-3 form-check">
148
- <input type="checkbox" class="form-check-input" id="updateIsAdmin">
149
- <label class="form-check-label" for="updateIsAdmin">Admin</label>
150
- </div>
151
- <div class="mb-3">
152
- <label for="updateQueryLimit class="form-label">Giới hạn hỏi đáp (cho tài khoản limited):</label>
153
- <input type="number" class="form-control" id="updateQueryLimit" min="1" placeholder="Nhập giới hạn (ví dụ: 10)">
154
- </div>
155
- <div class="d-flex justify-content-end">
156
- <button type="submit" class="btn btn-success me-2">Lưu</button>
157
- <button type="button" class="btn btn-secondary cancel-btn" onclick="closeUpdateModal()">Hủy</button>
158
- </div>
159
- </form>
160
- </div>
161
- </div>
162
- </div>
163
- </div>
164
-
165
- <!-- jQuery and Bootstrap JS -->
166
- <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
167
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
168
- <script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
169
- <script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
170
- <script>
171
- // Sidebar toggle
172
- document.getElementById('menuToggle').addEventListener('click', () => {
173
- const sidebar = document.getElementById('sidebar');
174
- const mainContent = document.getElementById('mainContent');
175
- sidebar.classList.toggle('collapsed');
176
- mainContent.classList.toggle('shifted');
177
- });
178
-
179
- // Tab switching
180
- document.getElementById('usersLink').addEventListener('click', (e) => {
181
- e.preventDefault();
182
- document.getElementById('usersTab').style.display = 'block';
183
- document.getElementById('pendingUsersTab').style.display = 'none';
184
- document.getElementById('usersLink').classList.add('active');
185
- document.getElementById('pendingUsersLink').classList.remove('active');
186
- });
187
-
188
- document.getElementById('pendingUsersLink').addEventListener('click', (e) => {
189
- e.preventDefault();
190
- document.getElementById('usersTab').style.display = 'none';
191
- document.getElementById('pendingUsersTab').style.display = 'block';
192
- document.getElementById('usersLink').classList.remove('active');
193
- document.getElementById('pendingUsersLink').classList.add('active');
194
- loadPendingUsers();
195
- });
196
-
197
- // Initialize DataTables for users
198
- $(document).ready(function () {
199
- $('#userTable').DataTable({
200
- language: { url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/vi.json' },
201
- pageLength: 10,
202
- responsive: true,
203
- columnDefs: [{ orderable: false, targets: 6 }]
204
- });
205
-
206
- $('#pendingUserTable').DataTable({
207
- language: { url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/vi.json' },
208
- pageLength: 10,
209
- responsive: true,
210
- columnDefs: [{ orderable: false, targets: 6 }],
211
- searching: false
212
- });
213
- });
214
-
215
- // Load pending users
216
- async function loadPendingUsers() {
217
- try {
218
- const response = await fetch('/admin/pending_users', {
219
- method: 'GET',
220
- headers: { 'Content-Type': 'application/json' }
221
- });
222
- const users = await response.json();
223
- const tbody = document.getElementById('pendingUsersBody');
224
- tbody.innerHTML = '';
225
- users.forEach(user => {
226
- const row = `
227
- <tr>
228
- <td data-label="ID">${user.id}</td>
229
- <td data-label="Tên người dùng">${user.username}</td>
230
- <td data-label="Email">${user.email}</td>
231
- <td data-label="Số điện thoại">${user.phone}</td>
232
- <td data-label="Loại tài khoản">
233
- <span class="badge badge-pill ${user.account_type === 'limited' ? 'badge-warning' : 'badge-success'}">
234
- ${user.account_type === 'limited' ? 'Giới hạn' : 'Không giới hạn'}
235
- </span>
236
- </td>
237
- <td data-label="Ngày đăng ký">${user.created_at}</td>
238
- <td data-label="Hành động" class="action-buttons">
239
- <button class="btn btn-success btn-sm bg-success" onclick="verifyUser('${user.id}', 'approve')">
240
- <i class="fas fa-check"></i>
241
- </button>
242
- <button class="btn btn-danger btn-sm bg-danger" onclick="verifyUser('${user.id}', 'reject')">
243
- <i class="fas fa-times"></i>
244
- </button>
245
- </td>
246
- </tr>`;
247
- tbody.insertAdjacentHTML('beforeend', row);
248
- });
249
- } catch (error) {
250
- alert('Lỗi khi tải danh sách người dùng chờ xác thực: ' + error.message);
251
- }
252
- }
253
-
254
- // Verify user (approve/reject)
255
- async function verifyUser(userId, action) {
256
- if (!confirm(`Bạn có chắc muốn ${action === 'approve' ? 'chấp nhận' : 'từ chối'} người dùng này?`)) return;
257
- try {
258
- const response = await fetch(`/admin/user/${userId}/verify`, {
259
- method: 'POST',
260
- headers: { 'Content-Type': 'application/json' },
261
- body: JSON.stringify({ action })
262
- });
263
- const result = await response.json();
264
- alert(result.message || result.error);
265
- loadPendingUsers(); // Refresh pending users list
266
- } catch (error) {
267
- alert('Lỗi khi xử lý: ' + error.message);
268
- }
269
- }
270
-
271
- // Existing functions (deleteUser, resetQuery, openUpdateModal, etc.)
272
- async function deleteUser(userId) {
273
- if (confirm('Bạn có chắc muốn xóa người dùng này?')) {
274
- const response = await fetch(`/admin/user/${userId}`, {
275
- method: 'DELETE',
276
- headers: { 'Content-Type': 'application/json' }
277
- });
278
- const result = await response.json();
279
- alert(result.message || result.error);
280
- location.reload();
281
- }
282
- }
283
-
284
- async function resetQuery(userId) {
285
- const response = await fetch(`/admin/user/${userId}/reset_query`, {
286
- method: 'POST',
287
- headers: { 'Content-Type': 'application/json' }
288
- });
289
- const result = await response.json();
290
- alert(result.message || result.error);
291
- location.reload();
292
- }
293
-
294
- function openUpdateModal(userId, accountType, isAdmin, queryLimit) {
295
- document.getElementById('updateUserId').value = userId;
296
- document.getElementById('updateAccountType').value = accountType;
297
- document.getElementById('updateIsAdmin').checked = isAdmin;
298
- document.getElementById('updateQueryLimit').value = queryLimit || '';
299
- toggleQueryLimit();
300
- const modal = new bootstrap.Modal(document.getElementById('updateModal'));
301
- modal.show();
302
- }
303
-
304
- function closeUpdateModal() {
305
- const modal = bootstrap.Modal.getInstance(document.getElementById('updateModal'));
306
- modal.hide();
307
- }
308
-
309
- function toggleQueryLimit() {
310
- const accountType = document.getElementById('updateAccountType').value;
311
- const queryLimitInput = document.getElementById('updateQueryLimit');
312
- queryLimitInput.disabled = accountType === 'unlimited';
313
- if (accountType === 'unlimited') {
314
- queryLimitInput.value = '';
315
- }
316
- }
317
-
318
- async function logout() {
319
- try {
320
- const response = await fetch('/logout', {
321
- method: 'POST',
322
- headers: { 'Content-Type': 'application/json' }
323
- });
324
- if (response.ok) {
325
- alert('Đăng xuất thành công');
326
- window.location.href = '/';
327
- } else {
328
- alert('Lỗi khi đăng xuất');
329
- }
330
- } catch (error) {
331
- alert('Lỗi khi đăng xuất: ' + error.message);
332
- }
333
- }
334
-
335
- document.getElementById('updateForm').addEventListener('submit', async (e) => {
336
- e.preventDefault();
337
- const userId = document.getElementById('updateUserId').value;
338
- const accountType = document.getElementById('updateAccountType').value;
339
- const isAdmin = document.getElementById('updateIsAdmin').checked;
340
- const queryLimit = document.getElementById('updateQueryLimit').value;
341
- if (accountType === 'limited' && queryLimit && !/^\d+$/.test(queryLimit)) {
342
- alert('Giới hạn hỏi đáp phải là một số nguyên dương.');
343
- return;
344
- }
345
- const updates = { account_type: accountType, is_admin: isAdmin };
346
- if (accountType === 'limited' && queryLimit) {
347
- updates.query_limit = parseInt(queryLimit);
348
- }
349
- try {
350
- const response = await fetch(`/admin/user/${userId}`, {
351
- method: 'PUT',
352
- headers: { 'Content-Type': 'application/json' },
353
- body: JSON.stringify(updates)
354
- });
355
- const result = await response.json();
356
- alert(result.message || result.error);
357
- closeUpdateModal();
358
- location.reload();
359
- } catch (error) {
360
- alert('Lỗi khi cập nhật người dùng: ' + error.message);
361
- }
362
- });
363
- </script>
364
- </body>
365
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/change_password.html DELETED
@@ -1,443 +0,0 @@
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/bootstrap@5.3.0/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/bootstrap@5.3.0/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>