@vegintech/langchain-react-agent 0.0.42 → 0.0.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +257 -154
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -318,8 +318,8 @@ interface MessageContentRendererProps {
|
|
|
318
318
|
components?: Components;
|
|
319
319
|
securityConfig?: MessageListProps["securityConfig"];
|
|
320
320
|
}
|
|
321
|
-
/**
|
|
322
|
-
declare const MessageContentRenderer: React$1.
|
|
321
|
+
/** memo 化:content/components/securityConfig 未变化时跳过重渲染 */
|
|
322
|
+
declare const MessageContentRenderer: React$1.NamedExoticComponent<MessageContentRendererProps>;
|
|
323
323
|
//#endregion
|
|
324
324
|
//#region src/components/DebugPanel.d.ts
|
|
325
325
|
interface DebugPanelProps {
|
|
@@ -351,4 +351,4 @@ type AgentStream = ReturnType<typeof useStream>;
|
|
|
351
351
|
*/
|
|
352
352
|
declare function useAgentStream(options: AgentStreamOptions): AgentStream;
|
|
353
353
|
//#endregion
|
|
354
|
-
export { AgentChat, type AgentChatInputRef, type AgentChatProps, type AgentChatRef, type AgentChatStreamProps, type AgentStream, type AgentStreamOptions, type BackendTool, type ChatMessage, type ContextItem, DebugPanel, type DebugPanelProps, type EmptyStateConfig, type FrontendTool, type InputConfig, type InterruptConfig, type InterruptEvent, type InterruptManagerProps, type InterruptRenderProps, type MessageConfig, type MessageContent, type MessageContentBlock, MessageContentRenderer, type MessageContentRendererProps, type MessageType, type SenderCustomizationProps, type SenderSlotConfig, type SenderSubmitParams, type ToolCallInput, ToolCard, type ToolCardProps, type ToolDefinition, type ToolExecutionRecord, type ToolExecutionStatus, type ToolParameterSchema, type ToolRenderProps, useAgentStream };
|
|
354
|
+
export { AgentChat, type AgentChatInputRef, type AgentChatProps, type AgentChatRef, type AgentChatStreamProps, type AgentStream, type AgentStreamOptions, type BackendTool, type ChatMessage, type ContextItem, DebugPanel, type DebugPanelProps, type EmptyStateConfig, type FrontendTool, type InputConfig, type InterruptConfig, type InterruptEvent, type InterruptManagerProps, type InterruptRenderProps, type MessageConfig, type MessageContent, type MessageContentBlock, MessageContentRenderer, type MessageContentRendererProps, type MessageType, type PreSendResult, type SenderCustomizationProps, type SenderSlotConfig, type SenderSubmitParams, type ToolCallInput, ToolCard, type ToolCardProps, type ToolDefinition, type ToolExecutionRecord, type ToolExecutionStatus, type ToolParameterSchema, type ToolRenderProps, useAgentStream };
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
1
|
+
import React, { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { Actions, Bubble, Sender, Think, ThoughtChain } from "@ant-design/x";
|
|
3
3
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
import { Streamdown } from "streamdown";
|
|
5
5
|
import { useStream } from "@langchain/langgraph-sdk/react";
|
|
6
6
|
//#region src/components/ChatInput.tsx
|
|
7
7
|
const slotConfig = [];
|
|
8
|
-
const
|
|
8
|
+
const ChatInputInner = forwardRef(({ onSend, onStop, isLoading = false, disabled = false, placeholder = "输入消息...", className = "", onPasteFile, footer, skill: externalSkill, header, prefix }, ref) => {
|
|
9
9
|
const senderRef = useRef(null);
|
|
10
10
|
const [internalSkill, setInternalSkill] = useState(externalSkill);
|
|
11
11
|
useImperativeHandle(ref, () => ({
|
|
@@ -16,6 +16,7 @@ const ChatInput = forwardRef(({ onSend, onStop, isLoading = false, disabled = fa
|
|
|
16
16
|
}));
|
|
17
17
|
const handleSubmit = useCallback((message, slotConfig, skillData) => {
|
|
18
18
|
if (message.trim() && !disabled && !isLoading) {
|
|
19
|
+
console.log("handle submit", slotConfig);
|
|
19
20
|
onSend({
|
|
20
21
|
message,
|
|
21
22
|
slotConfig,
|
|
@@ -50,7 +51,9 @@ const ChatInput = forwardRef(({ onSend, onStop, isLoading = false, disabled = fa
|
|
|
50
51
|
})
|
|
51
52
|
});
|
|
52
53
|
});
|
|
53
|
-
|
|
54
|
+
ChatInputInner.displayName = "ChatInput";
|
|
55
|
+
/** 输入框组件:memo 化,避免父组件每次渲染时重建 Sender 内部状态 */
|
|
56
|
+
const ChatInput = memo(ChatInputInner);
|
|
54
57
|
//#endregion
|
|
55
58
|
//#region src/components/ToolCallRenderer.tsx
|
|
56
59
|
/**
|
|
@@ -62,9 +65,10 @@ ChatInput.displayName = "ChatInput";
|
|
|
62
65
|
* 3. 默认渲染:简单的工具卡片样式
|
|
63
66
|
*/
|
|
64
67
|
const ToolCallRenderer = ({ tool, record, isLoading }) => {
|
|
65
|
-
|
|
68
|
+
const customRender = tool?.render;
|
|
69
|
+
if (customRender) return /* @__PURE__ */ jsx("div", {
|
|
66
70
|
className: "tool-call-wrapper",
|
|
67
|
-
children:
|
|
71
|
+
children: /* @__PURE__ */ jsx(customRender, {
|
|
68
72
|
name: record.name,
|
|
69
73
|
args: record.args,
|
|
70
74
|
result: record.result,
|
|
@@ -119,25 +123,25 @@ function isFrontendTool(tool) {
|
|
|
119
123
|
//#endregion
|
|
120
124
|
//#region src/components/ReasoningContent.tsx
|
|
121
125
|
const REASONING_CONTENT_MAX_HEIGHT = 58;
|
|
122
|
-
const ReasoningContent = ({ content })
|
|
126
|
+
const ReasoningContent = memo(function ReasoningContent({ content }) {
|
|
123
127
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
124
128
|
const containerRef = useRef(null);
|
|
125
129
|
const [isOverflowing, setIsOverflowing] = useState(false);
|
|
130
|
+
const checkOverflow = useCallback(() => {
|
|
131
|
+
const node = containerRef.current;
|
|
132
|
+
if (!node) return;
|
|
133
|
+
setIsOverflowing(node.scrollHeight > REASONING_CONTENT_MAX_HEIGHT);
|
|
134
|
+
}, []);
|
|
126
135
|
useEffect(() => {
|
|
127
|
-
const checkOverflow = () => {
|
|
128
|
-
if (containerRef.current) {
|
|
129
|
-
const originalMaxHeight = containerRef.current.style.maxHeight;
|
|
130
|
-
containerRef.current.style.maxHeight = "none";
|
|
131
|
-
const height = containerRef.current.scrollHeight;
|
|
132
|
-
containerRef.current.style.maxHeight = originalMaxHeight;
|
|
133
|
-
setIsOverflowing(height > REASONING_CONTENT_MAX_HEIGHT);
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
136
|
checkOverflow();
|
|
137
|
+
}, [content, checkOverflow]);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
const node = containerRef.current;
|
|
140
|
+
if (!node) return;
|
|
137
141
|
const resizeObserver = new ResizeObserver(checkOverflow);
|
|
138
|
-
|
|
142
|
+
resizeObserver.observe(node);
|
|
139
143
|
return () => resizeObserver.disconnect();
|
|
140
|
-
}, [
|
|
144
|
+
}, [checkOverflow]);
|
|
141
145
|
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
|
|
142
146
|
ref: containerRef,
|
|
143
147
|
style: {
|
|
@@ -164,49 +168,46 @@ const ReasoningContent = ({ content }) => {
|
|
|
164
168
|
}],
|
|
165
169
|
onClick: () => setIsExpanded(true)
|
|
166
170
|
})] });
|
|
167
|
-
};
|
|
171
|
+
});
|
|
172
|
+
ReasoningContent.displayName = "ReasoningContent";
|
|
168
173
|
//#endregion
|
|
169
174
|
//#region src/components/MessageContentRenderer.tsx
|
|
170
175
|
const CustomParagraph = (props) => {
|
|
171
176
|
const { node: _node, ...rest } = props;
|
|
172
177
|
return /* @__PURE__ */ jsx("span", { ...rest });
|
|
173
178
|
};
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
};
|
|
207
|
-
img.src = imageUrl;
|
|
208
|
-
}, [imageUrl]);
|
|
209
|
-
const getImageStyle = () => {
|
|
179
|
+
const STREAMDOWN_CONTROLS = {
|
|
180
|
+
table: {
|
|
181
|
+
copy: false,
|
|
182
|
+
download: false,
|
|
183
|
+
fullscreen: false
|
|
184
|
+
},
|
|
185
|
+
code: {
|
|
186
|
+
copy: false,
|
|
187
|
+
download: false
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* 图片内容块
|
|
192
|
+
*
|
|
193
|
+
* 独立组件承载图片加载状态(修复在条件分支中调用 hooks 的问题)
|
|
194
|
+
*/
|
|
195
|
+
const ImageBlock = memo(function ImageBlock({ imageUrl }) {
|
|
196
|
+
const [imageSize, setImageSize] = React.useState(null);
|
|
197
|
+
React.useEffect(() => {
|
|
198
|
+
const img = new Image();
|
|
199
|
+
img.onload = () => {
|
|
200
|
+
setImageSize({
|
|
201
|
+
width: img.naturalWidth,
|
|
202
|
+
height: img.naturalHeight
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
img.src = imageUrl;
|
|
206
|
+
}, [imageUrl]);
|
|
207
|
+
return /* @__PURE__ */ jsx("img", {
|
|
208
|
+
src: imageUrl,
|
|
209
|
+
alt: "Message content",
|
|
210
|
+
style: useMemo(() => {
|
|
210
211
|
const baseStyle = {
|
|
211
212
|
borderRadius: "8px",
|
|
212
213
|
marginTop: "8px",
|
|
@@ -223,50 +224,55 @@ const ContentBlock = ({ block, index, components, securityConfig }) => {
|
|
|
223
224
|
width: "auto",
|
|
224
225
|
height: "auto"
|
|
225
226
|
};
|
|
226
|
-
|
|
227
|
+
return {
|
|
227
228
|
...baseStyle,
|
|
228
229
|
maxWidth: "100%",
|
|
229
230
|
width: "auto",
|
|
230
231
|
height: "auto"
|
|
231
232
|
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
233
|
+
}, [imageSize])
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
/** 渲染单个内容块(memo 化,内容未变化时跳过重渲染) */
|
|
237
|
+
const ContentBlock = memo(function ContentBlock({ block, components, securityConfig }) {
|
|
238
|
+
const mergedComponents = useMemo(() => ({
|
|
239
|
+
p: CustomParagraph,
|
|
240
|
+
...components
|
|
241
|
+
}), [components]);
|
|
242
|
+
if (block.type === "text" && "text" in block) return /* @__PURE__ */ jsx(Streamdown, {
|
|
243
|
+
components: mergedComponents,
|
|
244
|
+
allowedTags: securityConfig?.allowedTags,
|
|
245
|
+
literalTagContent: securityConfig?.literalTagContent,
|
|
246
|
+
controls: STREAMDOWN_CONTROLS,
|
|
247
|
+
children: String(block.text)
|
|
248
|
+
});
|
|
249
|
+
if (block.type === "image_url" && "image_url" in block) {
|
|
250
|
+
const imageUrl = block.image_url.url;
|
|
251
|
+
return /* @__PURE__ */ jsx(ImageBlock, { imageUrl });
|
|
238
252
|
}
|
|
239
253
|
return null;
|
|
240
|
-
};
|
|
254
|
+
});
|
|
241
255
|
/** 消息内容渲染组件 - 支持字符串和多模态内容块 */
|
|
242
|
-
const
|
|
256
|
+
const MessageContentRendererInner = ({ content, components, securityConfig }) => {
|
|
257
|
+
const mergedComponents = useMemo(() => ({
|
|
258
|
+
p: CustomParagraph,
|
|
259
|
+
...components
|
|
260
|
+
}), [components]);
|
|
243
261
|
if (typeof content === "string") return /* @__PURE__ */ jsx(Streamdown, {
|
|
244
|
-
components:
|
|
245
|
-
p: CustomParagraph,
|
|
246
|
-
...components
|
|
247
|
-
},
|
|
262
|
+
components: mergedComponents,
|
|
248
263
|
allowedTags: securityConfig?.allowedTags,
|
|
249
264
|
literalTagContent: securityConfig?.literalTagContent,
|
|
250
|
-
controls:
|
|
251
|
-
table: {
|
|
252
|
-
copy: false,
|
|
253
|
-
download: false,
|
|
254
|
-
fullscreen: false
|
|
255
|
-
},
|
|
256
|
-
code: {
|
|
257
|
-
copy: false,
|
|
258
|
-
download: false
|
|
259
|
-
}
|
|
260
|
-
},
|
|
265
|
+
controls: STREAMDOWN_CONTROLS,
|
|
261
266
|
children: content
|
|
262
267
|
});
|
|
263
268
|
return /* @__PURE__ */ jsx(Fragment, { children: content.map((block, index) => /* @__PURE__ */ jsx(ContentBlock, {
|
|
264
269
|
block,
|
|
265
|
-
index,
|
|
266
270
|
components,
|
|
267
271
|
securityConfig
|
|
268
272
|
}, index)) });
|
|
269
273
|
};
|
|
274
|
+
/** memo 化:content/components/securityConfig 未变化时跳过重渲染 */
|
|
275
|
+
const MessageContentRenderer = memo(MessageContentRendererInner);
|
|
270
276
|
//#endregion
|
|
271
277
|
//#region src/components/WaveLoading.tsx
|
|
272
278
|
/**
|
|
@@ -308,6 +314,32 @@ const WaveLoading = ({ color = "#1890ff" }) => {
|
|
|
308
314
|
};
|
|
309
315
|
//#endregion
|
|
310
316
|
//#region src/components/MessageList.tsx
|
|
317
|
+
/** 浅比较依赖数组(用于判断渲染结果是否可以复用) */
|
|
318
|
+
const depsEqual = (a, b) => {
|
|
319
|
+
if (a.length !== b.length) return false;
|
|
320
|
+
for (let i = 0; i < a.length; i++) if (!Object.is(a[i], b[i])) return false;
|
|
321
|
+
return true;
|
|
322
|
+
};
|
|
323
|
+
/**
|
|
324
|
+
* 依赖不变时复用缓存值
|
|
325
|
+
*
|
|
326
|
+
* 保证 React 元素/渲染项的引用稳定,让 React 的 bailout 与 antd Bubble 的 memo 生效,
|
|
327
|
+
* 流式更新时只重建真正变化的消息
|
|
328
|
+
*/
|
|
329
|
+
const getCachedValue = (cache, key, deps, build) => {
|
|
330
|
+
const cached = cache.get(key);
|
|
331
|
+
if (cached && depsEqual(cached.deps, deps)) return cached.value;
|
|
332
|
+
const value = build();
|
|
333
|
+
cache.set(key, {
|
|
334
|
+
deps,
|
|
335
|
+
value
|
|
336
|
+
});
|
|
337
|
+
return value;
|
|
338
|
+
};
|
|
339
|
+
/** 清理已不存在的缓存项(如切换会话后) */
|
|
340
|
+
const sweepCache = (cache, aliveKeys) => {
|
|
341
|
+
for (const key of cache.keys()) if (!aliveKeys.has(key)) cache.delete(key);
|
|
342
|
+
};
|
|
311
343
|
const renderMessageContent = (message, isLastMessage, isLoading, tools, toolExecutions, components, securityConfig) => {
|
|
312
344
|
const hasToolCalls = message.toolCalls && message.toolCalls.length > 0;
|
|
313
345
|
const isContentEmpty = !message.content || (typeof message.content === "string" ? message.content === "" : message.content.length === 0);
|
|
@@ -342,14 +374,6 @@ const renderMessageContent = (message, isLastMessage, isLoading, tools, toolExec
|
|
|
342
374
|
]
|
|
343
375
|
}, message.id);
|
|
344
376
|
};
|
|
345
|
-
const toBubbleItem = (message, isLastMessage, isLoading, tools, toolExecutions, components, securityConfig) => {
|
|
346
|
-
return {
|
|
347
|
-
key: message.id,
|
|
348
|
-
role: message.type === "human" ? "user" : "ai",
|
|
349
|
-
content: renderMessageContent(message, isLastMessage, isLoading, tools, toolExecutions, components, securityConfig),
|
|
350
|
-
placement: message.type === "human" ? "end" : "start"
|
|
351
|
-
};
|
|
352
|
-
};
|
|
353
377
|
const renderToolCalls = (toolCalls, tools, toolExecutions, isLoading) => {
|
|
354
378
|
return toolCalls.map((call) => {
|
|
355
379
|
const tool = findTool(tools, call.name);
|
|
@@ -395,18 +419,32 @@ const roleConfig = {
|
|
|
395
419
|
};
|
|
396
420
|
const MessageList = ({ messages, isLoading = false, className = "", tools, toolExecutions, components, securityConfig, loadingColor, interruptRender }) => {
|
|
397
421
|
const reasoningCacheRef = useRef(/* @__PURE__ */ new Map());
|
|
422
|
+
const fixedMessageCacheRef = useRef(/* @__PURE__ */ new Map());
|
|
423
|
+
const messageElementCacheRef = useRef(/* @__PURE__ */ new Map());
|
|
424
|
+
const bubbleItemCacheRef = useRef(/* @__PURE__ */ new Map());
|
|
398
425
|
const processedMessages = useMemo(() => {
|
|
399
426
|
const cache = reasoningCacheRef.current;
|
|
427
|
+
const fixedCache = fixedMessageCacheRef.current;
|
|
400
428
|
return messages.map((message) => {
|
|
401
|
-
if (message.type
|
|
402
|
-
if (message.
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
...message,
|
|
406
|
-
reasoningContent: cachedReasoning
|
|
407
|
-
};
|
|
429
|
+
if (message.type !== "ai") return message;
|
|
430
|
+
if (message.reasoningContent) {
|
|
431
|
+
cache.set(message.id, message.reasoningContent);
|
|
432
|
+
return message;
|
|
408
433
|
}
|
|
409
|
-
|
|
434
|
+
const cachedReasoning = cache.get(message.id);
|
|
435
|
+
if (!cachedReasoning) return message;
|
|
436
|
+
const existing = fixedCache.get(message.id);
|
|
437
|
+
if (existing && existing.raw === message && existing.reasoning === cachedReasoning) return existing.fixed;
|
|
438
|
+
const fixed = {
|
|
439
|
+
...message,
|
|
440
|
+
reasoningContent: cachedReasoning
|
|
441
|
+
};
|
|
442
|
+
fixedCache.set(message.id, {
|
|
443
|
+
raw: message,
|
|
444
|
+
reasoning: cachedReasoning,
|
|
445
|
+
fixed
|
|
446
|
+
});
|
|
447
|
+
return fixed;
|
|
410
448
|
});
|
|
411
449
|
}, [messages]);
|
|
412
450
|
const groupedItems = useMemo(() => {
|
|
@@ -421,28 +459,48 @@ const MessageList = ({ messages, isLoading = false, className = "", tools, toolE
|
|
|
421
459
|
}
|
|
422
460
|
return groups;
|
|
423
461
|
}, [processedMessages]);
|
|
462
|
+
const getMessageElement = (message, isLastMessage) => {
|
|
463
|
+
const hasToolCalls = !!(message.toolCalls && message.toolCalls.length > 0);
|
|
464
|
+
const deps = [
|
|
465
|
+
message,
|
|
466
|
+
isLastMessage,
|
|
467
|
+
components,
|
|
468
|
+
securityConfig
|
|
469
|
+
];
|
|
470
|
+
if (isLastMessage || hasToolCalls) deps.push(isLoading);
|
|
471
|
+
if (hasToolCalls) {
|
|
472
|
+
deps.push(tools);
|
|
473
|
+
for (const call of message.toolCalls) {
|
|
474
|
+
const execution = toolExecutions.get(call.id);
|
|
475
|
+
deps.push(call.id, execution?.status, execution?.result, execution?.error);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return getCachedValue(messageElementCacheRef.current, message.id, deps, () => renderMessageContent(message, isLastMessage, isLoading, tools, toolExecutions, components, securityConfig));
|
|
479
|
+
};
|
|
424
480
|
const items = groupedItems.map((group, groupIndex) => {
|
|
425
481
|
const isLastGroup = groupIndex === groupedItems.length - 1;
|
|
426
482
|
const isLastAiGroup = isLastGroup && group.type === "ai";
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
483
|
+
const cacheKey = group.messages[0].id;
|
|
484
|
+
const elements = group.messages.map((message, msgIndex) => getMessageElement(message, isLastGroup && msgIndex === group.messages.length - 1));
|
|
485
|
+
const includeInterrupt = !!(isLastAiGroup && interruptRender);
|
|
486
|
+
const deps = [
|
|
487
|
+
isLastGroup,
|
|
488
|
+
isLastAiGroup,
|
|
489
|
+
...elements
|
|
490
|
+
];
|
|
491
|
+
if (includeInterrupt) deps.push(interruptRender);
|
|
492
|
+
return getCachedValue(bubbleItemCacheRef.current, cacheKey, deps, () => {
|
|
493
|
+
const content = includeInterrupt ? /* @__PURE__ */ jsxs(Fragment, { children: [elements, interruptRender?.()] }) : elements.length === 1 ? elements[0] : /* @__PURE__ */ jsx(Fragment, { children: elements });
|
|
494
|
+
return {
|
|
495
|
+
key: cacheKey,
|
|
496
|
+
role: group.type === "user" ? "user" : "ai",
|
|
497
|
+
content,
|
|
498
|
+
placement: group.type === "user" ? "end" : "start"
|
|
433
499
|
};
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
const mergedContent = /* @__PURE__ */ jsxs(Fragment, { children: [group.messages.map((message, msgIndex) => {
|
|
437
|
-
return renderMessageContent(message, isLastGroup && msgIndex === group.messages.length - 1, isLoading, tools, toolExecutions, components, securityConfig);
|
|
438
|
-
}), isLastAiGroup && interruptRender?.()] });
|
|
439
|
-
return {
|
|
440
|
-
key: group.messages.map((m) => m.id).join("-"),
|
|
441
|
-
role: "ai",
|
|
442
|
-
content: mergedContent,
|
|
443
|
-
placement: "start"
|
|
444
|
-
};
|
|
500
|
+
});
|
|
445
501
|
});
|
|
502
|
+
if (messageElementCacheRef.current.size > processedMessages.length) sweepCache(messageElementCacheRef.current, new Set(processedMessages.map((m) => m.id)));
|
|
503
|
+
if (bubbleItemCacheRef.current.size > groupedItems.length) sweepCache(bubbleItemCacheRef.current, new Set(groupedItems.map((group) => group.messages[0].id)));
|
|
446
504
|
const hasVisibleContent = (msg) => {
|
|
447
505
|
if (!msg) return false;
|
|
448
506
|
const hasContent = msg.content && (typeof msg.content === "string" ? msg.content !== "" : msg.content.length > 0);
|
|
@@ -577,9 +635,13 @@ function useToolExecution({ tools, toolCalls, isLoading = false, onExecutionChan
|
|
|
577
635
|
const batchSubmittedRef = useRef(false);
|
|
578
636
|
const cancelledCallIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
579
637
|
const cancelledExecutionsRef = useRef(/* @__PURE__ */ new Set());
|
|
638
|
+
const toolCallsRef = useRef(toolCalls);
|
|
639
|
+
useEffect(() => {
|
|
640
|
+
toolCallsRef.current = toolCalls;
|
|
641
|
+
}, [toolCalls]);
|
|
580
642
|
useEffect(() => {
|
|
581
643
|
if (!completedToolResults || completedToolResults.size === 0) return;
|
|
582
|
-
completedToolResults.forEach(({ result
|
|
644
|
+
completedToolResults.forEach(({ result }, callId) => {
|
|
583
645
|
if (notifiedCompletedRef.current.has(callId)) return;
|
|
584
646
|
if (cancelledExecutionsRef.current.has(callId)) {
|
|
585
647
|
notifiedCompletedRef.current.add(callId);
|
|
@@ -733,7 +795,7 @@ function useToolExecution({ tools, toolCalls, isLoading = false, onExecutionChan
|
|
|
733
795
|
cancelledExecutionsRef.current.add(callId);
|
|
734
796
|
batchCallIdsRef.current.delete(callId);
|
|
735
797
|
}
|
|
736
|
-
const call =
|
|
798
|
+
const call = toolCallsRef.current.find((c) => c.id === callId);
|
|
737
799
|
if (call) onExecutionChange?.({
|
|
738
800
|
callId,
|
|
739
801
|
name: call.name,
|
|
@@ -741,7 +803,7 @@ function useToolExecution({ tools, toolCalls, isLoading = false, onExecutionChan
|
|
|
741
803
|
status: "cancelled"
|
|
742
804
|
});
|
|
743
805
|
}
|
|
744
|
-
}, [
|
|
806
|
+
}, [onExecutionChange]);
|
|
745
807
|
useEffect(() => {
|
|
746
808
|
processToolCalls();
|
|
747
809
|
}, [useMemo(() => {
|
|
@@ -826,37 +888,72 @@ function toChatMessage(message, toolResults) {
|
|
|
826
888
|
};
|
|
827
889
|
}
|
|
828
890
|
/**
|
|
829
|
-
*
|
|
891
|
+
* 创建增量消息处理器
|
|
892
|
+
*
|
|
893
|
+
* 与每次全量重建不同,处理器按消息对象引用做缓存:
|
|
894
|
+
* - 未变化的原始消息会复用同一个 ChatMessage 引用(流式场景下大部分消息不变,下游 memo 才能生效)
|
|
895
|
+
* - ToolMessage 的 JSON 解析结果逐条缓存,避免每个流式分片重复解析
|
|
896
|
+
* - 工具结果集合未变化时返回同一个 Map 引用
|
|
830
897
|
*/
|
|
831
|
-
function
|
|
832
|
-
const
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
898
|
+
function createMessageProcessor() {
|
|
899
|
+
const toolParseCache = /* @__PURE__ */ new WeakMap();
|
|
900
|
+
const chatCache = /* @__PURE__ */ new WeakMap();
|
|
901
|
+
let toolResults = /* @__PURE__ */ new Map();
|
|
902
|
+
return { process(rawMessages) {
|
|
903
|
+
const nextToolResults = /* @__PURE__ */ new Map();
|
|
904
|
+
for (const message of rawMessages) {
|
|
905
|
+
if (message.type !== "tool") continue;
|
|
906
|
+
let parsed = toolParseCache.get(message);
|
|
907
|
+
if (!parsed) {
|
|
908
|
+
const callId = message.tool_call_id || message.additional_kwargs?.tool_call_id;
|
|
909
|
+
if (!callId) continue;
|
|
910
|
+
const textContent = extractTextFromContent(extractContent(message));
|
|
911
|
+
const status = message.status;
|
|
912
|
+
let result;
|
|
913
|
+
try {
|
|
914
|
+
result = JSON.parse(textContent);
|
|
915
|
+
} catch {
|
|
916
|
+
result = textContent;
|
|
917
|
+
}
|
|
918
|
+
parsed = {
|
|
919
|
+
callId,
|
|
920
|
+
entry: {
|
|
921
|
+
result,
|
|
922
|
+
status
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
toolParseCache.set(message, parsed);
|
|
848
926
|
}
|
|
927
|
+
nextToolResults.set(parsed.callId, parsed.entry);
|
|
849
928
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
929
|
+
let toolResultsChanged = nextToolResults.size !== toolResults.size;
|
|
930
|
+
if (!toolResultsChanged) {
|
|
931
|
+
for (const [callId, entry] of nextToolResults) if (toolResults.get(callId) !== entry) {
|
|
932
|
+
toolResultsChanged = true;
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (toolResultsChanged) toolResults = nextToolResults;
|
|
937
|
+
const messages = [];
|
|
938
|
+
for (const message of rawMessages) {
|
|
939
|
+
let chat;
|
|
940
|
+
if (chatCache.has(message)) {
|
|
941
|
+
chat = chatCache.get(message) ?? null;
|
|
942
|
+
if (chat && chat.toolCalls && toolResultsChanged) {
|
|
943
|
+
chat = toChatMessage(message, toolResults);
|
|
944
|
+
chatCache.set(message, chat);
|
|
945
|
+
}
|
|
946
|
+
} else {
|
|
947
|
+
chat = toChatMessage(message, toolResults);
|
|
948
|
+
chatCache.set(message, chat);
|
|
949
|
+
}
|
|
950
|
+
if (chat) messages.push(chat);
|
|
951
|
+
}
|
|
952
|
+
return {
|
|
953
|
+
messages,
|
|
954
|
+
toolResults
|
|
955
|
+
};
|
|
956
|
+
} };
|
|
860
957
|
}
|
|
861
958
|
//#endregion
|
|
862
959
|
//#region src/utils/injectStyles.ts
|
|
@@ -1045,6 +1142,11 @@ injectStyles();
|
|
|
1045
1142
|
const AgentChat = forwardRef(({ stream, className = "", tools, contexts, messageConfig, inputConfig, interruptConfig, agentState, welcome }, ref) => {
|
|
1046
1143
|
const { onPreSend, ...chatInputConfig } = inputConfig || {};
|
|
1047
1144
|
const chatInputRef = useRef(null);
|
|
1145
|
+
const [messageProcessor] = useState(() => createMessageProcessor());
|
|
1146
|
+
const streamRef = useRef(stream);
|
|
1147
|
+
useEffect(() => {
|
|
1148
|
+
streamRef.current = stream;
|
|
1149
|
+
});
|
|
1048
1150
|
useImperativeHandle(ref, () => ({
|
|
1049
1151
|
input: chatInputRef.current,
|
|
1050
1152
|
setSkill: (skill) => chatInputRef.current?.setSkill?.(skill),
|
|
@@ -1052,18 +1154,19 @@ const AgentChat = forwardRef(({ stream, className = "", tools, contexts, message
|
|
|
1052
1154
|
focusInput: () => chatInputRef.current?.focus?.()
|
|
1053
1155
|
}));
|
|
1054
1156
|
const interruptEvent = stream.interrupt;
|
|
1157
|
+
const submitLatest = useCallback((values, options) => streamRef.current.submit(values, options), []);
|
|
1055
1158
|
const { renderInterrupt: interruptRender } = useInterrupt({
|
|
1056
1159
|
interrupt: interruptEvent ? {
|
|
1057
1160
|
value: interruptEvent.value,
|
|
1058
1161
|
id: interruptEvent.id
|
|
1059
1162
|
} : null,
|
|
1060
1163
|
config: interruptConfig,
|
|
1061
|
-
onSubmit:
|
|
1164
|
+
onSubmit: submitLatest
|
|
1062
1165
|
});
|
|
1063
1166
|
const [toolExecutions, setToolExecutions] = useState(/* @__PURE__ */ new Map());
|
|
1064
1167
|
const { messages, toolResults } = useMemo(() => {
|
|
1065
|
-
return
|
|
1066
|
-
}, [stream.messages]);
|
|
1168
|
+
return messageProcessor.process(stream.messages);
|
|
1169
|
+
}, [messageProcessor, stream.messages]);
|
|
1067
1170
|
const allToolCalls = useMemo(() => {
|
|
1068
1171
|
return messages.flatMap((msg) => msg.toolCalls || []);
|
|
1069
1172
|
}, [messages]);
|
|
@@ -1083,8 +1186,12 @@ const AgentChat = forwardRef(({ stream, className = "", tools, contexts, message
|
|
|
1083
1186
|
return false;
|
|
1084
1187
|
}, [toolExecutions, frontendToolNames]);
|
|
1085
1188
|
const isLoading = stream.isLoading || hasToolExecuting;
|
|
1189
|
+
const securityConfig = useMemo(() => ({
|
|
1190
|
+
allowedTags: messageConfig?.allowedTags,
|
|
1191
|
+
literalTagContent: messageConfig?.literalTagContent
|
|
1192
|
+
}), [messageConfig?.allowedTags, messageConfig?.literalTagContent]);
|
|
1086
1193
|
const submitToStream = useCallback(async (submitMessages) => {
|
|
1087
|
-
await
|
|
1194
|
+
await streamRef.current.submit({
|
|
1088
1195
|
...agentState,
|
|
1089
1196
|
messages: submitMessages,
|
|
1090
1197
|
agentkit: {
|
|
@@ -1104,7 +1211,6 @@ const AgentChat = forwardRef(({ stream, className = "", tools, contexts, message
|
|
|
1104
1211
|
}
|
|
1105
1212
|
});
|
|
1106
1213
|
}, [
|
|
1107
|
-
stream,
|
|
1108
1214
|
frontendTools,
|
|
1109
1215
|
contexts,
|
|
1110
1216
|
agentState
|
|
@@ -1137,8 +1243,8 @@ const AgentChat = forwardRef(({ stream, className = "", tools, contexts, message
|
|
|
1137
1243
|
});
|
|
1138
1244
|
const handleStop = useCallback(async () => {
|
|
1139
1245
|
cancelToolExecution();
|
|
1140
|
-
await
|
|
1141
|
-
}, [
|
|
1246
|
+
await streamRef.current.stop();
|
|
1247
|
+
}, [cancelToolExecution]);
|
|
1142
1248
|
const shouldRenderWelcome = messages.length === 0 && !isLoading && welcome;
|
|
1143
1249
|
return /* @__PURE__ */ jsxs("div", {
|
|
1144
1250
|
className: `agent-chat-container ${className}`,
|
|
@@ -1151,10 +1257,7 @@ const AgentChat = forwardRef(({ stream, className = "", tools, contexts, message
|
|
|
1151
1257
|
tools,
|
|
1152
1258
|
toolExecutions,
|
|
1153
1259
|
components: messageConfig?.components,
|
|
1154
|
-
securityConfig
|
|
1155
|
-
allowedTags: messageConfig?.allowedTags,
|
|
1156
|
-
literalTagContent: messageConfig?.literalTagContent
|
|
1157
|
-
},
|
|
1260
|
+
securityConfig,
|
|
1158
1261
|
loadingColor: messageConfig?.loadingColor,
|
|
1159
1262
|
interruptRender
|
|
1160
1263
|
}), /* @__PURE__ */ jsx(ChatInput, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vegintech/langchain-react-agent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.43",
|
|
4
4
|
"description": "LangChain Agent UI component library for React",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"prepublishOnly": "vp run build"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@ant-design/x": "^2.
|
|
26
|
+
"@ant-design/x": "^2.8.0",
|
|
27
27
|
"@langchain/langgraph-sdk": "^1.8.7",
|
|
28
|
-
"streamdown": "^2.
|
|
28
|
+
"streamdown": "^2.6.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^25.5.0",
|