@langgraph-js/ui 1.1.0 → 1.2.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
@@ -14,8 +14,8 @@
14
14
  height: 100%;
15
15
  }
16
16
  </style>
17
- <script type="module" crossorigin src="/assets/index-CCaE6qp1.js"></script>
18
- <link rel="stylesheet" crossorigin href="/assets/index-CBfok6qC.css">
17
+ <script type="module" crossorigin src="/assets/index-C7SfDwhG.js"></script>
18
+ <link rel="stylesheet" crossorigin href="/assets/index-BlHtM5cu.css">
19
19
  </head>
20
20
  <body>
21
21
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langgraph-js/ui",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
@@ -21,7 +21,7 @@
21
21
  "vite": "^6.2.0",
22
22
  "react-markdown": "^10.1.0",
23
23
  "remark-gfm": "^4.0.1",
24
- "@langgraph-js/sdk": "1.1.6"
24
+ "@langgraph-js/sdk": "1.1.9"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/react": "^19.0.10",
package/src/chat/Chat.tsx CHANGED
@@ -1,51 +1,39 @@
1
1
  import React, { useState } from "react";
2
2
  import "./chat.css";
3
- import MessageHuman from "./components/MessageHuman";
4
- import MessageAI from "./components/MessageAI";
5
- import MessageTool from "./components/MessageTool";
3
+ import { MessagesBox } from "./components/MessageBox";
6
4
  import HistoryList from "./components/HistoryList";
7
5
  import { ChatProvider, useChat } from "./context/ChatContext";
6
+ import { ExtraParamsProvider, useExtraParams } from "./context/ExtraParamsContext";
8
7
  import { UsageMetadata } from "./components/UsageMetadata";
9
- import { formatTime, formatTokens, getMessageContent, Message } from "@langgraph-js/sdk";
8
+ import { formatTime, Message } from "@langgraph-js/sdk";
10
9
  import FileList from "./components/FileList";
10
+ import JsonEditorPopup from "./components/JsonEditorPopup";
11
+ import { JsonToMessageButton } from "./components/JsonToMessage";
11
12
 
12
13
  const ChatMessages: React.FC = () => {
13
14
  const { renderMessages, loading, inChatError, client, collapsedTools, toggleToolCollapse } = useChat();
14
15
 
15
16
  return (
16
17
  <div className="chat-messages">
17
- {renderMessages.map((message) =>
18
- message.type === "human" ? (
19
- <MessageHuman content={message.content} key={message.unique_id} />
20
- ) : message.type === "tool" ? (
21
- <MessageTool
22
- key={message.unique_id}
23
- message={message}
24
- client={client!}
25
- getMessageContent={getMessageContent}
26
- formatTokens={formatTokens}
27
- isCollapsed={collapsedTools.includes(message.id!)}
28
- onToggleCollapse={() => toggleToolCollapse(message.id!)}
29
- />
30
- ) : (
31
- <MessageAI key={message.unique_id} message={message} />
32
- )
33
- )}
18
+ <MessagesBox renderMessages={renderMessages} collapsedTools={collapsedTools} toggleToolCollapse={toggleToolCollapse} client={client!} />
34
19
  {loading && <div className="loading-indicator">正在思考中...</div>}
35
- {inChatError && <div className="error-message">{inChatError}</div>}
20
+ {inChatError && <div className="error-message">{JSON.stringify(inChatError)}</div>}
36
21
  </div>
37
22
  );
38
23
  };
39
24
 
40
25
  const ChatInput: React.FC = () => {
41
26
  const { userInput, setUserInput, loading, sendMessage, stopGeneration, currentAgent, setCurrentAgent, client } = useChat();
42
- const [extraParams, setExtraParams] = useState({});
27
+ const { extraParams } = useExtraParams();
43
28
  const [imageUrls, setImageUrls] = useState<string[]>([]);
44
29
 
45
30
  const handleFileUploaded = (url: string) => {
46
31
  setImageUrls((prev) => [...prev, url]);
47
32
  };
48
-
33
+ const _setCurrentAgent = (agent: string) => {
34
+ localStorage.setItem("agent_name", agent);
35
+ setCurrentAgent(agent);
36
+ };
49
37
  const sendMultiModalMessage = () => {
50
38
  const content: Message[] = [
51
39
  {
@@ -80,14 +68,18 @@ const ChatInput: React.FC = () => {
80
68
 
81
69
  return (
82
70
  <div className="chat-input">
83
- <FileList onFileUploaded={handleFileUploaded} />
84
71
  <div className="chat-input-header">
85
- <select value={currentAgent} onChange={(e) => setCurrentAgent(e.target.value)}>
72
+ <FileList onFileUploaded={handleFileUploaded} />
73
+ <UsageMetadata usage_metadata={client?.tokenCounter || {}} />
74
+ <select value={currentAgent} onChange={(e) => _setCurrentAgent(e.target.value)}>
86
75
  {client?.availableAssistants.map((i) => {
87
- return <option value={i.graph_id}>{i.name}</option>;
76
+ return (
77
+ <option value={i.graph_id} key={i.graph_id}>
78
+ {i.name}
79
+ </option>
80
+ );
88
81
  })}
89
82
  </select>
90
- <UsageMetadata usage_metadata={client?.tokenCounter || {}} />
91
83
  </div>
92
84
  <div className="input-container">
93
85
  <textarea
@@ -112,13 +104,19 @@ const ChatInput: React.FC = () => {
112
104
  };
113
105
 
114
106
  const Chat: React.FC = () => {
107
+ const [isPopupOpen, setIsPopupOpen] = useState(false);
115
108
  const { showHistory, toggleHistoryVisible } = useChat();
109
+ const { extraParams, setExtraParams } = useExtraParams();
116
110
 
117
111
  return (
118
112
  <div className="chat-container">
119
113
  {showHistory && <HistoryList onClose={() => toggleHistoryVisible()} formatTime={formatTime} />}
120
114
  <div className="chat-main">
121
115
  <div className="chat-header">
116
+ <JsonToMessageButton></JsonToMessageButton>
117
+ <button onClick={() => setIsPopupOpen(true)} className="edit-params-button">
118
+ 编辑参数
119
+ </button>
122
120
  <button className="history-button" onClick={() => toggleHistoryVisible()}>
123
121
  历史记录
124
122
  </button>
@@ -134,6 +132,7 @@ const Chat: React.FC = () => {
134
132
  </div>
135
133
  <ChatMessages />
136
134
  <ChatInput />
135
+ <JsonEditorPopup isOpen={isPopupOpen} initialJson={extraParams} onClose={() => setIsPopupOpen(false)} onSave={setExtraParams} />
137
136
  </div>
138
137
  </div>
139
138
  );
@@ -142,7 +141,9 @@ const Chat: React.FC = () => {
142
141
  const ChatWrapper: React.FC = () => {
143
142
  return (
144
143
  <ChatProvider>
145
- <Chat />
144
+ <ExtraParamsProvider>
145
+ <Chat />
146
+ </ExtraParamsProvider>
146
147
  </ChatProvider>
147
148
  );
148
149
  };
package/src/chat/chat.css CHANGED
@@ -260,14 +260,14 @@
260
260
 
261
261
  .chat-input {
262
262
  border-top: 1px solid #e5e7eb;
263
- padding: 0 1rem 1rem 1rem;
263
+ padding: 0.5rem 1rem 1rem 1rem;
264
264
  background-color: #ffffff;
265
265
  }
266
266
  .chat-input-header {
267
267
  display: flex;
268
268
  align-items: center;
269
- justify-content: space-between;
270
- padding: 0.5rem 1rem;
269
+ gap: 0.5rem;
270
+ margin-bottom: 0.5rem;
271
271
  }
272
272
  .input-container {
273
273
  display: flex;
@@ -386,3 +386,18 @@
386
386
  text-align: left;
387
387
  }
388
388
 
389
+ .edit-params-button {
390
+ padding: 6px 12px;
391
+ background-color: #f0f0f0;
392
+ border: 1px solid #e0e0e0;
393
+ border-radius: 4px;
394
+ cursor: pointer;
395
+ font-size: 0.8rem;
396
+ color: #333;
397
+ white-space: nowrap;
398
+ }
399
+
400
+ .edit-params-button:hover {
401
+ background-color: #e0e0e0;
402
+ }
403
+
@@ -1,8 +1,8 @@
1
1
  .file-list {
2
- padding: 0.5rem;
3
- background-color: #f9fafb;
2
+ display: flex;
3
+ gap: 0.5rem;
4
4
  border-radius: 8px;
5
- margin-bottom: 1rem;
5
+ flex:1;
6
6
  }
7
7
 
8
8
  .file-list-header {
@@ -15,16 +15,26 @@
15
15
  justify-content: center;
16
16
  width: 80px;
17
17
  height: 80px;
18
- background-color: #3b82f6;
19
- color: white;
18
+ color: #929292;
19
+ background-color: #ebebeb;
20
20
  border-radius: 6px;
21
21
  cursor: pointer;
22
- font-size: 2rem;
23
- transition: background-color 0.2s;
22
+ transition: all 0.2s;
24
23
  }
25
24
 
26
- .file-upload-button:hover {
27
- background-color: #2563eb;
25
+ .file-upload-button svg {
26
+ width: 32px;
27
+ height: 32px;
28
+ }
29
+
30
+ .file-upload-button.empty {
31
+ width: 32px;
32
+ height: 32px;
33
+ }
34
+
35
+ .file-upload-button.empty svg {
36
+ width: 20px;
37
+ height: 20px;
28
38
  }
29
39
 
30
40
  .file-list-content {
@@ -40,6 +50,10 @@
40
50
  border-radius: 6px;
41
51
  overflow: hidden;
42
52
  }
53
+ .file-item img{
54
+ border: 1px solid #e5e7eb;
55
+ overflow: hidden;
56
+ }
43
57
 
44
58
  .file-preview {
45
59
  width: 100%;
@@ -78,8 +92,7 @@
78
92
  }
79
93
 
80
94
  .upload-button {
81
- background-color: #3b82f6;
82
- color: white;
95
+ color: black;
83
96
  }
84
97
 
85
98
  .upload-button:hover {
@@ -1,6 +1,6 @@
1
- import React, { useState, useCallback } from 'react';
2
- import { TmpFilesClient } from '../FileUpload';
3
- import './FileList.css';
1
+ import React, { useState, useCallback } from "react";
2
+ import { TmpFilesClient } from "../FileUpload";
3
+ import "./FileList.css";
4
4
 
5
5
  interface FileListProps {
6
6
  onFileUploaded: (url: string) => void;
@@ -9,55 +9,58 @@ interface FileListProps {
9
9
  const FileList: React.FC<FileListProps> = ({ onFileUploaded }) => {
10
10
  const [files, setFiles] = useState<File[]>([]);
11
11
  const client = new TmpFilesClient();
12
+ const MAX_FILES = 3;
12
13
 
13
- const handleFileChange = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
14
- const selectedFiles = Array.from(event.target.files || []);
15
- const imageFiles = selectedFiles.filter(file => file.type.startsWith('image/'));
16
- setFiles(prev => [...prev, ...imageFiles]);
17
-
18
- for (const file of imageFiles) {
19
- try {
20
- const result = await client.upload(file);
21
- if (result.data?.url) {
22
- onFileUploaded(result.data.url);
14
+ const handleFileChange = useCallback(
15
+ async (event: React.ChangeEvent<HTMLInputElement>) => {
16
+ const selectedFiles = Array.from(event.target.files || []);
17
+ const imageFiles = selectedFiles.filter((file) => file.type.startsWith("image/"));
18
+
19
+ // 检查是否超过最大数量限制
20
+ if (files.length + imageFiles.length > MAX_FILES) {
21
+ alert(`最多只能上传${MAX_FILES}张图片`);
22
+ event.target.value = "";
23
+ return;
24
+ }
25
+
26
+ setFiles((prev) => [...prev, ...imageFiles]);
27
+
28
+ for (const file of imageFiles) {
29
+ try {
30
+ const result = await client.upload(file);
31
+ if (result.data?.url) {
32
+ onFileUploaded(result.data.url);
33
+ }
34
+ } catch (error) {
35
+ console.error("Upload failed:", error);
23
36
  }
24
- } catch (error) {
25
- console.error('Upload failed:', error);
26
37
  }
27
- }
28
-
29
- // 清空 input 值,允许重复选择相同文件
30
- event.target.value = '';
31
- }, [onFileUploaded]);
38
+
39
+ event.target.value = "";
40
+ },
41
+ [onFileUploaded, files.length]
42
+ );
32
43
 
33
44
  const removeFile = useCallback((index: number) => {
34
- setFiles(prev => prev.filter((_, i) => i !== index));
45
+ setFiles((prev) => prev.filter((_, i) => i !== index));
35
46
  }, []);
36
47
 
37
48
  return (
38
49
  <div className="file-list">
39
- <div className="file-list-content">
40
- <label className="file-upload-button">
41
- <span>+</span>
42
- <input
43
- type="file"
44
- accept="image/*"
45
- multiple
46
- onChange={handleFileChange}
47
- style={{ display: 'none' }}
48
- />
50
+ {files.length < MAX_FILES && (
51
+ <label className={`file-upload-button ${files.length === 0 ? "empty" : ""}`}>
52
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
53
+ <path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/>
54
+ <path d="M20 4h-3.17l-1.24-1.35A1.99 1.99 0 0 0 14.12 2H9.88c-.56 0-1.1.24-1.48.65L7.17 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 13c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z"/>
55
+ </svg>
56
+ <input type="file" accept="image/*" multiple onChange={handleFileChange} style={{ display: "none" }} />
49
57
  </label>
58
+ )}
59
+ <div className="file-list-content">
50
60
  {files.map((file, index) => (
51
61
  <div key={index} className="file-item">
52
- <img
53
- src={URL.createObjectURL(file)}
54
- alt={file.name}
55
- className="file-preview"
56
- />
57
- <button
58
- className="remove-button"
59
- onClick={() => removeFile(index)}
60
- >
62
+ <img src={URL.createObjectURL(file)} alt={file.name} className="file-preview" />
63
+ <button className="remove-button" onClick={() => removeFile(index)}>
61
64
  ×
62
65
  </button>
63
66
  </div>
@@ -67,4 +70,4 @@ const FileList: React.FC<FileListProps> = ({ onFileUploaded }) => {
67
70
  );
68
71
  };
69
72
 
70
- export default FileList;
73
+ export default FileList;
@@ -0,0 +1,81 @@
1
+ .json-editor-popup-overlay {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ right: 0;
6
+ bottom: 0;
7
+ background-color: rgba(0, 0, 0, 0.5);
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ z-index: 1000;
12
+ }
13
+
14
+ .json-editor-popup-content {
15
+ background-color: white;
16
+ padding: 20px;
17
+ border-radius: 8px;
18
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
19
+ width: 80%;
20
+ max-width: 600px;
21
+ display: flex;
22
+ flex-direction: column;
23
+ }
24
+
25
+ .json-editor-popup-content h2 {
26
+ margin-top: 0;
27
+ margin-bottom: 15px;
28
+ font-size: 1.5rem;
29
+ color: #333;
30
+ }
31
+
32
+ .json-editor-popup-content textarea {
33
+ width: 100%;
34
+ padding: 10px;
35
+ border: 1px solid #ccc;
36
+ border-radius: 4px;
37
+ font-family: monospace;
38
+ font-size: 0.9rem;
39
+ resize: vertical;
40
+ box-sizing: border-box;
41
+ }
42
+
43
+ .json-editor-popup-content .error-message {
44
+ color: red;
45
+ font-size: 0.8rem;
46
+ margin-top: 5px;
47
+ margin-bottom: 10px;
48
+ }
49
+
50
+ .popup-actions {
51
+ display: flex;
52
+ justify-content: flex-end;
53
+ margin-top: 15px;
54
+ gap: 10px;
55
+ }
56
+
57
+ .popup-actions button {
58
+ padding: 8px 16px;
59
+ border: none;
60
+ border-radius: 4px;
61
+ cursor: pointer;
62
+ font-size: 0.9rem;
63
+ }
64
+
65
+ .popup-actions .save-button {
66
+ background-color: #3b82f6;
67
+ color: white;
68
+ }
69
+
70
+ .popup-actions .save-button:hover {
71
+ background-color: #2563eb;
72
+ }
73
+
74
+ .popup-actions .cancel-button {
75
+ background-color: #e5e7eb;
76
+ color: #374151;
77
+ }
78
+
79
+ .popup-actions .cancel-button:hover {
80
+ background-color: #d1d5db;
81
+ }
@@ -0,0 +1,57 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import './JsonEditorPopup.css';
3
+
4
+ interface JsonEditorPopupProps {
5
+ isOpen: boolean;
6
+ initialJson: object;
7
+ onClose: () => void;
8
+ onSave: (jsonData: object) => void;
9
+ }
10
+
11
+ const JsonEditorPopup: React.FC<JsonEditorPopupProps> = ({ isOpen, initialJson, onClose, onSave }) => {
12
+ const [jsonString, setJsonString] = useState('');
13
+ const [error, setError] = useState<string | null>(null);
14
+
15
+ useEffect(() => {
16
+ setJsonString(JSON.stringify(initialJson, null, 2));
17
+ setError(null); // Reset error when initialJson changes or popup opens
18
+ }, [initialJson, isOpen]);
19
+
20
+ if (!isOpen) {
21
+ return null;
22
+ }
23
+
24
+ const handleSave = () => {
25
+ try {
26
+ const parsedJson = JSON.parse(jsonString);
27
+ onSave(parsedJson);
28
+ onClose();
29
+ } catch (e) {
30
+ setError('JSON 格式无效,请检查后重试。');
31
+ console.error("Invalid JSON format:", e);
32
+ }
33
+ };
34
+
35
+ return (
36
+ <div className="json-editor-popup-overlay">
37
+ <div className="json-editor-popup-content">
38
+ <h2>编辑 Extra Parameters</h2>
39
+ <textarea
40
+ value={jsonString}
41
+ onChange={(e) => {
42
+ setJsonString(e.target.value);
43
+ setError(null); // Clear error on edit
44
+ }}
45
+ rows={15}
46
+ />
47
+ {error && <p className="error-message">{error}</p>}
48
+ <div className="popup-actions">
49
+ <button onClick={onClose} className="cancel-button">取消</button>
50
+ <button onClick={handleSave} className="save-button">保存</button>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ );
55
+ };
56
+
57
+ export default JsonEditorPopup;
@@ -0,0 +1,104 @@
1
+ .json-to-message-overlay {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ right: 0;
6
+ bottom: 0;
7
+ background-color: rgba(0, 0, 0, 0.5);
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ z-index: 1000;
12
+ }
13
+
14
+ .json-to-message-content {
15
+ background-color: white;
16
+ border-radius: 8px;
17
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
18
+ width: 90%;
19
+ max-width: 1200px;
20
+ height: 80vh;
21
+ max-height: 800px;
22
+ display: flex;
23
+ flex-direction: column;
24
+ overflow: hidden;
25
+ }
26
+
27
+ .json-to-message-header {
28
+ display: flex;
29
+ justify-content: space-between;
30
+ align-items: center;
31
+ padding: 15px 20px;
32
+ border-bottom: 1px solid #eaeaea;
33
+ }
34
+
35
+ .json-to-message-header h2 {
36
+ margin: 0;
37
+ font-size: 1.5rem;
38
+ color: #333;
39
+ }
40
+
41
+ .close-button {
42
+ background: none;
43
+ border: none;
44
+ font-size: 1.5rem;
45
+ cursor: pointer;
46
+ color: #666;
47
+ }
48
+
49
+ .close-button:hover {
50
+ color: #000;
51
+ }
52
+
53
+ .json-to-message-body {
54
+ display: flex;
55
+ flex: 1;
56
+ overflow: hidden;
57
+ }
58
+
59
+ .json-editor-pane {
60
+ flex: 1;
61
+ padding: 15px;
62
+ display: flex;
63
+ flex-direction: column;
64
+ border-right: 1px solid #eaeaea;
65
+ }
66
+
67
+ .json-editor-pane textarea {
68
+ flex: 1;
69
+ font-family: "Monaco", "Menlo", "Ubuntu Mono", "Consolas", monospace;
70
+ font-size: 0.9rem;
71
+ padding: 10px;
72
+ border: 1px solid #ddd;
73
+ border-radius: 4px;
74
+ resize: none;
75
+ }
76
+
77
+ .error-message {
78
+ color: #e53e3e;
79
+ margin-top: 10px;
80
+ font-size: 0.9rem;
81
+ }
82
+
83
+ .message-preview-pane {
84
+ flex: 1;
85
+ padding: 15px;
86
+ overflow-y: auto;
87
+ background-color: #f9f9f9;
88
+ }
89
+
90
+ .preview-container {
91
+ padding: 10px;
92
+ background-color: white;
93
+ border-radius: 4px;
94
+ min-height: 100%;
95
+ }
96
+
97
+ .no-preview {
98
+ display: flex;
99
+ align-items: center;
100
+ justify-content: center;
101
+ height: 100%;
102
+ color: #666;
103
+ font-style: italic;
104
+ }