teszenofficial commited on
Commit
ff50991
verified
1 Parent(s): 709e9ca

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -68
app.py CHANGED
@@ -261,7 +261,7 @@ header {
261
  .bot-actions {
262
  display: flex;
263
  gap: 10px;
264
- opacity: 0; /* Se muestra al hover o al terminar de escribir */
265
  transition: opacity 0.3s;
266
  margin-top: 5px;
267
  }
@@ -333,7 +333,7 @@ header {
333
  padding: 10px 0;
334
  }
335
 
336
- #sendBtn {
337
  background: white;
338
  color: black;
339
  border: none;
@@ -348,8 +348,7 @@ header {
348
  transition: transform 0.2s;
349
  }
350
 
351
- #sendBtn:hover { transform: scale(1.05); }
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="sendBtn" onclick="sendMessage()">
411
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
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 sendBtn = document.getElementById('sendBtn');
425
 
 
426
  let isGenerating = false;
427
- let lastUserPrompt = ""; // Para la funci贸n regenerar
 
 
 
 
 
 
 
 
 
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 || isGenerating) return;
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
- isGenerating = true;
450
- sendBtn.disabled = true;
451
- sendBtn.style.opacity = "0.5";
452
 
453
- // Crear placeholder del Bot
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'; // Animaci贸n pensando
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'; // Vacio por ahora
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
- const reply = data.reply || "No pude generar respuesta.";
 
 
 
483
 
484
- // Iniciar escritura
485
  await typeWriter(msgText, reply);
486
 
487
- // A帽adir botones de acci贸n al terminar
488
- addActions(wrapper, reply);
 
 
489
 
490
  } catch (error) {
491
- avatar.classList.remove('pulsing');
492
- msgText.textContent = "Error de conexi贸n. Intenta de nuevo.";
493
- msgText.style.color = "#ff8b8b";
494
- } finally {
495
- isGenerating = false;
496
- sendBtn.disabled = false;
497
- sendBtn.style.opacity = "1";
498
- if (!textOverride) userInput.focus();
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
- setTimeout(type, speed + Math.random() * 5);
 
 
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
- // Bot贸n Copiar
542
  const copyBtn = document.createElement('button');
543
  copyBtn.className = 'action-btn';
544
- copyBtn.title = "Copiar";
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
- // Bot贸n Regenerar
560
  const regenBtn = document.createElement('button');
561
  regenBtn.className = 'action-btn';
562
- regenBtn.title = "Regenerar respuesta";
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
- // Peque帽o fade in
580
- requestAnimationFrame(() => {
581
- actionsDiv.style.opacity = "1";
582
- });
583
  scrollToBottom();
584
  }
585
 
586
  // Listeners
587
  userInput.addEventListener('keydown', (e) => {
588
- if (e.key === 'Enter') sendMessage();
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();