@cntyclub/agent-react 0.8.0 → 0.9.1
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/api-client.d.ts +7 -1
- package/dist/api-client.d.ts.map +1 -1
- package/dist/components/agent-composer.d.ts +7 -1
- package/dist/components/agent-composer.d.ts.map +1 -1
- package/dist/components/agent-panel.d.ts.map +1 -1
- package/dist/components/chat-scroller.d.ts +17 -0
- package/dist/components/chat-scroller.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +286 -66
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +34 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/use-agent-chat.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Button, cn, ChatMessage, ChatMessageBubble, Markdown, Spinner, Checkbox, Collapsible, CollapsibleTrigger, ChatToolChip, CollapsiblePanel, ChatToolCard, ChatToolCardHeader, ChatToolCardActions, Group, Menu, MenuTrigger, MenuPopup, MenuItem, ChatHeader, ChatHeaderAvatar, ChatHeaderTitle, ChatHeaderActions, Tooltip, TooltipTrigger, TooltipContent, Chat,
|
|
1
|
+
import { Button, cn, ChatMessage, ChatMessageBubble, Markdown, Spinner, Checkbox, Collapsible, CollapsibleTrigger, ChatToolChip, CollapsiblePanel, ChatToolCard, ChatToolCardHeader, ChatToolCardActions, Group, Menu, MenuTrigger, MenuPopup, MenuItem, ChatHeader, ChatHeaderAvatar, ChatHeaderTitle, ChatHeaderActions, Tooltip, TooltipTrigger, TooltipContent, Chat, ChatEmptyState, ChatEmptyStateIcon, ChatEmptyStateTitle, ChatEmptyStateDescription, ChatSuggestions, ChatSuggestion, useMediaQuery, TooltipProvider, Fab, DataTablePaged } from '@cntyclub/ui-react';
|
|
2
2
|
import { FileTextIcon, XIcon, PaperclipIcon, ArrowUpIcon, Maximize2Icon, ListChecksIcon, ChevronDownIcon, CheckIcon, ShieldAlertIcon, CheckCheckIcon, UploadCloudIcon, HistoryIcon, SquarePenIcon, Minimize2Icon, Trash2Icon, SparklesIcon, DownloadIcon, CodeIcon } from 'lucide-react';
|
|
3
|
-
import * as
|
|
3
|
+
import * as React7 from 'react';
|
|
4
4
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
5
5
|
|
|
6
6
|
// src/api-client.ts
|
|
@@ -75,6 +75,98 @@ var AgentApiClient = class {
|
|
|
75
75
|
method: "POST"
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
+
// ------------------------------------------------------------- streaming
|
|
79
|
+
/** POST a chat turn and yield parsed SSE events as they stream in. */
|
|
80
|
+
async streamMessage(message, conversationId, files, onEvent, signal) {
|
|
81
|
+
let body;
|
|
82
|
+
const headers = await this.streamHeaders();
|
|
83
|
+
if (files && files.length > 0) {
|
|
84
|
+
const form = new FormData();
|
|
85
|
+
form.append("client_id", this.config.clientId);
|
|
86
|
+
form.append("message", message);
|
|
87
|
+
if (conversationId) form.append("conversation_id", conversationId);
|
|
88
|
+
for (const file of files) form.append("files", file, file.name);
|
|
89
|
+
body = form;
|
|
90
|
+
} else {
|
|
91
|
+
headers["Content-Type"] = "application/json";
|
|
92
|
+
body = JSON.stringify({
|
|
93
|
+
client_id: this.config.clientId,
|
|
94
|
+
conversation_id: conversationId ?? null,
|
|
95
|
+
message
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
await this.consumeStream("/agent-mode/chat/stream/", { body, headers, signal }, onEvent);
|
|
99
|
+
}
|
|
100
|
+
/** POST an approval decision and yield the continued reply as SSE events. */
|
|
101
|
+
async streamResolveApproval(callId, approve, onEvent, signal) {
|
|
102
|
+
const headers = await this.streamHeaders();
|
|
103
|
+
headers["Content-Type"] = "application/json";
|
|
104
|
+
await this.consumeStream(
|
|
105
|
+
`/agent-mode/approvals/${callId}/resolve/stream/`,
|
|
106
|
+
{
|
|
107
|
+
body: JSON.stringify({ approve, client_id: this.config.clientId }),
|
|
108
|
+
headers,
|
|
109
|
+
signal
|
|
110
|
+
},
|
|
111
|
+
onEvent
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
async streamHeaders() {
|
|
115
|
+
const token = await this.config.getAccessToken();
|
|
116
|
+
if (!token) {
|
|
117
|
+
throw new AgentApiError("You need to be signed in to use the assistant.", 401);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
// Include application/json so DRF content-negotiation always finds a match.
|
|
121
|
+
Accept: "text/event-stream, application/json",
|
|
122
|
+
Authorization: `Bearer ${token}`,
|
|
123
|
+
"X-Agent-Client-Id": this.config.clientId
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async consumeStream(path, init, onEvent) {
|
|
127
|
+
const base = this.config.apiBaseUrl.replace(/\/$/, "");
|
|
128
|
+
const separator = path.includes("?") ? "&" : "?";
|
|
129
|
+
const url = `${base}${path}${separator}client_id=${encodeURIComponent(this.config.clientId)}`;
|
|
130
|
+
const response = await fetch(url, {
|
|
131
|
+
body: init.body,
|
|
132
|
+
headers: init.headers,
|
|
133
|
+
method: "POST",
|
|
134
|
+
signal: init.signal
|
|
135
|
+
});
|
|
136
|
+
if (!response.ok || !response.body) {
|
|
137
|
+
let detail = `Request failed (${response.status})`;
|
|
138
|
+
try {
|
|
139
|
+
const payload = await response.json();
|
|
140
|
+
detail = payload.detail || payload.message || detail;
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
throw new AgentApiError(detail, response.status || 0);
|
|
144
|
+
}
|
|
145
|
+
const reader = response.body.getReader();
|
|
146
|
+
const decoder = new TextDecoder();
|
|
147
|
+
let buffer = "";
|
|
148
|
+
const flushFrame = (frame) => {
|
|
149
|
+
const dataLines = frame.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart());
|
|
150
|
+
if (dataLines.length === 0) return;
|
|
151
|
+
const raw = dataLines.join("\n");
|
|
152
|
+
try {
|
|
153
|
+
onEvent(JSON.parse(raw));
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
for (; ; ) {
|
|
158
|
+
const { done, value } = await reader.read();
|
|
159
|
+
if (done) break;
|
|
160
|
+
buffer += decoder.decode(value, { stream: true });
|
|
161
|
+
let boundary = buffer.indexOf("\n\n");
|
|
162
|
+
while (boundary !== -1) {
|
|
163
|
+
flushFrame(buffer.slice(0, boundary));
|
|
164
|
+
buffer = buffer.slice(boundary + 2);
|
|
165
|
+
boundary = buffer.indexOf("\n\n");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (buffer.trim()) flushFrame(buffer);
|
|
169
|
+
}
|
|
78
170
|
listConversations() {
|
|
79
171
|
return this.request("/agent-mode/conversations/");
|
|
80
172
|
}
|
|
@@ -127,6 +219,7 @@ function formatBytes(bytes) {
|
|
|
127
219
|
}
|
|
128
220
|
function AgentComposer({
|
|
129
221
|
acceptAttribute: acceptAttribute2,
|
|
222
|
+
busy,
|
|
130
223
|
className,
|
|
131
224
|
disabled,
|
|
132
225
|
files,
|
|
@@ -137,14 +230,18 @@ function AgentComposer({
|
|
|
137
230
|
placeholder = "Ask anything\u2026",
|
|
138
231
|
value
|
|
139
232
|
}) {
|
|
140
|
-
const textareaRef =
|
|
141
|
-
const inputRef =
|
|
142
|
-
const canSubmit = !disabled && (value.trim().length > 0 || files.length > 0);
|
|
143
|
-
const submit =
|
|
233
|
+
const textareaRef = React7.useRef(null);
|
|
234
|
+
const inputRef = React7.useRef(null);
|
|
235
|
+
const canSubmit = !disabled && !busy && (value.trim().length > 0 || files.length > 0);
|
|
236
|
+
const submit = React7.useCallback(() => {
|
|
144
237
|
if (!canSubmit) return;
|
|
145
238
|
onSubmit(value.trim());
|
|
239
|
+
textareaRef.current?.focus();
|
|
146
240
|
}, [canSubmit, onSubmit, value]);
|
|
147
|
-
|
|
241
|
+
React7.useEffect(() => {
|
|
242
|
+
if (!disabled) textareaRef.current?.focus();
|
|
243
|
+
}, [disabled]);
|
|
244
|
+
React7.useEffect(() => {
|
|
148
245
|
const textarea = textareaRef.current;
|
|
149
246
|
if (!textarea) return;
|
|
150
247
|
textarea.style.height = "auto";
|
|
@@ -325,6 +422,32 @@ function groupMessages(messages) {
|
|
|
325
422
|
flush();
|
|
326
423
|
return items;
|
|
327
424
|
}
|
|
425
|
+
function ChatScroller({ children, className, ...props }) {
|
|
426
|
+
const viewportRef = React7.useRef(null);
|
|
427
|
+
const pinnedRef = React7.useRef(true);
|
|
428
|
+
const handleScroll = React7.useCallback(() => {
|
|
429
|
+
const viewport = viewportRef.current;
|
|
430
|
+
if (!viewport) return;
|
|
431
|
+
const distanceFromBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
|
|
432
|
+
pinnedRef.current = distanceFromBottom < 64;
|
|
433
|
+
}, []);
|
|
434
|
+
React7.useEffect(() => {
|
|
435
|
+
const viewport = viewportRef.current;
|
|
436
|
+
if (!viewport || !pinnedRef.current) return;
|
|
437
|
+
viewport.scrollTop = viewport.scrollHeight;
|
|
438
|
+
});
|
|
439
|
+
return /* @__PURE__ */ jsx(
|
|
440
|
+
"div",
|
|
441
|
+
{
|
|
442
|
+
className: cn("flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-4", className),
|
|
443
|
+
"data-slot": "chat-scroller",
|
|
444
|
+
onScroll: handleScroll,
|
|
445
|
+
ref: viewportRef,
|
|
446
|
+
...props,
|
|
447
|
+
children
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
}
|
|
328
451
|
function MessageItem({ message }) {
|
|
329
452
|
if (message.role === "user") {
|
|
330
453
|
const attachments = message.attachments ?? [];
|
|
@@ -359,8 +482,8 @@ var DEFAULT_PHRASES = [
|
|
|
359
482
|
];
|
|
360
483
|
function ThinkingIndicator({ className, intervalMs = 1800, phrases }) {
|
|
361
484
|
const words = phrases && phrases.length ? phrases : DEFAULT_PHRASES;
|
|
362
|
-
const [index, setIndex] =
|
|
363
|
-
|
|
485
|
+
const [index, setIndex] = React7.useState(0);
|
|
486
|
+
React7.useEffect(() => {
|
|
364
487
|
setIndex(0);
|
|
365
488
|
const timer = setInterval(() => {
|
|
366
489
|
setIndex((current) => (current + 1) % words.length);
|
|
@@ -389,7 +512,7 @@ function ThinkingIndicator({ className, intervalMs = 1800, phrases }) {
|
|
|
389
512
|
className: "fade-in-0 animate-in text-muted-foreground text-xs duration-300",
|
|
390
513
|
children: [
|
|
391
514
|
words[index],
|
|
392
|
-
"\u2026"
|
|
515
|
+
" \u2026"
|
|
393
516
|
]
|
|
394
517
|
},
|
|
395
518
|
index
|
|
@@ -622,7 +745,7 @@ function DownloadButton({ block, size = "sm" }) {
|
|
|
622
745
|
);
|
|
623
746
|
}
|
|
624
747
|
function DocumentViewer({ block, onClose }) {
|
|
625
|
-
|
|
748
|
+
React7.useEffect(() => {
|
|
626
749
|
const onKey = (event) => event.key === "Escape" && onClose();
|
|
627
750
|
window.addEventListener("keydown", onKey);
|
|
628
751
|
return () => window.removeEventListener("keydown", onKey);
|
|
@@ -647,7 +770,7 @@ function DocumentViewer({ block, onClose }) {
|
|
|
647
770
|
] });
|
|
648
771
|
}
|
|
649
772
|
function DocumentCard({ block }) {
|
|
650
|
-
const [open, setOpen] =
|
|
773
|
+
const [open, setOpen] = React7.useState(false);
|
|
651
774
|
const previewLines = (block.preview || "").split("\n").slice(0, 6).join("\n");
|
|
652
775
|
return /* @__PURE__ */ jsxs("div", { className: "w-full max-w-full overflow-hidden rounded-xl border border-border bg-card shadow-xs", children: [
|
|
653
776
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-border/72 border-b px-3 py-2", children: [
|
|
@@ -769,7 +892,7 @@ function StepDetail({
|
|
|
769
892
|
] });
|
|
770
893
|
}
|
|
771
894
|
function ToolActivityGroup({ describeToolCall, steps }) {
|
|
772
|
-
const [open, setOpen] =
|
|
895
|
+
const [open, setOpen] = React7.useState(false);
|
|
773
896
|
if (steps.length === 0) return null;
|
|
774
897
|
const last = steps[steps.length - 1];
|
|
775
898
|
const anyRunning = steps.some((step) => step.status === "running");
|
|
@@ -872,19 +995,19 @@ function AgentPanel({
|
|
|
872
995
|
onClose,
|
|
873
996
|
onToggleAgentMode
|
|
874
997
|
}) {
|
|
875
|
-
const [draft, setDraft] =
|
|
876
|
-
const [view, setView] =
|
|
877
|
-
const [files, setFiles] =
|
|
878
|
-
const [uploadError, setUploadError] =
|
|
879
|
-
const [dragging, setDragging] =
|
|
880
|
-
const dragDepth =
|
|
998
|
+
const [draft, setDraft] = React7.useState("");
|
|
999
|
+
const [view, setView] = React7.useState("chat");
|
|
1000
|
+
const [files, setFiles] = React7.useState([]);
|
|
1001
|
+
const [uploadError, setUploadError] = React7.useState(null);
|
|
1002
|
+
const [dragging, setDragging] = React7.useState(false);
|
|
1003
|
+
const dragDepth = React7.useRef(0);
|
|
881
1004
|
const agentName = config.agentName ?? chat.agentInfo?.name ?? "Assistant";
|
|
882
1005
|
const uploadEnabled = fileUploadEnabled(config);
|
|
883
1006
|
const suggestions = config.suggestions ?? [
|
|
884
1007
|
"What can you help me with?",
|
|
885
1008
|
"Give me a quick summary of my data."
|
|
886
1009
|
];
|
|
887
|
-
const addFiles =
|
|
1010
|
+
const addFiles = React7.useCallback(
|
|
888
1011
|
(incoming) => {
|
|
889
1012
|
const accepted = [];
|
|
890
1013
|
let rejection = null;
|
|
@@ -901,10 +1024,10 @@ function AgentPanel({
|
|
|
901
1024
|
},
|
|
902
1025
|
[config]
|
|
903
1026
|
);
|
|
904
|
-
const removeFile =
|
|
1027
|
+
const removeFile = React7.useCallback((index) => {
|
|
905
1028
|
setFiles((current) => current.filter((_, i) => i !== index));
|
|
906
1029
|
}, []);
|
|
907
|
-
const handleSubmit =
|
|
1030
|
+
const handleSubmit = React7.useCallback(
|
|
908
1031
|
(text) => {
|
|
909
1032
|
const staged = files;
|
|
910
1033
|
setDraft("");
|
|
@@ -914,7 +1037,7 @@ function AgentPanel({
|
|
|
914
1037
|
},
|
|
915
1038
|
[chat, files]
|
|
916
1039
|
);
|
|
917
|
-
const onDragEnter =
|
|
1040
|
+
const onDragEnter = React7.useCallback(
|
|
918
1041
|
(event) => {
|
|
919
1042
|
if (!uploadEnabled || !event.dataTransfer?.types?.includes("Files")) return;
|
|
920
1043
|
dragDepth.current += 1;
|
|
@@ -922,12 +1045,12 @@ function AgentPanel({
|
|
|
922
1045
|
},
|
|
923
1046
|
[uploadEnabled]
|
|
924
1047
|
);
|
|
925
|
-
const onDragLeave =
|
|
1048
|
+
const onDragLeave = React7.useCallback((event) => {
|
|
926
1049
|
if (!event.dataTransfer?.types?.includes("Files")) return;
|
|
927
1050
|
dragDepth.current = Math.max(0, dragDepth.current - 1);
|
|
928
1051
|
if (dragDepth.current === 0) setDragging(false);
|
|
929
1052
|
}, []);
|
|
930
|
-
const onDrop =
|
|
1053
|
+
const onDrop = React7.useCallback(
|
|
931
1054
|
(event) => {
|
|
932
1055
|
if (!uploadEnabled) return;
|
|
933
1056
|
event.preventDefault();
|
|
@@ -938,11 +1061,14 @@ function AgentPanel({
|
|
|
938
1061
|
},
|
|
939
1062
|
[addFiles, uploadEnabled]
|
|
940
1063
|
);
|
|
941
|
-
const openHistory =
|
|
1064
|
+
const openHistory = React7.useCallback(() => {
|
|
942
1065
|
setView("history");
|
|
943
1066
|
void chat.refreshConversations();
|
|
944
1067
|
}, [chat]);
|
|
945
1068
|
const busy = chat.sending || chat.resolvingApprovalId !== null;
|
|
1069
|
+
const lastMessage = chat.messages[chat.messages.length - 1];
|
|
1070
|
+
const replyStreaming = lastMessage?.role === "assistant" && Boolean(lastMessage.content);
|
|
1071
|
+
const showThinking = busy && !replyStreaming;
|
|
946
1072
|
return /* @__PURE__ */ jsxs(
|
|
947
1073
|
"div",
|
|
948
1074
|
{
|
|
@@ -1065,7 +1191,7 @@ function AgentPanel({
|
|
|
1065
1191
|
},
|
|
1066
1192
|
conversation.id
|
|
1067
1193
|
)) }) : /* @__PURE__ */ jsxs(Chat, { children: [
|
|
1068
|
-
/* @__PURE__ */ jsx(
|
|
1194
|
+
/* @__PURE__ */ jsx(ChatScroller, { className: cn(agentMode && "mx-auto w-full max-w-3xl"), children: chat.messages.length === 0 && !chat.sending ? /* @__PURE__ */ jsxs(ChatEmptyState, { children: [
|
|
1069
1195
|
/* @__PURE__ */ jsx(ChatEmptyStateIcon, {}),
|
|
1070
1196
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
1071
1197
|
/* @__PURE__ */ jsxs(ChatEmptyStateTitle, { children: [
|
|
@@ -1099,7 +1225,7 @@ function AgentPanel({
|
|
|
1099
1225
|
},
|
|
1100
1226
|
approval.id
|
|
1101
1227
|
)),
|
|
1102
|
-
|
|
1228
|
+
showThinking ? /* @__PURE__ */ jsx(ThinkingIndicator, {}) : null
|
|
1103
1229
|
] }) }),
|
|
1104
1230
|
chat.error || uploadError ? /* @__PURE__ */ jsxs("div", { className: "mx-3 mb-1 flex items-center justify-between gap-2 rounded-lg bg-destructive/10 px-3 py-2 text-destructive text-xs", children: [
|
|
1105
1231
|
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: chat.error ?? uploadError }),
|
|
@@ -1120,8 +1246,9 @@ function AgentPanel({
|
|
|
1120
1246
|
AgentComposer,
|
|
1121
1247
|
{
|
|
1122
1248
|
acceptAttribute: acceptAttribute(config),
|
|
1249
|
+
busy,
|
|
1123
1250
|
className: cn(agentMode && "mx-auto w-full max-w-3xl"),
|
|
1124
|
-
disabled:
|
|
1251
|
+
disabled: chat.pendingApprovals.length > 0,
|
|
1125
1252
|
files,
|
|
1126
1253
|
onPickFiles: uploadEnabled ? addFiles : void 0,
|
|
1127
1254
|
onRemoveFile: removeFile,
|
|
@@ -1137,7 +1264,9 @@ function AgentPanel({
|
|
|
1137
1264
|
);
|
|
1138
1265
|
}
|
|
1139
1266
|
var OPTIMISTIC_ID_PREFIX = "optimistic-";
|
|
1267
|
+
var STREAMING_ID_PREFIX = "streaming-";
|
|
1140
1268
|
var optimisticCounter = 0;
|
|
1269
|
+
var streamingCounter = 0;
|
|
1141
1270
|
function buildOptimisticUserMessage(text, files) {
|
|
1142
1271
|
optimisticCounter += 1;
|
|
1143
1272
|
const attachments = (files ?? []).map((file, index) => ({
|
|
@@ -1155,16 +1284,16 @@ function buildOptimisticUserMessage(text, files) {
|
|
|
1155
1284
|
};
|
|
1156
1285
|
}
|
|
1157
1286
|
function useAgentChat(config) {
|
|
1158
|
-
const client =
|
|
1159
|
-
const [agentInfo, setAgentInfo] =
|
|
1160
|
-
const [conversationId, setConversationId] =
|
|
1161
|
-
const [conversations, setConversations] =
|
|
1162
|
-
const [messages, setMessages] =
|
|
1163
|
-
const [pendingApprovals, setPendingApprovals] =
|
|
1164
|
-
const [sending, setSending] =
|
|
1165
|
-
const [resolvingApprovalId, setResolvingApprovalId] =
|
|
1166
|
-
const [error, setError] =
|
|
1167
|
-
|
|
1287
|
+
const client = React7.useMemo(() => new AgentApiClient(config), [config]);
|
|
1288
|
+
const [agentInfo, setAgentInfo] = React7.useState(null);
|
|
1289
|
+
const [conversationId, setConversationId] = React7.useState(null);
|
|
1290
|
+
const [conversations, setConversations] = React7.useState([]);
|
|
1291
|
+
const [messages, setMessages] = React7.useState([]);
|
|
1292
|
+
const [pendingApprovals, setPendingApprovals] = React7.useState([]);
|
|
1293
|
+
const [sending, setSending] = React7.useState(false);
|
|
1294
|
+
const [resolvingApprovalId, setResolvingApprovalId] = React7.useState(null);
|
|
1295
|
+
const [error, setError] = React7.useState(null);
|
|
1296
|
+
React7.useEffect(() => {
|
|
1168
1297
|
let cancelled = false;
|
|
1169
1298
|
client.getConfig().then((info) => {
|
|
1170
1299
|
if (!cancelled) setAgentInfo(info);
|
|
@@ -1174,11 +1303,11 @@ function useAgentChat(config) {
|
|
|
1174
1303
|
cancelled = true;
|
|
1175
1304
|
};
|
|
1176
1305
|
}, [client]);
|
|
1177
|
-
const handleFailure =
|
|
1306
|
+
const handleFailure = React7.useCallback((err) => {
|
|
1178
1307
|
const message = err instanceof AgentApiError ? err.message : "Something went wrong. Please try again.";
|
|
1179
1308
|
setError(message);
|
|
1180
1309
|
}, []);
|
|
1181
|
-
const applyResponse =
|
|
1310
|
+
const applyResponse = React7.useCallback(
|
|
1182
1311
|
(response) => {
|
|
1183
1312
|
if (response.status === "error" && !response.messages?.length) {
|
|
1184
1313
|
setError(response.message ?? "The assistant could not answer.");
|
|
@@ -1200,7 +1329,72 @@ function useAgentChat(config) {
|
|
|
1200
1329
|
},
|
|
1201
1330
|
[]
|
|
1202
1331
|
);
|
|
1203
|
-
const
|
|
1332
|
+
const handleStreamEvent = React7.useCallback(
|
|
1333
|
+
(event, ctx) => {
|
|
1334
|
+
switch (event.type) {
|
|
1335
|
+
case "conversation":
|
|
1336
|
+
setConversationId(event.conversation_id);
|
|
1337
|
+
break;
|
|
1338
|
+
case "user_message":
|
|
1339
|
+
setMessages((current) => {
|
|
1340
|
+
const settled = current.filter((message) => !message.id.startsWith(OPTIMISTIC_ID_PREFIX));
|
|
1341
|
+
const seen = new Set(settled.map((message) => message.id));
|
|
1342
|
+
return seen.has(event.message.id) ? settled : [...settled, event.message];
|
|
1343
|
+
});
|
|
1344
|
+
break;
|
|
1345
|
+
case "text_delta": {
|
|
1346
|
+
if (!ctx.streamingId) {
|
|
1347
|
+
streamingCounter += 1;
|
|
1348
|
+
ctx.streamingId = `${STREAMING_ID_PREFIX}${streamingCounter}`;
|
|
1349
|
+
const bubble = {
|
|
1350
|
+
id: ctx.streamingId,
|
|
1351
|
+
role: "assistant",
|
|
1352
|
+
content: event.delta,
|
|
1353
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1354
|
+
};
|
|
1355
|
+
setMessages((current) => [...current, bubble]);
|
|
1356
|
+
} else {
|
|
1357
|
+
const id = ctx.streamingId;
|
|
1358
|
+
setMessages(
|
|
1359
|
+
(current) => current.map(
|
|
1360
|
+
(message) => message.id === id ? { ...message, content: (message.content ?? "") + event.delta } : message
|
|
1361
|
+
)
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
break;
|
|
1365
|
+
}
|
|
1366
|
+
case "assistant_message": {
|
|
1367
|
+
const streamingId = ctx.streamingId;
|
|
1368
|
+
ctx.streamingId = null;
|
|
1369
|
+
setMessages((current) => {
|
|
1370
|
+
const withoutTransient = streamingId ? current.filter((message) => message.id !== streamingId) : current;
|
|
1371
|
+
const seen = new Set(withoutTransient.map((message) => message.id));
|
|
1372
|
+
return seen.has(event.message.id) ? withoutTransient : [...withoutTransient, event.message];
|
|
1373
|
+
});
|
|
1374
|
+
break;
|
|
1375
|
+
}
|
|
1376
|
+
case "tool_result":
|
|
1377
|
+
setMessages((current) => {
|
|
1378
|
+
const seen = new Set(current.map((message) => message.id));
|
|
1379
|
+
return seen.has(event.message.id) ? current : [...current, event.message];
|
|
1380
|
+
});
|
|
1381
|
+
break;
|
|
1382
|
+
case "pending_approvals":
|
|
1383
|
+
setPendingApprovals(event.approvals ?? []);
|
|
1384
|
+
break;
|
|
1385
|
+
case "error":
|
|
1386
|
+
setError(event.message);
|
|
1387
|
+
break;
|
|
1388
|
+
case "done":
|
|
1389
|
+
if (event.conversation_id) setConversationId(event.conversation_id);
|
|
1390
|
+
if (event.pending_approvals) setPendingApprovals(event.pending_approvals);
|
|
1391
|
+
break;
|
|
1392
|
+
}
|
|
1393
|
+
},
|
|
1394
|
+
[]
|
|
1395
|
+
);
|
|
1396
|
+
const streamingEnabled = config.streaming !== false && typeof ReadableStream !== "undefined";
|
|
1397
|
+
const send = React7.useCallback(
|
|
1204
1398
|
async (text, files) => {
|
|
1205
1399
|
if (sending) return;
|
|
1206
1400
|
if (!text.trim() && !(files && files.length)) return;
|
|
@@ -1208,6 +1402,19 @@ function useAgentChat(config) {
|
|
|
1208
1402
|
setError(null);
|
|
1209
1403
|
setMessages((current) => [...current, buildOptimisticUserMessage(text, files)]);
|
|
1210
1404
|
try {
|
|
1405
|
+
if (streamingEnabled) {
|
|
1406
|
+
const ctx = { streamingId: null };
|
|
1407
|
+
let started = false;
|
|
1408
|
+
try {
|
|
1409
|
+
await client.streamMessage(text, conversationId, files, (event) => {
|
|
1410
|
+
started = true;
|
|
1411
|
+
handleStreamEvent(event, ctx);
|
|
1412
|
+
});
|
|
1413
|
+
return;
|
|
1414
|
+
} catch (err) {
|
|
1415
|
+
if (started) throw err;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1211
1418
|
const response = await client.sendMessage(text, conversationId, files);
|
|
1212
1419
|
applyResponse(response);
|
|
1213
1420
|
} catch (err) {
|
|
@@ -1216,14 +1423,27 @@ function useAgentChat(config) {
|
|
|
1216
1423
|
setSending(false);
|
|
1217
1424
|
}
|
|
1218
1425
|
},
|
|
1219
|
-
[applyResponse, client, conversationId, handleFailure, sending]
|
|
1426
|
+
[applyResponse, client, conversationId, handleFailure, handleStreamEvent, sending, streamingEnabled]
|
|
1220
1427
|
);
|
|
1221
|
-
const resolveApproval =
|
|
1428
|
+
const resolveApproval = React7.useCallback(
|
|
1222
1429
|
async (callId, approve) => {
|
|
1223
1430
|
if (resolvingApprovalId) return;
|
|
1224
1431
|
setResolvingApprovalId(callId);
|
|
1225
1432
|
setError(null);
|
|
1226
1433
|
try {
|
|
1434
|
+
if (streamingEnabled) {
|
|
1435
|
+
const ctx = { streamingId: null };
|
|
1436
|
+
let started = false;
|
|
1437
|
+
try {
|
|
1438
|
+
await client.streamResolveApproval(callId, approve, (event) => {
|
|
1439
|
+
started = true;
|
|
1440
|
+
handleStreamEvent(event, ctx);
|
|
1441
|
+
});
|
|
1442
|
+
return;
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
if (started) throw err;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1227
1447
|
const response = await client.resolveApproval(callId, approve);
|
|
1228
1448
|
applyResponse(response);
|
|
1229
1449
|
} catch (err) {
|
|
@@ -1232,15 +1452,15 @@ function useAgentChat(config) {
|
|
|
1232
1452
|
setResolvingApprovalId(null);
|
|
1233
1453
|
}
|
|
1234
1454
|
},
|
|
1235
|
-
[applyResponse, client, handleFailure, resolvingApprovalId]
|
|
1455
|
+
[applyResponse, client, handleFailure, handleStreamEvent, resolvingApprovalId, streamingEnabled]
|
|
1236
1456
|
);
|
|
1237
|
-
const newChat =
|
|
1457
|
+
const newChat = React7.useCallback(() => {
|
|
1238
1458
|
setConversationId(null);
|
|
1239
1459
|
setMessages([]);
|
|
1240
1460
|
setPendingApprovals([]);
|
|
1241
1461
|
setError(null);
|
|
1242
1462
|
}, []);
|
|
1243
|
-
const refreshConversations =
|
|
1463
|
+
const refreshConversations = React7.useCallback(async () => {
|
|
1244
1464
|
try {
|
|
1245
1465
|
const { conversations: list } = await client.listConversations();
|
|
1246
1466
|
setConversations(list);
|
|
@@ -1248,7 +1468,7 @@ function useAgentChat(config) {
|
|
|
1248
1468
|
handleFailure(err);
|
|
1249
1469
|
}
|
|
1250
1470
|
}, [client, handleFailure]);
|
|
1251
|
-
const openConversation =
|
|
1471
|
+
const openConversation = React7.useCallback(
|
|
1252
1472
|
async (id) => {
|
|
1253
1473
|
setError(null);
|
|
1254
1474
|
try {
|
|
@@ -1262,7 +1482,7 @@ function useAgentChat(config) {
|
|
|
1262
1482
|
},
|
|
1263
1483
|
[client, handleFailure]
|
|
1264
1484
|
);
|
|
1265
|
-
const deleteConversation =
|
|
1485
|
+
const deleteConversation = React7.useCallback(
|
|
1266
1486
|
async (id) => {
|
|
1267
1487
|
try {
|
|
1268
1488
|
await client.deleteConversation(id);
|
|
@@ -1274,7 +1494,7 @@ function useAgentChat(config) {
|
|
|
1274
1494
|
},
|
|
1275
1495
|
[client, conversationId, handleFailure, newChat]
|
|
1276
1496
|
);
|
|
1277
|
-
const clearError =
|
|
1497
|
+
const clearError = React7.useCallback(() => setError(null), []);
|
|
1278
1498
|
return {
|
|
1279
1499
|
agentInfo,
|
|
1280
1500
|
clearError,
|
|
@@ -1309,11 +1529,11 @@ function readStored(clientId) {
|
|
|
1309
1529
|
}
|
|
1310
1530
|
}
|
|
1311
1531
|
function useAlwaysApprove(clientId) {
|
|
1312
|
-
const [approved, setApproved] =
|
|
1313
|
-
|
|
1532
|
+
const [approved, setApproved] = React7.useState(() => /* @__PURE__ */ new Set());
|
|
1533
|
+
React7.useEffect(() => {
|
|
1314
1534
|
setApproved(readStored(clientId));
|
|
1315
1535
|
}, [clientId]);
|
|
1316
|
-
const persist =
|
|
1536
|
+
const persist = React7.useCallback(
|
|
1317
1537
|
(next) => {
|
|
1318
1538
|
setApproved(next);
|
|
1319
1539
|
if (typeof window === "undefined") return;
|
|
@@ -1324,8 +1544,8 @@ function useAlwaysApprove(clientId) {
|
|
|
1324
1544
|
},
|
|
1325
1545
|
[clientId]
|
|
1326
1546
|
);
|
|
1327
|
-
const isAlwaysApproved =
|
|
1328
|
-
const setAlwaysApprove =
|
|
1547
|
+
const isAlwaysApproved = React7.useCallback((toolName) => approved.has(toolName), [approved]);
|
|
1548
|
+
const setAlwaysApprove = React7.useCallback(
|
|
1329
1549
|
(toolName) => {
|
|
1330
1550
|
if (approved.has(toolName)) return;
|
|
1331
1551
|
const next = new Set(approved);
|
|
@@ -1334,7 +1554,7 @@ function useAlwaysApprove(clientId) {
|
|
|
1334
1554
|
},
|
|
1335
1555
|
[approved, persist]
|
|
1336
1556
|
);
|
|
1337
|
-
const clearAlwaysApprove =
|
|
1557
|
+
const clearAlwaysApprove = React7.useCallback(
|
|
1338
1558
|
(toolName) => {
|
|
1339
1559
|
if (!approved.has(toolName)) return;
|
|
1340
1560
|
const next = new Set(approved);
|
|
@@ -1346,16 +1566,16 @@ function useAlwaysApprove(clientId) {
|
|
|
1346
1566
|
return { clearAlwaysApprove, isAlwaysApproved, setAlwaysApprove };
|
|
1347
1567
|
}
|
|
1348
1568
|
function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
|
|
1349
|
-
const [uncontrolledOpen, setUncontrolledOpen] =
|
|
1569
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React7.useState(false);
|
|
1350
1570
|
const open = controlledOpen ?? uncontrolledOpen;
|
|
1351
|
-
const setOpen =
|
|
1571
|
+
const setOpen = React7.useCallback(
|
|
1352
1572
|
(next) => {
|
|
1353
1573
|
setUncontrolledOpen(next);
|
|
1354
1574
|
onOpenChange?.(next);
|
|
1355
1575
|
},
|
|
1356
1576
|
[onOpenChange]
|
|
1357
1577
|
);
|
|
1358
|
-
const [agentMode, setAgentMode] =
|
|
1578
|
+
const [agentMode, setAgentMode] = React7.useState(false);
|
|
1359
1579
|
const isDesktop = useMediaQuery("(min-width: 640px)", {
|
|
1360
1580
|
defaultSSRValue: true,
|
|
1361
1581
|
ssr: true
|
|
@@ -1365,14 +1585,14 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
|
|
|
1365
1585
|
const alwaysApprove = useAlwaysApprove(config.clientId);
|
|
1366
1586
|
const { pendingApprovals, resolveApproval, resolvingApprovalId } = chat;
|
|
1367
1587
|
const { isAlwaysApproved } = alwaysApprove;
|
|
1368
|
-
|
|
1588
|
+
React7.useEffect(() => {
|
|
1369
1589
|
if (!open || resolvingApprovalId) return;
|
|
1370
1590
|
const target = pendingApprovals.find((approval) => isAlwaysApproved(approval.tool_name));
|
|
1371
1591
|
if (target) void resolveApproval(target.id, true);
|
|
1372
1592
|
}, [open, pendingApprovals, resolvingApprovalId, isAlwaysApproved, resolveApproval]);
|
|
1373
|
-
const firedWriteIds =
|
|
1593
|
+
const firedWriteIds = React7.useRef(/* @__PURE__ */ new Set());
|
|
1374
1594
|
const { onWriteSuccess } = config;
|
|
1375
|
-
|
|
1595
|
+
React7.useEffect(() => {
|
|
1376
1596
|
if (!onWriteSuccess) return;
|
|
1377
1597
|
for (const message of chat.messages) {
|
|
1378
1598
|
if (message.role !== "tool" || !message.is_write || message.tool_status !== "success") continue;
|
|
@@ -1382,9 +1602,9 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
|
|
|
1382
1602
|
onWriteSuccess({ arguments: args, toolName: message.tool_name ?? "" });
|
|
1383
1603
|
}
|
|
1384
1604
|
}, [chat.messages, onWriteSuccess]);
|
|
1385
|
-
const lastNavigatedMessageId =
|
|
1605
|
+
const lastNavigatedMessageId = React7.useRef(null);
|
|
1386
1606
|
const { messages } = chat;
|
|
1387
|
-
|
|
1607
|
+
React7.useEffect(() => {
|
|
1388
1608
|
if (!open || fullscreen || !config.navigate || !config.pages?.length) return;
|
|
1389
1609
|
const toolMessages = messages.filter((message) => message.role === "tool");
|
|
1390
1610
|
const latest = toolMessages[toolMessages.length - 1];
|
|
@@ -1396,7 +1616,7 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
|
|
|
1396
1616
|
const path = mapping.buildRoute ? mapping.buildRoute(args) : mapping.route;
|
|
1397
1617
|
if (path) config.navigate(path);
|
|
1398
1618
|
}, [config, fullscreen, messages, open]);
|
|
1399
|
-
const closePanel =
|
|
1619
|
+
const closePanel = React7.useCallback(() => {
|
|
1400
1620
|
setOpen(false);
|
|
1401
1621
|
setAgentMode(false);
|
|
1402
1622
|
}, [setOpen]);
|
|
@@ -1475,6 +1695,6 @@ function defineAgentConfig(config) {
|
|
|
1475
1695
|
return config;
|
|
1476
1696
|
}
|
|
1477
1697
|
|
|
1478
|
-
export { AgentApiClient, AgentApiError, AgentComposer, AgentPanel, AgentWidget, DEFAULT_ACCEPT, DocumentCard, MessageItem, TaskChecklist, ThinkingIndicator, ToolActivityGroup, ToolApprovalCard, acceptAttribute, acceptedExtensions, defineAgentConfig, deriveDetails, fileUploadEnabled, formatBytes, groupMessages, humanizeToolName, resolveToolPresentation, useAgentChat, useAlwaysApprove, validateFile };
|
|
1698
|
+
export { AgentApiClient, AgentApiError, AgentComposer, AgentPanel, AgentWidget, ChatScroller, DEFAULT_ACCEPT, DocumentCard, MessageItem, TaskChecklist, ThinkingIndicator, ToolActivityGroup, ToolApprovalCard, acceptAttribute, acceptedExtensions, defineAgentConfig, deriveDetails, fileUploadEnabled, formatBytes, groupMessages, humanizeToolName, resolveToolPresentation, useAgentChat, useAlwaysApprove, validateFile };
|
|
1479
1699
|
//# sourceMappingURL=index.js.map
|
|
1480
1700
|
//# sourceMappingURL=index.js.map
|