Spaces:
Sleeping
Sleeping
demo
#1
by
Nguyendat92929
- opened
This view is limited to 50 files because it contains too many changes.
See the raw diff here.
- .env +0 -7
- .gitattributes +35 -3
- .gitignore +0 -2
- Dockerfile +0 -46
- app.py +0 -1149
- config.yaml +0 -36
- embedding_data/embeddings.pkl +0 -3
- embedding_data/faiss_index_23_06.index +0 -3
- gemini_handler.py +0 -559
- readme.md +0 -1
- requirements.txt +0 -20
- static/assets/01-dark.png +0 -0
- static/assets/01-light.png +0 -3
- static/assets/01.jpg +0 -0
- static/assets/02-dark.png +0 -0
- static/assets/02-light.png +0 -0
- static/assets/02.jpg +0 -0
- static/assets/03-dark.png +0 -0
- static/assets/03-light.png +0 -0
- static/assets/48.jpg +0 -0
- static/assets/49.jpg +0 -0
- static/assets/50.jpg +0 -0
- static/assets/awwwards.png +0 -0
- static/assets/boxicons.min.css +0 -1
- static/assets/clutch-rating.png +0 -0
- static/assets/clutch.png +0 -0
- static/assets/good-firms.png +0 -0
- static/assets/jarallax.min.js +0 -6
- static/assets/java.png +0 -0
- static/assets/landings.jpg +0 -0
- static/assets/logo.svg +0 -75
- static/assets/node-dark.png +0 -0
- static/assets/node-light.png +0 -0
- static/assets/product-hunt.png +0 -0
- static/assets/react.png +0 -0
- static/assets/rellax.min.js +0 -14
- static/assets/swiper-bundle.min.css +0 -13
- static/assets/swiper-bundle.min.js +0 -0
- static/assets/theme-switcher.js +0 -68
- static/assets/theme.min.css +0 -0
- static/assets/theme.min.js +0 -23
- static/assets/vue-dark.png +0 -0
- static/assets/vue-light.png +0 -0
- static/css/styles.css +0 -39
- static/script.js +0 -506
- static/style.css +0 -1036
- static/style_admin.css +0 -616
- static/translations.js +0 -134
- templates/admin_dashboard.html +0 -365
- 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 |
-
|
2 |
-
|
3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
.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
|
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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|