Spaces:
Running
Running
Miguel Diaz
commited on
Commit
•
9c5c050
1
Parent(s):
ae5a484
Update assistant + google oauth + google search
Browse files- .gitattributes +34 -34
- .gitignore +2 -0
- Dockerfile +21 -21
- main.py +177 -171
- modules/chat_functions/__init__.py +22 -0
- modules/chat_functions/buscar_google.py +72 -0
- modules/chat_functions/leer_pagina_web.py +37 -0
- modules/chat_functions/obtener_clima.py +49 -0
- modules/error_map.py +41 -0
- modules/llm.py +82 -0
- modules/log_module.py +25 -0
- modules/model.py +375 -0
- modules/oauth.py +29 -0
- modules/personality_cores.py +18 -0
- modules/security.py +96 -0
- modules/settings.py +42 -0
- requirements.txt +17 -7
- static/css/app.css +5 -1
- static/css/menu.css +177 -0
- static/css/tabs.css +8 -14
- static/favicon.png +0 -0
- static/img/icon-t.png +0 -0
- static/img/icon-w.png +0 -0
- static/img/menu.svg +13 -13
- static/js/SecurityHandler.js +59 -0
- static/js/chatHandler.js +302 -105
- static/js/fingerprintv4.js +12 -0
- static/js/windowHandler.js +157 -130
- templates/PrivacyPolicy.html +203 -0
- templates/login.html +219 -0
- templates/main.html +197 -0
- templates/no_access.html +66 -0
.gitattributes
CHANGED
@@ -1,34 +1,34 @@
|
|
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 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
29 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
30 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
31 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
32 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
33 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
34 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
28 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
29 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
30 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
31 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
32 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
33 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
34 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
.gitignore
CHANGED
@@ -2,3 +2,5 @@
|
|
2 |
.vscode/
|
3 |
__pycache__/
|
4 |
keys/
|
|
|
|
|
|
2 |
.vscode/
|
3 |
__pycache__/
|
4 |
keys/
|
5 |
+
.env
|
6 |
+
logs/eventos.log
|
Dockerfile
CHANGED
@@ -1,21 +1,21 @@
|
|
1 |
-
FROM python:3.
|
2 |
-
|
3 |
-
WORKDIR /code
|
4 |
-
COPY ./requirements.txt /code/requirements.txt
|
5 |
-
|
6 |
-
|
7 |
-
RUN useradd -m -u 1000 user
|
8 |
-
|
9 |
-
USER user
|
10 |
-
|
11 |
-
ENV HOME=/home/user \
|
12 |
-
PATH=/home/user/.local/bin:$PATH
|
13 |
-
|
14 |
-
WORKDIR $HOME/app
|
15 |
-
|
16 |
-
|
17 |
-
COPY --chown=user . $HOME/app
|
18 |
-
RUN pip install --user --no-cache-dir --upgrade -r /code/requirements.txt
|
19 |
-
#CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
20 |
-
CMD ["hypercorn", "main:app", "--workers", "6", "--bind", "0.0.0.0:7860"]
|
21 |
-
|
|
|
1 |
+
FROM python:3.11
|
2 |
+
|
3 |
+
WORKDIR /code
|
4 |
+
COPY ./requirements.txt /code/requirements.txt
|
5 |
+
|
6 |
+
|
7 |
+
RUN useradd -m -u 1000 user
|
8 |
+
|
9 |
+
USER user
|
10 |
+
|
11 |
+
ENV HOME=/home/user \
|
12 |
+
PATH=/home/user/.local/bin:$PATH
|
13 |
+
|
14 |
+
WORKDIR $HOME/app
|
15 |
+
|
16 |
+
|
17 |
+
COPY --chown=user . $HOME/app
|
18 |
+
RUN pip install --user --no-cache-dir --upgrade -r /code/requirements.txt
|
19 |
+
#CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
20 |
+
CMD ["hypercorn", "main:app", "--workers", "6", "--bind", "0.0.0.0:7860"]
|
21 |
+
|
main.py
CHANGED
@@ -1,186 +1,192 @@
|
|
1 |
-
from fastapi import FastAPI, Request,
|
2 |
-
from fastapi.responses import HTMLResponse,
|
3 |
-
|
4 |
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
5 |
-
import
|
6 |
-
|
7 |
-
import time
|
8 |
-
import jwt
|
9 |
-
import openai
|
10 |
from hashlib import sha256
|
11 |
-
from
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
-
|
|
|
|
|
14 |
|
15 |
-
app = FastAPI()
|
16 |
-
security = HTTPBasic()
|
17 |
-
model = "gpt-3.5-turbo-16k"
|
18 |
-
app.mount("/static", staticfiles.StaticFiles(directory="static"), name="static")
|
19 |
-
users = json.loads(str(os.getenv("USER_KEYS")).replace("\n", ""))
|
20 |
-
for key in users:
|
21 |
-
if key == "master": continue
|
22 |
-
password = key+users[key]+users["master"]
|
23 |
-
users[key] = sha256(password.encode('UTF-8')).hexdigest()
|
24 |
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
f.write(f"[{level}][{fecha}] - {line}\n")
|
29 |
|
30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
|
32 |
-
|
|
|
33 |
|
34 |
-
|
35 |
-
|
36 |
-
JWT_EXPIRATION_TIME_MINUTES = 30
|
37 |
-
|
38 |
-
def create_jwt_token(data):
|
39 |
-
to_encode = {"data": data}
|
40 |
-
expire = datetime.utcnow() + timedelta(minutes=JWT_EXPIRATION_TIME_MINUTES)
|
41 |
-
to_encode.update({"exp": expire})
|
42 |
-
encoded_jwt = jwt.encode(to_encode, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
43 |
-
return encoded_jwt
|
44 |
-
|
45 |
-
async def validate_token(request: Request):
|
46 |
-
data = {}
|
47 |
-
try:
|
48 |
-
data = await request.json()
|
49 |
-
token = data.pop("token")
|
50 |
-
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
51 |
-
data["token_data"] = payload["data"]
|
52 |
-
except Exception as e:
|
53 |
-
raise HTTPException(status_code=404, detail="Datos no válidos")
|
54 |
-
return data
|
55 |
-
|
56 |
-
def authenticate_user(credentials: HTTPBasicCredentials):
|
57 |
-
|
58 |
-
password = credentials.username+credentials.password+users["master"]
|
59 |
-
password = sha256(password.encode('UTF-8')).hexdigest()
|
60 |
-
|
61 |
-
if credentials.username not in users or password != users[credentials.username]:
|
62 |
-
raise HTTPException(
|
63 |
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
64 |
-
detail="Incorrect username or password",
|
65 |
-
headers={"WWW-Authenticate": "Basic"},
|
66 |
-
)
|
67 |
|
68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
config
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
return JSONResponse(response)
|
120 |
-
|
121 |
-
@app.post("/chat_async")
|
122 |
-
async def chat_async(data = Depends(validate_token)):
|
123 |
-
messages = data.get("messages", "")
|
124 |
-
if not messages:
|
125 |
-
print("Empty message")
|
126 |
-
error = "What ??"
|
127 |
-
write_line("Empty message")
|
128 |
raise HTTPException(
|
129 |
status_code=status.HTTP_418_IM_A_TEAPOT,
|
130 |
-
detail=
|
131 |
)
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
st_times += 1
|
174 |
-
st = time.time()
|
175 |
-
|
176 |
-
|
177 |
-
yield json.dumps({"comando": "mensaje", "mensaje": mensaje})
|
178 |
-
return StreamingResponse(__streamer(), media_type="application/json")
|
179 |
-
|
180 |
-
|
181 |
-
@app.get("/read_log", response_class=HTMLResponse)
|
182 |
-
async def read_log(request: Request, credentials: HTTPBasicCredentials = Depends(security)):
|
183 |
-
if authenticate_user(credentials):
|
184 |
-
with open("log.txt", "r") as f:
|
185 |
-
return HTMLResponse(f.read())
|
186 |
|
|
|
1 |
+
from fastapi import FastAPI, Request, Depends, HTTPException, status, Response
|
2 |
+
from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse
|
|
|
3 |
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
4 |
+
from base64 import b64decode
|
5 |
+
import time, json
|
|
|
|
|
|
|
6 |
from hashlib import sha256
|
7 |
+
from modules import model, oauth, security, log_module, llm, chat_functions, settings
|
8 |
+
from modules.model import User, Session
|
9 |
+
from fastapi.staticfiles import StaticFiles
|
10 |
+
from fastapi.templating import Jinja2Templates
|
11 |
+
from datetime import datetime
|
12 |
+
from typing import Annotated
|
13 |
+
|
14 |
+
####################### APP SETUP ########################
|
15 |
+
app = FastAPI(docs_url=None, redoc_url=None)
|
16 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
17 |
+
templates = Jinja2Templates(directory="templates")
|
18 |
|
19 |
+
fecha_unix = str(int(time.time()))
|
20 |
+
log_module.logger("system").info("iniciado")
|
21 |
+
##########################################################
|
22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
|
24 |
+
######################## ROUTE: / ########################
|
25 |
+
@app.get("/", response_class=HTMLResponse)
|
26 |
+
async def main_page(request: Request):
|
|
|
27 |
|
28 |
+
tools = [{"name": k, "desc": v} for k, v in chat_functions.function_user_text.items()]
|
29 |
+
asistants = model.get_all_personality_cores()
|
30 |
+
|
31 |
+
response = templates.TemplateResponse(
|
32 |
+
"main.html", {
|
33 |
+
"request": request,
|
34 |
+
"version": fecha_unix,
|
35 |
+
"tools": tools,
|
36 |
+
"asistants": asistants
|
37 |
+
})
|
38 |
+
return response
|
39 |
+
##########################################################
|
40 |
+
|
41 |
+
|
42 |
+
|
43 |
+
#################### SECURITY (OAUTH) ####################
|
44 |
+
@app.get("/login", response_class=HTMLResponse)
|
45 |
+
async def login(request: Request):
|
46 |
+
# Shows the Start "session with google" button, and deletes token cookie
|
47 |
+
ret = templates.TemplateResponse("login.html", {"request": request, "redirecturi": settings.OAUTH_REDIRECT})
|
48 |
+
ret.delete_cookie("token")
|
49 |
+
return ret
|
50 |
+
|
51 |
+
@app.get("/oauth")
|
52 |
+
async def validate_oauth(request: Request, response: Response):
|
53 |
+
# Get the oauth get params,
|
54 |
+
# look for the google validation and info,
|
55 |
+
# set the session cookie and save user in DB
|
56 |
+
|
57 |
+
# Extract the Get params
|
58 |
+
params = dict(request.query_params)
|
59 |
+
|
60 |
+
# Client browser fingerprint bypassed with google oauth as "state"
|
61 |
+
data = params["state"].split("=",1)[1]
|
62 |
+
data = b64decode(data)
|
63 |
+
data = json.loads(data)
|
64 |
+
|
65 |
+
|
66 |
|
67 |
+
# Get the google user info
|
68 |
+
google_userinfo = oauth.validate_redirect(params)
|
69 |
|
70 |
+
# Create the user model with the google info
|
71 |
+
user:User = model.User.find_or_create(google_userinfo, data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
|
73 |
+
# Prepare redirect response and set the session cookie
|
74 |
+
|
75 |
+
token = user.create_cookie()
|
76 |
+
if not user.can_use("chat"):
|
77 |
+
response = RedirectResponse(url='/hold')
|
78 |
+
security.set_cookie(response, key="token", value=token)
|
79 |
+
return response
|
80 |
+
|
81 |
+
# Saves the user
|
82 |
+
user.update_user()
|
83 |
+
security.set_cookie(response, key="token", value=token)
|
84 |
+
|
85 |
+
return {"success": True}
|
86 |
+
|
87 |
+
##########################################################
|
88 |
+
|
89 |
+
|
90 |
+
###################### APIs Configs ######################
|
91 |
|
92 |
+
# Get data and structures from its models
|
93 |
+
User_find_from_data = Annotated[User, Depends(User.find_from_data)]
|
94 |
+
Session_find_from_data = Annotated[Session, Depends(Session.find_from_data)]
|
95 |
+
|
96 |
+
@app.post("/getConfigs")
|
97 |
+
async def get_configs(response: Response, user: User_find_from_data):
|
98 |
|
99 |
+
if not user.can_use("chat"):
|
100 |
+
response = RedirectResponse(url='/hold')
|
101 |
+
return response
|
102 |
+
|
103 |
+
# Get llm tokens used
|
104 |
+
year_, month_ = datetime.now().strftime("%y-%m").split("-")
|
105 |
+
month = sum(user.tokens.get(year_, {}).get(month_, {"_":0}).values())
|
106 |
+
total = sum([z for x in user.tokens.values() for y in x.values() for z in y.values()])
|
107 |
+
tokens = {"month": month, "total": total}
|
108 |
+
|
109 |
+
# Create cookies and answer
|
110 |
+
user._session.create_cookie(response)
|
111 |
+
return user.configs.model_dump() | {"tokens": tokens, "challenge": user._session.challenge}
|
112 |
+
|
113 |
+
@app.post("/setConfigs")
|
114 |
+
async def set_configs(response: Response, user: User_find_from_data):
|
115 |
+
|
116 |
+
if not user.can_use("chat"):
|
117 |
+
response = RedirectResponse(url='/hold')
|
118 |
+
return response
|
119 |
+
|
120 |
+
# Create config model
|
121 |
+
user.configs = model.Configs(**user._data)
|
122 |
+
user._session.configs = user.configs
|
123 |
+
assisntat_prompt = user.update_user()
|
124 |
+
|
125 |
+
# Create cookies and answer
|
126 |
+
user._session.create_cookie(response)
|
127 |
+
return {"success":True, "challenge": user._session.challenge, "assistantPrompt": assisntat_prompt}
|
128 |
+
|
129 |
+
@app.post("/getToken")
|
130 |
+
async def get_token(response: Response, user: User_find_from_data):
|
131 |
+
|
132 |
+
# Generate api token
|
133 |
+
user.create_cookie()
|
134 |
+
if not user.can_use("chat"):
|
135 |
+
return {"success":False, "redirect": "/hold"}
|
136 |
+
|
137 |
+
# Create cookies and answer
|
138 |
+
return {"success":True, "challenge": user._session.challenge}
|
139 |
+
|
140 |
+
##########################################################
|
141 |
+
|
142 |
+
@app.post("/chat")
|
143 |
+
async def chat_async(session: Session_find_from_data):
|
144 |
+
chat = model.Chat(messages = session.data["messages"], personality=session.configs.assistant)
|
145 |
+
if(len(chat.messages) < 1 or chat.messages[-1].content==""):
|
146 |
+
log_module.logger(session.gid).warning("Empty message")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
147 |
raise HTTPException(
|
148 |
status_code=status.HTTP_418_IM_A_TEAPOT,
|
149 |
+
detail= "Nope"
|
150 |
)
|
151 |
+
return StreamingResponse(llm.streamer(chat, session), media_type="application/json")
|
152 |
+
|
153 |
+
|
154 |
+
########################## Static Pages ##########################
|
155 |
+
@app.get("/privacy", response_class=HTMLResponse)
|
156 |
+
async def privacy_policy(request: Request):
|
157 |
+
return templates.TemplateResponse("PrivacyPolicy.html", {"request": request})
|
158 |
+
|
159 |
+
@app.get("/hold")
|
160 |
+
async def on_hold(request: Request, user: User = Depends(User.find_from_cookie)):
|
161 |
+
if user.can_use("chat"):
|
162 |
+
return RedirectResponse(url='/')
|
163 |
+
return templates.TemplateResponse(
|
164 |
+
"no_access.html", {
|
165 |
+
"request": request,
|
166 |
+
"description": bool(user.description)
|
167 |
+
})
|
168 |
+
|
169 |
+
@app.post("/hold")
|
170 |
+
async def on_hold(request: Request, user: User = Depends(User.find_from_cookie)):
|
171 |
+
if not user.description:
|
172 |
+
form = await request.form()
|
173 |
+
if message := form["message"].strip():
|
174 |
+
user.update_description(message)
|
175 |
+
|
176 |
+
return templates.TemplateResponse(
|
177 |
+
"no_access.html", {
|
178 |
+
"request": request,
|
179 |
+
"description": bool(user.description)
|
180 |
+
})
|
181 |
+
|
182 |
+
|
183 |
+
########################## Other ##########################
|
184 |
+
# @app.get("/read_log", response_class=HTMLResponse)
|
185 |
+
# async def read_log(request: Request, credentials: HTTPBasicCredentials = Depends(httpsecurity)):
|
186 |
+
# if sha256(credentials.username.encode()).hexdigest()=="bc1c32d709aef061bbde4fc848421cdb933e8a9f391c3a089f2861ac0772c168" and security.authenticate_user(credentials):
|
187 |
+
# log_module.log_write(credentials.username, "Log Accesado", "")
|
188 |
+
# with open("logs/eventos.log", "r") as f:
|
189 |
+
# return HTMLResponse(f.read())
|
190 |
+
# log_module.log_write(credentials.username, "Intento acceder logs", f"{request.client.host}:{request.client.port} - {str(dict(request.headers))}")
|
191 |
+
# raise HTTPException(status_code=404)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
192 |
|
modules/chat_functions/__init__.py
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
functions = []
|
2 |
+
function_callbacks = {}
|
3 |
+
function_user_text = {}
|
4 |
+
import importlib
|
5 |
+
import glob
|
6 |
+
|
7 |
+
|
8 |
+
# Obtiene la lista de archivos .py en la carpeta actual
|
9 |
+
archivos = glob.glob("*.py", root_dir=__path__[0])
|
10 |
+
|
11 |
+
# Recorre cada archivo y lee la variable "info" dentro de ellos
|
12 |
+
for archivo in archivos:
|
13 |
+
if "__" in archivo[:-3]:
|
14 |
+
continue
|
15 |
+
modulo = importlib.import_module('.'+archivo[:-3], "modules.chat_functions")
|
16 |
+
if not modulo.activo:
|
17 |
+
continue
|
18 |
+
# Lee la variable "info" del módulo
|
19 |
+
|
20 |
+
functions.append(modulo.info)
|
21 |
+
function_callbacks[modulo.info["function"]["name"]] = modulo.ejecutar
|
22 |
+
function_user_text[modulo.info["function"]["name"]] = modulo.user_text
|
modules/chat_functions/buscar_google.py
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import os
|
3 |
+
from googleapiclient.discovery import build
|
4 |
+
from .. import log_module
|
5 |
+
|
6 |
+
|
7 |
+
activo = True
|
8 |
+
try:
|
9 |
+
api_key = os.getenv("GOOGLE_API_KEY")
|
10 |
+
cse_id = os.getenv("GOOGLE_CSE_ID")
|
11 |
+
except:
|
12 |
+
api_key = False
|
13 |
+
cse_id = False
|
14 |
+
if not api_key or not cse_id:
|
15 |
+
activo = False
|
16 |
+
|
17 |
+
user_text = "Busqueda en google"
|
18 |
+
|
19 |
+
structure_info = {
|
20 |
+
"conseguidos": [
|
21 |
+
{
|
22 |
+
"title": "This will contain the title of the link",
|
23 |
+
"link": "Link to the page where the information is found",
|
24 |
+
"snippet": "This will contain a summary of the page's content. Use this information to extrapolate a useful response for the user"
|
25 |
+
},
|
26 |
+
{'x':'y'},
|
27 |
+
]
|
28 |
+
}
|
29 |
+
info = {
|
30 |
+
"type": "function",
|
31 |
+
"function":{
|
32 |
+
"name": "google_search",
|
33 |
+
"description": "A wrapper around Google Search. Useful for when you need to answer questions about current events. Use it ONLY WHEN IS NECESSARY if you want too look for news or people. The you must return a easily readable result. Avoid using this tool too frequently.\nThe output of the execution will be a JSON: \n"+json.dumps(structure_info, indent=2).replace("{'x':'y'},", "...,"),
|
34 |
+
"parameters": {
|
35 |
+
"type": "object",
|
36 |
+
"properties": {
|
37 |
+
"search": {
|
38 |
+
"type": "string",
|
39 |
+
"description": "The search term to use in Google to obtain information. e.g. Achievements of Alexander the Great in Asia.",
|
40 |
+
},
|
41 |
+
"info": {
|
42 |
+
"type": "string",
|
43 |
+
"description": "The user won't see this. Explain why did you call this function",
|
44 |
+
}
|
45 |
+
},
|
46 |
+
"required": ["search"],
|
47 |
+
}
|
48 |
+
}
|
49 |
+
}
|
50 |
+
|
51 |
+
|
52 |
+
def ejecutar(params, gid):
|
53 |
+
busqueda = params["search"]
|
54 |
+
try:
|
55 |
+
service = build("customsearch", "v1", developerKey=api_key)
|
56 |
+
results = service.cse().list(q=busqueda, cx=cse_id, num=5).execute()
|
57 |
+
retorno = {}
|
58 |
+
retorno["founds"] = []
|
59 |
+
for result in results["items"]:
|
60 |
+
if "title" not in result or "snippet" not in result: continue
|
61 |
+
retorno["founds"].append({
|
62 |
+
"title": result["title"],
|
63 |
+
"link": result["link"],
|
64 |
+
"snippet": result["snippet"]
|
65 |
+
})
|
66 |
+
log_module.logger(gid).info(f"Searh google {params}")
|
67 |
+
return json.dumps(retorno)
|
68 |
+
except Exception as e:
|
69 |
+
log_module.logger(gid).error(f"Searh google failed: {repr(e)}")
|
70 |
+
return json.dumps({"status": "api failed"})
|
71 |
+
|
72 |
+
|
modules/chat_functions/leer_pagina_web.py
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import os
|
3 |
+
import requests
|
4 |
+
from modules import log_module
|
5 |
+
activo = False
|
6 |
+
|
7 |
+
user_text = "Leyendo pagina web"
|
8 |
+
|
9 |
+
info = {
|
10 |
+
"type": "function",
|
11 |
+
"function":{
|
12 |
+
"name": "leer_pagina_web",
|
13 |
+
"description": "Un portal hacia Internet. Úsalo cuando necesites obtener contenido específico de un sitio web. La entrada debe ser una URL (por ejemplo, https://www.google.com). La salida será la respuesta en texto de la solicitud GET.",
|
14 |
+
"parameters": {
|
15 |
+
"type": "object",
|
16 |
+
"properties": {
|
17 |
+
"url": {
|
18 |
+
"type": "string",
|
19 |
+
"description": "el url a consultar",
|
20 |
+
},
|
21 |
+
},
|
22 |
+
"required": ["url"],
|
23 |
+
}
|
24 |
+
}
|
25 |
+
}
|
26 |
+
|
27 |
+
def ejecutar(url):
|
28 |
+
try:
|
29 |
+
req = requests.get(url=url, timeout=5)
|
30 |
+
retorno = {
|
31 |
+
"data": req.text
|
32 |
+
}
|
33 |
+
return json.dumps(retorno)
|
34 |
+
except Exception as e:
|
35 |
+
log_module.logger.error(repr(e))
|
36 |
+
return json.dumps({"status": "api failed"})
|
37 |
+
|
modules/chat_functions/obtener_clima.py
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import os
|
3 |
+
import requests
|
4 |
+
from modules import log_module
|
5 |
+
activo = False
|
6 |
+
try:
|
7 |
+
api_key = os.getenv("WEATHERAPI_KEY")
|
8 |
+
except:
|
9 |
+
api_key = False
|
10 |
+
if not api_key:
|
11 |
+
activo = False
|
12 |
+
|
13 |
+
user_text = "Consultando el clima"
|
14 |
+
|
15 |
+
info = {
|
16 |
+
"type": "function",
|
17 |
+
"function": {
|
18 |
+
"name": "obtener_clima",
|
19 |
+
"description": "Obtiene el clima de una ubicación",
|
20 |
+
"parameters": {
|
21 |
+
"type": "object",
|
22 |
+
"properties": {
|
23 |
+
"ubicacion": {
|
24 |
+
"type": "string",
|
25 |
+
"description": "La ciudad, estado y pais, e.g. Caracas, Distrito Capital, Venezuela",
|
26 |
+
},
|
27 |
+
"unidad": {
|
28 |
+
"type": "string",
|
29 |
+
"enum": ["celsius"]
|
30 |
+
},
|
31 |
+
},
|
32 |
+
"required": ["ubicacion", "unidad"],
|
33 |
+
}
|
34 |
+
}
|
35 |
+
}
|
36 |
+
|
37 |
+
def ejecutar(ubicacion, unidad="celsius"):
|
38 |
+
url = f"https://api.weatherapi.com/v1/current.json?key={api_key}&q={ubicacion}&aqi=no"
|
39 |
+
elementos = ["temp_c", "is_day", "condition", "wind_kph", "wind_dir", "precip_mm", "humidity", "cloud", "feelslike_c", "vis_km"]
|
40 |
+
try:
|
41 |
+
rq = requests.get(url, timeout=3)
|
42 |
+
if rq.status_code!=200:
|
43 |
+
return json.dumps({"status": "api failed"})
|
44 |
+
clima = json.loads(rq.text)
|
45 |
+
clima = {k: v for k, v in clima["current"].items() if k in elementos}
|
46 |
+
return json.dumps(clima)
|
47 |
+
except Exception as e:
|
48 |
+
log_module.error(repr(e))
|
49 |
+
return json.dumps({"status": "api failed"})
|
modules/error_map.py
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from openai._exceptions import (
|
2 |
+
APIError,
|
3 |
+
OpenAIError,
|
4 |
+
ConflictError,
|
5 |
+
NotFoundError,
|
6 |
+
APIStatusError,
|
7 |
+
RateLimitError,
|
8 |
+
APITimeoutError,
|
9 |
+
BadRequestError,
|
10 |
+
APIConnectionError,
|
11 |
+
AuthenticationError,
|
12 |
+
InternalServerError,
|
13 |
+
PermissionDeniedError,
|
14 |
+
UnprocessableEntityError,
|
15 |
+
APIResponseValidationError,
|
16 |
+
)
|
17 |
+
from fastapi import status
|
18 |
+
from requests.exceptions import RequestException
|
19 |
+
error_table = {
|
20 |
+
RequestException: {
|
21 |
+
"status_code": status.HTTP_408_REQUEST_TIMEOUT,
|
22 |
+
"detail": "Los servidores tardaron mucho en responder, puede haber sobrecarga en OpenAI, reintenta luego (error 1)"
|
23 |
+
},
|
24 |
+
APIConnectionError: {
|
25 |
+
"status_code": status.HTTP_408_REQUEST_TIMEOUT,
|
26 |
+
"detail": "El servidor no respondió, hubo un error de API"
|
27 |
+
},
|
28 |
+
APITimeoutError: {
|
29 |
+
"status_code": status.HTTP_408_REQUEST_TIMEOUT,
|
30 |
+
"detail": "El servidor tardó demasiado en responder"
|
31 |
+
},
|
32 |
+
RateLimitError: {
|
33 |
+
"status_code": status.HTTP_408_REQUEST_TIMEOUT,
|
34 |
+
"detail": "ChatGPT se gomitó 🤮, este chat ya es muy largo, limpia el chat y reintenta."
|
35 |
+
},
|
36 |
+
"undefined": {
|
37 |
+
"status_code": status.HTTP_408_REQUEST_TIMEOUT,
|
38 |
+
"detail": "Error no definido 🙄"
|
39 |
+
}
|
40 |
+
}
|
41 |
+
|
modules/llm.py
ADDED
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json, time
|
2 |
+
|
3 |
+
from hashlib import sha256
|
4 |
+
from fastapi import HTTPException
|
5 |
+
from openai import OpenAI
|
6 |
+
import tiktoken
|
7 |
+
|
8 |
+
from . import log_module, error_map, chat_functions as tools, settings
|
9 |
+
from typing import TYPE_CHECKING
|
10 |
+
if TYPE_CHECKING:
|
11 |
+
from . import model
|
12 |
+
|
13 |
+
import asyncio
|
14 |
+
|
15 |
+
|
16 |
+
|
17 |
+
|
18 |
+
encoding = tiktoken.encoding_for_model(settings.GPT_MODEL)
|
19 |
+
|
20 |
+
|
21 |
+
def ejecutar(chat: "model.Chat", session: "model.Session"):
|
22 |
+
temp_messages = [msg.model_dump() for msg in chat.messages]
|
23 |
+
|
24 |
+
try:
|
25 |
+
client = OpenAI(
|
26 |
+
api_key=settings.OPENAI_API_KEY,
|
27 |
+
timeout=30.0,
|
28 |
+
max_retries=3
|
29 |
+
)
|
30 |
+
generated = client.chat.completions.create(
|
31 |
+
model=settings.GPT_MODEL,
|
32 |
+
messages=temp_messages,
|
33 |
+
temperature=session.configs.temperature,
|
34 |
+
frequency_penalty=session.configs.frequency_penalty,
|
35 |
+
presence_penalty=session.configs.presence_penalty,
|
36 |
+
tools= tools.functions if session.configs.useTool else None,
|
37 |
+
stream=True,
|
38 |
+
user=sha256(session.gid.encode('UTF-8')).hexdigest()
|
39 |
+
)
|
40 |
+
|
41 |
+
return generated
|
42 |
+
except Exception as error:
|
43 |
+
log_module.logger(session.gid).error(repr(error) + " - " + session.gid)
|
44 |
+
raise HTTPException( **error_map.error_table.get(type(error), error_map.error_table["undefined"]))
|
45 |
+
|
46 |
+
async def streamer(chat: "model.Chat", session: "model.Session", sub_exec:bool = False):
|
47 |
+
response_async = ejecutar(chat, session)
|
48 |
+
if not sub_exec:
|
49 |
+
yield json.dumps({"comando": "status", "status":{"mensaje":"Cargando", "modo": "reemplazar"}})
|
50 |
+
|
51 |
+
message = None
|
52 |
+
role = None
|
53 |
+
|
54 |
+
|
55 |
+
chunk = next(response_async)
|
56 |
+
role = chunk.choices[0].delta.role
|
57 |
+
|
58 |
+
if isinstance(chunk.choices[0].delta.content, str):
|
59 |
+
message = chat.new_msg(role, response_async)
|
60 |
+
chat.append(message)
|
61 |
+
else:
|
62 |
+
message = chat.new_func(role, response_async, chunk)
|
63 |
+
if not sub_exec:
|
64 |
+
yield json.dumps({"comando": "status", "status":{"mensaje":"Buscando en google o algo así", "modo": "reemplazar"}})
|
65 |
+
message.exec(session.gid)
|
66 |
+
|
67 |
+
chat.append(message)
|
68 |
+
|
69 |
+
async for r_async in streamer(chat, session, True):
|
70 |
+
yield r_async
|
71 |
+
|
72 |
+
if not sub_exec:
|
73 |
+
session.update_usage(chat.tokens)
|
74 |
+
|
75 |
+
log_module.logger(session.gid).info(f"Chat used, tokens: {chat.tokens}")
|
76 |
+
yield json.dumps({"comando":"challenge", "challenge": session.challenge} )
|
77 |
+
yield json.dumps({"comando":"token", "token": session.create_cookie_token() } )
|
78 |
+
yield json.dumps({"comando":"mensaje", "mensaje": chat.messages[-1].model_dump()} )
|
79 |
+
|
80 |
+
|
81 |
+
|
82 |
+
|
modules/log_module.py
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging, os
|
2 |
+
|
3 |
+
_logger = logging.getLogger(__name__)
|
4 |
+
_logger.setLevel(logging.DEBUG)
|
5 |
+
# Configura el archivo donde se guardarán los eventos
|
6 |
+
if not os.path.exists("logs"):
|
7 |
+
os.makedirs("logs")
|
8 |
+
file_handler = logging.FileHandler('logs/eventos.log', mode='a', encoding=None, delay=False)
|
9 |
+
file_handler.setLevel(logging.DEBUG)
|
10 |
+
|
11 |
+
stream_handler = logging.StreamHandler()
|
12 |
+
stream_handler.setLevel(logging.DEBUG)
|
13 |
+
|
14 |
+
# Define el formato de los mensajes de log
|
15 |
+
formatter = logging.Formatter('%(asctime)s [%(filename)s:%(lineno)d @ %(funcName)s] - %(levelname)s - %(user)s - %(message)s')
|
16 |
+
|
17 |
+
file_handler.setFormatter(formatter)
|
18 |
+
stream_handler.setFormatter(formatter)
|
19 |
+
|
20 |
+
# Agrega el handler al logger
|
21 |
+
_logger.addHandler(file_handler)
|
22 |
+
_logger.addHandler(stream_handler)
|
23 |
+
|
24 |
+
def logger(user:str = "", action:str = ""):
|
25 |
+
return logging.LoggerAdapter(_logger, extra={'user': user})
|
modules/model.py
ADDED
@@ -0,0 +1,375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
|
3 |
+
from pymongo.mongo_client import MongoClient
|
4 |
+
from pymongo.server_api import ServerApi
|
5 |
+
from fastapi import Request, Response
|
6 |
+
from . import log_module, security, settings, chat_functions
|
7 |
+
from datetime import timezone, datetime, timedelta
|
8 |
+
from pydantic import BaseModel, Field, PrivateAttr
|
9 |
+
from typing import Any, List, Self
|
10 |
+
import tiktoken
|
11 |
+
import uuid
|
12 |
+
|
13 |
+
client: MongoClient = MongoClient(settings.DB_URI, server_api=ServerApi('1'))
|
14 |
+
class __DB:
|
15 |
+
user = client.ChatDB.users
|
16 |
+
sess = client.ChatDB.sessions
|
17 |
+
personalities = client.ChatDB.personalityCores
|
18 |
+
DB: __DB = __DB()
|
19 |
+
|
20 |
+
tz: timezone = timezone(timedelta(hours=-4))
|
21 |
+
encoding = tiktoken.encoding_for_model(settings.GPT_MODEL)
|
22 |
+
|
23 |
+
def count_tokens_on_message(args: list) -> int:
|
24 |
+
token_count = 0
|
25 |
+
for n in args:
|
26 |
+
if n: token_count += len(encoding.encode(n))
|
27 |
+
return token_count
|
28 |
+
|
29 |
+
def get_personality_core (personality):
|
30 |
+
found = DB.personalities.find_one({"name":personality})
|
31 |
+
return found["prompt"].replace(
|
32 |
+
"{date}", datetime.now().strftime("%Y-%m-%D")
|
33 |
+
)
|
34 |
+
|
35 |
+
def get_all_personality_cores():
|
36 |
+
return [x["name"] for x in DB.personalities.find({})]
|
37 |
+
|
38 |
+
class Configs(BaseModel):
|
39 |
+
temperature: float = 0.5
|
40 |
+
frequency_penalty: float = 0.0
|
41 |
+
presence_penalty: float = 0.0
|
42 |
+
useTool: bool = True
|
43 |
+
assistant: str = "Chatsito clasico"
|
44 |
+
assistantPrompt: str = get_personality_core("Chatsito clasico")
|
45 |
+
|
46 |
+
def __init__(self, *args, **kwargs):
|
47 |
+
|
48 |
+
super(Configs, self).__init__(*args, **kwargs)
|
49 |
+
self.assistantPrompt = get_personality_core(self.assistant)
|
50 |
+
|
51 |
+
|
52 |
+
|
53 |
+
|
54 |
+
class Message(BaseModel):
|
55 |
+
role: str
|
56 |
+
content: str = ""
|
57 |
+
_tokens: int = 1
|
58 |
+
_thread = None
|
59 |
+
_running:bool = False
|
60 |
+
|
61 |
+
def __init__(self, *args, **kwargs):
|
62 |
+
super(Message, self).__init__(*args, **kwargs)
|
63 |
+
self._tokens = count_tokens_on_message([self.content])
|
64 |
+
|
65 |
+
def consume_stream(self, stream):
|
66 |
+
self._running = True
|
67 |
+
for chunk in stream:
|
68 |
+
self._tokens += 1
|
69 |
+
choice = chunk.choices[0]
|
70 |
+
if choice.finish_reason == "stop" or not choice.delta.content:
|
71 |
+
return
|
72 |
+
|
73 |
+
self.content += choice.delta.content
|
74 |
+
|
75 |
+
self._running = False
|
76 |
+
|
77 |
+
def get_tokens(self):
|
78 |
+
return self._tokens, self._tokensOutput
|
79 |
+
|
80 |
+
class ToolCallsInputFunction(BaseModel):
|
81 |
+
name: str = ""
|
82 |
+
arguments: str = ""
|
83 |
+
class ToolCallsInput(BaseModel):
|
84 |
+
id: str
|
85 |
+
function: ToolCallsInputFunction
|
86 |
+
type: str = "function"
|
87 |
+
|
88 |
+
class ToolCallsOutput(BaseModel):
|
89 |
+
role: str
|
90 |
+
tool_call_id: str
|
91 |
+
name: str
|
92 |
+
content: str
|
93 |
+
_tokens: int = 0
|
94 |
+
|
95 |
+
def __init__(self, *args, **kwargs):
|
96 |
+
|
97 |
+
super(ToolCallsOutput, self).__init__(*args, **kwargs)
|
98 |
+
self._tokens = count_tokens_on_message([self.content])
|
99 |
+
|
100 |
+
|
101 |
+
class MessageTool(BaseModel):
|
102 |
+
role: str
|
103 |
+
content: str = ""
|
104 |
+
tool_calls: list[ToolCallsInput]
|
105 |
+
_tokens: int = 0
|
106 |
+
_outputs: list[ToolCallsOutput] = []
|
107 |
+
|
108 |
+
def __init__(self, **kwargs):
|
109 |
+
stream = kwargs.pop("stream")
|
110 |
+
chunk = kwargs.pop("chunk")
|
111 |
+
kwargs["tool_calls"] = []
|
112 |
+
super(MessageTool, self).__init__(**kwargs)
|
113 |
+
|
114 |
+
while True:
|
115 |
+
choice = chunk.choices[0]
|
116 |
+
self._tokens += 1
|
117 |
+
|
118 |
+
if choice.finish_reason:
|
119 |
+
break
|
120 |
+
|
121 |
+
if chunk.choices[0].delta.tool_calls == None:
|
122 |
+
chunk = next(stream)
|
123 |
+
continue
|
124 |
+
|
125 |
+
tool_call = chunk.choices[0].delta.tool_calls[0]
|
126 |
+
|
127 |
+
if tool_call.id:
|
128 |
+
self.tool_calls.append(
|
129 |
+
ToolCallsInput(
|
130 |
+
id=tool_call.id,
|
131 |
+
function=ToolCallsInputFunction(
|
132 |
+
**tool_call.function.model_dump()
|
133 |
+
)))
|
134 |
+
elif tool_call.function.arguments:
|
135 |
+
self.tool_calls[-1].function.arguments += tool_call.function.arguments
|
136 |
+
|
137 |
+
|
138 |
+
chunk = next(stream)
|
139 |
+
if not chunk:
|
140 |
+
self._tokens += sum
|
141 |
+
break
|
142 |
+
|
143 |
+
def exec(self, gid):
|
144 |
+
for func in self.tool_calls:
|
145 |
+
self._outputs.append(ToolCallsOutput(
|
146 |
+
role="tool",
|
147 |
+
tool_call_id=func.id,
|
148 |
+
name=func.function.name ,
|
149 |
+
content=chat_functions.function_callbacks[func.function.name](json.loads(func.function.arguments), gid)
|
150 |
+
))
|
151 |
+
|
152 |
+
|
153 |
+
class Chat(BaseModel):
|
154 |
+
messages: List[Message|MessageTool|ToolCallsOutput]
|
155 |
+
tokens: int = 3
|
156 |
+
|
157 |
+
def __init__(self: Self, *args: list, **kwargs: dict):
|
158 |
+
temp = []
|
159 |
+
for i in kwargs.pop("messages"):
|
160 |
+
temp.append(Message(**i))
|
161 |
+
kwargs["messages"] = temp
|
162 |
+
super(Chat, self).__init__(*args, **kwargs)
|
163 |
+
self.tokens += sum([x._tokens+3 for x in self.messages])
|
164 |
+
|
165 |
+
def append(self: Self, message: Message|MessageTool):
|
166 |
+
if isinstance(message, Message):
|
167 |
+
self.messages.append(message)
|
168 |
+
self.tokens += message._tokens
|
169 |
+
else:
|
170 |
+
self.messages.append(message)
|
171 |
+
self.tokens += message._tokens
|
172 |
+
for out in message._outputs:
|
173 |
+
self.tokens += out._tokens
|
174 |
+
self.messages.append(out)
|
175 |
+
|
176 |
+
@classmethod
|
177 |
+
def new_msg(cls: Self, role: str, stream):
|
178 |
+
message = Message(role=role)
|
179 |
+
message.consume_stream(stream)
|
180 |
+
return message
|
181 |
+
|
182 |
+
@classmethod
|
183 |
+
def new_func(cls: Self, role: str, stream, chunk):
|
184 |
+
return MessageTool(role=role, stream=stream, chunk=chunk)
|
185 |
+
|
186 |
+
class Session(BaseModel):
|
187 |
+
gid: str
|
188 |
+
fprint: str
|
189 |
+
hashed: str
|
190 |
+
guid: str
|
191 |
+
public_key: str = ""
|
192 |
+
challenge: str = str(uuid.uuid4())
|
193 |
+
data: dict = {}
|
194 |
+
configs: Configs | None = None
|
195 |
+
|
196 |
+
def __init__(self, **kwargs):
|
197 |
+
kwargs["guid"] = kwargs.get("guid", str(uuid.uuid4()))
|
198 |
+
kwargs["hashed"] = security.sha256(kwargs["guid"] + kwargs["fprint"])
|
199 |
+
super(Session, self).__init__(**kwargs)
|
200 |
+
|
201 |
+
|
202 |
+
def validate_signature(self: Self, signature:str):
|
203 |
+
valid = security.validate_signature(self.public_key, signature, self.challenge)
|
204 |
+
if not valid:
|
205 |
+
security.raise_401("Cannot validate Session signature")
|
206 |
+
return True
|
207 |
+
|
208 |
+
@classmethod
|
209 |
+
def find_from_data(cls:Self, request: Request, data:dict) -> Self:
|
210 |
+
cookie_data:dict = security.token_from_cookie(request)
|
211 |
+
|
212 |
+
if "gid" not in cookie_data or "guid" not in cookie_data:
|
213 |
+
log_module.logger().error("Cookie without session needed data")
|
214 |
+
security.raise_401("gid or guid not in cookie")
|
215 |
+
|
216 |
+
if not (public_key := cookie_data.get("public_key", None)): # FIX Vuln Code
|
217 |
+
if request.scope["path"] != "/getToken":
|
218 |
+
log_module.logger(cookie_data["gid"]).error(f"User without public key saved | {json.dumps(cookie_data)}")
|
219 |
+
security.raise_401("the user must have a public key saved in token")
|
220 |
+
else:
|
221 |
+
log_module.logger(cookie_data['gid']).info("API public key set for user")
|
222 |
+
public_key = data["public_key"]
|
223 |
+
else:
|
224 |
+
cls.check_challenge(data["fingerprint"], cookie_data["challenge"])
|
225 |
+
|
226 |
+
session: Self = cls(
|
227 |
+
gid = cookie_data["gid"],
|
228 |
+
fprint = data["fingerprint"],
|
229 |
+
guid = cookie_data["guid"],
|
230 |
+
public_key = public_key,
|
231 |
+
data = data,
|
232 |
+
configs = Configs(**cookie_data["configs"])
|
233 |
+
)
|
234 |
+
|
235 |
+
|
236 |
+
if session.hashed != cookie_data["fprint"]:
|
237 |
+
log_module.logger(session.gid).error(f"Fingerprint didnt match | {json.dumps(cookie_data)}")
|
238 |
+
security.raise_401("Fingerprint didnt match")
|
239 |
+
|
240 |
+
|
241 |
+
session.add_challenge()
|
242 |
+
|
243 |
+
return session
|
244 |
+
|
245 |
+
|
246 |
+
def create_cookie_token(self:Self):
|
247 |
+
return security.create_jwt_token({
|
248 |
+
"gid":self.gid,
|
249 |
+
"guid": self.guid,
|
250 |
+
"fprint": self.hashed,
|
251 |
+
"public_key": self.public_key,
|
252 |
+
"challenge": self.challenge,
|
253 |
+
"configs": self.configs.model_dump()
|
254 |
+
})
|
255 |
+
|
256 |
+
def create_cookie(self:Self, response: Response):
|
257 |
+
jwt = self.create_cookie_token()
|
258 |
+
security.set_cookie(response, "token", jwt, {"hours": 24})
|
259 |
+
|
260 |
+
def update_usage(self: Self, message:Message):
|
261 |
+
User.update_usage(self.gid, message)
|
262 |
+
|
263 |
+
def add_challenge(self: Self):
|
264 |
+
return True
|
265 |
+
self.challenge = str(uuid.uuid4())
|
266 |
+
DB.sess.insert_one(self.model_dump(include={"fprint", "challenge"}) )
|
267 |
+
|
268 |
+
@staticmethod
|
269 |
+
def check_challenge(fprint:str, challenge: str):
|
270 |
+
return True
|
271 |
+
found = DB.sess.find_one_and_delete({"fprint":fprint})
|
272 |
+
if not found or found["challenge"] != challenge:
|
273 |
+
security.raise_401("Check challenge failed")
|
274 |
+
|
275 |
+
|
276 |
+
class User(BaseModel):
|
277 |
+
name: str
|
278 |
+
tokens: dict = {}
|
279 |
+
created: datetime = datetime.now(tz)
|
280 |
+
approved: datetime | None = None
|
281 |
+
description: str = ""
|
282 |
+
email: str
|
283 |
+
gid: str
|
284 |
+
role: str = "on hold"
|
285 |
+
configs: Configs = Configs()
|
286 |
+
|
287 |
+
_session: Session | None = None
|
288 |
+
_data: dict | None = None
|
289 |
+
|
290 |
+
@classmethod
|
291 |
+
def find_or_create(cls: Self, data: dict, loginData: dict)-> Self:
|
292 |
+
|
293 |
+
found = DB.user.find_one({"gid":data["gid"]})
|
294 |
+
|
295 |
+
user:Self = cls(**found) if found else cls(**data)
|
296 |
+
|
297 |
+
if not found:
|
298 |
+
DB.user.insert_one(user.model_dump())
|
299 |
+
|
300 |
+
user._session = Session(gid=user.gid, fprint=loginData["fp"], public_key=loginData["pk"])
|
301 |
+
|
302 |
+
log_module.logger(user.gid).info(f"User {'logged' if found else'created'} | fp: {user._session.fprint}")
|
303 |
+
return user
|
304 |
+
|
305 |
+
@classmethod
|
306 |
+
def find_from_cookie(cls:Self, request: Request) -> Self:
|
307 |
+
cookie_data:dict = security.token_from_cookie(request)
|
308 |
+
|
309 |
+
if "gid" not in cookie_data or "guid" not in cookie_data:
|
310 |
+
log_module.logger().error("Cookie without needed data")
|
311 |
+
security.raise_307("gid or guid not in cookie")
|
312 |
+
|
313 |
+
found:dict = DB.user.find_one({"gid":cookie_data["gid"]})
|
314 |
+
|
315 |
+
if not found:
|
316 |
+
log_module.logger(cookie_data["gid"]).error("User not found on DB")
|
317 |
+
security.raise_307("User not found on DB")
|
318 |
+
|
319 |
+
user: Self = cls(**found)
|
320 |
+
user._session = Session(
|
321 |
+
gid = cookie_data["gid"],
|
322 |
+
guid = cookie_data["guid"],
|
323 |
+
fprint = cookie_data["fprint"],
|
324 |
+
configs = user.configs
|
325 |
+
)
|
326 |
+
return user
|
327 |
+
|
328 |
+
@classmethod
|
329 |
+
def find_from_data(cls:Self, request: Request, data:dict) -> Self:
|
330 |
+
session:Session = Session.find_from_data(request, data)
|
331 |
+
|
332 |
+
found:dict = DB.user.find_one({"gid":session.gid})
|
333 |
+
|
334 |
+
if not found:
|
335 |
+
log_module.logger(session.gid).error("User not found on DB")
|
336 |
+
security.raise_307("User not found on DB")
|
337 |
+
|
338 |
+
user: Self = cls(**found)
|
339 |
+
user._session = session
|
340 |
+
user._data = data
|
341 |
+
return user
|
342 |
+
|
343 |
+
def update_description(self: Self, message: str) -> None:
|
344 |
+
log_module.logger(self.gid).info("Description Updated")
|
345 |
+
DB.user.update_one(
|
346 |
+
{"gid":self.gid},
|
347 |
+
{"$set": { "description": message}}
|
348 |
+
)
|
349 |
+
self.description = message
|
350 |
+
|
351 |
+
def can_use(self: Self, activity: str):
|
352 |
+
return security.can_use(self.role, activity)
|
353 |
+
|
354 |
+
def update_user(self: Self) -> None:
|
355 |
+
log_module.logger(self.gid).info("User Updated")
|
356 |
+
DB.user.update_one({"gid": self.gid}, {"$set": self.model_dump()})
|
357 |
+
return self.configs.assistantPrompt
|
358 |
+
|
359 |
+
@staticmethod
|
360 |
+
def update_usage(gid:str, tokens:int):
|
361 |
+
inc_field = datetime.now().strftime("tokens.%y.%m.%d")
|
362 |
+
DB.user.update_one({"gid": gid}, {"$inc":{inc_field: tokens}})
|
363 |
+
|
364 |
+
def create_cookie(self:Self):
|
365 |
+
return security.create_jwt_token({
|
366 |
+
"gid":self._session.gid,
|
367 |
+
"guid": self._session.guid,
|
368 |
+
"fprint": self._session.hashed,
|
369 |
+
"public_key": self._session.public_key,
|
370 |
+
"challenge": self._session.challenge,
|
371 |
+
"configs": self.configs.model_dump()
|
372 |
+
})
|
373 |
+
|
374 |
+
|
375 |
+
|
modules/oauth.py
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
from . import settings
|
3 |
+
|
4 |
+
def validate_redirect(params):
|
5 |
+
data = {
|
6 |
+
'code': params["code"],
|
7 |
+
'redirect_uri': settings.OAUTH_REDIRECT,
|
8 |
+
'client_id': settings.GOOGLE_CLIENT_ID,
|
9 |
+
'client_secret': settings.GOOGLE_CLIENT_SECRET,
|
10 |
+
'scope': params["scope"],
|
11 |
+
'grant_type': 'authorization_code'
|
12 |
+
}
|
13 |
+
|
14 |
+
google_auth_rq = requests.post("https://oauth2.googleapis.com/token", data=data, timeout=30)
|
15 |
+
google_auth = google_auth_rq.json()
|
16 |
+
|
17 |
+
try:
|
18 |
+
headers = {
|
19 |
+
'Authorization': google_auth["token_type"]+" "+google_auth["access_token"]
|
20 |
+
}
|
21 |
+
except Exception as e:
|
22 |
+
print(e)
|
23 |
+
print(google_auth_rq.content)
|
24 |
+
return str(e)+" "+google_auth_rq.content
|
25 |
+
google_userinfo_rq = requests.get("https://www.googleapis.com/oauth2/v2/userinfo", headers=headers, timeout=30)
|
26 |
+
ret = google_userinfo_rq.json()
|
27 |
+
ret["gid"] = ret.get("id","")
|
28 |
+
return ret
|
29 |
+
|
modules/personality_cores.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
from pymongo.mongo_client import MongoClient
|
3 |
+
from pymongo.server_api import ServerApi
|
4 |
+
from . import settings
|
5 |
+
|
6 |
+
client: MongoClient = MongoClient(settings.DB_URI, server_api=ServerApi('1'))
|
7 |
+
class __DB:
|
8 |
+
personalities = client.ChatDB.personalityCores
|
9 |
+
DB: __DB = __DB()
|
10 |
+
|
11 |
+
|
12 |
+
def get (asistente):
|
13 |
+
found = DB.personalities.find_one({"name":asistente})
|
14 |
+
return found["prompt"].replace(
|
15 |
+
"{date}", datetime.now().strftime("%Y-%m-%D")
|
16 |
+
)
|
17 |
+
|
18 |
+
|
modules/security.py
ADDED
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import jwt
|
2 |
+
from hashlib import sha256
|
3 |
+
from fastapi import HTTPException, status, Request, Response
|
4 |
+
from . import log_module, settings
|
5 |
+
from datetime import datetime, timedelta
|
6 |
+
from Crypto.Signature import pss
|
7 |
+
from Crypto.Hash import SHA256
|
8 |
+
from Crypto.PublicKey import RSA
|
9 |
+
|
10 |
+
import base64
|
11 |
+
|
12 |
+
|
13 |
+
for key in settings.USERS:
|
14 |
+
if key == "master": continue
|
15 |
+
password = key+settings.USERS[key]+settings.USERS["master"]
|
16 |
+
settings.USERS[key] = sha256(password.encode('UTF-8')).hexdigest()
|
17 |
+
|
18 |
+
|
19 |
+
def create_jwt_token(data, maxlife=settings.JWT_EXPIRATION_TIME_MINUTES_API):
|
20 |
+
expire = datetime.utcnow() + timedelta(minutes=maxlife)
|
21 |
+
to_encode = data | {"exp": expire}
|
22 |
+
encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM)
|
23 |
+
return encoded_jwt
|
24 |
+
|
25 |
+
def validate_jwt_token(token: str, usage: str = "api"):
|
26 |
+
try:
|
27 |
+
payload = jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])
|
28 |
+
return payload
|
29 |
+
except Exception as e:
|
30 |
+
log_module.logger().error(repr(e) + " - Invalid token: " + str(token))
|
31 |
+
if usage == "view":
|
32 |
+
return raise_307("Failed validating jwt token")
|
33 |
+
return raise_401("Failed validating jwt token")
|
34 |
+
|
35 |
+
def token_from_cookie(request:Request):
|
36 |
+
if token := request.cookies.get('token', ""):
|
37 |
+
return validate_jwt_token(token, "view")
|
38 |
+
return raise_307("Token not found")
|
39 |
+
|
40 |
+
def token_from_headers(request:Request):
|
41 |
+
if (bearer := request.headers.get("Autorization", " ").split(" ",1)) and bearer[0] == "Bearer":
|
42 |
+
return validate_jwt_token(bearer[1])
|
43 |
+
return raise_401("Bearer malformed")
|
44 |
+
|
45 |
+
|
46 |
+
def can_use(role, activity):
|
47 |
+
can = {
|
48 |
+
"chat": ["user", "admin"]
|
49 |
+
}
|
50 |
+
return role in can[activity]
|
51 |
+
|
52 |
+
def raise_401(detail:str):
|
53 |
+
headers = {"set-cookie": "token=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT",
|
54 |
+
"Location": "/login"}
|
55 |
+
raise HTTPException(
|
56 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
57 |
+
detail=detail,
|
58 |
+
headers=headers
|
59 |
+
)
|
60 |
+
|
61 |
+
def raise_307(detail:str):
|
62 |
+
headers = {
|
63 |
+
"set-cookie": "token=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT",
|
64 |
+
#"Location": "/login"
|
65 |
+
}
|
66 |
+
raise HTTPException(
|
67 |
+
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
|
68 |
+
detail=detail,
|
69 |
+
headers=headers
|
70 |
+
)
|
71 |
+
|
72 |
+
def validate_signature(public_key: str, signature: str, data:str) -> bool:
|
73 |
+
public_key = RSA.import_key(public_key)
|
74 |
+
signature = base64.b64decode(signature)
|
75 |
+
data_ = SHA256.new(data.encode())
|
76 |
+
try:
|
77 |
+
pss.new(public_key).verify(data_, signature)
|
78 |
+
return True
|
79 |
+
except ValueError:
|
80 |
+
raise_401("Signature failed")
|
81 |
+
|
82 |
+
def sha256(data:str|bytes) -> str:
|
83 |
+
data_ = data if isinstance(data, bytes) else data.encode()
|
84 |
+
return SHA256.new(data_).hexdigest()
|
85 |
+
|
86 |
+
def set_cookie(response: Response, key: str, value: str, expire_time: dict = {"days":7}):
|
87 |
+
expires = datetime.now(tz=settings.TZ) + timedelta(**expire_time)
|
88 |
+
response.set_cookie(
|
89 |
+
key=key,
|
90 |
+
value=value,
|
91 |
+
# httponly=True,
|
92 |
+
# samesite='none',
|
93 |
+
expires=expires.strftime("%a, %d %b %Y %H:%M:%S %Z"),
|
94 |
+
#domain='.<YOUR DOMAIN>'
|
95 |
+
|
96 |
+
)
|
modules/settings.py
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os, json
|
2 |
+
|
3 |
+
from datetime import timezone, timedelta
|
4 |
+
from modules import log_module
|
5 |
+
from Crypto.Random import get_random_bytes
|
6 |
+
|
7 |
+
def environ_get(variable, required=False):
|
8 |
+
if variable not in os.environ:
|
9 |
+
if not required:
|
10 |
+
log_module.logger("system").warning(f"Variable not set {variable}")
|
11 |
+
return None
|
12 |
+
else:
|
13 |
+
log_module.logger("system").error(f"Required variable not set {variable}")
|
14 |
+
|
15 |
+
return os.environ[variable]
|
16 |
+
|
17 |
+
TZ = timezone(timedelta(hours=-4))
|
18 |
+
|
19 |
+
GPT_MODEL = environ_get("GPT_MODEL", True)
|
20 |
+
|
21 |
+
MONGO_URL = environ_get("MONGO_URL")
|
22 |
+
MONGO_PWD = environ_get("MONGO_PWD")
|
23 |
+
MONGO_USR = environ_get("MONGO_USR")
|
24 |
+
DB_URI = f"mongodb+srv://{MONGO_USR}:{MONGO_PWD}@{MONGO_URL}/?retryWrites=true&w=majority"
|
25 |
+
DB_URI = "mongodb://127.0.0.1:27017"
|
26 |
+
|
27 |
+
|
28 |
+
OPENAI_API_KEY=environ_get('OPENAI_API_KEY', True)
|
29 |
+
USERS = json.loads(str(environ_get("USER_KEYS", True)).replace("\n", ""))
|
30 |
+
|
31 |
+
JWT_SECRET = environ_get('JWT_SECRET', True)
|
32 |
+
|
33 |
+
JWT_ALGORITHM = "HS256"
|
34 |
+
JWT_EXPIRATION_TIME_MINUTES_API = 7*24*60
|
35 |
+
JWT_EXPIRATION_TIME_MINUTES_VIEW = 7*24*60
|
36 |
+
|
37 |
+
GOOGLE_CLIENT_ID = environ_get("GOOGLE_CLIENT_ID", True)
|
38 |
+
GOOGLE_CLIENT_SECRET = environ_get("GOOGLE_CLIENT_SECRET", True)
|
39 |
+
GOOGLE_API_KEY = environ_get("GOOGLE_API_KEY")
|
40 |
+
GOOGLE_CSE_ID = environ_get("GOOGLE_CSE_ID")
|
41 |
+
|
42 |
+
OAUTH_REDIRECT = os.environ.get("OAUTH_REDIRECT", True)
|
requirements.txt
CHANGED
@@ -1,7 +1,17 @@
|
|
1 |
-
fastapi==0.
|
2 |
-
requests==2.
|
3 |
-
openai==
|
4 |
-
uvicorn[standard]==0.
|
5 |
-
PyJWT==2.
|
6 |
-
hypercorn>=0.
|
7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
fastapi==0.104.*
|
2 |
+
requests==2.31.*
|
3 |
+
openai==1.2.*
|
4 |
+
uvicorn[standard]==0.23.*
|
5 |
+
PyJWT==2.8.*
|
6 |
+
hypercorn>=0.15.*
|
7 |
+
google-api-python-client==2.108.*
|
8 |
+
pymongo[srv]==4.6.*
|
9 |
+
Jinja2>=3.1.*
|
10 |
+
python-multipart==0.0.6
|
11 |
+
tiktoken>=0.5.*
|
12 |
+
pycryptodome==3.19.*
|
13 |
+
pydantic==2.5.*
|
14 |
+
PyJWT==2.8.*
|
15 |
+
pymongo[srv]==4.6.*
|
16 |
+
python-multipart>=0.0.6
|
17 |
+
tiktoken==0.5.*
|
static/css/app.css
CHANGED
@@ -21,6 +21,7 @@ button{
|
|
21 |
display: grid;
|
22 |
grid-template-rows: 40px auto;
|
23 |
height: 100%;
|
|
|
24 |
}
|
25 |
.chat {
|
26 |
--textarea: 0px;
|
@@ -132,7 +133,6 @@ button{
|
|
132 |
|
133 |
.input-box textarea,
|
134 |
.input-box button {
|
135 |
-
height: 100%;
|
136 |
margin: 0;
|
137 |
border: none;
|
138 |
padding: 0 15px;
|
@@ -164,6 +164,9 @@ button{
|
|
164 |
.input-box .input-delete {
|
165 |
background-image: url(/static/img/delete.png);
|
166 |
}
|
|
|
|
|
|
|
167 |
.input-box button:first-child{
|
168 |
border-left: none;
|
169 |
}
|
@@ -245,4 +248,5 @@ button{
|
|
245 |
}
|
246 |
dialog{
|
247 |
margin: auto;
|
|
|
248 |
}
|
|
|
21 |
display: grid;
|
22 |
grid-template-rows: 40px auto;
|
23 |
height: 100%;
|
24 |
+
padding: 0px 20px;
|
25 |
}
|
26 |
.chat {
|
27 |
--textarea: 0px;
|
|
|
133 |
|
134 |
.input-box textarea,
|
135 |
.input-box button {
|
|
|
136 |
margin: 0;
|
137 |
border: none;
|
138 |
padding: 0 15px;
|
|
|
164 |
.input-box .input-delete {
|
165 |
background-image: url(/static/img/delete.png);
|
166 |
}
|
167 |
+
.input-box .input-menu {
|
168 |
+
background-image: url(/static/img/menu.svg);
|
169 |
+
}
|
170 |
.input-box button:first-child{
|
171 |
border-left: none;
|
172 |
}
|
|
|
248 |
}
|
249 |
dialog{
|
250 |
margin: auto;
|
251 |
+
outline: none;
|
252 |
}
|
static/css/menu.css
ADDED
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.menu{
|
2 |
+
background-color: #FFF;
|
3 |
+
}
|
4 |
+
|
5 |
+
dialog{
|
6 |
+
width: 380px;
|
7 |
+
/* height: 500px; */
|
8 |
+
border: solid 1px #000;
|
9 |
+
box-shadow: 5px 5px 5px #000A;
|
10 |
+
overflow-x: hidden;
|
11 |
+
overflow-y: auto;
|
12 |
+
padding: 20px;
|
13 |
+
}
|
14 |
+
|
15 |
+
dialog#menu .title{
|
16 |
+
font-size: 32px;
|
17 |
+
margin-top: -15px;
|
18 |
+
margin-left: -5px;
|
19 |
+
}
|
20 |
+
|
21 |
+
dialog#menu .close{
|
22 |
+
position: relative;
|
23 |
+
float: right;
|
24 |
+
top: -35px;
|
25 |
+
right: -12px;
|
26 |
+
font-size: 25px;
|
27 |
+
padding: 0px 6px;
|
28 |
+
line-height: 24px;
|
29 |
+
outline: none;
|
30 |
+
}
|
31 |
+
|
32 |
+
dialog#menu .saveButton{
|
33 |
+
width: 100%;
|
34 |
+
display: flex;
|
35 |
+
}
|
36 |
+
|
37 |
+
dialog#menu .saveButton button{
|
38 |
+
margin: auto;
|
39 |
+
font-size: 20px;
|
40 |
+
}
|
41 |
+
|
42 |
+
dialog#menu .item{
|
43 |
+
width: 100%;
|
44 |
+
display: grid;
|
45 |
+
align-items: center;
|
46 |
+
grid-template-columns: 170px auto;
|
47 |
+
margin-bottom: 10px;
|
48 |
+
}
|
49 |
+
|
50 |
+
dialog#menu .item:hover{
|
51 |
+
background-color: #eee;
|
52 |
+
}
|
53 |
+
|
54 |
+
dialog#menu hr{
|
55 |
+
margin: 15px -5px;
|
56 |
+
}
|
57 |
+
dialog#menu .item .indented{
|
58 |
+
padding-left: 10px;
|
59 |
+
|
60 |
+
}
|
61 |
+
dialog#menu .item .switch {
|
62 |
+
--secondary-container: #3a4b39;
|
63 |
+
--primary: #84da89;
|
64 |
+
font-size: 17px;
|
65 |
+
position: relative;
|
66 |
+
display: inline-block;
|
67 |
+
width: 3.7em;
|
68 |
+
height: 1.8em;
|
69 |
+
zoom: 0.6;
|
70 |
+
margin: auto;
|
71 |
+
}
|
72 |
+
dialog#menu .item .switch input {
|
73 |
+
display: none;
|
74 |
+
opacity: 0;
|
75 |
+
width: 0;
|
76 |
+
height: 0;
|
77 |
+
}
|
78 |
+
dialog#menu .item .slider {
|
79 |
+
position: absolute;
|
80 |
+
cursor: pointer;
|
81 |
+
top: 0;
|
82 |
+
left: 0;
|
83 |
+
right: 0;
|
84 |
+
bottom: 0;
|
85 |
+
background-color: #313033;
|
86 |
+
transition: 0.2s;
|
87 |
+
border-radius: 30px;
|
88 |
+
}
|
89 |
+
dialog#menu .item .slider:before {
|
90 |
+
position: absolute;
|
91 |
+
content: "";
|
92 |
+
height: 1.4em;
|
93 |
+
width: 1.4em;
|
94 |
+
border-radius: 20px;
|
95 |
+
left: 0.2em;
|
96 |
+
bottom: 0.2em;
|
97 |
+
background-color: #aeaaae;
|
98 |
+
transition: 0.4s;
|
99 |
+
}
|
100 |
+
dialog#menu .item input:checked + .slider::before {
|
101 |
+
background-color: var(--primary);
|
102 |
+
}
|
103 |
+
dialog#menu .item input:checked + .slider {
|
104 |
+
background-color: var(--secondary-container);
|
105 |
+
}
|
106 |
+
dialog#menu .item input:focus + .slider {
|
107 |
+
box-shadow: 0 0 1px var(--secondary-container);
|
108 |
+
}
|
109 |
+
dialog#menu .item input:checked + .slider:before {
|
110 |
+
transform: translateX(1.9em);
|
111 |
+
}
|
112 |
+
|
113 |
+
dialog#menu .item:has(input:disabled){
|
114 |
+
opacity: 0.5;
|
115 |
+
}
|
116 |
+
|
117 |
+
dialog#menu .item label{
|
118 |
+
|
119 |
+
}
|
120 |
+
|
121 |
+
dialog#menu .item input{
|
122 |
+
outline: none;
|
123 |
+
}
|
124 |
+
|
125 |
+
dialog#menu .item input[type=text]{
|
126 |
+
|
127 |
+
}
|
128 |
+
|
129 |
+
dialog#menu .item .range{
|
130 |
+
width: 100%;
|
131 |
+
display: grid;
|
132 |
+
align-items: center;
|
133 |
+
grid-template-columns: auto 40px;
|
134 |
+
}
|
135 |
+
|
136 |
+
dialog#menu .item .range span{
|
137 |
+
text-align: right;
|
138 |
+
}
|
139 |
+
*:has(.tooltip){
|
140 |
+
position: relative;
|
141 |
+
}
|
142 |
+
*:hover > .tooltip {
|
143 |
+
top: -35px;
|
144 |
+
opacity: 0.9;
|
145 |
+
}
|
146 |
+
|
147 |
+
.tooltip {
|
148 |
+
position: absolute;
|
149 |
+
left: 50px;
|
150 |
+
top: 60%;
|
151 |
+
padding: 0.55rem 1rem;
|
152 |
+
max-width: 300px;
|
153 |
+
color: #000;
|
154 |
+
border-radius: 50px;
|
155 |
+
background-color: #fff;
|
156 |
+
box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07);
|
157 |
+
pointer-events: none;
|
158 |
+
user-select: none;
|
159 |
+
opacity: 0;
|
160 |
+
transition: all 0.2s ease-in-out;
|
161 |
+
width: max-content;
|
162 |
+
z-index: 30;
|
163 |
+
}
|
164 |
+
|
165 |
+
.inserted{
|
166 |
+
width: 380px;
|
167 |
+
height: 500px;
|
168 |
+
border: solid 1px #000;
|
169 |
+
box-shadow: 5px 5px 5px #000A;
|
170 |
+
overflow: hidden;
|
171 |
+
padding: 0px;
|
172 |
+
}
|
173 |
+
.inserted iframe{
|
174 |
+
width: 100%;
|
175 |
+
height: 100%;
|
176 |
+
border: none;
|
177 |
+
}
|
static/css/tabs.css
CHANGED
@@ -19,7 +19,6 @@
|
|
19 |
}
|
20 |
|
21 |
.tab {
|
22 |
-
padding: 4px 20px;
|
23 |
display: none;
|
24 |
}
|
25 |
.tab.active{
|
@@ -36,6 +35,7 @@
|
|
36 |
margin-right: 4px;
|
37 |
border-radius: 0px 0px 10px 10px;
|
38 |
transition: 0.2s background-color;
|
|
|
39 |
}
|
40 |
.tab-label-add {
|
41 |
background: #3b74ad;
|
@@ -47,26 +47,20 @@
|
|
47 |
display: block;
|
48 |
line-height: 26px;
|
49 |
}
|
50 |
-
.tab-label >
|
51 |
display: flex;
|
52 |
align-items: center;
|
53 |
-
|
54 |
text-wrap: nowrap;
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
|
|
59 |
}
|
60 |
|
61 |
.tab-label:has(.tab-switch:checked) {
|
62 |
background: #fff;
|
63 |
-
|
64 |
-
/* color: #2c3e50;
|
65 |
-
border-bottom: 0;
|
66 |
-
border-right: 0.125rem solid #fff;
|
67 |
-
transition: all 0.35s;
|
68 |
-
z-index: 1;
|
69 |
-
top: -0.0625rem; */
|
70 |
}
|
71 |
.template{
|
72 |
display: none;
|
|
|
19 |
}
|
20 |
|
21 |
.tab {
|
|
|
22 |
display: none;
|
23 |
}
|
24 |
.tab.active{
|
|
|
35 |
margin-right: 4px;
|
36 |
border-radius: 0px 0px 10px 10px;
|
37 |
transition: 0.2s background-color;
|
38 |
+
background: #7d9dbd;
|
39 |
}
|
40 |
.tab-label-add {
|
41 |
background: #3b74ad;
|
|
|
47 |
display: block;
|
48 |
line-height: 26px;
|
49 |
}
|
50 |
+
.tab-label > .tab-name, .tab-label-add > .tab-name{
|
51 |
display: flex;
|
52 |
align-items: center;
|
53 |
+
margin: 0 10px;
|
54 |
text-wrap: nowrap;
|
55 |
+
width: 64px;
|
56 |
+
overflow: hidden;
|
57 |
+
background: linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(0,0,0,0.8) 70%, rgba(0,0,0,0) 100%);
|
58 |
+
-webkit-background-clip: text;
|
59 |
+
-webkit-text-fill-color: transparent;
|
60 |
}
|
61 |
|
62 |
.tab-label:has(.tab-switch:checked) {
|
63 |
background: #fff;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
}
|
65 |
.template{
|
66 |
display: none;
|
static/favicon.png
CHANGED
static/img/icon-t.png
ADDED
static/img/icon-w.png
ADDED
static/img/menu.svg
CHANGED
static/js/SecurityHandler.js
ADDED
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class Security{
|
2 |
+
keypair;
|
3 |
+
privateKey;
|
4 |
+
publicKey;
|
5 |
+
fingerprint;
|
6 |
+
ready = false;
|
7 |
+
|
8 |
+
async start(){
|
9 |
+
let keypair = await window.crypto.subtle.generateKey(
|
10 |
+
{
|
11 |
+
name: "RSA-PSS",
|
12 |
+
modulusLength: 1024,
|
13 |
+
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
14 |
+
hash: {name: "SHA-256"},
|
15 |
+
},
|
16 |
+
false,
|
17 |
+
["sign"]
|
18 |
+
)
|
19 |
+
this.keypair = keypair
|
20 |
+
this.privateKey = keypair.privateKey
|
21 |
+
this.publicKey = keypair.publicKey
|
22 |
+
|
23 |
+
let exportKey_ba = await window.crypto.subtle.exportKey(
|
24 |
+
"spki",
|
25 |
+
keypair.publicKey
|
26 |
+
)
|
27 |
+
|
28 |
+
let exportKey_u8 = new Uint8Array(exportKey_ba);
|
29 |
+
this.publicKey = "-----BEGIN PUBLIC KEY-----\n"+btoa(String.fromCharCode(...exportKey_u8))+"\n-----END PUBLIC KEY-----"
|
30 |
+
|
31 |
+
this.fingerprint = await this.generateFingerprint()
|
32 |
+
|
33 |
+
this.ready = true
|
34 |
+
return true
|
35 |
+
|
36 |
+
}
|
37 |
+
|
38 |
+
async sign(data){
|
39 |
+
let encoder = new TextEncoder()
|
40 |
+
let sign_ba = await window.crypto.subtle.sign(
|
41 |
+
{
|
42 |
+
name: "RSA-PSS",
|
43 |
+
saltLength: 32,
|
44 |
+
},
|
45 |
+
this.keypair.privateKey,
|
46 |
+
encoder.encode(data)
|
47 |
+
)
|
48 |
+
let sign_u8 = new Uint8Array(sign_ba);
|
49 |
+
return btoa(String.fromCharCode(...sign_u8))
|
50 |
+
}
|
51 |
+
|
52 |
+
async generateFingerprint(){
|
53 |
+
const fpPromise = await import('/static/js/fingerprintv4.js').then(FingerprintJS => FingerprintJS.load())
|
54 |
+
const fp = await fpPromise.get()
|
55 |
+
return fp.visitorId
|
56 |
+
}
|
57 |
+
|
58 |
+
|
59 |
+
}
|
static/js/chatHandler.js
CHANGED
@@ -1,184 +1,381 @@
|
|
1 |
class ChatGPT{
|
2 |
-
definicion = "Te llamas Chatsito, eres un asistente de apoyo a los amigos de MIA, " +
|
3 |
-
"tu objetivo principal es responder preguntas de manera puntual y objetiva " +
|
4 |
-
"a tu interlocutor.\n" +
|
5 |
-
"Responde de manera amistosa con en el texto más corto y objetivo posible.\n" +
|
6 |
-
"Knowledge cutoff: 2021-09-01\nCurrent date: {date}";
|
7 |
-
constructor(token){
|
8 |
-
|
9 |
-
let fecha = new Date().toJSON().slice(0, 10);
|
10 |
-
this.definicion = this.definicion.replace("{date}", fecha)
|
11 |
-
this.definicion = {role: "system", content: this.definicion, tokens: 100};
|
12 |
-
this.cargarHistorial()
|
13 |
|
14 |
-
|
15 |
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
$(document).on("chat:enviar", (event, params) => {
|
21 |
-
this.reintentos = 0;
|
22 |
-
this.enviar(params.mensaje, params.ctx);
|
23 |
-
});
|
24 |
|
25 |
-
|
26 |
-
|
27 |
-
$(document).on("chat:crear", ()=>this.crearChat())
|
28 |
|
29 |
-
|
30 |
-
|
31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
}
|
33 |
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
frequency_penalty: 0.0,
|
47 |
-
presence_penalty: 0.0
|
48 |
-
};
|
49 |
-
}
|
50 |
|
51 |
-
this.
|
52 |
-
for(let conversacion of this.conversaciones){
|
53 |
-
this.wHand.push(new WindowHandler(conversacion, this.wHand.length));
|
54 |
-
}
|
55 |
-
$(document).trigger("chat:creado");
|
56 |
|
|
|
57 |
|
58 |
|
59 |
}
|
60 |
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
}
|
70 |
-
}
|
71 |
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
|
79 |
}
|
80 |
|
81 |
-
|
82 |
-
this
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
this.crearChat()
|
|
|
97 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
}
|
100 |
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
this.
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
111 |
}
|
112 |
|
113 |
-
enviar(
|
114 |
-
|
|
|
|
|
115 |
let tempMensajes = [];
|
116 |
for(let actMsg of conversacion){
|
117 |
tempMensajes.push({role: actMsg.role, content: actMsg.content})
|
118 |
}
|
119 |
|
120 |
-
|
121 |
-
|
122 |
-
|
|
|
123 |
let self = this;
|
124 |
|
|
|
125 |
const consume = responseReader => {
|
|
|
|
|
126 |
return responseReader.read().then(result => {
|
|
|
|
|
127 |
if (result.done) { return; }
|
128 |
|
129 |
-
|
130 |
const chunk = result.value;
|
131 |
let text = new TextDecoder("utf-8").decode(chunk)
|
132 |
|
|
|
133 |
let responses = JSON.parse('[' + text.replace(/\}\{/g, '},{') + ']')
|
|
|
|
|
134 |
for(let response of responses){
|
135 |
switch(response.comando){
|
|
|
136 |
case "token":
|
137 |
self.token = response.token;
|
138 |
break;
|
|
|
|
|
|
|
|
|
|
|
139 |
case "status":
|
140 |
-
ctx.
|
141 |
break;
|
|
|
142 |
case "mensaje":
|
143 |
-
|
144 |
-
localStorage.setItem("conversaciones", JSON.stringify(self.conversaciones))
|
145 |
-
ctx.trigger("precarga:mensaje", response.mensaje.content);
|
146 |
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
147 |
default:
|
148 |
-
console.log("???")
|
149 |
}
|
150 |
}
|
|
|
|
|
151 |
return consume(responseReader);
|
152 |
}).catch(err =>{
|
|
|
153 |
console.log('algo paso', err)
|
154 |
});
|
155 |
}
|
156 |
|
157 |
-
//
|
158 |
fetch(this.endpointChat, {
|
159 |
method: "POST",
|
160 |
body: JSON.stringify({
|
161 |
messages: tempMensajes,
|
162 |
-
|
163 |
-
|
164 |
}),
|
|
|
|
|
|
|
|
|
165 |
timeout: 60000,
|
166 |
dataType: "json"
|
167 |
}).then(response => {
|
168 |
if(response.status != 200){
|
169 |
-
ctx.
|
170 |
console.log("Error: ", response)
|
171 |
return
|
172 |
}
|
173 |
-
ctx.trigger("precarga:iniciada");
|
174 |
return consume(response.body.getReader());
|
175 |
-
})
|
176 |
-
|
177 |
console.log('Solicitud fallida', err)
|
178 |
-
ctx.
|
|
|
179 |
});
|
180 |
|
181 |
}
|
182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
183 |
|
184 |
}
|
|
|
1 |
class ChatGPT{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
+
/* Eventos generados
|
4 |
|
5 |
+
**********************/
|
6 |
+
/* Eventos escuchados
|
7 |
+
- chat:enviar
|
8 |
+
- chat:eliminar
|
|
|
|
|
|
|
|
|
9 |
|
10 |
+
**********************/
|
|
|
|
|
11 |
|
12 |
+
|
13 |
+
definicion ={
|
14 |
+
conversacion: [],
|
15 |
+
usedTokens: 0
|
16 |
+
};
|
17 |
+
|
18 |
+
config = {
|
19 |
+
temperature: 0.5,
|
20 |
+
frequency_penalty: 0.0,
|
21 |
+
presence_penalty: 0.0,
|
22 |
+
useTool: true,
|
23 |
+
assistant: "clasico",
|
24 |
+
assistantPrompt: ""
|
25 |
}
|
26 |
|
27 |
+
tokens = {
|
28 |
+
month: 0,
|
29 |
+
total: 0
|
30 |
+
}
|
31 |
+
|
32 |
+
endpointChat = "/chat";
|
33 |
+
challenge = null;
|
34 |
+
windowHandlers = {}
|
35 |
+
|
36 |
+
constructor(secHand){
|
37 |
+
// Token JWT de ejecución
|
38 |
+
this.secHand = secHand;
|
|
|
|
|
|
|
|
|
39 |
|
40 |
+
this.obtenerToken();
|
|
|
|
|
|
|
|
|
41 |
|
42 |
+
this.cargarEventos();
|
43 |
|
44 |
|
45 |
}
|
46 |
|
47 |
+
obtenerToken(){
|
48 |
+
$.ajax({
|
49 |
+
method: "POST",
|
50 |
+
url: "/getToken",
|
51 |
+
headers: {
|
52 |
+
"Autorization": "Bearer " + this.token,
|
53 |
+
'Content-Type': 'application/json',
|
54 |
+
},
|
55 |
+
data: JSON.stringify({
|
56 |
+
fingerprint: this.secHand.fingerprint,
|
57 |
+
public_key: this.secHand.publicKey
|
58 |
+
}),
|
59 |
+
timeout: 5000,
|
60 |
+
dataType: "json"
|
61 |
+
}).done((data) => {
|
62 |
+
|
63 |
+
if(data.redirect){
|
64 |
+
$("#inserted")[0].showModal()
|
65 |
+
$("#inserted").html("<iframe src='"+data.redirect+"'>")
|
66 |
+
$("#inserted").on("close", (event) =>{
|
67 |
+
document.location.reload()
|
68 |
+
})
|
69 |
+
|
70 |
+
return
|
71 |
}
|
|
|
72 |
|
73 |
+
this.secHand.sign(data.challenge).then(
|
74 |
+
(result) => {
|
75 |
+
this.challenge = result
|
76 |
+
this.cargarConfigs(false, true);
|
77 |
+
})
|
78 |
+
this.token = data.token
|
79 |
+
}).fail((data => {
|
80 |
+
$("#inserted")[0].showModal()
|
81 |
+
$("#inserted").html("<iframe src='/login'>")
|
82 |
+
$("#inserted").on("close", (event) =>{
|
83 |
+
document.location.reload()
|
84 |
+
})
|
85 |
+
$("#inserted iframe").on("load", (event) =>{
|
86 |
+
let location = event.target.contentDocument.location.pathname
|
87 |
+
if(location == "/login"){
|
88 |
+
$("#inserted iframe")[0].contentWindow.data = {
|
89 |
+
fp: this.secHand.fingerprint,
|
90 |
+
pk: this.secHand.publicKey
|
91 |
+
}
|
92 |
+
}
|
93 |
+
if(location == "/"){
|
94 |
+
$("#inserted").html("")
|
95 |
+
$("#inserted")[0].close()
|
96 |
+
|
97 |
+
}
|
98 |
+
|
99 |
+
|
100 |
+
})
|
101 |
+
}))
|
102 |
+
|
103 |
|
104 |
}
|
105 |
|
106 |
+
cargarConfigs(abrirMenu, starting){
|
107 |
+
self = this
|
108 |
+
$.ajax({
|
109 |
+
method: "POST",
|
110 |
+
url: "/getConfigs",
|
111 |
+
headers: {
|
112 |
+
"Autorization": "Bearer " + this.token,
|
113 |
+
'Content-Type': 'application/json',
|
114 |
+
},
|
115 |
+
data:JSON.stringify({
|
116 |
+
challenge: this.challenge,
|
117 |
+
fingerprint: this.secHand.fingerprint
|
118 |
+
}),
|
119 |
+
timeout: 5000,
|
120 |
+
dataType: "json"
|
121 |
+
}).done((data) => {
|
122 |
+
let challenge = data["challenge"];
|
123 |
+
delete data["challenge"];
|
124 |
+
$("#assistant option[value='"+ data.assistant +"']")[0].selected=true
|
125 |
+
this.secHand.sign(challenge).then((result) => {this.challenge = result;})
|
126 |
+
this.config = data
|
127 |
+
|
128 |
+
for(let confEl in data){
|
129 |
+
if(typeof(data[confEl]) == "boolean"){
|
130 |
+
$("#"+confEl).prop("checked", data[confEl])
|
131 |
+
}else{
|
132 |
+
$("#"+confEl).val(data[confEl])
|
133 |
+
}
|
134 |
+
}
|
135 |
+
$(".range input").each(recalculateRanges)
|
136 |
+
$(".range input").on("input", recalculateRanges)
|
137 |
+
$(".item.parent input").on("change", function(){
|
138 |
+
self.config[this.id] = this.checked
|
139 |
+
if(!this.checked){
|
140 |
+
$(".item."+this.id+" input").prop("disabled", true)
|
141 |
+
}else{
|
142 |
+
$(".item."+this.id+" input").prop("disabled", false)
|
143 |
+
}
|
144 |
+
})
|
145 |
+
$(".range select").on("change", function(){
|
146 |
+
self.config[this.id] = this.value
|
147 |
+
})
|
148 |
+
$("#tokensUsedMonth").text(data.tokens.month)
|
149 |
+
$("#tokensUsedTotal").text(data.tokens.total)
|
150 |
+
|
151 |
+
|
152 |
+
if(abrirMenu) $("#menu")[0].showModal()
|
153 |
+
|
154 |
+
|
155 |
+
function recalculateRanges(){
|
156 |
+
let value = (Math.round(this.value * 100) / 100).toFixed(2)
|
157 |
+
$(this).parent().find("span").text(value);
|
158 |
+
self.config[this.id] = parseFloat(value)
|
159 |
+
}
|
160 |
+
$(".item.parent input").trigger("change")
|
161 |
+
|
162 |
+
if(starting){this.cargarChats()}
|
163 |
+
})
|
164 |
+
.fail(() => {
|
165 |
+
document.location.href = "/";
|
166 |
+
})
|
167 |
+
}
|
168 |
+
|
169 |
+
cargarEventos(){
|
170 |
+
$(document).on("chat:enviar", (event, params) => {
|
171 |
+
// Al enviar un mensaje, reintentos vuelve a 0
|
172 |
+
this.reintentos = 0;
|
173 |
+
this.enviar(params.ctx, params.ctx.conversacion);
|
174 |
+
});
|
175 |
+
$(document).on("chat:salvar", (event, params) => this.salvarChats(params.index, params.conversacion))
|
176 |
+
$(document).on("chat:eliminar", (event, params) => this.eliminarChat(params.ctx, params.index))
|
177 |
+
$(document).on("mostrar:opciones", () => this.mostrarOpciones())
|
178 |
+
$("#nuevoChat").on("click", () => this.crearChat())
|
179 |
+
$("#menu .saveButton").on("click", () => this.salvarConfigs())
|
180 |
+
|
181 |
+
}
|
182 |
+
|
183 |
+
cargarChats(){
|
184 |
+
let conversaciones = {}
|
185 |
+
try{conversaciones = JSON.parse(localStorage.getItem("conversaciones"))||{}}catch{}
|
186 |
+
|
187 |
+
if(!conversaciones || !Object.keys(conversaciones).length){
|
188 |
this.crearChat()
|
189 |
+
return
|
190 |
}
|
191 |
+
for(let key in conversaciones){
|
192 |
+
if(!conversaciones[key]){
|
193 |
+
continue
|
194 |
+
}
|
195 |
+
if(Object.keys(conversaciones[key]).indexOf('conversacion')!=-1){
|
196 |
+
this.crearChat(key, conversaciones[key].conversacion)
|
197 |
+
}else{
|
198 |
+
this.crearChat(key, conversaciones[key])
|
199 |
+
}
|
200 |
+
}
|
201 |
+
localStorage.setItem("conversaciones", JSON.stringify(conversaciones))
|
202 |
+
|
203 |
+
}
|
204 |
+
|
205 |
+
|
206 |
|
207 |
+
salvarConfigs(){
|
208 |
+
$.ajax({
|
209 |
+
method: "POST",
|
210 |
+
url: "/setConfigs",
|
211 |
+
headers: {
|
212 |
+
"Autorization": "Bearer " + this.token,
|
213 |
+
'Content-Type': 'application/json',
|
214 |
+
},
|
215 |
+
data: JSON.stringify(
|
216 |
+
{...this.config,
|
217 |
+
fingerprint: this.secHand.fingerprint,
|
218 |
+
challenge: this.challenge,
|
219 |
+
}),
|
220 |
+
timeout: 5000,
|
221 |
+
dataType: "json"
|
222 |
+
}).done((data) => {
|
223 |
+
this.secHand.sign(data.challenge).then((result) => {this.challenge = result})
|
224 |
+
this.token = data.token
|
225 |
+
this.config.assistantPrompt = data.assistantPrompt
|
226 |
+
}).fail(() => {
|
227 |
+
document.location.href = "/";
|
228 |
+
})
|
229 |
}
|
230 |
|
231 |
+
crearChat(index, conversacion){
|
232 |
+
// Se crea el nuevo manejador de ventana
|
233 |
+
let uuid = this.generateRandID()
|
234 |
+
this.windowHandlers[index||uuid] = new WindowHandler(
|
235 |
+
conversacion||[],
|
236 |
+
index||uuid,
|
237 |
+
this
|
238 |
+
);
|
239 |
+
}
|
240 |
+
|
241 |
+
eliminarChat(ctx, index){
|
242 |
+
// Elimina el elemento de la lista de conversas y handler de ventanas
|
243 |
+
delete this.windowHandlers[index]
|
244 |
+
let conversaciones = {}
|
245 |
+
try{conversaciones = JSON.parse(localStorage.getItem("conversaciones"))||{}}catch{}
|
246 |
+
if(index in conversaciones){delete conversaciones[index]}
|
247 |
+
localStorage.setItem("conversaciones", JSON.stringify(conversaciones))
|
248 |
|
249 |
+
// Renumera las etiquetas, y selecciona la primera
|
250 |
+
let labels = $(".tab-label")
|
251 |
+
$(labels[0]).click();
|
252 |
+
|
253 |
+
// Si no quedaron chats, crea uno vacio
|
254 |
+
if(Object.keys(this.windowHandlers).length==0){
|
255 |
+
this.crearChat()
|
256 |
+
}
|
257 |
}
|
258 |
|
259 |
+
enviar(ctx, conversacion){
|
260 |
+
// Envio de mensaje y manejo de comandos async
|
261 |
+
|
262 |
+
// Crea un espacio temporal para almacenar los mensajes con el formato correcto
|
263 |
let tempMensajes = [];
|
264 |
for(let actMsg of conversacion){
|
265 |
tempMensajes.push({role: actMsg.role, content: actMsg.content})
|
266 |
}
|
267 |
|
268 |
+
// Se anuncia la precarga
|
269 |
+
ctx.respuestaInicio()
|
270 |
+
|
271 |
+
// Se almacena el contexto this
|
272 |
let self = this;
|
273 |
|
274 |
+
// Consumo de mensajes asincronos
|
275 |
const consume = responseReader => {
|
276 |
+
|
277 |
+
// Se lee la respuesta
|
278 |
return responseReader.read().then(result => {
|
279 |
+
|
280 |
+
//si finalizó el mensaje, se termina el proceso
|
281 |
if (result.done) { return; }
|
282 |
|
283 |
+
// Se obtiene y decodifica el segmento
|
284 |
const chunk = result.value;
|
285 |
let text = new TextDecoder("utf-8").decode(chunk)
|
286 |
|
287 |
+
// Se obtienen los mensajes y separan si son varios json juntos
|
288 |
let responses = JSON.parse('[' + text.replace(/\}\{/g, '},{') + ']')
|
289 |
+
|
290 |
+
// Por cada mensaje conseguido validamos el caso de accion
|
291 |
for(let response of responses){
|
292 |
switch(response.comando){
|
293 |
+
// Se carga el nuevo token
|
294 |
case "token":
|
295 |
self.token = response.token;
|
296 |
break;
|
297 |
+
// Se carga el nuevo challenge
|
298 |
+
case "challenge":
|
299 |
+
this.secHand.sign(response.challenge).then((result) => {self.challenge = result})
|
300 |
+
break;
|
301 |
+
// Status del comportamiento
|
302 |
case "status":
|
303 |
+
ctx.respuestaStatus(response.status.mensaje, response.status.modo)
|
304 |
break;
|
305 |
+
// Mensaje a mostrar
|
306 |
case "mensaje":
|
307 |
+
ctx.respuestaMensaje(response.mensaje)
|
|
|
|
|
308 |
break;
|
309 |
+
// // Es una función
|
310 |
+
// case "function":
|
311 |
+
// conversacion.push(response.function);
|
312 |
+
// localStorage.setItem("conversaciones", JSON.stringify(self.conversaciones))
|
313 |
+
// break;
|
314 |
+
// Algo falló
|
315 |
default:
|
316 |
+
console.log("???", response)
|
317 |
}
|
318 |
}
|
319 |
+
|
320 |
+
// Se consume la respuesta para continuar el proceso
|
321 |
return consume(responseReader);
|
322 |
}).catch(err =>{
|
323 |
+
// Error
|
324 |
console.log('algo paso', err)
|
325 |
});
|
326 |
}
|
327 |
|
328 |
+
// Se ejecuta el request
|
329 |
fetch(this.endpointChat, {
|
330 |
method: "POST",
|
331 |
body: JSON.stringify({
|
332 |
messages: tempMensajes,
|
333 |
+
challenge: this.challenge,
|
334 |
+
fingerprint: this.secHand.fingerprint
|
335 |
}),
|
336 |
+
headers: {
|
337 |
+
"Autorization": "Bearer " + this.token,
|
338 |
+
'Content-Type': 'application/json',
|
339 |
+
},
|
340 |
timeout: 60000,
|
341 |
dataType: "json"
|
342 |
}).then(response => {
|
343 |
if(response.status != 200){
|
344 |
+
ctx.respuestaError(response)
|
345 |
console.log("Error: ", response)
|
346 |
return
|
347 |
}
|
|
|
348 |
return consume(response.body.getReader());
|
349 |
+
}).catch(err =>{
|
350 |
+
// Error
|
351 |
console.log('Solicitud fallida', err)
|
352 |
+
ctx.respuestaError(response)
|
353 |
+
document.location.href = "/";
|
354 |
});
|
355 |
|
356 |
}
|
357 |
|
358 |
+
salvarChats(index, conversacion){
|
359 |
+
let conversaciones = {}
|
360 |
+
try{conversaciones = JSON.parse(localStorage.getItem("conversaciones"))||{}}catch{}
|
361 |
+
let temp = {}
|
362 |
+
for (let i in conversaciones){
|
363 |
+
temp[i]=conversaciones[i]
|
364 |
+
}
|
365 |
+
temp[index] = conversacion
|
366 |
+
localStorage.setItem("conversaciones", JSON.stringify(temp))
|
367 |
+
}
|
368 |
+
|
369 |
+
|
370 |
+
|
371 |
+
mostrarOpciones(){
|
372 |
+
this.cargarConfigs(true);
|
373 |
+
|
374 |
+
}
|
375 |
+
|
376 |
+
generateRandID(){
|
377 |
+
return btoa((Math.random()*100**8)).replaceAll("=","").split("").reverse().join("");
|
378 |
+
}
|
379 |
+
|
380 |
|
381 |
}
|
static/js/fingerprintv4.js
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* FingerprintJS v4.2.1 - Copyright (c) FingerprintJS, Inc, 2023 (https://fingerprint.com)
|
3 |
+
*
|
4 |
+
* Licensed under Business Source License 1.1 https://mariadb.com/bsl11/
|
5 |
+
* Licensor: FingerprintJS, Inc.
|
6 |
+
* Licensed Work: FingerprintJS browser fingerprinting library
|
7 |
+
* Additional Use Grant: None
|
8 |
+
* Change Date: Four years from first release for the specific version.
|
9 |
+
* Change License: MIT, text at https://opensource.org/license/mit/ with the following copyright notice:
|
10 |
+
* Copyright 2015-present FingerprintJS, Inc.
|
11 |
+
*/
|
12 |
+
var e=function(){return e=Object.assign||function(e){for(var n,t=1,r=arguments.length;t<r;t++)for(var o in n=arguments[t])Object.prototype.hasOwnProperty.call(n,o)&&(e[o]=n[o]);return e},e.apply(this,arguments)};function n(e,n,t,r){return new(t||(t=Promise))((function(o,i){function a(e){try{u(r.next(e))}catch(n){i(n)}}function c(e){try{u(r.throw(e))}catch(n){i(n)}}function u(e){var n;e.done?o(e.value):(n=e.value,n instanceof t?n:new t((function(e){e(n)}))).then(a,c)}u((r=r.apply(e,n||[])).next())}))}function t(e,n){var t,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:c(0),throw:c(1),return:c(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function c(c){return function(u){return function(c){if(t)throw new TypeError("Generator is already executing.");for(;i&&(i=0,c[0]&&(a=0)),a;)try{if(t=1,r&&(o=2&c[0]?r.return:c[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,c[1])).done)return o;switch(r=0,o&&(c=[2&c[0],o.value]),c[0]){case 0:case 1:o=c;break;case 4:return a.label++,{value:c[1],done:!1};case 5:a.label++,r=c[1],c=[0];continue;case 7:c=a.ops.pop(),a.trys.pop();continue;default:if(!(o=a.trys,(o=o.length>0&&o[o.length-1])||6!==c[0]&&2!==c[0])){a=0;continue}if(3===c[0]&&(!o||c[1]>o[0]&&c[1]<o[3])){a.label=c[1];break}if(6===c[0]&&a.label<o[1]){a.label=o[1],o=c;break}if(o&&a.label<o[2]){a.label=o[2],a.ops.push(c);break}o[2]&&a.ops.pop(),a.trys.pop();continue}c=n.call(e,a)}catch(u){c=[6,u],r=0}finally{t=o=0}if(5&c[0])throw c[1];return{value:c[0]?c[1]:void 0,done:!0}}([c,u])}}}function r(e,n,t){if(t||2===arguments.length)for(var r,o=0,i=n.length;o<i;o++)!r&&o in n||(r||(r=Array.prototype.slice.call(n,0,o)),r[o]=n[o]);return e.concat(r||Array.prototype.slice.call(n))}function o(e,n){return new Promise((function(t){return setTimeout(t,e,n)}))}function i(){return o(0)}function a(e){return!!e&&"function"==typeof e.then}function c(e,n){try{var t=e();a(t)?t.then((function(e){return n(!0,e)}),(function(e){return n(!1,e)})):n(!0,t)}catch(r){n(!1,r)}}function u(e,r,i){return void 0===i&&(i=16),n(this,void 0,void 0,(function(){var n,a,c,u;return t(this,(function(t){switch(t.label){case 0:n=Array(e.length),a=Date.now(),c=0,t.label=1;case 1:return c<e.length?(n[c]=r(e[c],c),(u=Date.now())>=a+i?(a=u,[4,o(0)]):[3,3]):[3,4];case 2:t.sent(),t.label=3;case 3:return++c,[3,1];case 4:return[2,n]}}))}))}function l(e){e.then(void 0,(function(){}))}function s(e){return parseInt(e)}function d(e){return parseFloat(e)}function f(e,n){return"number"==typeof e&&isNaN(e)?n:e}function m(e){return e.reduce((function(e,n){return e+(n?1:0)}),0)}function v(e,n){if(void 0===n&&(n=1),Math.abs(n)>=1)return Math.round(e/n)*n;var t=1/n;return Math.round(e*t)/t}function h(e,n){var t=e[0]>>>16,r=65535&e[0],o=e[1]>>>16,i=65535&e[1],a=n[0]>>>16,c=65535&n[0],u=n[1]>>>16,l=0,s=0,d=0,f=0;d+=(f+=i+(65535&n[1]))>>>16,f&=65535,s+=(d+=o+u)>>>16,d&=65535,l+=(s+=r+c)>>>16,s&=65535,l+=t+a,l&=65535,e[0]=l<<16|s,e[1]=d<<16|f}function p(e,n){var t=e[0]>>>16,r=65535&e[0],o=e[1]>>>16,i=65535&e[1],a=n[0]>>>16,c=65535&n[0],u=n[1]>>>16,l=65535&n[1],s=0,d=0,f=0,m=0;f+=(m+=i*l)>>>16,m&=65535,d+=(f+=o*l)>>>16,f&=65535,d+=(f+=i*u)>>>16,f&=65535,s+=(d+=r*l)>>>16,d&=65535,s+=(d+=o*u)>>>16,d&=65535,s+=(d+=i*c)>>>16,d&=65535,s+=t*l+r*u+o*c+i*a,s&=65535,e[0]=s<<16|d,e[1]=f<<16|m}function b(e,n){var t=e[0];32===(n%=64)?(e[0]=e[1],e[1]=t):n<32?(e[0]=t<<n|e[1]>>>32-n,e[1]=e[1]<<n|t>>>32-n):(n-=32,e[0]=e[1]<<n|t>>>32-n,e[1]=t<<n|e[1]>>>32-n)}function y(e,n){0!==(n%=64)&&(n<32?(e[0]=e[1]>>>32-n,e[1]=e[1]<<n):(e[0]=e[1]<<n-32,e[1]=0))}function g(e,n){e[0]^=n[0],e[1]^=n[1]}var w=[4283543511,3981806797],L=[3301882366,444984403];function k(e){var n=[0,e[0]>>>1];g(e,n),p(e,w),n[1]=e[0]>>>1,g(e,n),p(e,L),n[1]=e[0]>>>1,g(e,n)}var V=[2277735313,289559509],S=[1291169091,658871167],W=[0,5],Z=[0,1390208809],x=[0,944331445];function M(e,n){var t=function(e){for(var n=new Uint8Array(e.length),t=0;t<e.length;t++){var r=e.charCodeAt(t);if(r>127)return(new TextEncoder).encode(e);n[t]=r}return n}(e);n=n||0;var r,o=[0,t.length],i=o[1]%16,a=o[1]-i,c=[0,n],u=[0,n],l=[0,0],s=[0,0];for(r=0;r<a;r+=16)l[0]=t[r+4]|t[r+5]<<8|t[r+6]<<16|t[r+7]<<24,l[1]=t[r]|t[r+1]<<8|t[r+2]<<16|t[r+3]<<24,s[0]=t[r+12]|t[r+13]<<8|t[r+14]<<16|t[r+15]<<24,s[1]=t[r+8]|t[r+9]<<8|t[r+10]<<16|t[r+11]<<24,p(l,V),b(l,31),p(l,S),g(c,l),b(c,27),h(c,u),p(c,W),h(c,Z),p(s,S),b(s,33),p(s,V),g(u,s),b(u,31),h(u,c),p(u,W),h(u,x);l[0]=0,l[1]=0,s[0]=0,s[1]=0;var d=[0,0];switch(i){case 15:d[1]=t[r+14],y(d,48),g(s,d);case 14:d[1]=t[r+13],y(d,40),g(s,d);case 13:d[1]=t[r+12],y(d,32),g(s,d);case 12:d[1]=t[r+11],y(d,24),g(s,d);case 11:d[1]=t[r+10],y(d,16),g(s,d);case 10:d[1]=t[r+9],y(d,8),g(s,d);case 9:d[1]=t[r+8],g(s,d),p(s,S),b(s,33),p(s,V),g(u,s);case 8:d[1]=t[r+7],y(d,56),g(l,d);case 7:d[1]=t[r+6],y(d,48),g(l,d);case 6:d[1]=t[r+5],y(d,40),g(l,d);case 5:d[1]=t[r+4],y(d,32),g(l,d);case 4:d[1]=t[r+3],y(d,24),g(l,d);case 3:d[1]=t[r+2],y(d,16),g(l,d);case 2:d[1]=t[r+1],y(d,8),g(l,d);case 1:d[1]=t[r],g(l,d),p(l,V),b(l,31),p(l,S),g(c,l)}return g(c,o),g(u,o),h(c,u),h(u,c),k(c),k(u),h(c,u),h(u,c),("00000000"+(c[0]>>>0).toString(16)).slice(-8)+("00000000"+(c[1]>>>0).toString(16)).slice(-8)+("00000000"+(u[0]>>>0).toString(16)).slice(-8)+("00000000"+(u[1]>>>0).toString(16)).slice(-8)}function R(e){return"function"!=typeof e}function F(e,r,o){var i=Object.keys(e).filter((function(e){return!function(e,n){for(var t=0,r=e.length;t<r;++t)if(e[t]===n)return!0;return!1}(o,e)})),a=u(i,(function(n){return function(e,n){var t=new Promise((function(t){var r=Date.now();c(e.bind(null,n),(function(){for(var e=[],n=0;n<arguments.length;n++)e[n]=arguments[n];var o=Date.now()-r;if(!e[0])return t((function(){return{error:e[1],duration:o}}));var i=e[1];if(R(i))return t((function(){return{value:i,duration:o}}));t((function(){return new Promise((function(e){var n=Date.now();c(i,(function(){for(var t=[],r=0;r<arguments.length;r++)t[r]=arguments[r];var i=o+Date.now()-n;if(!t[0])return e({error:t[1],duration:i});e({value:t[1],duration:i})}))}))}))}))}));return l(t),function(){return t.then((function(e){return e()}))}}(e[n],r)}));return l(a),function(){return n(this,void 0,void 0,(function(){var e,n,r,o;return t(this,(function(t){switch(t.label){case 0:return[4,a];case 1:return[4,u(t.sent(),(function(e){var n=e();return l(n),n}))];case 2:return e=t.sent(),[4,Promise.all(e)];case 3:for(n=t.sent(),r={},o=0;o<i.length;++o)r[i[o]]=n[o];return[2,r]}}))}))}}function G(e,n){var t=function(e){return R(e)?n(e):function(){var t=e();return a(t)?t.then(n):n(t)}};return function(n){var r=e(n);return a(r)?r.then(t):t(r)}}function I(){var e=window,n=navigator;return m(["MSCSSMatrix"in e,"msSetImmediate"in e,"msIndexedDB"in e,"msMaxTouchPoints"in n,"msPointerEnabled"in n])>=4}function Y(){var e=window,n=navigator;return m(["msWriteProfilerMark"in e,"MSStream"in e,"msLaunchUri"in n,"msSaveBlob"in n])>=3&&!I()}function j(){var e=window,n=navigator;return m(["webkitPersistentStorage"in n,"webkitTemporaryStorage"in n,0===n.vendor.indexOf("Google"),"webkitResolveLocalFileSystemURL"in e,"BatteryManager"in e,"webkitMediaStream"in e,"webkitSpeechGrammar"in e])>=5}function X(){var e=window,n=navigator;return m(["ApplePayError"in e,"CSSPrimitiveValue"in e,"Counter"in e,0===n.vendor.indexOf("Apple"),"getStorageUpdates"in n,"WebKitMediaKeys"in e])>=4}function P(){var e=window,n=e.HTMLElement,t=e.Document;return m(["safari"in e,!("ongestureend"in e),!("TouchEvent"in e),!("orientation"in e),n&&!("autocapitalize"in n.prototype),t&&"pointerLockElement"in t.prototype])>=4}function E(){var e,n=window;return e=n.print,!!/^function\s.*?\{\s*\[native code]\s*}$/.test(String(e))&&m(["[object WebPageNamespace]"===String(n.browser),"MicrodataExtractor"in n])>=1}function C(){var e,n,t=window;return m(["buildID"in navigator,"MozAppearance"in(null!==(n=null===(e=document.documentElement)||void 0===e?void 0:e.style)&&void 0!==n?n:{}),"onmozfullscreenchange"in t,"mozInnerScreenX"in t,"CSSMozDocumentRule"in t,"CanvasCaptureMediaStream"in t])>=4}function H(){var e=window,n=navigator,t=e.CSS,r=e.HTMLButtonElement;return m([!("getStorageUpdates"in n),r&&"popover"in r.prototype,"CSSCounterStyleRule"in e,t.supports("font-size-adjust: ex-height 0.5"),t.supports("text-transform: full-width")])>=4}function A(){var e=document;return e.fullscreenElement||e.msFullscreenElement||e.mozFullScreenElement||e.webkitFullscreenElement||null}function N(){var e=j(),n=C(),t=window,r=navigator,o="connection";return e?m([!("SharedWorker"in t),r[o]&&"ontypechange"in r[o],!("sinkId"in new window.Audio)])>=2:!!n&&m(["onorientationchange"in t,"orientation"in t,/android/i.test(navigator.appVersion)])>=2}function J(e,r,i){var a,c,u;return void 0===i&&(i=50),n(this,void 0,void 0,(function(){var n,l;return t(this,(function(t){switch(t.label){case 0:n=document,t.label=1;case 1:return n.body?[3,3]:[4,o(i)];case 2:return t.sent(),[3,1];case 3:l=n.createElement("iframe"),t.label=4;case 4:return t.trys.push([4,,10,11]),[4,new Promise((function(e,t){var o=!1,i=function(){o=!0,e()};l.onload=i,l.onerror=function(e){o=!0,t(e)};var a=l.style;a.setProperty("display","block","important"),a.position="absolute",a.top="0",a.left="0",a.visibility="hidden",r&&"srcdoc"in l?l.srcdoc=r:l.src="about:blank",n.body.appendChild(l);var c=function(){var e,n;o||("complete"===(null===(n=null===(e=l.contentWindow)||void 0===e?void 0:e.document)||void 0===n?void 0:n.readyState)?i():setTimeout(c,10))};c()}))];case 5:t.sent(),t.label=6;case 6:return(null===(c=null===(a=l.contentWindow)||void 0===a?void 0:a.document)||void 0===c?void 0:c.body)?[3,8]:[4,o(i)];case 7:return t.sent(),[3,6];case 8:return[4,e(l,l.contentWindow)];case 9:return[2,t.sent()];case 10:return null===(u=l.parentNode)||void 0===u||u.removeChild(l),[7];case 11:return[2]}}))}))}function T(e){for(var n=function(e){for(var n,t,r="Unexpected syntax '".concat(e,"'"),o=/^\s*([a-z-]*)(.*)$/i.exec(e),i=o[1]||void 0,a={},c=/([.:#][\w-]+|\[.+?\])/gi,u=function(e,n){a[e]=a[e]||[],a[e].push(n)};;){var l=c.exec(o[2]);if(!l)break;var s=l[0];switch(s[0]){case".":u("class",s.slice(1));break;case"#":u("id",s.slice(1));break;case"[":var d=/^\[([\w-]+)([~|^$*]?=("(.*?)"|([\w-]+)))?(\s+[is])?\]$/.exec(s);if(!d)throw new Error(r);u(d[1],null!==(t=null!==(n=d[4])&&void 0!==n?n:d[5])&&void 0!==t?t:"");break;default:throw new Error(r)}}return[i,a]}(e),t=n[0],r=n[1],o=document.createElement(null!=t?t:"div"),i=0,a=Object.keys(r);i<a.length;i++){var c=a[i],u=r[c].join(" ");"style"===c?D(o.style,u):o.setAttribute(c,u)}return o}function D(e,n){for(var t=0,r=n.split(";");t<r.length;t++){var o=r[t],i=/^\s*([\w-]+)\s*:\s*(.+?)(\s*!([\w-]+))?\s*$/.exec(o);if(i){var a=i[1],c=i[2],u=i[4];e.setProperty(a,c,u||"")}}}function _(){return n(this,void 0,void 0,(function(){var e,r,i;return t(this,(function(a){switch(a.label){case 0:return r=new Promise((function(e){var n=document,t="visibilitychange",r=function(){n.hidden||(n.removeEventListener(t,r),e())};n.addEventListener(t,r),r()})).then((function(){return o(500)})),i=function(){return n(this,void 0,void 0,(function(){var e,n,r,o,i,a,c;return t(this,(function(t){switch(t.label){case 0:return e=window,(n=e.OfflineAudioContext||e.webkitOfflineAudioContext)?z()?[2,-1]:[4,B(n)]:[2,-2];case 1:return(r=t.sent())?(o=new n(1,r.length-1+4e4,44100),(i=o.createBufferSource()).buffer=r,i.loop=!0,i.loopStart=(r.length-1)/44100,i.loopEnd=r.length/44100,i.connect(o.destination),i.start(),[4,O(o)]):[2,-3];case 2:return(a=t.sent())?(c=function(e,n){for(var t=void 0,r=!1,o=0;o<n.length;o+=Math.floor(n.length/10))if(0===n[o]);else if(void 0===t)t=n[o];else if(t!==n[o]){r=!0;break}void 0===t?t=e.getChannelData(0)[e.length-1]:r&&(t=function(e){for(var n=1/0,t=-1/0,r=0;r<e.length;r++){var o=e[r];0!==o&&(o<n&&(n=o),o>t&&(t=o))}return(n+t)/2}(n));return t}(r,a.getChannelData(0).subarray(r.length-1)),[2,Math.abs(c)]):[2,-3]}}))}))}().then((function(n){return e=[!0,n]}),(function(n){return e=[!1,n]})),[4,Promise.race([r,i])];case 1:return a.sent(),[2,function(){if(!e)return-3;if(!e[0])throw e[1];return e[1]}]}}))}))}function z(){return X()&&!P()&&!(m(["DOMRectList"in(e=window),"RTCPeerConnectionIceEvent"in e,"SVGGeometryElement"in e,"ontransitioncancel"in e])>=3);var e}function B(e){return n(this,void 0,void 0,(function(){var n,r,o,i;return t(this,(function(t){switch(t.label){case 0:return 3395,n=new e(1,3396,44100),(r=n.createOscillator()).type="square",r.frequency.value=1e3,(o=n.createDynamicsCompressor()).threshold.value=-70,o.knee.value=40,o.ratio.value=12,o.attack.value=0,o.release.value=.25,(i=n.createBiquadFilter()).type="allpass",i.frequency.value=5.239622852977861,i.Q.value=.1,r.connect(o),o.connect(i),i.connect(n.destination),r.start(0),[4,O(n)];case 1:return[2,t.sent()]}}))}))}function O(e){return new Promise((function(n,t){var r=25;e.oncomplete=function(e){return n(e.renderedBuffer)};var o=function(){try{var i=e.startRendering();a(i)&&l(i),"suspended"===e.state&&(document.hidden||r--,r>0?setTimeout(o,200):n(null))}catch(c){t(c)}};o()}))}var U=["monospace","sans-serif","serif"],Q=["sans-serif-thin","ARNO PRO","Agency FB","Arabic Typesetting","Arial Unicode MS","AvantGarde Bk BT","BankGothic Md BT","Batang","Bitstream Vera Sans Mono","Calibri","Century","Century Gothic","Clarendon","EUROSTILE","Franklin Gothic","Futura Bk BT","Futura Md BT","GOTHAM","Gill Sans","HELV","Haettenschweiler","Helvetica Neue","Humanst521 BT","Leelawadee","Letter Gothic","Levenim MT","Lucida Bright","Lucida Sans","Menlo","MS Mincho","MS Outlook","MS Reference Specialty","MS UI Gothic","MT Extra","MYRIAD PRO","Marlett","Meiryo UI","Microsoft Uighur","Minion Pro","Monotype Corsiva","PMingLiU","Pristina","SCRIPTINA","Segoe UI Light","Serifa","SimHei","Small Fonts","Staccato222 BT","TRAJAN PRO","Univers CE 55 Medium","Vrinda","ZWAdobeF"];function K(e){return n(this,void 0,void 0,(function(){var n,r,o,i,a,c,u;return t(this,(function(t){switch(t.label){case 0:return n=!1,i=function(){var e=document.createElement("canvas");return e.width=1,e.height=1,[e,e.getContext("2d")]}(),a=i[0],c=i[1],function(e,n){return!(!n||!e.toDataURL)}(a,c)?[3,1]:(r=o="unsupported",[3,4]);case 1:return n=function(e){return e.rect(0,0,10,10),e.rect(2,2,6,6),!e.isPointInPath(5,5,"evenodd")}(c),e?(r=o="skipped",[3,4]):[3,2];case 2:return[4,q(a,c)];case 3:u=t.sent(),r=u[0],o=u[1],t.label=4;case 4:return[2,{winding:n,geometry:r,text:o}]}}))}))}function q(e,r){return n(this,void 0,void 0,(function(){var n,o,a;return t(this,(function(t){switch(t.label){case 0:return function(e,n){e.width=240,e.height=60,n.textBaseline="alphabetic",n.fillStyle="#f60",n.fillRect(100,1,62,20),n.fillStyle="#069",n.font='11pt "Times New Roman"';var t="Cwm fjordbank gly ".concat(String.fromCharCode(55357,56835));n.fillText(t,2,15),n.fillStyle="rgba(102, 204, 0, 0.2)",n.font="18pt Arial",n.fillText(t,4,45)}(e,r),[4,i()];case 1:return t.sent(),n=$(e),o=$(e),n!==o?[2,["unstable","unstable"]]:(function(e,n){e.width=122,e.height=110,n.globalCompositeOperation="multiply";for(var t=0,r=[["#f2f",40,40],["#2ff",80,40],["#ff2",60,80]];t<r.length;t++){var o=r[t],i=o[0],a=o[1],c=o[2];n.fillStyle=i,n.beginPath(),n.arc(a,c,40,0,2*Math.PI,!0),n.closePath(),n.fill()}n.fillStyle="#f9c",n.arc(60,60,60,0,2*Math.PI,!0),n.arc(60,60,20,0,2*Math.PI,!0),n.fill("evenodd")}(e,r),[4,i()]);case 2:return t.sent(),a=$(e),[2,[n,a]]}}))}))}function $(e){return e.toDataURL()}function ee(){var e=screen,n=function(e){return f(s(e),null)},t=[n(e.width),n(e.height)];return t.sort().reverse(),t}var ne,te;function re(){var e=this;return function(){if(void 0===te){var e=function(){var n=oe();ie(n)?te=setTimeout(e,2500):(ne=n,te=void 0)};e()}}(),function(){return n(e,void 0,void 0,(function(){var e;return t(this,(function(n){switch(n.label){case 0:return ie(e=oe())?ne?[2,r([],ne,!0)]:A()?[4,(t=document,(t.exitFullscreen||t.msExitFullscreen||t.mozCancelFullScreen||t.webkitExitFullscreen).call(t))]:[3,2]:[3,2];case 1:n.sent(),e=oe(),n.label=2;case 2:return ie(e)||(ne=e),[2,e]}var t}))}))}}function oe(){var e=screen;return[f(d(e.availTop),null),f(d(e.width)-d(e.availWidth)-f(d(e.availLeft),0),null),f(d(e.height)-d(e.availHeight)-f(d(e.availTop),0),null),f(d(e.availLeft),null)]}function ie(e){for(var n=0;n<4;++n)if(e[n])return!1;return!0}function ae(e){var r;return n(this,void 0,void 0,(function(){var n,a,c,u,l,s,d;return t(this,(function(t){switch(t.label){case 0:for(n=document,a=n.createElement("div"),c=new Array(e.length),u={},ce(a),d=0;d<e.length;++d)"DIALOG"===(l=T(e[d])).tagName&&l.show(),ce(s=n.createElement("div")),s.appendChild(l),a.appendChild(s),c[d]=l;t.label=1;case 1:return n.body?[3,3]:[4,o(50)];case 2:return t.sent(),[3,1];case 3:return n.body.appendChild(a),[4,i()];case 4:t.sent();try{for(d=0;d<e.length;++d)c[d].offsetParent||(u[e[d]]=!0)}finally{null===(r=a.parentNode)||void 0===r||r.removeChild(a)}return[2,u]}}))}))}function ce(e){e.style.setProperty("visibility","hidden","important"),e.style.setProperty("display","block","important")}function ue(e){return matchMedia("(inverted-colors: ".concat(e,")")).matches}function le(e){return matchMedia("(forced-colors: ".concat(e,")")).matches}function se(e){return matchMedia("(prefers-contrast: ".concat(e,")")).matches}function de(e){return matchMedia("(prefers-reduced-motion: ".concat(e,")")).matches}function fe(e){return matchMedia("(prefers-reduced-transparency: ".concat(e,")")).matches}function me(e){return matchMedia("(dynamic-range: ".concat(e,")")).matches}var ve=Math,he=function(){return 0};var pe={default:[],apple:[{font:"-apple-system-body"}],serif:[{fontFamily:"serif"}],sans:[{fontFamily:"sans-serif"}],mono:[{fontFamily:"monospace"}],min:[{fontSize:"1px"}],system:[{fontFamily:"system-ui"}]};function be(e){if(e instanceof Error){if("InvalidAccessError"===e.name){if(/\bfrom\b.*\binsecure\b/i.test(e.message))return-2;if(/\bdifferent\b.*\borigin\b.*top.level\b.*\bframe\b/i.test(e.message))return-3}if("SecurityError"===e.name&&/\bthird.party iframes?.*\bnot.allowed\b/i.test(e.message))return-3}throw e}var ye=new Set([10752,2849,2884,2885,2886,2928,2929,2930,2931,2932,2960,2961,2962,2963,2964,2965,2966,2967,2968,2978,3024,3042,3088,3089,3106,3107,32773,32777,32777,32823,32824,32936,32937,32938,32939,32968,32969,32970,32971,3317,33170,3333,3379,3386,33901,33902,34016,34024,34076,3408,3410,3411,3412,3413,3414,3415,34467,34816,34817,34818,34819,34877,34921,34930,35660,35661,35724,35738,35739,36003,36004,36005,36347,36348,36349,37440,37441,37443,7936,7937,7938]),ge=new Set([34047,35723,36063,34852,34853,34854,34229,36392,36795,38449]),we=["FRAGMENT_SHADER","VERTEX_SHADER"],Le=["LOW_FLOAT","MEDIUM_FLOAT","HIGH_FLOAT","LOW_INT","MEDIUM_INT","HIGH_INT"];function ke(e){if(e.webgl)return e.webgl.context;var n,t=document.createElement("canvas");t.addEventListener("webglCreateContextError",(function(){return n=void 0}));for(var r=0,o=["webgl","experimental-webgl"];r<o.length;r++){var i=o[r];try{n=t.getContext(i)}catch(a){}if(n)break}return e.webgl={context:n},n}function Ve(e,n,t){var r=e.getShaderPrecisionFormat(e[n],e[t]);return r?[r.rangeMin,r.rangeMax,r.precision]:[]}function Se(e){return Object.keys(e.__proto__).filter(We)}function We(e){return"string"==typeof e&&!e.match(/[^A-Z0-9_x]/)}function Ze(){return C()}function xe(e){return"function"==typeof e.getParameter}var Me={fonts:function(){var e=this;return J((function(r,o){var a=o.document;return n(e,void 0,void 0,(function(){var e,n,r,o,c,u,l,s,d,f,m;return t(this,(function(t){switch(t.label){case 0:return(e=a.body).style.fontSize="48px",(n=a.createElement("div")).style.setProperty("visibility","hidden","important"),r={},o={},c=function(e){var t=a.createElement("span"),r=t.style;return r.position="absolute",r.top="0",r.left="0",r.fontFamily=e,t.textContent="mmMwWLliI0O&1",n.appendChild(t),t},u=function(e,n){return c("'".concat(e,"',").concat(n))},l=function(){for(var e={},n=function(n){e[n]=U.map((function(e){return u(n,e)}))},t=0,r=Q;t<r.length;t++){n(r[t])}return e},s=function(e){return U.some((function(n,t){return e[t].offsetWidth!==r[n]||e[t].offsetHeight!==o[n]}))},d=function(){return U.map(c)}(),f=l(),e.appendChild(n),[4,i()];case 1:for(t.sent(),m=0;m<U.length;m++)r[U[m]]=d[m].offsetWidth,o[U[m]]=d[m].offsetHeight;return[2,Q.filter((function(e){return s(f[e])}))]}}))}))}))},domBlockers:function(e){var r=(void 0===e?{}:e).debug;return n(this,void 0,void 0,(function(){var e,n,o,i,a;return t(this,(function(t){switch(t.label){case 0:return X()||N()?(c=atob,e={abpIndo:["#Iklan-Melayang","#Kolom-Iklan-728","#SidebarIklan-wrapper",'[title="ALIENBOLA" i]',c("I0JveC1CYW5uZXItYWRz")],abpvn:[".quangcao","#mobileCatfish",c("LmNsb3NlLWFkcw=="),'[id^="bn_bottom_fixed_"]',"#pmadv"],adBlockFinland:[".mainostila",c("LnNwb25zb3JpdA=="),".ylamainos",c("YVtocmVmKj0iL2NsaWNrdGhyZ2guYXNwPyJd"),c("YVtocmVmXj0iaHR0cHM6Ly9hcHAucmVhZHBlYWsuY29tL2FkcyJd")],adBlockPersian:["#navbar_notice_50",".kadr",'TABLE[width="140px"]',"#divAgahi",c("YVtocmVmXj0iaHR0cDovL2cxLnYuZndtcm0ubmV0L2FkLyJd")],adBlockWarningRemoval:["#adblock-honeypot",".adblocker-root",".wp_adblock_detect",c("LmhlYWRlci1ibG9ja2VkLWFk"),c("I2FkX2Jsb2NrZXI=")],adGuardAnnoyances:[".hs-sosyal","#cookieconsentdiv",'div[class^="app_gdpr"]',".as-oil",'[data-cypress="soft-push-notification-modal"]'],adGuardBase:[".BetterJsPopOverlay",c("I2FkXzMwMFgyNTA="),c("I2Jhbm5lcmZsb2F0MjI="),c("I2NhbXBhaWduLWJhbm5lcg=="),c("I0FkLUNvbnRlbnQ=")],adGuardChinese:[c("LlppX2FkX2FfSA=="),c("YVtocmVmKj0iLmh0aGJldDM0LmNvbSJd"),"#widget-quan",c("YVtocmVmKj0iLzg0OTkyMDIwLnh5eiJd"),c("YVtocmVmKj0iLjE5NTZobC5jb20vIl0=")],adGuardFrench:["#pavePub",c("LmFkLWRlc2t0b3AtcmVjdGFuZ2xl"),".mobile_adhesion",".widgetadv",c("LmFkc19iYW4=")],adGuardGerman:['aside[data-portal-id="leaderboard"]'],adGuardJapanese:["#kauli_yad_1",c("YVtocmVmXj0iaHR0cDovL2FkMi50cmFmZmljZ2F0ZS5uZXQvIl0="),c("Ll9wb3BJbl9pbmZpbml0ZV9hZA=="),c("LmFkZ29vZ2xl"),c("Ll9faXNib29zdFJldHVybkFk")],adGuardMobile:[c("YW1wLWF1dG8tYWRz"),c("LmFtcF9hZA=="),'amp-embed[type="24smi"]',"#mgid_iframe1",c("I2FkX2ludmlld19hcmVh")],adGuardRussian:[c("YVtocmVmXj0iaHR0cHM6Ly9hZC5sZXRtZWFkcy5jb20vIl0="),c("LnJlY2xhbWE="),'div[id^="smi2adblock"]',c("ZGl2W2lkXj0iQWRGb3hfYmFubmVyXyJd"),"#psyduckpockeball"],adGuardSocial:[c("YVtocmVmXj0iLy93d3cuc3R1bWJsZXVwb24uY29tL3N1Ym1pdD91cmw9Il0="),c("YVtocmVmXj0iLy90ZWxlZ3JhbS5tZS9zaGFyZS91cmw/Il0="),".etsy-tweet","#inlineShare",".popup-social"],adGuardSpanishPortuguese:["#barraPublicidade","#Publicidade","#publiEspecial","#queTooltip",".cnt-publi"],adGuardTrackingProtection:["#qoo-counter",c("YVtocmVmXj0iaHR0cDovL2NsaWNrLmhvdGxvZy5ydS8iXQ=="),c("YVtocmVmXj0iaHR0cDovL2hpdGNvdW50ZXIucnUvdG9wL3N0YXQucGhwIl0="),c("YVtocmVmXj0iaHR0cDovL3RvcC5tYWlsLnJ1L2p1bXAiXQ=="),"#top100counter"],adGuardTurkish:["#backkapat",c("I3Jla2xhbWk="),c("YVtocmVmXj0iaHR0cDovL2Fkc2Vydi5vbnRlay5jb20udHIvIl0="),c("YVtocmVmXj0iaHR0cDovL2l6bGVuemkuY29tL2NhbXBhaWduLyJd"),c("YVtocmVmXj0iaHR0cDovL3d3dy5pbnN0YWxsYWRzLm5ldC8iXQ==")],bulgarian:[c("dGQjZnJlZW5ldF90YWJsZV9hZHM="),"#ea_intext_div",".lapni-pop-over","#xenium_hot_offers"],easyList:[".yb-floorad",c("LndpZGdldF9wb19hZHNfd2lkZ2V0"),c("LnRyYWZmaWNqdW5reS1hZA=="),".textad_headline",c("LnNwb25zb3JlZC10ZXh0LWxpbmtz")],easyListChina:[c("LmFwcGd1aWRlLXdyYXBbb25jbGljayo9ImJjZWJvcy5jb20iXQ=="),c("LmZyb250cGFnZUFkdk0="),"#taotaole","#aafoot.top_box",".cfa_popup"],easyListCookie:[".ezmob-footer",".cc-CookieWarning","[data-cookie-number]",c("LmF3LWNvb2tpZS1iYW5uZXI="),".sygnal24-gdpr-modal-wrap"],easyListCzechSlovak:["#onlajny-stickers",c("I3Jla2xhbW5pLWJveA=="),c("LnJla2xhbWEtbWVnYWJvYXJk"),".sklik",c("W2lkXj0ic2tsaWtSZWtsYW1hIl0=")],easyListDutch:[c("I2FkdmVydGVudGll"),c("I3ZpcEFkbWFya3RCYW5uZXJCbG9jaw=="),".adstekst",c("YVtocmVmXj0iaHR0cHM6Ly94bHR1YmUubmwvY2xpY2svIl0="),"#semilo-lrectangle"],easyListGermany:["#SSpotIMPopSlider",c("LnNwb25zb3JsaW5rZ3J1ZW4="),c("I3dlcmJ1bmdza3k="),c("I3Jla2xhbWUtcmVjaHRzLW1pdHRl"),c("YVtocmVmXj0iaHR0cHM6Ly9iZDc0Mi5jb20vIl0=")],easyListItaly:[c("LmJveF9hZHZfYW5udW5jaQ=="),".sb-box-pubbliredazionale",c("YVtocmVmXj0iaHR0cDovL2FmZmlsaWF6aW9uaWFkcy5zbmFpLml0LyJd"),c("YVtocmVmXj0iaHR0cHM6Ly9hZHNlcnZlci5odG1sLml0LyJd"),c("YVtocmVmXj0iaHR0cHM6Ly9hZmZpbGlhemlvbmlhZHMuc25haS5pdC8iXQ==")],easyListLithuania:[c("LnJla2xhbW9zX3RhcnBhcw=="),c("LnJla2xhbW9zX251b3JvZG9z"),c("aW1nW2FsdD0iUmVrbGFtaW5pcyBza3lkZWxpcyJd"),c("aW1nW2FsdD0iRGVkaWt1b3RpLmx0IHNlcnZlcmlhaSJd"),c("aW1nW2FsdD0iSG9zdGluZ2FzIFNlcnZlcmlhaS5sdCJd")],estonian:[c("QVtocmVmKj0iaHR0cDovL3BheTRyZXN1bHRzMjQuZXUiXQ==")],fanboyAnnoyances:["#ac-lre-player",".navigate-to-top","#subscribe_popup",".newsletter_holder","#back-top"],fanboyAntiFacebook:[".util-bar-module-firefly-visible"],fanboyEnhancedTrackers:[".open.pushModal","#issuem-leaky-paywall-articles-zero-remaining-nag","#sovrn_container",'div[class$="-hide"][zoompage-fontsize][style="display: block;"]',".BlockNag__Card"],fanboySocial:["#FollowUs","#meteored_share","#social_follow",".article-sharer",".community__social-desc"],frellwitSwedish:[c("YVtocmVmKj0iY2FzaW5vcHJvLnNlIl1bdGFyZ2V0PSJfYmxhbmsiXQ=="),c("YVtocmVmKj0iZG9rdG9yLXNlLm9uZWxpbmsubWUiXQ=="),"article.category-samarbete",c("ZGl2LmhvbGlkQWRz"),"ul.adsmodern"],greekAdBlock:[c("QVtocmVmKj0iYWRtYW4ub3RlbmV0LmdyL2NsaWNrPyJd"),c("QVtocmVmKj0iaHR0cDovL2F4aWFiYW5uZXJzLmV4b2R1cy5nci8iXQ=="),c("QVtocmVmKj0iaHR0cDovL2ludGVyYWN0aXZlLmZvcnRobmV0LmdyL2NsaWNrPyJd"),"DIV.agores300","TABLE.advright"],hungarian:["#cemp_doboz",".optimonk-iframe-container",c("LmFkX19tYWlu"),c("W2NsYXNzKj0iR29vZ2xlQWRzIl0="),"#hirdetesek_box"],iDontCareAboutCookies:['.alert-info[data-block-track*="CookieNotice"]',".ModuleTemplateCookieIndicator",".o--cookies--container","#cookies-policy-sticky","#stickyCookieBar"],icelandicAbp:[c("QVtocmVmXj0iL2ZyYW1ld29yay9yZXNvdXJjZXMvZm9ybXMvYWRzLmFzcHgiXQ==")],latvian:[c("YVtocmVmPSJodHRwOi8vd3d3LnNhbGlkemluaS5sdi8iXVtzdHlsZT0iZGlzcGxheTogYmxvY2s7IHdpZHRoOiAxMjBweDsgaGVpZ2h0OiA0MHB4OyBvdmVyZmxvdzogaGlkZGVuOyBwb3NpdGlvbjogcmVsYXRpdmU7Il0="),c("YVtocmVmPSJodHRwOi8vd3d3LnNhbGlkemluaS5sdi8iXVtzdHlsZT0iZGlzcGxheTogYmxvY2s7IHdpZHRoOiA4OHB4OyBoZWlnaHQ6IDMxcHg7IG92ZXJmbG93OiBoaWRkZW47IHBvc2l0aW9uOiByZWxhdGl2ZTsiXQ==")],listKr:[c("YVtocmVmKj0iLy9hZC5wbGFuYnBsdXMuY28ua3IvIl0="),c("I2xpdmVyZUFkV3JhcHBlcg=="),c("YVtocmVmKj0iLy9hZHYuaW1hZHJlcC5jby5rci8iXQ=="),c("aW5zLmZhc3R2aWV3LWFk"),".revenue_unit_item.dable"],listeAr:[c("LmdlbWluaUxCMUFk"),".right-and-left-sponsers",c("YVtocmVmKj0iLmFmbGFtLmluZm8iXQ=="),c("YVtocmVmKj0iYm9vcmFxLm9yZyJd"),c("YVtocmVmKj0iZHViaXp6bGUuY29tL2FyLz91dG1fc291cmNlPSJd")],listeFr:[c("YVtocmVmXj0iaHR0cDovL3Byb21vLnZhZG9yLmNvbS8iXQ=="),c("I2FkY29udGFpbmVyX3JlY2hlcmNoZQ=="),c("YVtocmVmKj0id2Vib3JhbWEuZnIvZmNnaS1iaW4vIl0="),".site-pub-interstitiel",'div[id^="crt-"][data-criteo-id]'],officialPolish:["#ceneo-placeholder-ceneo-12",c("W2hyZWZePSJodHRwczovL2FmZi5zZW5kaHViLnBsLyJd"),c("YVtocmVmXj0iaHR0cDovL2Fkdm1hbmFnZXIudGVjaGZ1bi5wbC9yZWRpcmVjdC8iXQ=="),c("YVtocmVmXj0iaHR0cDovL3d3dy50cml6ZXIucGwvP3V0bV9zb3VyY2UiXQ=="),c("ZGl2I3NrYXBpZWNfYWQ=")],ro:[c("YVtocmVmXj0iLy9hZmZ0cmsuYWx0ZXgucm8vQ291bnRlci9DbGljayJd"),c("YVtocmVmXj0iaHR0cHM6Ly9ibGFja2ZyaWRheXNhbGVzLnJvL3Ryay9zaG9wLyJd"),c("YVtocmVmXj0iaHR0cHM6Ly9ldmVudC4ycGVyZm9ybWFudC5jb20vZXZlbnRzL2NsaWNrIl0="),c("YVtocmVmXj0iaHR0cHM6Ly9sLnByb2ZpdHNoYXJlLnJvLyJd"),'a[href^="/url/"]'],ruAd:[c("YVtocmVmKj0iLy9mZWJyYXJlLnJ1LyJd"),c("YVtocmVmKj0iLy91dGltZy5ydS8iXQ=="),c("YVtocmVmKj0iOi8vY2hpa2lkaWtpLnJ1Il0="),"#pgeldiz",".yandex-rtb-block"],thaiAds:["a[href*=macau-uta-popup]",c("I2Fkcy1nb29nbGUtbWlkZGxlX3JlY3RhbmdsZS1ncm91cA=="),c("LmFkczMwMHM="),".bumq",".img-kosana"],webAnnoyancesUltralist:["#mod-social-share-2","#social-tools",c("LmN0cGwtZnVsbGJhbm5lcg=="),".zergnet-recommend",".yt.btn-link.btn-md.btn"]},n=Object.keys(e),[4,ae((a=[]).concat.apply(a,n.map((function(n){return e[n]}))))]):[2,void 0];case 1:return o=t.sent(),r&&function(e,n){for(var t="DOM blockers debug:\n```",r=0,o=Object.keys(e);r<o.length;r++){var i=o[r];t+="\n".concat(i,":");for(var a=0,c=e[i];a<c.length;a++){var u=c[a];t+="\n ".concat(n[u]?"🚫":"➡️"," ").concat(u)}}console.log("".concat(t,"\n```"))}(e,o),(i=n.filter((function(n){var t=e[n];return m(t.map((function(e){return o[e]})))>.6*t.length}))).sort(),[2,i]}var c}))}))},fontPreferences:function(){return function(e,n){void 0===n&&(n=4e3);return J((function(t,o){var i=o.document,a=i.body,c=a.style;c.width="".concat(n,"px"),c.webkitTextSizeAdjust=c.textSizeAdjust="none",j()?a.style.zoom="".concat(1/o.devicePixelRatio):X()&&(a.style.zoom="reset");var u=i.createElement("div");return u.textContent=r([],Array(n/20<<0),!0).map((function(){return"word"})).join(" "),a.appendChild(u),e(i,a)}),'<!doctype html><html><head><meta name="viewport" content="width=device-width, initial-scale=1">')}((function(e,n){for(var t={},r={},o=0,i=Object.keys(pe);o<i.length;o++){var a=i[o],c=pe[a],u=c[0],l=void 0===u?{}:u,s=c[1],d=void 0===s?"mmMwWLliI0fiflO&1":s,f=e.createElement("span");f.textContent=d,f.style.whiteSpace="nowrap";for(var m=0,v=Object.keys(l);m<v.length;m++){var h=v[m],p=l[h];void 0!==p&&(f.style[h]=p)}t[a]=f,n.append(e.createElement("br"),f)}for(var b=0,y=Object.keys(pe);b<y.length;b++){r[a=y[b]]=t[a].getBoundingClientRect().width}return r}))},audio:function(){return n(this,void 0,void 0,(function(){var e;return t(this,(function(n){switch(n.label){case 0:return[4,_()];case 1:return e=n.sent(),[2,function(){return function(e,n){if(0===e)return e;var t=Math.floor(Math.log10(Math.abs(e)))-Math.floor(n)+1,r=Math.pow(10,-t)*(10*n%10||1);return Math.round(e*r)/r}(e(),6.2)}]}}))}))},screenFrame:function(){var e=this;if(X()&&H()&&E())return function(){return Promise.resolve(void 0)};var r=re();return function(){return n(e,void 0,void 0,(function(){var e,n;return t(this,(function(t){switch(t.label){case 0:return[4,r()];case 1:return e=t.sent(),[2,[(n=function(e){return null===e?null:v(e,10)})(e[0]),n(e[1]),n(e[2]),n(e[3])]]}}))}))}},canvas:function(){return K(X()&&H()&&E())},osCpu:function(){return navigator.oscpu},languages:function(){var e,n=navigator,t=[],r=n.language||n.userLanguage||n.browserLanguage||n.systemLanguage;if(void 0!==r&&t.push([r]),Array.isArray(n.languages))j()&&m([!("MediaSettingsRange"in(e=window)),"RTCEncodedAudioFrame"in e,""+e.Intl=="[object Intl]",""+e.Reflect=="[object Reflect]"])>=3||t.push(n.languages);else if("string"==typeof n.languages){var o=n.languages;o&&t.push(o.split(","))}return t},colorDepth:function(){return window.screen.colorDepth},deviceMemory:function(){return f(d(navigator.deviceMemory),void 0)},screenResolution:function(){if(!(X()&&H()&&E()))return ee()},hardwareConcurrency:function(){return f(s(navigator.hardwareConcurrency),void 0)},timezone:function(){var e,n=null===(e=window.Intl)||void 0===e?void 0:e.DateTimeFormat;if(n){var t=(new n).resolvedOptions().timeZone;if(t)return t}var r,o=(r=(new Date).getFullYear(),-Math.max(d(new Date(r,0,1).getTimezoneOffset()),d(new Date(r,6,1).getTimezoneOffset())));return"UTC".concat(o>=0?"+":"").concat(Math.abs(o))},sessionStorage:function(){try{return!!window.sessionStorage}catch(e){return!0}},localStorage:function(){try{return!!window.localStorage}catch(e){return!0}},indexedDB:function(){if(!I()&&!Y())try{return!!window.indexedDB}catch(e){return!0}},openDatabase:function(){return!!window.openDatabase},cpuClass:function(){return navigator.cpuClass},platform:function(){var e=navigator.platform;return"MacIntel"===e&&X()&&!P()?function(){if("iPad"===navigator.platform)return!0;var e=screen,n=e.width/e.height;return m(["MediaSource"in window,!!Element.prototype.webkitRequestFullscreen,n>.65&&n<1.53])>=2}()?"iPad":"iPhone":e},plugins:function(){var e=navigator.plugins;if(e){for(var n=[],t=0;t<e.length;++t){var r=e[t];if(r){for(var o=[],i=0;i<r.length;++i){var a=r[i];o.push({type:a.type,suffixes:a.suffixes})}n.push({name:r.name,description:r.description,mimeTypes:o})}}return n}},touchSupport:function(){var e,n=navigator,t=0;void 0!==n.maxTouchPoints?t=s(n.maxTouchPoints):void 0!==n.msMaxTouchPoints&&(t=n.msMaxTouchPoints);try{document.createEvent("TouchEvent"),e=!0}catch(r){e=!1}return{maxTouchPoints:t,touchEvent:e,touchStart:"ontouchstart"in window}},vendor:function(){return navigator.vendor||""},vendorFlavors:function(){for(var e=[],n=0,t=["chrome","safari","__crWeb","__gCrWeb","yandex","__yb","__ybro","__firefox__","__edgeTrackingPreventionStatistics","webkit","oprt","samsungAr","ucweb","UCShellJava","puffinDevice"];n<t.length;n++){var r=t[n],o=window[r];o&&"object"==typeof o&&e.push(r)}return e.sort()},cookiesEnabled:function(){var e=document;try{e.cookie="cookietest=1; SameSite=Strict;";var n=-1!==e.cookie.indexOf("cookietest=");return e.cookie="cookietest=1; SameSite=Strict; expires=Thu, 01-Jan-1970 00:00:01 GMT",n}catch(t){return!1}},colorGamut:function(){for(var e=0,n=["rec2020","p3","srgb"];e<n.length;e++){var t=n[e];if(matchMedia("(color-gamut: ".concat(t,")")).matches)return t}},invertedColors:function(){return!!ue("inverted")||!ue("none")&&void 0},forcedColors:function(){return!!le("active")||!le("none")&&void 0},monochrome:function(){if(matchMedia("(min-monochrome: 0)").matches){for(var e=0;e<=100;++e)if(matchMedia("(max-monochrome: ".concat(e,")")).matches)return e;throw new Error("Too high value")}},contrast:function(){return se("no-preference")?0:se("high")||se("more")?1:se("low")||se("less")?-1:se("forced")?10:void 0},reducedMotion:function(){return!!de("reduce")||!de("no-preference")&&void 0},reducedTransparency:function(){return!!fe("reduce")||!fe("no-preference")&&void 0},hdr:function(){return!!me("high")||!me("standard")&&void 0},math:function(){var e,n=ve.acos||he,t=ve.acosh||he,r=ve.asin||he,o=ve.asinh||he,i=ve.atanh||he,a=ve.atan||he,c=ve.sin||he,u=ve.sinh||he,l=ve.cos||he,s=ve.cosh||he,d=ve.tan||he,f=ve.tanh||he,m=ve.exp||he,v=ve.expm1||he,h=ve.log1p||he;return{acos:n(.12312423423423424),acosh:t(1e308),acoshPf:(e=1e154,ve.log(e+ve.sqrt(e*e-1))),asin:r(.12312423423423424),asinh:o(1),asinhPf:function(e){return ve.log(e+ve.sqrt(e*e+1))}(1),atanh:i(.5),atanhPf:function(e){return ve.log((1+e)/(1-e))/2}(.5),atan:a(.5),sin:c(-1e300),sinh:u(1),sinhPf:function(e){return ve.exp(e)-1/ve.exp(e)/2}(1),cos:l(10.000000000123),cosh:s(1),coshPf:function(e){return(ve.exp(e)+1/ve.exp(e))/2}(1),tan:d(-1e300),tanh:f(1),tanhPf:function(e){return(ve.exp(2*e)-1)/(ve.exp(2*e)+1)}(1),exp:m(1),expm1:v(1),expm1Pf:function(e){return ve.exp(e)-1}(1),log1p:h(10),log1pPf:function(e){return ve.log(1+e)}(10),powPI:function(e){return ve.pow(ve.PI,e)}(-100)}},pdfViewerEnabled:function(){return navigator.pdfViewerEnabled},architecture:function(){var e=new Float32Array(1),n=new Uint8Array(e.buffer);return e[0]=1/0,e[0]=e[0]-e[0],n[3]},applePay:function(){var e=window.ApplePaySession;if("function"!=typeof(null==e?void 0:e.canMakePayments))return-1;try{return e.canMakePayments()?1:0}catch(n){return be(n)}},privateClickMeasurement:function(){var e,n=document.createElement("a"),t=null!==(e=n.attributionSourceId)&&void 0!==e?e:n.attributionsourceid;return void 0===t?void 0:String(t)},webGlBasics:function(e){var n,t,r,o,i,a,c=ke(e.cache);if(!c)return-1;if(!xe(c))return-2;var u=Ze()?null:c.getExtension("WEBGL_debug_renderer_info");return{version:(null===(n=c.getParameter(c.VERSION))||void 0===n?void 0:n.toString())||"",vendor:(null===(t=c.getParameter(c.VENDOR))||void 0===t?void 0:t.toString())||"",vendorUnmasked:u?null===(r=c.getParameter(u.UNMASKED_VENDOR_WEBGL))||void 0===r?void 0:r.toString():"",renderer:(null===(o=c.getParameter(c.RENDERER))||void 0===o?void 0:o.toString())||"",rendererUnmasked:u?null===(i=c.getParameter(u.UNMASKED_RENDERER_WEBGL))||void 0===i?void 0:i.toString():"",shadingLanguageVersion:(null===(a=c.getParameter(c.SHADING_LANGUAGE_VERSION))||void 0===a?void 0:a.toString())||""}},webGlExtensions:function(e){var n=ke(e.cache);if(!n)return-1;if(!xe(n))return-2;var t=n.getSupportedExtensions(),r=n.getContextAttributes(),o=[],i=[],a=[],c=[];if(r)for(var u=0,l=Object.keys(r);u<l.length;u++){var s=l[u];o.push("".concat(s,"=").concat(r[s]))}for(var d=0,f=Se(n);d<f.length;d++){var m=n[w=f[d]];i.push("".concat(w,"=").concat(m).concat(ye.has(m)?"=".concat(n.getParameter(m)):""))}if(t)for(var v=0,h=t;v<h.length;v++){var p=h[v];if("WEBGL_debug_renderer_info"!==p||!Ze()){var b=n.getExtension(p);if(b)for(var y=0,g=Se(b);y<g.length;y++){var w;m=b[w=g[y]];a.push("".concat(w,"=").concat(m).concat(ge.has(m)?"=".concat(n.getParameter(m)):""))}}}for(var L=0,k=we;L<k.length;L++)for(var V=k[L],S=0,W=Le;S<W.length;S++){var Z=W[S],x=Ve(n,V,Z);c.push("".concat(V,".").concat(Z,"=").concat(x.join(",")))}return a.sort(),i.sort(),{contextAttributes:o,parameters:i,shaderPrecisions:c,extensions:t,extensionParameters:a}}};function Re(e){var n=function(e){if(N())return.4;if(X())return!P()||H()&&E()?.3:.5;var n="value"in e.platform?e.platform.value:"";if(/^Win/.test(n))return.6;if(/^Mac/.test(n))return.5;return.7}(e),t=function(e){return v(.99+.01*e,1e-4)}(n);return{score:n,comment:"$ if upgrade to Pro: https://fpjs.dev/pro".replace(/\$/g,"".concat(t))}}function Fe(n){return JSON.stringify(n,(function(n,t){return t instanceof Error?e({name:(r=t).name,message:r.message,stack:null===(o=r.stack)||void 0===o?void 0:o.split("\n")},r):t;var r,o}),2)}function Ge(e){return M(function(e){for(var n="",t=0,r=Object.keys(e).sort();t<r.length;t++){var o=r[t],i=e[o],a="error"in i?"error":JSON.stringify(i.value);n+="".concat(n?"|":"").concat(o.replace(/([:|\\])/g,"\\$1"),":").concat(a)}return n}(e))}function Ie(e){return void 0===e&&(e=50),function(e,n){void 0===n&&(n=1/0);var t=window.requestIdleCallback;return t?new Promise((function(e){return t.call(window,(function(){return e()}),{timeout:n})})):o(Math.min(e,n))}(e,2*e)}function Ye(e,r){var o=Date.now();return{get:function(i){return n(this,void 0,void 0,(function(){var n,a,c;return t(this,(function(t){switch(t.label){case 0:return n=Date.now(),[4,e()];case 1:return a=t.sent(),c=function(e){var n;return{get visitorId(){return void 0===n&&(n=Ge(this.components)),n},set visitorId(e){n=e},confidence:Re(e),components:e,version:"4.2.1"}}(a),(r||(null==i?void 0:i.debug))&&console.log("Copy the text below to get the debug data:\n\n```\nversion: ".concat(c.version,"\nuserAgent: ").concat(navigator.userAgent,"\ntimeBetweenLoadAndGet: ").concat(n-o,"\nvisitorId: ").concat(c.visitorId,"\ncomponents: ").concat(Fe(a),"\n```")),[2,c]}}))}))}}}function je(e){return void 0===e&&(e={}),n(this,void 0,void 0,(function(){var n,r,o;return t(this,(function(t){switch(t.label){case 0:return e.monitoring,n=e.delayFallback,r=e.debug,[4,Ie(n)];case 1:return t.sent(),o=function(e){return F(Me,e,[])}({cache:{},debug:r}),[2,Ye(o,r)]}}))}))}var Xe={load:je,hashComponents:Ge,componentsToDebugString:Fe},Pe=M;export{Fe as componentsToDebugString,Xe as default,z as doesBrowserSuspendAudioContext,A as getFullscreenElement,_ as getUnstableAudioFingerprint,K as getUnstableCanvasFingerprint,re as getUnstableScreenFrame,ee as getUnstableScreenResolution,ke as getWebGLContext,be as handleApplePayError,Ge as hashComponents,N as isAndroid,j as isChromium,P as isDesktopWebKit,Y as isEdgeHTML,C as isGecko,I as isTrident,X as isWebKit,je as load,F as loadSources,Pe as murmurX64Hash128,Ie as prepareForSources,O as renderAudio,Me as sources,G as transformSource,J as withIframe};
|
static/js/windowHandler.js
CHANGED
@@ -1,159 +1,137 @@
|
|
1 |
class WindowHandler{
|
2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
this.index = index
|
4 |
-
this.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
this.active = false;
|
6 |
-
|
|
|
7 |
this.interacted = false
|
8 |
|
|
|
|
|
|
|
9 |
this.cargarChat(conversacion)
|
|
|
|
|
10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
|
12 |
-
|
13 |
-
|
|
|
|
|
14 |
|
15 |
-
this.ctx.find(".input-text").keypress((event) => {
|
16 |
-
if (!event.shiftKey && event.keyCode === 13) {
|
17 |
-
this.manejadorEnviar();
|
18 |
-
}});
|
19 |
-
this.ctx.find(".input-send").click(() => this.manejadorEnviar());
|
20 |
-
this.ctx.find(".input-text").keypress(() => this.recalcularTextarea());
|
21 |
-
this.ctx.find(".input-text").keyup(() => this.recalcularTextarea());
|
22 |
-
this.ctx.find(".input-text").keydown(() => this.recalcularTextarea());
|
23 |
-
this.ctx.find(".input-delete").click(()=> this.ctx.trigger("chat:eliminar:pre"))
|
24 |
|
25 |
-
|
26 |
-
this.respuestaInicio(params)
|
27 |
-
});
|
28 |
-
this.ctx.on("precarga:iniciada", (event, params) => {
|
29 |
-
this.respuestaIniciada()
|
30 |
-
});
|
31 |
-
this.ctx.on("precarga:status", (event, params) => {
|
32 |
-
this.respuestaStatus(...Object.values(params))
|
33 |
-
});
|
34 |
-
this.ctx.on("precarga:mensaje", (event, params) => {
|
35 |
-
this.respuestaMensaje(params)}
|
36 |
-
);
|
37 |
-
this.ctx.on("precarga:error", (event, params) => {
|
38 |
-
this.respuestaError(params)
|
39 |
-
});
|
40 |
-
|
41 |
-
}
|
42 |
-
|
43 |
-
cargarChat(conversacion){
|
44 |
-
let tempTabSel = $(".tab-label-template").clone();
|
45 |
-
tempTabSel.removeClass("tab-label-template")
|
46 |
-
tempTabSel.addClass("tab-label")
|
47 |
-
tempTabSel.find("div").text("Tab "+this.index);
|
48 |
-
tempTabSel.find("input").val(this.index);
|
49 |
-
if(this.index==0){
|
50 |
-
tempTabSel.find("input").prop("checked", true);
|
51 |
-
}
|
52 |
-
$(".tabs").append(tempTabSel)
|
53 |
-
|
54 |
-
let tempTab = $(".tab-template").clone();
|
55 |
-
tempTab.removeClass("tab-template")
|
56 |
-
tempTab.addClass("tab")
|
57 |
-
$(".wrapper").append(tempTab)
|
58 |
this.ctx = tempTab
|
59 |
this.chatbox = this.ctx.find(".chat");
|
|
|
|
|
60 |
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
this.chatbox.append(clone);
|
74 |
-
this.active = clone;
|
75 |
-
Prism.highlightAllUnder(this.active[0])
|
76 |
-
this.active = false;
|
77 |
-
this.chatbox.scrollTop(this.chatbox[0].scrollHeight);
|
78 |
-
this.interacted=true
|
79 |
}
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
|
86 |
-
|
|
|
|
|
|
|
87 |
|
88 |
-
eliminarChat(){
|
89 |
-
if(confirm("¿Estás seguro que quieres eliminar esta conversación?") == true){
|
90 |
-
$(document).trigger("chat:eliminar", {ctx:this.ctx, index:this.index})
|
91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
}
|
93 |
-
|
|
|
|
|
|
|
94 |
}
|
95 |
|
96 |
manejadorEnviar(){
|
97 |
let mensaje = this.ctx.find(".input-text").val();
|
|
|
|
|
98 |
if(mensaje==""){
|
99 |
return false;
|
100 |
}
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
recalcularTextarea(){
|
105 |
-
this.ctx.find(".input-box").css("height", "30px");
|
106 |
-
let height = parseInt((this.ctx.find(".input-text").prop('scrollHeight')+15)/15)*15;
|
107 |
-
this.ctx.find(".input-box").css("height", height+"px");
|
108 |
-
height -= 30;
|
109 |
-
this.ctx.find(".chat").css("--textarea", height+"px");
|
110 |
-
}
|
111 |
-
|
112 |
-
procesarTexto(texto){
|
113 |
-
|
114 |
-
let resultado = "";
|
115 |
-
let codigo = false;
|
116 |
-
for(let actual of texto.split("```")){
|
117 |
-
if(codigo){
|
118 |
-
let temp = actual.split("\n",1);
|
119 |
-
resultado += "<pre><code class='language-";
|
120 |
-
resultado += temp[0].length>1?temp[0]:"none";
|
121 |
-
temp = $("<div></div>").text(actual.substr(temp[0].length+1)).html()
|
122 |
-
resultado += "'>"+temp+"</code></pre>";
|
123 |
-
}else{
|
124 |
-
resultado += $("<div></div>").text(actual).html().replace(/`([^`]+?)`/gm, "<b>$1</b>").replace(/\n/g, "<br>");
|
125 |
-
}
|
126 |
-
codigo = !codigo;
|
127 |
}
|
128 |
-
|
129 |
-
|
|
|
|
|
130 |
}
|
131 |
-
|
132 |
|
133 |
-
|
134 |
-
|
135 |
-
this.ctx.find(".input-text").val("");
|
136 |
-
this.ctx.find("button").prop("disabled", true);
|
137 |
-
this.ctx.find("textarea").prop("disabled", true);
|
138 |
-
this.mensaje = ""
|
139 |
-
let clone = this.template.clone();
|
140 |
-
clone.addClass("me");
|
141 |
-
clone.find("div p").text(mensaje);
|
142 |
-
this.chatbox.append(clone);
|
143 |
-
|
144 |
-
clone = this.template.clone();
|
145 |
-
clone.find("div p").html('<div class="loader-wrap"><span class="loader"></span></div>');
|
146 |
-
this.chatbox.append(clone);
|
147 |
-
this.active = clone;
|
148 |
-
this.chatbox.scrollTop(this.chatbox[0].scrollHeight);
|
149 |
}
|
150 |
|
151 |
-
respuestaIniciada(){
|
152 |
-
this.active.find(".loader").addClass("firststage")
|
153 |
-
}
|
154 |
respuestaStatus(mensaje, modo=false){
|
155 |
let temp = $("<div></div>")
|
156 |
temp.text(mensaje)
|
|
|
|
|
|
|
157 |
switch(modo){
|
158 |
case "enlinea":
|
159 |
this.active.find("div p div:last-child").text(this.active.find("div p div:last-child").text() + mensaje);
|
@@ -164,29 +142,35 @@ class WindowHandler{
|
|
164 |
break;
|
165 |
default:
|
166 |
this.active.find("div p").append(temp)
|
167 |
-
|
168 |
-
|
169 |
-
}
|
170 |
-
|
171 |
}
|
172 |
|
173 |
-
respuestaMensaje(
|
|
|
174 |
this.active.find("div p").html("");
|
175 |
mensaje = this.procesarTexto(mensaje);
|
176 |
this.active.find("div p").html(mensaje);
|
177 |
Prism.highlightAllUnder(this.active[0]);
|
|
|
|
|
|
|
178 |
this.active = false;
|
|
|
|
|
179 |
this.ctx.find("button").prop("disabled", false);
|
180 |
this.ctx.find("textarea").prop("disabled", false);
|
|
|
181 |
this.ctx.find("textarea").focus();
|
182 |
|
183 |
-
|
184 |
-
|
185 |
}
|
186 |
|
187 |
respuestaError(error){
|
188 |
this.ctx.find("button").prop("disabled", false);
|
189 |
this.ctx.find("textarea").prop("disabled", false);
|
|
|
190 |
this.ctx.find("textarea").focus();
|
191 |
if(error.hasOwnProperty("responseJSON")){
|
192 |
this.active.find("div p").html(error.responseJSON.detail)
|
@@ -207,11 +191,54 @@ class WindowHandler{
|
|
207 |
}
|
208 |
this.active = false;
|
209 |
this.chatbox.scrollTop(this.chatbox[0].scrollHeight)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
210 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
211 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
213 |
}
|
214 |
|
|
|
|
|
|
|
|
|
215 |
|
216 |
|
217 |
|
|
|
1 |
class WindowHandler{
|
2 |
+
/* Eventos generados
|
3 |
+
- chat:enviar
|
4 |
+
- chat:eliminar
|
5 |
+
**********************/
|
6 |
+
/* Eventos escuchados
|
7 |
+
|
8 |
+
**********************/
|
9 |
+
|
10 |
+
conversacion = []
|
11 |
+
constructor(conversacion, index, chatHndl){
|
12 |
+
|
13 |
+
// El indice de este chat
|
14 |
this.index = index
|
15 |
+
this.conversacion = conversacion
|
16 |
+
this.chatHandler = chatHndl || 0
|
17 |
+
|
18 |
+
// Template de mensajes
|
19 |
+
this.templateMsg = $("#template-message").contents("div.message").clone()
|
20 |
+
|
21 |
+
this.tabs = $(".tabs")
|
22 |
+
|
23 |
+
this.templateLabel = $("#template-label").contents("label").clone()
|
24 |
+
this.templateTab = $("#template-tab").contents("div.tab").clone()
|
25 |
+
this.chatbox = this.templateTab.find(".chat")
|
26 |
+
|
27 |
+
|
28 |
+
// Parametros base
|
29 |
this.active = false;
|
30 |
+
|
31 |
+
// ha tenido su primera interacción
|
32 |
this.interacted = false
|
33 |
|
34 |
+
this.crearVentanaChat(index, "Tab "+ index)
|
35 |
+
this.cargarEventos();
|
36 |
+
$(document).trigger("ventana:cambiada")
|
37 |
this.cargarChat(conversacion)
|
38 |
+
|
39 |
+
}
|
40 |
|
41 |
+
crearVentanaChat(index, nombre){
|
42 |
+
// coloca el valor al input y lo selecciona
|
43 |
+
let newLabel = this.templateLabel.clone()
|
44 |
+
newLabel.find("input").val(index).prop("checked", true);
|
45 |
+
newLabel.find("> div").text(nombre)
|
46 |
+
this.tabs.append(newLabel)
|
47 |
+
this.label = newLabel
|
48 |
|
49 |
+
// Crea la ventana de chat
|
50 |
+
let tempTab = this.templateTab.clone();
|
51 |
+
tempTab.attr("id", index)
|
52 |
+
$(".chats").append(tempTab)
|
53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
|
55 |
+
// establece los contextos de ventana
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
this.ctx = tempTab
|
57 |
this.chatbox = this.ctx.find(".chat");
|
58 |
+
this.ventanaSeleccionada()
|
59 |
+
}
|
60 |
|
61 |
+
ventanaSeleccionada(){
|
62 |
+
// cuando la ventana es seleccionada, cambia
|
63 |
+
$(".tab").removeClass("active")
|
64 |
+
this.label.find("input")[0].checked=true;
|
65 |
+
this.ctx.addClass("active");
|
66 |
+
}
|
67 |
+
|
68 |
+
cargarEventos(){
|
69 |
+
this.label.click(()=> this.ventanaSeleccionada())
|
70 |
+
this.ctx.find(".input-text").keypress((event) => {
|
71 |
+
if (!event.shiftKey && event.keyCode === 13) {
|
72 |
+
this.manejadorEnviar();
|
|
|
|
|
|
|
|
|
|
|
|
|
73 |
}
|
74 |
+
this.recalcularTextarea()
|
75 |
+
});
|
|
|
|
|
|
|
76 |
|
77 |
+
this.ctx.find(".input-send").click(() => this.manejadorEnviar());
|
78 |
+
this.ctx.find(".input-text").on("keyup,keydown", () => this.recalcularTextarea());
|
79 |
+
this.ctx.find(".input-delete").click(() => this.eliminarChat() )
|
80 |
+
this.ctx.find(".input-menu").click(() => $(document).trigger("mostrar:opciones"))
|
81 |
|
|
|
|
|
|
|
82 |
|
83 |
+
}
|
84 |
+
|
85 |
+
crearMensaje(texto, user){
|
86 |
+
this.active = this.templateMsg.clone();
|
87 |
+
switch(user){
|
88 |
+
case "system":
|
89 |
+
return;
|
90 |
+
case "user":
|
91 |
+
this.active.addClass("me");
|
92 |
+
this.active.find("div p").text(texto);
|
93 |
+
break;
|
94 |
+
case "assistant":
|
95 |
+
texto = this.procesarTexto(texto);
|
96 |
+
this.active.find("div p").html(texto);
|
97 |
+
Prism.highlightAllUnder(this.active[0])
|
98 |
+
break;
|
99 |
+
case "loading":
|
100 |
+
this.active.find("div p").html('<div class="loader-wrap"><span class="loader"></span></div>');
|
101 |
+
break;
|
102 |
}
|
103 |
+
this.chatbox.append(this.active);
|
104 |
+
this.chatbox.scrollTop(this.chatbox[0].scrollHeight);
|
105 |
+
this.interacted=true
|
106 |
+
return this.active
|
107 |
}
|
108 |
|
109 |
manejadorEnviar(){
|
110 |
let mensaje = this.ctx.find(".input-text").val();
|
111 |
+
this.ctx.find("button").prop("disabled", true);
|
112 |
+
this.ctx.find("textarea").prop("disabled", true);
|
113 |
if(mensaje==""){
|
114 |
return false;
|
115 |
}
|
116 |
+
if(this.conversacion.length==0){
|
117 |
+
this.conversacion.push({role:"system", content: this.chatHandler.config.assistantPrompt})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
118 |
}
|
119 |
+
this.conversacion.push({role: "user", content: mensaje})
|
120 |
+
self = this
|
121 |
+
this.crearMensaje(mensaje, "user");
|
122 |
+
$(document).trigger("chat:enviar", {ctx: self, conversacion:this.conversacion})
|
123 |
}
|
|
|
124 |
|
125 |
+
respuestaInicio(){
|
126 |
+
this.crearMensaje("", "loading");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
127 |
}
|
128 |
|
|
|
|
|
|
|
129 |
respuestaStatus(mensaje, modo=false){
|
130 |
let temp = $("<div></div>")
|
131 |
temp.text(mensaje)
|
132 |
+
if(!this.active.find(".loader").hasClass("firststage")){
|
133 |
+
this.active.find(".loader").addClass("firststage")
|
134 |
+
}
|
135 |
switch(modo){
|
136 |
case "enlinea":
|
137 |
this.active.find("div p div:last-child").text(this.active.find("div p div:last-child").text() + mensaje);
|
|
|
142 |
break;
|
143 |
default:
|
144 |
this.active.find("div p").append(temp)
|
145 |
+
}
|
146 |
+
this.chatbox.scrollTop(this.chatbox[0].scrollHeight);
|
|
|
|
|
147 |
}
|
148 |
|
149 |
+
respuestaMensaje(data){
|
150 |
+
let mensaje = data.content
|
151 |
this.active.find("div p").html("");
|
152 |
mensaje = this.procesarTexto(mensaje);
|
153 |
this.active.find("div p").html(mensaje);
|
154 |
Prism.highlightAllUnder(this.active[0]);
|
155 |
+
this.chatbox.scrollTop(this.chatbox[0].scrollHeight);
|
156 |
+
|
157 |
+
this.conversacion.push(data)
|
158 |
this.active = false;
|
159 |
+
this.interacted = true;
|
160 |
+
|
161 |
this.ctx.find("button").prop("disabled", false);
|
162 |
this.ctx.find("textarea").prop("disabled", false);
|
163 |
+
this.ctx.find("textarea").val("")
|
164 |
this.ctx.find("textarea").focus();
|
165 |
|
166 |
+
$(document).trigger("chat:salvar", {index: this.index, conversacion: this.conversacion})
|
167 |
+
|
168 |
}
|
169 |
|
170 |
respuestaError(error){
|
171 |
this.ctx.find("button").prop("disabled", false);
|
172 |
this.ctx.find("textarea").prop("disabled", false);
|
173 |
+
this.ctx.find("textarea").val("")
|
174 |
this.ctx.find("textarea").focus();
|
175 |
if(error.hasOwnProperty("responseJSON")){
|
176 |
this.active.find("div p").html(error.responseJSON.detail)
|
|
|
191 |
}
|
192 |
this.active = false;
|
193 |
this.chatbox.scrollTop(this.chatbox[0].scrollHeight)
|
194 |
+
}
|
195 |
+
|
196 |
+
cargarChat(conversacion){
|
197 |
+
for(let mensaje of this.conversacion){
|
198 |
+
this.crearMensaje(mensaje.content, mensaje.role)
|
199 |
+
}
|
200 |
+
}
|
201 |
|
202 |
+
eliminarChat(){
|
203 |
+
if(confirm("¿Estás seguro que quieres eliminar esta conversación?")){
|
204 |
+
this.label.remove()
|
205 |
+
this.ctx.remove()
|
206 |
+
$(document).trigger("chat:eliminar", {ctx:this.ctx, index:this.index})
|
207 |
+
}
|
208 |
+
}
|
209 |
|
210 |
+
recalcularTextarea(){
|
211 |
+
this.ctx.find(".input-box").css("height", "30px");
|
212 |
+
let height = parseInt((this.ctx.find(".input-text").prop('scrollHeight')+15)/15)*15;
|
213 |
+
this.ctx.find(".input-box").css("height", height+"px");
|
214 |
+
height -= 30;
|
215 |
+
this.ctx.find(".chat").css("--textarea", height+"px");
|
216 |
+
}
|
217 |
|
218 |
+
procesarTexto(texto){
|
219 |
+
let resultado = "";
|
220 |
+
let codigo = false;
|
221 |
+
for(let actual of texto.split("```")){
|
222 |
+
if(codigo){
|
223 |
+
let temp = actual.split("\n",1);
|
224 |
+
resultado += "<pre><code class='language-";
|
225 |
+
resultado += temp[0].length>1?temp[0]:"none";
|
226 |
+
temp = $("<div></div>").text(actual.substr(temp[0].length+1)).html()
|
227 |
+
resultado += "'>"+temp+"</code></pre>";
|
228 |
+
}else{
|
229 |
+
resultado += $("<div></div>").text(actual).html().replace(/`([^`]+?)`/gm, "<b>$1</b>").replace(/\n/g, "<br>");
|
230 |
+
resultado = resultado.replace(/\[(.*?)\]\((.*?)\)/gm, "[<a href='$2' target='_blank'>$1</a>]")
|
231 |
+
}
|
232 |
+
codigo = !codigo;
|
233 |
+
}
|
234 |
+
|
235 |
+
return resultado
|
236 |
}
|
237 |
|
238 |
+
|
239 |
+
|
240 |
+
|
241 |
+
|
242 |
|
243 |
|
244 |
|
templates/PrivacyPolicy.html
ADDED
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
3 |
+
<title>Privacy Policy</title>
|
4 |
+
|
5 |
+
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
7 |
+
<style>body{background:#fff;color:#000;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;font-size:16px;line-height:28px;margin:0}h1,h2,h3,h4,h5,h6,li,p{margin:0 0 16px}h1{font-size:40px;line-height:60px}h1,h2{font-weight:700}h2{font-size:32px;line-height:48px}h3{font-size:24px;line-height:36px}h3,h4{font-weight:700}h4{font-size:20px;line-height:30px}h5,h6{font-size:16px;line-height:24px;font-weight:700}a{text-decoration:none;cursor:pointer;color:#000}a:hover,a[rel~=nofollow]{text-decoration:underline}a[rel~=nofollow]{color:#008461}a[rel~=nofollow]:hover{text-decoration:none}.visible{display:block}.hidden{display:none}.page{width:100%}.container{position:relative;width:90%;max-width:1024px;margin:0 auto}.header{color:#000;padding:16px 0;margin:0 0 16px}.header .title{font-size:24px;line-height:36px;font-weight:700;margin:0}.translations-list-container{padding-bottom:8px;margin:0 0 16px}.translations-list-container .translations-list{list-style:none;margin:0;padding:0}.translations-list-container .translations-list .translations-list-item{display:inline-block;padding:0;margin:0 8px 8px 0;font-weight:700;color:#008461}.translations-list-container .translations-list .translations-list-item a{display:inline-block;color:#008461;border:2px solid #008461;border-radius:4px;padding:4px 8px}.translations-content-container{padding-top:16px;border-top:1px solid #eee}.footer{border-top:1px solid #eee;margin:32px 0 0;padding:16px 0}</style>
|
8 |
+
<meta name="robots" content="noindex">
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<div class="page">
|
12 |
+
<div class="header">
|
13 |
+
<div class="container">
|
14 |
+
<p class="title">Privacy Policy for Chatsito</p>
|
15 |
+
</div>
|
16 |
+
</div>
|
17 |
+
<div class="translations-content-container">
|
18 |
+
<div class="container">
|
19 |
+
<div class="tab-content translations-content-item en visible" id="en">
|
20 |
+
<h1>Privacy Policy</h1>
|
21 |
+
<p>Last updated: December 01, 2023</p>
|
22 |
+
<p>This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You.</p>
|
23 |
+
<p>We use Your Personal data to provide and improve the Service. By using the Service, You agree to the collection and use of information in accordance with this Privacy Policy. This Privacy Policy has been created with the help of the <a href="https://www.privacypolicies.com/privacy-policy-generator/" target="_blank">Privacy Policy Generator</a>.</p>
|
24 |
+
<h2>Interpretation and Definitions</h2>
|
25 |
+
<h3>Interpretation</h3>
|
26 |
+
<p>The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural.</p>
|
27 |
+
<h3>Definitions</h3>
|
28 |
+
<p>For the purposes of this Privacy Policy:</p>
|
29 |
+
<ul>
|
30 |
+
<li>
|
31 |
+
<p><strong>Account</strong> means a unique account created for You to access our Service or parts of our Service.</p>
|
32 |
+
</li>
|
33 |
+
<li>
|
34 |
+
<p><strong>Affiliate</strong> means an entity that controls, is controlled by or is under common control with a party, where "control" means ownership of 50% or more of the shares, equity interest or other securities entitled to vote for election of directors or other managing authority.</p>
|
35 |
+
</li>
|
36 |
+
<li>
|
37 |
+
<p><strong>Company</strong> (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to Chatsito.</p>
|
38 |
+
</li>
|
39 |
+
<li>
|
40 |
+
<p><strong>Cookies</strong> are small files that are placed on Your computer, mobile device or any other device by a website, containing the details of Your browsing history on that website among its many uses.</p>
|
41 |
+
</li>
|
42 |
+
<li>
|
43 |
+
<p><strong>Country</strong> refers to: Venezuela</p>
|
44 |
+
</li>
|
45 |
+
<li>
|
46 |
+
<p><strong>Device</strong> means any device that can access the Service such as a computer, a cellphone or a digital tablet.</p>
|
47 |
+
</li>
|
48 |
+
<li>
|
49 |
+
<p><strong>Personal Data</strong> is any information that relates to an identified or identifiable individual.</p>
|
50 |
+
</li>
|
51 |
+
<li>
|
52 |
+
<p><strong>Service</strong> refers to the Website.</p>
|
53 |
+
</li>
|
54 |
+
<li>
|
55 |
+
<p><strong>Service Provider</strong> means any natural or legal person who processes the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used.</p>
|
56 |
+
</li>
|
57 |
+
<li>
|
58 |
+
<p><strong>Usage Data</strong> refers to data collected automatically, either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit).</p>
|
59 |
+
</li>
|
60 |
+
<li>
|
61 |
+
<p><strong>Website</strong> refers to Chatsito, accessible from <a href="https://mgldzm-chatlocal.hf.space/" rel="external nofollow noopener" target="_blank">https://mgldzm-chatlocal.hf.space/</a></p>
|
62 |
+
</li>
|
63 |
+
<li>
|
64 |
+
<p><strong>You</strong> means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.</p>
|
65 |
+
</li>
|
66 |
+
</ul>
|
67 |
+
<h2>Collecting and Using Your Personal Data</h2>
|
68 |
+
<h3>Types of Data Collected</h3>
|
69 |
+
<h4>Personal Data</h4>
|
70 |
+
<p>While using Our Service, We may ask You to provide Us with certain personally identifiable information that can be used to contact or identify You. Personally identifiable information may include, but is not limited to:</p>
|
71 |
+
<ul>
|
72 |
+
<li>
|
73 |
+
<p>Email address</p>
|
74 |
+
</li>
|
75 |
+
<li>
|
76 |
+
<p>First name and last name</p>
|
77 |
+
</li>
|
78 |
+
<li>
|
79 |
+
<p>Usage Data</p>
|
80 |
+
</li>
|
81 |
+
</ul>
|
82 |
+
<h4>Usage Data</h4>
|
83 |
+
<p>Usage Data is collected automatically when using the Service.</p>
|
84 |
+
<p>Usage Data may include information such as Your Device's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that You visit, the time and date of Your visit, the time spent on those pages, unique device identifiers and other diagnostic data.</p>
|
85 |
+
<p>When You access the Service by or through a mobile device, We may collect certain information automatically, including, but not limited to, the type of mobile device You use, Your mobile device unique ID, the IP address of Your mobile device, Your mobile operating system, the type of mobile Internet browser You use, unique device identifiers and other diagnostic data.</p>
|
86 |
+
<p>We may also collect information that Your browser sends whenever You visit our Service or when You access the Service by or through a mobile device.</p>
|
87 |
+
<h4>Tracking Technologies and Cookies</h4>
|
88 |
+
<p>We use Cookies and similar tracking technologies to track the activity on Our Service and store certain information. Tracking technologies used are beacons, tags, and scripts to collect and track information and to improve and analyze Our Service. The technologies We use may include:</p>
|
89 |
+
<ul>
|
90 |
+
<li><strong>Cookies or Browser Cookies.</strong> A cookie is a small file placed on Your Device. You can instruct Your browser to refuse all Cookies or to indicate when a Cookie is being sent. However, if You do not accept Cookies, You may not be able to use some parts of our Service. Unless you have adjusted Your browser setting so that it will refuse Cookies, our Service may use Cookies.</li>
|
91 |
+
<li><strong>Web Beacons.</strong> Certain sections of our Service and our emails may contain small electronic files known as web beacons (also referred to as clear gifs, pixel tags, and single-pixel gifs) that permit the Company, for example, to count users who have visited those pages or opened an email and for other related website statistics (for example, recording the popularity of a certain section and verifying system and server integrity).</li>
|
92 |
+
</ul>
|
93 |
+
<p>Cookies can be "Persistent" or "Session" Cookies. Persistent Cookies remain on Your personal computer or mobile device when You go offline, while Session Cookies are deleted as soon as You close Your web browser. Learn more about cookies on the <a href="https://www.privacypolicies.com/blog/privacy-policy-template/#Use_Of_Cookies_Log_Files_And_Tracking" target="_blank">Privacy Policies website</a> article.</p>
|
94 |
+
<p>We use both Session and Persistent Cookies for the purposes set out below:</p>
|
95 |
+
<ul>
|
96 |
+
<li>
|
97 |
+
<p><strong>Necessary / Essential Cookies</strong></p>
|
98 |
+
<p>Type: Session Cookies</p>
|
99 |
+
<p>Administered by: Us</p>
|
100 |
+
<p>Purpose: These Cookies are essential to provide You with services available through the Website and to enable You to use some of its features. They help to authenticate users and prevent fraudulent use of user accounts. Without these Cookies, the services that You have asked for cannot be provided, and We only use these Cookies to provide You with those services.</p>
|
101 |
+
</li>
|
102 |
+
<li>
|
103 |
+
<p><strong>Cookies Policy / Notice Acceptance Cookies</strong></p>
|
104 |
+
<p>Type: Persistent Cookies</p>
|
105 |
+
<p>Administered by: Us</p>
|
106 |
+
<p>Purpose: These Cookies identify if users have accepted the use of cookies on the Website.</p>
|
107 |
+
</li>
|
108 |
+
<li>
|
109 |
+
<p><strong>Functionality Cookies</strong></p>
|
110 |
+
<p>Type: Persistent Cookies</p>
|
111 |
+
<p>Administered by: Us</p>
|
112 |
+
<p>Purpose: These Cookies allow us to remember choices You make when You use the Website, such as remembering your login details or language preference. The purpose of these Cookies is to provide You with a more personal experience and to avoid You having to re-enter your preferences every time You use the Website.</p>
|
113 |
+
</li>
|
114 |
+
</ul>
|
115 |
+
<p>For more information about the cookies we use and your choices regarding cookies, please visit our Cookies Policy or the Cookies section of our Privacy Policy.</p>
|
116 |
+
<h3>Use of Your Personal Data</h3>
|
117 |
+
<p>The Company may use Personal Data for the following purposes:</p>
|
118 |
+
<ul>
|
119 |
+
<li>
|
120 |
+
<p><strong>To provide and maintain our Service</strong>, including to monitor the usage of our Service.</p>
|
121 |
+
</li>
|
122 |
+
<li>
|
123 |
+
<p><strong>To manage Your Account:</strong> to manage Your registration as a user of the Service. The Personal Data You provide can give You access to different functionalities of the Service that are available to You as a registered user.</p>
|
124 |
+
</li>
|
125 |
+
<li>
|
126 |
+
<p><strong>For the performance of a contract:</strong> the development, compliance and undertaking of the purchase contract for the products, items or services You have purchased or of any other contract with Us through the Service.</p>
|
127 |
+
</li>
|
128 |
+
<li>
|
129 |
+
<p><strong>To contact You:</strong> To contact You by email, telephone calls, SMS, or other equivalent forms of electronic communication, such as a mobile application's push notifications regarding updates or informative communications related to the functionalities, products or contracted services, including the security updates, when necessary or reasonable for their implementation.</p>
|
130 |
+
</li>
|
131 |
+
<li>
|
132 |
+
<p><strong>To provide You</strong> with news, special offers and general information about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless You have opted not to receive such information.</p>
|
133 |
+
</li>
|
134 |
+
<li>
|
135 |
+
<p><strong>To manage Your requests:</strong> To attend and manage Your requests to Us.</p>
|
136 |
+
</li>
|
137 |
+
<li>
|
138 |
+
<p><strong>For business transfers:</strong> We may use Your information to evaluate or conduct a merger, divestiture, restructuring, reorganization, dissolution, or other sale or transfer of some or all of Our assets, whether as a going concern or as part of bankruptcy, liquidation, or similar proceeding, in which Personal Data held by Us about our Service users is among the assets transferred.</p>
|
139 |
+
</li>
|
140 |
+
<li>
|
141 |
+
<p><strong>For other purposes</strong>: We may use Your information for other purposes, such as data analysis, identifying usage trends, determining the effectiveness of our promotional campaigns and to evaluate and improve our Service, products, services, marketing and your experience.</p>
|
142 |
+
</li>
|
143 |
+
</ul>
|
144 |
+
<p>We may share Your personal information in the following situations:</p>
|
145 |
+
<ul>
|
146 |
+
<li><strong>With Service Providers:</strong> We may share Your personal information with Service Providers to monitor and analyze the use of our Service, to contact You.</li>
|
147 |
+
<li><strong>For business transfers:</strong> We may share or transfer Your personal information in connection with, or during negotiations of, any merger, sale of Company assets, financing, or acquisition of all or a portion of Our business to another company.</li>
|
148 |
+
<li><strong>With Affiliates:</strong> We may share Your information with Our affiliates, in which case we will require those affiliates to honor this Privacy Policy. Affiliates include Our parent company and any other subsidiaries, joint venture partners or other companies that We control or that are under common control with Us.</li>
|
149 |
+
<li><strong>With business partners:</strong> We may share Your information with Our business partners to offer You certain products, services or promotions.</li>
|
150 |
+
<li><strong>With other users:</strong> when You share personal information or otherwise interact in the public areas with other users, such information may be viewed by all users and may be publicly distributed outside.</li>
|
151 |
+
<li><strong>With Your consent</strong>: We may disclose Your personal information for any other purpose with Your consent.</li>
|
152 |
+
</ul>
|
153 |
+
<h3>Retention of Your Personal Data</h3>
|
154 |
+
<p>The Company will retain Your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use Your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies.</p>
|
155 |
+
<p>The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of Our Service, or We are legally obligated to retain this data for longer time periods.</p>
|
156 |
+
<h3>Transfer of Your Personal Data</h3>
|
157 |
+
<p>Your information, including Personal Data, is processed at the Company's operating offices and in any other places where the parties involved in the processing are located. It means that this information may be transferred to — and maintained on — computers located outside of Your state, province, country or other governmental jurisdiction where the data protection laws may differ than those from Your jurisdiction.</p>
|
158 |
+
<p>Your consent to this Privacy Policy followed by Your submission of such information represents Your agreement to that transfer.</p>
|
159 |
+
<p>The Company will take all steps reasonably necessary to ensure that Your data is treated securely and in accordance with this Privacy Policy and no transfer of Your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of Your data and other personal information.</p>
|
160 |
+
<h3>Delete Your Personal Data</h3>
|
161 |
+
<p>You have the right to delete or request that We assist in deleting the Personal Data that We have collected about You.</p>
|
162 |
+
<p>Our Service may give You the ability to delete certain information about You from within the Service.</p>
|
163 |
+
<p>You may update, amend, or delete Your information at any time by signing in to Your Account, if you have one, and visiting the account settings section that allows you to manage Your personal information. You may also contact Us to request access to, correct, or delete any personal information that You have provided to Us.</p>
|
164 |
+
<p>Please note, however, that We may need to retain certain information when we have a legal obligation or lawful basis to do so.</p>
|
165 |
+
<h3>Disclosure of Your Personal Data</h3>
|
166 |
+
<h4>Business Transactions</h4>
|
167 |
+
<p>If the Company is involved in a merger, acquisition or asset sale, Your Personal Data may be transferred. We will provide notice before Your Personal Data is transferred and becomes subject to a different Privacy Policy.</p>
|
168 |
+
<h4>Law enforcement</h4>
|
169 |
+
<p>Under certain circumstances, the Company may be required to disclose Your Personal Data if required to do so by law or in response to valid requests by public authorities (e.g. a court or a government agency).</p>
|
170 |
+
<h4>Other legal requirements</h4>
|
171 |
+
<p>The Company may disclose Your Personal Data in the good faith belief that such action is necessary to:</p>
|
172 |
+
<ul>
|
173 |
+
<li>Comply with a legal obligation</li>
|
174 |
+
<li>Protect and defend the rights or property of the Company</li>
|
175 |
+
<li>Prevent or investigate possible wrongdoing in connection with the Service</li>
|
176 |
+
<li>Protect the personal safety of Users of the Service or the public</li>
|
177 |
+
<li>Protect against legal liability</li>
|
178 |
+
</ul>
|
179 |
+
<h3>Security of Your Personal Data</h3>
|
180 |
+
<p>The security of Your Personal Data is important to Us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While We strive to use commercially acceptable means to protect Your Personal Data, We cannot guarantee its absolute security.</p>
|
181 |
+
<h2>Children's Privacy</h2>
|
182 |
+
<p>Our Service does not address anyone under the age of 13. We do not knowingly collect personally identifiable information from anyone under the age of 13. If You are a parent or guardian and You are aware that Your child has provided Us with Personal Data, please contact Us. If We become aware that We have collected Personal Data from anyone under the age of 13 without verification of parental consent, We take steps to remove that information from Our servers.</p>
|
183 |
+
<p>If We need to rely on consent as a legal basis for processing Your information and Your country requires consent from a parent, We may require Your parent's consent before We collect and use that information.</p>
|
184 |
+
<h2>Links to Other Websites</h2>
|
185 |
+
<p>Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit.</p>
|
186 |
+
<p>We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.</p>
|
187 |
+
<h2>Changes to this Privacy Policy</h2>
|
188 |
+
<p>We may update Our Privacy Policy from time to time. We will notify You of any changes by posting the new Privacy Policy on this page.</p>
|
189 |
+
<p>We will let You know via email and/or a prominent notice on Our Service, prior to the change becoming effective and update the "Last updated" date at the top of this Privacy Policy.</p>
|
190 |
+
<p>You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.</p>
|
191 |
+
<h2>Contact Us</h2>
|
192 |
+
<p>If you have any questions about this Privacy Policy, You can contact us:</p>
|
193 |
+
<ul>
|
194 |
+
<li>By visiting this page on our website: <a href="https://mgldzm-chatlocal.hf.space/privacy" rel="external nofollow noopener" target="_blank">https://mgldzm-chatlocal.hf.space/privacy</a></li>
|
195 |
+
</ul>
|
196 |
+
</div>
|
197 |
+
</div>
|
198 |
+
</div>
|
199 |
+
|
200 |
+
<div class="footer">
|
201 |
+
</div>
|
202 |
+
</div>
|
203 |
+
</body></html>
|
templates/login.html
ADDED
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
|
3 |
+
<html lang="ES">
|
4 |
+
<head>
|
5 |
+
<title>En espera</title>
|
6 |
+
|
7 |
+
<style>
|
8 |
+
html,
|
9 |
+
body {
|
10 |
+
background: #f1f1f1;
|
11 |
+
font-family: "Merriweather", sans-serif;
|
12 |
+
padding: 1em;
|
13 |
+
}
|
14 |
+
|
15 |
+
h1 {
|
16 |
+
text-align: center;
|
17 |
+
color: #a8a8a8;
|
18 |
+
}
|
19 |
+
|
20 |
+
form {
|
21 |
+
max-width: 600px;
|
22 |
+
text-align: center;
|
23 |
+
margin: 20px auto;
|
24 |
+
}
|
25 |
+
|
26 |
+
form button {
|
27 |
+
border: 0;
|
28 |
+
outline: 0;
|
29 |
+
padding: 1em;
|
30 |
+
display: block;
|
31 |
+
width: 100%;
|
32 |
+
margin-top: 1em;
|
33 |
+
font-family: "Merriweather", sans-serif;
|
34 |
+
resize: none;
|
35 |
+
|
36 |
+
}
|
37 |
+
|
38 |
+
#input-submit {
|
39 |
+
color: white;
|
40 |
+
background: #000;
|
41 |
+
cursor: pointer;
|
42 |
+
}
|
43 |
+
|
44 |
+
.half{
|
45 |
+
display: flex;
|
46 |
+
justify-content: center;
|
47 |
+
}
|
48 |
+
textarea {
|
49 |
+
height: 126px;
|
50 |
+
}
|
51 |
+
|
52 |
+
.gsi-material-button {
|
53 |
+
-moz-user-select: none;
|
54 |
+
-webkit-user-select: none;
|
55 |
+
-ms-user-select: none;
|
56 |
+
-webkit-appearance: none;
|
57 |
+
background-color: WHITE;
|
58 |
+
background-image: none;
|
59 |
+
border: 1px solid #747775;
|
60 |
+
-webkit-border-radius: 20px;
|
61 |
+
border-radius: 20px;
|
62 |
+
-webkit-box-sizing: border-box;
|
63 |
+
box-sizing: border-box;
|
64 |
+
color: #1f1f1f;
|
65 |
+
cursor: pointer;
|
66 |
+
font-family: 'Roboto', arial, sans-serif;
|
67 |
+
font-size: 14px;
|
68 |
+
height: 40px;
|
69 |
+
letter-spacing: 0.25px;
|
70 |
+
outline: none;
|
71 |
+
overflow: hidden;
|
72 |
+
padding: 0 12px;
|
73 |
+
position: relative;
|
74 |
+
text-align: center;
|
75 |
+
-webkit-transition: background-color .218s, border-color .218s, box-shadow .218s;
|
76 |
+
transition: background-color .218s, border-color .218s, box-shadow .218s;
|
77 |
+
vertical-align: middle;
|
78 |
+
white-space: nowrap;
|
79 |
+
width: auto;
|
80 |
+
max-width: 400px;
|
81 |
+
min-width: min-content;
|
82 |
+
}
|
83 |
+
|
84 |
+
.gsi-material-button .gsi-material-button-icon {
|
85 |
+
height: 20px;
|
86 |
+
margin-right: 12px;
|
87 |
+
min-width: 20px;
|
88 |
+
width: 20px;
|
89 |
+
}
|
90 |
+
|
91 |
+
.gsi-material-button .gsi-material-button-content-wrapper {
|
92 |
+
-webkit-align-items: center;
|
93 |
+
align-items: center;
|
94 |
+
display: flex;
|
95 |
+
-webkit-flex-direction: row;
|
96 |
+
flex-direction: row;
|
97 |
+
-webkit-flex-wrap: nowrap;
|
98 |
+
flex-wrap: nowrap;
|
99 |
+
height: 100%;
|
100 |
+
justify-content: space-between;
|
101 |
+
position: relative;
|
102 |
+
width: 100%;
|
103 |
+
}
|
104 |
+
|
105 |
+
.gsi-material-button .gsi-material-button-contents {
|
106 |
+
-webkit-flex-grow: 1;
|
107 |
+
flex-grow: 1;
|
108 |
+
font-family: 'Roboto', arial, sans-serif;
|
109 |
+
font-weight: 500;
|
110 |
+
overflow: hidden;
|
111 |
+
text-overflow: ellipsis;
|
112 |
+
vertical-align: top;
|
113 |
+
}
|
114 |
+
|
115 |
+
.gsi-material-button .gsi-material-button-state {
|
116 |
+
-webkit-transition: opacity .218s;
|
117 |
+
transition: opacity .218s;
|
118 |
+
bottom: 0;
|
119 |
+
left: 0;
|
120 |
+
opacity: 0;
|
121 |
+
position: absolute;
|
122 |
+
right: 0;
|
123 |
+
top: 0;
|
124 |
+
}
|
125 |
+
|
126 |
+
.gsi-material-button:disabled {
|
127 |
+
cursor: default;
|
128 |
+
background-color: #ffffff61;
|
129 |
+
border-color: #1f1f1f1f;
|
130 |
+
}
|
131 |
+
|
132 |
+
.gsi-material-button:disabled .gsi-material-button-contents {
|
133 |
+
opacity: 38%;
|
134 |
+
}
|
135 |
+
|
136 |
+
.gsi-material-button:disabled .gsi-material-button-icon {
|
137 |
+
opacity: 38%;
|
138 |
+
}
|
139 |
+
|
140 |
+
.gsi-material-button:not(:disabled):active .gsi-material-button-state,
|
141 |
+
.gsi-material-button:not(:disabled):focus .gsi-material-button-state {
|
142 |
+
background-color: #303030;
|
143 |
+
opacity: 12%;
|
144 |
+
}
|
145 |
+
|
146 |
+
.gsi-material-button:not(:disabled):hover {
|
147 |
+
-webkit-box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .30), 0 1px 3px 1px rgba(60, 64, 67, .15);
|
148 |
+
box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .30), 0 1px 3px 1px rgba(60, 64, 67, .15);
|
149 |
+
}
|
150 |
+
|
151 |
+
.gsi-material-button:not(:disabled):hover .gsi-material-button-state {
|
152 |
+
background-color: #303030;
|
153 |
+
opacity: 8%;
|
154 |
+
}
|
155 |
+
|
156 |
+
</style>
|
157 |
+
|
158 |
+
</head>
|
159 |
+
<body>
|
160 |
+
{% if not description %}
|
161 |
+
<h1>Haz click para iniciar sesion con Google Oauth</h1>
|
162 |
+
<div class="half cf">
|
163 |
+
|
164 |
+
<button class="gsi-material-button" onclick="iniciarSesion()">
|
165 |
+
<div class="gsi-material-button-state"></div>
|
166 |
+
<div class="gsi-material-button-content-wrapper">
|
167 |
+
<div class="gsi-material-button-icon">
|
168 |
+
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" xmlns:xlink="http://www.w3.org/1999/xlink" style="display: block;">
|
169 |
+
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"></path>
|
170 |
+
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"></path>
|
171 |
+
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"></path>
|
172 |
+
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"></path>
|
173 |
+
<path fill="none" d="M0 0h48v48H0z"></path>
|
174 |
+
</svg>
|
175 |
+
</div>
|
176 |
+
<span class="gsi-material-button-contents">Continue with Google</span>
|
177 |
+
<span style="display: none;">Continue with Google</span>
|
178 |
+
</div>
|
179 |
+
</button>
|
180 |
+
|
181 |
+
</div>
|
182 |
+
{% else %}
|
183 |
+
<h1>Aun no te he aprobado, avisame y espera</h1>
|
184 |
+
{% endif %}
|
185 |
+
|
186 |
+
|
187 |
+
<script>
|
188 |
+
var data = {
|
189 |
+
|
190 |
+
}
|
191 |
+
|
192 |
+
function iniciarSesion() {
|
193 |
+
var redirectUri = '{{redirecturi}}';
|
194 |
+
var clientId = '1051565752404-h4un77s3k4el280e589hn98s8ea4bg69.apps.googleusercontent.com';
|
195 |
+
let encodedData = btoa(JSON.stringify(data))
|
196 |
+
var url = 'https://accounts.google.com/o/oauth2/auth' +
|
197 |
+
'?response_type=code' +
|
198 |
+
'&client_id=' + encodeURIComponent(clientId) +
|
199 |
+
'&state=' + encodeURIComponent("data="+encodedData) +
|
200 |
+
'&redirect_uri=' + encodeURIComponent(redirectUri) +
|
201 |
+
'&scope=email%20profile';
|
202 |
+
|
203 |
+
|
204 |
+
var OauthWindow = window.open(url, "_blank", "popup");
|
205 |
+
OauthWindow.addEventListener("load", (event) => {
|
206 |
+
|
207 |
+
let location = event.target.location.pathname
|
208 |
+
|
209 |
+
if(location == "/oauth" && JSON.parse(event.target.documentElement.innerText).success){
|
210 |
+
event.currentTarget.close()
|
211 |
+
}
|
212 |
+
window.location.href = "/hold"
|
213 |
+
})
|
214 |
+
|
215 |
+
}
|
216 |
+
</script>
|
217 |
+
</body>
|
218 |
+
|
219 |
+
|
templates/main.html
ADDED
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
|
3 |
+
<html lang="ES">
|
4 |
+
<head>
|
5 |
+
<title> Chatbot </title>
|
6 |
+
<meta name="viewport" content="width=device-width, interactive-widget=resizes-content">
|
7 |
+
<link rel="shortcut icon" href="static/favicon.png" type="image/png">
|
8 |
+
<link rel="stylesheet" href="static/css/app.css?v={{ version }}">
|
9 |
+
<link rel="stylesheet" href="static/css/tabs.css?v={{ version }}">
|
10 |
+
<link rel="stylesheet" href="static/css/menu.css?v={{ version }}">
|
11 |
+
|
12 |
+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
13 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" rel="stylesheet" />
|
14 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js" integrity="sha512-9khQRAUBYEJDCDVP2yw3LRUQvjJ0Pjx0EShmaQjcHa6AXiOv6qHQu9lCAIR8O+/D8FtaCoJ2c0Tf9Xo7hYH01Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
15 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js" integrity="sha512-SkmBfuA2hqjzEVpmnMt/LINrjop3GKWqsuLSSB3e7iBmYK7JuWw4ldmmxwD9mdm2IRTTi0OxSAfEGvgEi0i2Kw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
16 |
+
|
17 |
+
<script src="static/js/windowHandler.js?v={{ version }}"></script>
|
18 |
+
<script src="static/js/chatHandler.js?v={{ version }}"></script>
|
19 |
+
<script src="static/js/SecurityHandler.js?v={{ version }}"></script>
|
20 |
+
|
21 |
+
|
22 |
+
|
23 |
+
</head>
|
24 |
+
<body>
|
25 |
+
|
26 |
+
<!-- Estructura principal -->
|
27 |
+
<div class="wrapper">
|
28 |
+
|
29 |
+
<!-- Pestañas -->
|
30 |
+
<div class="tabs">
|
31 |
+
<label class="tab-label-add" for="nuevoChat" >
|
32 |
+
+
|
33 |
+
<input type="button" class="tab-add" id="nuevoChat">
|
34 |
+
</label>
|
35 |
+
</div>
|
36 |
+
<!--/ Pestañas -->
|
37 |
+
|
38 |
+
<!-- Contenedor de chats -->
|
39 |
+
<div class="chats">
|
40 |
+
|
41 |
+
</div>
|
42 |
+
<!--/ Contenedor de chats -->
|
43 |
+
</div>
|
44 |
+
<!--/ Estructura principal -->
|
45 |
+
|
46 |
+
<!-- Templates del chat -->
|
47 |
+
<template id="template-label">
|
48 |
+
|
49 |
+
<!-- Pestaña -->
|
50 |
+
<label class="tab-label">
|
51 |
+
<div class="tab-name">Tab </div>
|
52 |
+
<input type="radio" name="tabs" class="tab-switch" value=0>
|
53 |
+
</label>
|
54 |
+
<!--/ Pestaña -->
|
55 |
+
</template>
|
56 |
+
<template id="template-tab">
|
57 |
+
<!-- Contenido Pestaña -->
|
58 |
+
<div class="tab">
|
59 |
+
<div class="chat"></div>
|
60 |
+
<div class='input-box' id="inputBox">
|
61 |
+
<textarea class='input-text' placeholder="Escribe aquí" type="text" autofocus=""></textarea>
|
62 |
+
<button class='input-send' ></button>
|
63 |
+
<button class='input-menu' ></button>
|
64 |
+
<button class='input-delete' ></button>
|
65 |
+
</div>
|
66 |
+
</div>
|
67 |
+
<!--/ Contenido Pestaña -->
|
68 |
+
</template>
|
69 |
+
<!--/ Templates del chat -->
|
70 |
+
<template id="template-message">
|
71 |
+
<div class="message">
|
72 |
+
<div>
|
73 |
+
<p></p>
|
74 |
+
</div>
|
75 |
+
</div>
|
76 |
+
</template>
|
77 |
+
<dialog class="inserted" id="inserted">
|
78 |
+
|
79 |
+
</dialog>
|
80 |
+
|
81 |
+
<dialog class="menu" id="menu">
|
82 |
+
<form method="dialog">
|
83 |
+
<div class="title">
|
84 |
+
Configuración
|
85 |
+
</div>
|
86 |
+
<button class="close" value="cancel" >
|
87 |
+
X
|
88 |
+
</button>
|
89 |
+
<div class="item">
|
90 |
+
<label for="temperature">Temperatura:
|
91 |
+
<span class="tooltip">Si no sabe que hace no toque</span>
|
92 |
+
</label>
|
93 |
+
<div class="range">
|
94 |
+
<input id="temperature" type="range" min=0 max=1 value=0.5 step=0.05>
|
95 |
+
<span>0.00</span>
|
96 |
+
</div>
|
97 |
+
</div>
|
98 |
+
<div class="item">
|
99 |
+
<label for="frequency_penalty">Frequency penalty:
|
100 |
+
<span class="tooltip">Si no sabe que hace no toque</span>
|
101 |
+
</label>
|
102 |
+
<div class="range">
|
103 |
+
<input id="frequency_penalty" type="range" min=-1 max=1 value=0 step=0.05>
|
104 |
+
<span>0.00</span>
|
105 |
+
</div>
|
106 |
+
</div>
|
107 |
+
<div class="item">
|
108 |
+
<label for="presence_penalty">Presence penalty:
|
109 |
+
<span class="tooltip">Si no sabe que hace no toque</span>
|
110 |
+
</label>
|
111 |
+
<div class="range">
|
112 |
+
<input id="presence_penalty" type="range" min=-1 max=1 value=0 step=0.05>
|
113 |
+
<span>0.00</span>
|
114 |
+
</div>
|
115 |
+
</div>
|
116 |
+
<hr>
|
117 |
+
<div class="item parent">
|
118 |
+
<label for="useTool">Usar herramientas:
|
119 |
+
<span class="tooltip">Activar o desactivar la capacidad del chatbot de usar herramientas</span>
|
120 |
+
</label>
|
121 |
+
<label class="switch">
|
122 |
+
<input id="useTool" type="checkbox" checked>
|
123 |
+
<span class="slider"></span>
|
124 |
+
</label>
|
125 |
+
|
126 |
+
</div>
|
127 |
+
|
128 |
+
{% for tool in tools %}
|
129 |
+
<div class="item useTool">
|
130 |
+
<label class="indented" for="tool:{{ tool['name'] }}">{{ tool['desc'] }}:</label>
|
131 |
+
<label class="switch">
|
132 |
+
<input id="tool:{{ tool['name'] }}" type="checkbox" checked>
|
133 |
+
<span class="slider"></span>
|
134 |
+
</label>
|
135 |
+
</div>
|
136 |
+
{% endfor %}
|
137 |
+
|
138 |
+
<hr>
|
139 |
+
<div class="item">
|
140 |
+
<label>Tipo Asistente:
|
141 |
+
<span class="tooltip">Modos predefinidos de comportamiento del bot, aplica solo a nuevos chats abiertos</span>
|
142 |
+
</label>
|
143 |
+
<div class="range">
|
144 |
+
<select id="assistant" >
|
145 |
+
{% for asistant in asistants %}
|
146 |
+
<option value="{{ asistant }}">{{ asistant }}</option>
|
147 |
+
{% endfor %}
|
148 |
+
</select>
|
149 |
+
|
150 |
+
</div>
|
151 |
+
|
152 |
+
|
153 |
+
</div>
|
154 |
+
<hr>
|
155 |
+
<div class="item">
|
156 |
+
<label>Tokens usados Mes:
|
157 |
+
<span class="tooltip">Que tantas vacaciones le has dado a tu cerebro este mes</span>
|
158 |
+
</label>
|
159 |
+
|
160 |
+
<span id="tokensUsedMonth">0</span>
|
161 |
+
</div>
|
162 |
+
<div class="item">
|
163 |
+
<label>Tokens usados Total:
|
164 |
+
<span class="tooltip">Que tantas vacaciones le has dado a tu cerebro desde quien sabe cuando...</span>
|
165 |
+
</label>
|
166 |
+
|
167 |
+
<span id="tokensUsedTotal">0</span>
|
168 |
+
</div>
|
169 |
+
<hr>
|
170 |
+
<div class="saveButton">
|
171 |
+
<button>Guardar</span>
|
172 |
+
</div>
|
173 |
+
</form>
|
174 |
+
</dialog>
|
175 |
+
<script>
|
176 |
+
|
177 |
+
|
178 |
+
|
179 |
+
|
180 |
+
|
181 |
+
$(document).ready(async function() {
|
182 |
+
|
183 |
+
var secHand = new Security()
|
184 |
+
await secHand.start()
|
185 |
+
|
186 |
+
let versionLocal = localStorage.getItem("version")
|
187 |
+
let versionRemota = "{{ version }}"
|
188 |
+
|
189 |
+
var cHand = new ChatGPT(secHand);
|
190 |
+
|
191 |
+
});
|
192 |
+
|
193 |
+
|
194 |
+
|
195 |
+
</script>
|
196 |
+
</body>
|
197 |
+
</html>
|
templates/no_access.html
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
|
3 |
+
<html lang="ES">
|
4 |
+
<head>
|
5 |
+
<title>En espera</title>
|
6 |
+
|
7 |
+
<style>
|
8 |
+
html,
|
9 |
+
body {
|
10 |
+
background: #f1f1f1;
|
11 |
+
font-family: "Merriweather", sans-serif;
|
12 |
+
padding: 1em;
|
13 |
+
}
|
14 |
+
|
15 |
+
h1 {
|
16 |
+
text-align: center;
|
17 |
+
color: #a8a8a8;
|
18 |
+
}
|
19 |
+
|
20 |
+
form {
|
21 |
+
max-width: 600px;
|
22 |
+
text-align: center;
|
23 |
+
margin: 20px auto;
|
24 |
+
}
|
25 |
+
|
26 |
+
form input, form textarea {
|
27 |
+
border: 0;
|
28 |
+
outline: 0;
|
29 |
+
padding: 1em;
|
30 |
+
display: block;
|
31 |
+
width: 100%;
|
32 |
+
margin-top: 1em;
|
33 |
+
font-family: "Merriweather", sans-serif;
|
34 |
+
resize: none;
|
35 |
+
|
36 |
+
}
|
37 |
+
|
38 |
+
#input-submit {
|
39 |
+
color: white;
|
40 |
+
background: #000;
|
41 |
+
cursor: pointer;
|
42 |
+
}
|
43 |
+
|
44 |
+
|
45 |
+
textarea {
|
46 |
+
height: 126px;
|
47 |
+
}
|
48 |
+
|
49 |
+
|
50 |
+
</style>
|
51 |
+
</head>
|
52 |
+
<body>
|
53 |
+
{% if not description %}
|
54 |
+
<h1>Dime quien eres y quien te dio el link</h1>
|
55 |
+
<form class="cf" method="POST", action="/hold">
|
56 |
+
<div class="half cf">
|
57 |
+
<textarea name="message" type="text" id="input-message" placeholder="Message"></textarea>
|
58 |
+
<input type="submit" value="Submit" id="input-submit">
|
59 |
+
</div>
|
60 |
+
</form>
|
61 |
+
{% else %}
|
62 |
+
<h1>Aun no te he aprobado, avisame y espera</h1>
|
63 |
+
{% endif %}
|
64 |
+
</body>
|
65 |
+
|
66 |
+
|