@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/assets/index-BlHtM5cu.css +1 -0
- package/dist/assets/index-C7SfDwhG.js +192 -0
- package/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/chat/Chat.tsx +30 -29
- package/src/chat/chat.css +18 -3
- package/src/chat/components/FileList.css +24 -11
- package/src/chat/components/FileList.tsx +44 -41
- package/src/chat/components/JsonEditorPopup.css +81 -0
- package/src/chat/components/JsonEditorPopup.tsx +57 -0
- package/src/chat/components/JsonToMessage/JsonToMessage.css +104 -0
- package/src/chat/components/JsonToMessage/JsonToMessage.tsx +114 -0
- package/src/chat/components/JsonToMessage/JsonToMessageButton.tsx +27 -0
- package/src/chat/components/JsonToMessage/index.tsx +5 -0
- package/src/chat/components/MessageBox.tsx +39 -0
- package/src/chat/context/ChatContext.tsx +4 -2
- package/src/chat/context/ExtraParamsContext.tsx +42 -0
- package/src/chat/store/index.ts +2 -1
- package/dist/assets/index-CBfok6qC.css +0 -1
- package/dist/assets/index-CCaE6qp1.js +0 -192
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-
|
|
18
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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
|
|
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.
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
<
|
|
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
|
|
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
|
-
<
|
|
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
|
-
|
|
270
|
-
|
|
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
|
-
|
|
3
|
-
|
|
2
|
+
display: flex;
|
|
3
|
+
gap: 0.5rem;
|
|
4
4
|
border-radius: 8px;
|
|
5
|
-
|
|
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
|
-
|
|
19
|
-
color:
|
|
18
|
+
color: #929292;
|
|
19
|
+
background-color: #ebebeb;
|
|
20
20
|
border-radius: 6px;
|
|
21
21
|
cursor: pointer;
|
|
22
|
-
|
|
23
|
-
transition: background-color 0.2s;
|
|
22
|
+
transition: all 0.2s;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
.file-upload-button
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
2
|
-
import { TmpFilesClient } from
|
|
3
|
-
import
|
|
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(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
<
|
|
40
|
-
<label className=
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
+
}
|