teszenofficial commited on
Commit
ca29546
·
verified ·
1 Parent(s): 492c0f6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +508 -133
app.py CHANGED
@@ -11,6 +11,7 @@ import uvicorn
11
  # ======================
12
  # CONFIGURACIÓN DE DISPOSITIVO (GPU/CPU)
13
  # ======================
 
14
  if torch.cuda.is_available():
15
  DEVICE = "cuda"
16
  print("✅ GPU NVIDIA detectada. Usando CUDA.")
@@ -23,9 +24,8 @@ MODEL_REPO = "teszenofficial/mtptz"
23
  # ======================
24
  # DESCARGA DEL MODELO
25
  # ======================
26
- print("--- SISTEMA MTP 1.1 ---")
27
  print(f"Descargando/Verificando modelo desde {MODEL_REPO}...")
28
-
29
  repo_path = snapshot_download(
30
  repo_id=MODEL_REPO,
31
  repo_type="model",
@@ -34,14 +34,17 @@ repo_path = snapshot_download(
34
 
35
  sys.path.insert(0, repo_path)
36
 
37
- from model import MTPMiniModel
38
- from tokenizer import MTPTokenizer
 
 
 
 
39
 
40
  # ======================
41
  # CARGA DEL MODELO
42
  # ======================
43
  print("Cargando modelo en memoria...")
44
-
45
  with open(os.path.join(repo_path, "mtp_mini.pkl"), "rb") as f:
46
  model_data = pickle.load(f)
47
 
@@ -61,111 +64,20 @@ model = MTPMiniModel(
61
  dropout=0.0
62
  )
63
 
 
64
  model.load_state_dict(model_data["model_state_dict"])
65
  model.to(DEVICE)
66
  model.eval()
67
-
68
  print(f"🚀 MTP 1.1 listo y corriendo en: {DEVICE.upper()}")
69
 
70
  # ======================
71
- # PARÁMETROS DE GENERACIÓN (de mtp_chat.py)
72
- # ======================
73
- TEMPERATURE = 0.7
74
- TOP_K = 40
75
- TOP_P = 0.92
76
- MAX_TOKENS = 150
77
- REPETITION_PENALTY = 1.2
78
-
79
- SHORT_RESPONSES = [
80
- "hola", "hey", "buenas", "buenos días", "buenas tardes",
81
- "buenas noches", "qué tal", "qué onda", "saludos", "holi"
82
- ]
83
-
84
- IDENTITY_QUERIES = [
85
- "quién eres", "cómo te llamas", "tu nombre",
86
- "qué eres", "eres ia", "preséntate"
87
- ]
88
-
89
- # ======================
90
- # GENERACIÓN REAL (FIX)
91
- # ======================
92
- def apply_repetition_penalty(logits, input_ids, penalty):
93
- for token_id in set(input_ids.view(-1).tolist()):
94
- if logits[0, token_id] > 0:
95
- logits[0, token_id] /= penalty
96
- else:
97
- logits[0, token_id] *= penalty
98
- return logits
99
-
100
-
101
- def generate_response(full_prompt: str):
102
- lower = full_prompt.lower()
103
- max_tokens = MAX_TOKENS
104
-
105
- if any(x in lower for x in SHORT_RESPONSES):
106
- max_tokens = 50
107
- elif any(x in lower for x in IDENTITY_QUERIES):
108
- max_tokens = 80
109
-
110
- tokens = [tokenizer.bos_id()] + tokenizer.encode(full_prompt)
111
- input_ids = torch.tensor([tokens], device=DEVICE)
112
-
113
- generated = []
114
-
115
- with torch.no_grad():
116
- for _ in range(max_tokens):
117
- input_cond = input_ids[:, -model.max_seq_len:]
118
- logits, _ = model(input_cond)
119
- logits = logits[:, -1, :]
120
-
121
- logits = apply_repetition_penalty(logits, input_ids, REPETITION_PENALTY)
122
- logits /= TEMPERATURE
123
-
124
- # Top-k
125
- topk_vals, _ = torch.topk(logits, TOP_K)
126
- logits[logits < topk_vals[:, -1].unsqueeze(1)] = -float("inf")
127
-
128
- # Top-p
129
- sorted_logits, sorted_indices = torch.sort(logits, descending=True)
130
- probs = torch.softmax(sorted_logits, dim=-1)
131
- cum_probs = torch.cumsum(probs, dim=-1)
132
-
133
- remove = cum_probs > TOP_P
134
- remove[..., 1:] = remove[..., :-1]
135
- remove[..., 0] = False
136
-
137
- indices_remove = remove.scatter(1, sorted_indices, remove)
138
- logits[indices_remove] = -float("inf")
139
-
140
- probs = torch.softmax(logits, dim=-1)
141
- next_token = torch.multinomial(probs, 1)
142
-
143
- if next_token.item() == tokenizer.eos_id():
144
- break
145
-
146
- generated.append(next_token.item())
147
- input_ids = torch.cat([input_ids, next_token], dim=1)
148
-
149
- if len(generated) > 30:
150
- tail = tokenizer.decode(generated[-5:])
151
- if any(p in tail for p in [".", "!", "?"]):
152
- break
153
-
154
- if not generated:
155
- return "Lo siento, no pude generar una respuesta."
156
-
157
- return tokenizer.decode(generated).strip()
158
-
159
-
160
- # ======================
161
- # FASTAPI
162
  # ======================
163
  app = FastAPI(title="MTP 1.1 API")
164
 
165
  class Prompt(BaseModel):
166
  text: str
167
 
168
-
169
  @app.post("/generate")
170
  def generate(prompt: Prompt):
171
  user_input = prompt.text.strip()
@@ -173,19 +85,38 @@ def generate(prompt: Prompt):
173
  return {"reply": ""}
174
 
175
  full_prompt = f"### Instrucción:\n{user_input}\n\n### Respuesta:\n"
176
- response = generate_response(full_prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
 
 
 
 
178
  if "###" in response:
179
  response = response.split("###")[0].strip()
180
 
181
  return {"reply": response}
182
 
183
  # ======================
184
- # UI (HTML COMPLETO)
185
  # ======================
186
  @app.get("/", response_class=HTMLResponse)
187
  def chat_ui():
188
- return """<!DOCTYPE html>
 
189
  <html lang="es">
190
  <head>
191
  <meta charset="UTF-8">
@@ -195,6 +126,7 @@ def chat_ui():
195
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
196
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
197
  <style>
 
198
  :root {
199
  --bg-color: #131314;
200
  --surface-color: #1E1F20;
@@ -202,56 +134,499 @@ def chat_ui():
202
  --text-primary: #e3e3e3;
203
  --text-secondary: #9aa0a6;
204
  --user-bubble: #282a2c;
 
205
  --logo-url: url('https://i.postimg.cc/yxS54PF3/IMG-3082.jpg');
206
  }
207
- *{box-sizing:border-box}
208
- body{margin:0;background:var(--bg-color);font-family:Inter;color:var(--text-primary);height:100vh;display:flex;flex-direction:column}
209
- header{padding:12px 20px;display:flex;align-items:center;gap:12px;border-bottom:1px solid rgba(255,255,255,.05)}
210
- .brand-logo{width:32px;height:32px;border-radius:50%;background-image:var(--logo-url);background-size:cover}
211
- .chat-scroll{flex:1;overflow-y:auto;padding:20px;max-width:850px;margin:auto;width:100%}
212
- .msg-row{margin-bottom:18px}
213
- .msg-row.user{text-align:right}
214
- .msg-row.bot{text-align:left}
215
- .msg-content{display:inline-block;padding:10px 16px;border-radius:18px;max-width:80%}
216
- .user .msg-content{background:var(--user-bubble)}
217
- .footer-container{padding:20px}
218
- .input-box{display:flex;background:var(--surface-color);border-radius:30px;padding:10px}
219
- #userInput{flex:1;background:transparent;border:none;color:white;font-size:16px}
220
- #mainBtn{background:white;border:none;border-radius:50%;width:36px;height:36px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  </style>
222
  </head>
223
  <body>
 
224
  <header>
225
- <div class="brand-logo"></div>
226
- <strong>MTP 1.1</strong>
 
 
 
 
227
  </header>
 
228
  <div id="chatScroll" class="chat-scroll">
229
- <div class="msg-row bot"><div class="msg-content">¡Hola! Soy MTP 1.1. ¿En qué puedo ayudarte?</div></div>
 
 
 
 
 
 
 
 
230
  </div>
 
231
  <div class="footer-container">
232
- <div class="input-box">
233
- <input id="userInput" placeholder="Escribe un mensaje..." />
234
- <button id="mainBtn" onclick="send()">➤</button>
235
- </div>
 
 
 
 
 
236
  </div>
 
237
  <script>
238
- async function send(){
239
- const i=document.getElementById('userInput');
240
- const t=i.value.trim();
241
- if(!t)return;
242
- i.value='';
243
- chatScroll.innerHTML+=`<div class="msg-row user"><div class="msg-content">${t}</div></div>`;
244
- const r=await fetch('/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:t})});
245
- const d=await r.json();
246
- chatScroll.innerHTML+=`<div class="msg-row bot"><div class="msg-content">${d.reply}</div></div>`;
247
- chatScroll.scrollTop=chatScroll.scrollHeight;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  </script>
250
  </body>
251
- </html>"""
 
252
 
253
  # ======================
254
  # ENTRYPOINT
255
  # ======================
256
  if __name__ == "__main__":
257
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
11
  # ======================
12
  # CONFIGURACIÓN DE DISPOSITIVO (GPU/CPU)
13
  # ======================
14
+ # Detectar automáticamente si hay una GPU NVIDIA disponible
15
  if torch.cuda.is_available():
16
  DEVICE = "cuda"
17
  print("✅ GPU NVIDIA detectada. Usando CUDA.")
 
24
  # ======================
25
  # DESCARGA DEL MODELO
26
  # ======================
27
+ print(f"--- SISTEMA MTP 1.1 ---")
28
  print(f"Descargando/Verificando modelo desde {MODEL_REPO}...")
 
29
  repo_path = snapshot_download(
30
  repo_id=MODEL_REPO,
31
  repo_type="model",
 
34
 
35
  sys.path.insert(0, repo_path)
36
 
37
+ try:
38
+ from model import MTPMiniModel
39
+ from tokenizer import MTPTokenizer
40
+ except ImportError:
41
+ print("Advertencia: Verifica la estructura de archivos del modelo.")
42
+ pass
43
 
44
  # ======================
45
  # CARGA DEL MODELO
46
  # ======================
47
  print("Cargando modelo en memoria...")
 
48
  with open(os.path.join(repo_path, "mtp_mini.pkl"), "rb") as f:
49
  model_data = pickle.load(f)
50
 
 
64
  dropout=0.0
65
  )
66
 
67
+ # Cargar pesos y mover a GPU
68
  model.load_state_dict(model_data["model_state_dict"])
69
  model.to(DEVICE)
70
  model.eval()
 
71
  print(f"🚀 MTP 1.1 listo y corriendo en: {DEVICE.upper()}")
72
 
73
  # ======================
74
+ # API FASTAPI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  # ======================
76
  app = FastAPI(title="MTP 1.1 API")
77
 
78
  class Prompt(BaseModel):
79
  text: str
80
 
 
81
  @app.post("/generate")
82
  def generate(prompt: Prompt):
83
  user_input = prompt.text.strip()
 
85
  return {"reply": ""}
86
 
87
  full_prompt = f"### Instrucción:\n{user_input}\n\n### Respuesta:\n"
88
+ tokens = [tokenizer.bos_id()] + tokenizer.encode(full_prompt)
89
+
90
+ # IMPORTANTE: Mover los inputs también a la GPU
91
+ input_ids = torch.tensor([tokens], device=DEVICE)
92
+
93
+ with torch.no_grad():
94
+ output_ids = model.generate(
95
+ input_ids,
96
+ max_new_tokens=150,
97
+ temperature=0.7,
98
+ top_k=50,
99
+ top_p=0.9
100
+ )
101
+
102
+ gen_tokens = output_ids[0, len(tokens):].tolist()
103
 
104
+ if tokenizer.eos_id() in gen_tokens:
105
+ gen_tokens = gen_tokens[:gen_tokens.index(tokenizer.eos_id())]
106
+
107
+ response = tokenizer.decode(gen_tokens).strip()
108
  if "###" in response:
109
  response = response.split("###")[0].strip()
110
 
111
  return {"reply": response}
112
 
113
  # ======================
114
+ # INTERFAZ WEB (FRONTEND MEJORADO)
115
  # ======================
116
  @app.get("/", response_class=HTMLResponse)
117
  def chat_ui():
118
+ return """
119
+ <!DOCTYPE html>
120
  <html lang="es">
121
  <head>
122
  <meta charset="UTF-8">
 
126
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
127
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
128
  <style>
129
+ /* --- VARIABLES & THEME --- */
130
  :root {
131
  --bg-color: #131314;
132
  --surface-color: #1E1F20;
 
134
  --text-primary: #e3e3e3;
135
  --text-secondary: #9aa0a6;
136
  --user-bubble: #282a2c;
137
+ --bot-actions-color: #c4c7c5;
138
  --logo-url: url('https://i.postimg.cc/yxS54PF3/IMG-3082.jpg');
139
  }
140
+
141
+ * { box-sizing: border-box; outline: none; -webkit-tap-highlight-color: transparent; }
142
+
143
+ body {
144
+ margin: 0;
145
+ background-color: var(--bg-color);
146
+ font-family: 'Inter', sans-serif;
147
+ color: var(--text-primary);
148
+ height: 100dvh;
149
+ display: flex;
150
+ flex-direction: column;
151
+ overflow: hidden;
152
+ }
153
+
154
+ /* --- HEADER --- */
155
+ header {
156
+ padding: 12px 20px;
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: space-between;
160
+ background: rgba(19, 19, 20, 0.85);
161
+ backdrop-filter: blur(12px);
162
+ position: fixed;
163
+ top: 0;
164
+ width: 100%;
165
+ z-index: 50;
166
+ border-bottom: 1px solid rgba(255,255,255,0.05);
167
+ }
168
+
169
+ .brand-wrapper {
170
+ display: flex;
171
+ align-items: center;
172
+ gap: 12px;
173
+ cursor: pointer;
174
+ }
175
+
176
+ .brand-logo {
177
+ width: 32px;
178
+ height: 32px;
179
+ border-radius: 50%;
180
+ background-image: var(--logo-url);
181
+ background-size: cover;
182
+ background-position: center;
183
+ border: 1px solid rgba(255,255,255,0.1);
184
+ }
185
+
186
+ .brand-text {
187
+ font-weight: 500;
188
+ font-size: 1.05rem;
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 8px;
192
+ }
193
+
194
+ .version-badge {
195
+ font-size: 0.75rem;
196
+ background: rgba(74, 158, 255, 0.15);
197
+ color: #8ab4f8;
198
+ padding: 2px 8px;
199
+ border-radius: 12px;
200
+ font-weight: 600;
201
+ }
202
+
203
+ /* --- CHAT AREA --- */
204
+ .chat-scroll {
205
+ flex: 1;
206
+ overflow-y: auto;
207
+ padding: 80px 20px 40px 20px;
208
+ display: flex;
209
+ flex-direction: column;
210
+ gap: 30px;
211
+ max-width: 850px;
212
+ margin: 0 auto;
213
+ width: 100%;
214
+ scroll-behavior: smooth;
215
+ }
216
+
217
+ /* Filas de Mensaje */
218
+ .msg-row {
219
+ display: flex;
220
+ gap: 16px;
221
+ width: 100%;
222
+ opacity: 0;
223
+ transform: translateY(10px);
224
+ animation: slideUpFade 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
225
+ }
226
+
227
+ .msg-row.user { justify-content: flex-end; }
228
+ .msg-row.bot { justify-content: flex-start; align-items: flex-start; }
229
+
230
+ /* Contenido */
231
+ .msg-content {
232
+ line-height: 1.6;
233
+ font-size: 1rem;
234
+ word-wrap: break-word;
235
+ max-width: 85%;
236
+ }
237
+
238
+ .user .msg-content {
239
+ background-color: var(--user-bubble);
240
+ padding: 10px 18px;
241
+ border-radius: 18px;
242
+ border-top-right-radius: 4px;
243
+ color: #fff;
244
+ }
245
+
246
+ .bot .msg-content-wrapper {
247
+ display: flex;
248
+ flex-direction: column;
249
+ gap: 8px;
250
+ width: 100%;
251
+ }
252
+
253
+ .bot .msg-text {
254
+ padding-top: 6px;
255
+ color: var(--text-primary);
256
+ }
257
+
258
+ /* Avatar Bot */
259
+ .bot-avatar {
260
+ width: 34px;
261
+ height: 34px;
262
+ min-width: 34px;
263
+ border-radius: 50%;
264
+ background-image: var(--logo-url);
265
+ background-size: cover;
266
+ box-shadow: 0 2px 6px rgba(0,0,0,0.2);
267
+ }
268
+
269
+ /* Acciones Bot */
270
+ .bot-actions {
271
+ display: flex;
272
+ gap: 10px;
273
+ opacity: 0;
274
+ transition: opacity 0.3s;
275
+ margin-top: 5px;
276
+ }
277
+
278
+ .action-btn {
279
+ background: transparent;
280
+ border: none;
281
+ color: var(--text-secondary);
282
+ cursor: pointer;
283
+ padding: 4px;
284
+ border-radius: 4px;
285
+ display: flex;
286
+ align-items: center;
287
+ transition: color 0.2s, background 0.2s;
288
+ }
289
+
290
+ .action-btn:hover {
291
+ color: var(--text-primary);
292
+ background: rgba(255,255,255,0.08);
293
+ }
294
+
295
+ .action-btn svg { width: 16px; height: 16px; fill: currentColor; }
296
+
297
+ /* Efecto Escritura (BOLITA AZUL) */
298
+ .typing-cursor::after {
299
+ content: '';
300
+ display: inline-block;
301
+ width: 10px;
302
+ height: 10px;
303
+ background: var(--accent-color);
304
+ border-radius: 50%;
305
+ margin-left: 5px;
306
+ vertical-align: middle;
307
+ animation: blink 1s infinite;
308
+ }
309
+
310
+ /* --- FOOTER & INPUT --- */
311
+ .footer-container {
312
+ padding: 0 20px 20px 20px;
313
+ background: linear-gradient(to top, var(--bg-color) 85%, transparent);
314
+ position: relative;
315
+ z-index: 60;
316
+ }
317
+
318
+ .input-box {
319
+ max-width: 850px;
320
+ margin: 0 auto;
321
+ background: var(--surface-color);
322
+ border-radius: 28px;
323
+ padding: 8px 10px 8px 20px;
324
+ display: flex;
325
+ align-items: center;
326
+ border: 1px solid rgba(255,255,255,0.1);
327
+ transition: border-color 0.2s, box-shadow 0.2s;
328
+ }
329
+
330
+ .input-box:focus-within {
331
+ border-color: rgba(74, 158, 255, 0.5);
332
+ box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.1);
333
+ }
334
+
335
+ #userInput {
336
+ flex: 1;
337
+ background: transparent;
338
+ border: none;
339
+ color: white;
340
+ font-size: 1rem;
341
+ font-family: inherit;
342
+ padding: 10px 0;
343
+ }
344
+
345
+ #mainBtn {
346
+ background: white;
347
+ color: black;
348
+ border: none;
349
+ width: 36px;
350
+ height: 36px;
351
+ border-radius: 50%;
352
+ display: flex;
353
+ align-items: center;
354
+ justify-content: center;
355
+ cursor: pointer;
356
+ margin-left: 8px;
357
+ transition: transform 0.2s;
358
+ }
359
+
360
+ #mainBtn:hover { transform: scale(1.05); }
361
+
362
+ .disclaimer {
363
+ text-align: center;
364
+ font-size: 0.75rem;
365
+ color: #666;
366
+ margin-top: 12px;
367
+ }
368
+
369
+ /* --- ANIMACIONES --- */
370
+ @keyframes slideUpFade {
371
+ from { opacity: 0; transform: translateY(15px); }
372
+ to { opacity: 1; transform: translateY(0); }
373
+ }
374
+
375
+ @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
376
+
377
+ @keyframes pulseAvatar {
378
+ 0% { box-shadow: 0 0 0 0 rgba(74, 158, 255, 0.4); }
379
+ 70% { box-shadow: 0 0 0 8px rgba(74, 158, 255, 0); }
380
+ 100% { box-shadow: 0 0 0 0 rgba(74, 158, 255, 0); }
381
+ }
382
+
383
+ .pulsing { animation: pulseAvatar 1.5s infinite; }
384
+
385
+ ::-webkit-scrollbar { width: 8px; }
386
+ ::-webkit-scrollbar-track { background: transparent; }
387
+ ::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
388
+
389
  </style>
390
  </head>
391
  <body>
392
+
393
  <header>
394
+ <div class="brand-wrapper" onclick="location.reload()">
395
+ <div class="brand-logo"></div>
396
+ <div class="brand-text">
397
+ MTP <span class="version-badge">1.1</span>
398
+ </div>
399
+ </div>
400
  </header>
401
+
402
  <div id="chatScroll" class="chat-scroll">
403
+ <!-- Bienvenida -->
404
+ <div class="msg-row bot" style="animation-delay: 0.1s;">
405
+ <div class="bot-avatar"></div>
406
+ <div class="msg-content-wrapper">
407
+ <div class="msg-text">
408
+ ¡Hola! Soy MTP 1.1. ¿En qué puedo ayudarte hoy?
409
+ </div>
410
+ </div>
411
+ </div>
412
  </div>
413
+
414
  <div class="footer-container">
415
+ <div class="input-box">
416
+ <input type="text" id="userInput" placeholder="Escribe un mensaje..." autocomplete="off">
417
+ <button id="mainBtn" onclick="handleBtnClick()">
418
+ <!-- Icono dinámico -->
419
+ </button>
420
+ </div>
421
+ <div class="disclaimer">
422
+ MTP puede cometer errores. Considera verificar la información importante.
423
+ </div>
424
  </div>
425
+
426
  <script>
427
+ const chatScroll = document.getElementById('chatScroll');
428
+ const userInput = document.getElementById('userInput');
429
+ const mainBtn = document.getElementById('mainBtn');
430
+
431
+ // Variables de Estado
432
+ let isGenerating = false;
433
+ let abortController = null;
434
+ let typingTimeout = null;
435
+ let lastUserPrompt = "";
436
+
437
+ // Iconos SVG
438
+ const ICON_SEND = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"></path></svg>`;
439
+ const ICON_STOP = `<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="0"><rect x="2" y="2" width="20" height="20" rx="4" ry="4"></rect></svg>`;
440
+
441
+ // Inicial
442
+ mainBtn.innerHTML = ICON_SEND;
443
+
444
+ // --- UTILS ---
445
+ function scrollToBottom() {
446
+ chatScroll.scrollTop = chatScroll.scrollHeight;
447
+ }
448
+
449
+ function setBtnState(state) {
450
+ if (state === 'sending') {
451
+ mainBtn.innerHTML = ICON_STOP;
452
+ isGenerating = true;
453
+ } else {
454
+ mainBtn.innerHTML = ICON_SEND;
455
+ isGenerating = false;
456
+ abortController = null;
457
+ }
458
  }
459
+
460
+ // --- CORE ---
461
+
462
+ function handleBtnClick() {
463
+ if (isGenerating) {
464
+ stopGeneration();
465
+ } else {
466
+ sendMessage();
467
+ }
468
+ }
469
+
470
+ function stopGeneration() {
471
+ if (abortController) abortController.abort();
472
+ if (typingTimeout) clearTimeout(typingTimeout);
473
+
474
+ // UI Limpieza
475
+ const activeCursor = document.querySelector('.typing-cursor');
476
+ if (activeCursor) activeCursor.classList.remove('typing-cursor');
477
+
478
+ const activeAvatar = document.querySelector('.pulsing');
479
+ if (activeAvatar) activeAvatar.classList.remove('pulsing');
480
+
481
+ setBtnState('idle');
482
+ userInput.focus();
483
+ }
484
+
485
+ async function sendMessage(textOverride = null) {
486
+ const text = textOverride || userInput.value.trim();
487
+ if (!text) return;
488
+
489
+ lastUserPrompt = text;
490
+
491
+ if (!textOverride) {
492
+ userInput.value = '';
493
+ addMessage(text, 'user');
494
+ }
495
+
496
+ setBtnState('sending');
497
+ abortController = new AbortController();
498
+
499
+ // Bot Placeholder
500
+ const botRow = document.createElement('div');
501
+ botRow.className = 'msg-row bot';
502
+
503
+ const avatar = document.createElement('div');
504
+ avatar.className = 'bot-avatar pulsing';
505
+
506
+ const wrapper = document.createElement('div');
507
+ wrapper.className = 'msg-content-wrapper';
508
+
509
+ const msgText = document.createElement('div');
510
+ msgText.className = 'msg-text';
511
+
512
+ wrapper.appendChild(msgText);
513
+ botRow.appendChild(avatar);
514
+ botRow.appendChild(wrapper);
515
+ chatScroll.appendChild(botRow);
516
+ scrollToBottom();
517
+
518
+ try {
519
+ const response = await fetch('/generate', {
520
+ method: 'POST',
521
+ headers: { 'Content-Type': 'application/json' },
522
+ body: JSON.stringify({ text: text }),
523
+ signal: abortController.signal
524
+ });
525
+
526
+ const data = await response.json();
527
+
528
+ if (!isGenerating) return;
529
+
530
+ avatar.classList.remove('pulsing');
531
+ const reply = data.reply || "No entendí eso.";
532
+
533
+ await typeWriter(msgText, reply);
534
+
535
+ if (isGenerating) {
536
+ addActions(wrapper, reply);
537
+ setBtnState('idle');
538
+ }
539
+
540
+ } catch (error) {
541
+ if (error.name === 'AbortError') {
542
+ msgText.textContent += " [Detenido]";
543
+ } else {
544
+ avatar.classList.remove('pulsing');
545
+ msgText.textContent = "Error de conexión.";
546
+ msgText.style.color = "#ff8b8b";
547
+ setBtnState('idle');
548
+ }
549
+ }
550
+ }
551
+
552
+ function addMessage(text, sender) {
553
+ const row = document.createElement('div');
554
+ row.className = `msg-row ${sender}`;
555
+ const content = document.createElement('div');
556
+ content.className = 'msg-content';
557
+ content.textContent = text;
558
+ row.appendChild(content);
559
+ chatScroll.appendChild(row);
560
+ scrollToBottom();
561
+ }
562
+
563
+ function typeWriter(element, text, speed = 12) {
564
+ return new Promise(resolve => {
565
+ let i = 0;
566
+ element.classList.add('typing-cursor');
567
+
568
+ function type() {
569
+ if (!isGenerating) {
570
+ element.classList.remove('typing-cursor');
571
+ resolve();
572
+ return;
573
+ }
574
+
575
+ if (i < text.length) {
576
+ element.textContent += text.charAt(i);
577
+ i++;
578
+ scrollToBottom();
579
+ typingTimeout = setTimeout(type, speed + Math.random() * 5);
580
+ } else {
581
+ element.classList.remove('typing-cursor');
582
+ resolve();
583
+ }
584
+ }
585
+ type();
586
+ });
587
+ }
588
+
589
+ function addActions(wrapperElement, textToCopy) {
590
+ const actionsDiv = document.createElement('div');
591
+ actionsDiv.className = 'bot-actions';
592
+
593
+ const copyBtn = document.createElement('button');
594
+ copyBtn.className = 'action-btn';
595
+ copyBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
596
+ copyBtn.onclick = () => {
597
+ navigator.clipboard.writeText(textToCopy);
598
+ };
599
+
600
+ const regenBtn = document.createElement('button');
601
+ regenBtn.className = 'action-btn';
602
+ regenBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6"></path><path d="M1 20v-6h6"></path><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`;
603
+ regenBtn.onclick = () => {
604
+ sendMessage(lastUserPrompt);
605
+ };
606
+
607
+ actionsDiv.appendChild(copyBtn);
608
+ actionsDiv.appendChild(regenBtn);
609
+ wrapperElement.appendChild(actionsDiv);
610
+
611
+ requestAnimationFrame(() => actionsDiv.style.opacity = "1");
612
+ scrollToBottom();
613
+ }
614
+
615
+ userInput.addEventListener('keydown', (e) => {
616
+ if (e.key === 'Enter') handleBtnClick();
617
+ });
618
+
619
+ window.onload = () => userInput.focus();
620
+
621
  </script>
622
  </body>
623
+ </html>
624
+ """
625
 
626
  # ======================
627
  # ENTRYPOINT
628
  # ======================
629
  if __name__ == "__main__":
630
  uvicorn.run(app, host="0.0.0.0", port=7860)
631
+
632
+