@coral-ai/chat 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -44,18 +44,29 @@ type ChatMessage = {
44
44
  approval?: ChatApprovalRequest;
45
45
  /**
46
46
  * If present, this assistant message carries a multi-step task plan.
47
- * Rendered inline as a collapsible checklist above the message text and
48
- * persists in history for replay. Updated in place as the agent streams.
47
+ * Legacy field — new messages use `blocks` to position the todo list
48
+ * inline at the stream point where it first appeared. Still read by the
49
+ * renderer as a fallback for persisted messages saved before blocks.
49
50
  */
50
51
  todos?: ChatTodo[];
51
52
  /**
52
53
  * If present, this assistant message carries one or more chart specs.
53
- * Rendered inline (above the message text) via `<CoralChart>` from
54
- * `@coral-ai/charts`. Streamed in incrementally as the agent calls the
55
- * built-in `render_chart` tool, and persisted with the conversation so
56
- * the visualizations survive reloads.
54
+ * Legacy field — new messages use `blocks` to position each chart at
55
+ * the stream point where it was emitted. Still read by the renderer
56
+ * as a fallback for persisted messages saved before blocks.
57
57
  */
58
58
  charts?: CoralChartSpec[];
59
+ /**
60
+ * Ordered list of inline blocks (text, charts, todos) in the order the
61
+ * agent emitted them. Renderers iterate this in stream order so a chart
62
+ * tool call appears at the exact point in the prose where it happened,
63
+ * matching the convention used by Claude.ai and ChatGPT.
64
+ *
65
+ * When absent, the renderer synthesizes blocks from the legacy
66
+ * `content` / `charts` / `todos` fields so old persisted conversations
67
+ * still display correctly.
68
+ */
69
+ blocks?: ChatBlock[];
59
70
  /**
60
71
  * If present, the turn that produced this assistant message failed. The
61
72
  * message renders as an error card (with a Retry button) instead of normal
@@ -97,6 +108,22 @@ type ChatApprovalRequest = {
97
108
  text?: string;
98
109
  };
99
110
  };
111
+ /**
112
+ * One inline block within an assistant message. Blocks are rendered in the
113
+ * order the agent emitted them, so charts and todo lists appear at the exact
114
+ * stream position where they happened (e.g. text → chart → text), rather than
115
+ * being pinned above the message.
116
+ */
117
+ type ChatBlock = {
118
+ type: "text";
119
+ content: string;
120
+ } | {
121
+ type: "chart";
122
+ spec: CoralChartSpec;
123
+ } | {
124
+ type: "todos";
125
+ todos: ChatTodo[];
126
+ };
100
127
  /**
101
128
  * One step in a multi-step task plan. The agent emits the whole list on each
102
129
  * update; the convention is exactly one item `in_progress` at a time.
package/dist/index.js CHANGED
@@ -424,32 +424,54 @@ var ChatCanvas = ({
424
424
  const response = await resolvedHandler(textToSend, historyForAI, snapshotAttachments.map((a) => a.file));
425
425
  if (isAsyncIterable(response)) {
426
426
  let assistantContent = "";
427
+ const blocks = [];
428
+ let openText = "";
429
+ let todosIndex = null;
427
430
  let rafId = null;
428
- let pendingContent = "";
429
431
  let streamedApproval;
430
- let streamedTodos;
431
- let streamedCharts;
432
+ const snapshotBlocks = () => openText ? [...blocks, { type: "text", content: openText }] : blocks;
433
+ const flushOpenText = () => {
434
+ if (openText) {
435
+ blocks.push({ type: "text", content: openText });
436
+ openText = "";
437
+ }
438
+ };
439
+ const commit = () => {
440
+ const nextBlocks = snapshotBlocks();
441
+ let nextMsg = {
442
+ id: assistantId,
443
+ role: "assistant",
444
+ content: assistantContent,
445
+ blocks: nextBlocks
446
+ };
447
+ setMessages((prev) => {
448
+ const updated = [...prev];
449
+ const last = updated[updated.length - 1];
450
+ if (last && last.id === assistantId) {
451
+ nextMsg = {
452
+ ...last,
453
+ content: assistantContent,
454
+ blocks: nextBlocks
455
+ };
456
+ }
457
+ updated[updated.length - 1] = nextMsg;
458
+ return updated;
459
+ });
460
+ onMessagesChange?.(
461
+ [...updatedMessages, nextMsg],
462
+ sessionAtSend
463
+ );
464
+ };
432
465
  for await (const chunk of response) {
433
466
  if (shouldStopRef.current) break;
434
467
  if (sessionAtSend !== void 0 && currentConversationIdRef.current !== sessionAtSend) break;
435
468
  if (typeof chunk === "string") {
436
469
  assistantContent += chunk;
437
- pendingContent = assistantContent;
470
+ openText += chunk;
438
471
  if (rafId === null) {
439
472
  rafId = requestAnimationFrame(() => {
440
- const content = pendingContent;
441
- let nextMsg = { id: assistantId, role: "assistant", content };
442
- setMessages((prev) => {
443
- const updated = [...prev];
444
- const last = updated[updated.length - 1];
445
- if (last && last.id === assistantId) {
446
- nextMsg = { ...last, content };
447
- }
448
- updated[updated.length - 1] = nextMsg;
449
- return updated;
450
- });
451
- onMessagesChange?.([...updatedMessages, nextMsg], sessionAtSend);
452
473
  rafId = null;
474
+ commit();
453
475
  });
454
476
  }
455
477
  } else if (chunk.type === "reasoning") {
@@ -468,26 +490,18 @@ var ChatCanvas = ({
468
490
  return updated;
469
491
  });
470
492
  } else if (chunk.type === "todos") {
471
- streamedTodos = chunk.todos;
472
- setMessages((prev) => {
473
- const updated = [...prev];
474
- const last = updated[updated.length - 1];
475
- if (last && last.id === assistantId) {
476
- updated[updated.length - 1] = { ...last, todos: chunk.todos };
477
- }
478
- return updated;
479
- });
493
+ if (todosIndex === null) {
494
+ flushOpenText();
495
+ todosIndex = blocks.length;
496
+ blocks.push({ type: "todos", todos: chunk.todos });
497
+ } else {
498
+ blocks[todosIndex] = { type: "todos", todos: chunk.todos };
499
+ }
500
+ commit();
480
501
  } else if (chunk.type === "chart") {
481
- streamedCharts = [...streamedCharts ?? [], chunk.spec];
482
- const nextCharts = streamedCharts;
483
- setMessages((prev) => {
484
- const updated = [...prev];
485
- const last = updated[updated.length - 1];
486
- if (last && last.id === assistantId) {
487
- updated[updated.length - 1] = { ...last, charts: nextCharts };
488
- }
489
- return updated;
490
- });
502
+ flushOpenText();
503
+ blocks.push({ type: "chart", spec: chunk.spec });
504
+ commit();
491
505
  }
492
506
  }
493
507
  if (rafId !== null) cancelAnimationFrame(rafId);
@@ -495,14 +509,14 @@ var ChatCanvas = ({
495
509
  if (sessionAtSend !== void 0 && currentConversationIdRef.current !== sessionAtSend) {
496
510
  break;
497
511
  }
512
+ flushOpenText();
498
513
  const finalMsg = {
499
514
  id: assistantId,
500
515
  role: "assistant",
501
516
  content: assistantContent,
517
+ blocks,
502
518
  ...reasoningRef.current ? { metadata: { reasoning: reasoningRef.current } } : {},
503
- ...streamedApproval ? { approval: streamedApproval } : {},
504
- ...streamedTodos ? { todos: streamedTodos } : {},
505
- ...streamedCharts ? { charts: streamedCharts } : {}
519
+ ...streamedApproval ? { approval: streamedApproval } : {}
506
520
  };
507
521
  const finalMessages = [...updatedMessages, finalMsg];
508
522
  setMessages(finalMessages);
@@ -1215,6 +1229,18 @@ var Chat_TodoList_default = ChatTodoList;
1215
1229
  import { CoralChart } from "@coral-ai/charts";
1216
1230
  import { CheckmarkCircle20Filled } from "@fluentui/react-icons";
1217
1231
  import { Fragment, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1232
+ function resolveBlocks(msg) {
1233
+ if (msg.blocks) return msg.blocks;
1234
+ const legacy = [];
1235
+ if (msg.todos && msg.todos.length > 0) {
1236
+ legacy.push({ type: "todos", todos: msg.todos });
1237
+ }
1238
+ if (msg.charts) {
1239
+ for (const spec of msg.charts) legacy.push({ type: "chart", spec });
1240
+ }
1241
+ if (msg.content) legacy.push({ type: "text", content: msg.content });
1242
+ return legacy;
1243
+ }
1218
1244
  var useStyles7 = makeStyles7({
1219
1245
  messages: {
1220
1246
  flex: "1",
@@ -1272,7 +1298,7 @@ var useStyles7 = makeStyles7({
1272
1298
  display: "flex",
1273
1299
  flexDirection: "column",
1274
1300
  alignItems: "stretch",
1275
- width: "min(640px, 80vw)",
1301
+ // width: "min(640px, 80vw)",
1276
1302
  gap: "8px"
1277
1303
  },
1278
1304
  userEditTextarea: {
@@ -1578,17 +1604,24 @@ var ChatMessages = ({
1578
1604
  isStreaming: isLatest && isTyping && !msg.content
1579
1605
  }
1580
1606
  ),
1581
- msg.todos && msg.todos.length > 0 && /* @__PURE__ */ jsx7(Chat_TodoList_default, { todos: msg.todos }),
1582
- msg.charts && msg.charts.length > 0 && /* @__PURE__ */ jsx7(Fragment, { children: msg.charts.map((spec, i) => /* @__PURE__ */ jsx7(CoralChart, { spec }, i)) }),
1583
- /* @__PURE__ */ jsx7(
1584
- CoralMarkdown,
1585
- {
1586
- onNavigate,
1587
- fontSize,
1588
- fontFamily,
1589
- children: msg.content
1607
+ resolveBlocks(msg).map((block, i) => {
1608
+ if (block.type === "todos") {
1609
+ return /* @__PURE__ */ jsx7(Chat_TodoList_default, { todos: block.todos }, `todos-${i}`);
1590
1610
  }
1591
- ),
1611
+ if (block.type === "chart") {
1612
+ return /* @__PURE__ */ jsx7(CoralChart, { spec: block.spec }, `chart-${i}`);
1613
+ }
1614
+ return /* @__PURE__ */ jsx7(
1615
+ CoralMarkdown,
1616
+ {
1617
+ onNavigate,
1618
+ fontSize,
1619
+ fontFamily,
1620
+ children: block.content
1621
+ },
1622
+ `text-${i}`
1623
+ );
1624
+ }),
1592
1625
  msg.error && /* @__PURE__ */ jsx7(
1593
1626
  Chat_Error_default,
1594
1627
  {