akhaliq HF Staff commited on
Commit
b35d8de
·
1 Parent(s): 3b84323

feat: add generation state management and a 30s stream timeout to prevent hanging

Browse files
Files changed (1) hide show
  1. index.html +46 -24
index.html CHANGED
@@ -409,6 +409,7 @@
409
  <script type="module">
410
  let client;
411
  let history = [];
 
412
 
413
  const chatContainer = document.getElementById('chat-container');
414
  const userInput = document.getElementById('user-input');
@@ -429,18 +430,22 @@
429
  }
430
  }
431
 
 
 
 
 
432
  // Auto-resize textarea
433
  userInput.addEventListener('input', function () {
434
  this.style.height = 'auto';
435
  this.style.height = Math.min(this.scrollHeight, 200) + 'px';
436
- sendBtn.disabled = this.value.trim().length === 0;
437
  });
438
 
439
  // Handle Enter key (Shift+Enter for newline)
440
  userInput.addEventListener('keydown', function (e) {
441
  if (e.key === 'Enter' && !e.shiftKey) {
442
  e.preventDefault();
443
- if (!sendBtn.disabled) {
444
  sendMessage();
445
  }
446
  }
@@ -492,15 +497,24 @@
492
  chatContainer.scrollTop = chatContainer.scrollHeight;
493
  }
494
 
 
 
 
 
 
 
 
 
495
  async function sendMessage() {
496
  const text = userInput.value.trim();
497
- if (!text || !client) return;
 
 
498
 
499
  // Reset input
500
  userInput.value = '';
501
  userInput.style.height = 'auto';
502
- sendBtn.disabled = true;
503
- userInput.disabled = true;
504
 
505
  // Add user message to UI
506
  addMessage('user', text);
@@ -520,19 +534,30 @@
520
  history_json: history
521
  });
522
 
523
- // Use manual iterator + while loop instead of for-await.
524
- // In a while loop, `break` does NOT call iterator.return(),
525
- // so it won't hang on Gradio client cleanup.
526
  const iterator = submission[Symbol.asyncIterator]();
527
- while (true) {
528
- const { value: event, done } = await iterator.next();
529
- if (done) break;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
 
531
  if (event.type === "data") {
532
- if (typingIndicatorWrapper.style.display !== 'none') {
533
- typingIndicatorWrapper.style.display = 'none';
534
- typingIndicator.style.display = 'none';
535
- }
536
  fullResponse = event.data[0];
537
  botContentDiv.innerHTML = marked.parse(fullResponse || "");
538
  scrollToBottom();
@@ -542,7 +567,7 @@
542
  throw new Error(event.message);
543
  }
544
  if (event.stage === "complete") {
545
- break;
546
  }
547
  }
548
  }
@@ -553,16 +578,13 @@
553
 
554
  } catch (err) {
555
  console.error("Error during generation:", err);
556
- typingIndicatorWrapper.style.display = 'none';
557
- typingIndicator.style.display = 'none';
558
  botContentDiv.innerHTML = `<span style="color: #ef4444;">Error: ${err.message || 'Something went wrong.'}</span>`;
559
- } finally {
560
- typingIndicatorWrapper.style.display = 'none';
561
- typingIndicator.style.display = 'none';
562
- userInput.disabled = false;
563
- sendBtn.disabled = userInput.value.trim().length === 0;
564
- userInput.focus();
565
  }
 
 
 
 
 
566
  }
567
 
568
  // Run init
 
409
  <script type="module">
410
  let client;
411
  let history = [];
412
+ let isGenerating = false;
413
 
414
  const chatContainer = document.getElementById('chat-container');
415
  const userInput = document.getElementById('user-input');
 
430
  }
431
  }
432
 
433
+ function updateSendButton() {
434
+ sendBtn.disabled = isGenerating || userInput.value.trim().length === 0;
435
+ }
436
+
437
  // Auto-resize textarea
438
  userInput.addEventListener('input', function () {
439
  this.style.height = 'auto';
440
  this.style.height = Math.min(this.scrollHeight, 200) + 'px';
441
+ updateSendButton();
442
  });
443
 
444
  // Handle Enter key (Shift+Enter for newline)
445
  userInput.addEventListener('keydown', function (e) {
446
  if (e.key === 'Enter' && !e.shiftKey) {
447
  e.preventDefault();
448
+ if (!isGenerating && userInput.value.trim().length > 0) {
449
  sendMessage();
450
  }
451
  }
 
497
  chatContainer.scrollTop = chatContainer.scrollHeight;
498
  }
499
 
500
+ function finishGeneration() {
501
+ isGenerating = false;
502
+ typingIndicatorWrapper.style.display = 'none';
503
+ typingIndicator.style.display = 'none';
504
+ updateSendButton();
505
+ userInput.focus();
506
+ }
507
+
508
  async function sendMessage() {
509
  const text = userInput.value.trim();
510
+ if (!text || !client || isGenerating) return;
511
+
512
+ isGenerating = true;
513
 
514
  // Reset input
515
  userInput.value = '';
516
  userInput.style.height = 'auto';
517
+ updateSendButton();
 
518
 
519
  // Add user message to UI
520
  addMessage('user', text);
 
534
  history_json: history
535
  });
536
 
 
 
 
537
  const iterator = submission[Symbol.asyncIterator]();
538
+ let streamDone = false;
539
+
540
+ while (!streamDone) {
541
+ // Race each iterator.next() against a 30s idle timeout.
542
+ // The Gradio client's iterator may hang forever after the
543
+ // last event, so this guarantees we always break out.
544
+ const result = await Promise.race([
545
+ iterator.next(),
546
+ new Promise(resolve =>
547
+ setTimeout(() => resolve({ value: undefined, done: true }), 30000)
548
+ )
549
+ ]);
550
+
551
+ if (result.done) {
552
+ streamDone = true;
553
+ break;
554
+ }
555
+
556
+ const event = result.value;
557
 
558
  if (event.type === "data") {
559
+ typingIndicatorWrapper.style.display = 'none';
560
+ typingIndicator.style.display = 'none';
 
 
561
  fullResponse = event.data[0];
562
  botContentDiv.innerHTML = marked.parse(fullResponse || "");
563
  scrollToBottom();
 
567
  throw new Error(event.message);
568
  }
569
  if (event.stage === "complete") {
570
+ streamDone = true;
571
  }
572
  }
573
  }
 
578
 
579
  } catch (err) {
580
  console.error("Error during generation:", err);
 
 
581
  botContentDiv.innerHTML = `<span style="color: #ef4444;">Error: ${err.message || 'Something went wrong.'}</span>`;
 
 
 
 
 
 
582
  }
583
+
584
+ // Always runs — not inside finally (which depends on iterator cleanup)
585
+ // but sequentially after try/catch, which is guaranteed to complete
586
+ // thanks to Promise.race timeout.
587
+ finishGeneration();
588
  }
589
 
590
  // Run init