@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.
- package/.env +2 -0
- package/.env.example +2 -0
- package/dist/assets/index-C0dczJ0v.css +1 -0
- package/dist/assets/index-JlVlMqZ-.js +248 -0
- package/dist/index.html +3 -5
- package/index.html +1 -3
- package/package.json +11 -2
- package/src/artifacts/ArtifactViewer.tsx +158 -0
- package/src/artifacts/ArtifactsContext.tsx +99 -0
- package/src/artifacts/SourceCodeViewer.tsx +15 -0
- package/src/chat/Chat.tsx +100 -32
- package/src/chat/FileUpload/index.ts +3 -7
- package/src/chat/components/FileList.tsx +16 -12
- package/src/chat/components/HistoryList.tsx +39 -137
- package/src/chat/components/JsonEditorPopup.tsx +196 -48
- package/src/chat/components/JsonToMessage/JsonToMessage.tsx +20 -14
- package/src/chat/components/JsonToMessage/JsonToMessageButton.tsx +9 -16
- package/src/chat/components/JsonToMessage/index.tsx +1 -1
- package/src/chat/components/MessageAI.tsx +5 -4
- package/src/chat/components/MessageBox.tsx +20 -19
- package/src/chat/components/MessageHuman.tsx +13 -10
- package/src/chat/components/MessageTool.tsx +85 -8
- package/src/chat/components/UsageMetadata.tsx +41 -22
- package/src/chat/context/ChatContext.tsx +14 -5
- package/src/chat/index.css +4 -0
- package/src/chat/store/index.ts +25 -2
- package/src/chat/tools/ask_user_for_approve.tsx +25 -13
- package/src/chat/tools/create_artifacts.tsx +50 -0
- package/src/chat/tools/index.ts +3 -2
- package/src/chat/tools/update_plan.tsx +75 -0
- package/src/chat/tools/web_search_tool.tsx +89 -0
- package/src/graph/index.tsx +9 -6
- package/src/index.ts +2 -0
- package/src/login/Login.tsx +155 -47
- package/src/memory/BaseDB.ts +92 -0
- package/src/memory/db.ts +232 -0
- package/src/memory/fulltext-search.ts +191 -0
- package/src/memory/index.ts +4 -0
- package/src/memory/tools.ts +170 -0
- package/test/main.tsx +2 -2
- package/vite.config.ts +7 -1
- package/dist/assets/index-CLyKQAUN.js +0 -214
- package/dist/assets/index-D80TEgwy.css +0 -1
- package/src/chat/chat.css +0 -552
- package/src/chat/components/FileList.css +0 -129
- package/src/chat/components/JsonEditorPopup.css +0 -81
- package/src/chat/components/JsonToMessage/JsonToMessage.css +0 -104
- 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="
|
|
14
|
-
<div className="
|
|
15
|
-
<div className="
|
|
16
|
-
<h3>历史记录</h3>
|
|
17
|
-
<button
|
|
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
|
|
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="
|
|
34
|
+
<div className="flex-1 overflow-y-auto p-4">
|
|
26
35
|
<div
|
|
27
|
-
className="
|
|
36
|
+
className="flex flex-col gap-3 cursor-pointer"
|
|
28
37
|
onClick={() => {
|
|
29
38
|
createNewChat();
|
|
30
39
|
}}
|
|
31
40
|
>
|
|
32
|
-
<div className="
|
|
33
|
-
<div className="
|
|
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="
|
|
46
|
+
<div className="text-center text-gray-500 py-8">暂无历史记录</div>
|
|
38
47
|
) : (
|
|
39
|
-
<div className="
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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="
|
|
65
|
+
<div className="flex gap-2 shrink-0">
|
|
52
66
|
<button
|
|
53
|
-
className="
|
|
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="
|
|
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
|
|
2
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
×
|
|
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
|
-
|
|
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="
|
|
85
|
-
<div className="
|
|
86
|
-
<div className="
|
|
87
|
-
<h2>JSON 消息预览</h2>
|
|
88
|
-
<button onClick={onClose} className="
|
|
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="
|
|
94
|
-
<div className="
|
|
95
|
-
<textarea
|
|
96
|
-
|
|
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="
|
|
100
|
-
<div className="
|
|
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="
|
|
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
|
-
|
|
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
|
|
19
|
-
{
|
|
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;
|
|
@@ -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="
|
|
14
|
-
<div className="
|
|
15
|
-
<div className="
|
|
16
|
-
<div className="
|
|
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.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
};
|