@vegintech/langchain-react-agent 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # @vegintech/agentkit
2
+
3
+ 一个用于 React 的 LangChain Agent UI 组件库,基于 Ant Design X 构建,提供开箱即用的智能对话界面。
4
+
5
+ ## 特性
6
+
7
+ - 🚀 **基于 LangChain** - 与 LangChain 生态无缝集成,支持 LangGraph 后端
8
+ - 💬 **流式消息** - 内置流式响应支持,实时显示 AI 回复
9
+ - 🛠️ **工具调用** - 支持前端工具和后端工具的定义与执行
10
+ - 🎨 **Ant Design X** - 基于 Ant Design X 组件,提供优秀的视觉体验
11
+ - 🔧 **高度可定制** - 灵活的配置选项,支持自定义消息渲染和输入框行为
12
+ - 📱 **响应式设计** - 适配各种屏幕尺寸
13
+
14
+ ## 安装
15
+
16
+ ```bash
17
+ npm install @vegintech/agentkit
18
+ # 或
19
+ yarn add @vegintech/agentkit
20
+ # 或
21
+ pnpm add @vegintech/agentkit
22
+ ```
23
+
24
+ ### 对等依赖
25
+
26
+ 确保你的项目已安装以下依赖:
27
+
28
+ ```bash
29
+ npm install react react-dom @langchain/react
30
+ ```
31
+
32
+ ## 快速开始
33
+
34
+ ```tsx
35
+ import { AgentChat } from '@vegintech/agentkit';
36
+ import '@vegintech/agentkit/style.css';
37
+
38
+ function App() {
39
+ return (
40
+ <AgentChat
41
+ apiUrl="http://localhost:8000/api"
42
+ assistantId="my-assistant"
43
+ onThreadIdChange={(threadId) => {
44
+ console.log('New thread:', threadId);
45
+ }}
46
+ />
47
+ );
48
+ }
49
+ ```
50
+
51
+ ## 组件
52
+
53
+ ### AgentChat
54
+
55
+ 核心聊天组件,封装了消息列表、输入框和工具管理功能。
56
+
57
+ ```tsx
58
+ import { AgentChat } from '@vegintech/agentkit';
59
+
60
+ <AgentChat
61
+ // 连接配置
62
+ apiUrl="https://api.example.com"
63
+ assistantId="your-assistant-id"
64
+ threadId={threadId} // 可选:会话 ID
65
+ headers={{ Authorization: 'Bearer token' }} // 可选:自定义请求头
66
+ onThreadIdChange={(id) => setThreadId(id)} // 可选:会话 ID 变化回调
67
+
68
+ // 工具配置
69
+ tools={[frontendTool, backendTool]} // 可选:工具定义数组
70
+
71
+ // 上下文
72
+ contexts={[{ description: '用户信息', value: '用户ID: 123' }]} // 可选
73
+
74
+ // 消息渲染配置
75
+ messageConfig={{
76
+ components: { /* 自定义 Markdown 组件 */ },
77
+ allowedTags: { div: ['class'] },
78
+ }}
79
+
80
+ // 输入框配置
81
+ inputConfig={{
82
+ placeholder: '请输入消息...',
83
+ onPreSend: (params) => ({
84
+ messages: [{ type: 'human', content: params.message }],
85
+ }),
86
+ }}
87
+
88
+ // 错误处理
89
+ onError={(error) => console.error(error)}
90
+ />
91
+ ```
92
+
93
+ ## 工具定义
94
+
95
+ ### 前端工具
96
+
97
+ 前端工具在浏览器端执行,适用于需要访问浏览器 API 或不需要后端处理的场景。
98
+
99
+ ```tsx
100
+ import type { FrontendTool } from '@vegintech/agentkit';
101
+
102
+ const alertTool: FrontendTool = {
103
+ type: 'frontend',
104
+ name: 'alert',
105
+ description: '显示一个警告弹窗',
106
+ parameters: {
107
+ type: 'object',
108
+ properties: {
109
+ message: { type: 'string', description: '警告消息内容' },
110
+ },
111
+ required: ['message'],
112
+ },
113
+ execute: async (args) => {
114
+ alert(args.message);
115
+ return { success: true };
116
+ },
117
+ render: (props) => (
118
+ <div className="tool-alert">
119
+ {props.status === 'running' ? '显示弹窗中...' : '弹窗已显示'}
120
+ </div>
121
+ ),
122
+ };
123
+ ```
124
+
125
+ ### 后端工具
126
+
127
+ 后端工具由 LangChain Agent 在后端执行,适用于需要访问数据库、API 等后端资源的场景。
128
+
129
+ ```tsx
130
+ import type { BackendTool } from '@vegintech/agentkit';
131
+
132
+ const searchTool: BackendTool = {
133
+ type: 'backend',
134
+ name: 'search',
135
+ description: '搜索知识库',
136
+ parameters: {
137
+ type: 'object',
138
+ properties: {
139
+ query: { type: 'string', description: '搜索关键词' },
140
+ },
141
+ required: ['query'],
142
+ },
143
+ render: (props) => (
144
+ <div className="tool-search">
145
+ {props.status === 'pending' && '等待执行...'}
146
+ {props.status === 'running' && `搜索: ${props.arguments.query}`}
147
+ {props.status === 'success' && `找到 ${props.result?.count || 0} 条结果`}
148
+ </div>
149
+ ),
150
+ };
151
+ ```
152
+
153
+ ## API 参考
154
+
155
+ ### AgentChat Props
156
+
157
+ | 属性 | 类型 | 必填 | 说明 |
158
+ |------|------|------|------|
159
+ | apiUrl | string | 是 | LangGraph API 地址 |
160
+ | assistantId | string | 是 | 助手 ID |
161
+ | threadId | string | 否 | 会话 ID,不传则创建新会话 |
162
+ | headers | Record<string, string \| undefined \| null> | 否 | 自定义请求头 |
163
+ | onThreadIdChange | (threadId: string) => void | 否 | 会话 ID 变化回调 |
164
+ | className | string | 否 | 自定义 CSS 类名 |
165
+ | tools | ToolDefinition[] | 否 | 工具定义数组 |
166
+ | contexts | ContextItem[] | 否 | 上下文信息数组 |
167
+ | messageConfig | MessageConfig | 否 | 消息渲染配置 |
168
+ | inputConfig | InputConfig | 否 | 输入框配置 |
169
+ | onError | (error: Error) => void | 否 | 错误处理回调 |
170
+
171
+ ### 类型定义
172
+
173
+ ```tsx
174
+ import type {
175
+ AgentChatProps,
176
+ AgentChatRef,
177
+ FrontendTool,
178
+ BackendTool,
179
+ ToolDefinition,
180
+ ChatMessage,
181
+ ContextItem,
182
+ MessageConfig,
183
+ InputConfig,
184
+ } from '@vegintech/agentkit';
185
+ ```
186
+
187
+ ## 开发
188
+
189
+ ```bash
190
+ # 安装依赖
191
+ vp install
192
+
193
+ # 开发模式
194
+ vp dev
195
+
196
+ # 运行测试
197
+ vp test
198
+
199
+ # 构建
200
+ vp pack
201
+
202
+ # 代码检查
203
+ vp check
204
+ ```
205
+
206
+ ## 许可证
207
+
208
+ MIT
@@ -0,0 +1,203 @@
1
+ import * as react from "react";
2
+ import { ReactNode } from "react";
3
+ import { Components } from "streamdown";
4
+ import { BaseNode, InsertPosition, NodeRender, SenderProps, SkillType, SlotConfigType } from "@ant-design/x/es/sender/interface.ts";
5
+ import { BaseMessage } from "@langchain/core/messages";
6
+
7
+ //#region src/types.d.ts
8
+ /** Context Item,用于传递上下文信息 */
9
+ interface ContextItem {
10
+ /** 描述信息 */
11
+ description: string;
12
+ /** 上下文值 */
13
+ value: string;
14
+ }
15
+ /** Sender onSubmit 插槽配置类型 */
16
+ type SenderSlotConfig = SenderProps['slotConfig'];
17
+ /** Sender onSubmit 完整参数 */
18
+ interface SenderSubmitParams {
19
+ message: string;
20
+ slotConfig?: SenderSlotConfig;
21
+ skillData?: SkillType;
22
+ }
23
+ /** onPreSend 钩子返回值:messages + 其他任意字段作为 state */
24
+ interface PreSendResult {
25
+ /** 要发送的消息数组 */
26
+ messages: Array<{
27
+ type: string;
28
+ content: string;
29
+ }>;
30
+ /** 其他任意字段会作为额外参数一起 submit */
31
+ [x: string]: unknown;
32
+ }
33
+ /** Sender 组件的可定制参数 */
34
+ interface SenderCustomizationProps {
35
+ /** 底部区域,可放置操作按钮等 */
36
+ footer?: BaseNode | NodeRender;
37
+ /** 技能配置,用于提示用户输入 */
38
+ skill?: SkillType;
39
+ /** 插槽配置 */
40
+ slotConfig?: SlotConfigType[];
41
+ /** 头部区域 */
42
+ header?: BaseNode | NodeRender;
43
+ /** 前缀区域 */
44
+ prefix?: BaseNode | NodeRender;
45
+ }
46
+ /** ChatInput 组件对外暴露的方法 */
47
+ interface AgentChatInputRef {
48
+ /** 清空输入框 */
49
+ clear: () => void;
50
+ /** 聚焦输入框 */
51
+ focus: () => void;
52
+ /** 插入内容到输入框 */
53
+ insert: (slotConfig: SlotConfigType[], position?: InsertPosition, replaceCharacters?: string, preventScroll?: boolean) => void;
54
+ /** 设置技能配置 */
55
+ setSkill: (skill?: SkillType) => void;
56
+ }
57
+ /** 简化版 JSON Schema 类型 */
58
+ interface ToolParameterSchema {
59
+ type: string | string[];
60
+ properties?: Record<string, ToolParameterSchema>;
61
+ required?: string[];
62
+ items?: ToolParameterSchema;
63
+ description?: string;
64
+ enum?: (string | number)[];
65
+ default?: unknown;
66
+ [key: string]: unknown;
67
+ }
68
+ /** 消息类型,用于区分用户和助手消息 */
69
+ type MessageType = 'human' | 'ai' | 'system' | 'tool' | 'function';
70
+ /** 扩展的消息类型,确保包含 id 和必要的字段 */
71
+ interface ChatMessage {
72
+ id: string;
73
+ type: MessageType;
74
+ content: string;
75
+ name?: string;
76
+ additional_kwargs?: Record<string, unknown>;
77
+ /** 思考内容(来自 additional_kwargs.reasoning_content) */
78
+ reasoningContent?: string;
79
+ /** 工具调用信息(AIMessage 时可能有) */
80
+ toolCalls?: ToolCallInput[];
81
+ /** 工具调用 ID(ToolMessage 时使用) */
82
+ toolCallId?: string;
83
+ /** 工具执行结果(ToolMessage 时使用) */
84
+ toolCallResult?: unknown;
85
+ }
86
+ /** 工具调用输入(来自 LLM 的 tool_call) */
87
+ interface ToolCallInput {
88
+ id: string;
89
+ name: string;
90
+ arguments: Record<string, unknown>;
91
+ /** 后端工具执行结果(来自 ToolMessage) */
92
+ result?: unknown;
93
+ }
94
+ /** 工具执行状态 */
95
+ type ToolExecutionStatus = 'pending' | 'running' | 'success' | 'error';
96
+ /** 工具执行结果 */
97
+ interface ToolExecutionRecord {
98
+ callId: string;
99
+ name: string;
100
+ arguments: Record<string, unknown>;
101
+ status: ToolExecutionStatus;
102
+ result?: unknown;
103
+ error?: string;
104
+ }
105
+ /** 渲染函数的 props */
106
+ interface ToolRenderProps<TArgs = Record<string, unknown>> {
107
+ name: string;
108
+ arguments: TArgs;
109
+ result?: unknown;
110
+ status: ToolExecutionStatus;
111
+ error?: string;
112
+ }
113
+ /** 工具基础属性 */
114
+ interface ToolBase<TArgs = Record<string, unknown>> {
115
+ name: string;
116
+ description: string;
117
+ parameters: ToolParameterSchema;
118
+ /** 渲染函数:可选,用于在消息列表中展示工具调用过程/结果 */
119
+ render?: (props: ToolRenderProps<TArgs>) => ReactNode;
120
+ }
121
+ /** 前端工具:必须有 execute */
122
+ interface FrontendTool<TArgs = Record<string, unknown>> extends ToolBase<TArgs> {
123
+ /** 前端工具必须有 execute */
124
+ type: 'frontend';
125
+ execute: (args: TArgs) => Promise<unknown> | unknown;
126
+ }
127
+ /** 后端工具:不能有 execute */
128
+ interface BackendTool<TArgs = Record<string, unknown>> extends ToolBase<TArgs> {
129
+ /** 后端工具由 Agent 执行 */
130
+ type: 'backend';
131
+ execute?: never;
132
+ }
133
+ /** 工具定义联合类型 */
134
+ type ToolDefinition<TArgs = Record<string, unknown>> = FrontendTool<TArgs> | BackendTool<TArgs>;
135
+ /** 消息渲染配置 */
136
+ interface MessageConfig {
137
+ /** Markdown 自定义组件 */
138
+ components?: Components;
139
+ /** 允许的标签及其属性 */
140
+ allowedTags?: Record<string, string[]>;
141
+ /** 作为字面量内容处理的标签(不解析内部 Markdown) */
142
+ literalTagContent?: string[];
143
+ }
144
+ /** 输入框配置 */
145
+ interface InputConfig extends SenderCustomizationProps {
146
+ /** 提交前的钩子,允许修改消息内容和添加状态参数 */
147
+ onPreSend?: (params: SenderSubmitParams) => PreSendResult | Promise<PreSendResult>;
148
+ /** 输入框占位符文本 */
149
+ placeholder?: string;
150
+ }
151
+ interface AgentChatProps {
152
+ apiUrl?: string;
153
+ assistantId: string;
154
+ threadId?: string;
155
+ headers?: Record<string, string | undefined | null>;
156
+ onThreadIdChange?: (threadId: string) => void;
157
+ className?: string;
158
+ tools?: ToolDefinition<any>[];
159
+ contexts?: ContextItem[];
160
+ messageConfig?: MessageConfig;
161
+ inputConfig?: InputConfig;
162
+ onError?: (error: Error) => void;
163
+ }
164
+ /** AgentChat 组件对外暴露的方法 */
165
+ interface AgentChatRef {
166
+ /** ChatInput 组件的引用 */
167
+ input: AgentChatInputRef | null;
168
+ /** 设置技能配置 */
169
+ setSkill: (skill?: SkillType) => void;
170
+ /** 清空输入框 */
171
+ clearInput: () => void;
172
+ /** 聚焦输入框 */
173
+ focusInput: () => void;
174
+ }
175
+ //#endregion
176
+ //#region src/AgentChat.d.ts
177
+ declare const AgentChat: react.ForwardRefExoticComponent<AgentChatProps & react.RefAttributes<AgentChatRef>>;
178
+ //#endregion
179
+ //#region src/messageUtils.d.ts
180
+ /**
181
+ * 从 BaseMessage 中提取 tool_calls
182
+ */
183
+ declare function extractToolCalls(message: BaseMessage): ToolCallInput[] | undefined;
184
+ /**
185
+ * 从 BaseMessage 中提取文本内容
186
+ */
187
+ declare function extractContent(message: BaseMessage): string;
188
+ /**
189
+ * 将 BaseMessage 转换为 ChatMessage
190
+ */
191
+ declare function toChatMessage(message: BaseMessage, toolResults: Map<string, unknown>): ChatMessage | null;
192
+ /**
193
+ * 预处理消息列表:建立 tool_call_id -> result 映射,并过滤 ToolMessage
194
+ */
195
+ declare function processMessages(rawMessages: BaseMessage[]): {
196
+ messages: ChatMessage[];
197
+ toolResults: Map<string, unknown>;
198
+ };
199
+ //#endregion
200
+ //#region src/injectStyles.d.ts
201
+ declare function injectStyles(): void;
202
+ //#endregion
203
+ 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 };
package/dist/index.mjs ADDED
@@ -0,0 +1,823 @@
1
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
2
+ import { useStream } from "@langchain/react";
3
+ import { Bubble, Sender, Think } from "@ant-design/x";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
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();
115
+ const slotConfig = [];
116
+ const ChatInput = forwardRef(({ onSend, onStop, isLoading = false, disabled = false, placeholder = "输入消息...", className = "", footer, skill: externalSkill, header, prefix }, ref) => {
117
+ const senderRef = useRef(null);
118
+ const [internalSkill, setInternalSkill] = useState(externalSkill);
119
+ useImperativeHandle(ref, () => ({
120
+ clear: () => senderRef.current?.clear?.(),
121
+ focus: () => senderRef.current?.focus?.(),
122
+ insert: (slots, position, replaceChars, preventScroll) => senderRef.current?.insert?.(slots, position, replaceChars, preventScroll),
123
+ setSkill: (skill) => setInternalSkill(skill)
124
+ }));
125
+ const handleSubmit = useCallback((message, slotConfig, skillData) => {
126
+ if (message.trim() && !disabled && !isLoading) {
127
+ onSend({
128
+ message,
129
+ slotConfig,
130
+ skillData
131
+ });
132
+ senderRef.current?.clear?.();
133
+ }
134
+ }, [
135
+ disabled,
136
+ isLoading,
137
+ onSend
138
+ ]);
139
+ return /* @__PURE__ */ jsx("div", {
140
+ className: `agent-chat-input-container ${className}`,
141
+ children: /* @__PURE__ */ jsx(Sender, {
142
+ ref: senderRef,
143
+ slotConfig,
144
+ placeholder,
145
+ loading: isLoading,
146
+ disabled,
147
+ onSubmit: handleSubmit,
148
+ onCancel: onStop,
149
+ autoSize: {
150
+ minRows: 1,
151
+ maxRows: 6
152
+ },
153
+ footer,
154
+ skill: internalSkill,
155
+ header,
156
+ prefix
157
+ })
158
+ });
159
+ });
160
+ ChatInput.displayName = "ChatInput";
161
+ //#endregion
162
+ //#region src/ToolCallRenderer.tsx
163
+ /**
164
+ * 工具调用渲染组件
165
+ *
166
+ * 职责:
167
+ * 1. 渲染单个工具调用的状态和结果
168
+ * 2. 支持自定义 render 函数
169
+ * 3. 默认渲染:简单的工具卡片样式
170
+ */
171
+ const ToolCallRenderer = ({ tool, record }) => {
172
+ if (tool?.render) return /* @__PURE__ */ jsx("div", {
173
+ className: "tool-call-wrapper",
174
+ children: tool.render({
175
+ name: record.name,
176
+ arguments: record.arguments,
177
+ result: record.result,
178
+ status: record.status,
179
+ error: record.error
180
+ })
181
+ });
182
+ return /* @__PURE__ */ jsx(DefaultToolCallRenderer, { record });
183
+ };
184
+ /**
185
+ * 默认工具调用渲染器
186
+ */
187
+ const DefaultToolCallRenderer = ({ record }) => {
188
+ const { name, status } = record;
189
+ const getStatusText = () => {
190
+ switch (status) {
191
+ case "running": return "执行中...";
192
+ case "success": return "执行成功";
193
+ case "error": return "执行失败";
194
+ default: return "等待执行";
195
+ }
196
+ };
197
+ return /* @__PURE__ */ jsx("div", {
198
+ className: "tool-call-default",
199
+ children: /* @__PURE__ */ jsxs("div", {
200
+ className: "tool-call-header",
201
+ children: [/* @__PURE__ */ jsx("span", {
202
+ className: "tool-call-name",
203
+ children: name
204
+ }), /* @__PURE__ */ jsx("span", {
205
+ className: "tool-call-status",
206
+ children: getStatusText()
207
+ })]
208
+ })
209
+ });
210
+ };
211
+ //#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
+ };
331
+ /**
332
+ * 辅助函数:根据工具名查找工具定义
333
+ */
334
+ function findTool(tools, name) {
335
+ return tools?.find((t) => t.name === name);
336
+ }
337
+ /**
338
+ * 辅助函数:判断工具是否为前端工具
339
+ */
340
+ function isFrontendTool(tool) {
341
+ return tool.type === "frontend";
342
+ }
343
+ //#endregion
344
+ //#region src/ReasoningContent.tsx
345
+ const REASONING_CONTENT_MAX_HEIGHT = 58;
346
+ const ReasoningContent = ({ content }) => {
347
+ const [isExpanded, setIsExpanded] = useState(false);
348
+ const containerRef = useRef(null);
349
+ const [isOverflowing, setIsOverflowing] = useState(false);
350
+ useEffect(() => {
351
+ const checkOverflow = () => {
352
+ if (containerRef.current) {
353
+ const originalMaxHeight = containerRef.current.style.maxHeight;
354
+ containerRef.current.style.maxHeight = "none";
355
+ const height = containerRef.current.scrollHeight;
356
+ containerRef.current.style.maxHeight = originalMaxHeight;
357
+ setIsOverflowing(height > REASONING_CONTENT_MAX_HEIGHT);
358
+ }
359
+ };
360
+ checkOverflow();
361
+ const resizeObserver = new ResizeObserver(checkOverflow);
362
+ if (containerRef.current) resizeObserver.observe(containerRef.current);
363
+ return () => resizeObserver.disconnect();
364
+ }, [content]);
365
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
366
+ ref: containerRef,
367
+ style: {
368
+ whiteSpace: "pre-wrap",
369
+ lineHeight: "1.6",
370
+ maxHeight: isExpanded ? "none" : `${REASONING_CONTENT_MAX_HEIGHT}px`,
371
+ overflow: "hidden",
372
+ display: "flex",
373
+ flexDirection: "column",
374
+ justifyContent: "flex-end"
375
+ },
376
+ 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: "#1677ff"
387
+ },
388
+ children: "查看全部"
389
+ })] });
390
+ };
391
+ //#endregion
392
+ //#region src/WaveLoading.tsx
393
+ /**
394
+ * 波浪动画 Loading 组件
395
+ * 三个小圆点波浪状动画效果
396
+ */
397
+ const WaveLoading = ({ color = "#1890ff" }) => {
398
+ return /* @__PURE__ */ jsxs("div", {
399
+ style: {
400
+ display: "flex",
401
+ alignItems: "center",
402
+ justifyContent: "left",
403
+ gap: "6px",
404
+ padding: "16px 0"
405
+ },
406
+ children: [[
407
+ 0,
408
+ 1,
409
+ 2
410
+ ].map((index) => /* @__PURE__ */ jsx("div", { style: {
411
+ width: "6px",
412
+ height: "6px",
413
+ backgroundColor: color,
414
+ borderRadius: "50%",
415
+ animation: `wave 1.2s ease-in-out ${index * .2}s infinite`
416
+ } }, index)), /* @__PURE__ */ jsx("style", { children: `
417
+ @keyframes wave {
418
+ 0%, 40%, 100% {
419
+ transform: scaleY(1);
420
+ opacity: 0.6;
421
+ }
422
+ 20% {
423
+ transform: scaleY(1.8);
424
+ opacity: 1;
425
+ }
426
+ }
427
+ ` })]
428
+ });
429
+ };
430
+ //#endregion
431
+ //#region src/MessageList.tsx
432
+ injectStyles();
433
+ const CustomParagraph = (props) => {
434
+ const { node, ...rest } = props;
435
+ return /* @__PURE__ */ jsx("span", { ...rest });
436
+ };
437
+ const renderMarkdown = (content, customComponents, securityConfig) => {
438
+ return /* @__PURE__ */ jsx(Streamdown, {
439
+ components: {
440
+ p: CustomParagraph,
441
+ ...customComponents
442
+ },
443
+ allowedTags: securityConfig?.allowedTags,
444
+ literalTagContent: securityConfig?.literalTagContent,
445
+ controls: { table: {
446
+ copy: false,
447
+ download: false,
448
+ fullscreen: false
449
+ } },
450
+ children: content ?? ""
451
+ });
452
+ };
453
+ const renderMessageContent = (message, isLastMessage, isLoading, tools, toolExecutions, components, securityConfig) => {
454
+ const hasToolCalls = message.toolCalls && message.toolCalls.length > 0;
455
+ const shouldBlink = isLastMessage && isLoading && !message.content && message.toolCalls?.length == 0;
456
+ return /* @__PURE__ */ jsxs("div", {
457
+ className: "message-item",
458
+ children: [
459
+ message.reasoningContent && /* @__PURE__ */ jsx(Think, {
460
+ title: "深度思考",
461
+ style: {
462
+ paddingRight: "12px",
463
+ fontSize: "12px"
464
+ },
465
+ styles: { content: { marginTop: "6px" } },
466
+ defaultExpanded: true,
467
+ blink: shouldBlink,
468
+ children: /* @__PURE__ */ jsx(ReasoningContent, { content: message.reasoningContent })
469
+ }),
470
+ message.content && /* @__PURE__ */ jsx("div", {
471
+ style: { marginTop: message.reasoningContent ? "8px" : 0 },
472
+ children: /* @__PURE__ */ jsx(Streamdown, {
473
+ components: {
474
+ p: CustomParagraph,
475
+ ...components
476
+ },
477
+ allowedTags: securityConfig?.allowedTags,
478
+ literalTagContent: securityConfig?.literalTagContent,
479
+ controls: { table: {
480
+ copy: false,
481
+ download: false,
482
+ fullscreen: false
483
+ } },
484
+ children: message.content
485
+ })
486
+ }),
487
+ hasToolCalls && /* @__PURE__ */ jsx("div", {
488
+ className: "tool-calls-container",
489
+ style: { marginTop: "8px" },
490
+ children: renderToolCalls(message.toolCalls, tools, toolExecutions)
491
+ })
492
+ ]
493
+ }, message.id);
494
+ };
495
+ const toBubbleItem = (message, isLastMessage, isLoading, tools, toolExecutions, components, securityConfig) => {
496
+ const hasReasoning = !!message.reasoningContent;
497
+ const hasToolCalls = message.toolCalls && message.toolCalls.length > 0;
498
+ if (hasReasoning || hasToolCalls) return {
499
+ key: message.id,
500
+ role: message.type === "human" ? "user" : "ai",
501
+ content: renderMessageContent(message, isLastMessage, isLoading, tools, toolExecutions, components, securityConfig),
502
+ placement: message.type === "human" ? "end" : "start"
503
+ };
504
+ return {
505
+ key: message.id,
506
+ role: message.type === "human" ? "user" : "ai",
507
+ content: message.content,
508
+ contentRender: (content) => renderMarkdown(content, components, securityConfig),
509
+ placement: message.type === "human" ? "end" : "start"
510
+ };
511
+ };
512
+ const renderToolCalls = (toolCalls, tools, toolExecutions) => {
513
+ return toolCalls.map((call) => {
514
+ const tool = findTool(tools, call.name);
515
+ let record;
516
+ if (tool && isFrontendTool(tool)) record = toolExecutions.get(call.id) || {
517
+ callId: call.id,
518
+ name: call.name,
519
+ arguments: call.arguments,
520
+ status: "pending"
521
+ };
522
+ else record = {
523
+ callId: call.id,
524
+ name: call.name,
525
+ arguments: call.arguments,
526
+ status: call.result !== void 0 ? "success" : "pending",
527
+ result: call.result
528
+ };
529
+ return /* @__PURE__ */ jsx(ToolCallRenderer, {
530
+ tool,
531
+ record
532
+ }, call.id);
533
+ });
534
+ };
535
+ const roleConfig = {
536
+ user: {
537
+ placement: "end",
538
+ style: { paddingBlock: "8px" }
539
+ },
540
+ ai: {
541
+ placement: "start",
542
+ variant: "borderless",
543
+ style: {
544
+ paddingInlineEnd: 0,
545
+ paddingBlock: "8px"
546
+ }
547
+ }
548
+ };
549
+ const MessageList = ({ messages, isLoading = false, className = "", tools, toolExecutions, components, securityConfig }) => {
550
+ const reasoningCacheRef = useRef(/* @__PURE__ */ new Map());
551
+ const processedMessages = useMemo(() => {
552
+ const cache = reasoningCacheRef.current;
553
+ return messages.map((message) => {
554
+ if (message.type === "ai" && message.reasoningContent) cache.set(message.id, message.reasoningContent);
555
+ if (message.type === "ai" && !message.reasoningContent) {
556
+ const cachedReasoning = cache.get(message.id);
557
+ if (cachedReasoning) return {
558
+ ...message,
559
+ reasoningContent: cachedReasoning
560
+ };
561
+ }
562
+ return message;
563
+ });
564
+ }, [messages]);
565
+ const groupedItems = useMemo(() => {
566
+ const groups = [];
567
+ for (const message of processedMessages) {
568
+ const currentRole = message.type === "human" ? "user" : "ai";
569
+ if (groups.length === 0 || groups[groups.length - 1].type !== currentRole) groups.push({
570
+ type: currentRole,
571
+ messages: [message]
572
+ });
573
+ else groups[groups.length - 1].messages.push(message);
574
+ }
575
+ return groups;
576
+ }, [processedMessages]);
577
+ const items = groupedItems.map((group, groupIndex) => {
578
+ const isLastGroup = groupIndex === groupedItems.length - 1;
579
+ if (group.type === "user" || group.messages.length === 1) {
580
+ const message = group.messages[0];
581
+ return toBubbleItem(message, isLastGroup && group.messages.length === 1, isLoading, tools, toolExecutions, components, securityConfig);
582
+ }
583
+ const mergedContent = /* @__PURE__ */ jsx(Fragment, { children: group.messages.map((message, msgIndex) => {
584
+ return renderMessageContent(message, isLastGroup && msgIndex === group.messages.length - 1, isLoading, tools, toolExecutions, components, securityConfig);
585
+ }) });
586
+ return {
587
+ key: group.messages.map((m) => m.id).join("-"),
588
+ role: "ai",
589
+ content: mergedContent,
590
+ placement: "start"
591
+ };
592
+ });
593
+ const allItems = processedMessages[processedMessages.length - 1]?.type === "human" && isLoading ? [...items, {
594
+ key: "loading",
595
+ role: "ai",
596
+ content: /* @__PURE__ */ jsx(WaveLoading, {}),
597
+ placement: "start",
598
+ style: { paddingBlock: 0 }
599
+ }] : 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
+ });
607
+ return /* @__PURE__ */ jsx(Bubble.List, {
608
+ className: `agent-message-list ${className}`,
609
+ items: allItems,
610
+ role: roleConfig,
611
+ autoScroll: true
612
+ });
613
+ };
614
+ //#endregion
615
+ //#region src/messageUtils.ts
616
+ /**
617
+ * 从 BaseMessage 中提取 tool_calls
618
+ */
619
+ function extractToolCalls(message) {
620
+ if ("tool_calls" in message && Array.isArray(message.tool_calls)) return message.tool_calls.map((tc) => ({
621
+ id: tc.id || crypto.randomUUID(),
622
+ name: tc.name || tc.function?.name || "",
623
+ arguments: typeof tc.function?.arguments === "string" ? JSON.parse(tc.function.arguments) : tc.args || {}
624
+ }));
625
+ }
626
+ /**
627
+ * 从 BaseMessage 中提取文本内容
628
+ */
629
+ function extractContent(message) {
630
+ if (typeof message.content === "string") return message.content;
631
+ if (Array.isArray(message.content)) return message.content.map((c) => typeof c === "object" && "text" in c ? c.text : "").join("");
632
+ return "";
633
+ }
634
+ /**
635
+ * 将 BaseMessage 转换为 ChatMessage
636
+ */
637
+ function toChatMessage(message, toolResults) {
638
+ const additionalKwargs = message.additional_kwargs || {};
639
+ if (message.type === "tool") return null;
640
+ if (message.type === "system") return null;
641
+ let msgType = "ai";
642
+ if (message.type === "human") msgType = "human";
643
+ const toolCalls = extractToolCalls(message);
644
+ let toolCallsWithResult = toolCalls;
645
+ if (toolCalls) toolCallsWithResult = toolCalls.map((tc) => ({
646
+ ...tc,
647
+ result: toolResults.get(tc.id)
648
+ }));
649
+ const reasoningContent = additionalKwargs.reasoning_content;
650
+ return {
651
+ id: message.id || crypto.randomUUID(),
652
+ type: msgType,
653
+ content: extractContent(message),
654
+ name: message.name,
655
+ additional_kwargs: additionalKwargs,
656
+ reasoningContent,
657
+ toolCalls: toolCallsWithResult
658
+ };
659
+ }
660
+ /**
661
+ * 预处理消息列表:建立 tool_call_id -> result 映射,并过滤 ToolMessage
662
+ */
663
+ function processMessages(rawMessages) {
664
+ const toolResults = /* @__PURE__ */ new Map();
665
+ for (const message of rawMessages) if (message.type === "tool") {
666
+ const toolCallId = message.tool_call_id || message.additional_kwargs?.tool_call_id;
667
+ if (toolCallId) {
668
+ const content = extractContent(message);
669
+ try {
670
+ toolResults.set(toolCallId, JSON.parse(content));
671
+ } catch {
672
+ toolResults.set(toolCallId, content);
673
+ }
674
+ }
675
+ }
676
+ const messages = [];
677
+ for (const message of rawMessages) {
678
+ const chatMessage = toChatMessage(message, toolResults);
679
+ if (chatMessage) messages.push(chatMessage);
680
+ }
681
+ return {
682
+ messages,
683
+ toolResults
684
+ };
685
+ }
686
+ //#endregion
687
+ //#region src/AgentChat.tsx
688
+ injectStyles();
689
+ const AgentChat = forwardRef(({ apiUrl, assistantId, headers, threadId: externalThreadId, onThreadIdChange, className = "", tools, contexts, messageConfig, inputConfig, onError }, ref) => {
690
+ const [internalThreadId, setInternalThreadId] = useState(externalThreadId);
691
+ useEffect(() => {
692
+ setInternalThreadId(externalThreadId);
693
+ }, [externalThreadId]);
694
+ const { footer, skill, slotConfig, header, prefix, onPreSend, placeholder } = inputConfig || {};
695
+ const chatInputRef = useRef(null);
696
+ useImperativeHandle(ref, () => ({
697
+ input: chatInputRef.current,
698
+ setSkill: (skill) => chatInputRef.current?.setSkill?.(skill),
699
+ clearInput: () => chatInputRef.current?.clear?.(),
700
+ focusInput: () => chatInputRef.current?.focus?.()
701
+ }));
702
+ const stream = useStream({
703
+ apiUrl,
704
+ assistantId,
705
+ threadId: internalThreadId,
706
+ onThreadId: (newThreadId) => {
707
+ setInternalThreadId(newThreadId);
708
+ onThreadIdChange?.(newThreadId);
709
+ },
710
+ defaultHeaders: headers,
711
+ messagesKey: "messages",
712
+ onError: (error) => {
713
+ const err = error instanceof Error ? error : new Error(String(error));
714
+ onError?.(err);
715
+ }
716
+ });
717
+ const [toolExecutions, setToolExecutions] = useState(/* @__PURE__ */ new Map());
718
+ const { messages, toolResults } = useMemo(() => {
719
+ return processMessages(stream.messages);
720
+ }, [stream.messages]);
721
+ const allToolCalls = useMemo(() => {
722
+ return messages.flatMap((msg) => msg.toolCalls || []);
723
+ }, [messages]);
724
+ const handleExecutionChange = useCallback((record) => {
725
+ setToolExecutions((prev) => {
726
+ const next = new Map(prev);
727
+ next.set(record.callId, record);
728
+ return next;
729
+ });
730
+ }, []);
731
+ const submitToStream = useCallback((submitMessages, extraState = {}) => {
732
+ stream.submit({
733
+ messages: submitMessages,
734
+ ...extraState,
735
+ agentkit: {
736
+ actions: tools,
737
+ context: contexts
738
+ }
739
+ }, {
740
+ streamResumable: true,
741
+ optimisticValues(prev) {
742
+ const newMessages = [...prev.messages ?? [], ...submitMessages];
743
+ return {
744
+ ...prev,
745
+ messages: newMessages
746
+ };
747
+ }
748
+ });
749
+ }, [
750
+ stream,
751
+ tools,
752
+ contexts
753
+ ]);
754
+ const handleToolResult = useCallback((callId, name, result) => {
755
+ submitToStream([{
756
+ type: "tool",
757
+ content: typeof result === "string" ? result : JSON.stringify(result),
758
+ tool_call_id: callId,
759
+ name
760
+ }]);
761
+ }, [submitToStream]);
762
+ const handleSend = useCallback(async (params) => {
763
+ let messages = [];
764
+ let extraState = {};
765
+ if (onPreSend && params.message.trim()) {
766
+ const { messages: resultMessages, ...rest } = await onPreSend(params);
767
+ messages = resultMessages.filter((m) => m != null);
768
+ extraState = rest;
769
+ } else {
770
+ if (!params.message.trim()) return;
771
+ messages = [{
772
+ type: "human",
773
+ content: params.message
774
+ }];
775
+ }
776
+ if (messages.length === 0) return;
777
+ submitToStream(messages, extraState);
778
+ }, [onPreSend, submitToStream]);
779
+ const handleStop = useCallback(() => {
780
+ stream.stop();
781
+ }, [stream]);
782
+ return /* @__PURE__ */ jsxs("div", {
783
+ 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
+ }),
805
+ /* @__PURE__ */ jsx(ChatInput, {
806
+ ref: chatInputRef,
807
+ onSend: handleSend,
808
+ onStop: handleStop,
809
+ isLoading: stream.isLoading,
810
+ className: "agent-chat-input",
811
+ footer,
812
+ skill,
813
+ slotConfig,
814
+ header,
815
+ prefix,
816
+ placeholder
817
+ })
818
+ ]
819
+ });
820
+ });
821
+ AgentChat.displayName = "AgentChat";
822
+ //#endregion
823
+ export { AgentChat, extractContent, extractToolCalls, injectStyles, processMessages, toChatMessage };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@vegintech/langchain-react-agent",
3
+ "version": "0.0.1",
4
+ "description": "LangChain Agent UI component library for React",
5
+ "license": "MIT",
6
+ "sideEffects": false,
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "type": "module",
11
+ "exports": {
12
+ ".": "./dist/index.mjs",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "build": "vp pack",
20
+ "dev": "vp pack --watch",
21
+ "test": "vp test",
22
+ "check": "vp check",
23
+ "prepublishOnly": "vp run build"
24
+ },
25
+ "peerDependencies": {
26
+ "react": ">=18.0.0",
27
+ "react-dom": ">=18.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^25.5.0",
31
+ "@types/react": "^19.0.0",
32
+ "@types/react-dom": "^19.0.0",
33
+ "@typescript/native-preview": "7.0.0-dev.20260316.1",
34
+ "@vitejs/plugin-react": "catalog:",
35
+ "bumpp": "^11.0.1",
36
+ "react": "^19.0.0",
37
+ "react-dom": "^19.0.0",
38
+ "typescript": "^5.9.3",
39
+ "vite-plus": "^0.1.11"
40
+ },
41
+ "dependencies": {
42
+ "@ant-design/x": "catalog:",
43
+ "@langchain/core": "catalog:",
44
+ "@langchain/react": "catalog:",
45
+ "streamdown": "catalog:"
46
+ }
47
+ }