spagestic commited on
Commit
4b7fa35
·
1 Parent(s): 5b5d9ca

Add gradioStream SSE reader in gradio_api.js and wire runChat in app.js to live updates

Browse files
Files changed (3) hide show
  1. assets/app.js +85 -19
  2. assets/gradio_api.js +69 -11
  3. assets/server.css +62 -0
assets/app.js CHANGED
@@ -1,4 +1,4 @@
1
- import { gradioPredict } from "/assets/gradio_api.js?v=2";
2
 
3
  let markdownTools = null;
4
 
@@ -420,6 +420,66 @@ async function renderMessageBody(body, message, isTool) {
420
  }
421
  }
422
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  async function renderMessages() {
424
  els.chatMessages.innerHTML = "";
425
  for (const message of state.history) {
@@ -430,15 +490,15 @@ async function renderMessages() {
430
  if (metadata.status === "pending") {
431
  node.classList.add("pending");
432
  }
 
433
  if (isTool) {
434
- const title = document.createElement("div");
435
- title.className = "tool-title";
436
- title.textContent = metadata.title || "Tool";
437
- node.appendChild(title);
 
438
  }
439
- const body = document.createElement("div");
440
- await renderMessageBody(body, message, isTool);
441
- node.appendChild(body);
442
  els.chatMessages.appendChild(node);
443
  }
444
  els.chatMessages.scrollTop = els.chatMessages.scrollHeight;
@@ -499,17 +559,23 @@ async function loadChoices() {
499
  }
500
 
501
  async function runChat(message) {
502
- const result = await gradioPredict("/chat", {
503
- message,
504
- history: state.history,
505
- globe_state: state.globeState,
506
- });
507
- const payload = unwrapResult(result);
508
- state.history = payload.history || state.history;
509
- await renderMessages();
510
- if (payload.globe_state) {
511
- applyGlobeState(payload.globe_state);
512
- }
 
 
 
 
 
 
513
  persistActiveSession();
514
  }
515
 
 
1
+ import { gradioPredict, gradioStream } from "/assets/gradio_api.js?v=2";
2
 
3
  let markdownTools = null;
4
 
 
420
  }
421
  }
422
 
423
+ function appendToolLogSection(parent, label, value) {
424
+ const section = document.createElement("div");
425
+ section.className = "tool-log-section";
426
+ const heading = document.createElement("strong");
427
+ heading.textContent = label;
428
+ section.appendChild(heading);
429
+ const pre = document.createElement("pre");
430
+ pre.textContent =
431
+ typeof value === "string" ? value : JSON.stringify(value, null, 2);
432
+ section.appendChild(pre);
433
+ parent.appendChild(section);
434
+ }
435
+
436
+ function renderToolMessage(node, message, metadata) {
437
+ const isThinking = metadata.title === "Thinking";
438
+ if (isThinking) {
439
+ node.classList.add("thinking");
440
+ }
441
+
442
+ if (metadata.log) {
443
+ const details = document.createElement("details");
444
+ details.className = "tool-log";
445
+ const summary = document.createElement("summary");
446
+ const statusSuffix =
447
+ metadata.status === "pending"
448
+ ? " (running…)"
449
+ : metadata.duration
450
+ ? ` (${metadata.duration.toFixed(1)}s)`
451
+ : "";
452
+ summary.textContent = `${metadata.title || "Tool"}${statusSuffix}`;
453
+ details.appendChild(summary);
454
+
455
+ if (message.content) {
456
+ const summaryText = document.createElement("div");
457
+ summaryText.className = "tool-summary-text";
458
+ summaryText.textContent = message.content;
459
+ details.appendChild(summaryText);
460
+ }
461
+
462
+ if (metadata.log.arguments) {
463
+ appendToolLogSection(details, "Arguments", metadata.log.arguments);
464
+ }
465
+ if (metadata.log.result !== undefined) {
466
+ appendToolLogSection(details, "Result", metadata.log.result);
467
+ }
468
+
469
+ node.appendChild(details);
470
+ return;
471
+ }
472
+
473
+ const title = document.createElement("div");
474
+ title.className = "tool-title";
475
+ title.textContent = metadata.title || "Tool";
476
+ node.appendChild(title);
477
+ const body = document.createElement("div");
478
+ body.className = "chat-message-body";
479
+ body.textContent = message.content || "";
480
+ node.appendChild(body);
481
+ }
482
+
483
  async function renderMessages() {
484
  els.chatMessages.innerHTML = "";
485
  for (const message of state.history) {
 
490
  if (metadata.status === "pending") {
491
  node.classList.add("pending");
492
  }
493
+
494
  if (isTool) {
495
+ renderToolMessage(node, message, metadata);
496
+ } else {
497
+ const body = document.createElement("div");
498
+ await renderMessageBody(body, message, isTool);
499
+ node.appendChild(body);
500
  }
501
+
 
 
502
  els.chatMessages.appendChild(node);
503
  }
504
  els.chatMessages.scrollTop = els.chatMessages.scrollHeight;
 
559
  }
560
 
561
  async function runChat(message) {
562
+ await gradioStream(
563
+ "/chat",
564
+ {
565
+ message,
566
+ history: state.history,
567
+ globe_state: state.globeState,
568
+ },
569
+ async (chunk) => {
570
+ if (chunk.history) {
571
+ state.history = chunk.history;
572
+ }
573
+ if (chunk.globe_state) {
574
+ applyGlobeState(chunk.globe_state);
575
+ }
576
+ await renderMessages();
577
+ },
578
+ );
579
  persistActiveSession();
580
  }
581
 
assets/gradio_api.js CHANGED
@@ -1,4 +1,11 @@
1
- export async function gradioPredict(apiName, payload = {}) {
 
 
 
 
 
 
 
2
  const route = apiName.replace(/^\//, "");
3
  const start = await fetch(`/gradio_api/call/v2/${route}`, {
4
  method: "POST",
@@ -22,31 +29,82 @@ export async function gradioPredict(apiName, payload = {}) {
22
  throw new Error(`Gradio API stream failed (${stream.status})`);
23
  }
24
 
25
- const body = await stream.text();
26
- let resultData = null;
 
 
27
  let errorMessage = null;
 
28
 
29
- for (const line of body.split("\n")) {
30
- if (line.startsWith("event: error")) {
31
- errorMessage = "Gradio API returned an error";
32
- continue;
 
 
 
33
  }
34
  if (!line.startsWith("data: ")) {
35
- continue;
36
  }
 
37
  try {
38
- resultData = JSON.parse(line.slice(6));
 
 
 
 
 
 
 
39
  } catch {
40
  // Ignore malformed SSE chunks and keep the last valid payload.
41
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  }
43
 
44
  if (errorMessage) {
45
  throw new Error(errorMessage);
46
  }
47
- if (resultData === null) {
48
  throw new Error("Gradio API returned no data");
49
  }
50
 
51
- return { data: resultData };
 
 
 
 
 
 
 
 
52
  }
 
1
+ function unwrapStreamData(data) {
2
+ if (Array.isArray(data)) {
3
+ return data.length === 1 ? data[0] : data;
4
+ }
5
+ return data;
6
+ }
7
+
8
+ export async function gradioStream(apiName, payload = {}, onChunk) {
9
  const route = apiName.replace(/^\//, "");
10
  const start = await fetch(`/gradio_api/call/v2/${route}`, {
11
  method: "POST",
 
29
  throw new Error(`Gradio API stream failed (${stream.status})`);
30
  }
31
 
32
+ const reader = stream.body.getReader();
33
+ const decoder = new TextDecoder();
34
+ let buffer = "";
35
+ let lastData = null;
36
  let errorMessage = null;
37
+ let currentEvent = null;
38
 
39
+ const processLine = (line) => {
40
+ if (line.startsWith("event: ")) {
41
+ currentEvent = line.slice(7).trim();
42
+ if (currentEvent === "error") {
43
+ errorMessage = "Gradio API returned an error";
44
+ }
45
+ return;
46
  }
47
  if (!line.startsWith("data: ")) {
48
+ return;
49
  }
50
+
51
  try {
52
+ const parsed = JSON.parse(line.slice(6));
53
+ lastData = parsed;
54
+ if (currentEvent === "error") {
55
+ errorMessage =
56
+ typeof parsed === "string" ? parsed : JSON.stringify(parsed);
57
+ return;
58
+ }
59
+ onChunk(unwrapStreamData(parsed));
60
  } catch {
61
  // Ignore malformed SSE chunks and keep the last valid payload.
62
  }
63
+ };
64
+
65
+ while (true) {
66
+ const { done, value } = await reader.read();
67
+ if (done) {
68
+ break;
69
+ }
70
+
71
+ buffer += decoder.decode(value, { stream: true });
72
+ const lines = buffer.split("\n");
73
+ buffer = lines.pop() || "";
74
+
75
+ for (const line of lines) {
76
+ if (line.trim() === "") {
77
+ currentEvent = null;
78
+ continue;
79
+ }
80
+ processLine(line);
81
+ }
82
+ }
83
+
84
+ if (buffer.trim()) {
85
+ for (const line of buffer.split("\n")) {
86
+ if (line.trim() === "") {
87
+ currentEvent = null;
88
+ continue;
89
+ }
90
+ processLine(line);
91
+ }
92
  }
93
 
94
  if (errorMessage) {
95
  throw new Error(errorMessage);
96
  }
97
+ if (lastData === null) {
98
  throw new Error("Gradio API returned no data");
99
  }
100
 
101
+ return { data: lastData };
102
+ }
103
+
104
+ export async function gradioPredict(apiName, payload = {}) {
105
+ let resultData = null;
106
+ await gradioStream(apiName, payload, (chunk) => {
107
+ resultData = chunk;
108
+ });
109
+ return { data: [resultData] };
110
  }
assets/server.css CHANGED
@@ -266,6 +266,68 @@ button:disabled {
266
  margin-bottom: 4px;
267
  }
268
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  .markdown-body > :first-child {
270
  margin-top: 0;
271
  }
 
266
  margin-bottom: 4px;
267
  }
268
 
269
+ .chat-message.tool.thinking {
270
+ background: rgba(148, 163, 184, 0.08);
271
+ border-color: rgba(148, 163, 184, 0.2);
272
+ font-style: italic;
273
+ color: #d1d5db;
274
+ }
275
+
276
+ .chat-message.tool details.tool-log {
277
+ margin: 0;
278
+ }
279
+
280
+ .chat-message.tool details.tool-log > summary {
281
+ cursor: pointer;
282
+ font-weight: 600;
283
+ color: #fbbf24;
284
+ list-style-position: outside;
285
+ }
286
+
287
+ .chat-message.tool.thinking details.tool-log > summary {
288
+ color: #cbd5e1;
289
+ }
290
+
291
+ .chat-message.tool .tool-summary-text {
292
+ margin-top: 6px;
293
+ white-space: pre-wrap;
294
+ color: #e5e7eb;
295
+ font-style: normal;
296
+ }
297
+
298
+ .chat-message.tool.thinking .tool-summary-text {
299
+ color: #d1d5db;
300
+ font-style: italic;
301
+ }
302
+
303
+ .chat-message.tool .tool-log-section {
304
+ margin-top: 10px;
305
+ }
306
+
307
+ .chat-message.tool .tool-log-section strong {
308
+ display: block;
309
+ margin-bottom: 4px;
310
+ font-size: 0.8rem;
311
+ color: #9ca3af;
312
+ text-transform: uppercase;
313
+ letter-spacing: 0.04em;
314
+ }
315
+
316
+ .chat-message.tool .tool-log-section pre {
317
+ margin: 0;
318
+ padding: 8px 10px;
319
+ border-radius: 8px;
320
+ background: rgba(15, 23, 42, 0.85);
321
+ border: 1px solid rgba(148, 163, 184, 0.15);
322
+ overflow-x: auto;
323
+ font-size: 0.78rem;
324
+ line-height: 1.4;
325
+ white-space: pre-wrap;
326
+ word-break: break-word;
327
+ font-style: normal;
328
+ color: #e5e7eb;
329
+ }
330
+
331
  .markdown-body > :first-child {
332
  margin-top: 0;
333
  }