@langgraph-js/ui 1.5.0 → 1.7.0

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.
Files changed (48) hide show
  1. package/.env +2 -0
  2. package/.env.example +2 -0
  3. package/dist/assets/index-C0dczJ0v.css +1 -0
  4. package/dist/assets/index-JlVlMqZ-.js +248 -0
  5. package/dist/index.html +3 -5
  6. package/index.html +1 -3
  7. package/package.json +11 -2
  8. package/src/artifacts/ArtifactViewer.tsx +158 -0
  9. package/src/artifacts/ArtifactsContext.tsx +99 -0
  10. package/src/artifacts/SourceCodeViewer.tsx +15 -0
  11. package/src/chat/Chat.tsx +100 -32
  12. package/src/chat/FileUpload/index.ts +3 -7
  13. package/src/chat/components/FileList.tsx +16 -12
  14. package/src/chat/components/HistoryList.tsx +39 -137
  15. package/src/chat/components/JsonEditorPopup.tsx +196 -48
  16. package/src/chat/components/JsonToMessage/JsonToMessage.tsx +20 -14
  17. package/src/chat/components/JsonToMessage/JsonToMessageButton.tsx +9 -16
  18. package/src/chat/components/JsonToMessage/index.tsx +1 -1
  19. package/src/chat/components/MessageAI.tsx +5 -4
  20. package/src/chat/components/MessageBox.tsx +20 -19
  21. package/src/chat/components/MessageHuman.tsx +13 -10
  22. package/src/chat/components/MessageTool.tsx +85 -8
  23. package/src/chat/components/UsageMetadata.tsx +41 -22
  24. package/src/chat/context/ChatContext.tsx +14 -5
  25. package/src/chat/index.css +4 -0
  26. package/src/chat/store/index.ts +25 -2
  27. package/src/chat/tools/ask_user_for_approve.tsx +25 -13
  28. package/src/chat/tools/create_artifacts.tsx +50 -0
  29. package/src/chat/tools/index.ts +3 -2
  30. package/src/chat/tools/update_plan.tsx +75 -0
  31. package/src/chat/tools/web_search_tool.tsx +89 -0
  32. package/src/graph/index.tsx +9 -6
  33. package/src/index.ts +2 -0
  34. package/src/login/Login.tsx +155 -47
  35. package/src/memory/BaseDB.ts +92 -0
  36. package/src/memory/db.ts +232 -0
  37. package/src/memory/fulltext-search.ts +191 -0
  38. package/src/memory/index.ts +4 -0
  39. package/src/memory/tools.ts +170 -0
  40. package/test/main.tsx +2 -2
  41. package/vite.config.ts +7 -1
  42. package/dist/assets/index-CLyKQAUN.js +0 -214
  43. package/dist/assets/index-D80TEgwy.css +0 -1
  44. package/src/chat/chat.css +0 -552
  45. package/src/chat/components/FileList.css +0 -129
  46. package/src/chat/components/JsonEditorPopup.css +0 -81
  47. package/src/chat/components/JsonToMessage/JsonToMessage.css +0 -104
  48. package/src/login/Login.css +0 -93
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { useChat } from "../context/ChatContext";
3
3
  import { getHistoryContent } from "@langgraph-js/sdk";
4
+ import { RefreshCw, X, RotateCcw, Trash2 } from "lucide-react";
4
5
 
5
6
  interface HistoryListProps {
6
7
  onClose: () => void;
@@ -10,62 +11,75 @@ interface HistoryListProps {
10
11
  const HistoryList: React.FC<HistoryListProps> = ({ onClose, formatTime }) => {
11
12
  const { historyList, currentChatId, refreshHistoryList, createNewChat, deleteHistoryChat, toHistoryChat } = useChat();
12
13
  return (
13
- <div className="history-list">
14
- <div className="history-header">
15
- <div className="header-left">
16
- <h3>历史记录</h3>
17
- <button className="refresh-button" onClick={refreshHistoryList} title="刷新列表">
18
- 🔁
14
+ <div className=" bg-white rounded-lg shadow-md h-full flex flex-col border-r">
15
+ <div className="p-4 border-b border-gray-200 flex justify-between items-center h-16">
16
+ <div className="flex items-center gap-3">
17
+ <h3 className="m-0 text-lg text-gray-800">历史记录</h3>
18
+ <button
19
+ className="p-1.5 text-base rounded bg-blue-100 hover:bg-blue-200 transition-all duration-200 flex items-center justify-center hover:scale-110 group"
20
+ onClick={refreshHistoryList}
21
+ title="刷新列表"
22
+ >
23
+ <RefreshCw className="w-4 h-4 text-blue-600 group-hover:text-blue-700 group-hover:rotate-180 transition-all duration-300" />
19
24
  </button>
20
25
  </div>
21
- <button className="close-button" onClick={onClose} title="关闭">
22
-
26
+ <button
27
+ className="p-1.5 text-base rounded bg-red-100 hover:bg-red-200 transition-all duration-200 flex items-center justify-center hover:scale-110 group"
28
+ onClick={onClose}
29
+ title="关闭"
30
+ >
31
+ <X className="w-4 h-4 text-red-600 group-hover:text-red-700" />
23
32
  </button>
24
33
  </div>
25
- <div className="history-content">
34
+ <div className="flex-1 overflow-y-auto p-4">
26
35
  <div
27
- className="history-items"
36
+ className="flex flex-col gap-3 cursor-pointer"
28
37
  onClick={() => {
29
38
  createNewChat();
30
39
  }}
31
40
  >
32
- <div className="history-item">
33
- <div className="history-title"> New Chat</div>
41
+ <div className="flex justify-between items-center p-3 rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors duration-200">
42
+ <div className="text-sm text-gray-800 truncate">New Chat</div>
34
43
  </div>
35
44
  </div>
36
45
  {historyList.length === 0 ? (
37
- <div className="empty-history">暂无历史记录</div>
46
+ <div className="text-center text-gray-500 py-8">暂无历史记录</div>
38
47
  ) : (
39
- <div className="history-items">
48
+ <div className="flex flex-col gap-3 mt-3">
40
49
  {historyList
41
50
  .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
42
51
  .map((thread) => (
43
- <div className={`history-item ${thread.thread_id === currentChatId ? "active" : ""}`} key={thread.thread_id}>
44
- <div className="history-info">
45
- <div className="history-title">{getHistoryContent(thread)}</div>
46
- <div className="history-meta">
47
- <span className="history-time">{formatTime(new Date(thread.created_at))}</span>
48
- <span className="history-status">{thread.status}</span>
52
+ <div
53
+ className={`flex justify-between items-center p-3 rounded-lg transition-colors duration-200 ${
54
+ thread.thread_id === currentChatId ? "bg-blue-50 border border-blue-200" : "bg-gray-50 hover:bg-gray-100"
55
+ }`}
56
+ key={thread.thread_id}
57
+ >
58
+ <div className="flex-1 min-w-0 mr-2">
59
+ <div className="text-sm text-gray-800 mb-1 truncate max-w-[180px]">{getHistoryContent(thread)}</div>
60
+ <div className="flex gap-3 text-xs text-gray-500">
61
+ <span className="truncate max-w-[100px]">{formatTime(new Date(thread.created_at))}</span>
62
+ <span className="truncate max-w-[60px]">{thread.status}</span>
49
63
  </div>
50
64
  </div>
51
- <div className="history-actions">
65
+ <div className="flex gap-2 shrink-0">
52
66
  <button
53
- className="action-button"
67
+ className="p-1.5 text-base rounded bg-green-100 hover:bg-green-200 transition-all duration-200 flex items-center justify-center hover:scale-110 group"
54
68
  onClick={() => {
55
69
  toHistoryChat(thread);
56
70
  }}
57
71
  title="恢复对话"
58
72
  >
59
-
73
+ <RotateCcw className="w-4 h-4 text-green-600 group-hover:text-green-700 group-hover:-rotate-180 transition-all duration-300" />
60
74
  </button>
61
75
  <button
62
- className="action-button"
76
+ className="p-1.5 text-base rounded bg-red-100 hover:bg-red-200 transition-all duration-200 flex items-center justify-center hover:scale-110 group"
63
77
  onClick={async () => {
64
78
  await deleteHistoryChat(thread);
65
79
  }}
66
80
  title="删除对话"
67
81
  >
68
-
82
+ <Trash2 className="w-4 h-4 text-red-600 group-hover:text-red-700" />
69
83
  </button>
70
84
  </div>
71
85
  </div>
@@ -73,118 +87,6 @@ const HistoryList: React.FC<HistoryListProps> = ({ onClose, formatTime }) => {
73
87
  </div>
74
88
  )}
75
89
  </div>
76
- <style>{`
77
- .history-list {
78
- background: #fff;
79
- border-radius: 8px;
80
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
81
- height: 100%;
82
- display: flex;
83
- flex-direction: column;
84
- }
85
-
86
- .history-header {
87
- padding: 16px;
88
- border-bottom: 1px solid #eee;
89
- display: flex;
90
- justify-content: space-between;
91
- align-items: center;
92
- }
93
-
94
- .header-left {
95
- display: flex;
96
- align-items: center;
97
- gap: 12px;
98
- }
99
-
100
- .history-header h3 {
101
- margin: 0;
102
- font-size: 18px;
103
- color: #333;
104
- }
105
-
106
- .history-content {
107
- flex: 1;
108
- overflow-y: auto;
109
- padding: 16px;
110
- }
111
-
112
- .history-items {
113
- display: flex;
114
- flex-direction: column;
115
- gap: 12px;
116
- }
117
-
118
- .history-item {
119
- display: flex;
120
- justify-content: space-between;
121
- align-items: center;
122
- padding: 12px;
123
- border-radius: 6px;
124
- background: #f8f9fa;
125
- transition: all 0.2s ease;
126
- }
127
-
128
- .history-item:hover {
129
- background: #f0f2f5;
130
- }
131
-
132
- .history-item.active {
133
- background: #e6f7ff;
134
- border: 1px solid #91d5ff;
135
- }
136
-
137
- .history-info {
138
- flex: 1;
139
- min-width: 0;
140
- }
141
-
142
- .history-title {
143
- font-size: 14px;
144
- color: #333;
145
- margin-bottom: 4px;
146
- white-space: nowrap;
147
- overflow: hidden;
148
- text-overflow: ellipsis;
149
- }
150
-
151
- .history-meta {
152
- display: flex;
153
- gap: 12px;
154
- color: #666;
155
- font-size: 12px;
156
- }
157
-
158
- .history-actions {
159
- display: flex;
160
- gap: 8px;
161
- margin-left: 12px;
162
- }
163
-
164
- .action-button, .close-button, .refresh-button {
165
- background: none;
166
- border: none;
167
- cursor: pointer;
168
- padding: 6px;
169
- font-size: 16px;
170
- border-radius: 4px;
171
- transition: all 0.2s ease;
172
- display: flex;
173
- align-items: center;
174
- justify-content: center;
175
- }
176
-
177
- .action-button:hover, .close-button:hover, .refresh-button:hover {
178
- background: rgba(0, 0, 0, 0.05);
179
- transform: scale(1.1);
180
- }
181
-
182
- .empty-history {
183
- text-align: center;
184
- color: #999;
185
- padding: 32px 0;
186
- }
187
- `}</style>
188
90
  </div>
189
91
  );
190
92
  };
@@ -1,57 +1,205 @@
1
- import React, { useState, useEffect } from 'react';
2
- import './JsonEditorPopup.css';
1
+ import React, { useState, useEffect } from "react";
2
+
3
+ interface JsonPreset {
4
+ name: string;
5
+ data: object;
6
+ }
3
7
 
4
8
  interface JsonEditorPopupProps {
5
- isOpen: boolean;
6
- initialJson: object;
7
- onClose: () => void;
8
- onSave: (jsonData: object) => void;
9
+ isOpen: boolean;
10
+ initialJson: object;
11
+ onClose: () => void;
12
+ onSave: (jsonData: object) => void;
9
13
  }
10
14
 
15
+ const LOCAL_STORAGE_KEY = "json-editor-presets";
16
+ const ACTIVE_TAB_STORAGE_KEY = "json-editor-active-tab";
17
+
11
18
  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);
19
+ const [presets, setPresets] = useState<JsonPreset[]>([]);
20
+ const [activeTab, setActiveTab] = useState(0);
21
+ const [jsonString, setJsonString] = useState("");
22
+ const [error, setError] = useState<string | null>(null);
23
+ const [editingTab, setEditingTab] = useState<number | null>(null);
24
+ const [editingName, setEditingName] = useState("");
25
+
26
+ useEffect(() => {
27
+ if (isOpen) {
28
+ let storedData: JsonPreset[] = [];
29
+ try {
30
+ const item = localStorage.getItem(LOCAL_STORAGE_KEY);
31
+ storedData = item ? JSON.parse(item) : [];
32
+ } catch (e) {
33
+ console.error("Failed to parse presets from localStorage", e);
34
+ storedData = [];
35
+ }
36
+
37
+ if (storedData.length === 0) {
38
+ storedData = [{ name: "Default", data: initialJson }];
39
+ }
40
+ setPresets(storedData);
41
+
42
+ let activeTabIndex = 0;
43
+ try {
44
+ const storedIndex = localStorage.getItem(ACTIVE_TAB_STORAGE_KEY);
45
+ if (storedIndex) {
46
+ const parsedIndex = parseInt(storedIndex, 10);
47
+ if (parsedIndex >= 0 && parsedIndex < storedData.length) {
48
+ activeTabIndex = parsedIndex;
49
+ }
50
+ }
51
+ } catch (e) {
52
+ console.error("Failed to parse active tab from localStorage", e);
53
+ }
54
+ setActiveTab(activeTabIndex);
55
+ setError(null);
56
+ }
57
+ }, [isOpen, initialJson]);
58
+
59
+ useEffect(() => {
60
+ if (isOpen && presets.length > 0 && presets[activeTab]) {
61
+ setJsonString(JSON.stringify(presets[activeTab].data, null, 2));
62
+ setError(null);
63
+ }
64
+ }, [activeTab, presets, isOpen]);
65
+
66
+ useEffect(() => {
67
+ if (isOpen) {
68
+ localStorage.setItem(ACTIVE_TAB_STORAGE_KEY, activeTab.toString());
69
+ }
70
+ }, [activeTab, isOpen]);
71
+
72
+ if (!isOpen) {
73
+ return null;
32
74
  }
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>
75
+
76
+ const updatePresetsInStorage = (newPresets: JsonPreset[]) => {
77
+ setPresets(newPresets);
78
+ localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newPresets));
79
+ };
80
+
81
+ const handleAddTab = () => {
82
+ const newPreset: JsonPreset = { name: `Preset ${presets.length + 1}`, data: {} };
83
+ const newPresets = [...presets, newPreset];
84
+ updatePresetsInStorage(newPresets);
85
+ setActiveTab(newPresets.length - 1);
86
+ };
87
+
88
+ const handleDeleteTab = (indexToDelete: number) => {
89
+ let newPresets = presets.filter((_, index) => index !== indexToDelete);
90
+ if (newPresets.length === 0) {
91
+ newPresets = [{ name: "Default", data: initialJson }];
92
+ }
93
+ updatePresetsInStorage(newPresets);
94
+ if (activeTab >= indexToDelete && activeTab > 0) {
95
+ setActiveTab(activeTab - 1);
96
+ } else if (activeTab >= newPresets.length) {
97
+ setActiveTab(newPresets.length - 1);
98
+ }
99
+ };
100
+
101
+ const handleSave = () => {
102
+ try {
103
+ const parsedJson = JSON.parse(jsonString);
104
+ const updatedPresets = presets.map((preset, index) => {
105
+ if (index === activeTab) {
106
+ return { ...preset, data: parsedJson };
107
+ }
108
+ return preset;
109
+ });
110
+ updatePresetsInStorage(updatedPresets);
111
+ onSave(parsedJson);
112
+ onClose();
113
+ } catch (e) {
114
+ setError("JSON 格式无效,请检查后重试。");
115
+ console.error("Invalid JSON format:", e);
116
+ }
117
+ };
118
+
119
+ const handleRename = (index: number) => {
120
+ if (editingName.trim() !== "") {
121
+ const newPresets = presets.map((p, i) => (i === index ? { ...p, name: editingName.trim() } : p));
122
+ updatePresetsInStorage(newPresets);
123
+ setEditingTab(null);
124
+ }
125
+ };
126
+
127
+ return (
128
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
129
+ <div className="bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4 flex flex-col" style={{ height: "80vh" }}>
130
+ <div className="p-6 border-b border-gray-200">
131
+ <h2 className="text-xl font-semibold text-gray-900">编辑 Extra Parameters</h2>
132
+ </div>
133
+ <div className="flex-grow p-6 overflow-y-auto">
134
+ <div className="flex items-center border-b border-gray-200 mb-4">
135
+ {presets.map((preset, index) => (
136
+ <div
137
+ key={index}
138
+ className={`flex items-center px-4 py-2 border-b-2 cursor-pointer text-sm font-medium ${
139
+ activeTab === index ? "border-blue-500 text-blue-600" : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
140
+ }`}
141
+ onDoubleClick={() => {
142
+ setEditingTab(index);
143
+ setEditingName(preset.name);
144
+ }}
145
+ onClick={() => setActiveTab(index)}
146
+ >
147
+ {editingTab === index ? (
148
+ <input
149
+ type="text"
150
+ value={editingName}
151
+ onChange={(e) => setEditingName(e.target.value)}
152
+ onBlur={() => handleRename(index)}
153
+ onKeyDown={(e) => e.key === "Enter" && handleRename(index)}
154
+ className="text-sm p-0 border-none focus:ring-0 bg-transparent"
155
+ autoFocus
156
+ />
157
+ ) : (
158
+ <span>{preset.name}</span>
159
+ )}
160
+ <button
161
+ onClick={(e) => {
162
+ e.stopPropagation();
163
+ handleDeleteTab(index);
164
+ }}
165
+ className="ml-2 text-gray-400 hover:text-gray-600 w-4 h-4 flex items-center justify-center"
166
+ >
167
+ &times;
168
+ </button>
169
+ </div>
170
+ ))}
171
+ <button onClick={handleAddTab} className="ml-2 px-3 py-1 text-sm text-gray-500 hover:text-gray-700">
172
+ +
173
+ </button>
174
+ </div>
175
+
176
+ <textarea
177
+ value={jsonString}
178
+ onChange={(e) => {
179
+ setJsonString(e.target.value);
180
+ setError(null); // Clear error on edit
181
+ }}
182
+ className="w-full h-full p-3 border border-gray-300 rounded-lg font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
183
+ />
184
+ {error && <p className="mt-2 text-sm text-red-600">{error}</p>}
185
+ </div>
186
+ <div className="flex justify-end gap-3 p-6 border-t border-gray-200 bg-gray-50 rounded-b-lg">
187
+ <button
188
+ onClick={onClose}
189
+ className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
190
+ >
191
+ 取消
192
+ </button>
193
+ <button
194
+ onClick={handleSave}
195
+ className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
196
+ >
197
+ 保存并使用
198
+ </button>
199
+ </div>
200
+ </div>
51
201
  </div>
52
- </div>
53
- </div>
54
- );
202
+ );
55
203
  };
56
204
 
57
- export default JsonEditorPopup;
205
+ export default JsonEditorPopup;
@@ -1,7 +1,7 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { MessagesBox } from "../MessageBox";
3
- import "./JsonToMessage.css";
4
3
  import { RenderMessage } from "@langgraph-js/sdk";
4
+ import { X } from "lucide-react";
5
5
 
6
6
  interface JsonToMessageProps {
7
7
  isOpen: boolean;
@@ -81,27 +81,33 @@ const JsonToMessage: React.FC<JsonToMessageProps> = ({ isOpen, onClose, initialJ
81
81
  }
82
82
 
83
83
  return (
84
- <div className="json-to-message-overlay">
85
- <div className="json-to-message-content">
86
- <div className="json-to-message-header">
87
- <h2>JSON 消息预览</h2>
88
- <button onClick={onClose} className="close-button">
89
- ×
84
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
85
+ <div className="bg-white rounded-lg shadow-xl w-[90%] max-w-[1200px] h-[80vh] max-h-[800px] flex flex-col overflow-hidden">
86
+ <div className="flex justify-between items-center p-4 border-b border-gray-200">
87
+ <h2 className="text-xl font-semibold text-gray-900">JSON 消息预览</h2>
88
+ <button onClick={onClose} className="p-1.5 text-gray-500 hover:text-gray-900 transition-colors rounded-lg hover:bg-gray-100">
89
+ <X className="w-5 h-5" />
90
90
  </button>
91
91
  </div>
92
92
 
93
- <div className="json-to-message-body">
94
- <div className="json-editor-pane">
95
- <textarea value={jsonString} onChange={(e) => setJsonString(e.target.value)} rows={20} placeholder="输入JSON消息格式..." />
96
- {error && <p className="error-message">{error}</p>}
93
+ <div className="flex flex-1 overflow-hidden">
94
+ <div className="flex-1 p-4 flex flex-col border-r border-gray-200">
95
+ <textarea
96
+ value={jsonString}
97
+ onChange={(e) => setJsonString(e.target.value)}
98
+ rows={20}
99
+ placeholder="输入JSON消息格式..."
100
+ className="flex-1 font-mono text-sm p-3 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
101
+ />
102
+ {error && <p className="mt-2 text-sm text-red-600">{error}</p>}
97
103
  </div>
98
104
 
99
- <div className="message-preview-pane">
100
- <div className="preview-container chat-messages">
105
+ <div className="flex-1 p-4 overflow-y-auto bg-gray-50">
106
+ <div className="bg-white rounded-lg p-4 min-h-full">
101
107
  {previewMessages?.length > 0 ? (
102
108
  <MessagesBox renderMessages={previewMessages} collapsedTools={[]} toggleToolCollapse={() => {}} client={null as any} />
103
109
  ) : (
104
- <div className="no-preview">请输入有效的JSON以查看消息预览</div>
110
+ <div className="flex items-center justify-center h-full text-gray-500 italic">请输入有效的JSON以查看消息预览</div>
105
111
  )}
106
112
  </div>
107
113
  </div>
@@ -1,27 +1,20 @@
1
1
  import React, { useState } from "react";
2
2
  import JsonToMessage from "./JsonToMessage";
3
+ import { FileJson } from "lucide-react";
3
4
 
4
- interface JsonToMessageButtonProps {
5
- buttonText?: string;
6
- initialJson?: string | object;
7
- className?: string;
8
- }
9
-
10
- const JsonToMessageButton: React.FC<JsonToMessageButtonProps> = ({ buttonText = "JSON 预览", initialJson, className = "" }) => {
5
+ export const JsonToMessageButton: React.FC = () => {
11
6
  const [isOpen, setIsOpen] = useState(false);
12
7
 
13
- const openModal = () => setIsOpen(true);
14
- const closeModal = () => setIsOpen(false);
15
-
16
8
  return (
17
9
  <>
18
- <button className={`history-button ${className}`} onClick={openModal}>
19
- {buttonText}
10
+ <button
11
+ onClick={() => setIsOpen(true)}
12
+ 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"
13
+ >
14
+ <FileJson className="w-4 h-4" />
15
+ JSON 消息
20
16
  </button>
21
-
22
- <JsonToMessage isOpen={isOpen} onClose={closeModal} initialJson={initialJson} />
17
+ <JsonToMessage isOpen={isOpen} onClose={() => setIsOpen(false)} />
23
18
  </>
24
19
  );
25
20
  };
26
-
27
- export default JsonToMessageButton;
@@ -1,5 +1,5 @@
1
1
  import JsonToMessage from "./JsonToMessage";
2
- import JsonToMessageButton from "./JsonToMessageButton";
2
+ import { JsonToMessageButton } from "./JsonToMessageButton";
3
3
 
4
4
  export { JsonToMessage, JsonToMessageButton };
5
5
  export default JsonToMessage;
@@ -4,16 +4,17 @@ import { UsageMetadata } from "./UsageMetadata";
4
4
  import { getMessageContent } from "@langgraph-js/sdk";
5
5
  import Markdown from "react-markdown";
6
6
  import remarkGfm from "remark-gfm";
7
+
7
8
  interface MessageAIProps {
8
9
  message: RenderMessage;
9
10
  }
10
11
 
11
12
  const MessageAI: React.FC<MessageAIProps> = ({ message }) => {
12
13
  return (
13
- <div className="message ai">
14
- <div className="message-content">
15
- <div className="message-title">{message.name}</div>
16
- <div className="message-text markdown-body">
14
+ <div className="flex flex-col w-[80%] bg-white rounded-lg shadow-sm border border-gray-200">
15
+ <div className="flex flex-col p-4">
16
+ <div className="text-sm font-medium text-gray-700 mb-2">{message.name}</div>
17
+ <div className="markdown-body max-w-none">
17
18
  <Markdown remarkPlugins={[remarkGfm]}>{getMessageContent(message.content)}</Markdown>
18
19
  </div>
19
20
  <UsageMetadata response_metadata={message.response_metadata as any} usage_metadata={message.usage_metadata || {}} spend_time={message.spend_time} id={message.id} />
@@ -16,24 +16,25 @@ export const MessagesBox = ({
16
16
  client: LangGraphClient;
17
17
  }) => {
18
18
  return (
19
- <>
20
- {renderMessages.map((message) =>
21
- message.type === "human" ? (
22
- <MessageHuman content={message.content} key={message.unique_id} />
23
- ) : message.type === "tool" ? (
24
- <MessageTool
25
- key={message.unique_id}
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 key={message.unique_id} message={message} />
35
- )
36
- )}
37
- </>
19
+ <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
+ ))}
38
+ </div>
38
39
  );
39
40
  };