@langgraph-js/ui 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -12,8 +12,8 @@
12
12
  padding: 0;
13
13
  }
14
14
  </style>
15
- <script type="module" crossorigin src="/assets/index-g7kEDOdp.js"></script>
16
- <link rel="stylesheet" crossorigin href="/assets/index-DqBQMHgz.css">
15
+ <script type="module" crossorigin src="/assets/index-g0-NNf-r.js"></script>
16
+ <link rel="stylesheet" crossorigin href="/assets/index-DzEw-fFg.css">
17
17
  </head>
18
18
  <body>
19
19
  <div id="root" class="h-screen w-screen"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langgraph-js/ui",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
@@ -12,8 +12,13 @@
12
12
  "main": "./dist/react/src/index.js",
13
13
  "type": "module",
14
14
  "dependencies": {
15
+ "@andypf/json-viewer": "^2.2.0",
15
16
  "@dagrejs/dagre": "^1.1.4",
17
+ "@langgraph-js/sdk": "^workspace:*",
16
18
  "@nanostores/react": "^1.0.0",
19
+ "@rjsf/core": "^5.24.13",
20
+ "@rjsf/utils": "^5.24.13",
21
+ "@rjsf/validator-ajv8": "^5.24.13",
17
22
  "@unocss/reset": "^66.1.3",
18
23
  "@vitejs/plugin-basic-ssl": "^2.0.0",
19
24
  "@vitejs/plugin-react": "^4.3.4",
@@ -33,7 +38,7 @@
33
38
  "remark-gfm": "^4.0.1",
34
39
  "unocss": "^66.1.3",
35
40
  "vite": "^6.2.0",
36
- "@langgraph-js/sdk": "2.0.0"
41
+ "zod": "^3.25.56"
37
42
  },
38
43
  "devDependencies": {
39
44
  "@types/react": "^19.0.10",
package/src/chat/Chat.tsx CHANGED
@@ -10,11 +10,13 @@ import JsonEditorPopup from "./components/JsonEditorPopup";
10
10
  import { JsonToMessageButton } from "./components/JsonToMessage";
11
11
  import { GraphPanel } from "../graph/GraphPanel";
12
12
  import { setLocalConfig } from "./store";
13
- import { History, Network, LogOut, FileJson, Code } from "lucide-react";
13
+ import { History, Network, LogOut, FileJson } from "lucide-react";
14
14
  import { ArtifactViewer } from "../artifacts/ArtifactViewer";
15
15
  import "github-markdown-css/github-markdown.css";
16
16
  import { ArtifactsProvider, useArtifacts } from "../artifacts/ArtifactsContext";
17
17
  import "./index.css";
18
+ import { show_form } from "./tools/index";
19
+ import { create_artifacts } from "./tools/create_artifacts";
18
20
 
19
21
  const ChatMessages: React.FC = () => {
20
22
  const { renderMessages, loading, inChatError, client, collapsedTools, toggleToolCollapse, isFELocking } = useChat();
@@ -68,10 +70,9 @@ const ChatMessages: React.FC = () => {
68
70
  const ChatInput: React.FC = () => {
69
71
  const { userInput, setUserInput, loading, sendMessage, stopGeneration, currentAgent, setCurrentAgent, client, currentChatId } = useChat();
70
72
  const { extraParams } = useExtraParams();
71
- const [imageUrls, setImageUrls] = useState<string[]>([]);
72
-
73
+ const [imageUrls, setImageUrls] = useState<{ type: "image_url"; image_url: { url: string } }[]>([]);
73
74
  const handleFileUploaded = (url: string) => {
74
- setImageUrls((prev) => [...prev, url]);
75
+ setImageUrls((prev) => [...prev, { type: "image_url", image_url: { url } }]);
75
76
  };
76
77
  const _setCurrentAgent = (agent: string) => {
77
78
  localStorage.setItem("agent_name", agent);
@@ -86,14 +87,10 @@ const ChatInput: React.FC = () => {
86
87
  type: "text",
87
88
  text: userInput,
88
89
  },
89
- ...imageUrls.map((url) => ({
90
- type: "image_url" as const,
91
- image_url: { url },
92
- })),
90
+ ...imageUrls,
93
91
  ],
94
92
  },
95
93
  ];
96
-
97
94
  sendMessage(content, {
98
95
  extraParams,
99
96
  });
@@ -158,10 +155,13 @@ const ChatInput: React.FC = () => {
158
155
 
159
156
  const Chat: React.FC = () => {
160
157
  const [isPopupOpen, setIsPopupOpen] = useState(false);
161
- const { showHistory, toggleHistoryVisible, showGraph, toggleGraphVisible, renderMessages } = useChat();
158
+ const { showHistory, toggleHistoryVisible, showGraph, toggleGraphVisible, renderMessages, setTools, client } = useChat();
162
159
  const { extraParams, setExtraParams } = useExtraParams();
163
160
  const { showArtifact, setShowArtifact } = useArtifacts();
164
161
 
162
+ useEffect(() => {
163
+ setTools([show_form, create_artifacts]);
164
+ }, []);
165
165
  return (
166
166
  <div className="langgraph-chat-container flex h-full w-full overflow-hidden">
167
167
  {showHistory && <HistoryList onClose={() => toggleHistoryVisible()} formatTime={formatTime} />}
@@ -178,7 +178,7 @@ const Chat: React.FC = () => {
178
178
  历史记录
179
179
  </button>
180
180
  <div className="flex-1"></div>
181
- <JsonToMessageButton />
181
+
182
182
  <button
183
183
  onClick={() => setIsPopupOpen(true)}
184
184
  className="px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center gap-1.5"
@@ -195,6 +195,14 @@ const Chat: React.FC = () => {
195
195
  >
196
196
  打印日志数据
197
197
  </button>
198
+ <button
199
+ className="px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center gap-1.5"
200
+ onClick={() => {
201
+ console.log(client?.graphState);
202
+ }}
203
+ >
204
+ 打印 State
205
+ </button>
198
206
  <button
199
207
  className="px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center gap-1.5"
200
208
  onClick={() => {
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+
3
+ class ErrorBoundary extends React.Component {
4
+ constructor(props) {
5
+ super(props);
6
+ this.state = { hasError: false };
7
+ }
8
+
9
+ static getDerivedStateFromError(error) {
10
+ // 更新 state 以便下一次渲染将显示回退 UI
11
+ return { hasError: true };
12
+ }
13
+
14
+ componentDidCatch(error, errorInfo) {
15
+ // 你也可以将错误日志上报给服务器
16
+ console.error("Uncaught error:", error, errorInfo);
17
+ }
18
+
19
+ render() {
20
+ if (this.state.hasError) {
21
+ // 你可以渲染任何自定义的回退 UI
22
+ return <h1>出错了!请稍后重试。</h1>;
23
+ }
24
+
25
+ return this.props.children;
26
+ }
27
+ }
28
+
29
+ export default ErrorBoundary;
@@ -0,0 +1,18 @@
1
+ export const JSONViewer = (props: { data: any }) => {
2
+ return (
3
+ /** @ts-ignore */
4
+ <andypf-json-viewer
5
+ indent="4"
6
+ expanded="4"
7
+ theme="default-light"
8
+ show-data-types="false"
9
+ show-toolbar="false"
10
+ expand-icon-type="circle"
11
+ show-copy="true"
12
+ show-size="true"
13
+ data={JSON.stringify(props.data)}
14
+ >
15
+ {/* @ts-ignore */}
16
+ </andypf-json-viewer>
17
+ );
18
+ };
@@ -1,8 +1,15 @@
1
- import React from "react";
1
+ import React, { useState, useRef, useEffect, useCallback } from "react";
2
2
  import MessageHuman from "./MessageHuman";
3
3
  import MessageAI from "./MessageAI";
4
4
  import MessageTool from "./MessageTool";
5
5
  import { formatTokens, getMessageContent, LangGraphClient, RenderMessage } from "@langgraph-js/sdk";
6
+ import { JSONViewer } from "./JSONViewer";
7
+
8
+ interface MessageState {
9
+ showDetail: boolean;
10
+ showContextMenu: boolean;
11
+ contextMenuPosition: { x: number; y: number };
12
+ }
6
13
 
7
14
  export const MessagesBox = ({
8
15
  renderMessages,
@@ -15,26 +22,144 @@ export const MessagesBox = ({
15
22
  toggleToolCollapse: (id: string) => void;
16
23
  client: LangGraphClient;
17
24
  }) => {
25
+ // 使用 Map 来管理每个消息的状态
26
+ const [messageStates, setMessageStates] = useState<Map<string, MessageState>>(new Map());
27
+ const messageRefs = useRef<Map<string, HTMLDivElement | null>>(new Map());
28
+
29
+ const updateMessageState = useCallback((messageId: string, updates: Partial<MessageState>) => {
30
+ setMessageStates((prev) => {
31
+ const newStates = new Map(prev);
32
+ const currentState = newStates.get(messageId) || {
33
+ showDetail: false,
34
+ showContextMenu: false,
35
+ contextMenuPosition: { x: 0, y: 0 },
36
+ };
37
+ newStates.set(messageId, { ...currentState, ...updates });
38
+ return newStates;
39
+ });
40
+ }, []);
41
+
42
+ const handleContextMenu = useCallback(
43
+ (e: React.MouseEvent, messageId: string) => {
44
+ e.preventDefault();
45
+ updateMessageState(messageId, {
46
+ contextMenuPosition: { x: e.clientX, y: e.clientY },
47
+ showContextMenu: true,
48
+ });
49
+ },
50
+ [updateMessageState]
51
+ );
52
+
53
+ const handleCloseContextMenu = useCallback(
54
+ (messageId: string) => {
55
+ updateMessageState(messageId, { showContextMenu: false });
56
+ },
57
+ [updateMessageState]
58
+ );
59
+
60
+ const handleCopyMessage = useCallback(
61
+ (messageId: string, content: any) => {
62
+ navigator.clipboard.writeText(getMessageContent(content));
63
+ handleCloseContextMenu(messageId);
64
+ },
65
+ [handleCloseContextMenu]
66
+ );
67
+
68
+ const handleToggleDetail = useCallback((messageId: string) => {
69
+ setMessageStates((prev) => {
70
+ const newStates = new Map(prev);
71
+ const currentState = newStates.get(messageId);
72
+ if (currentState) {
73
+ newStates.set(messageId, {
74
+ ...currentState,
75
+ showDetail: !currentState.showDetail,
76
+ showContextMenu: false,
77
+ });
78
+ } else {
79
+ newStates.set(messageId, {
80
+ showDetail: true,
81
+ showContextMenu: false,
82
+ contextMenuPosition: { x: 0, y: 0 },
83
+ });
84
+ }
85
+ return newStates;
86
+ });
87
+ }, []);
88
+
89
+ // 点击外部关闭右键菜单
90
+ useEffect(() => {
91
+ const handleClickOutside = (event: MouseEvent) => {
92
+ messageStates.forEach((state, messageId) => {
93
+ if (state.showContextMenu) {
94
+ const ref = messageRefs.current.get(messageId);
95
+ if (ref && !ref.contains(event.target as Node)) {
96
+ handleCloseContextMenu(messageId);
97
+ }
98
+ }
99
+ });
100
+ };
101
+
102
+ if (Array.from(messageStates.values()).some((state) => state.showContextMenu)) {
103
+ document.addEventListener("click", handleClickOutside);
104
+ }
105
+
106
+ return () => {
107
+ document.removeEventListener("click", handleClickOutside);
108
+ };
109
+ }, [messageStates, handleCloseContextMenu]);
110
+
18
111
  return (
19
112
  <div className="flex flex-col gap-4 w-full">
20
- {renderMessages.map((message, index) => (
21
- <div key={message.unique_id}>
22
- {message.type === "human" ? (
23
- <MessageHuman content={message.content} />
24
- ) : message.type === "tool" ? (
25
- <MessageTool
26
- message={message}
27
- client={client!}
28
- getMessageContent={getMessageContent}
29
- formatTokens={formatTokens}
30
- isCollapsed={collapsedTools.includes(message.id!)}
31
- onToggleCollapse={() => toggleToolCollapse(message.id!)}
32
- />
33
- ) : (
34
- <MessageAI message={message} />
35
- )}
36
- </div>
37
- ))}
113
+ {renderMessages.map((message, index) => {
114
+ const messageId = message.unique_id || `message-${index}`;
115
+ const messageState = messageStates.get(messageId) || {
116
+ showDetail: false,
117
+ showContextMenu: false,
118
+ contextMenuPosition: { x: 0, y: 0 },
119
+ };
120
+
121
+ return (
122
+ <div
123
+ key={messageId}
124
+ ref={(el) => {
125
+ if (el) {
126
+ messageRefs.current.set(messageId, el);
127
+ }
128
+ }}
129
+ onContextMenu={(e) => handleContextMenu(e, messageId)}
130
+ >
131
+ {message.type === "human" ? (
132
+ <MessageHuman content={message.content} />
133
+ ) : message.type === "tool" ? (
134
+ <MessageTool
135
+ message={message}
136
+ client={client!}
137
+ getMessageContent={getMessageContent}
138
+ formatTokens={formatTokens}
139
+ isCollapsed={collapsedTools.includes(message.id!)}
140
+ onToggleCollapse={() => toggleToolCollapse(message.id!)}
141
+ />
142
+ ) : (
143
+ <MessageAI message={message} />
144
+ )}
145
+ {messageState.showDetail && <JSONViewer data={message} />}
146
+ {messageState.showContextMenu && (
147
+ <div
148
+ className="fixed bg-white border border-gray-200 rounded shadow-lg z-50 py-1 min-w-[150px]"
149
+ style={{ left: messageState.contextMenuPosition.x, top: messageState.contextMenuPosition.y }}
150
+ onClick={(e) => e.stopPropagation()}
151
+ >
152
+ <button className="w-full bg-white px-3 py-2 text-left hover:bg-gray-50 text-sm" onClick={() => handleCopyMessage(messageId, message.content)}>
153
+ 复制消息内容
154
+ </button>
155
+ <button className="w-full bg-white px-3 py-2 text-left hover:bg-gray-50 text-sm" onClick={() => handleToggleDetail(messageId)}>
156
+ {messageState.showDetail ? "隐藏详情" : "显示详情"}
157
+ </button>
158
+ </div>
159
+ )}
160
+ </div>
161
+ );
162
+ })}
38
163
  </div>
39
164
  );
40
165
  };
@@ -1,7 +1,5 @@
1
1
  import { createChatStore } from "@langgraph-js/sdk";
2
- import { ask_user_for_approve, update_plan, web_search_tool } from "../tools/index";
3
- import { FullTextSearchService, OpenAIVectorizer, VecDB, createMemoryTool } from "../../memory/index";
4
- import { create_artifacts } from "../tools/create_artifacts";
2
+ import { FullTextSearchService, createMemoryTool } from "../../memory/index";
5
3
 
6
4
  const F =
7
5
  localStorage.getItem("withCredentials") === "true"
@@ -41,12 +39,13 @@ const db = new FullTextSearchService({
41
39
  db.initialize();
42
40
  console.log(db);
43
41
  export const memoryTool = createMemoryTool(db);
42
+ const defaultHeaders = JSON.parse(localStorage.getItem("code") || "{}");
44
43
 
45
44
  export const globalChatStore = createChatStore(
46
45
  localStorage.getItem("agent_name") || "",
47
46
  {
48
47
  apiUrl: localStorage.getItem("apiUrl") || "http://localhost:8123",
49
- defaultHeaders: JSON.parse(localStorage.getItem("code") || "{}"),
48
+ defaultHeaders,
50
49
  callerOptions: {
51
50
  // 携带 cookie 的写法
52
51
  fetch: F,
@@ -54,8 +53,5 @@ export const globalChatStore = createChatStore(
54
53
  },
55
54
  {
56
55
  ...getLocalConfig(),
57
- onInit(client) {
58
- client.tools.bindTools([create_artifacts, web_search_tool, ask_user_for_approve, update_plan, memoryTool.manageMemory, memoryTool.searchMemory]);
59
- },
60
56
  }
61
57
  );
@@ -1,4 +1,4 @@
1
- import { createToolUI, ToolRenderData } from "@langgraph-js/sdk";
1
+ import { createUITool, ToolRenderData } from "@langgraph-js/sdk";
2
2
  import { FileIcon } from "lucide-react";
3
3
  import { useState } from "react";
4
4
  import { useArtifacts } from "../../artifacts/ArtifactsContext";
@@ -15,10 +15,10 @@ interface ArtifactsResponse {
15
15
  artifactsPath?: string;
16
16
  }
17
17
 
18
- export const create_artifacts = createToolUI({
18
+ export const create_artifacts = createUITool({
19
19
  name: "create_artifacts",
20
20
  description: "创建并保存代码文件到 artifacts 目录",
21
- parameters: [],
21
+ parameters: {},
22
22
  onlyRender: true,
23
23
  render(tool: ToolRenderData<ArtifactsInput, ArtifactsResponse>) {
24
24
  const data = tool.getInputRepaired();
@@ -1,5 +1,6 @@
1
- export { ask_user_for_approve } from "./ask_user_for_approve";
2
- export { update_plan } from "./update_plan";
3
- export { web_search_tool } from "./web_search_tool";
1
+ // export { ask_user_for_approve } from "./ask_user_for_approve";
2
+ // export { update_plan } from "./update_plan";
3
+ // export { web_search_tool } from "./web_search_tool";
4
+ export { show_form } from "./show_form";
4
5
  // 在这里添加其他工具的导出
5
6
  // export { other_tool } from "./other_tool";