@vegintech/langchain-react-agent 0.0.3 → 0.0.5

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/README.md CHANGED
@@ -176,4 +176,4 @@ import type {
176
176
  MessageConfig,
177
177
  InputConfig,
178
178
  } from "@vegintech/langchain-react-agent";
179
- ```
179
+ ```
package/dist/index.d.mts CHANGED
@@ -2,9 +2,57 @@ import * as react from "react";
2
2
  import { ReactNode } from "react";
3
3
  import { Components } from "streamdown";
4
4
  import { BaseNode, InsertPosition, NodeRender, SenderProps, SkillType, SlotConfigType } from "@ant-design/x/es/sender/interface.ts";
5
- import { BaseMessage } from "@langchain/core/messages";
6
5
 
7
6
  //#region src/types.d.ts
7
+ /** ToolCard 组件 Props */
8
+ interface ToolCardProps {
9
+ /**
10
+ * 前缀文本(中文,使用小字体)
11
+ */
12
+ prefix: string;
13
+ /**
14
+ * 正文内容(英文/文件名,保持默认大小)
15
+ */
16
+ content?: string;
17
+ /**
18
+ * 左侧图标
19
+ */
20
+ icon: ReactNode;
21
+ }
22
+ /** Interrupt 事件数据结构 */
23
+ interface InterruptEvent {
24
+ /** Interrupt 的值,可以是任意类型(由 LangChain 后端决定) */
25
+ value: unknown;
26
+ /** Interrupt 的唯一标识 */
27
+ id: string;
28
+ }
29
+ /** Interrupt 渲染函数的参数 */
30
+ interface InterruptRenderProps {
31
+ /** Interrupt 事件数据 */
32
+ event: InterruptEvent;
33
+ /** 解决 Interrupt 的回调函数,传入响应值 */
34
+ resolve: (response: unknown) => void;
35
+ }
36
+ /** Interrupt 处理配置 */
37
+ interface InterruptConfig {
38
+ /** 自定义 Interrupt 渲染函数 */
39
+ render: (props: InterruptRenderProps) => ReactNode;
40
+ }
41
+ /** InterruptManager Hook 的 Props */
42
+ interface InterruptManagerProps {
43
+ /** 当前 interrupt 事件 */
44
+ interrupt: InterruptEvent | null;
45
+ /** Interrupt 处理配置 */
46
+ config?: InterruptConfig;
47
+ /** 提交回调 */
48
+ onSubmit: (values: {
49
+ [x: string]: unknown;
50
+ } | null | undefined, options?: {
51
+ command?: {
52
+ resume: unknown;
53
+ };
54
+ }) => Promise<void> | void;
55
+ }
8
56
  /** Context Item,用于传递上下文信息 */
9
57
  interface ContextItem {
10
58
  /** 描述信息 */
@@ -132,6 +180,15 @@ interface BackendTool<TArgs = Record<string, unknown>> extends ToolBase<TArgs> {
132
180
  }
133
181
  /** 工具定义联合类型 */
134
182
  type ToolDefinition<TArgs = Record<string, unknown>> = FrontendTool<TArgs> | BackendTool<TArgs>;
183
+ /** 空状态配置 */
184
+ interface EmptyStateConfig {
185
+ /** 空状态标题 */
186
+ title?: string;
187
+ /** 空状态描述 */
188
+ description?: string;
189
+ /** 自定义渲染 */
190
+ render?: () => ReactNode;
191
+ }
135
192
  /** 消息渲染配置 */
136
193
  interface MessageConfig {
137
194
  /** Markdown 自定义组件 */
@@ -142,6 +199,8 @@ interface MessageConfig {
142
199
  literalTagContent?: string[];
143
200
  /** Loading 动画颜色,支持 CSS 颜色值 */
144
201
  loadingColor?: string;
202
+ /** 空状态配置 */
203
+ emptyState?: EmptyStateConfig;
145
204
  }
146
205
  /** 输入框配置 */
147
206
  interface InputConfig extends SenderCustomizationProps {
@@ -162,6 +221,7 @@ interface AgentChatProps {
162
221
  messageConfig?: MessageConfig;
163
222
  inputConfig?: InputConfig;
164
223
  onError?: (error: Error) => void;
224
+ interruptConfig?: InterruptConfig;
165
225
  }
166
226
  /** AgentChat 组件对外暴露的方法 */
167
227
  interface AgentChatRef {
@@ -175,31 +235,14 @@ interface AgentChatRef {
175
235
  focusInput: () => void;
176
236
  }
177
237
  //#endregion
178
- //#region src/AgentChat.d.ts
238
+ //#region src/components/AgentChat.d.ts
179
239
  declare const AgentChat: react.ForwardRefExoticComponent<AgentChatProps & react.RefAttributes<AgentChatRef>>;
180
240
  //#endregion
181
- //#region src/messageUtils.d.ts
182
- /**
183
- * 从 BaseMessage 中提取 tool_calls
184
- */
185
- declare function extractToolCalls(message: BaseMessage): ToolCallInput[] | undefined;
186
- /**
187
- * 从 BaseMessage 中提取文本内容
188
- */
189
- declare function extractContent(message: BaseMessage): string;
241
+ //#region src/components/ToolCard.d.ts
190
242
  /**
191
- * BaseMessage 转换为 ChatMessage
243
+ * ToolCard 组件 - 用于展示工具调用结果
244
+ * 样式:圆角矩形框,左侧 icon,label 分两行显示(prefix 小字体,content 默认字体)
192
245
  */
193
- declare function toChatMessage(message: BaseMessage, toolResults: Map<string, unknown>): ChatMessage | null;
194
- /**
195
- * 预处理消息列表:建立 tool_call_id -> result 映射,并过滤 ToolMessage
196
- */
197
- declare function processMessages(rawMessages: BaseMessage[]): {
198
- messages: ChatMessage[];
199
- toolResults: Map<string, unknown>;
200
- };
201
- //#endregion
202
- //#region src/injectStyles.d.ts
203
- declare function injectStyles(): void;
246
+ declare const ToolCard: React.FC<ToolCardProps>;
204
247
  //#endregion
205
- export { AgentChat, type AgentChatInputRef, type AgentChatProps, type AgentChatRef, type BackendTool, type ChatMessage, type ContextItem, type FrontendTool, type InputConfig, type MessageConfig, type MessageType, type SenderCustomizationProps, type SenderSlotConfig, type SenderSubmitParams, type ToolCallInput, type ToolDefinition, type ToolExecutionRecord, type ToolExecutionStatus, type ToolParameterSchema, type ToolRenderProps, extractContent, extractToolCalls, injectStyles, processMessages, toChatMessage };
248
+ export { AgentChat, type AgentChatInputRef, type AgentChatProps, type AgentChatRef, type BackendTool, type ChatMessage, type ContextItem, type EmptyStateConfig, type FrontendTool, type InputConfig, type InterruptConfig, type InterruptEvent, type InterruptManagerProps, type InterruptRenderProps, type MessageConfig, type MessageType, type SenderCustomizationProps, type SenderSlotConfig, type SenderSubmitParams, type ToolCallInput, ToolCard, type ToolCardProps, type ToolDefinition, type ToolExecutionRecord, type ToolExecutionStatus, type ToolParameterSchema, type ToolRenderProps };
package/dist/index.mjs CHANGED
@@ -1,117 +1,9 @@
1
1
  import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
2
2
  import { useStream } from "@langchain/react";
3
- import { Bubble, Sender, Think } from "@ant-design/x";
3
+ import { Actions, Bubble, Sender, Think } from "@ant-design/x";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { Streamdown } from "streamdown";
6
- //#region src/injectStyles.ts
7
- const styles = `
8
- /* Agent Chat Container */
9
- .agent-chat-container {
10
- display: flex;
11
- flex-direction: column;
12
- height: 100%;
13
- background-color: transparent;
14
- overflow: hidden;
15
- }
16
-
17
- /* Message List */
18
- .agent-message-list {
19
- flex: 1;
20
- overflow-y: auto;
21
- display: flex;
22
- flex-direction: column;
23
- }
24
-
25
- .agent-message-list .empty {
26
- display: flex;
27
- align-items: center;
28
- justify-content: center;
29
- }
30
-
31
- .agent-message-list .ant-think-status-text {
32
- font-size: 13px;
33
- }
34
-
35
- .agent-message-empty {
36
- color: #8b8d91;
37
- font-size: 14px;
38
- text-align: center;
39
- }
40
-
41
- @keyframes agent-loading-bounce {
42
- 0%,
43
- 80%,
44
- 100% {
45
- transform: scale(0);
46
- }
47
- 40% {
48
- transform: scale(1);
49
- }
50
- }
51
-
52
- /* Chat Input */
53
- .agent-chat-input-container {
54
- padding: 12px 4px;
55
- background-color: transparent;
56
- }
57
-
58
- /* Tool Calls Container */
59
- .tool-calls-container {
60
- display: flex;
61
- flex-direction: column;
62
- gap: 8px;
63
- margin-top: 8px;
64
- }
65
-
66
- /* Tool Call Default Style */
67
- .tool-call-default {
68
- display: flex;
69
- flex-direction: column;
70
- gap: 4px;
71
- padding: 6px 0;
72
- }
73
-
74
- .tool-call-header {
75
- display: flex;
76
- align-items: center;
77
- gap: 8px;
78
- }
79
-
80
- .tool-call-name {
81
- font-weight: 500;
82
- font-size: 13px;
83
- color: #fff;
84
- }
85
-
86
- .tool-call-status {
87
- font-size: 12px;
88
- color: #8b8d91;
89
- margin-left: auto;
90
- }
91
-
92
- .tool-call-error {
93
- display: flex;
94
- align-items: center;
95
- gap: 4px;
96
- font-size: 12px;
97
- color: #ff4d4f;
98
- }
99
-
100
- .tool-call-wrapper {
101
- width: 100%;
102
- }
103
- `;
104
- let injected = false;
105
- function injectStyles() {
106
- if (injected || typeof document === "undefined") return;
107
- const styleElement = document.createElement("style");
108
- styleElement.textContent = styles;
109
- document.head.appendChild(styleElement);
110
- injected = true;
111
- }
112
- //#endregion
113
- //#region src/ChatInput.tsx
114
- injectStyles();
6
+ //#region src/components/ChatInput.tsx
115
7
  const slotConfig = [];
116
8
  const ChatInput = forwardRef(({ onSend, onStop, isLoading = false, disabled = false, placeholder = "输入消息...", className = "", footer, skill: externalSkill, header, prefix }, ref) => {
117
9
  const senderRef = useRef(null);
@@ -159,7 +51,7 @@ const ChatInput = forwardRef(({ onSend, onStop, isLoading = false, disabled = fa
159
51
  });
160
52
  ChatInput.displayName = "ChatInput";
161
53
  //#endregion
162
- //#region src/ToolCallRenderer.tsx
54
+ //#region src/components/ToolCallRenderer.tsx
163
55
  /**
164
56
  * 工具调用渲染组件
165
57
  *
@@ -209,125 +101,7 @@ const DefaultToolCallRenderer = ({ record }) => {
209
101
  });
210
102
  };
211
103
  //#endregion
212
- //#region src/ToolManager.tsx
213
- /**
214
- * 工具管理器组件
215
- *
216
- * 职责:
217
- * 1. 管理工具执行状态
218
- * 2. 自动执行前端工具(避免重复执行)
219
- * 3. 通知外部执行状态变化
220
- *
221
- * 这是一个无 UI 组件,只负责逻辑处理
222
- */
223
- const ToolManager = ({ tools, toolCalls, isLoading = false, onExecutionChange, onToolResult, completedToolResults }) => {
224
- const executedCallsRef = useRef(/* @__PURE__ */ new Set());
225
- const executingCallsRef = useRef(/* @__PURE__ */ new Set());
226
- const pendingNotifiedRef = useRef(/* @__PURE__ */ new Set());
227
- const initializedRef = useRef(false);
228
- useEffect(() => {
229
- if (initializedRef.current || !completedToolResults || completedToolResults.size === 0) return;
230
- initializedRef.current = true;
231
- completedToolResults.forEach((result, callId) => {
232
- executedCallsRef.current.add(callId);
233
- const call = toolCalls.find((c) => c.id === callId);
234
- if (call) onExecutionChange?.({
235
- callId,
236
- name: call.name,
237
- arguments: call.arguments,
238
- status: "success",
239
- result
240
- });
241
- });
242
- }, [completedToolResults]);
243
- /**
244
- * 执行单个前端工具
245
- */
246
- const executeFrontendTool = useCallback(async (tool, call) => {
247
- const callId = call.id;
248
- executingCallsRef.current.add(callId);
249
- onExecutionChange?.({
250
- callId,
251
- name: call.name,
252
- arguments: call.arguments,
253
- status: "running"
254
- });
255
- try {
256
- const result = await tool.execute(call.arguments);
257
- onExecutionChange?.({
258
- callId,
259
- name: call.name,
260
- arguments: call.arguments,
261
- status: "success",
262
- result
263
- });
264
- onToolResult?.(callId, call.name, result);
265
- } catch (error) {
266
- const errorMessage = error instanceof Error ? error.message : String(error);
267
- onExecutionChange?.({
268
- callId,
269
- name: call.name,
270
- arguments: call.arguments,
271
- status: "error",
272
- error: errorMessage
273
- });
274
- } finally {
275
- executingCallsRef.current.delete(callId);
276
- executedCallsRef.current.add(callId);
277
- }
278
- }, [onExecutionChange, onToolResult]);
279
- /**
280
- * 处理工具调用
281
- */
282
- const processToolCalls = useCallback(async () => {
283
- if (!tools) return;
284
- for (const call of toolCalls) {
285
- const callId = call.id;
286
- if (executedCallsRef.current.has(callId) || executingCallsRef.current.has(callId)) continue;
287
- const tool = tools.find((t) => t.name === call.name);
288
- if (isLoading) {
289
- if (!pendingNotifiedRef.current.has(callId)) {
290
- pendingNotifiedRef.current.add(callId);
291
- onExecutionChange?.({
292
- callId,
293
- name: call.name,
294
- arguments: call.arguments,
295
- status: "pending"
296
- });
297
- }
298
- continue;
299
- }
300
- if (!tool) {
301
- onExecutionChange?.({
302
- callId,
303
- name: call.name,
304
- arguments: call.arguments,
305
- status: "pending"
306
- });
307
- continue;
308
- }
309
- if (tool.type === "frontend") await executeFrontendTool(tool, call);
310
- else onExecutionChange?.({
311
- callId,
312
- name: call.name,
313
- arguments: call.arguments,
314
- status: "pending"
315
- });
316
- }
317
- }, [
318
- tools,
319
- toolCalls,
320
- isLoading,
321
- executeFrontendTool,
322
- onExecutionChange
323
- ]);
324
- useEffect(() => {
325
- processToolCalls();
326
- }, [useMemo(() => {
327
- return toolCalls.filter((call) => !executedCallsRef.current.has(call.id) && !executingCallsRef.current.has(call.id)).map((call) => call.id).sort().join(",");
328
- }, [toolCalls]), isLoading]);
329
- return null;
330
- };
104
+ //#region src/utils/toolUtils.ts
331
105
  /**
332
106
  * 辅助函数:根据工具名查找工具定义
333
107
  */
@@ -341,7 +115,7 @@ function isFrontendTool(tool) {
341
115
  return tool.type === "frontend";
342
116
  }
343
117
  //#endregion
344
- //#region src/ReasoningContent.tsx
118
+ //#region src/components/ReasoningContent.tsx
345
119
  const REASONING_CONTENT_MAX_HEIGHT = 58;
346
120
  const ReasoningContent = ({ content }) => {
347
121
  const [isExpanded, setIsExpanded] = useState(false);
@@ -374,22 +148,23 @@ const ReasoningContent = ({ content }) => {
374
148
  justifyContent: "flex-end"
375
149
  },
376
150
  children: content.trim()
377
- }), isOverflowing && !isExpanded && /* @__PURE__ */ jsx("button", {
378
- onClick: () => setIsExpanded(true),
379
- style: {
380
- marginLeft: "-8px",
381
- fontSize: "12px",
382
- padding: "4px 8px",
383
- border: "none",
384
- background: "transparent",
385
- cursor: "pointer",
386
- color: "#ffffff"
387
- },
388
- children: "查看全部"
151
+ }), isOverflowing && !isExpanded && /* @__PURE__ */ jsx(Actions, {
152
+ items: [{
153
+ key: "expand",
154
+ icon: /* @__PURE__ */ jsx("span", {
155
+ style: {
156
+ fontSize: "12px",
157
+ marginLeft: "-4px",
158
+ opacity: .8
159
+ },
160
+ children: "查看全部"
161
+ })
162
+ }],
163
+ onClick: () => setIsExpanded(true)
389
164
  })] });
390
165
  };
391
166
  //#endregion
392
- //#region src/WaveLoading.tsx
167
+ //#region src/components/WaveLoading.tsx
393
168
  /**
394
169
  * 波浪动画 Loading 组件
395
170
  * 三个小圆点波浪状动画效果
@@ -428,8 +203,7 @@ const WaveLoading = ({ color = "#1890ff" }) => {
428
203
  });
429
204
  };
430
205
  //#endregion
431
- //#region src/MessageList.tsx
432
- injectStyles();
206
+ //#region src/components/MessageList.tsx
433
207
  const CustomParagraph = (props) => {
434
208
  const { node, ...rest } = props;
435
209
  return /* @__PURE__ */ jsx("span", { ...rest });
@@ -530,7 +304,7 @@ const renderToolCalls = (toolCalls, tools, toolExecutions) => {
530
304
  tool,
531
305
  record
532
306
  }, call.id);
533
- });
307
+ }).filter(Boolean);
534
308
  };
535
309
  const roleConfig = {
536
310
  user: {
@@ -546,7 +320,7 @@ const roleConfig = {
546
320
  }
547
321
  }
548
322
  };
549
- const MessageList = ({ messages, isLoading = false, className = "", tools, toolExecutions, components, securityConfig, loadingColor }) => {
323
+ const MessageList = ({ messages, isLoading = false, className = "", tools, toolExecutions, components, securityConfig, loadingColor, interruptRender, emptyState }) => {
550
324
  const reasoningCacheRef = useRef(/* @__PURE__ */ new Map());
551
325
  const processedMessages = useMemo(() => {
552
326
  const cache = reasoningCacheRef.current;
@@ -576,13 +350,19 @@ const MessageList = ({ messages, isLoading = false, className = "", tools, toolE
576
350
  }, [processedMessages]);
577
351
  const items = groupedItems.map((group, groupIndex) => {
578
352
  const isLastGroup = groupIndex === groupedItems.length - 1;
353
+ const isLastAiGroup = isLastGroup && group.type === "ai";
579
354
  if (group.type === "user" || group.messages.length === 1) {
580
355
  const message = group.messages[0];
581
- return toBubbleItem(message, isLastGroup && group.messages.length === 1, isLoading, tools, toolExecutions, components, securityConfig);
356
+ const bubbleItem = toBubbleItem(message, isLastGroup && group.messages.length === 1, isLoading, tools, toolExecutions, components, securityConfig);
357
+ if (isLastAiGroup && interruptRender) return {
358
+ ...bubbleItem,
359
+ content: /* @__PURE__ */ jsxs(Fragment, { children: [bubbleItem.content, interruptRender()] })
360
+ };
361
+ return bubbleItem;
582
362
  }
583
- const mergedContent = /* @__PURE__ */ jsx(Fragment, { children: group.messages.map((message, msgIndex) => {
363
+ const mergedContent = /* @__PURE__ */ jsxs(Fragment, { children: [group.messages.map((message, msgIndex) => {
584
364
  return renderMessageContent(message, isLastGroup && msgIndex === group.messages.length - 1, isLoading, tools, toolExecutions, components, securityConfig);
585
- }) });
365
+ }), isLastAiGroup && interruptRender?.()] });
586
366
  return {
587
367
  key: group.messages.map((m) => m.id).join("-"),
588
368
  role: "ai",
@@ -597,13 +377,19 @@ const MessageList = ({ messages, isLoading = false, className = "", tools, toolE
597
377
  placement: "start",
598
378
  style: { paddingBlock: 0 }
599
379
  }] : items;
600
- if (allItems.length === 0) return /* @__PURE__ */ jsx("div", {
601
- className: `agent-message-list empty ${className}`,
602
- children: /* @__PURE__ */ jsx("div", {
603
- className: "agent-message-empty",
604
- children: /* @__PURE__ */ jsx("p", { children: "欢迎使用AI智能剪辑助手" })
605
- })
606
- });
380
+ if (allItems.length === 0) {
381
+ if (emptyState?.render) return /* @__PURE__ */ jsx("div", {
382
+ className: `agent-message-list empty ${className}`,
383
+ children: emptyState.render()
384
+ });
385
+ return /* @__PURE__ */ jsx("div", {
386
+ className: `agent-message-list empty ${className}`,
387
+ children: /* @__PURE__ */ jsxs("div", {
388
+ className: "agent-message-empty",
389
+ children: [emptyState?.title && /* @__PURE__ */ jsx("h4", { children: emptyState.title }), emptyState?.description ? /* @__PURE__ */ jsx("p", { children: emptyState.description }) : /* @__PURE__ */ jsx("p", { children: "开始对话吧" })]
390
+ })
391
+ });
392
+ }
607
393
  return /* @__PURE__ */ jsx(Bubble.List, {
608
394
  className: `agent-message-list ${className}`,
609
395
  items: allItems,
@@ -612,7 +398,169 @@ const MessageList = ({ messages, isLoading = false, className = "", tools, toolE
612
398
  });
613
399
  };
614
400
  //#endregion
615
- //#region src/messageUtils.ts
401
+ //#region src/hooks/useInterrupt.tsx
402
+ /**
403
+ * InterruptManager - 管理 Interrupt 状态的 Hook
404
+ *
405
+ * 封装 Interrupt 状态管理和处理逻辑,避免在 AgentChat 组件中臃肿
406
+ */
407
+ function useInterrupt({ interrupt, config, onSubmit }) {
408
+ const [activeInterrupt, setActiveInterrupt] = useState(null);
409
+ const processedInterruptIdRef = useRef(null);
410
+ useEffect(() => {
411
+ if (interrupt) {
412
+ const interruptEvent = interrupt;
413
+ if (interruptEvent.id !== processedInterruptIdRef.current) {
414
+ processedInterruptIdRef.current = interruptEvent.id;
415
+ setActiveInterrupt(interruptEvent);
416
+ }
417
+ } else {
418
+ processedInterruptIdRef.current = null;
419
+ setActiveInterrupt(null);
420
+ }
421
+ }, [interrupt?.id]);
422
+ const handleResolveInterrupt = useCallback((response) => {
423
+ onSubmit(null, { command: { resume: response } });
424
+ setActiveInterrupt(null);
425
+ }, [onSubmit]);
426
+ return {
427
+ activeInterrupt,
428
+ handleResolveInterrupt,
429
+ renderInterrupt: useCallback(() => {
430
+ if (!activeInterrupt || !config?.render) return null;
431
+ return /* @__PURE__ */ jsx("div", {
432
+ className: "agent-chat-interrupt",
433
+ children: config.render({
434
+ event: activeInterrupt,
435
+ resolve: handleResolveInterrupt
436
+ })
437
+ });
438
+ }, [
439
+ activeInterrupt,
440
+ config,
441
+ handleResolveInterrupt
442
+ ])
443
+ };
444
+ }
445
+ //#endregion
446
+ //#region src/hooks/useToolExecution.ts
447
+ /**
448
+ * useToolExecution - 管理前端工具执行的 Hook
449
+ *
450
+ * 职责:
451
+ * 1. 管理工具执行状态
452
+ * 2. 自动执行前端工具(避免重复执行)
453
+ * 3. 通知外部执行状态变化
454
+ */
455
+ function useToolExecution({ tools, toolCalls, isLoading = false, onExecutionChange, onToolResult, completedToolResults }) {
456
+ const executedCallsRef = useRef(/* @__PURE__ */ new Set());
457
+ const executingCallsRef = useRef(/* @__PURE__ */ new Set());
458
+ const pendingNotifiedRef = useRef(/* @__PURE__ */ new Set());
459
+ const initializedRef = useRef(false);
460
+ useEffect(() => {
461
+ if (initializedRef.current || !completedToolResults || completedToolResults.size === 0) return;
462
+ initializedRef.current = true;
463
+ completedToolResults.forEach((result, callId) => {
464
+ executedCallsRef.current.add(callId);
465
+ const call = toolCalls.find((c) => c.id === callId);
466
+ if (call) onExecutionChange?.({
467
+ callId,
468
+ name: call.name,
469
+ arguments: call.arguments,
470
+ status: "success",
471
+ result
472
+ });
473
+ });
474
+ }, [completedToolResults]);
475
+ /**
476
+ * 执行单个前端工具
477
+ */
478
+ const executeFrontendTool = useCallback(async (tool, call) => {
479
+ const callId = call.id;
480
+ executingCallsRef.current.add(callId);
481
+ onExecutionChange?.({
482
+ callId,
483
+ name: call.name,
484
+ arguments: call.arguments,
485
+ status: "running"
486
+ });
487
+ try {
488
+ const result = await tool.execute(call.arguments);
489
+ onExecutionChange?.({
490
+ callId,
491
+ name: call.name,
492
+ arguments: call.arguments,
493
+ status: "success",
494
+ result
495
+ });
496
+ onToolResult?.(callId, call.name, result);
497
+ } catch (error) {
498
+ const errorMessage = error instanceof Error ? error.message : String(error);
499
+ onExecutionChange?.({
500
+ callId,
501
+ name: call.name,
502
+ arguments: call.arguments,
503
+ status: "error",
504
+ error: errorMessage
505
+ });
506
+ } finally {
507
+ executingCallsRef.current.delete(callId);
508
+ executedCallsRef.current.add(callId);
509
+ }
510
+ }, [onExecutionChange, onToolResult]);
511
+ /**
512
+ * 处理工具调用
513
+ */
514
+ const processToolCalls = useCallback(async () => {
515
+ if (!tools) return;
516
+ for (const call of toolCalls) {
517
+ const callId = call.id;
518
+ if (executedCallsRef.current.has(callId) || executingCallsRef.current.has(callId)) continue;
519
+ const tool = findTool(tools, call.name);
520
+ if (isLoading) {
521
+ if (!pendingNotifiedRef.current.has(callId)) {
522
+ pendingNotifiedRef.current.add(callId);
523
+ onExecutionChange?.({
524
+ callId,
525
+ name: call.name,
526
+ arguments: call.arguments,
527
+ status: "pending"
528
+ });
529
+ }
530
+ continue;
531
+ }
532
+ if (!tool) {
533
+ onExecutionChange?.({
534
+ callId,
535
+ name: call.name,
536
+ arguments: call.arguments,
537
+ status: "pending"
538
+ });
539
+ continue;
540
+ }
541
+ if (isFrontendTool(tool)) await executeFrontendTool(tool, call);
542
+ else onExecutionChange?.({
543
+ callId,
544
+ name: call.name,
545
+ arguments: call.arguments,
546
+ status: "pending"
547
+ });
548
+ }
549
+ }, [
550
+ tools,
551
+ toolCalls,
552
+ isLoading,
553
+ executeFrontendTool,
554
+ onExecutionChange
555
+ ]);
556
+ useEffect(() => {
557
+ processToolCalls();
558
+ }, [useMemo(() => {
559
+ return toolCalls.filter((call) => !executedCallsRef.current.has(call.id) && !executingCallsRef.current.has(call.id)).map((call) => call.id).sort().join(",");
560
+ }, [toolCalls]), isLoading]);
561
+ }
562
+ //#endregion
563
+ //#region src/utils/messageUtils.ts
616
564
  /**
617
565
  * 从 BaseMessage 中提取 tool_calls
618
566
  */
@@ -684,9 +632,115 @@ function processMessages(rawMessages) {
684
632
  };
685
633
  }
686
634
  //#endregion
687
- //#region src/AgentChat.tsx
635
+ //#region src/utils/injectStyles.ts
636
+ const styles = `
637
+ /* Agent Chat Container */
638
+ .agent-chat-container {
639
+ display: flex;
640
+ flex-direction: column;
641
+ height: 100%;
642
+ background-color: transparent;
643
+ overflow: hidden;
644
+ }
645
+
646
+ /* Message List */
647
+ .agent-message-list {
648
+ flex: 1;
649
+ overflow-y: auto;
650
+ display: flex;
651
+ flex-direction: column;
652
+ }
653
+
654
+ .agent-message-list .empty {
655
+ display: flex;
656
+ align-items: center;
657
+ justify-content: center;
658
+ }
659
+
660
+ .agent-message-list .ant-think-status-text {
661
+ font-size: 13px;
662
+ }
663
+
664
+ .agent-message-empty {
665
+ color: #8b8d91;
666
+ font-size: 14px;
667
+ text-align: center;
668
+ }
669
+
670
+ @keyframes agent-loading-bounce {
671
+ 0%,
672
+ 80%,
673
+ 100% {
674
+ transform: scale(0);
675
+ }
676
+ 40% {
677
+ transform: scale(1);
678
+ }
679
+ }
680
+
681
+ /* Chat Input */
682
+ .agent-chat-input-container {
683
+ padding: 12px 4px;
684
+ background-color: transparent;
685
+ }
686
+
687
+ /* Tool Calls Container */
688
+ .tool-calls-container {
689
+ display: flex;
690
+ flex-direction: column;
691
+ gap: 8px;
692
+ margin-top: 8px;
693
+ }
694
+
695
+ /* Tool Call Default Style */
696
+ .tool-call-default {
697
+ display: flex;
698
+ flex-direction: column;
699
+ gap: 4px;
700
+ padding: 6px 0;
701
+ }
702
+
703
+ .tool-call-header {
704
+ display: flex;
705
+ align-items: center;
706
+ gap: 8px;
707
+ }
708
+
709
+ .tool-call-name {
710
+ font-weight: 500;
711
+ font-size: 13px;
712
+ }
713
+
714
+ .tool-call-status {
715
+ font-size: 12px;
716
+ color: #8b8d91;
717
+ margin-left: auto;
718
+ }
719
+
720
+ .tool-call-error {
721
+ display: flex;
722
+ align-items: center;
723
+ gap: 4px;
724
+ font-size: 12px;
725
+ color: #ff4d4f;
726
+ }
727
+
728
+ .tool-call-wrapper {
729
+ width: 100%;
730
+ }
731
+ `;
732
+ let injected = false;
733
+ function injectStyles() {
734
+ if (injected || typeof document === "undefined") return;
735
+ const styleElement = document.createElement("style");
736
+ styleElement.textContent = styles;
737
+ document.head.appendChild(styleElement);
738
+ injected = true;
739
+ }
740
+ //#endregion
741
+ //#region src/components/AgentChat.tsx
688
742
  injectStyles();
689
- const AgentChat = forwardRef(({ apiUrl, assistantId, headers, threadId: externalThreadId, onThreadIdChange, className = "", tools, contexts, messageConfig, inputConfig, onError }, ref) => {
743
+ const AgentChat = forwardRef(({ apiUrl, assistantId, headers, threadId: externalThreadId, onThreadIdChange, className = "", tools, contexts, messageConfig, inputConfig, onError, interruptConfig }, ref) => {
690
744
  const [internalThreadId, setInternalThreadId] = useState(externalThreadId);
691
745
  useEffect(() => {
692
746
  setInternalThreadId(externalThreadId);
@@ -714,6 +768,11 @@ const AgentChat = forwardRef(({ apiUrl, assistantId, headers, threadId: external
714
768
  onError?.(err);
715
769
  }
716
770
  });
771
+ const { renderInterrupt: interruptRender } = useInterrupt({
772
+ interrupt: stream.interrupt,
773
+ config: interruptConfig,
774
+ onSubmit: stream.submit
775
+ });
717
776
  const [toolExecutions, setToolExecutions] = useState(/* @__PURE__ */ new Map());
718
777
  const { messages, toolResults } = useMemo(() => {
719
778
  return processMessages(stream.messages);
@@ -728,8 +787,8 @@ const AgentChat = forwardRef(({ apiUrl, assistantId, headers, threadId: external
728
787
  return next;
729
788
  });
730
789
  }, []);
731
- const submitToStream = useCallback((submitMessages, extraState = {}) => {
732
- stream.submit({
790
+ const submitToStream = useCallback(async (submitMessages, extraState = {}) => {
791
+ await stream.submit({
733
792
  messages: submitMessages,
734
793
  ...extraState,
735
794
  agentkit: {
@@ -774,51 +833,85 @@ const AgentChat = forwardRef(({ apiUrl, assistantId, headers, threadId: external
774
833
  }];
775
834
  }
776
835
  if (messages.length === 0) return;
777
- submitToStream(messages, extraState);
836
+ await submitToStream(messages, extraState);
778
837
  }, [onPreSend, submitToStream]);
779
- const handleStop = useCallback(() => {
780
- stream.stop();
838
+ const handleStop = useCallback(async () => {
839
+ await stream.stop();
781
840
  }, [stream]);
841
+ useToolExecution({
842
+ tools,
843
+ toolCalls: allToolCalls,
844
+ isLoading: stream.isLoading,
845
+ onExecutionChange: handleExecutionChange,
846
+ onToolResult: handleToolResult,
847
+ completedToolResults: toolResults
848
+ });
782
849
  return /* @__PURE__ */ jsxs("div", {
783
850
  className: `agent-chat-container ${className}`,
784
- children: [
785
- /* @__PURE__ */ jsx(ToolManager, {
786
- tools,
787
- toolCalls: allToolCalls,
788
- isLoading: stream.isLoading,
789
- onExecutionChange: handleExecutionChange,
790
- onToolResult: handleToolResult,
791
- completedToolResults: toolResults
792
- }),
793
- /* @__PURE__ */ jsx(MessageList, {
794
- messages,
795
- isLoading: stream.isLoading,
796
- className: "agent-chat-messages",
797
- tools,
798
- toolExecutions,
799
- components: messageConfig?.components,
800
- securityConfig: {
801
- allowedTags: messageConfig?.allowedTags,
802
- literalTagContent: messageConfig?.literalTagContent
803
- },
804
- loadingColor: messageConfig?.loadingColor
805
- }),
806
- /* @__PURE__ */ jsx(ChatInput, {
807
- ref: chatInputRef,
808
- onSend: handleSend,
809
- onStop: handleStop,
810
- isLoading: stream.isLoading,
811
- className: "agent-chat-input",
812
- footer,
813
- skill,
814
- slotConfig,
815
- header,
816
- prefix,
817
- placeholder
818
- })
819
- ]
851
+ children: [/* @__PURE__ */ jsx(MessageList, {
852
+ messages,
853
+ isLoading: stream.isLoading,
854
+ className: "agent-chat-messages",
855
+ tools,
856
+ toolExecutions,
857
+ components: messageConfig?.components,
858
+ securityConfig: {
859
+ allowedTags: messageConfig?.allowedTags,
860
+ literalTagContent: messageConfig?.literalTagContent
861
+ },
862
+ loadingColor: messageConfig?.loadingColor,
863
+ interruptRender,
864
+ emptyState: messageConfig?.emptyState
865
+ }), /* @__PURE__ */ jsx(ChatInput, {
866
+ ref: chatInputRef,
867
+ onSend: handleSend,
868
+ onStop: handleStop,
869
+ isLoading: stream.isLoading,
870
+ className: "agent-chat-input",
871
+ footer,
872
+ skill,
873
+ slotConfig,
874
+ header,
875
+ prefix,
876
+ placeholder
877
+ })]
820
878
  });
821
879
  });
822
880
  AgentChat.displayName = "AgentChat";
823
881
  //#endregion
824
- export { AgentChat, extractContent, extractToolCalls, injectStyles, processMessages, toChatMessage };
882
+ //#region src/components/ToolCard.tsx
883
+ /**
884
+ * ToolCard 组件 - 用于展示工具调用结果
885
+ * 样式:圆角矩形框,左侧 icon,label 分两行显示(prefix 小字体,content 默认字体)
886
+ */
887
+ const ToolCard = ({ prefix, content, icon }) => {
888
+ return /* @__PURE__ */ jsxs("div", {
889
+ style: {
890
+ display: "inline-flex",
891
+ alignItems: "center",
892
+ gap: 8,
893
+ borderRadius: 8,
894
+ fontSize: 13
895
+ },
896
+ children: [/* @__PURE__ */ jsx("span", {
897
+ style: {
898
+ display: "flex",
899
+ alignItems: "center"
900
+ },
901
+ children: icon
902
+ }), /* @__PURE__ */ jsxs("span", {
903
+ style: {
904
+ display: "flex",
905
+ alignItems: "center",
906
+ gap: 4,
907
+ color: "rgba(0, 0, 0, 0.45)"
908
+ },
909
+ children: [/* @__PURE__ */ jsx("span", {
910
+ style: { fontSize: 12 },
911
+ children: prefix
912
+ }), /* @__PURE__ */ jsx("span", { children: content })]
913
+ })]
914
+ });
915
+ };
916
+ //#endregion
917
+ export { AgentChat, ToolCard };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vegintech/langchain-react-agent",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "LangChain Agent UI component library for React",
5
5
  "license": "MIT",
6
6
  "files": [