teszenofficial commited on
Commit
8b69ca1
·
verified ·
1 Parent(s): e8ff400

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +631 -0
app.py ADDED
@@ -0,0 +1,631 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ======================== app.py COMPLETO PARA HF SPACE ========================
2
+ # Space name: TeszenAI/MTP-1.1
3
+ # Este archivo contiene el backend y frontend completo
4
+
5
+ import os
6
+ import json
7
+ import torch
8
+ import torch.nn as nn
9
+ import torch.nn.functional as F
10
+ import math
11
+ from fastapi import FastAPI, HTTPException
12
+ from fastapi.responses import HTMLResponse, JSONResponse
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from pydantic import BaseModel
15
+ from typing import Optional, List
16
+ import sentencepiece as spm
17
+ from transformers import PreTrainedModel, PretrainedConfig
18
+ import uvicorn
19
+
20
+ # ======================== DEFINIR ARQUITECTURA DEL MODELO ========================
21
+ class LayerNorm(nn.Module):
22
+ def __init__(self, d_model: int, eps: float = 1e-5):
23
+ super().__init__()
24
+ self.weight = nn.Parameter(torch.ones(d_model))
25
+ self.bias = nn.Parameter(torch.zeros(d_model))
26
+ self.eps = eps
27
+
28
+ def forward(self, x):
29
+ mean = x.mean(-1, keepdim=True)
30
+ std = x.std(-1, keepdim=True)
31
+ return self.weight * (x - mean) / (std + self.eps) + self.bias
32
+
33
+ class MultiHeadAttention(nn.Module):
34
+ def __init__(self, d_model: int, n_heads: int, dropout: float = 0.1):
35
+ super().__init__()
36
+ assert d_model % n_heads == 0
37
+ self.d_model = d_model
38
+ self.n_heads = n_heads
39
+ self.d_k = d_model // n_heads
40
+ self.w_q = nn.Linear(d_model, d_model)
41
+ self.w_k = nn.Linear(d_model, d_model)
42
+ self.w_v = nn.Linear(d_model, d_model)
43
+ self.w_o = nn.Linear(d_model, d_model)
44
+ self.dropout = nn.Dropout(dropout)
45
+ self.scale = math.sqrt(self.d_k)
46
+
47
+ def forward(self, x, mask=None):
48
+ batch_size, seq_len, _ = x.shape
49
+ Q = self.w_q(x).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
50
+ K = self.w_k(x).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
51
+ V = self.w_v(x).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
52
+ scores = torch.matmul(Q, K.transpose(-2, -1)) / self.scale
53
+ if mask is not None:
54
+ scores = scores.masked_fill(mask == 0, float('-inf'))
55
+ attn_weights = F.softmax(scores, dim=-1)
56
+ attn_weights = self.dropout(attn_weights)
57
+ attn_output = torch.matmul(attn_weights, V)
58
+ attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model)
59
+ return self.w_o(attn_output)
60
+
61
+ class FeedForward(nn.Module):
62
+ def __init__(self, d_model: int, d_ff: int, dropout: float = 0.1):
63
+ super().__init__()
64
+ self.linear1 = nn.Linear(d_model, d_ff)
65
+ self.linear2 = nn.Linear(d_ff, d_model)
66
+ self.dropout = nn.Dropout(dropout)
67
+
68
+ def forward(self, x):
69
+ return self.linear2(self.dropout(F.gelu(self.linear1(x))))
70
+
71
+ class TransformerBlock(nn.Module):
72
+ def __init__(self, d_model: int, n_heads: int, d_ff: int, dropout: float = 0.1):
73
+ super().__init__()
74
+ self.attention = MultiHeadAttention(d_model, n_heads, dropout)
75
+ self.feed_forward = FeedForward(d_model, d_ff, dropout)
76
+ self.norm1 = LayerNorm(d_model)
77
+ self.norm2 = LayerNorm(d_model)
78
+ self.dropout1 = nn.Dropout(dropout)
79
+ self.dropout2 = nn.Dropout(dropout)
80
+
81
+ def forward(self, x, mask=None):
82
+ attn_output = self.attention(x, mask)
83
+ x = x + self.dropout1(attn_output)
84
+ x = self.norm1(x)
85
+ ff_output = self.feed_forward(x)
86
+ x = x + self.dropout2(ff_output)
87
+ x = self.norm2(x)
88
+ return x
89
+
90
+ class PositionalEncoding(nn.Module):
91
+ def __init__(self, d_model: int, max_len: int = 5000):
92
+ super().__init__()
93
+ pe = torch.zeros(max_len, d_model)
94
+ position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
95
+ div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
96
+ pe[:, 0::2] = torch.sin(position * div_term)
97
+ pe[:, 1::2] = torch.cos(position * div_term)
98
+ self.register_buffer('pe', pe.unsqueeze(0))
99
+
100
+ def forward(self, x):
101
+ return x + self.pe[:, :x.size(1), :]
102
+
103
+ class MTPModel(nn.Module):
104
+ def __init__(self, vocab_size: int, d_model: int = 128, n_heads: int = 4,
105
+ n_layers: int = 4, d_ff: int = 512, dropout: float = 0.1, max_len: int = 256):
106
+ super().__init__()
107
+ self.vocab_size = vocab_size
108
+ self.d_model = d_model
109
+ self.max_len = max_len
110
+ self.token_embedding = nn.Embedding(vocab_size, d_model)
111
+ self.pos_encoding = PositionalEncoding(d_model, max_len)
112
+ self.blocks = nn.ModuleList([
113
+ TransformerBlock(d_model, n_heads, d_ff, dropout) for _ in range(n_layers)
114
+ ])
115
+ self.norm = LayerNorm(d_model)
116
+ self.lm_head = nn.Linear(d_model, vocab_size)
117
+
118
+ def forward(self, x, mask=None):
119
+ if mask is None:
120
+ mask = torch.tril(torch.ones(x.size(1), x.size(1))).unsqueeze(0).unsqueeze(0).to(x.device)
121
+ x = self.token_embedding(x) * math.sqrt(self.d_model)
122
+ x = self.pos_encoding(x)
123
+ for block in self.blocks:
124
+ x = block(x, mask)
125
+ x = self.norm(x)
126
+ logits = self.lm_head(x)
127
+ return logits
128
+
129
+ # ======================== CARGAR MODELO Y TOKENIZADOR ========================
130
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
131
+
132
+ # Cargar configuración
133
+ with open("config.json", "r") as f:
134
+ config = json.load(f)
135
+
136
+ # Cargar modelo
137
+ model = MTPModel(**config).to(device)
138
+ model.load_state_dict(torch.load("mtp_model.pt", map_location=device))
139
+ model.eval()
140
+
141
+ # Cargar tokenizador
142
+ sp = spm.SentencePieceProcessor()
143
+ sp.load("mtp_tokenizer.model")
144
+
145
+ # ======================== FUNCIÓN DE GENERACIÓN ========================
146
+ def generate_text(prompt: str, max_length: int = 150, temperature: float = 0.8,
147
+ top_k: int = 50, top_p: float = 0.9):
148
+ """Genera texto con sampling avanzado"""
149
+ input_ids = sp.encode(prompt)
150
+ generated = input_ids.copy()
151
+
152
+ for _ in range(max_length):
153
+ # Preparar input
154
+ input_tensor = torch.tensor([generated[-model.max_len:]], dtype=torch.long).to(device)
155
+
156
+ # Forward pass
157
+ with torch.no_grad():
158
+ logits = model(input_tensor)
159
+ next_logits = logits[0, -1, :] / temperature
160
+
161
+ # Top-k filtering
162
+ if top_k > 0:
163
+ indices_to_remove = next_logits < torch.topk(next_logits, top_k)[0][..., -1, None]
164
+ next_logits[indices_to_remove] = float('-inf')
165
+
166
+ # Top-p (nucleus) filtering
167
+ if top_p < 1.0:
168
+ sorted_logits, sorted_indices = torch.sort(next_logits, descending=True)
169
+ cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
170
+ sorted_indices_to_remove = cumulative_probs > top_p
171
+ sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
172
+ sorted_indices_to_remove[..., 0] = 0
173
+ indices_to_remove = sorted_indices[sorted_indices_to_remove]
174
+ next_logits[indices_to_remove] = float('-inf')
175
+
176
+ # Sampling
177
+ probs = F.softmax(next_logits, dim=-1)
178
+ next_token = torch.multinomial(probs, num_samples=1).item()
179
+
180
+ # Parar en token EOS
181
+ if next_token == sp.eos_id():
182
+ break
183
+
184
+ generated.append(next_token)
185
+
186
+ return sp.decode(generated)
187
+
188
+ # ======================== FASTAPI APP ========================
189
+ app = FastAPI(title="MTP Chat - Mi Transformer Pequeño",
190
+ description="Modelo de lenguaje desde cero para conversación")
191
+
192
+ app.add_middleware(
193
+ CORSMiddleware,
194
+ allow_origins=["*"],
195
+ allow_credentials=True,
196
+ allow_methods=["*"],
197
+ allow_headers=["*"],
198
+ )
199
+
200
+ class ChatRequest(BaseModel):
201
+ message: str
202
+ temperature: Optional[float] = 0.8
203
+ max_length: Optional[int] = 150
204
+
205
+ class ChatResponse(BaseModel):
206
+ response: str
207
+
208
+ @app.post("/chat", response_model=ChatResponse)
209
+ async def chat(request: ChatRequest):
210
+ """Endpoint principal para chat"""
211
+ try:
212
+ if not request.message.strip():
213
+ raise HTTPException(status_code=400, detail="Mensaje vacío")
214
+
215
+ # Generar respuesta
216
+ response = generate_text(
217
+ request.message,
218
+ max_length=request.max_length,
219
+ temperature=request.temperature
220
+ )
221
+
222
+ # Limpiar respuesta (remover el prompt si se repite)
223
+ if response.startswith(request.message):
224
+ response = response[len(request.message):].strip()
225
+
226
+ return ChatResponse(response=response)
227
+ except Exception as e:
228
+ raise HTTPException(status_code=500, detail=str(e))
229
+
230
+ @app.get("/health")
231
+ async def health():
232
+ return {"status": "healthy", "model": "MTP-1.1"}
233
+
234
+ # ======================== FRONTEND REACT + HTML ========================
235
+ HTML_PAGE = """
236
+ <!DOCTYPE html>
237
+ <html lang="es">
238
+ <head>
239
+ <meta charset="UTF-8">
240
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
241
+ <title>MTP-1.1 - Tu Asistente IA</title>
242
+ <script src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.development.js"></script>
243
+ <script src="https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.development.js"></script>
244
+ <script src="https://cdn.jsdelivr.net/npm/babel-standalone@6.26.0/babel.min.js"></script>
245
+ <script src="https://cdn.jsdelivr.net/npm/axios@1.6.0/dist/axios.min.js"></script>
246
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
247
+ <style>
248
+ * {
249
+ margin: 0;
250
+ padding: 0;
251
+ box-sizing: border-box;
252
+ }
253
+
254
+ body {
255
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
256
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
257
+ min-height: 100vh;
258
+ display: flex;
259
+ justify-content: center;
260
+ align-items: center;
261
+ padding: 20px;
262
+ }
263
+
264
+ #root {
265
+ width: 100%;
266
+ max-width: 1200px;
267
+ height: 90vh;
268
+ background: rgba(255, 255, 255, 0.95);
269
+ border-radius: 30px;
270
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
271
+ overflow: hidden;
272
+ display: flex;
273
+ flex-direction: column;
274
+ }
275
+
276
+ /* Animaciones */
277
+ @keyframes gradient {
278
+ 0% { background-position: 0% 50%; }
279
+ 50% { background-position: 100% 50%; }
280
+ 100% { background-position: 0% 50%; }
281
+ }
282
+
283
+ @keyframes pulse {
284
+ 0%, 100% { transform: scale(1); opacity: 0.6; }
285
+ 50% { transform: scale(1.1); opacity: 1; }
286
+ }
287
+
288
+ @keyframes slideIn {
289
+ from {
290
+ opacity: 0;
291
+ transform: translateY(20px);
292
+ }
293
+ to {
294
+ opacity: 1;
295
+ transform: translateY(0);
296
+ }
297
+ }
298
+
299
+ @keyframes typingCursor {
300
+ 0%, 100% { border-right-color: transparent; }
301
+ 50% { border-right-color: #667eea; }
302
+ }
303
+
304
+ /* Scrollbar personalizada */
305
+ ::-webkit-scrollbar {
306
+ width: 8px;
307
+ }
308
+
309
+ ::-webkit-scrollbar-track {
310
+ background: #f1f1f1;
311
+ border-radius: 10px;
312
+ }
313
+
314
+ ::-webkit-scrollbar-thumb {
315
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
316
+ border-radius: 10px;
317
+ }
318
+
319
+ ::-webkit-scrollbar-thumb:hover {
320
+ background: #764ba2;
321
+ }
322
+ </style>
323
+ </head>
324
+ <body>
325
+ <div id="root"></div>
326
+
327
+ <script type="text/babel">
328
+ const { useState, useEffect, useRef, useCallback } = React;
329
+
330
+ // Componente de mensaje con animación de escritura
331
+ const Message = ({ text, isUser, isTyping, onTypingComplete }) => {
332
+ const [displayText, setDisplayText] = useState('');
333
+ const [currentIndex, setCurrentIndex] = useState(0);
334
+ const [showCursor, setShowCursor] = useState(true);
335
+
336
+ useEffect(() => {
337
+ if (!isTyping && text && !isUser) {
338
+ setDisplayText('');
339
+ setCurrentIndex(0);
340
+ let interval;
341
+ const timer = setTimeout(() => {
342
+ interval = setInterval(() => {
343
+ setCurrentIndex(prev => {
344
+ if (prev < text.length) {
345
+ setDisplayText(text.slice(0, prev + 1));
346
+ return prev + 1;
347
+ } else {
348
+ clearInterval(interval);
349
+ if (onTypingComplete) onTypingComplete();
350
+ return prev;
351
+ }
352
+ });
353
+ }, 20);
354
+ }, 500);
355
+ return () => {
356
+ clearTimeout(timer);
357
+ clearInterval(interval);
358
+ };
359
+ } else if (isUser && text) {
360
+ setDisplayText(text);
361
+ }
362
+ }, [text, isTyping, isUser]);
363
+
364
+ useEffect(() => {
365
+ if (isTyping && !isUser) {
366
+ const cursorInterval = setInterval(() => {
367
+ setShowCursor(prev => !prev);
368
+ }, 500);
369
+ return () => clearInterval(cursorInterval);
370
+ }
371
+ }, [isTyping]);
372
+
373
+ return (
374
+ <div className={`flex ${isUser ? 'justify-end' : 'justify-start'} mb-4 animate-slideIn`}>
375
+ <div className={`flex items-start space-x-3 max-w-[70%] ${isUser ? 'flex-row-reverse space-x-reverse' : 'flex-row'}`}>
376
+ <div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
377
+ isUser ? 'bg-gradient-to-r from-green-400 to-blue-500' : 'bg-gradient-to-r from-purple-500 to-pink-500'
378
+ }`}>
379
+ <i className={`fas ${isUser ? 'fa-user' : 'fa-robot'} text-white text-sm`}></i>
380
+ </div>
381
+ <div className={`rounded-2xl p-4 ${
382
+ isUser ? 'bg-gradient-to-r from-blue-500 to-purple-600 text-white' : 'bg-gray-100 text-gray-800'
383
+ } shadow-lg`}>
384
+ <p className="whitespace-pre-wrap break-words" style={{ fontFamily: 'inherit' }}>
385
+ {displayText}
386
+ {!isUser && isTyping && displayText.length === 0 && (
387
+ <span className="inline-block w-2 h-4 ml-1 bg-purple-500 animate-pulse"></span>
388
+ )}
389
+ {!isUser && !isTyping && displayText.length < text?.length && (
390
+ <span className="inline-block w-2 h-4 ml-1 bg-purple-500" style={{ animation: 'typingCursor 1s step-end infinite' }}></span>
391
+ )}
392
+ </p>
393
+ </div>
394
+ </div>
395
+ </div>
396
+ );
397
+ };
398
+
399
+ // Componente de pensamiento
400
+ const ThinkingIndicator = () => (
401
+ <div className="flex justify-start mb-4 animate-slideIn">
402
+ <div className="flex items-start space-x-3">
403
+ <div className="w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center">
404
+ <i className="fas fa-robot text-white text-sm"></i>
405
+ </div>
406
+ <div className="bg-gray-100 rounded-2xl p-4 shadow-lg">
407
+ <div className="flex space-x-2">
408
+ <div className="w-2 h-2 bg-purple-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
409
+ <div className="w-2 h-2 bg-purple-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
410
+ <div className="w-2 h-2 bg-purple-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
411
+ </div>
412
+ <p className="text-gray-500 text-sm mt-2">MTP está pensando...</p>
413
+ </div>
414
+ </div>
415
+ </div>
416
+ );
417
+
418
+ // Componente principal
419
+ const ChatApp = () => {
420
+ const [messages, setMessages] = useState([]);
421
+ const [input, setInput] = useState('');
422
+ const [isThinking, setIsThinking] = useState(false);
423
+ const [temperature, setTemperature] = useState(0.8);
424
+ const [isTyping, setIsTyping] = useState(false);
425
+ const messagesEndRef = useRef(null);
426
+ const inputRef = useRef(null);
427
+
428
+ const scrollToBottom = () => {
429
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
430
+ };
431
+
432
+ useEffect(() => {
433
+ scrollToBottom();
434
+ }, [messages, isThinking]);
435
+
436
+ useEffect(() => {
437
+ inputRef.current?.focus();
438
+ }, []);
439
+
440
+ const sendMessage = useCallback(async () => {
441
+ if (!input.trim() || isThinking || isTyping) return;
442
+
443
+ const userMessage = { text: input, isUser: true, timestamp: Date.now() };
444
+ setMessages(prev => [...prev, userMessage]);
445
+ setInput('');
446
+ setIsThinking(true);
447
+
448
+ try {
449
+ const response = await axios.post('/chat', {
450
+ message: input,
451
+ temperature: temperature,
452
+ max_length: 200
453
+ });
454
+
455
+ setIsThinking(false);
456
+ setIsTyping(true);
457
+ setMessages(prev => [...prev, {
458
+ text: response.data.response,
459
+ isUser: false,
460
+ timestamp: Date.now(),
461
+ isTyping: true
462
+ }]);
463
+ } catch (error) {
464
+ console.error('Error:', error);
465
+ setIsThinking(false);
466
+ setMessages(prev => [...prev, {
467
+ text: 'Lo siento, hubo un error al procesar tu mensaje. 😔',
468
+ isUser: false,
469
+ timestamp: Date.now()
470
+ }]);
471
+ }
472
+ }, [input, isThinking, isTyping, temperature]);
473
+
474
+ const handleTypingComplete = () => {
475
+ setIsTyping(false);
476
+ };
477
+
478
+ const clearChat = () => {
479
+ setMessages([]);
480
+ setIsThinking(false);
481
+ setIsTyping(false);
482
+ };
483
+
484
+ return (
485
+ <div className="flex flex-col h-full">
486
+ {/* Header */}
487
+ <div className="bg-gradient-to-r from-purple-600 to-pink-600 text-white p-6 shadow-lg">
488
+ <div className="flex items-center justify-between">
489
+ <div className="flex items-center space-x-3">
490
+ <div className="w-12 h-12 bg-white bg-opacity-20 rounded-full flex items-center justify-center">
491
+ <i className="fas fa-brain text-2xl"></i>
492
+ </div>
493
+ <div>
494
+ <h1 className="text-2xl font-bold">MTP-1.1</h1>
495
+ <p className="text-sm opacity-90">Mi Transformer Pequeño · IA desde cero</p>
496
+ </div>
497
+ </div>
498
+ <div className="flex items-center space-x-3">
499
+ <div className="flex items-center space-x-2 bg-white bg-opacity-20 rounded-full px-4 py-2">
500
+ <i className="fas fa-thermometer-half"></i>
501
+ <span className="text-sm">Temp:</span>
502
+ <input
503
+ type="range"
504
+ min="0.1"
505
+ max="1.5"
506
+ step="0.01"
507
+ value={temperature}
508
+ onChange={(e) => setTemperature(parseFloat(e.target.value))}
509
+ className="w-24"
510
+ />
511
+ <span className="text-sm font-mono">{temperature.toFixed(2)}</span>
512
+ </div>
513
+ <button
514
+ onClick={clearChat}
515
+ className="bg-white bg-opacity-20 hover:bg-opacity-30 rounded-full p-2 transition-all"
516
+ >
517
+ <i className="fas fa-trash-alt"></i>
518
+ </button>
519
+ </div>
520
+ </div>
521
+ </div>
522
+
523
+ {/* Messages Area */}
524
+ <div className="flex-1 overflow-y-auto p-6 bg-gray-50">
525
+ {messages.length === 0 && (
526
+ <div className="flex flex-col items-center justify-center h-full text-center">
527
+ <div className="w-32 h-32 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full flex items-center justify-center mb-6 animate-pulse">
528
+ <i className="fas fa-comments text-5xl text-white"></i>
529
+ </div>
530
+ <h2 className="text-3xl font-bold text-gray-700 mb-2">¡Hola! Soy MTP-1.1</h2>
531
+ <p className="text-gray-500 max-w-md">
532
+ Un modelo de lenguaje entrenado desde cero. Puedo ayudarte a generar texto,
533
+ responder preguntas y mantener conversaciones. ¿Qué te gustaría hablar?
534
+ </p>
535
+ <div className="grid grid-cols-2 gap-4 mt-8 max-w-lg">
536
+ {["¿Qué es la inteligencia artificial?", "Cuéntame un chiste", "Explica el machine learning", "Hola, ¿cómo estás?"].map((suggestion) => (
537
+ <button
538
+ key={suggestion}
539
+ onClick={() => setInput(suggestion)}
540
+ className="bg-white border border-purple-200 hover:border-purple-400 rounded-lg p-3 text-sm transition-all"
541
+ >
542
+ {suggestion}
543
+ </button>
544
+ ))}
545
+ </div>
546
+ </div>
547
+ )}
548
+
549
+ {messages.map((msg, idx) => (
550
+ <Message
551
+ key={idx}
552
+ text={msg.text}
553
+ isUser={msg.isUser}
554
+ isTyping={msg.isTyping && idx === messages.length - 1}
555
+ onTypingComplete={idx === messages.length - 1 ? handleTypingComplete : null}
556
+ />
557
+ ))}
558
+
559
+ {isThinking && <ThinkingIndicator />}
560
+ <div ref={messagesEndRef} />
561
+ </div>
562
+
563
+ {/* Input Area */}
564
+ <div className="p-6 bg-white border-t border-gray-200">
565
+ <div className="flex space-x-3">
566
+ <div className="flex-1 relative">
567
+ <textarea
568
+ ref={inputRef}
569
+ value={input}
570
+ onChange={(e) => setInput(e.target.value)}
571
+ onKeyPress={(e) => {
572
+ if (e.key === 'Enter' && !e.shiftKey) {
573
+ e.preventDefault();
574
+ sendMessage();
575
+ }
576
+ }}
577
+ placeholder="Escribe tu mensaje aquí..."
578
+ rows="1"
579
+ className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:border-purple-500 resize-none"
580
+ style={{ fontFamily: 'inherit' }}
581
+ />
582
+ </div>
583
+ <button
584
+ onClick={sendMessage}
585
+ disabled={!input.trim() || isThinking || isTyping}
586
+ className="bg-gradient-to-r from-purple-500 to-pink-500 text-white rounded-xl px-6 py-3 hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
587
+ >
588
+ <i className="fas fa-paper-plane"></i>
589
+ </button>
590
+ </div>
591
+ <div className="text-xs text-center text-gray-400 mt-3">
592
+ <i className="fas fa-microchip"></i> MTP-1.1 · Modelo desde cero ·
593
+ <i className="fas fa-brain ml-2"></i> ~4M parámetros
594
+ </div>
595
+ </div>
596
+ </div>
597
+ );
598
+ };
599
+
600
+ ReactDOM.render(<ChatApp />, document.getElementById('root'));
601
+ </script>
602
+
603
+ <style>
604
+ .animate-slideIn {
605
+ animation: slideIn 0.3s ease-out;
606
+ }
607
+
608
+ .animate-bounce {
609
+ animation: bounce 1.4s infinite;
610
+ }
611
+
612
+ @keyframes bounce {
613
+ 0%, 60%, 100% {
614
+ transform: translateY(0);
615
+ }
616
+ 30% {
617
+ transform: translateY(-10px);
618
+ }
619
+ }
620
+ </style>
621
+ </body>
622
+ </html>
623
+ """
624
+
625
+ @app.get("/")
626
+ async def root():
627
+ return HTMLResponse(HTML_PAGE)
628
+
629
+ # ======================== EJECUTAR APP ========================
630
+ if __name__ == "__main__":
631
+ uvicorn.run(app, host="0.0.0.0", port=7860)