feat: implement support for aborting active chat generation jobs
Browse files- index.html +65 -15
index.html
CHANGED
|
@@ -205,10 +205,10 @@ async function getClient() {
|
|
| 205 |
return gradioClient;
|
| 206 |
}
|
| 207 |
|
| 208 |
-
// ── State ──
|
| 209 |
let conversations = [];
|
| 210 |
let currentConv = null;
|
| 211 |
let isGenerating = false;
|
|
|
|
| 212 |
|
| 213 |
// ── Init ──
|
| 214 |
function init() {
|
|
@@ -231,6 +231,14 @@ function autoResize() {
|
|
| 231 |
|
| 232 |
// ── Chat Management ──
|
| 233 |
function newChat() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
const conv = { id: Date.now(), title: 'New Chat', messages: [] };
|
| 235 |
conversations.unshift(conv);
|
| 236 |
currentConv = conv;
|
|
@@ -240,6 +248,14 @@ function newChat() {
|
|
| 240 |
}
|
| 241 |
|
| 242 |
function switchChat(id) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
currentConv = conversations.find(c => c.id === id);
|
| 244 |
renderChatList();
|
| 245 |
renderMessages();
|
|
@@ -307,11 +323,23 @@ function escapeHTML(s) {
|
|
| 307 |
const d = document.createElement('div'); d.textContent = s; return d.innerHTML;
|
| 308 |
}
|
| 309 |
|
|
|
|
| 310 |
// ── Send Message (uses Gradio JS Client) ──
|
| 311 |
async function sendMessage() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
const input = document.getElementById('userInput');
|
| 313 |
const text = input.value.trim();
|
| 314 |
-
if (!text
|
| 315 |
|
| 316 |
// Hide welcome
|
| 317 |
const welcome = document.getElementById('welcome');
|
|
@@ -331,7 +359,7 @@ async function sendMessage() {
|
|
| 331 |
input.value = '';
|
| 332 |
input.style.height = 'auto';
|
| 333 |
isGenerating = true;
|
| 334 |
-
|
| 335 |
|
| 336 |
// Add assistant placeholder with typing indicator
|
| 337 |
const assistantDiv = document.createElement('div');
|
|
@@ -347,7 +375,7 @@ async function sendMessage() {
|
|
| 347 |
const client = await getClient();
|
| 348 |
const history = currentConv.messages.slice(0, -1);
|
| 349 |
|
| 350 |
-
|
| 351 |
message: text,
|
| 352 |
history: JSON.stringify(history),
|
| 353 |
system_prompt: document.getElementById('systemPrompt').value,
|
|
@@ -356,24 +384,46 @@ async function sendMessage() {
|
|
| 356 |
max_new_tokens: parseInt(document.getElementById('maxTokens').value),
|
| 357 |
});
|
| 358 |
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
} catch (err) {
|
|
|
|
| 367 |
if (!fullText) {
|
| 368 |
fullText = 'Sorry, an error occurred. Please try again.';
|
| 369 |
bubble.innerHTML = `<span style="color:var(--red)">${escapeHTML(err.message || fullText)}</span>`;
|
| 370 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
}
|
|
|
|
| 372 |
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
}
|
| 378 |
|
| 379 |
// ── Globals for onclick handlers ──
|
|
|
|
| 205 |
return gradioClient;
|
| 206 |
}
|
| 207 |
|
|
|
|
| 208 |
let conversations = [];
|
| 209 |
let currentConv = null;
|
| 210 |
let isGenerating = false;
|
| 211 |
+
let currentJob = null;
|
| 212 |
|
| 213 |
// ── Init ──
|
| 214 |
function init() {
|
|
|
|
| 231 |
|
| 232 |
// ── Chat Management ──
|
| 233 |
function newChat() {
|
| 234 |
+
if (isGenerating && currentJob) {
|
| 235 |
+
try { currentJob.destroy(); } catch(e) {}
|
| 236 |
+
}
|
| 237 |
+
isGenerating = false;
|
| 238 |
+
currentJob = null;
|
| 239 |
+
document.getElementById('sendBtn').disabled = false;
|
| 240 |
+
updateSendBtnIcon(false);
|
| 241 |
+
|
| 242 |
const conv = { id: Date.now(), title: 'New Chat', messages: [] };
|
| 243 |
conversations.unshift(conv);
|
| 244 |
currentConv = conv;
|
|
|
|
| 248 |
}
|
| 249 |
|
| 250 |
function switchChat(id) {
|
| 251 |
+
if (isGenerating && currentJob) {
|
| 252 |
+
try { currentJob.destroy(); } catch(e) {}
|
| 253 |
+
}
|
| 254 |
+
isGenerating = false;
|
| 255 |
+
currentJob = null;
|
| 256 |
+
document.getElementById('sendBtn').disabled = false;
|
| 257 |
+
updateSendBtnIcon(false);
|
| 258 |
+
|
| 259 |
currentConv = conversations.find(c => c.id === id);
|
| 260 |
renderChatList();
|
| 261 |
renderMessages();
|
|
|
|
| 323 |
const d = document.createElement('div'); d.textContent = s; return d.innerHTML;
|
| 324 |
}
|
| 325 |
|
| 326 |
+
// ── Send Message (uses Gradio JS Client) ──
|
| 327 |
// ── Send Message (uses Gradio JS Client) ──
|
| 328 |
async function sendMessage() {
|
| 329 |
+
if (isGenerating) {
|
| 330 |
+
if (currentJob) {
|
| 331 |
+
try { currentJob.destroy(); } catch(e) {}
|
| 332 |
+
}
|
| 333 |
+
isGenerating = false;
|
| 334 |
+
currentJob = null;
|
| 335 |
+
document.getElementById('sendBtn').disabled = false;
|
| 336 |
+
updateSendBtnIcon(false);
|
| 337 |
+
return;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
const input = document.getElementById('userInput');
|
| 341 |
const text = input.value.trim();
|
| 342 |
+
if (!text) return;
|
| 343 |
|
| 344 |
// Hide welcome
|
| 345 |
const welcome = document.getElementById('welcome');
|
|
|
|
| 359 |
input.value = '';
|
| 360 |
input.style.height = 'auto';
|
| 361 |
isGenerating = true;
|
| 362 |
+
updateSendBtnIcon(true);
|
| 363 |
|
| 364 |
// Add assistant placeholder with typing indicator
|
| 365 |
const assistantDiv = document.createElement('div');
|
|
|
|
| 375 |
const client = await getClient();
|
| 376 |
const history = currentConv.messages.slice(0, -1);
|
| 377 |
|
| 378 |
+
currentJob = client.submit("/generate", {
|
| 379 |
message: text,
|
| 380 |
history: JSON.stringify(history),
|
| 381 |
system_prompt: document.getElementById('systemPrompt').value,
|
|
|
|
| 384 |
max_new_tokens: parseInt(document.getElementById('maxTokens').value),
|
| 385 |
});
|
| 386 |
|
| 387 |
+
await new Promise((resolve, reject) => {
|
| 388 |
+
currentJob.on("data", (event) => {
|
| 389 |
+
if (event.data && event.data[0]) {
|
| 390 |
+
fullText = event.data[0];
|
| 391 |
+
bubble.innerHTML = formatResponse(fullText);
|
| 392 |
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
| 393 |
+
}
|
| 394 |
+
});
|
| 395 |
+
currentJob.on("status", (status) => {
|
| 396 |
+
if (status.stage === "complete") resolve();
|
| 397 |
+
if (status.stage === "error") reject(new Error(status.message || "Generation failed"));
|
| 398 |
+
});
|
| 399 |
+
});
|
| 400 |
} catch (err) {
|
| 401 |
+
console.error("Generation error:", err);
|
| 402 |
if (!fullText) {
|
| 403 |
fullText = 'Sorry, an error occurred. Please try again.';
|
| 404 |
bubble.innerHTML = `<span style="color:var(--red)">${escapeHTML(err.message || fullText)}</span>`;
|
| 405 |
}
|
| 406 |
+
} finally {
|
| 407 |
+
isGenerating = false;
|
| 408 |
+
currentJob = null;
|
| 409 |
+
document.getElementById('sendBtn').disabled = false;
|
| 410 |
+
updateSendBtnIcon(false);
|
| 411 |
+
if (fullText) {
|
| 412 |
+
currentConv.messages.push({ role: 'assistant', content: fullText });
|
| 413 |
+
}
|
| 414 |
+
document.getElementById('userInput').focus();
|
| 415 |
}
|
| 416 |
+
}
|
| 417 |
|
| 418 |
+
function updateSendBtnIcon(loading) {
|
| 419 |
+
const btn = document.getElementById('sendBtn');
|
| 420 |
+
if (loading) {
|
| 421 |
+
btn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="6" width="12" height="12"/></svg>`;
|
| 422 |
+
btn.title = "Stop generating";
|
| 423 |
+
} else {
|
| 424 |
+
btn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 2L11 13"/><path d="M22 2L15 22L11 13L2 9L22 2Z"/></svg>`;
|
| 425 |
+
btn.title = "Send message";
|
| 426 |
+
}
|
| 427 |
}
|
| 428 |
|
| 429 |
// ── Globals for onclick handlers ──
|