@cntyclub/agent-react 0.7.1 → 0.9.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/api-client.d.ts +7 -1
- package/dist/api-client.d.ts.map +1 -1
- package/dist/components/agent-panel.d.ts.map +1 -1
- package/dist/components/thinking-indicator.d.ts +14 -0
- package/dist/components/thinking-indicator.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +319 -62
- 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, ChatMessages, ChatEmptyState, ChatEmptyStateIcon, ChatEmptyStateTitle, ChatEmptyStateDescription, ChatSuggestions, ChatSuggestion,
|
|
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, ChatMessages, 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 React6 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
|
}
|
|
@@ -137,14 +229,14 @@ function AgentComposer({
|
|
|
137
229
|
placeholder = "Ask anything\u2026",
|
|
138
230
|
value
|
|
139
231
|
}) {
|
|
140
|
-
const textareaRef =
|
|
141
|
-
const inputRef =
|
|
232
|
+
const textareaRef = React6.useRef(null);
|
|
233
|
+
const inputRef = React6.useRef(null);
|
|
142
234
|
const canSubmit = !disabled && (value.trim().length > 0 || files.length > 0);
|
|
143
|
-
const submit =
|
|
235
|
+
const submit = React6.useCallback(() => {
|
|
144
236
|
if (!canSubmit) return;
|
|
145
237
|
onSubmit(value.trim());
|
|
146
238
|
}, [canSubmit, onSubmit, value]);
|
|
147
|
-
|
|
239
|
+
React6.useEffect(() => {
|
|
148
240
|
const textarea = textareaRef.current;
|
|
149
241
|
if (!textarea) return;
|
|
150
242
|
textarea.style.height = "auto";
|
|
@@ -349,6 +441,55 @@ function MessageItem({ message }) {
|
|
|
349
441
|
}
|
|
350
442
|
return null;
|
|
351
443
|
}
|
|
444
|
+
var DEFAULT_PHRASES = [
|
|
445
|
+
"Thinking",
|
|
446
|
+
"Working on it",
|
|
447
|
+
"Looking that up",
|
|
448
|
+
"Reading the data",
|
|
449
|
+
"Putting it together",
|
|
450
|
+
"Almost there"
|
|
451
|
+
];
|
|
452
|
+
function ThinkingIndicator({ className, intervalMs = 1800, phrases }) {
|
|
453
|
+
const words = phrases && phrases.length ? phrases : DEFAULT_PHRASES;
|
|
454
|
+
const [index, setIndex] = React6.useState(0);
|
|
455
|
+
React6.useEffect(() => {
|
|
456
|
+
setIndex(0);
|
|
457
|
+
const timer = setInterval(() => {
|
|
458
|
+
setIndex((current) => (current + 1) % words.length);
|
|
459
|
+
}, intervalMs);
|
|
460
|
+
return () => clearInterval(timer);
|
|
461
|
+
}, [intervalMs, words]);
|
|
462
|
+
return /* @__PURE__ */ jsxs(
|
|
463
|
+
"div",
|
|
464
|
+
{
|
|
465
|
+
"aria-label": "Assistant is working",
|
|
466
|
+
className: cn(
|
|
467
|
+
"fade-in-0 flex w-fit animate-in items-center gap-2 rounded-2xl rounded-bl-md bg-muted px-3.5 py-2.5 duration-200",
|
|
468
|
+
className
|
|
469
|
+
),
|
|
470
|
+
"data-slot": "thinking-indicator",
|
|
471
|
+
role: "status",
|
|
472
|
+
children: [
|
|
473
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
|
|
474
|
+
/* @__PURE__ */ jsx("span", { className: "size-1.5 animate-bounce rounded-full bg-muted-foreground/60 [animation-delay:0ms]" }),
|
|
475
|
+
/* @__PURE__ */ jsx("span", { className: "size-1.5 animate-bounce rounded-full bg-muted-foreground/60 [animation-delay:150ms]" }),
|
|
476
|
+
/* @__PURE__ */ jsx("span", { className: "size-1.5 animate-bounce rounded-full bg-muted-foreground/60 [animation-delay:300ms]" })
|
|
477
|
+
] }),
|
|
478
|
+
/* @__PURE__ */ jsxs(
|
|
479
|
+
"span",
|
|
480
|
+
{
|
|
481
|
+
className: "fade-in-0 animate-in text-muted-foreground text-xs duration-300",
|
|
482
|
+
children: [
|
|
483
|
+
words[index],
|
|
484
|
+
"\u2026"
|
|
485
|
+
]
|
|
486
|
+
},
|
|
487
|
+
index
|
|
488
|
+
)
|
|
489
|
+
]
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
}
|
|
352
493
|
var NOISE_KEYS = /* @__PURE__ */ new Set([
|
|
353
494
|
"id",
|
|
354
495
|
"pk",
|
|
@@ -573,7 +714,7 @@ function DownloadButton({ block, size = "sm" }) {
|
|
|
573
714
|
);
|
|
574
715
|
}
|
|
575
716
|
function DocumentViewer({ block, onClose }) {
|
|
576
|
-
|
|
717
|
+
React6.useEffect(() => {
|
|
577
718
|
const onKey = (event) => event.key === "Escape" && onClose();
|
|
578
719
|
window.addEventListener("keydown", onKey);
|
|
579
720
|
return () => window.removeEventListener("keydown", onKey);
|
|
@@ -598,7 +739,7 @@ function DocumentViewer({ block, onClose }) {
|
|
|
598
739
|
] });
|
|
599
740
|
}
|
|
600
741
|
function DocumentCard({ block }) {
|
|
601
|
-
const [open, setOpen] =
|
|
742
|
+
const [open, setOpen] = React6.useState(false);
|
|
602
743
|
const previewLines = (block.preview || "").split("\n").slice(0, 6).join("\n");
|
|
603
744
|
return /* @__PURE__ */ jsxs("div", { className: "w-full max-w-full overflow-hidden rounded-xl border border-border bg-card shadow-xs", children: [
|
|
604
745
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-border/72 border-b px-3 py-2", children: [
|
|
@@ -720,7 +861,7 @@ function StepDetail({
|
|
|
720
861
|
] });
|
|
721
862
|
}
|
|
722
863
|
function ToolActivityGroup({ describeToolCall, steps }) {
|
|
723
|
-
const [open, setOpen] =
|
|
864
|
+
const [open, setOpen] = React6.useState(false);
|
|
724
865
|
if (steps.length === 0) return null;
|
|
725
866
|
const last = steps[steps.length - 1];
|
|
726
867
|
const anyRunning = steps.some((step) => step.status === "running");
|
|
@@ -823,19 +964,19 @@ function AgentPanel({
|
|
|
823
964
|
onClose,
|
|
824
965
|
onToggleAgentMode
|
|
825
966
|
}) {
|
|
826
|
-
const [draft, setDraft] =
|
|
827
|
-
const [view, setView] =
|
|
828
|
-
const [files, setFiles] =
|
|
829
|
-
const [uploadError, setUploadError] =
|
|
830
|
-
const [dragging, setDragging] =
|
|
831
|
-
const dragDepth =
|
|
967
|
+
const [draft, setDraft] = React6.useState("");
|
|
968
|
+
const [view, setView] = React6.useState("chat");
|
|
969
|
+
const [files, setFiles] = React6.useState([]);
|
|
970
|
+
const [uploadError, setUploadError] = React6.useState(null);
|
|
971
|
+
const [dragging, setDragging] = React6.useState(false);
|
|
972
|
+
const dragDepth = React6.useRef(0);
|
|
832
973
|
const agentName = config.agentName ?? chat.agentInfo?.name ?? "Assistant";
|
|
833
974
|
const uploadEnabled = fileUploadEnabled(config);
|
|
834
975
|
const suggestions = config.suggestions ?? [
|
|
835
976
|
"What can you help me with?",
|
|
836
977
|
"Give me a quick summary of my data."
|
|
837
978
|
];
|
|
838
|
-
const addFiles =
|
|
979
|
+
const addFiles = React6.useCallback(
|
|
839
980
|
(incoming) => {
|
|
840
981
|
const accepted = [];
|
|
841
982
|
let rejection = null;
|
|
@@ -852,10 +993,10 @@ function AgentPanel({
|
|
|
852
993
|
},
|
|
853
994
|
[config]
|
|
854
995
|
);
|
|
855
|
-
const removeFile =
|
|
996
|
+
const removeFile = React6.useCallback((index) => {
|
|
856
997
|
setFiles((current) => current.filter((_, i) => i !== index));
|
|
857
998
|
}, []);
|
|
858
|
-
const handleSubmit =
|
|
999
|
+
const handleSubmit = React6.useCallback(
|
|
859
1000
|
(text) => {
|
|
860
1001
|
const staged = files;
|
|
861
1002
|
setDraft("");
|
|
@@ -865,7 +1006,7 @@ function AgentPanel({
|
|
|
865
1006
|
},
|
|
866
1007
|
[chat, files]
|
|
867
1008
|
);
|
|
868
|
-
const onDragEnter =
|
|
1009
|
+
const onDragEnter = React6.useCallback(
|
|
869
1010
|
(event) => {
|
|
870
1011
|
if (!uploadEnabled || !event.dataTransfer?.types?.includes("Files")) return;
|
|
871
1012
|
dragDepth.current += 1;
|
|
@@ -873,12 +1014,12 @@ function AgentPanel({
|
|
|
873
1014
|
},
|
|
874
1015
|
[uploadEnabled]
|
|
875
1016
|
);
|
|
876
|
-
const onDragLeave =
|
|
1017
|
+
const onDragLeave = React6.useCallback((event) => {
|
|
877
1018
|
if (!event.dataTransfer?.types?.includes("Files")) return;
|
|
878
1019
|
dragDepth.current = Math.max(0, dragDepth.current - 1);
|
|
879
1020
|
if (dragDepth.current === 0) setDragging(false);
|
|
880
1021
|
}, []);
|
|
881
|
-
const onDrop =
|
|
1022
|
+
const onDrop = React6.useCallback(
|
|
882
1023
|
(event) => {
|
|
883
1024
|
if (!uploadEnabled) return;
|
|
884
1025
|
event.preventDefault();
|
|
@@ -889,11 +1030,14 @@ function AgentPanel({
|
|
|
889
1030
|
},
|
|
890
1031
|
[addFiles, uploadEnabled]
|
|
891
1032
|
);
|
|
892
|
-
const openHistory =
|
|
1033
|
+
const openHistory = React6.useCallback(() => {
|
|
893
1034
|
setView("history");
|
|
894
1035
|
void chat.refreshConversations();
|
|
895
1036
|
}, [chat]);
|
|
896
1037
|
const busy = chat.sending || chat.resolvingApprovalId !== null;
|
|
1038
|
+
const lastMessage = chat.messages[chat.messages.length - 1];
|
|
1039
|
+
const replyStreaming = lastMessage?.role === "assistant" && Boolean(lastMessage.content);
|
|
1040
|
+
const showThinking = busy && !replyStreaming;
|
|
897
1041
|
return /* @__PURE__ */ jsxs(
|
|
898
1042
|
"div",
|
|
899
1043
|
{
|
|
@@ -1050,7 +1194,7 @@ function AgentPanel({
|
|
|
1050
1194
|
},
|
|
1051
1195
|
approval.id
|
|
1052
1196
|
)),
|
|
1053
|
-
|
|
1197
|
+
showThinking ? /* @__PURE__ */ jsx(ThinkingIndicator, {}) : null
|
|
1054
1198
|
] }) }),
|
|
1055
1199
|
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: [
|
|
1056
1200
|
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: chat.error ?? uploadError }),
|
|
@@ -1087,17 +1231,37 @@ function AgentPanel({
|
|
|
1087
1231
|
}
|
|
1088
1232
|
);
|
|
1089
1233
|
}
|
|
1234
|
+
var OPTIMISTIC_ID_PREFIX = "optimistic-";
|
|
1235
|
+
var STREAMING_ID_PREFIX = "streaming-";
|
|
1236
|
+
var optimisticCounter = 0;
|
|
1237
|
+
var streamingCounter = 0;
|
|
1238
|
+
function buildOptimisticUserMessage(text, files) {
|
|
1239
|
+
optimisticCounter += 1;
|
|
1240
|
+
const attachments = (files ?? []).map((file, index) => ({
|
|
1241
|
+
id: `${OPTIMISTIC_ID_PREFIX}att-${optimisticCounter}-${index}`,
|
|
1242
|
+
filename: file.name,
|
|
1243
|
+
format: file.name.split(".").pop()?.toLowerCase() ?? "file",
|
|
1244
|
+
size_bytes: file.size
|
|
1245
|
+
}));
|
|
1246
|
+
return {
|
|
1247
|
+
id: `${OPTIMISTIC_ID_PREFIX}${optimisticCounter}`,
|
|
1248
|
+
role: "user",
|
|
1249
|
+
content: text,
|
|
1250
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1251
|
+
...attachments.length ? { attachments } : {}
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1090
1254
|
function useAgentChat(config) {
|
|
1091
|
-
const client =
|
|
1092
|
-
const [agentInfo, setAgentInfo] =
|
|
1093
|
-
const [conversationId, setConversationId] =
|
|
1094
|
-
const [conversations, setConversations] =
|
|
1095
|
-
const [messages, setMessages] =
|
|
1096
|
-
const [pendingApprovals, setPendingApprovals] =
|
|
1097
|
-
const [sending, setSending] =
|
|
1098
|
-
const [resolvingApprovalId, setResolvingApprovalId] =
|
|
1099
|
-
const [error, setError] =
|
|
1100
|
-
|
|
1255
|
+
const client = React6.useMemo(() => new AgentApiClient(config), [config]);
|
|
1256
|
+
const [agentInfo, setAgentInfo] = React6.useState(null);
|
|
1257
|
+
const [conversationId, setConversationId] = React6.useState(null);
|
|
1258
|
+
const [conversations, setConversations] = React6.useState([]);
|
|
1259
|
+
const [messages, setMessages] = React6.useState([]);
|
|
1260
|
+
const [pendingApprovals, setPendingApprovals] = React6.useState([]);
|
|
1261
|
+
const [sending, setSending] = React6.useState(false);
|
|
1262
|
+
const [resolvingApprovalId, setResolvingApprovalId] = React6.useState(null);
|
|
1263
|
+
const [error, setError] = React6.useState(null);
|
|
1264
|
+
React6.useEffect(() => {
|
|
1101
1265
|
let cancelled = false;
|
|
1102
1266
|
client.getConfig().then((info) => {
|
|
1103
1267
|
if (!cancelled) setAgentInfo(info);
|
|
@@ -1107,11 +1271,11 @@ function useAgentChat(config) {
|
|
|
1107
1271
|
cancelled = true;
|
|
1108
1272
|
};
|
|
1109
1273
|
}, [client]);
|
|
1110
|
-
const handleFailure =
|
|
1274
|
+
const handleFailure = React6.useCallback((err) => {
|
|
1111
1275
|
const message = err instanceof AgentApiError ? err.message : "Something went wrong. Please try again.";
|
|
1112
1276
|
setError(message);
|
|
1113
1277
|
}, []);
|
|
1114
|
-
const applyResponse =
|
|
1278
|
+
const applyResponse = React6.useCallback(
|
|
1115
1279
|
(response) => {
|
|
1116
1280
|
if (response.status === "error" && !response.messages?.length) {
|
|
1117
1281
|
setError(response.message ?? "The assistant could not answer.");
|
|
@@ -1120,9 +1284,10 @@ function useAgentChat(config) {
|
|
|
1120
1284
|
if (response.conversation_id) setConversationId(response.conversation_id);
|
|
1121
1285
|
if (response.messages?.length) {
|
|
1122
1286
|
setMessages((current) => {
|
|
1123
|
-
const
|
|
1287
|
+
const settled = current.filter((message) => !message.id.startsWith(OPTIMISTIC_ID_PREFIX));
|
|
1288
|
+
const seen = new Set(settled.map((message) => message.id));
|
|
1124
1289
|
const fresh = response.messages?.filter((message) => !seen.has(message.id)) ?? [];
|
|
1125
|
-
return [...
|
|
1290
|
+
return [...settled, ...fresh];
|
|
1126
1291
|
});
|
|
1127
1292
|
}
|
|
1128
1293
|
setPendingApprovals(response.pending_approvals ?? []);
|
|
@@ -1132,13 +1297,92 @@ function useAgentChat(config) {
|
|
|
1132
1297
|
},
|
|
1133
1298
|
[]
|
|
1134
1299
|
);
|
|
1135
|
-
const
|
|
1300
|
+
const handleStreamEvent = React6.useCallback(
|
|
1301
|
+
(event, ctx) => {
|
|
1302
|
+
switch (event.type) {
|
|
1303
|
+
case "conversation":
|
|
1304
|
+
setConversationId(event.conversation_id);
|
|
1305
|
+
break;
|
|
1306
|
+
case "user_message":
|
|
1307
|
+
setMessages((current) => {
|
|
1308
|
+
const settled = current.filter((message) => !message.id.startsWith(OPTIMISTIC_ID_PREFIX));
|
|
1309
|
+
const seen = new Set(settled.map((message) => message.id));
|
|
1310
|
+
return seen.has(event.message.id) ? settled : [...settled, event.message];
|
|
1311
|
+
});
|
|
1312
|
+
break;
|
|
1313
|
+
case "text_delta": {
|
|
1314
|
+
if (!ctx.streamingId) {
|
|
1315
|
+
streamingCounter += 1;
|
|
1316
|
+
ctx.streamingId = `${STREAMING_ID_PREFIX}${streamingCounter}`;
|
|
1317
|
+
const bubble = {
|
|
1318
|
+
id: ctx.streamingId,
|
|
1319
|
+
role: "assistant",
|
|
1320
|
+
content: event.delta,
|
|
1321
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1322
|
+
};
|
|
1323
|
+
setMessages((current) => [...current, bubble]);
|
|
1324
|
+
} else {
|
|
1325
|
+
const id = ctx.streamingId;
|
|
1326
|
+
setMessages(
|
|
1327
|
+
(current) => current.map(
|
|
1328
|
+
(message) => message.id === id ? { ...message, content: (message.content ?? "") + event.delta } : message
|
|
1329
|
+
)
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1334
|
+
case "assistant_message": {
|
|
1335
|
+
const streamingId = ctx.streamingId;
|
|
1336
|
+
ctx.streamingId = null;
|
|
1337
|
+
setMessages((current) => {
|
|
1338
|
+
const withoutTransient = streamingId ? current.filter((message) => message.id !== streamingId) : current;
|
|
1339
|
+
const seen = new Set(withoutTransient.map((message) => message.id));
|
|
1340
|
+
return seen.has(event.message.id) ? withoutTransient : [...withoutTransient, event.message];
|
|
1341
|
+
});
|
|
1342
|
+
break;
|
|
1343
|
+
}
|
|
1344
|
+
case "tool_result":
|
|
1345
|
+
setMessages((current) => {
|
|
1346
|
+
const seen = new Set(current.map((message) => message.id));
|
|
1347
|
+
return seen.has(event.message.id) ? current : [...current, event.message];
|
|
1348
|
+
});
|
|
1349
|
+
break;
|
|
1350
|
+
case "pending_approvals":
|
|
1351
|
+
setPendingApprovals(event.approvals ?? []);
|
|
1352
|
+
break;
|
|
1353
|
+
case "error":
|
|
1354
|
+
setError(event.message);
|
|
1355
|
+
break;
|
|
1356
|
+
case "done":
|
|
1357
|
+
if (event.conversation_id) setConversationId(event.conversation_id);
|
|
1358
|
+
if (event.pending_approvals) setPendingApprovals(event.pending_approvals);
|
|
1359
|
+
break;
|
|
1360
|
+
}
|
|
1361
|
+
},
|
|
1362
|
+
[]
|
|
1363
|
+
);
|
|
1364
|
+
const streamingEnabled = config.streaming !== false && typeof ReadableStream !== "undefined";
|
|
1365
|
+
const send = React6.useCallback(
|
|
1136
1366
|
async (text, files) => {
|
|
1137
1367
|
if (sending) return;
|
|
1138
1368
|
if (!text.trim() && !(files && files.length)) return;
|
|
1139
1369
|
setSending(true);
|
|
1140
1370
|
setError(null);
|
|
1371
|
+
setMessages((current) => [...current, buildOptimisticUserMessage(text, files)]);
|
|
1141
1372
|
try {
|
|
1373
|
+
if (streamingEnabled) {
|
|
1374
|
+
const ctx = { streamingId: null };
|
|
1375
|
+
let started = false;
|
|
1376
|
+
try {
|
|
1377
|
+
await client.streamMessage(text, conversationId, files, (event) => {
|
|
1378
|
+
started = true;
|
|
1379
|
+
handleStreamEvent(event, ctx);
|
|
1380
|
+
});
|
|
1381
|
+
return;
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
if (started) throw err;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1142
1386
|
const response = await client.sendMessage(text, conversationId, files);
|
|
1143
1387
|
applyResponse(response);
|
|
1144
1388
|
} catch (err) {
|
|
@@ -1147,14 +1391,27 @@ function useAgentChat(config) {
|
|
|
1147
1391
|
setSending(false);
|
|
1148
1392
|
}
|
|
1149
1393
|
},
|
|
1150
|
-
[applyResponse, client, conversationId, handleFailure, sending]
|
|
1394
|
+
[applyResponse, client, conversationId, handleFailure, handleStreamEvent, sending, streamingEnabled]
|
|
1151
1395
|
);
|
|
1152
|
-
const resolveApproval =
|
|
1396
|
+
const resolveApproval = React6.useCallback(
|
|
1153
1397
|
async (callId, approve) => {
|
|
1154
1398
|
if (resolvingApprovalId) return;
|
|
1155
1399
|
setResolvingApprovalId(callId);
|
|
1156
1400
|
setError(null);
|
|
1157
1401
|
try {
|
|
1402
|
+
if (streamingEnabled) {
|
|
1403
|
+
const ctx = { streamingId: null };
|
|
1404
|
+
let started = false;
|
|
1405
|
+
try {
|
|
1406
|
+
await client.streamResolveApproval(callId, approve, (event) => {
|
|
1407
|
+
started = true;
|
|
1408
|
+
handleStreamEvent(event, ctx);
|
|
1409
|
+
});
|
|
1410
|
+
return;
|
|
1411
|
+
} catch (err) {
|
|
1412
|
+
if (started) throw err;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1158
1415
|
const response = await client.resolveApproval(callId, approve);
|
|
1159
1416
|
applyResponse(response);
|
|
1160
1417
|
} catch (err) {
|
|
@@ -1163,15 +1420,15 @@ function useAgentChat(config) {
|
|
|
1163
1420
|
setResolvingApprovalId(null);
|
|
1164
1421
|
}
|
|
1165
1422
|
},
|
|
1166
|
-
[applyResponse, client, handleFailure, resolvingApprovalId]
|
|
1423
|
+
[applyResponse, client, handleFailure, handleStreamEvent, resolvingApprovalId, streamingEnabled]
|
|
1167
1424
|
);
|
|
1168
|
-
const newChat =
|
|
1425
|
+
const newChat = React6.useCallback(() => {
|
|
1169
1426
|
setConversationId(null);
|
|
1170
1427
|
setMessages([]);
|
|
1171
1428
|
setPendingApprovals([]);
|
|
1172
1429
|
setError(null);
|
|
1173
1430
|
}, []);
|
|
1174
|
-
const refreshConversations =
|
|
1431
|
+
const refreshConversations = React6.useCallback(async () => {
|
|
1175
1432
|
try {
|
|
1176
1433
|
const { conversations: list } = await client.listConversations();
|
|
1177
1434
|
setConversations(list);
|
|
@@ -1179,7 +1436,7 @@ function useAgentChat(config) {
|
|
|
1179
1436
|
handleFailure(err);
|
|
1180
1437
|
}
|
|
1181
1438
|
}, [client, handleFailure]);
|
|
1182
|
-
const openConversation =
|
|
1439
|
+
const openConversation = React6.useCallback(
|
|
1183
1440
|
async (id) => {
|
|
1184
1441
|
setError(null);
|
|
1185
1442
|
try {
|
|
@@ -1193,7 +1450,7 @@ function useAgentChat(config) {
|
|
|
1193
1450
|
},
|
|
1194
1451
|
[client, handleFailure]
|
|
1195
1452
|
);
|
|
1196
|
-
const deleteConversation =
|
|
1453
|
+
const deleteConversation = React6.useCallback(
|
|
1197
1454
|
async (id) => {
|
|
1198
1455
|
try {
|
|
1199
1456
|
await client.deleteConversation(id);
|
|
@@ -1205,7 +1462,7 @@ function useAgentChat(config) {
|
|
|
1205
1462
|
},
|
|
1206
1463
|
[client, conversationId, handleFailure, newChat]
|
|
1207
1464
|
);
|
|
1208
|
-
const clearError =
|
|
1465
|
+
const clearError = React6.useCallback(() => setError(null), []);
|
|
1209
1466
|
return {
|
|
1210
1467
|
agentInfo,
|
|
1211
1468
|
clearError,
|
|
@@ -1240,11 +1497,11 @@ function readStored(clientId) {
|
|
|
1240
1497
|
}
|
|
1241
1498
|
}
|
|
1242
1499
|
function useAlwaysApprove(clientId) {
|
|
1243
|
-
const [approved, setApproved] =
|
|
1244
|
-
|
|
1500
|
+
const [approved, setApproved] = React6.useState(() => /* @__PURE__ */ new Set());
|
|
1501
|
+
React6.useEffect(() => {
|
|
1245
1502
|
setApproved(readStored(clientId));
|
|
1246
1503
|
}, [clientId]);
|
|
1247
|
-
const persist =
|
|
1504
|
+
const persist = React6.useCallback(
|
|
1248
1505
|
(next) => {
|
|
1249
1506
|
setApproved(next);
|
|
1250
1507
|
if (typeof window === "undefined") return;
|
|
@@ -1255,8 +1512,8 @@ function useAlwaysApprove(clientId) {
|
|
|
1255
1512
|
},
|
|
1256
1513
|
[clientId]
|
|
1257
1514
|
);
|
|
1258
|
-
const isAlwaysApproved =
|
|
1259
|
-
const setAlwaysApprove =
|
|
1515
|
+
const isAlwaysApproved = React6.useCallback((toolName) => approved.has(toolName), [approved]);
|
|
1516
|
+
const setAlwaysApprove = React6.useCallback(
|
|
1260
1517
|
(toolName) => {
|
|
1261
1518
|
if (approved.has(toolName)) return;
|
|
1262
1519
|
const next = new Set(approved);
|
|
@@ -1265,7 +1522,7 @@ function useAlwaysApprove(clientId) {
|
|
|
1265
1522
|
},
|
|
1266
1523
|
[approved, persist]
|
|
1267
1524
|
);
|
|
1268
|
-
const clearAlwaysApprove =
|
|
1525
|
+
const clearAlwaysApprove = React6.useCallback(
|
|
1269
1526
|
(toolName) => {
|
|
1270
1527
|
if (!approved.has(toolName)) return;
|
|
1271
1528
|
const next = new Set(approved);
|
|
@@ -1277,16 +1534,16 @@ function useAlwaysApprove(clientId) {
|
|
|
1277
1534
|
return { clearAlwaysApprove, isAlwaysApproved, setAlwaysApprove };
|
|
1278
1535
|
}
|
|
1279
1536
|
function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
|
|
1280
|
-
const [uncontrolledOpen, setUncontrolledOpen] =
|
|
1537
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React6.useState(false);
|
|
1281
1538
|
const open = controlledOpen ?? uncontrolledOpen;
|
|
1282
|
-
const setOpen =
|
|
1539
|
+
const setOpen = React6.useCallback(
|
|
1283
1540
|
(next) => {
|
|
1284
1541
|
setUncontrolledOpen(next);
|
|
1285
1542
|
onOpenChange?.(next);
|
|
1286
1543
|
},
|
|
1287
1544
|
[onOpenChange]
|
|
1288
1545
|
);
|
|
1289
|
-
const [agentMode, setAgentMode] =
|
|
1546
|
+
const [agentMode, setAgentMode] = React6.useState(false);
|
|
1290
1547
|
const isDesktop = useMediaQuery("(min-width: 640px)", {
|
|
1291
1548
|
defaultSSRValue: true,
|
|
1292
1549
|
ssr: true
|
|
@@ -1296,14 +1553,14 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
|
|
|
1296
1553
|
const alwaysApprove = useAlwaysApprove(config.clientId);
|
|
1297
1554
|
const { pendingApprovals, resolveApproval, resolvingApprovalId } = chat;
|
|
1298
1555
|
const { isAlwaysApproved } = alwaysApprove;
|
|
1299
|
-
|
|
1556
|
+
React6.useEffect(() => {
|
|
1300
1557
|
if (!open || resolvingApprovalId) return;
|
|
1301
1558
|
const target = pendingApprovals.find((approval) => isAlwaysApproved(approval.tool_name));
|
|
1302
1559
|
if (target) void resolveApproval(target.id, true);
|
|
1303
1560
|
}, [open, pendingApprovals, resolvingApprovalId, isAlwaysApproved, resolveApproval]);
|
|
1304
|
-
const firedWriteIds =
|
|
1561
|
+
const firedWriteIds = React6.useRef(/* @__PURE__ */ new Set());
|
|
1305
1562
|
const { onWriteSuccess } = config;
|
|
1306
|
-
|
|
1563
|
+
React6.useEffect(() => {
|
|
1307
1564
|
if (!onWriteSuccess) return;
|
|
1308
1565
|
for (const message of chat.messages) {
|
|
1309
1566
|
if (message.role !== "tool" || !message.is_write || message.tool_status !== "success") continue;
|
|
@@ -1313,9 +1570,9 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
|
|
|
1313
1570
|
onWriteSuccess({ arguments: args, toolName: message.tool_name ?? "" });
|
|
1314
1571
|
}
|
|
1315
1572
|
}, [chat.messages, onWriteSuccess]);
|
|
1316
|
-
const lastNavigatedMessageId =
|
|
1573
|
+
const lastNavigatedMessageId = React6.useRef(null);
|
|
1317
1574
|
const { messages } = chat;
|
|
1318
|
-
|
|
1575
|
+
React6.useEffect(() => {
|
|
1319
1576
|
if (!open || fullscreen || !config.navigate || !config.pages?.length) return;
|
|
1320
1577
|
const toolMessages = messages.filter((message) => message.role === "tool");
|
|
1321
1578
|
const latest = toolMessages[toolMessages.length - 1];
|
|
@@ -1327,7 +1584,7 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
|
|
|
1327
1584
|
const path = mapping.buildRoute ? mapping.buildRoute(args) : mapping.route;
|
|
1328
1585
|
if (path) config.navigate(path);
|
|
1329
1586
|
}, [config, fullscreen, messages, open]);
|
|
1330
|
-
const closePanel =
|
|
1587
|
+
const closePanel = React6.useCallback(() => {
|
|
1331
1588
|
setOpen(false);
|
|
1332
1589
|
setAgentMode(false);
|
|
1333
1590
|
}, [setOpen]);
|
|
@@ -1406,6 +1663,6 @@ function defineAgentConfig(config) {
|
|
|
1406
1663
|
return config;
|
|
1407
1664
|
}
|
|
1408
1665
|
|
|
1409
|
-
export { AgentApiClient, AgentApiError, AgentComposer, AgentPanel, AgentWidget, DEFAULT_ACCEPT, DocumentCard, MessageItem, TaskChecklist, ToolActivityGroup, ToolApprovalCard, acceptAttribute, acceptedExtensions, defineAgentConfig, deriveDetails, fileUploadEnabled, formatBytes, groupMessages, humanizeToolName, resolveToolPresentation, useAgentChat, useAlwaysApprove, validateFile };
|
|
1666
|
+
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 };
|
|
1410
1667
|
//# sourceMappingURL=index.js.map
|
|
1411
1668
|
//# sourceMappingURL=index.js.map
|