Spaces:
Running
Running
feat: add generation state management and a 30s stream timeout to prevent hanging
Browse files- 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 |
-
|
| 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 (!
|
| 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 |
-
|
| 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 |
-
|
| 528 |
-
|
| 529 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
|
| 531 |
if (event.type === "data") {
|
| 532 |
-
|
| 533 |
-
|
| 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 |
-
|
| 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
|