@coral-ai/chat 0.3.0 → 0.4.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.js CHANGED
@@ -1,14 +1,16 @@
1
1
  // src/Chat.tsx
2
+ import { useLayoutEffect as useLayoutEffect3, useRef as useRef7, useState as useState7 } from "react";
2
3
  import {
3
- InteractionTag,
4
- InteractionTagPrimary,
5
- makeStyles as makeStyles8,
4
+ Caption1 as Caption14,
5
+ Title3,
6
+ makeStyles as makeStyles10,
6
7
  motionTokens
7
8
  } from "@fluentui/react-components";
9
+ import { useCoralConfig } from "@coral-ai/react";
8
10
 
9
11
  // src/Chat.Canvas.tsx
10
12
  import { useState, useEffect, useLayoutEffect, useRef, useCallback } from "react";
11
- import { Button, makeStyles, tokens } from "@fluentui/react-components";
13
+ import { Button as Button2, makeStyles as makeStyles2, tokens as tokens2 } from "@fluentui/react-components";
12
14
  import { ArrowDown } from "@coral-ai/bundleicons";
13
15
 
14
16
  // src/Chat.Context.tsx
@@ -18,9 +20,154 @@ function useChatContext() {
18
20
  return useContext(ChatContext);
19
21
  }
20
22
 
21
- // src/Chat.Canvas.tsx
23
+ // src/Chat.Error.tsx
24
+ import { Body1, Button, makeStyles, tokens } from "@fluentui/react-components";
25
+ import {
26
+ ErrorCircle20Regular,
27
+ Clock20Regular,
28
+ LockClosed20Regular,
29
+ PlugDisconnected20Regular,
30
+ Warning20Regular
31
+ } from "@fluentui/react-icons";
22
32
  import { jsx, jsxs } from "react/jsx-runtime";
33
+ var ERROR_CATEGORIES = [
34
+ "auth_billing",
35
+ "rate_limit",
36
+ "overloaded",
37
+ "bad_input",
38
+ "not_found",
39
+ "server_error",
40
+ "unknown"
41
+ ];
42
+ function isNormalizedChatError(err) {
43
+ return !!err && typeof err === "object" && typeof err.message === "string" && ERROR_CATEGORIES.includes(err.category);
44
+ }
45
+ function normalizeChatError(err) {
46
+ if (isNormalizedChatError(err)) return err;
47
+ const anyErr = err;
48
+ const rawMessage = String(
49
+ anyErr?.message ?? anyErr?.error?.message ?? err ?? ""
50
+ ).toLowerCase();
51
+ const status = Number(anyErr?.status ?? anyErr?.statusCode ?? 0);
52
+ const errorType = String(
53
+ anyErr?.error?.type ?? anyErr?.type ?? anyErr?.code ?? ""
54
+ );
55
+ const errorCode = String(
56
+ anyErr?.error?.code ?? anyErr?.code ?? ""
57
+ );
58
+ if (rawMessage.includes("credit") || rawMessage.includes("billing") || rawMessage.includes("insufficient_quota") || errorCode === "insufficient_quota" || errorType === "PERMISSION_DENIED") {
59
+ return {
60
+ category: "auth_billing",
61
+ message: "API credits exhausted. Add credits in your provider's billing dashboard."
62
+ };
63
+ }
64
+ if (errorType === "rate_limit_error" || rawMessage.includes("rate limit") || rawMessage.includes("too many requests")) {
65
+ return {
66
+ category: "rate_limit",
67
+ message: "You're being rate limited. Wait a moment and try again."
68
+ };
69
+ }
70
+ if (status === 529 || status === 503 || errorType === "overloaded_error" || errorType === "RESOURCE_EXHAUSTED" || rawMessage.includes("overload") || rawMessage.includes("capacity")) {
71
+ return {
72
+ category: "overloaded",
73
+ message: "The AI service is overloaded right now. Try again in a few seconds."
74
+ };
75
+ }
76
+ if (status === 404 || errorType === "NOT_FOUND" || rawMessage.includes("model_not_found") || rawMessage.includes("not found")) {
77
+ return {
78
+ category: "not_found",
79
+ message: "Model not found. Check the model name in your configuration."
80
+ };
81
+ }
82
+ if (status === 400 || errorType === "invalid_request_error" || errorType === "INVALID_ARGUMENT") {
83
+ return {
84
+ category: "bad_input",
85
+ message: "The request was invalid. Check your message or API configuration."
86
+ };
87
+ }
88
+ if (status >= 500 || errorType === "api_error" || errorType === "INTERNAL") {
89
+ return {
90
+ category: "server_error",
91
+ message: "The AI service hit an internal error. Try again shortly."
92
+ };
93
+ }
94
+ return {
95
+ category: "unknown",
96
+ message: "Something went wrong. Check your connection and try again."
97
+ };
98
+ }
23
99
  var useStyles = makeStyles({
100
+ root: {
101
+ display: "flex",
102
+ alignItems: "center",
103
+ justifyContent: "space-between",
104
+ gap: "8px",
105
+ padding: "12px",
106
+ borderRadius: tokens.borderRadiusXLarge,
107
+ backgroundColor: tokens.colorNeutralBackground3,
108
+ border: `1px solid ${tokens.colorNeutralStroke2}`,
109
+ color: tokens.colorNeutralForeground3,
110
+ width: "100%",
111
+ boxSizing: "border-box",
112
+ margin: "8px 0"
113
+ },
114
+ left: {
115
+ display: "flex",
116
+ alignItems: "center",
117
+ gap: "8px",
118
+ minWidth: 0
119
+ },
120
+ message: {
121
+ fontFamily: "var(--fontFamilyMonospace)"
122
+ }
123
+ });
124
+ var categoryIcon = {
125
+ auth_billing: /* @__PURE__ */ jsx(LockClosed20Regular, {}),
126
+ rate_limit: /* @__PURE__ */ jsx(Clock20Regular, {}),
127
+ overloaded: /* @__PURE__ */ jsx(PlugDisconnected20Regular, {}),
128
+ bad_input: /* @__PURE__ */ jsx(Warning20Regular, {}),
129
+ not_found: /* @__PURE__ */ jsx(ErrorCircle20Regular, {}),
130
+ server_error: /* @__PURE__ */ jsx(ErrorCircle20Regular, {}),
131
+ unknown: /* @__PURE__ */ jsx(Warning20Regular, {})
132
+ };
133
+ var ChatError = ({ error, onRetry }) => {
134
+ const styles = useStyles();
135
+ const { category, message } = normalizeChatError(error);
136
+ return /* @__PURE__ */ jsxs("div", { className: styles.root, children: [
137
+ /* @__PURE__ */ jsxs("span", { className: styles.left, children: [
138
+ categoryIcon[category],
139
+ /* @__PURE__ */ jsx(Body1, { className: styles.message, children: message })
140
+ ] }),
141
+ onRetry && /* @__PURE__ */ jsx(
142
+ Button,
143
+ {
144
+ size: "small",
145
+ appearance: "primary",
146
+ onClick: onRetry,
147
+ children: "Retry"
148
+ }
149
+ )
150
+ ] });
151
+ };
152
+ var Chat_Error_default = ChatError;
153
+
154
+ // src/fileToBase64.ts
155
+ function fileToBase64(file) {
156
+ return new Promise((resolve, reject) => {
157
+ const reader = new FileReader();
158
+ reader.onload = () => {
159
+ const dataUrl = reader.result;
160
+ const comma = dataUrl.indexOf(",");
161
+ resolve(comma === -1 ? dataUrl : dataUrl.slice(comma + 1));
162
+ };
163
+ reader.onerror = () => reject(reader.error ?? new Error("FileReader failed"));
164
+ reader.readAsDataURL(file);
165
+ });
166
+ }
167
+
168
+ // src/Chat.Canvas.tsx
169
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
170
+ var useStyles2 = makeStyles2({
24
171
  root: {
25
172
  width: "100%",
26
173
  display: "flex",
@@ -35,8 +182,10 @@ var useStyles = makeStyles({
35
182
  transform: "translateX(-50%)",
36
183
  zIndex: 1e3,
37
184
  cursor: "pointer",
38
- boxShadow: tokens.shadow8,
39
- marginBottom: "16px"
185
+ boxShadow: tokens2.shadow8,
186
+ marginBottom: "48px",
187
+ backgroundColor: "var(--colorNeutralBackgroundAlpha2)",
188
+ backdropFilter: "blur(40px) saturate(150%)"
40
189
  }
41
190
  });
42
191
  function isAsyncIterable(value) {
@@ -48,9 +197,54 @@ function isRateLimitError(err) {
48
197
  const msg = String(e?.message ?? "").toLowerCase();
49
198
  return status === 429 || msg.includes("rate limit") || msg.includes("too many requests");
50
199
  }
200
+ async function* wrapWithStatusChunk(taskType, reply) {
201
+ yield { type: "status", name: taskType };
202
+ if (reply) yield reply;
203
+ }
204
+ async function* parseStream(res) {
205
+ const reader = res.body?.getReader();
206
+ if (!reader) return;
207
+ const decoder = new TextDecoder();
208
+ let buffer = "";
209
+ while (true) {
210
+ const { value, done } = await reader.read();
211
+ if (done) break;
212
+ buffer += decoder.decode(value, { stream: true });
213
+ const lines = buffer.split("\n");
214
+ buffer = lines.pop() ?? "";
215
+ for (const raw of lines) {
216
+ const line = raw.startsWith("data:") ? raw.slice(5).trim() : raw.trim();
217
+ if (!line || line === "[DONE]") continue;
218
+ try {
219
+ const parsed = JSON.parse(line);
220
+ if (typeof parsed === "string") {
221
+ yield parsed;
222
+ } else if (parsed && typeof parsed === "object" && "type" in parsed) {
223
+ yield parsed;
224
+ } else if (parsed && typeof parsed === "object" && typeof parsed.reply === "string") {
225
+ if (typeof parsed.type === "string") {
226
+ yield { type: "status", name: parsed.type };
227
+ }
228
+ yield parsed.reply;
229
+ }
230
+ } catch {
231
+ yield line;
232
+ }
233
+ }
234
+ }
235
+ if (buffer.trim()) {
236
+ try {
237
+ const parsed = JSON.parse(buffer);
238
+ if (typeof parsed === "string") yield parsed;
239
+ else if (parsed && typeof parsed === "object" && "type" in parsed) yield parsed;
240
+ } catch {
241
+ yield buffer;
242
+ }
243
+ }
244
+ }
51
245
  var ChatCanvas = ({
52
- agent,
53
- agents,
246
+ api,
247
+ allowedAttachments,
54
248
  conversationId,
55
249
  initialMessages,
56
250
  systemPrompt,
@@ -63,20 +257,67 @@ var ChatCanvas = ({
63
257
  onStop,
64
258
  children
65
259
  }) => {
66
- const styles = useStyles();
67
- const agentKey = agent ?? "default";
68
- const resolvedHandler = onSendMessage ?? agents?.[agentKey]?.handler ?? agents?.["default"]?.handler;
69
- const [messages, setMessages] = useState(initialMessages ?? []);
260
+ const styles = useStyles2();
261
+ const apiHandler = api ? async function(input2, history, files) {
262
+ const attachments2 = await Promise.all(
263
+ files.map(async (file) => ({
264
+ name: file.name,
265
+ mediaType: file.type,
266
+ data: await fileToBase64(file)
267
+ }))
268
+ );
269
+ const res = await fetch(api, {
270
+ method: "POST",
271
+ headers: { "Content-Type": "application/json" },
272
+ body: JSON.stringify({
273
+ text: input2,
274
+ history,
275
+ attachments: attachments2
276
+ })
277
+ });
278
+ if (!res.ok) {
279
+ const text = await res.text().catch(() => "");
280
+ throw Object.assign(
281
+ new Error(`Chat API request failed (${res.status}): ${text || res.statusText}`),
282
+ { status: res.status }
283
+ );
284
+ }
285
+ const contentType = res.headers.get("content-type") ?? "";
286
+ if (contentType.includes("text/event-stream") || contentType.includes("application/x-ndjson")) {
287
+ return parseStream(res);
288
+ }
289
+ const data = await res.json();
290
+ if (data && typeof data === "object") {
291
+ if (typeof data.type === "string") {
292
+ return wrapWithStatusChunk(data.type, String(data.reply ?? ""));
293
+ }
294
+ if (typeof data.reply === "string") return data.reply;
295
+ }
296
+ return typeof data === "string" ? data : "";
297
+ } : void 0;
298
+ const resolvedHandler = onSendMessage ?? apiHandler;
299
+ const [messages, setMessages] = useState(
300
+ () => (initialMessages ?? []).map((m) => m.id ? m : { ...m, id: crypto.randomUUID() })
301
+ );
70
302
  const [input, setInput] = useState("");
71
303
  const [isTyping, setIsTyping] = useState(false);
72
304
  const [isRetrying, setIsRetrying] = useState(false);
73
- const [chatError, setChatError] = useState(null);
74
305
  const [showScrollButton, setShowScrollButton] = useState(false);
75
306
  const [inputHeight, setInputHeightState] = useState(0);
76
307
  const [isInterrupted, setIsInterrupted] = useState(false);
77
308
  const [streamingReasoning, setStreamingReasoning] = useState("");
78
- const [streamingAgentName, setStreamingAgentName] = useState();
309
+ const [streamingStatus, setStreamingStatus] = useState();
79
310
  const [attachments, setAttachments] = useState([]);
311
+ const [awaitingFreeformApproval, setAwaitingFreeformApproval] = useState(null);
312
+ const pendingApproval = (() => {
313
+ for (let i = messages.length - 1; i >= 0; i--) {
314
+ const m = messages[i];
315
+ if (m.role !== "assistant") continue;
316
+ if (m.approval && !m.approval.answer) return m.approval;
317
+ if (m.approval && m.approval.answer) return null;
318
+ }
319
+ return null;
320
+ })();
80
321
  const reasoningRef = useRef("");
81
322
  const lastUserInputRef = useRef("");
82
323
  const hasEverSentRef = useRef(false);
@@ -89,7 +330,9 @@ var ChatCanvas = ({
89
330
  useEffect(() => {
90
331
  setMessages((current) => {
91
332
  if ((initialMessages?.length ?? 0) > 0 || current.length === 0) {
92
- return initialMessages ?? [];
333
+ return (initialMessages ?? []).map(
334
+ (m) => m.id ? m : { ...m, id: crypto.randomUUID() }
335
+ );
93
336
  }
94
337
  return current;
95
338
  });
@@ -156,40 +399,18 @@ var ChatCanvas = ({
156
399
  const removeAttachment = (id) => {
157
400
  setAttachments((prev) => prev.filter((a) => a.id !== id));
158
401
  };
159
- const sendMessage = async (overrideInput) => {
160
- const textToSend = overrideInput ?? input;
161
- if (!textToSend.trim()) return;
162
- hasEverSentRef.current = true;
402
+ const runHandler = async (textToSend, updatedMessages, snapshotAttachments) => {
163
403
  const sessionAtSend = conversationId;
164
- const baseMessages = overrideInput ? messages.filter((_, i) => i < messages.length - 2) : messages;
165
- const snapshotAttachments = attachments;
166
- const attachmentMeta = snapshotAttachments.map((a) => ({
167
- name: a.file.name,
168
- type: a.file.type,
169
- preview: a.preview
170
- }));
171
- const updatedMessages = [
172
- ...baseMessages,
173
- {
174
- id: crypto.randomUUID(),
175
- role: "user",
176
- content: textToSend.replace(/^[\n\r]+|[\n\r]+$/g, ""),
177
- ...attachmentMeta.length > 0 ? { metadata: { attachments: attachmentMeta } } : {}
178
- }
179
- ];
180
404
  lastUserInputRef.current = textToSend;
181
405
  userScrolledUpRef.current = false;
182
406
  setMessages(updatedMessages);
183
407
  onMessagesChange?.(updatedMessages, sessionAtSend);
184
- if (!overrideInput) setInput("");
185
- setAttachments([]);
186
408
  setIsTyping(true);
187
409
  setIsInterrupted(false);
188
- setChatError(null);
189
410
  shouldStopRef.current = false;
190
411
  reasoningRef.current = "";
191
412
  setStreamingReasoning("");
192
- setStreamingAgentName(void 0);
413
+ setStreamingStatus(void 0);
193
414
  const historyForAI = systemPrompt ? [{ role: "system", content: systemPrompt }, ...updatedMessages] : updatedMessages;
194
415
  const MAX_RETRIES = 3;
195
416
  try {
@@ -205,6 +426,8 @@ var ChatCanvas = ({
205
426
  let assistantContent = "";
206
427
  let rafId = null;
207
428
  let pendingContent = "";
429
+ let streamedApproval;
430
+ let streamedTodos;
208
431
  for await (const chunk of response) {
209
432
  if (shouldStopRef.current) break;
210
433
  if (sessionAtSend !== void 0 && currentConversationIdRef.current !== sessionAtSend) break;
@@ -214,9 +437,13 @@ var ChatCanvas = ({
214
437
  if (rafId === null) {
215
438
  rafId = requestAnimationFrame(() => {
216
439
  const content = pendingContent;
217
- const nextMsg = { id: assistantId, role: "assistant", content };
440
+ let nextMsg = { id: assistantId, role: "assistant", content };
218
441
  setMessages((prev) => {
219
442
  const updated = [...prev];
443
+ const last = updated[updated.length - 1];
444
+ if (last && last.id === assistantId) {
445
+ nextMsg = { ...last, content };
446
+ }
220
447
  updated[updated.length - 1] = nextMsg;
221
448
  return updated;
222
449
  });
@@ -227,8 +454,28 @@ var ChatCanvas = ({
227
454
  } else if (chunk.type === "reasoning") {
228
455
  reasoningRef.current += chunk.content;
229
456
  setStreamingReasoning(reasoningRef.current);
230
- } else if (chunk.type === "agent") {
231
- setStreamingAgentName(chunk.name);
457
+ } else if (chunk.type === "status") {
458
+ setStreamingStatus(chunk.name);
459
+ } else if (chunk.type === "approval") {
460
+ streamedApproval = chunk.approval;
461
+ setMessages((prev) => {
462
+ const updated = [...prev];
463
+ const last = updated[updated.length - 1];
464
+ if (last && last.id === assistantId) {
465
+ updated[updated.length - 1] = { ...last, approval: chunk.approval };
466
+ }
467
+ return updated;
468
+ });
469
+ } else if (chunk.type === "todos") {
470
+ streamedTodos = chunk.todos;
471
+ setMessages((prev) => {
472
+ const updated = [...prev];
473
+ const last = updated[updated.length - 1];
474
+ if (last && last.id === assistantId) {
475
+ updated[updated.length - 1] = { ...last, todos: chunk.todos };
476
+ }
477
+ return updated;
478
+ });
232
479
  }
233
480
  }
234
481
  if (rafId !== null) cancelAnimationFrame(rafId);
@@ -240,7 +487,9 @@ var ChatCanvas = ({
240
487
  id: assistantId,
241
488
  role: "assistant",
242
489
  content: assistantContent,
243
- ...reasoningRef.current ? { metadata: { reasoning: reasoningRef.current } } : {}
490
+ ...reasoningRef.current ? { metadata: { reasoning: reasoningRef.current } } : {},
491
+ ...streamedApproval ? { approval: streamedApproval } : {},
492
+ ...streamedTodos ? { todos: streamedTodos } : {}
244
493
  };
245
494
  const finalMessages = [...updatedMessages, finalMsg];
246
495
  setMessages(finalMessages);
@@ -259,7 +508,8 @@ var ChatCanvas = ({
259
508
  role: "assistant",
260
509
  content: response.content,
261
510
  type: response.type,
262
- metadata: response.metadata
511
+ metadata: response.metadata,
512
+ todos: response.todos
263
513
  };
264
514
  const finalMessages = [...updatedMessages, assistantMsg];
265
515
  setMessages(finalMessages);
@@ -279,17 +529,148 @@ var ChatCanvas = ({
279
529
  }
280
530
  } catch (err) {
281
531
  onError?.(err);
282
- setChatError(err);
532
+ if (sessionAtSend === void 0 || currentConversationIdRef.current === sessionAtSend) {
533
+ const erroredMessages = [
534
+ ...updatedMessages,
535
+ {
536
+ id: crypto.randomUUID(),
537
+ role: "assistant",
538
+ content: "",
539
+ error: normalizeChatError(err)
540
+ }
541
+ ];
542
+ setMessages(erroredMessages);
543
+ onMessagesChange?.(erroredMessages, sessionAtSend);
544
+ }
283
545
  } finally {
284
546
  setIsRetrying(false);
285
547
  setIsTyping(false);
286
548
  }
287
549
  };
288
- return /* @__PURE__ */ jsx(
550
+ const resolveApproval = (approvalId, optionId) => {
551
+ const target = messages.find(
552
+ (m) => m.role === "assistant" && m.approval?.id === approvalId
553
+ );
554
+ if (!target || !target.approval) return;
555
+ if (target.approval.answer) return;
556
+ const option = target.approval.options.find((o) => o.id === optionId);
557
+ if (!option) return;
558
+ if (option.freeform) {
559
+ setAwaitingFreeformApproval({ approvalId, optionId });
560
+ return;
561
+ }
562
+ const updated = messages.map(
563
+ (m) => m.role === "assistant" && m.approval?.id === approvalId ? { ...m, approval: { ...m.approval, answer: { optionId } } } : m
564
+ );
565
+ const replyText = option.label;
566
+ const userMsg = {
567
+ id: crypto.randomUUID(),
568
+ role: "user",
569
+ type: "approval_response",
570
+ content: replyText,
571
+ metadata: { approvalId, optionId, label: option.label }
572
+ };
573
+ const next = [...updated, userMsg];
574
+ void runHandler(replyText, next, []);
575
+ };
576
+ const sendMessage = async (overrideInput) => {
577
+ const textToSend = overrideInput ?? input;
578
+ if (!textToSend.trim()) return;
579
+ if (awaitingFreeformApproval) {
580
+ const { approvalId, optionId } = awaitingFreeformApproval;
581
+ const target = messages.find(
582
+ (m) => m.role === "assistant" && m.approval?.id === approvalId
583
+ );
584
+ if (target && target.approval) {
585
+ const option = target.approval.options.find((o) => o.id === optionId);
586
+ const updated = messages.map(
587
+ (m) => m.role === "assistant" && m.approval?.id === approvalId ? { ...m, approval: { ...m.approval, answer: { optionId, text: textToSend } } } : m
588
+ );
589
+ const userMsg = {
590
+ id: crypto.randomUUID(),
591
+ role: "user",
592
+ type: "approval_response",
593
+ content: textToSend.replace(/^[\n\r]+|[\n\r]+$/g, ""),
594
+ metadata: { approvalId, optionId, label: option?.label, text: textToSend }
595
+ };
596
+ const next = [...updated, userMsg];
597
+ if (!overrideInput) setInput("");
598
+ setAwaitingFreeformApproval(null);
599
+ hasEverSentRef.current = true;
600
+ await runHandler(textToSend, next, []);
601
+ return;
602
+ }
603
+ setAwaitingFreeformApproval(null);
604
+ }
605
+ hasEverSentRef.current = true;
606
+ const lastMessage = messages[messages.length - 1];
607
+ const baseMessages = overrideInput ? messages.filter((_, i) => i < messages.length - 2) : lastMessage?.role === "assistant" && lastMessage.error ? messages.slice(0, -1) : messages;
608
+ const snapshotAttachments = attachments;
609
+ const attachmentMeta = snapshotAttachments.map((a) => ({
610
+ name: a.file.name,
611
+ type: a.file.type,
612
+ preview: a.preview
613
+ }));
614
+ const updatedMessages = [
615
+ ...baseMessages,
616
+ {
617
+ id: crypto.randomUUID(),
618
+ role: "user",
619
+ content: textToSend.replace(/^[\n\r]+|[\n\r]+$/g, ""),
620
+ ...attachmentMeta.length > 0 ? { metadata: { attachments: attachmentMeta } } : {}
621
+ }
622
+ ];
623
+ if (!overrideInput) setInput("");
624
+ setAttachments([]);
625
+ await runHandler(textToSend, updatedMessages, snapshotAttachments);
626
+ };
627
+ const resolveRegenerateTarget = (messageId) => {
628
+ if (!messageId) {
629
+ for (let i = messages.length - 1; i >= 0; i--) {
630
+ if (messages[i].role === "user") return i;
631
+ }
632
+ return -1;
633
+ }
634
+ const idx = messages.findIndex((m) => m.id === messageId);
635
+ if (idx === -1) return -1;
636
+ if (messages[idx].role === "user") return idx;
637
+ if (messages[idx].role === "assistant") {
638
+ for (let i = idx - 1; i >= 0; i--) {
639
+ if (messages[i].role === "user") return i;
640
+ }
641
+ }
642
+ return -1;
643
+ };
644
+ const regenerate = async (messageId) => {
645
+ const userIdx = resolveRegenerateTarget(messageId);
646
+ if (userIdx === -1) return;
647
+ const userMsg = messages[userIdx];
648
+ const baseMessages = messages.slice(0, userIdx + 1);
649
+ await runHandler(userMsg.content, baseMessages, []);
650
+ };
651
+ const editMessage = async (messageId, newContent) => {
652
+ const idx = messages.findIndex((m) => m.id === messageId);
653
+ if (idx === -1) return;
654
+ if (messages[idx].role !== "user") return;
655
+ const trimmed = newContent.replace(/^[\n\r]+|[\n\r]+$/g, "");
656
+ if (!trimmed.trim()) return;
657
+ const editedMessage = {
658
+ ...messages[idx],
659
+ content: trimmed
660
+ };
661
+ const baseMessages = [
662
+ ...messages.slice(0, idx),
663
+ editedMessage
664
+ ];
665
+ await runHandler(trimmed, baseMessages, []);
666
+ };
667
+ return /* @__PURE__ */ jsx2(
289
668
  ChatContext.Provider,
290
669
  {
291
670
  value: {
292
671
  sendMessage,
672
+ regenerate,
673
+ editMessage,
293
674
  scrollToBottom,
294
675
  handleCopy,
295
676
  messages,
@@ -300,9 +681,8 @@ var ChatCanvas = ({
300
681
  isRetrying,
301
682
  isInterrupted,
302
683
  streamingReasoning,
303
- streamingAgentName,
684
+ streamingStatus,
304
685
  thinkingStatus,
305
- chatError,
306
686
  showScrollButton,
307
687
  setInputHeight: setInputHeightState,
308
688
  messagesContainerRef,
@@ -314,12 +694,16 @@ var ChatCanvas = ({
314
694
  onStop,
315
695
  attachments,
316
696
  addAttachments,
317
- removeAttachment
697
+ removeAttachment,
698
+ allowedAttachments,
699
+ pendingApproval,
700
+ awaitingFreeformApproval,
701
+ resolveApproval
318
702
  },
319
- children: /* @__PURE__ */ jsxs("div", { className: styles.root, children: [
703
+ children: /* @__PURE__ */ jsxs2("div", { className: styles.root, children: [
320
704
  children,
321
- showScrollButton && /* @__PURE__ */ jsx(
322
- Button,
705
+ showScrollButton && /* @__PURE__ */ jsx2(
706
+ Button2,
323
707
  {
324
708
  onClick: scrollToBottom,
325
709
  className: styles.scrollToBottom,
@@ -327,10 +711,10 @@ var ChatCanvas = ({
327
711
  style: {
328
712
  bottom: inputHeight + 16,
329
713
  backgroundColor: "transparent",
330
- border: `1px solid ${tokens.colorNeutralStroke3}`,
714
+ border: `1px solid ${tokens2.colorNeutralStroke3}`,
331
715
  backdropFilter: "saturate(180%) blur(16px)"
332
716
  },
333
- icon: /* @__PURE__ */ jsx(ArrowDown, { style: { fontSize: "16px" } })
717
+ icon: /* @__PURE__ */ jsx2(ArrowDown, { style: { fontSize: "16px" } })
334
718
  }
335
719
  )
336
720
  ] })
@@ -340,22 +724,34 @@ var ChatCanvas = ({
340
724
  var Chat_Canvas_default = ChatCanvas;
341
725
 
342
726
  // src/Chat.Messages.tsx
343
- import React4, { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
344
- import { Body1 as Body14, Button as Button4, makeStyles as makeStyles6, tokens as tokens6 } from "@fluentui/react-components";
345
- import { Copy, Heart } from "@coral-ai/bundleicons";
346
- import { CoralMarkdown } from "@coral-ai/markdown";
727
+ import React5, { useEffect as useEffect4, useLayoutEffect as useLayoutEffect2, useRef as useRef4, useState as useState5 } from "react";
728
+ import {
729
+ Body1 as Body15,
730
+ Button as Button4,
731
+ Caption1 as Caption12,
732
+ makeStyles as makeStyles7,
733
+ tokens as tokens7
734
+ } from "@fluentui/react-components";
735
+ import {
736
+ ArrowClockwise,
737
+ Copy,
738
+ Edit as EditIcon,
739
+ Info
740
+ } from "@coral-ai/bundleicons";
741
+ import {
742
+ CoralMarkdown
743
+ } from "@coral-ai/markdown";
347
744
 
348
745
  // src/Chat.ThinkingIndicator.tsx
349
746
  import { useState as useState2, useEffect as useEffect2, useRef as useRef2 } from "react";
350
- import { makeStyles as makeStyles2, tokens as tokens2 } from "@fluentui/react-components";
351
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
747
+ import { makeStyles as makeStyles3, Spinner, tokens as tokens3 } from "@fluentui/react-components";
748
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
352
749
  var thinkingMessages = [
353
750
  "Generating",
354
751
  "Thinking",
355
752
  "Optimizing",
356
753
  "Processing",
357
754
  "Analyzing",
358
- "Reasoning",
359
755
  "Drafting",
360
756
  "Evaluating",
361
757
  "Computing",
@@ -374,45 +770,13 @@ var retryingMessages = [
374
770
  ];
375
771
  var TYPING_SPEED = 80;
376
772
  var HOLD_DURATION = 1300;
377
- var SPINNER_BG = "#000000";
378
- var useStyles2 = makeStyles2({
773
+ var useStyles3 = makeStyles3({
379
774
  root: {
380
775
  display: "flex",
381
776
  alignItems: "center",
382
777
  gap: "8px",
383
778
  width: "100%",
384
- color: tokens2.colorNeutralForeground3
385
- // fontSize: "14px",
386
- },
387
- icon: {
388
- width: "28px",
389
- height: "28px",
390
- flexShrink: 0,
391
- border: `3px solid ${SPINNER_BG}`,
392
- boxSizing: "border-box",
393
- backgroundColor: "${SPINNER_BG}",
394
- opacity: 0.8,
395
- backgroundImage: [
396
- `radial-gradient(farthest-side, ${tokens2.colorNeutralForeground1} 98%, ${SPINNER_BG})`,
397
- `radial-gradient(farthest-side, ${tokens2.colorNeutralForeground1} 98%, ${SPINNER_BG})`,
398
- `radial-gradient(farthest-side, ${tokens2.colorNeutralForeground1} 98%, ${SPINNER_BG})`,
399
- `radial-gradient(farthest-side, ${tokens2.colorNeutralForeground1} 98%, ${SPINNER_BG})`,
400
- `radial-gradient(farthest-side, ${tokens2.colorNeutralForeground1} 98%, ${SPINNER_BG})`,
401
- `linear-gradient(${tokens2.colorNeutralForeground1} 0 0)`,
402
- `linear-gradient(${tokens2.colorNeutralForeground1} 0 0)`
403
- ].join(", "),
404
- backgroundSize: "7px 7px, 7px 7px, 7px 7px, 7px 7px, 7px 7px, 100% 3px, 3px 100%",
405
- backgroundPosition: "50%, 50%, 50%, 50%, 50%, 50%, 50%",
406
- backgroundRepeat: "no-repeat",
407
- filter: "blur(2px) contrast(10)",
408
- animationName: {
409
- "100%": {
410
- backgroundPosition: "50% -7px, -7px 50%, 22px 50%, 50% 22px, 50%, 50%, 50%"
411
- }
412
- },
413
- animationDuration: "0.8s",
414
- animationIterationCount: "infinite",
415
- animationTimingFunction: "ease"
779
+ color: tokens3.colorNeutralForeground3
416
780
  },
417
781
  text: {
418
782
  // fontFamily: tokens.fontFamilyBase,
@@ -425,7 +789,7 @@ var useStyles2 = makeStyles2({
425
789
  display: "inline-block",
426
790
  width: "6px",
427
791
  height: "1em",
428
- backgroundColor: tokens2.colorNeutralForeground3,
792
+ backgroundColor: tokens3.colorNeutralForeground3,
429
793
  marginLeft: "1px",
430
794
  verticalAlign: "text-bottom",
431
795
  animationName: {
@@ -437,14 +801,14 @@ var useStyles2 = makeStyles2({
437
801
  animationIterationCount: "infinite"
438
802
  }
439
803
  });
440
- var ThinkingIndicator = ({ isRetrying = false, status }) => {
441
- const styles = useStyles2();
804
+ var ThinkingIndicator = ({ isRetrying = false, status, messages }) => {
805
+ const styles = useStyles3();
442
806
  const [displayText, setDisplayText] = useState2("");
443
807
  const timerRef = useRef2(void 0);
444
808
  const msgIdxRef = useRef2(0);
445
809
  const charIdxRef = useRef2(0);
446
810
  useEffect2(() => {
447
- const pool = isRetrying ? retryingMessages : thinkingMessages;
811
+ const pool = isRetrying ? retryingMessages : messages && messages.length > 0 ? messages : thinkingMessages;
448
812
  msgIdxRef.current = 0;
449
813
  charIdxRef.current = 0;
450
814
  const tick = () => {
@@ -463,32 +827,58 @@ var ThinkingIndicator = ({ isRetrying = false, status }) => {
463
827
  };
464
828
  tick();
465
829
  return () => clearTimeout(timerRef.current);
466
- }, [isRetrying]);
467
- return /* @__PURE__ */ jsxs2("div", { className: styles.root, children: [
468
- /* @__PURE__ */ jsx2("div", { className: styles.icon }),
469
- /* @__PURE__ */ jsxs2("span", { className: styles.text, children: [
830
+ }, [isRetrying, messages]);
831
+ return /* @__PURE__ */ jsxs3("div", { className: styles.root, children: [
832
+ /* @__PURE__ */ jsx3(Spinner, { size: "tiny" }),
833
+ /* @__PURE__ */ jsxs3("span", { className: styles.text, children: [
470
834
  status ?? displayText,
471
- /* @__PURE__ */ jsx2("span", { className: styles.cursor })
835
+ /* @__PURE__ */ jsx3("span", { className: styles.cursor })
472
836
  ] })
473
837
  ] });
474
838
  };
475
839
  var Chat_ThinkingIndicator_default = ThinkingIndicator;
476
840
 
841
+ // src/Chat.TaskMessages.ts
842
+ var DEFAULT_TASK_MESSAGES = {
843
+ text: [
844
+ "Generating",
845
+ "Thinking",
846
+ "Optimizing",
847
+ "Processing",
848
+ "Analyzing",
849
+ "Drafting",
850
+ "Evaluating",
851
+ "Computing",
852
+ "Synthesizing",
853
+ "Reviewing",
854
+ "Refining"
855
+ ],
856
+ photo: ["Looking", "Examining", "Reading image", "Analyzing visuals"],
857
+ file: ["Reading document", "Parsing", "Reviewing", "Scanning"],
858
+ voice: ["Listening", "Transcribing", "Processing audio"]
859
+ };
860
+ function resolveTaskMessages(name) {
861
+ if (name && name in DEFAULT_TASK_MESSAGES) {
862
+ return DEFAULT_TASK_MESSAGES[name];
863
+ }
864
+ return DEFAULT_TASK_MESSAGES.text;
865
+ }
866
+
477
867
  // src/Chat.Callout.tsx
478
- import { Body1, Button as Button2, makeStyles as makeStyles3, mergeClasses, tokens as tokens3 } from "@fluentui/react-components";
868
+ import { Body1 as Body12, Button as Button3, makeStyles as makeStyles4, mergeClasses, tokens as tokens4 } from "@fluentui/react-components";
479
869
  import { ChatWarning20Regular } from "@fluentui/react-icons";
480
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
481
- var useStyles3 = makeStyles3({
870
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
871
+ var useStyles4 = makeStyles4({
482
872
  root: {
483
873
  display: "inline-flex",
484
874
  alignItems: "center",
485
875
  justifyContent: "space-between",
486
876
  gap: "8px",
487
877
  padding: "12px 12px",
488
- borderRadius: tokens3.borderRadiusXLarge,
489
- backgroundColor: tokens3.colorNeutralBackground3,
490
- border: `1px solid ${tokens3.colorNeutralStroke2}`,
491
- color: tokens3.colorNeutralForeground3,
878
+ borderRadius: tokens4.borderRadiusXLarge,
879
+ backgroundColor: tokens4.colorNeutralBackground3,
880
+ border: `1px solid ${tokens4.colorNeutralStroke2}`,
881
+ color: tokens4.colorNeutralForeground3,
492
882
  width: "100%",
493
883
  boxSizing: "border-box",
494
884
  margin: "24px 0"
@@ -506,143 +896,25 @@ var useStyles3 = makeStyles3({
506
896
  }
507
897
  });
508
898
  var ChatCallout = ({ message, onRetry, className }) => {
509
- const styles = useStyles3();
510
- return /* @__PURE__ */ jsxs3("div", { className: mergeClasses(styles.root, className), children: [
511
- /* @__PURE__ */ jsxs3("span", { className: styles.left, children: [
512
- /* @__PURE__ */ jsx3(ChatWarning20Regular, {}),
513
- /* @__PURE__ */ jsx3(Body1, { className: styles.label, children: message })
514
- ] }),
515
- onRetry && /* @__PURE__ */ jsx3(
516
- Button2,
517
- {
518
- onClick: onRetry,
519
- className: styles.retryButton,
520
- size: "small",
521
- appearance: "primary",
522
- children: "Retry"
523
- }
524
- )
525
- ] });
526
- };
527
- var Chat_Callout_default = ChatCallout;
528
-
529
- // src/Chat.Error.tsx
530
- import { Body1 as Body12, Button as Button3, makeStyles as makeStyles4, tokens as tokens4 } from "@fluentui/react-components";
531
- import {
532
- ErrorCircle20Regular,
533
- Clock20Regular,
534
- LockClosed20Regular,
535
- PlugDisconnected20Regular,
536
- Warning20Regular
537
- } from "@fluentui/react-icons";
538
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
539
- function normalizeChatError(err) {
540
- const anyErr = err;
541
- const rawMessage = String(
542
- anyErr?.message ?? anyErr?.error?.message ?? err ?? ""
543
- ).toLowerCase();
544
- const status = Number(anyErr?.status ?? anyErr?.statusCode ?? 0);
545
- const errorType = String(
546
- anyErr?.error?.type ?? anyErr?.type ?? anyErr?.code ?? ""
547
- );
548
- const errorCode = String(
549
- anyErr?.error?.code ?? anyErr?.code ?? ""
550
- );
551
- if (rawMessage.includes("credit") || rawMessage.includes("billing") || rawMessage.includes("insufficient_quota") || errorCode === "insufficient_quota" || errorType === "PERMISSION_DENIED") {
552
- return {
553
- category: "auth_billing",
554
- message: "API credits exhausted. Add credits in your provider's billing dashboard."
555
- };
556
- }
557
- if (errorType === "rate_limit_error" || rawMessage.includes("rate limit") || rawMessage.includes("too many requests")) {
558
- return {
559
- category: "rate_limit",
560
- message: "You're being rate limited. Wait a moment and try again."
561
- };
562
- }
563
- if (status === 529 || status === 503 || errorType === "overloaded_error" || errorType === "RESOURCE_EXHAUSTED" || rawMessage.includes("overload") || rawMessage.includes("capacity")) {
564
- return {
565
- category: "overloaded",
566
- message: "The AI service is overloaded right now. Try again in a few seconds."
567
- };
568
- }
569
- if (status === 404 || errorType === "NOT_FOUND" || rawMessage.includes("model_not_found") || rawMessage.includes("not found")) {
570
- return {
571
- category: "not_found",
572
- message: "Model not found. Check the model name in your configuration."
573
- };
574
- }
575
- if (status === 400 || errorType === "invalid_request_error" || errorType === "INVALID_ARGUMENT") {
576
- return {
577
- category: "bad_input",
578
- message: "The request was invalid. Check your message or API configuration."
579
- };
580
- }
581
- if (status >= 500 || errorType === "api_error" || errorType === "INTERNAL") {
582
- return {
583
- category: "server_error",
584
- message: "The AI service hit an internal error. Try again shortly."
585
- };
586
- }
587
- return {
588
- category: "unknown",
589
- message: "Something went wrong. Check your connection and try again."
590
- };
591
- }
592
- var useStyles4 = makeStyles4({
593
- root: {
594
- display: "flex",
595
- alignItems: "center",
596
- justifyContent: "space-between",
597
- gap: "8px",
598
- padding: "12px",
599
- borderRadius: tokens4.borderRadiusXLarge,
600
- backgroundColor: tokens4.colorNeutralBackground3,
601
- border: `1px solid ${tokens4.colorNeutralStroke2}`,
602
- color: tokens4.colorNeutralForeground3,
603
- width: "100%",
604
- boxSizing: "border-box",
605
- margin: "8px 0"
606
- },
607
- left: {
608
- display: "flex",
609
- alignItems: "center",
610
- gap: "8px",
611
- minWidth: 0
612
- },
613
- message: {
614
- fontFamily: "var(--fontFamilyMonospace)"
615
- }
616
- });
617
- var categoryIcon = {
618
- auth_billing: /* @__PURE__ */ jsx4(LockClosed20Regular, {}),
619
- rate_limit: /* @__PURE__ */ jsx4(Clock20Regular, {}),
620
- overloaded: /* @__PURE__ */ jsx4(PlugDisconnected20Regular, {}),
621
- bad_input: /* @__PURE__ */ jsx4(Warning20Regular, {}),
622
- not_found: /* @__PURE__ */ jsx4(ErrorCircle20Regular, {}),
623
- server_error: /* @__PURE__ */ jsx4(ErrorCircle20Regular, {}),
624
- unknown: /* @__PURE__ */ jsx4(Warning20Regular, {})
625
- };
626
- var ChatError = ({ error, onRetry }) => {
627
899
  const styles = useStyles4();
628
- const { category, message } = normalizeChatError(error);
629
- return /* @__PURE__ */ jsxs4("div", { className: styles.root, children: [
900
+ return /* @__PURE__ */ jsxs4("div", { className: mergeClasses(styles.root, className), children: [
630
901
  /* @__PURE__ */ jsxs4("span", { className: styles.left, children: [
631
- categoryIcon[category],
632
- /* @__PURE__ */ jsx4(Body12, { className: styles.message, children: message })
902
+ /* @__PURE__ */ jsx4(ChatWarning20Regular, {}),
903
+ /* @__PURE__ */ jsx4(Body12, { className: styles.label, children: message })
633
904
  ] }),
634
905
  onRetry && /* @__PURE__ */ jsx4(
635
906
  Button3,
636
907
  {
908
+ onClick: onRetry,
909
+ className: styles.retryButton,
637
910
  size: "small",
638
911
  appearance: "primary",
639
- onClick: onRetry,
640
912
  children: "Retry"
641
913
  }
642
914
  )
643
915
  ] });
644
916
  };
645
- var Chat_Error_default = ChatError;
917
+ var Chat_Callout_default = ChatCallout;
646
918
 
647
919
  // src/Chat.ReasoningBlock.tsx
648
920
  import { useState as useState3, useEffect as useEffect3, useRef as useRef3 } from "react";
@@ -763,10 +1035,168 @@ var ReasoningBlock = ({
763
1035
  };
764
1036
  var Chat_ReasoningBlock_default = ReasoningBlock;
765
1037
 
766
- // src/Chat.Messages.tsx
767
- import { CheckmarkCircle20Filled } from "@fluentui/react-icons";
1038
+ // src/Chat.TodoList.tsx
1039
+ import { useState as useState4 } from "react";
1040
+ import {
1041
+ Body1 as Body14,
1042
+ Caption1,
1043
+ makeStyles as makeStyles6,
1044
+ Spinner as Spinner2,
1045
+ tokens as tokens6
1046
+ } from "@fluentui/react-components";
1047
+ import {
1048
+ CheckmarkCircle16Filled,
1049
+ ChevronRight16Regular as ChevronRight16Regular2,
1050
+ Circle16Regular
1051
+ } from "@fluentui/react-icons";
768
1052
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
769
1053
  var useStyles6 = makeStyles6({
1054
+ root: {
1055
+ boxSizing: "border-box",
1056
+ display: "flex",
1057
+ flexDirection: "column",
1058
+ margin: "12px 0",
1059
+ borderRadius: "var(--borderRadiusXLarge)",
1060
+ backgroundColor: "var(--colorNeutralBackgroundAlpha2)",
1061
+ border: `1px solid ${tokens6.colorNeutralStroke2}`,
1062
+ overflow: "hidden"
1063
+ },
1064
+ header: {
1065
+ display: "flex",
1066
+ alignItems: "center",
1067
+ gap: "8px",
1068
+ padding: "10px 14px",
1069
+ cursor: "pointer",
1070
+ background: "none",
1071
+ border: "none",
1072
+ width: "100%",
1073
+ textAlign: "left"
1074
+ },
1075
+ label: {
1076
+ color: tokens6.colorNeutralForeground2,
1077
+ flexShrink: 0
1078
+ },
1079
+ count: {
1080
+ color: tokens6.colorNeutralForeground3,
1081
+ marginLeft: "auto",
1082
+ flexShrink: 0
1083
+ },
1084
+ chevron: {
1085
+ color: tokens6.colorNeutralForeground3,
1086
+ flexShrink: 0,
1087
+ transition: "transform 0.2s ease"
1088
+ },
1089
+ chevronExpanded: {
1090
+ transform: "rotate(90deg)"
1091
+ },
1092
+ list: {
1093
+ display: "flex",
1094
+ flexDirection: "column",
1095
+ gap: "8px",
1096
+ padding: "2px 14px 14px 14px"
1097
+ },
1098
+ item: {
1099
+ display: "flex",
1100
+ alignItems: "flex-start",
1101
+ gap: "8px"
1102
+ },
1103
+ iconCell: {
1104
+ display: "flex",
1105
+ alignItems: "center",
1106
+ justifyContent: "center",
1107
+ width: "16px",
1108
+ height: "20px",
1109
+ flexShrink: 0
1110
+ },
1111
+ pendingIcon: {
1112
+ color: tokens6.colorNeutralForeground4
1113
+ },
1114
+ doneIcon: {
1115
+ color: tokens6.colorStatusSuccessForeground1
1116
+ },
1117
+ text: {
1118
+ minWidth: 0,
1119
+ wordBreak: "break-word"
1120
+ },
1121
+ pendingText: {
1122
+ color: tokens6.colorNeutralForeground3
1123
+ },
1124
+ activeText: {
1125
+ color: tokens6.colorNeutralForeground1,
1126
+ fontWeight: tokens6.fontWeightSemibold
1127
+ },
1128
+ doneText: {
1129
+ color: tokens6.colorNeutralForeground3,
1130
+ textDecorationLine: "line-through"
1131
+ }
1132
+ });
1133
+ var ChatTodoList = ({
1134
+ todos,
1135
+ className,
1136
+ style
1137
+ }) => {
1138
+ const styles = useStyles6();
1139
+ const total = todos.length;
1140
+ const completed = todos.filter((t) => t.status === "completed").length;
1141
+ const [expanded, setExpanded] = useState4(true);
1142
+ if (total === 0) return null;
1143
+ return /* @__PURE__ */ jsxs6(
1144
+ "div",
1145
+ {
1146
+ className: `${styles.root}${className ? ` ${className}` : ""}`,
1147
+ style,
1148
+ role: "group",
1149
+ "aria-label": `Task plan, ${completed} of ${total} done`,
1150
+ children: [
1151
+ /* @__PURE__ */ jsxs6(
1152
+ "button",
1153
+ {
1154
+ type: "button",
1155
+ className: styles.header,
1156
+ onClick: () => setExpanded((e) => !e),
1157
+ "aria-expanded": expanded,
1158
+ children: [
1159
+ /* @__PURE__ */ jsx6(
1160
+ ChevronRight16Regular2,
1161
+ {
1162
+ className: `${styles.chevron}${expanded ? ` ${styles.chevronExpanded}` : ""}`
1163
+ }
1164
+ ),
1165
+ /* @__PURE__ */ jsx6(Body14, { className: styles.label, children: "Tasks" }),
1166
+ /* @__PURE__ */ jsxs6(Caption1, { className: styles.count, children: [
1167
+ completed,
1168
+ "/",
1169
+ total
1170
+ ] })
1171
+ ]
1172
+ }
1173
+ ),
1174
+ expanded && /* @__PURE__ */ jsx6("div", { className: styles.list, role: "list", children: todos.map((todo, index) => {
1175
+ const text = todo.status === "in_progress" ? todo.activeForm ?? todo.content : todo.content;
1176
+ const textClass = todo.status === "completed" ? styles.doneText : todo.status === "in_progress" ? styles.activeText : styles.pendingText;
1177
+ return /* @__PURE__ */ jsxs6(
1178
+ "div",
1179
+ {
1180
+ className: styles.item,
1181
+ role: "listitem",
1182
+ children: [
1183
+ /* @__PURE__ */ jsx6("span", { className: styles.iconCell, children: todo.status === "completed" ? /* @__PURE__ */ jsx6(CheckmarkCircle16Filled, { className: styles.doneIcon }) : todo.status === "in_progress" ? /* @__PURE__ */ jsx6(Spinner2, { size: "extra-tiny" }) : /* @__PURE__ */ jsx6(Circle16Regular, { className: styles.pendingIcon }) }),
1184
+ /* @__PURE__ */ jsx6(Body14, { className: `${styles.text} ${textClass}`, children: text })
1185
+ ]
1186
+ },
1187
+ todo.id ?? `todo-${index}`
1188
+ );
1189
+ }) })
1190
+ ]
1191
+ }
1192
+ );
1193
+ };
1194
+ var Chat_TodoList_default = ChatTodoList;
1195
+
1196
+ // src/Chat.Messages.tsx
1197
+ import { CheckmarkCircle20Filled } from "@fluentui/react-icons";
1198
+ import { Fragment, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1199
+ var useStyles7 = makeStyles7({
770
1200
  messages: {
771
1201
  flex: "1",
772
1202
  overflowY: "auto",
@@ -787,43 +1217,119 @@ var useStyles6 = makeStyles6({
787
1217
  padding: "0 16px",
788
1218
  boxSizing: "border-box"
789
1219
  },
790
- messageUser: {
1220
+ // ── User message column: holds attachments + bubble + hover-only footer ──
1221
+ userRow: {
791
1222
  alignSelf: "flex-end",
1223
+ display: "flex",
1224
+ flexDirection: "column",
1225
+ alignItems: "flex-end",
792
1226
  maxWidth: "80%",
793
- backgroundColor: tokens6.colorNeutralBackground2,
794
- color: tokens6.colorNeutralForeground2,
795
- border: `1px solid ${tokens6.colorNeutralStroke2}`,
1227
+ "&:hover .coral-user-footer, &:focus-within .coral-user-footer": {
1228
+ opacity: 1
1229
+ }
1230
+ },
1231
+ userBubble: {
1232
+ backgroundColor: tokens7.colorNeutralBackground2,
1233
+ color: tokens7.colorNeutralForeground2,
1234
+ border: `1px solid ${tokens7.colorNeutralStroke2}`,
796
1235
  padding: "8px 12px",
797
- borderRadius: tokens6.borderRadiusXLarge,
1236
+ borderRadius: tokens7.borderRadiusXLarge,
798
1237
  wordBreak: "break-word",
799
1238
  boxSizing: "border-box"
800
1239
  },
1240
+ userFooter: {
1241
+ display: "flex",
1242
+ justifyContent: "flex-end",
1243
+ marginTop: "6px",
1244
+ marginRight: "-10px",
1245
+ opacity: 0,
1246
+ transition: "opacity 120ms ease-in-out"
1247
+ },
1248
+ userActions: {
1249
+ display: "flex"
1250
+ },
1251
+ // ── Inline edit mode ──
1252
+ userEditWrap: {
1253
+ display: "flex",
1254
+ flexDirection: "column",
1255
+ alignItems: "stretch",
1256
+ width: "min(640px, 80vw)",
1257
+ gap: "8px"
1258
+ },
1259
+ userEditTextarea: {
1260
+ backgroundColor: tokens7.colorNeutralBackground2,
1261
+ color: tokens7.colorNeutralForeground2,
1262
+ border: `1px solid ${tokens7.colorNeutralStroke1}`,
1263
+ padding: "8px 12px",
1264
+ borderRadius: tokens7.borderRadiusLarge,
1265
+ fontFamily: tokens7.fontFamilyBase,
1266
+ fontSize: tokens7.fontSizeBase300,
1267
+ lineHeight: tokens7.lineHeightBase300,
1268
+ resize: "none",
1269
+ outline: "none",
1270
+ width: "100%",
1271
+ boxSizing: "border-box",
1272
+ minHeight: "40px",
1273
+ "&:focus": {
1274
+ border: `1px solid ${tokens7.colorNeutralStroke3}`
1275
+ }
1276
+ },
1277
+ userEditFooter: {
1278
+ display: "flex",
1279
+ marginTop: "8px",
1280
+ // marginLeft: "12px",
1281
+ color: `${tokens7.colorNeutralForeground3}`,
1282
+ gap: "12px",
1283
+ alignItems: "center"
1284
+ },
1285
+ userEditActions: {
1286
+ display: "flex",
1287
+ justifyContent: "flex-end",
1288
+ gap: "8px"
1289
+ },
1290
+ // ── Assistant message ──
801
1291
  messageAssistant: {
802
1292
  alignSelf: "flex-start",
803
1293
  width: "100%",
804
- color: tokens6.colorNeutralForeground2,
1294
+ color: tokens7.colorNeutralForeground2,
805
1295
  wordBreak: "break-word",
806
- boxSizing: "border-box"
1296
+ boxSizing: "border-box",
1297
+ "&:hover .coral-assistant-footer, &:focus-within .coral-assistant-footer": {
1298
+ opacity: 1
1299
+ }
807
1300
  },
808
1301
  assistantFooter: {
809
1302
  display: "flex",
810
1303
  justifyContent: "space-between",
811
1304
  marginTop: "6px",
812
- marginLeft: "-10px"
1305
+ marginLeft: "-10px",
1306
+ // Hover-only by default; the latest message overrides this to stay visible.
1307
+ opacity: 0,
1308
+ transition: "opacity 120ms ease-in-out"
813
1309
  },
814
1310
  assistantActions: {
815
1311
  display: "flex"
1312
+ },
1313
+ copiedBadge: {
1314
+ display: "inline-flex",
1315
+ alignItems: "center",
1316
+ justifyContent: "center",
1317
+ width: "32px",
1318
+ height: "32px",
1319
+ color: tokens7.colorPaletteSeafoamForeground2
1320
+ },
1321
+ footerIconButton: {
1322
+ color: tokens7.colorNeutralForeground3
816
1323
  }
817
1324
  });
818
1325
  var ChatMessages = ({
819
1326
  components,
820
- onFeedback,
821
1327
  onCopy,
822
1328
  onNavigate,
823
1329
  fontSize,
824
1330
  fontFamily
825
1331
  }) => {
826
- const styles = useStyles6();
1332
+ const styles = useStyles7();
827
1333
  const ctx = useChatContext();
828
1334
  const {
829
1335
  messages,
@@ -832,35 +1338,204 @@ var ChatMessages = ({
832
1338
  isRetrying,
833
1339
  isInterrupted,
834
1340
  streamingReasoning,
835
- streamingAgentName,
1341
+ streamingStatus,
836
1342
  thinkingStatus,
837
- chatError,
838
1343
  sendMessage,
1344
+ regenerate,
1345
+ editMessage,
839
1346
  handleCopy,
840
1347
  lastUserInputRef,
841
1348
  registerMessagesContainer,
842
1349
  bottomRef
843
1350
  } = ctx;
844
1351
  const containerRef = useRef4(null);
845
- const [copiedIndex, setCopiedIndex] = useState4(null);
1352
+ const [surface, setSurface] = useState5(null);
1353
+ useLayoutEffect2(() => {
1354
+ const el = containerRef.current?.closest("[data-coral-surface]");
1355
+ setSurface(el?.getAttribute("data-coral-surface") ?? null);
1356
+ }, []);
1357
+ const isSidecar = surface === "panel-right";
1358
+ const [copiedKey, setCopiedKey] = useState5(null);
846
1359
  const copiedTimerRef = useRef4(null);
1360
+ const [editingId, setEditingId] = useState5(null);
1361
+ const [editDraft, setEditDraft] = useState5("");
1362
+ const editTextareaRef = useRef4(null);
847
1363
  useEffect4(() => {
848
1364
  registerMessagesContainer(containerRef.current);
849
1365
  return () => registerMessagesContainer(null);
850
1366
  }, []);
851
- const onCopyMerged = (content, index) => {
1367
+ useLayoutEffect2(() => {
1368
+ if (editingId && editTextareaRef.current) {
1369
+ const ta = editTextareaRef.current;
1370
+ ta.focus();
1371
+ ta.setSelectionRange(ta.value.length, ta.value.length);
1372
+ ta.style.height = "auto";
1373
+ ta.style.height = `${ta.scrollHeight}px`;
1374
+ }
1375
+ }, [editingId]);
1376
+ const startEdit = (msg) => {
1377
+ if (!msg.id) return;
1378
+ setEditingId(msg.id);
1379
+ setEditDraft(msg.content);
1380
+ };
1381
+ const cancelEdit = () => {
1382
+ setEditingId(null);
1383
+ setEditDraft("");
1384
+ };
1385
+ const saveEdit = async () => {
1386
+ if (!editingId) return;
1387
+ const trimmed = editDraft.trim();
1388
+ if (!trimmed) return;
1389
+ const id = editingId;
1390
+ setEditingId(null);
1391
+ setEditDraft("");
1392
+ await editMessage(id, trimmed);
1393
+ };
1394
+ const handleEditTextareaInput = (e) => {
1395
+ setEditDraft(e.target.value);
1396
+ const ta = e.target;
1397
+ ta.style.height = "auto";
1398
+ ta.style.height = `${ta.scrollHeight}px`;
1399
+ };
1400
+ const handleEditKeyDown = (e) => {
1401
+ if (e.key === "Escape") {
1402
+ e.preventDefault();
1403
+ cancelEdit();
1404
+ } else if (e.key === "Enter" && !e.shiftKey) {
1405
+ e.preventDefault();
1406
+ void saveEdit();
1407
+ }
1408
+ };
1409
+ const onCopyMerged = (content, key) => {
852
1410
  handleCopy(content);
853
1411
  onCopy?.(content);
854
1412
  if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
855
- setCopiedIndex(index);
856
- copiedTimerRef.current = setTimeout(() => setCopiedIndex(null), 1500);
1413
+ setCopiedKey(key);
1414
+ copiedTimerRef.current = setTimeout(() => setCopiedKey(null), 1500);
1415
+ };
1416
+ const renderUserMessage = (msg, index) => {
1417
+ const key = msg.id ?? `idx-${index}`;
1418
+ const isEditing = !!msg.id && editingId === msg.id;
1419
+ const attachments = Array.isArray(msg.metadata?.attachments) ? msg.metadata.attachments : null;
1420
+ return /* @__PURE__ */ jsxs7("div", { className: styles.userRow, children: [
1421
+ attachments && /* @__PURE__ */ jsx7(
1422
+ "div",
1423
+ {
1424
+ style: {
1425
+ display: "flex",
1426
+ gap: "6px",
1427
+ flexWrap: "wrap",
1428
+ marginBottom: "6px"
1429
+ },
1430
+ children: attachments.map(
1431
+ (a, i) => a.type.startsWith("image/") && a.preview ? /* @__PURE__ */ jsx7(
1432
+ "img",
1433
+ {
1434
+ src: a.preview,
1435
+ alt: a.name,
1436
+ style: {
1437
+ width: 64,
1438
+ height: 64,
1439
+ borderRadius: tokens7.borderRadiusMedium,
1440
+ objectFit: "cover"
1441
+ }
1442
+ },
1443
+ i
1444
+ ) : /* @__PURE__ */ jsxs7(
1445
+ "span",
1446
+ {
1447
+ style: {
1448
+ fontSize: 12,
1449
+ padding: "2px 8px",
1450
+ borderRadius: tokens7.borderRadiusSmall,
1451
+ backgroundColor: tokens7.colorNeutralBackground3,
1452
+ color: tokens7.colorNeutralForeground2
1453
+ },
1454
+ children: [
1455
+ "\u{1F4C4} ",
1456
+ a.name
1457
+ ]
1458
+ },
1459
+ i
1460
+ )
1461
+ )
1462
+ }
1463
+ ),
1464
+ isEditing ? /* @__PURE__ */ jsxs7("div", { className: styles.userEditWrap, children: [
1465
+ /* @__PURE__ */ jsx7(
1466
+ "textarea",
1467
+ {
1468
+ ref: editTextareaRef,
1469
+ className: styles.userEditTextarea,
1470
+ value: editDraft,
1471
+ onChange: handleEditTextareaInput,
1472
+ onKeyDown: handleEditKeyDown,
1473
+ rows: 1
1474
+ }
1475
+ ),
1476
+ /* @__PURE__ */ jsxs7("div", { className: styles.userEditFooter, children: [
1477
+ /* @__PURE__ */ jsx7(
1478
+ Info,
1479
+ {
1480
+ style: {
1481
+ marginRight: "6px",
1482
+ color: `${tokens7.colorNeutralForeground3}`,
1483
+ fontSize: "20px",
1484
+ boxSizing: "border-box",
1485
+ flexShrink: 0
1486
+ }
1487
+ }
1488
+ ),
1489
+ /* @__PURE__ */ jsx7(Caption12, { children: "Editing this message will discard the current response and regenerate from here." }),
1490
+ /* @__PURE__ */ jsxs7("div", { className: styles.userEditActions, children: [
1491
+ /* @__PURE__ */ jsx7(Button4, { appearance: "subtle", onClick: cancelEdit, children: "Cancel" }),
1492
+ /* @__PURE__ */ jsx7(
1493
+ Button4,
1494
+ {
1495
+ appearance: "primary",
1496
+ onClick: saveEdit,
1497
+ disabled: !editDraft.trim(),
1498
+ children: "Save"
1499
+ }
1500
+ )
1501
+ ] })
1502
+ ] })
1503
+ ] }) : /* @__PURE__ */ jsxs7(Fragment, { children: [
1504
+ /* @__PURE__ */ jsx7("div", { className: styles.userBubble, children: /* @__PURE__ */ jsx7(Body15, { children: msg.content.split("\n").map((line, i, arr) => /* @__PURE__ */ jsxs7(React5.Fragment, { children: [
1505
+ line,
1506
+ i < arr.length - 1 && /* @__PURE__ */ jsx7("br", {})
1507
+ ] }, i)) }) }),
1508
+ msg.id && /* @__PURE__ */ jsx7("div", { className: `${styles.userFooter} coral-user-footer`, children: /* @__PURE__ */ jsxs7("div", { className: styles.userActions, children: [
1509
+ copiedKey === `user-${key}` ? /* @__PURE__ */ jsx7("span", { className: styles.copiedBadge, children: /* @__PURE__ */ jsx7(CheckmarkCircle20Filled, {}) }) : /* @__PURE__ */ jsx7(
1510
+ Button4,
1511
+ {
1512
+ onClick: () => onCopyMerged(msg.content, `user-${key}`),
1513
+ title: "Copy message",
1514
+ appearance: "subtle",
1515
+ className: styles.footerIconButton,
1516
+ icon: /* @__PURE__ */ jsx7(Copy, {})
1517
+ }
1518
+ ),
1519
+ /* @__PURE__ */ jsx7(
1520
+ Button4,
1521
+ {
1522
+ onClick: () => startEdit(msg),
1523
+ title: "Edit message",
1524
+ appearance: "subtle",
1525
+ className: styles.footerIconButton,
1526
+ icon: /* @__PURE__ */ jsx7(EditIcon, {})
1527
+ }
1528
+ )
1529
+ ] }) })
1530
+ ] })
1531
+ ] }, key);
857
1532
  };
858
1533
  const renderAssistantMessage = (msg, index) => {
859
1534
  const isLatest = index === messages.length - 1;
860
1535
  const reasoning = isLatest && isTyping ? streamingReasoning : msg.metadata?.reasoning;
861
1536
  if (msg.type && components?.[msg.type]) {
862
1537
  const CustomRenderer = components[msg.type];
863
- return /* @__PURE__ */ jsx6(
1538
+ return /* @__PURE__ */ jsx7(
864
1539
  CustomRenderer,
865
1540
  {
866
1541
  message: msg,
@@ -875,131 +1550,197 @@ var ChatMessages = ({
875
1550
  }
876
1551
  );
877
1552
  }
878
- return /* @__PURE__ */ jsxs6("div", { children: [
879
- reasoning && /* @__PURE__ */ jsx6(
1553
+ const key = msg.id ?? `idx-${index}`;
1554
+ return /* @__PURE__ */ jsxs7("div", { children: [
1555
+ reasoning && /* @__PURE__ */ jsx7(
880
1556
  Chat_ReasoningBlock_default,
881
1557
  {
882
1558
  content: reasoning,
883
1559
  isStreaming: isLatest && isTyping && !msg.content
884
1560
  }
885
1561
  ),
886
- /* @__PURE__ */ jsx6(CoralMarkdown, { onNavigate, fontSize, fontFamily, children: msg.content }),
887
- isInterrupted && !isTyping && index === messages.length - 1 && /* @__PURE__ */ jsx6(
1562
+ msg.todos && msg.todos.length > 0 && /* @__PURE__ */ jsx7(Chat_TodoList_default, { todos: msg.todos }),
1563
+ /* @__PURE__ */ jsx7(
1564
+ CoralMarkdown,
1565
+ {
1566
+ onNavigate,
1567
+ fontSize,
1568
+ fontFamily,
1569
+ children: msg.content
1570
+ }
1571
+ ),
1572
+ msg.error && /* @__PURE__ */ jsx7(
1573
+ Chat_Error_default,
1574
+ {
1575
+ error: msg.error,
1576
+ onRetry: () => {
1577
+ const userMsg = messages[index - 1];
1578
+ if (userMsg?.role === "user") sendMessage(userMsg.content);
1579
+ }
1580
+ }
1581
+ ),
1582
+ isInterrupted && !isTyping && isLatest && /* @__PURE__ */ jsx7(
888
1583
  Chat_Callout_default,
889
1584
  {
890
1585
  message: "Response was interrupted",
891
1586
  onRetry: () => sendMessage(lastUserInputRef.current)
892
1587
  }
893
1588
  ),
894
- !(isTyping && isLatest) && /* @__PURE__ */ jsx6("div", { className: `${styles.assistantFooter} coral-assistant-footer`, children: /* @__PURE__ */ jsxs6("div", { className: styles.assistantActions, children: [
895
- copiedIndex === index ? /* @__PURE__ */ jsx6(
896
- "span",
897
- {
898
- style: {
899
- display: "inline-flex",
900
- alignItems: "center",
901
- justifyContent: "center",
902
- width: "32px",
903
- height: "32px",
904
- color: tokens6.colorPaletteSeafoamForeground2
905
- },
906
- children: /* @__PURE__ */ jsx6(CheckmarkCircle20Filled, {})
1589
+ !(isTyping && isLatest) && msg.content.trim() && /* @__PURE__ */ jsx7(
1590
+ "div",
1591
+ {
1592
+ className: `${styles.assistantFooter} coral-assistant-footer`,
1593
+ style: isLatest ? { opacity: 1 } : void 0,
1594
+ children: /* @__PURE__ */ jsxs7("div", { className: styles.assistantActions, children: [
1595
+ copiedKey === `assistant-${key}` ? /* @__PURE__ */ jsx7("span", { className: styles.copiedBadge, children: /* @__PURE__ */ jsx7(CheckmarkCircle20Filled, {}) }) : /* @__PURE__ */ jsx7(
1596
+ Button4,
1597
+ {
1598
+ onClick: () => onCopyMerged(msg.content, `assistant-${key}`),
1599
+ title: "Copy response",
1600
+ appearance: "subtle",
1601
+ className: styles.footerIconButton,
1602
+ icon: /* @__PURE__ */ jsx7(Copy, {})
1603
+ }
1604
+ ),
1605
+ isLatest && /* @__PURE__ */ jsx7(
1606
+ Button4,
1607
+ {
1608
+ onClick: () => regenerate(msg.id),
1609
+ title: "Regenerate response",
1610
+ appearance: "subtle",
1611
+ className: styles.footerIconButton,
1612
+ icon: /* @__PURE__ */ jsx7(ArrowClockwise, {})
1613
+ }
1614
+ )
1615
+ ] })
1616
+ }
1617
+ )
1618
+ ] });
1619
+ };
1620
+ return /* @__PURE__ */ jsx7("div", { className: styles.messages, ref: containerRef, children: /* @__PURE__ */ jsxs7(
1621
+ "div",
1622
+ {
1623
+ className: styles.messageWrapper,
1624
+ style: isSidecar ? { padding: "0 8px" } : void 0,
1625
+ children: [
1626
+ messages.map((msg, index) => {
1627
+ if (msg.role === "user") {
1628
+ return renderUserMessage(msg, index);
907
1629
  }
908
- ) : /* @__PURE__ */ jsx6(
909
- Button4,
1630
+ const key = msg.id ?? `idx-${index}`;
1631
+ const isLatest = index === messages.length - 1;
1632
+ return /* @__PURE__ */ jsx7(
1633
+ "div",
1634
+ {
1635
+ className: `${styles.messageAssistant} coral-message-assistant${isLatest ? " is-latest" : ""}`,
1636
+ children: renderAssistantMessage(msg, index)
1637
+ },
1638
+ key
1639
+ );
1640
+ }),
1641
+ isTyping && /* @__PURE__ */ jsx7(
1642
+ Chat_ThinkingIndicator_default,
910
1643
  {
911
- onClick: () => onCopyMerged(msg.content, index),
912
- title: "Copy Response",
913
- appearance: "subtle",
914
- style: { color: tokens6.colorNeutralForeground3 },
915
- icon: /* @__PURE__ */ jsx6(Copy, {})
1644
+ isRetrying,
1645
+ status: thinkingStatus,
1646
+ messages: resolveTaskMessages(streamingStatus)
916
1647
  }
917
1648
  ),
918
- /* @__PURE__ */ jsx6(
919
- Button4,
920
- {
921
- onClick: () => onFeedback?.(index, "up"),
922
- title: "Like",
923
- appearance: "subtle",
924
- style: { color: tokens6.colorNeutralForeground3 },
925
- icon: /* @__PURE__ */ jsx6(Heart, {})
926
- }
927
- )
928
- ] }) })
929
- ] });
930
- };
931
- return /* @__PURE__ */ jsx6("div", { className: styles.messages, ref: containerRef, children: /* @__PURE__ */ jsxs6("div", { className: styles.messageWrapper, children: [
932
- messages.map((msg, index) => /* @__PURE__ */ jsxs6(
933
- "div",
934
- {
935
- className: msg.role === "user" ? styles.messageUser : `${styles.messageAssistant} coral-message-assistant${index === messages.length - 1 ? " is-latest" : ""}`,
936
- children: [
937
- msg.role === "user" && Array.isArray(msg.metadata?.attachments) && /* @__PURE__ */ jsx6("div", { style: { display: "flex", gap: "6px", flexWrap: "wrap", marginBottom: "6px" }, children: msg.metadata.attachments.map(
938
- (a, i) => a.type.startsWith("image/") && a.preview ? /* @__PURE__ */ jsx6(
939
- "img",
940
- {
941
- src: a.preview,
942
- alt: a.name,
943
- style: { width: 64, height: 64, borderRadius: "var(--borderRadiusMedium)", objectFit: "cover" }
944
- },
945
- i
946
- ) : /* @__PURE__ */ jsxs6(
947
- "span",
948
- {
949
- style: {
950
- fontSize: 12,
951
- padding: "2px 8px",
952
- borderRadius: "var(--borderRadiusSmall)",
953
- backgroundColor: tokens6.colorNeutralBackground3,
954
- color: tokens6.colorNeutralForeground2
955
- },
956
- children: [
957
- "\u{1F4C4} ",
958
- a.name
959
- ]
960
- },
961
- i
962
- )
963
- ) }),
964
- /* @__PURE__ */ jsx6(Body14, { children: msg.role === "user" ? msg.content.split("\n").map((line, i, arr) => /* @__PURE__ */ jsxs6(React4.Fragment, { children: [
965
- line,
966
- i < arr.length - 1 && /* @__PURE__ */ jsx6("br", {})
967
- ] }, i)) : renderAssistantMessage(msg, index) })
968
- ]
969
- },
970
- msg.id ?? index
971
- )),
972
- chatError !== null && !isTyping && /* @__PURE__ */ jsx6(
973
- Chat_Error_default,
974
- {
975
- error: chatError,
976
- onRetry: () => sendMessage(lastUserInputRef.current)
977
- }
978
- ),
979
- isTyping && /* @__PURE__ */ jsx6(
980
- Chat_ThinkingIndicator_default,
981
- {
982
- isRetrying,
983
- status: streamingAgentName ? `${streamingAgentName}...` : thinkingStatus
984
- }
985
- ),
986
- /* @__PURE__ */ jsx6("div", { ref: bottomRef })
987
- ] }) });
1649
+ /* @__PURE__ */ jsx7("div", { ref: bottomRef })
1650
+ ]
1651
+ }
1652
+ ) });
988
1653
  };
989
1654
  var Chat_Messages_default = ChatMessages;
990
1655
 
991
1656
  // src/Chat.Input.tsx
992
1657
  import {
993
- useRef as useRef5,
994
- useState as useState5,
1658
+ useRef as useRef6,
1659
+ useState as useState6,
995
1660
  useEffect as useEffect5,
996
1661
  forwardRef,
997
1662
  useImperativeHandle
998
1663
  } from "react";
999
- import { Body1 as Body15, Button as Button5, Tag, makeStyles as makeStyles7, tokens as tokens7 } from "@fluentui/react-components";
1664
+ import { Body1 as Body16, Button as Button6, Tag, makeStyles as makeStyles8, tokens as tokens8 } from "@fluentui/react-components";
1000
1665
  import { Dismiss, Send, Stop } from "@coral-ai/bundleicons";
1001
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1002
- var useStyles7 = makeStyles7({
1666
+
1667
+ // src/Chat.AttachButton.tsx
1668
+ import { useRef as useRef5 } from "react";
1669
+ import {
1670
+ Button as Button5,
1671
+ Menu,
1672
+ MenuTrigger,
1673
+ MenuPopover,
1674
+ MenuList,
1675
+ MenuItem
1676
+ } from "@fluentui/react-components";
1677
+ import { Add, DocumentAdd, ImageAdd } from "@coral-ai/bundleicons";
1678
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1679
+ var ChatAttachButton = ({
1680
+ types = ["files", "photos"]
1681
+ }) => {
1682
+ const { addAttachments } = useChatContext();
1683
+ const fileRef = useRef5(null);
1684
+ const photoRef = useRef5(null);
1685
+ const showFiles = types.includes("files");
1686
+ const showPhotos = types.includes("photos");
1687
+ return /* @__PURE__ */ jsxs8(Fragment2, { children: [
1688
+ showFiles && /* @__PURE__ */ jsx8(
1689
+ "input",
1690
+ {
1691
+ ref: fileRef,
1692
+ type: "file",
1693
+ hidden: true,
1694
+ multiple: true,
1695
+ onChange: (e) => {
1696
+ if (e.target.files) addAttachments([...e.target.files]);
1697
+ e.target.value = "";
1698
+ }
1699
+ }
1700
+ ),
1701
+ showPhotos && /* @__PURE__ */ jsx8(
1702
+ "input",
1703
+ {
1704
+ ref: photoRef,
1705
+ type: "file",
1706
+ hidden: true,
1707
+ multiple: true,
1708
+ accept: "image/*",
1709
+ onChange: (e) => {
1710
+ if (e.target.files) addAttachments([...e.target.files]);
1711
+ e.target.value = "";
1712
+ }
1713
+ }
1714
+ ),
1715
+ /* @__PURE__ */ jsxs8(Menu, { children: [
1716
+ /* @__PURE__ */ jsx8(MenuTrigger, { children: /* @__PURE__ */ jsx8(Button5, { appearance: "subtle", icon: /* @__PURE__ */ jsx8(Add, {}) }) }),
1717
+ /* @__PURE__ */ jsx8(MenuPopover, { children: /* @__PURE__ */ jsx8(MenuList, { children: types.map(
1718
+ (t) => t === "files" ? /* @__PURE__ */ jsx8(
1719
+ MenuItem,
1720
+ {
1721
+ icon: /* @__PURE__ */ jsx8(DocumentAdd, {}),
1722
+ onClick: () => fileRef.current?.click(),
1723
+ children: "Add files"
1724
+ },
1725
+ "files"
1726
+ ) : /* @__PURE__ */ jsx8(
1727
+ MenuItem,
1728
+ {
1729
+ icon: /* @__PURE__ */ jsx8(ImageAdd, {}),
1730
+ onClick: () => photoRef.current?.click(),
1731
+ children: "Add photos"
1732
+ },
1733
+ "photos"
1734
+ )
1735
+ ) }) })
1736
+ ] })
1737
+ ] });
1738
+ };
1739
+ var Chat_AttachButton_default = ChatAttachButton;
1740
+
1741
+ // src/Chat.Input.tsx
1742
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1743
+ var useStyles8 = makeStyles8({
1003
1744
  wrapper: {
1004
1745
  width: "100%",
1005
1746
  margin: "0 auto"
@@ -1019,7 +1760,7 @@ var useStyles7 = makeStyles7({
1019
1760
  overflow: "hidden"
1020
1761
  },
1021
1762
  cardFocused: {
1022
- boxShadow: tokens7.shadow28
1763
+ boxShadow: tokens8.shadow28
1023
1764
  },
1024
1765
  cardDisabled: {
1025
1766
  opacity: "0.3",
@@ -1038,7 +1779,7 @@ var useStyles7 = makeStyles7({
1038
1779
  height: "128px",
1039
1780
  borderRadius: "var(--borderRadiusMedium)",
1040
1781
  overflow: "hidden",
1041
- border: `1px solid ${tokens7.colorNeutralStroke2}`,
1782
+ border: `1px solid ${tokens8.colorNeutralStroke2}`,
1042
1783
  backgroundColor: "var(--colorNeutralBackground3)",
1043
1784
  display: "flex",
1044
1785
  flexDirection: "column"
@@ -1124,18 +1865,20 @@ var ChatInput = forwardRef(
1124
1865
  actionsStart,
1125
1866
  actionsEnd,
1126
1867
  attachments: attachmentsProp,
1127
- removeAttachment: removeAttachmentProp
1868
+ removeAttachment: removeAttachmentProp,
1869
+ allowedAttachments: allowedAttachmentsProp
1128
1870
  }, ref) => {
1129
- const styles = useStyles7();
1130
- const [isFocused, setIsFocused] = useState5(false);
1131
- const textareaRef = useRef5(null);
1132
- const containerRef = useRef5(null);
1871
+ const styles = useStyles8();
1872
+ const [isFocused, setIsFocused] = useState6(false);
1873
+ const textareaRef = useRef6(null);
1874
+ const containerRef = useRef6(null);
1133
1875
  useImperativeHandle(ref, () => textareaRef.current);
1134
1876
  const ctx = useChatContext();
1135
1877
  const isInsideCanvas = ctx.setInput !== void 0;
1136
1878
  const attachments = isInsideCanvas ? ctx.attachments : attachmentsProp ?? [];
1137
1879
  const removeAttachment = isInsideCanvas ? ctx.removeAttachment : removeAttachmentProp ?? (() => {
1138
1880
  });
1881
+ const allowedAttachments = allowedAttachmentsProp ?? (isInsideCanvas ? ctx.allowedAttachments : void 0);
1139
1882
  const value = valueProp ?? (isInsideCanvas ? ctx.input : "");
1140
1883
  const onChange = onChangeProp ?? (isInsideCanvas ? ctx.setInput : () => {
1141
1884
  });
@@ -1170,53 +1913,53 @@ var ChatInput = forwardRef(
1170
1913
  observer.observe(el);
1171
1914
  return () => observer.disconnect();
1172
1915
  }, []);
1173
- const sendStopButton = isInsideCanvas ? ctx.isTyping ? /* @__PURE__ */ jsx7(
1174
- Button5,
1916
+ const sendStopButton = isInsideCanvas ? ctx.isTyping ? /* @__PURE__ */ jsx9(
1917
+ Button6,
1175
1918
  {
1176
1919
  appearance: "transparent",
1177
1920
  onClick: () => {
1178
1921
  ctx.shouldStopRef.current = true;
1179
1922
  ctx.onStop?.();
1180
1923
  },
1181
- icon: /* @__PURE__ */ jsx7(Stop, {}),
1924
+ icon: /* @__PURE__ */ jsx9(Stop, {}),
1182
1925
  style: { cursor: "default" }
1183
1926
  }
1184
- ) : /* @__PURE__ */ jsx7(
1185
- Button5,
1927
+ ) : /* @__PURE__ */ jsx9(
1928
+ Button6,
1186
1929
  {
1187
1930
  appearance: "transparent",
1188
1931
  onClick: () => ctx.sendMessage(),
1189
- icon: /* @__PURE__ */ jsx7(Send, {}),
1932
+ icon: /* @__PURE__ */ jsx9(Send, {}),
1190
1933
  disabled: !value.trim(),
1191
1934
  style: { cursor: "default" }
1192
1935
  }
1193
1936
  ) : null;
1194
- return /* @__PURE__ */ jsx7("div", { ref: containerRef, className: styles.wrapper, style, children: /* @__PURE__ */ jsxs7(
1937
+ return /* @__PURE__ */ jsx9("div", { ref: containerRef, className: styles.wrapper, style, children: /* @__PURE__ */ jsxs9(
1195
1938
  "div",
1196
1939
  {
1197
1940
  className: `${styles.card}${isFocused ? ` ${styles.cardFocused}` : ""}${disabled ? ` ${styles.cardDisabled}` : ""}`,
1198
1941
  style: {
1199
- border: isFocused ? `1px solid ${tokens7.colorNeutralStroke1}` : `1px solid ${tokens7.colorNeutralStroke2}`
1942
+ border: isFocused ? `1px solid ${tokens8.colorNeutralStroke1}` : `1px solid ${tokens8.colorNeutralStroke2}`
1200
1943
  },
1201
1944
  children: [
1202
- attachments.length > 0 && /* @__PURE__ */ jsx7("div", { className: styles.attachmentsRow, children: attachments.map((a) => {
1945
+ attachments.length > 0 && /* @__PURE__ */ jsx9("div", { className: styles.attachmentsRow, children: attachments.map((a) => {
1203
1946
  const ext = a.file.name.split(".").pop()?.toUpperCase() ?? "FILE";
1204
- return /* @__PURE__ */ jsxs7("div", { className: styles.attachmentItem, children: [
1205
- a.preview ? /* @__PURE__ */ jsx7(
1947
+ return /* @__PURE__ */ jsxs9("div", { className: styles.attachmentItem, children: [
1948
+ a.preview ? /* @__PURE__ */ jsx9(
1206
1949
  "img",
1207
1950
  {
1208
1951
  src: a.preview,
1209
1952
  alt: a.file.name,
1210
1953
  className: styles.attachmentImage
1211
1954
  }
1212
- ) : /* @__PURE__ */ jsxs7("div", { className: styles.attachmentFile, children: [
1213
- /* @__PURE__ */ jsx7(Body15, { className: styles.attachmentFileName, children: a.file.name }),
1214
- /* @__PURE__ */ jsx7(Tag, { appearance: "outline", size: "small", children: ext })
1955
+ ) : /* @__PURE__ */ jsxs9("div", { className: styles.attachmentFile, children: [
1956
+ /* @__PURE__ */ jsx9(Body16, { className: styles.attachmentFileName, children: a.file.name }),
1957
+ /* @__PURE__ */ jsx9(Tag, { appearance: "outline", size: "small", children: ext })
1215
1958
  ] }),
1216
- /* @__PURE__ */ jsx7(
1217
- Button5,
1959
+ /* @__PURE__ */ jsx9(
1960
+ Button6,
1218
1961
  {
1219
- icon: /* @__PURE__ */ jsx7(Dismiss, {}),
1962
+ icon: /* @__PURE__ */ jsx9(Dismiss, {}),
1220
1963
  size: "small",
1221
1964
  onClick: () => removeAttachment(a.id),
1222
1965
  className: styles.attachmentDismiss
@@ -1224,7 +1967,7 @@ var ChatInput = forwardRef(
1224
1967
  )
1225
1968
  ] }, a.id);
1226
1969
  }) }),
1227
- /* @__PURE__ */ jsx7(
1970
+ /* @__PURE__ */ jsx9(
1228
1971
  "textarea",
1229
1972
  {
1230
1973
  disabled,
@@ -1244,14 +1987,17 @@ var ChatInput = forwardRef(
1244
1987
  className: styles.textarea
1245
1988
  }
1246
1989
  ),
1247
- /* @__PURE__ */ jsxs7("div", { className: styles.toolbar, children: [
1248
- /* @__PURE__ */ jsx7("div", { className: styles.toolbarSide, children: actionsStart }),
1249
- /* @__PURE__ */ jsxs7("div", { className: styles.toolbarSide, children: [
1990
+ /* @__PURE__ */ jsxs9("div", { className: styles.toolbar, children: [
1991
+ /* @__PURE__ */ jsxs9("div", { className: styles.toolbarSide, children: [
1992
+ allowedAttachments && allowedAttachments.length > 0 && /* @__PURE__ */ jsx9(Chat_AttachButton_default, { types: allowedAttachments }),
1993
+ actionsStart
1994
+ ] }),
1995
+ /* @__PURE__ */ jsxs9("div", { className: styles.toolbarSide, children: [
1250
1996
  actionsEnd,
1251
1997
  sendStopButton
1252
1998
  ] })
1253
1999
  ] }),
1254
- /* @__PURE__ */ jsx7(
2000
+ /* @__PURE__ */ jsx9(
1255
2001
  "span",
1256
2002
  {
1257
2003
  className: `${styles.focusBar} ${isFocused ? styles.focusBarVisible : styles.focusBarHidden}`
@@ -1264,9 +2010,112 @@ var ChatInput = forwardRef(
1264
2010
  );
1265
2011
  var Chat_Input_default = ChatInput;
1266
2012
 
2013
+ // src/Chat.Approval.tsx
2014
+ import { Body1 as Body17, Body1Strong, Button as Button7, Caption1 as Caption13, makeStyles as makeStyles9, tokens as tokens9 } from "@fluentui/react-components";
2015
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
2016
+ var useStyles9 = makeStyles9({
2017
+ root: {
2018
+ width: "100%",
2019
+ boxSizing: "border-box",
2020
+ display: "flex",
2021
+ flexDirection: "column",
2022
+ gap: "24px",
2023
+ padding: "12px 14px",
2024
+ marginBottom: "8px",
2025
+ borderRadius: "var(--borderRadiusXLarge)",
2026
+ backgroundColor: "var(--colorNeutralBackgroundAlpha2)",
2027
+ border: `1px solid ${tokens9.colorNeutralStroke2}`
2028
+ },
2029
+ header: {
2030
+ display: "flex",
2031
+ flexDirection: "column",
2032
+ gap: "2px"
2033
+ },
2034
+ description: {
2035
+ color: tokens9.colorNeutralForeground3
2036
+ },
2037
+ optionsRow: {
2038
+ display: "flex",
2039
+ flexWrap: "wrap",
2040
+ gap: "6px"
2041
+ },
2042
+ optionsColumn: {
2043
+ display: "flex",
2044
+ flexDirection: "column",
2045
+ gap: "6px"
2046
+ },
2047
+ optionStacked: {
2048
+ justifyContent: "flex-start"
2049
+ },
2050
+ hint: {
2051
+ color: tokens9.colorNeutralForeground3
2052
+ // fontStyle: "italic",
2053
+ }
2054
+ });
2055
+ function shouldStackVertically(options) {
2056
+ if (options.length > 3) return true;
2057
+ const totalLength = options.reduce((sum, o) => sum + o.label.length, 0);
2058
+ if (totalLength > 40) return true;
2059
+ if (options.some((o) => !!o.description)) return true;
2060
+ return false;
2061
+ }
2062
+ var ChatApproval = ({
2063
+ approval: approvalProp,
2064
+ onSelect: onSelectProp,
2065
+ disabled = false,
2066
+ className,
2067
+ style
2068
+ }) => {
2069
+ const styles = useStyles9();
2070
+ const ctx = useChatContext();
2071
+ const approval = approvalProp ?? ctx.pendingApproval;
2072
+ if (!approval) return null;
2073
+ const onSelect = onSelectProp ?? ((optionId) => ctx.resolveApproval(approval.id, optionId));
2074
+ const answeredId = approval.answer?.optionId;
2075
+ const isAnswered = answeredId !== void 0;
2076
+ const awaitingFreeform = ctx.awaitingFreeformApproval?.approvalId === approval.id ? ctx.awaitingFreeformApproval : null;
2077
+ const stack = shouldStackVertically(approval.options);
2078
+ return /* @__PURE__ */ jsxs10(
2079
+ "div",
2080
+ {
2081
+ className: `${styles.root}${className ? ` ${className}` : ""}`,
2082
+ style,
2083
+ role: "group",
2084
+ "aria-label": approval.question,
2085
+ children: [
2086
+ /* @__PURE__ */ jsxs10("div", { className: styles.header, children: [
2087
+ /* @__PURE__ */ jsx10(Body1Strong, { children: approval.question }),
2088
+ approval.description && /* @__PURE__ */ jsx10(Body17, { className: styles.description, children: approval.description })
2089
+ ] }),
2090
+ /* @__PURE__ */ jsx10("div", { className: stack ? styles.optionsColumn : styles.optionsRow, children: approval.options.map((opt) => {
2091
+ const selected = answeredId === opt.id || awaitingFreeform?.optionId === opt.id;
2092
+ const baseAppearance = opt.appearance ?? (opt.freeform ? "subtle" : "outline");
2093
+ const appearance = selected ? "primary" : baseAppearance;
2094
+ return /* @__PURE__ */ jsx10(
2095
+ Button7,
2096
+ {
2097
+ appearance,
2098
+ disabled: disabled || isAnswered,
2099
+ onClick: () => onSelect(opt.id),
2100
+ className: stack ? styles.optionStacked : void 0,
2101
+ style: {
2102
+ height: "40px"
2103
+ },
2104
+ children: opt.label
2105
+ },
2106
+ opt.id
2107
+ );
2108
+ }) }),
2109
+ awaitingFreeform && !isAnswered && /* @__PURE__ */ jsx10(Caption13, { className: styles.hint, children: "Type your reply in the message box below." })
2110
+ ]
2111
+ }
2112
+ );
2113
+ };
2114
+ var Chat_Approval_default = ChatApproval;
2115
+
1267
2116
  // src/Chat.tsx
1268
- import { Fragment, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1269
- var useStyles8 = makeStyles8({
2117
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
2118
+ var useStyles10 = makeStyles10({
1270
2119
  emptySlot: {
1271
2120
  width: "100%",
1272
2121
  maxWidth: "768px",
@@ -1295,50 +2144,52 @@ var useStyles8 = makeStyles8({
1295
2144
  maxWidth: "768px",
1296
2145
  padding: "0 16px 16px 16px",
1297
2146
  boxSizing: "border-box"
1298
- },
1299
- starterRow: {
1300
- display: "flex",
1301
- flexWrap: "wrap",
1302
- justifyContent: "center",
1303
- gap: "6px",
1304
- width: "100%",
1305
- maxWidth: "768px",
1306
- margin: "0 auto",
1307
- padding: "0 16px",
1308
- boxSizing: "border-box",
1309
- overflow: "hidden"
1310
2147
  }
1311
2148
  });
2149
+ var remapFontSizeForSurface = (size, surface) => {
2150
+ if (surface === "panel-right" && (size === void 0 || size === "default")) {
2151
+ return "small";
2152
+ }
2153
+ return size;
2154
+ };
1312
2155
  var ChatLayout = ({
1313
- emptyState,
1314
- promptStarters,
2156
+ welcomeMessage,
1315
2157
  surfaceMotion,
1316
2158
  placeholder,
1317
2159
  disabled,
1318
2160
  actionsStart,
1319
2161
  actionsEnd,
1320
2162
  components,
1321
- onFeedback,
1322
2163
  onCopy,
1323
2164
  onNavigate,
1324
2165
  fontSize,
1325
2166
  fontFamily
1326
2167
  }) => {
1327
- const styles = useStyles8();
1328
- const { messages, isTyping, sendMessage } = useChatContext();
2168
+ const welcomeIsFill = welcomeMessage !== void 0 && typeof welcomeMessage !== "string";
2169
+ const welcomeNode = typeof welcomeMessage === "string" ? /* @__PURE__ */ jsx11(Title3, { children: welcomeMessage }) : welcomeMessage;
2170
+ const styles = useStyles10();
2171
+ const surfaceProbeRef = useRef7(null);
2172
+ const [surface, setSurface] = useState7(null);
2173
+ useLayoutEffect3(() => {
2174
+ const el = surfaceProbeRef.current?.closest("[data-coral-surface]");
2175
+ setSurface(el?.getAttribute("data-coral-surface") ?? null);
2176
+ }, []);
2177
+ const effectiveFontSize = remapFontSizeForSurface(fontSize, surface);
2178
+ const { messages, isTyping, pendingApproval, awaitingFreeformApproval } = useChatContext();
1329
2179
  const empty = messages.every((m) => m.role === "system") && !isTyping;
2180
+ const approvalLocksInput = pendingApproval !== null && awaitingFreeformApproval === null;
2181
+ const inputDisabled = disabled || approvalLocksInput;
1330
2182
  const motionEnabled = surfaceMotion !== null;
1331
2183
  const curve = motionTokens.curveDecelerateMax;
1332
2184
  const spacerTransition = motionEnabled ? `flex-grow 300ms ${curve}` : "none";
1333
2185
  const gapTransition = motionEnabled ? `height 300ms ${curve}` : "none";
1334
- const starterTransition = motionEnabled ? `opacity 300ms ${curve}, max-height 300ms ${curve}, padding-bottom 300ms ${curve}` : "none";
1335
- const hasStarters = !!promptStarters && promptStarters.length > 0;
1336
- return /* @__PURE__ */ jsxs8(Fragment, { children: [
1337
- /* @__PURE__ */ jsx8(
2186
+ return /* @__PURE__ */ jsxs11(Fragment3, { children: [
2187
+ /* @__PURE__ */ jsx11(
1338
2188
  "div",
1339
2189
  {
2190
+ ref: surfaceProbeRef,
1340
2191
  style: {
1341
- flexGrow: empty ? 1 : 0,
2192
+ flexGrow: empty && !welcomeIsFill ? 1 : 0,
1342
2193
  flexShrink: 1,
1343
2194
  flexBasis: 0,
1344
2195
  minHeight: 0,
@@ -1346,79 +2197,80 @@ var ChatLayout = ({
1346
2197
  }
1347
2198
  }
1348
2199
  ),
1349
- /* @__PURE__ */ jsx8(
2200
+ /* @__PURE__ */ jsx11(
1350
2201
  "div",
1351
2202
  {
1352
2203
  className: styles.contentArea,
1353
2204
  style: {
1354
- flex: empty ? "none" : 1,
1355
- overflow: empty ? "visible" : "hidden"
2205
+ flex: empty && !welcomeIsFill ? "none" : 1,
2206
+ overflow: empty && !welcomeIsFill ? "visible" : "hidden"
1356
2207
  },
1357
- children: empty ? /* @__PURE__ */ jsx8("div", { className: styles.emptySlot, children: emptyState }) : /* @__PURE__ */ jsx8(
2208
+ children: empty ? /* @__PURE__ */ jsx11(
2209
+ "div",
2210
+ {
2211
+ className: styles.emptySlot,
2212
+ style: welcomeIsFill ? {
2213
+ flex: 1,
2214
+ justifyContent: "center",
2215
+ alignContent: "center"
2216
+ } : void 0,
2217
+ children: welcomeNode
2218
+ }
2219
+ ) : /* @__PURE__ */ jsx11(
1358
2220
  Chat_Messages_default,
1359
2221
  {
1360
2222
  components,
1361
- onFeedback,
1362
2223
  onCopy,
1363
2224
  onNavigate,
1364
- fontSize,
2225
+ fontSize: effectiveFontSize,
1365
2226
  fontFamily
1366
2227
  }
1367
2228
  )
1368
2229
  }
1369
2230
  ),
1370
- /* @__PURE__ */ jsx8("div", { style: { height: empty ? 24 : 0, transition: gapTransition } }),
1371
- /* @__PURE__ */ jsx8("div", { className: styles.inputRow, children: /* @__PURE__ */ jsx8("div", { className: styles.inputInner, children: /* @__PURE__ */ jsx8(
1372
- Chat_Input_default,
1373
- {
1374
- placeholder,
1375
- disabled,
1376
- actionsStart,
1377
- actionsEnd
1378
- }
1379
- ) }) }),
1380
- hasStarters && /* @__PURE__ */ jsx8(
1381
- "div",
1382
- {
1383
- className: styles.starterRow,
1384
- style: {
1385
- opacity: empty ? 1 : 0,
1386
- maxHeight: empty ? 160 : 0,
1387
- paddingBottom: empty ? 16 : 0,
1388
- pointerEvents: empty ? "auto" : "none",
1389
- transition: starterTransition,
1390
- gap: "8px"
1391
- },
1392
- "aria-hidden": !empty,
1393
- children: promptStarters.map((p) => /* @__PURE__ */ jsx8(
1394
- InteractionTag,
1395
- {
1396
- appearance: "outline",
1397
- onClick: () => sendMessage(p),
1398
- tabIndex: empty ? 0 : -1,
1399
- children: /* @__PURE__ */ jsx8(InteractionTagPrimary, { children: p })
1400
- },
1401
- p
1402
- ))
1403
- }
1404
- ),
1405
- /* @__PURE__ */ jsx8(
2231
+ /* @__PURE__ */ jsx11("div", { style: { height: empty ? 24 : 0, transition: gapTransition } }),
2232
+ /* @__PURE__ */ jsx11("div", { className: styles.inputRow, children: /* @__PURE__ */ jsxs11("div", { className: styles.inputInner, style: { flexDirection: "column" }, children: [
2233
+ pendingApproval && /* @__PURE__ */ jsx11(Chat_Approval_default, {}),
2234
+ /* @__PURE__ */ jsx11(
2235
+ Chat_Input_default,
2236
+ {
2237
+ placeholder: approvalLocksInput ? "Choose an option above to continue..." : placeholder,
2238
+ disabled: inputDisabled,
2239
+ actionsStart,
2240
+ actionsEnd
2241
+ }
2242
+ )
2243
+ ] }) }),
2244
+ /* @__PURE__ */ jsx11(
1406
2245
  "div",
1407
2246
  {
1408
2247
  style: {
1409
- flexGrow: empty ? 1 : 0,
2248
+ flexGrow: empty && !welcomeIsFill ? 1 : 0,
1410
2249
  flexShrink: 1,
1411
2250
  flexBasis: 0,
1412
2251
  minHeight: 0,
1413
2252
  transition: spacerTransition
1414
2253
  }
1415
2254
  }
2255
+ ),
2256
+ /* @__PURE__ */ jsx11(
2257
+ Caption14,
2258
+ {
2259
+ style: {
2260
+ display: "flex",
2261
+ width: "100%",
2262
+ justifyContent: "center",
2263
+ height: "32px",
2264
+ opacity: 0.5
2265
+ },
2266
+ children: "AI makes mistakes. Check outputs."
2267
+ }
1416
2268
  )
1417
2269
  ] });
1418
2270
  };
1419
2271
  var Chat = ({
1420
- agent,
1421
- agents,
2272
+ api,
2273
+ allowedAttachments,
1422
2274
  conversationId,
1423
2275
  initialMessages,
1424
2276
  systemPrompt,
@@ -1429,11 +2281,9 @@ var Chat = ({
1429
2281
  autoSendMessage,
1430
2282
  thinkingStatus,
1431
2283
  onStop,
1432
- emptyState,
1433
- promptStarters,
2284
+ welcomeMessage,
1434
2285
  surfaceMotion,
1435
2286
  components,
1436
- onFeedback,
1437
2287
  onCopy,
1438
2288
  onNavigate,
1439
2289
  placeholder,
@@ -1443,11 +2293,14 @@ var Chat = ({
1443
2293
  fontSize,
1444
2294
  fontFamily
1445
2295
  }) => {
1446
- return /* @__PURE__ */ jsx8(
2296
+ const chatConfig = useCoralConfig()?.chat;
2297
+ const resolvedApi = api ?? chatConfig?.api;
2298
+ const resolvedAllowedAttachments = allowedAttachments ?? chatConfig?.attachments;
2299
+ return /* @__PURE__ */ jsx11(
1447
2300
  Chat_Canvas_default,
1448
2301
  {
1449
- agent,
1450
- agents,
2302
+ api: resolvedApi,
2303
+ allowedAttachments: resolvedAllowedAttachments,
1451
2304
  conversationId,
1452
2305
  initialMessages,
1453
2306
  systemPrompt,
@@ -1458,18 +2311,16 @@ var Chat = ({
1458
2311
  autoSendMessage,
1459
2312
  thinkingStatus,
1460
2313
  onStop,
1461
- children: /* @__PURE__ */ jsx8(
2314
+ children: /* @__PURE__ */ jsx11(
1462
2315
  ChatLayout,
1463
2316
  {
1464
- emptyState,
1465
- promptStarters,
2317
+ welcomeMessage,
1466
2318
  surfaceMotion,
1467
2319
  placeholder,
1468
2320
  disabled,
1469
2321
  actionsStart,
1470
2322
  actionsEnd,
1471
2323
  components,
1472
- onFeedback,
1473
2324
  onCopy,
1474
2325
  onNavigate,
1475
2326
  fontSize,
@@ -1483,11 +2334,18 @@ var Chat_default = Chat;
1483
2334
 
1484
2335
  // src/createHandler.ts
1485
2336
  function createHandler({ endpoint, headers }) {
1486
- return async function* (input, history, context) {
2337
+ return async function* (input, history, files) {
2338
+ const attachments = await Promise.all(
2339
+ files.map(async (file) => ({
2340
+ name: file.name,
2341
+ mediaType: file.type,
2342
+ data: await fileToBase64(file)
2343
+ }))
2344
+ );
1487
2345
  const res = await fetch(endpoint, {
1488
2346
  method: "POST",
1489
2347
  headers: { "Content-Type": "application/json", ...headers },
1490
- body: JSON.stringify({ input, history, context })
2348
+ body: JSON.stringify({ input, history, attachments })
1491
2349
  });
1492
2350
  if (!res.ok || !res.body) {
1493
2351
  throw Object.assign(new Error(`${res.status} ${res.statusText}`), {
@@ -1518,20 +2376,407 @@ function createHandler({ endpoint, headers }) {
1518
2376
  };
1519
2377
  }
1520
2378
 
2379
+ // src/Chat.Agent.ts
2380
+ var UPDATE_TODOS_TOOL = {
2381
+ name: "update_todos",
2382
+ description: "Display or update the multi-step task plan shown to the user as an inline checklist. Call this when you begin a task that takes 3 or more distinct steps, and again every time a step changes status. Always send the COMPLETE list of todos \u2014 it replaces the previous one. Keep exactly one item 'in_progress' at a time.",
2383
+ parameters: {
2384
+ type: "object",
2385
+ properties: {
2386
+ todos: {
2387
+ type: "array",
2388
+ description: "The full task list, in order.",
2389
+ items: {
2390
+ type: "object",
2391
+ properties: {
2392
+ content: {
2393
+ type: "string",
2394
+ description: "Imperative title, e.g. 'Optimize images'."
2395
+ },
2396
+ activeForm: {
2397
+ type: "string",
2398
+ description: "Present-tense title shown while this step is in progress, e.g. 'Optimizing images'."
2399
+ },
2400
+ status: {
2401
+ type: "string",
2402
+ enum: ["pending", "in_progress", "completed"]
2403
+ }
2404
+ },
2405
+ required: ["content", "status"]
2406
+ }
2407
+ }
2408
+ },
2409
+ required: ["todos"]
2410
+ }
2411
+ };
2412
+ var REQUEST_APPROVAL_TOOL = {
2413
+ name: "request_approval",
2414
+ description: "Pause and ask the user to approve an action or choose between options before continuing. The conversation halts until the user responds \u2014 their choice comes back as this tool's result. Use this before any consequential or irreversible action (deleting, sending, purchasing, etc).",
2415
+ parameters: {
2416
+ type: "object",
2417
+ properties: {
2418
+ question: {
2419
+ type: "string",
2420
+ description: "The question or action to confirm."
2421
+ },
2422
+ description: {
2423
+ type: "string",
2424
+ description: "Optional extra context shown below the question."
2425
+ },
2426
+ options: {
2427
+ type: "array",
2428
+ description: "The choices offered to the user.",
2429
+ items: {
2430
+ type: "object",
2431
+ properties: {
2432
+ id: { type: "string" },
2433
+ label: { type: "string" },
2434
+ description: { type: "string" },
2435
+ freeform: {
2436
+ type: "boolean",
2437
+ description: "If true, choosing this option lets the user type a free-text reply instead of a canned choice."
2438
+ }
2439
+ },
2440
+ required: ["id", "label"]
2441
+ }
2442
+ }
2443
+ },
2444
+ required: ["question", "options"]
2445
+ }
2446
+ };
2447
+ function createMemoryStore() {
2448
+ const map = /* @__PURE__ */ new Map();
2449
+ return {
2450
+ get: (id) => map.get(id),
2451
+ set: (id, snap) => {
2452
+ map.set(id, snap);
2453
+ },
2454
+ delete: (id) => {
2455
+ map.delete(id);
2456
+ }
2457
+ };
2458
+ }
2459
+ function historyToAgentMessages(history) {
2460
+ const out = [];
2461
+ for (const m of history) {
2462
+ if (m.role === "user") out.push({ role: "user", content: m.content });
2463
+ else if (m.role === "assistant") out.push({ role: "assistant", content: m.content });
2464
+ }
2465
+ return out;
2466
+ }
2467
+ function extractTodos(input) {
2468
+ const todos = input?.todos;
2469
+ if (!Array.isArray(todos)) return [];
2470
+ return todos.map((t, i) => {
2471
+ const o = t ?? {};
2472
+ return {
2473
+ id: typeof o.id === "string" ? o.id : `todo-${i}`,
2474
+ content: String(o.content ?? ""),
2475
+ activeForm: typeof o.activeForm === "string" ? o.activeForm : void 0,
2476
+ status: o.status === "in_progress" || o.status === "completed" ? o.status : "pending"
2477
+ };
2478
+ });
2479
+ }
2480
+ function buildApproval(tc) {
2481
+ const input = tc.input ?? {};
2482
+ const rawOptions = Array.isArray(input.options) ? input.options : [];
2483
+ const options = rawOptions.map((o, i) => {
2484
+ const obj = o ?? {};
2485
+ return {
2486
+ id: typeof obj.id === "string" ? obj.id : `opt-${i}`,
2487
+ label: String(obj.label ?? `Option ${i + 1}`),
2488
+ description: typeof obj.description === "string" ? obj.description : void 0,
2489
+ freeform: obj.freeform === true ? true : void 0
2490
+ };
2491
+ });
2492
+ return {
2493
+ // The tool-call id is the approval id — Coral round-trips it as
2494
+ // metadata.approvalId, which is the session-store key on resume.
2495
+ id: tc.id,
2496
+ question: String(input.question ?? "Approve?"),
2497
+ description: typeof input.description === "string" ? input.description : void 0,
2498
+ options
2499
+ };
2500
+ }
2501
+ function createAgentHandler(options) {
2502
+ const { callModel, system, tools = [], maxSteps = 8 } = options;
2503
+ const store = options.sessionStore ?? createMemoryStore();
2504
+ const customByName = new Map(tools.map((t) => [t.spec.name, t]));
2505
+ const toolSpecs = [
2506
+ UPDATE_TODOS_TOOL,
2507
+ REQUEST_APPROVAL_TOOL,
2508
+ ...tools.map((t) => t.spec)
2509
+ ];
2510
+ return async function* (_input, history, attachments) {
2511
+ const sysFromHistory = history.filter((m) => m.role === "system").map((m) => m.content);
2512
+ const effectiveSystem = [system, ...sysFromHistory].filter(Boolean).join("\n\n") || void 0;
2513
+ const last = history[history.length - 1];
2514
+ let messages;
2515
+ let firstCallAttachments;
2516
+ const resumeId = last?.role === "user" && last.type === "approval_response" && typeof last.metadata?.approvalId === "string" ? last.metadata.approvalId : void 0;
2517
+ const snapshot = resumeId ? await store.get(resumeId) : void 0;
2518
+ if (resumeId && snapshot) {
2519
+ await store.delete(resumeId);
2520
+ const meta = last.metadata;
2521
+ const answer = typeof meta.text === "string" && meta.text || typeof meta.label === "string" && meta.label || last.content;
2522
+ messages = [
2523
+ ...snapshot.messages,
2524
+ {
2525
+ role: "tool",
2526
+ toolCallId: snapshot.pendingToolCallId,
2527
+ content: `The user chose: ${answer}`
2528
+ }
2529
+ ];
2530
+ } else {
2531
+ messages = historyToAgentMessages(history);
2532
+ firstCallAttachments = attachments.length > 0 ? attachments : void 0;
2533
+ }
2534
+ for (let step = 0; step < maxSteps; step++) {
2535
+ const events = callModel({
2536
+ system: effectiveSystem,
2537
+ messages,
2538
+ tools: toolSpecs,
2539
+ attachments: step === 0 ? firstCallAttachments : void 0
2540
+ });
2541
+ let assistantText = "";
2542
+ let assistantRaw;
2543
+ const toolCalls = [];
2544
+ for await (const ev of events) {
2545
+ if (ev.type === "text") {
2546
+ assistantText += ev.delta;
2547
+ yield ev.delta;
2548
+ } else if (ev.type === "reasoning") {
2549
+ yield { type: "reasoning", content: ev.delta };
2550
+ } else if (ev.type === "tool_call") {
2551
+ toolCalls.push(ev.toolCall);
2552
+ } else if (ev.type === "raw") {
2553
+ assistantRaw = ev.raw;
2554
+ }
2555
+ }
2556
+ messages.push({
2557
+ role: "assistant",
2558
+ content: assistantText,
2559
+ ...toolCalls.length > 0 ? { toolCalls } : {},
2560
+ ...assistantRaw !== void 0 ? { raw: assistantRaw } : {}
2561
+ });
2562
+ if (toolCalls.length === 0) return;
2563
+ for (const tc of toolCalls) {
2564
+ if (tc.name === UPDATE_TODOS_TOOL.name) {
2565
+ yield { type: "todos", todos: extractTodos(tc.input) };
2566
+ messages.push({
2567
+ role: "tool",
2568
+ toolCallId: tc.id,
2569
+ content: "Task list updated and shown to the user."
2570
+ });
2571
+ } else if (tc.name === REQUEST_APPROVAL_TOOL.name) {
2572
+ yield { type: "approval", approval: buildApproval(tc) };
2573
+ await store.set(tc.id, { messages, pendingToolCallId: tc.id });
2574
+ return;
2575
+ } else {
2576
+ const tool = customByName.get(tc.name);
2577
+ let content;
2578
+ if (!tool) {
2579
+ content = `Error: unknown tool "${tc.name}".`;
2580
+ } else {
2581
+ try {
2582
+ content = (await tool.execute(tc.input)).content;
2583
+ } catch (err) {
2584
+ content = `Error: ${err instanceof Error ? err.message : String(err)}`;
2585
+ }
2586
+ }
2587
+ messages.push({ role: "tool", toolCallId: tc.id, content });
2588
+ }
2589
+ }
2590
+ }
2591
+ yield "\n\n_(Reached the tool-step limit for this turn.)_";
2592
+ };
2593
+ }
2594
+
2595
+ // src/Chat.Threads.tsx
2596
+ import * as React9 from "react";
2597
+ import { jsx as jsx12 } from "react/jsx-runtime";
2598
+ var ChatThreadsContext = React9.createContext(
2599
+ null
2600
+ );
2601
+ function useChatThreads() {
2602
+ const value = React9.useContext(ChatThreadsContext);
2603
+ if (!value) {
2604
+ throw new Error(
2605
+ "useChatThreads must be used inside <ChatThreadsProvider>."
2606
+ );
2607
+ }
2608
+ return value;
2609
+ }
2610
+ function createMemoryThreadsAdapter(initialThreads = []) {
2611
+ let threads = [...initialThreads];
2612
+ return {
2613
+ list: () => threads,
2614
+ get: (id) => threads.find((t) => t.id === id),
2615
+ create: (initial) => {
2616
+ const thread = {
2617
+ id: initial?.id ?? crypto.randomUUID(),
2618
+ title: initial?.title ?? "New chat",
2619
+ messages: initial?.messages ?? [],
2620
+ createdAt: Date.now(),
2621
+ updatedAt: Date.now()
2622
+ };
2623
+ threads = [thread, ...threads];
2624
+ return thread;
2625
+ },
2626
+ save: (thread) => {
2627
+ const idx = threads.findIndex((t) => t.id === thread.id);
2628
+ const next = { ...thread, updatedAt: Date.now() };
2629
+ threads = idx >= 0 ? threads.map((t, i) => i === idx ? next : t) : [next, ...threads];
2630
+ },
2631
+ rename: (id, newTitle) => {
2632
+ threads = threads.map(
2633
+ (t) => t.id === id ? { ...t, title: newTitle, updatedAt: Date.now() } : t
2634
+ );
2635
+ },
2636
+ remove: (id) => {
2637
+ threads = threads.filter((t) => t.id !== id);
2638
+ }
2639
+ };
2640
+ }
2641
+ function ChatThreadsProvider({
2642
+ adapter: providedAdapter,
2643
+ activeThreadId: controlledId,
2644
+ defaultActiveThreadId,
2645
+ onActiveThreadChange,
2646
+ children
2647
+ }) {
2648
+ const adapter = React9.useMemo(
2649
+ () => providedAdapter ?? createMemoryThreadsAdapter(),
2650
+ [providedAdapter]
2651
+ );
2652
+ const [threads, setThreads] = React9.useState([]);
2653
+ const [uncontrolledId, setUncontrolledId] = React9.useState(
2654
+ defaultActiveThreadId
2655
+ );
2656
+ const [isLoading, setIsLoading] = React9.useState(true);
2657
+ const isControlled = controlledId !== void 0;
2658
+ const activeThreadId = isControlled ? controlledId : uncontrolledId;
2659
+ const setActiveThreadId = React9.useCallback(
2660
+ (id) => {
2661
+ if (!isControlled) setUncontrolledId(id);
2662
+ onActiveThreadChange?.(id);
2663
+ },
2664
+ [isControlled, onActiveThreadChange]
2665
+ );
2666
+ React9.useEffect(() => {
2667
+ let cancelled = false;
2668
+ (async () => {
2669
+ setIsLoading(true);
2670
+ const result = await adapter.list();
2671
+ if (cancelled) return;
2672
+ setThreads(result);
2673
+ setIsLoading(false);
2674
+ })();
2675
+ return () => {
2676
+ cancelled = true;
2677
+ };
2678
+ }, [adapter]);
2679
+ const createThread = React9.useCallback(
2680
+ async (initial) => {
2681
+ const thread = await adapter.create(initial);
2682
+ setThreads((prev) => [thread, ...prev.filter((t) => t.id !== thread.id)]);
2683
+ return thread;
2684
+ },
2685
+ [adapter]
2686
+ );
2687
+ const renameThread = React9.useCallback(
2688
+ async (id, newTitle) => {
2689
+ await adapter.rename(id, newTitle);
2690
+ setThreads(
2691
+ (prev) => prev.map(
2692
+ (t) => t.id === id ? { ...t, title: newTitle, updatedAt: Date.now() } : t
2693
+ )
2694
+ );
2695
+ },
2696
+ [adapter]
2697
+ );
2698
+ const removeThread = React9.useCallback(
2699
+ async (id) => {
2700
+ await adapter.remove(id);
2701
+ setThreads((prev) => prev.filter((t) => t.id !== id));
2702
+ if (activeThreadId === id) setActiveThreadId(void 0);
2703
+ },
2704
+ [adapter, activeThreadId, setActiveThreadId]
2705
+ );
2706
+ const threadsRef = React9.useRef(threads);
2707
+ threadsRef.current = threads;
2708
+ const updateThreadMessages = React9.useCallback(
2709
+ (id, messages) => {
2710
+ const existing = threadsRef.current.find((t) => t.id === id);
2711
+ if (!existing) return;
2712
+ const next = {
2713
+ ...existing,
2714
+ messages,
2715
+ updatedAt: Date.now()
2716
+ };
2717
+ setThreads((prev) => prev.map((t) => t.id === id ? next : t));
2718
+ void adapter.save(next);
2719
+ },
2720
+ [adapter]
2721
+ );
2722
+ const activeThread = React9.useMemo(
2723
+ () => threads.find((t) => t.id === activeThreadId),
2724
+ [threads, activeThreadId]
2725
+ );
2726
+ const value = React9.useMemo(
2727
+ () => ({
2728
+ threads,
2729
+ activeThreadId,
2730
+ activeThread,
2731
+ setActiveThreadId,
2732
+ createThread,
2733
+ renameThread,
2734
+ removeThread,
2735
+ updateThreadMessages,
2736
+ isLoading
2737
+ }),
2738
+ [
2739
+ threads,
2740
+ activeThreadId,
2741
+ activeThread,
2742
+ setActiveThreadId,
2743
+ createThread,
2744
+ renameThread,
2745
+ removeThread,
2746
+ updateThreadMessages,
2747
+ isLoading
2748
+ ]
2749
+ );
2750
+ return /* @__PURE__ */ jsx12(ChatThreadsContext.Provider, { value, children });
2751
+ }
2752
+
1521
2753
  // src/index.ts
1522
2754
  var Chat2 = Object.assign(Chat_default, {
1523
2755
  Canvas: Chat_Canvas_default,
1524
2756
  Messages: Chat_Messages_default,
1525
2757
  Input: Chat_Input_default,
1526
2758
  Error: Chat_Error_default,
1527
- Callout: Chat_Callout_default
2759
+ Callout: Chat_Callout_default,
2760
+ Approval: Chat_Approval_default,
2761
+ TodoList: Chat_TodoList_default,
2762
+ AttachButton: Chat_AttachButton_default,
2763
+ ThinkingIndicator: Chat_ThinkingIndicator_default
1528
2764
  });
1529
2765
  var normalizeChatError2 = normalizeChatError;
1530
2766
  var createHandler2 = createHandler;
1531
2767
  export {
1532
2768
  Chat2 as Chat,
2769
+ ChatThreadsProvider,
2770
+ DEFAULT_TASK_MESSAGES,
2771
+ REQUEST_APPROVAL_TOOL,
2772
+ UPDATE_TODOS_TOOL,
2773
+ createAgentHandler,
1533
2774
  createHandler2 as createHandler,
2775
+ createMemoryThreadsAdapter,
2776
+ fileToBase64,
1534
2777
  normalizeChatError2 as normalizeChatError,
1535
- useChatContext
2778
+ resolveTaskMessages,
2779
+ useChatContext,
2780
+ useChatThreads
1536
2781
  };
1537
2782
  //# sourceMappingURL=index.js.map