Update app.py
Browse files
app.py
CHANGED
|
@@ -261,7 +261,7 @@ header {
|
|
| 261 |
.bot-actions {
|
| 262 |
display: flex;
|
| 263 |
gap: 10px;
|
| 264 |
-
opacity: 0;
|
| 265 |
transition: opacity 0.3s;
|
| 266 |
margin-top: 5px;
|
| 267 |
}
|
|
@@ -333,7 +333,7 @@ header {
|
|
| 333 |
padding: 10px 0;
|
| 334 |
}
|
| 335 |
|
| 336 |
-
#
|
| 337 |
background: white;
|
| 338 |
color: black;
|
| 339 |
border: none;
|
|
@@ -348,8 +348,7 @@ header {
|
|
| 348 |
transition: transform 0.2s;
|
| 349 |
}
|
| 350 |
|
| 351 |
-
#
|
| 352 |
-
#sendBtn:disabled { background: #555; cursor: default; transform: none; }
|
| 353 |
|
| 354 |
.disclaimer {
|
| 355 |
text-align: center;
|
|
@@ -374,7 +373,6 @@ header {
|
|
| 374 |
|
| 375 |
.pulsing { animation: pulseAvatar 1.5s infinite; }
|
| 376 |
|
| 377 |
-
/* Scrollbar */
|
| 378 |
::-webkit-scrollbar { width: 8px; }
|
| 379 |
::-webkit-scrollbar-track { background: transparent; }
|
| 380 |
::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
|
|
@@ -407,10 +405,8 @@ header {
|
|
| 407 |
<div class="footer-container">
|
| 408 |
<div class="input-box">
|
| 409 |
<input type="text" id="userInput" placeholder="Escribe un mensaje..." autocomplete="off">
|
| 410 |
-
<button id="
|
| 411 |
-
<
|
| 412 |
-
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"></path>
|
| 413 |
-
</svg>
|
| 414 |
</button>
|
| 415 |
</div>
|
| 416 |
<div class="disclaimer">
|
|
@@ -421,23 +417,71 @@ header {
|
|
| 421 |
<script>
|
| 422 |
const chatScroll = document.getElementById('chatScroll');
|
| 423 |
const userInput = document.getElementById('userInput');
|
| 424 |
-
const
|
| 425 |
|
|
|
|
| 426 |
let isGenerating = false;
|
| 427 |
-
let
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
|
| 429 |
// --- UTILS ---
|
| 430 |
function scrollToBottom() {
|
| 431 |
chatScroll.scrollTop = chatScroll.scrollHeight;
|
| 432 |
}
|
| 433 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
// --- CORE FUNCTIONS ---
|
| 435 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
async function sendMessage(textOverride = null) {
|
| 437 |
const text = textOverride || userInput.value.trim();
|
| 438 |
-
if (!text
|
| 439 |
|
| 440 |
-
// Guardar para regenerar
|
| 441 |
lastUserPrompt = text;
|
| 442 |
|
| 443 |
// UI Updates
|
|
@@ -446,22 +490,18 @@ async function sendMessage(textOverride = null) {
|
|
| 446 |
addMessage(text, 'user');
|
| 447 |
}
|
| 448 |
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
sendBtn.style.opacity = "0.5";
|
| 452 |
|
| 453 |
-
//
|
| 454 |
const botRow = document.createElement('div');
|
| 455 |
botRow.className = 'msg-row bot';
|
| 456 |
-
|
| 457 |
const avatar = document.createElement('div');
|
| 458 |
-
avatar.className = 'bot-avatar pulsing';
|
| 459 |
-
|
| 460 |
const wrapper = document.createElement('div');
|
| 461 |
wrapper.className = 'msg-content-wrapper';
|
| 462 |
-
|
| 463 |
const msgText = document.createElement('div');
|
| 464 |
-
msgText.className = 'msg-text';
|
| 465 |
|
| 466 |
wrapper.appendChild(msgText);
|
| 467 |
botRow.appendChild(avatar);
|
|
@@ -473,57 +513,66 @@ async function sendMessage(textOverride = null) {
|
|
| 473 |
const response = await fetch('/generate', {
|
| 474 |
method: 'POST',
|
| 475 |
headers: { 'Content-Type': 'application/json' },
|
| 476 |
-
body: JSON.stringify({ text: text })
|
|
|
|
| 477 |
});
|
| 478 |
|
| 479 |
const data = await response.json();
|
| 480 |
-
avatar.classList.remove('pulsing');
|
| 481 |
|
| 482 |
-
|
|
|
|
|
|
|
|
|
|
| 483 |
|
| 484 |
-
// Iniciar escritura
|
| 485 |
await typeWriter(msgText, reply);
|
| 486 |
|
| 487 |
-
//
|
| 488 |
-
|
|
|
|
|
|
|
| 489 |
|
| 490 |
} catch (error) {
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
}
|
| 500 |
}
|
| 501 |
|
| 502 |
function addMessage(text, sender) {
|
| 503 |
const row = document.createElement('div');
|
| 504 |
row.className = `msg-row ${sender}`;
|
| 505 |
-
|
| 506 |
const content = document.createElement('div');
|
| 507 |
content.className = 'msg-content';
|
| 508 |
content.textContent = text;
|
| 509 |
-
|
| 510 |
row.appendChild(content);
|
| 511 |
chatScroll.appendChild(row);
|
| 512 |
scrollToBottom();
|
| 513 |
}
|
| 514 |
|
| 515 |
-
// Efecto m谩quina de escribir
|
| 516 |
function typeWriter(element, text, speed = 12) {
|
| 517 |
return new Promise(resolve => {
|
| 518 |
let i = 0;
|
| 519 |
element.classList.add('typing-cursor');
|
| 520 |
|
| 521 |
function type() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
if (i < text.length) {
|
| 523 |
element.textContent += text.charAt(i);
|
| 524 |
i++;
|
| 525 |
scrollToBottom();
|
| 526 |
-
|
|
|
|
|
|
|
| 527 |
} else {
|
| 528 |
element.classList.remove('typing-cursor');
|
| 529 |
resolve();
|
|
@@ -533,59 +582,37 @@ function typeWriter(element, text, speed = 12) {
|
|
| 533 |
});
|
| 534 |
}
|
| 535 |
|
| 536 |
-
// A帽adir botones Copiar / Regenerar
|
| 537 |
function addActions(wrapperElement, textToCopy) {
|
| 538 |
const actionsDiv = document.createElement('div');
|
| 539 |
actionsDiv.className = 'bot-actions';
|
| 540 |
|
| 541 |
-
//
|
| 542 |
const copyBtn = document.createElement('button');
|
| 543 |
copyBtn.className = 'action-btn';
|
| 544 |
-
copyBtn.
|
| 545 |
-
copyBtn.innerHTML = `
|
| 546 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 547 |
-
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
| 548 |
-
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
| 549 |
-
</svg>
|
| 550 |
-
`;
|
| 551 |
copyBtn.onclick = () => {
|
| 552 |
navigator.clipboard.writeText(textToCopy);
|
| 553 |
-
// Feedback visual temporal
|
| 554 |
-
const originalIcon = copyBtn.innerHTML;
|
| 555 |
-
copyBtn.innerHTML = `<svg viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
|
| 556 |
-
setTimeout(() => copyBtn.innerHTML = originalIcon, 2000);
|
| 557 |
};
|
| 558 |
|
| 559 |
-
//
|
| 560 |
const regenBtn = document.createElement('button');
|
| 561 |
regenBtn.className = 'action-btn';
|
| 562 |
-
regenBtn.
|
| 563 |
-
regenBtn.innerHTML = `
|
| 564 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 565 |
-
<path d="M23 4v6h-6"></path>
|
| 566 |
-
<path d="M1 20v-6h6"></path>
|
| 567 |
-
<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>
|
| 568 |
-
</svg>
|
| 569 |
-
`;
|
| 570 |
regenBtn.onclick = () => {
|
| 571 |
sendMessage(lastUserPrompt);
|
| 572 |
};
|
| 573 |
|
| 574 |
actionsDiv.appendChild(copyBtn);
|
| 575 |
actionsDiv.appendChild(regenBtn);
|
| 576 |
-
|
| 577 |
wrapperElement.appendChild(actionsDiv);
|
| 578 |
|
| 579 |
-
|
| 580 |
-
requestAnimationFrame(() => {
|
| 581 |
-
actionsDiv.style.opacity = "1";
|
| 582 |
-
});
|
| 583 |
scrollToBottom();
|
| 584 |
}
|
| 585 |
|
| 586 |
// Listeners
|
| 587 |
userInput.addEventListener('keydown', (e) => {
|
| 588 |
-
if (e.key === 'Enter')
|
| 589 |
});
|
| 590 |
|
| 591 |
window.onload = () => userInput.focus();
|
|
|
|
| 261 |
.bot-actions {
|
| 262 |
display: flex;
|
| 263 |
gap: 10px;
|
| 264 |
+
opacity: 0;
|
| 265 |
transition: opacity 0.3s;
|
| 266 |
margin-top: 5px;
|
| 267 |
}
|
|
|
|
| 333 |
padding: 10px 0;
|
| 334 |
}
|
| 335 |
|
| 336 |
+
#mainBtn {
|
| 337 |
background: white;
|
| 338 |
color: black;
|
| 339 |
border: none;
|
|
|
|
| 348 |
transition: transform 0.2s;
|
| 349 |
}
|
| 350 |
|
| 351 |
+
#mainBtn:hover { transform: scale(1.05); }
|
|
|
|
| 352 |
|
| 353 |
.disclaimer {
|
| 354 |
text-align: center;
|
|
|
|
| 373 |
|
| 374 |
.pulsing { animation: pulseAvatar 1.5s infinite; }
|
| 375 |
|
|
|
|
| 376 |
::-webkit-scrollbar { width: 8px; }
|
| 377 |
::-webkit-scrollbar-track { background: transparent; }
|
| 378 |
::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
|
|
|
|
| 405 |
<div class="footer-container">
|
| 406 |
<div class="input-box">
|
| 407 |
<input type="text" id="userInput" placeholder="Escribe un mensaje..." autocomplete="off">
|
| 408 |
+
<button id="mainBtn" onclick="handleBtnClick()">
|
| 409 |
+
<!-- El icono se inyecta por JS -->
|
|
|
|
|
|
|
| 410 |
</button>
|
| 411 |
</div>
|
| 412 |
<div class="disclaimer">
|
|
|
|
| 417 |
<script>
|
| 418 |
const chatScroll = document.getElementById('chatScroll');
|
| 419 |
const userInput = document.getElementById('userInput');
|
| 420 |
+
const mainBtn = document.getElementById('mainBtn');
|
| 421 |
|
| 422 |
+
// Variables de Estado
|
| 423 |
let isGenerating = false;
|
| 424 |
+
let abortController = null; // Para cancelar fetch
|
| 425 |
+
let typingTimeout = null; // Para cancelar escritura
|
| 426 |
+
let lastUserPrompt = "";
|
| 427 |
+
|
| 428 |
+
// Iconos SVG
|
| 429 |
+
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>`;
|
| 430 |
+
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>`;
|
| 431 |
+
|
| 432 |
+
// Inicializar bot贸n
|
| 433 |
+
mainBtn.innerHTML = ICON_SEND;
|
| 434 |
|
| 435 |
// --- UTILS ---
|
| 436 |
function scrollToBottom() {
|
| 437 |
chatScroll.scrollTop = chatScroll.scrollHeight;
|
| 438 |
}
|
| 439 |
|
| 440 |
+
function setBtnState(state) {
|
| 441 |
+
if (state === 'sending') {
|
| 442 |
+
mainBtn.innerHTML = ICON_STOP;
|
| 443 |
+
isGenerating = true;
|
| 444 |
+
} else {
|
| 445 |
+
mainBtn.innerHTML = ICON_SEND;
|
| 446 |
+
isGenerating = false;
|
| 447 |
+
abortController = null;
|
| 448 |
+
}
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
// --- CORE FUNCTIONS ---
|
| 452 |
|
| 453 |
+
// Manejador del Click
|
| 454 |
+
function handleBtnClick() {
|
| 455 |
+
if (isGenerating) {
|
| 456 |
+
stopGeneration();
|
| 457 |
+
} else {
|
| 458 |
+
sendMessage();
|
| 459 |
+
}
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
function stopGeneration() {
|
| 463 |
+
// 1. Cancelar fetch
|
| 464 |
+
if (abortController) abortController.abort();
|
| 465 |
+
|
| 466 |
+
// 2. Cancelar escritura
|
| 467 |
+
if (typingTimeout) clearTimeout(typingTimeout);
|
| 468 |
+
|
| 469 |
+
// 3. UI Cleanup
|
| 470 |
+
// Buscamos el cursor activo para quitarlo
|
| 471 |
+
const activeCursor = document.querySelector('.typing-cursor');
|
| 472 |
+
if (activeCursor) activeCursor.classList.remove('typing-cursor');
|
| 473 |
+
|
| 474 |
+
const activeAvatar = document.querySelector('.pulsing');
|
| 475 |
+
if (activeAvatar) activeAvatar.classList.remove('pulsing');
|
| 476 |
+
|
| 477 |
+
setBtnState('idle');
|
| 478 |
+
userInput.focus();
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
async function sendMessage(textOverride = null) {
|
| 482 |
const text = textOverride || userInput.value.trim();
|
| 483 |
+
if (!text) return;
|
| 484 |
|
|
|
|
| 485 |
lastUserPrompt = text;
|
| 486 |
|
| 487 |
// UI Updates
|
|
|
|
| 490 |
addMessage(text, 'user');
|
| 491 |
}
|
| 492 |
|
| 493 |
+
setBtnState('sending');
|
| 494 |
+
abortController = new AbortController();
|
|
|
|
| 495 |
|
| 496 |
+
// Placeholder Bot
|
| 497 |
const botRow = document.createElement('div');
|
| 498 |
botRow.className = 'msg-row bot';
|
|
|
|
| 499 |
const avatar = document.createElement('div');
|
| 500 |
+
avatar.className = 'bot-avatar pulsing';
|
|
|
|
| 501 |
const wrapper = document.createElement('div');
|
| 502 |
wrapper.className = 'msg-content-wrapper';
|
|
|
|
| 503 |
const msgText = document.createElement('div');
|
| 504 |
+
msgText.className = 'msg-text';
|
| 505 |
|
| 506 |
wrapper.appendChild(msgText);
|
| 507 |
botRow.appendChild(avatar);
|
|
|
|
| 513 |
const response = await fetch('/generate', {
|
| 514 |
method: 'POST',
|
| 515 |
headers: { 'Content-Type': 'application/json' },
|
| 516 |
+
body: JSON.stringify({ text: text }),
|
| 517 |
+
signal: abortController.signal
|
| 518 |
});
|
| 519 |
|
| 520 |
const data = await response.json();
|
|
|
|
| 521 |
|
| 522 |
+
if (!isGenerating) return; // Si se detuvo justo antes
|
| 523 |
+
|
| 524 |
+
avatar.classList.remove('pulsing');
|
| 525 |
+
const reply = data.reply || "No entend铆 eso.";
|
| 526 |
|
|
|
|
| 527 |
await typeWriter(msgText, reply);
|
| 528 |
|
| 529 |
+
if (isGenerating) { // Solo a帽adir acciones si no se cancel贸
|
| 530 |
+
addActions(wrapper, reply);
|
| 531 |
+
setBtnState('idle');
|
| 532 |
+
}
|
| 533 |
|
| 534 |
} catch (error) {
|
| 535 |
+
if (error.name === 'AbortError') {
|
| 536 |
+
msgText.textContent += " [Detenido]";
|
| 537 |
+
} else {
|
| 538 |
+
avatar.classList.remove('pulsing');
|
| 539 |
+
msgText.textContent = "Error de conexi贸n.";
|
| 540 |
+
msgText.style.color = "#ff8b8b";
|
| 541 |
+
setBtnState('idle');
|
| 542 |
+
}
|
| 543 |
}
|
| 544 |
}
|
| 545 |
|
| 546 |
function addMessage(text, sender) {
|
| 547 |
const row = document.createElement('div');
|
| 548 |
row.className = `msg-row ${sender}`;
|
|
|
|
| 549 |
const content = document.createElement('div');
|
| 550 |
content.className = 'msg-content';
|
| 551 |
content.textContent = text;
|
|
|
|
| 552 |
row.appendChild(content);
|
| 553 |
chatScroll.appendChild(row);
|
| 554 |
scrollToBottom();
|
| 555 |
}
|
| 556 |
|
|
|
|
| 557 |
function typeWriter(element, text, speed = 12) {
|
| 558 |
return new Promise(resolve => {
|
| 559 |
let i = 0;
|
| 560 |
element.classList.add('typing-cursor');
|
| 561 |
|
| 562 |
function type() {
|
| 563 |
+
if (!isGenerating) { // Check de seguridad por si se dio Stop
|
| 564 |
+
element.classList.remove('typing-cursor');
|
| 565 |
+
resolve();
|
| 566 |
+
return;
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
if (i < text.length) {
|
| 570 |
element.textContent += text.charAt(i);
|
| 571 |
i++;
|
| 572 |
scrollToBottom();
|
| 573 |
+
typingTimeout = setTimeout(() => {
|
| 574 |
+
type();
|
| 575 |
+
}, speed + Math.random() * 5);
|
| 576 |
} else {
|
| 577 |
element.classList.remove('typing-cursor');
|
| 578 |
resolve();
|
|
|
|
| 582 |
});
|
| 583 |
}
|
| 584 |
|
|
|
|
| 585 |
function addActions(wrapperElement, textToCopy) {
|
| 586 |
const actionsDiv = document.createElement('div');
|
| 587 |
actionsDiv.className = 'bot-actions';
|
| 588 |
|
| 589 |
+
// Copy Btn
|
| 590 |
const copyBtn = document.createElement('button');
|
| 591 |
copyBtn.className = 'action-btn';
|
| 592 |
+
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>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
copyBtn.onclick = () => {
|
| 594 |
navigator.clipboard.writeText(textToCopy);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 595 |
};
|
| 596 |
|
| 597 |
+
// Regen Btn
|
| 598 |
const regenBtn = document.createElement('button');
|
| 599 |
regenBtn.className = 'action-btn';
|
| 600 |
+
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>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 601 |
regenBtn.onclick = () => {
|
| 602 |
sendMessage(lastUserPrompt);
|
| 603 |
};
|
| 604 |
|
| 605 |
actionsDiv.appendChild(copyBtn);
|
| 606 |
actionsDiv.appendChild(regenBtn);
|
|
|
|
| 607 |
wrapperElement.appendChild(actionsDiv);
|
| 608 |
|
| 609 |
+
requestAnimationFrame(() => actionsDiv.style.opacity = "1");
|
|
|
|
|
|
|
|
|
|
| 610 |
scrollToBottom();
|
| 611 |
}
|
| 612 |
|
| 613 |
// Listeners
|
| 614 |
userInput.addEventListener('keydown', (e) => {
|
| 615 |
+
if (e.key === 'Enter') handleBtnClick();
|
| 616 |
});
|
| 617 |
|
| 618 |
window.onload = () => userInput.focus();
|