@langgraph-js/ui 3.1.1 → 4.0.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 +1 -1
- package/.langgraph_api/trace.db +0 -0
- package/.langgraph_api/trace.db-shm +0 -0
- package/.langgraph_api/trace.db-wal +0 -0
- package/dist/assets/{arc-CGVbGqUF.js → arc-B4gD06tH.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-CqtrDEw9.js → architectureDiagram-VXUJARFQ-BgFtItBV.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-CNqe-K1B.js → blockDiagram-VD42YOAC-BFGzNg6Q.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-CPTzKGRp.js → c4Diagram-YG6GDRKO-CuVylzXj.js} +1 -1
- package/dist/assets/channel-BKsvQ92l.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-CnFclA68.js → chunk-4BX2VUAB-cU_FCghb.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-CVyjVz79.js → chunk-55IACEB6-Whk-5ZBd.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-DiGvJtfO.js → chunk-B4BG7PRW-CreLfPEt.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-CiqWzq60.js → chunk-DI55MBZ5-DeFeTByd.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-KfYlm8ba.js → chunk-FMBD7UC4-D8IFRGWy.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-Bwbc5Dxk.js → chunk-QN33PNHL-BtcE2bYr.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-DlQbvjLB.js → chunk-QZHKN3VN-Dqyf850H.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-6_Y4HlEU.js → chunk-TZMSLE5B-B9mCm-HN.js} +1 -1
- package/dist/assets/classDiagram-2ON5EDUG-DY6hfbKg.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-DY6hfbKg.js +1 -0
- package/dist/assets/clone-DiADKkIv.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-BL_xZw_z.js → cose-bilkent-S5V4N54A-BsyvYd6P.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-CKK6NwPE.js → dagre-6UL2VRFP-CJWBl4IX.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-D77jB3aN.js → diagram-PSM6KHXK-s-3r9foi.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-C3V63KEf.js → diagram-QEK2KX5R-C6wNh_Hf.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-8vXvyFcA.js → diagram-S2PKOQOG-CpOHanaU.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-BvdZN7ll.js → erDiagram-Q2GNP2WA-CGoTaByR.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-Cmn4g1Tt.js → flowDiagram-NV44I4VS-BwGzdn6-.js} +1 -1
- package/dist/assets/{ganttDiagram-LVOFAZNH-BtK3XTtk.js → ganttDiagram-LVOFAZNH-qaR08rFo.js} +1 -1
- package/dist/assets/{gitGraphDiagram-NY62KEGX-Ckd-OTVj.js → gitGraphDiagram-NY62KEGX-MjuWAJcA.js} +1 -1
- package/dist/assets/{graph-C8xl-aUs.js → graph-BHPAGsrk.js} +1 -1
- package/dist/assets/index-BwQLftC0.css +1 -0
- package/dist/assets/index-CeSzlsK_.js +1 -0
- package/dist/assets/{index-EdVqpCiz.js → index-Cw_pxs59.js} +171 -177
- package/dist/assets/{infoDiagram-F6ZHWCRC-Z0X_yr7r.js → infoDiagram-F6ZHWCRC-DJxuxLb8.js} +1 -1
- package/dist/assets/{isUndefined-Daq2Snc8.js → isUndefined-BZle6qE3.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-Ce4GbjgA.js → journeyDiagram-XKPGCS4Q-Bx-5B721.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-jKKX8cF-.js → kanban-definition-3W4ZIXB7-BzyJeB-d.js} +1 -1
- package/dist/assets/{layout-CehWuyQv.js → layout-D9r9e3x5.js} +1 -1
- package/dist/assets/{linear-Do6UjTtT.js → linear-CH-g37GF.js} +1 -1
- package/dist/assets/{mermaid.core-DboFnVDg.js → mermaid.core-DQOzsCw4.js} +5 -5
- package/dist/assets/min-Bkz53M0y.js +1 -0
- package/dist/assets/{mindmap-definition-VGOIOE7T-DHqc52RG.js → mindmap-definition-VGOIOE7T-Dmukcpsg.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-bG2J_WkI.js → pieDiagram-ADFJNKIX-D2A5t1l4.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-CDEBrcZS.js → quadrantDiagram-AYHSOK5B-fSb36W--.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-D88jc_6e.js → requirementDiagram-UZGBJVZJ-Clv63BU0.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-C93LzfJ0.js → sankeyDiagram-TZEHDZUN-CWJ180dG.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-DjpL4ZUq.js → sequenceDiagram-WL72ISMW-DgluEJrO.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-Cy4Qc8UW.js → stateDiagram-FKZM4ZOC-Cn6H8hoO.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-CzszURC6.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-DNVlI21w.js → timeline-definition-IT6M3QCI-D650ceX-.js} +1 -1
- package/dist/assets/{treemap-KMMF4GRG-Cf7rPol4.js → treemap-KMMF4GRG-DXDGlAhA.js} +1 -1
- package/dist/assets/{xychartDiagram-PRI3JC2R-CRudqT7y.js → xychartDiagram-PRI3JC2R-B5EWvgyz.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +5 -4
- package/src/artifacts/ArtifactViewer.tsx +2 -2
- package/src/chat/Chat.tsx +133 -141
- package/src/chat/components/FileList.tsx +34 -78
- package/src/chat/components/FileListContainer.tsx +14 -0
- package/src/chat/components/FileListContext.tsx +157 -0
- package/src/chat/components/HistoryList.tsx +1 -1
- package/src/chat/components/MessageAI.tsx +1 -1
- package/src/chat/components/UploadButton.tsx +20 -0
- package/src/chat/components/UsageMetadata.tsx +10 -1
- package/src/index.ts +0 -1
- package/src/monitor/index.tsx +70 -0
- package/src/settings/ConsoleSettings.tsx +48 -0
- package/src/settings/LoginSettings.tsx +64 -11
- package/src/settings/SettingPanel.tsx +6 -5
- package/vite.config.ts +48 -2
- package/dist/assets/channel-CbJkpW4g.js +0 -1
- package/dist/assets/classDiagram-2ON5EDUG--F4r2fzQ.js +0 -1
- package/dist/assets/classDiagram-v2-WZHVMYZB--F4r2fzQ.js +0 -1
- package/dist/assets/clone-CxJednSn.js +0 -1
- package/dist/assets/index-BpNQAzdK.css +0 -1
- package/dist/assets/min-CoD9aTVe.js +0 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-wfRa7ccT.js +0 -1
package/src/chat/Chat.tsx
CHANGED
|
@@ -6,11 +6,12 @@ import { ExtraParamsProvider, useExtraParams } from "./context/ExtraParamsContex
|
|
|
6
6
|
import { UsageMetadata } from "./components/UsageMetadata";
|
|
7
7
|
import { Message } from "@langgraph-js/sdk";
|
|
8
8
|
import FileList from "./components/FileList";
|
|
9
|
+
import { FileListProvider, useFileList } from "./components/FileListContext";
|
|
9
10
|
import type { SupportedFileType } from "./components/FileList";
|
|
10
11
|
import JsonEditorPopup from "./components/JsonEditorPopup";
|
|
11
12
|
import { GraphPanel } from "../graph/GraphPanel";
|
|
12
13
|
import { setLocalConfig } from "./store";
|
|
13
|
-
import { History, Network, FileJson, Settings, Send } from "lucide-react";
|
|
14
|
+
import { History, Network, FileJson, Settings, Send, UploadCloudIcon } from "lucide-react";
|
|
14
15
|
import { ArtifactViewer } from "../artifacts/ArtifactViewer";
|
|
15
16
|
import "github-markdown-css/github-markdown.css";
|
|
16
17
|
|
|
@@ -20,6 +21,10 @@ import { create_artifacts } from "./tools/create_artifacts";
|
|
|
20
21
|
import SettingPanel from "../settings/SettingPanel";
|
|
21
22
|
import { toast } from "sonner";
|
|
22
23
|
import { Toaster } from "@/components/ui/sonner";
|
|
24
|
+
|
|
25
|
+
import { MonitorProvider, Monitor, useMonitor } from "../monitor";
|
|
26
|
+
import UploadButton from "./components/UploadButton";
|
|
27
|
+
|
|
23
28
|
const ChatMessages: React.FC = () => {
|
|
24
29
|
const { renderMessages, loading, inChatError, client, collapsedTools, toggleToolCollapse, isFELocking } = useChat();
|
|
25
30
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
@@ -55,7 +60,19 @@ const ChatMessages: React.FC = () => {
|
|
|
55
60
|
|
|
56
61
|
return (
|
|
57
62
|
<div className="flex-1 overflow-y-auto overflow-x-hidden p-6 bg-gray-100" ref={MessageContainer}>
|
|
58
|
-
|
|
63
|
+
{renderMessages.length === 0 && !loading ? (
|
|
64
|
+
<div className="flex items-center justify-center h-full">
|
|
65
|
+
<div className="text-center">
|
|
66
|
+
<div className="flex items-center justify-center">
|
|
67
|
+
<div className="text-6xl mb-4 w-24 h-24 border border-green-300 rounded-full p-4 bg-green-100">🦜</div>
|
|
68
|
+
</div>
|
|
69
|
+
<h1 className="text-4xl font-bold text-gray-700 mb-2">LangGraph Console</h1>
|
|
70
|
+
<div className="text-lg text-gray-500">AI 助手控制台</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
) : (
|
|
74
|
+
<MessagesBox renderMessages={renderMessages} collapsedTools={collapsedTools} toggleToolCollapse={toggleToolCollapse} client={client!} />
|
|
75
|
+
)}
|
|
59
76
|
{/* {isFELocking() && <div className="flex items-center justify-center py-4 text-gray-500">请你继续操作</div>} */}
|
|
60
77
|
{loading && !isFELocking() && (
|
|
61
78
|
<div className="flex items-center justify-center py-6 text-gray-500">
|
|
@@ -70,37 +87,10 @@ const ChatMessages: React.FC = () => {
|
|
|
70
87
|
};
|
|
71
88
|
|
|
72
89
|
const ChatInput: React.FC = () => {
|
|
73
|
-
const { userInput, setUserInput, loading, sendMessage, stopGeneration, currentAgent, setCurrentAgent, client, currentChatId } = useChat();
|
|
90
|
+
const { userInput, renderMessages, setUserInput, loading, sendMessage, stopGeneration, currentAgent, setCurrentAgent, client, currentChatId } = useChat();
|
|
74
91
|
const { extraParams } = useExtraParams();
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
image: false,
|
|
78
|
-
video: false,
|
|
79
|
-
audio: false,
|
|
80
|
-
other: true,
|
|
81
|
-
});
|
|
82
|
-
const handleFileUploaded = (url: string, fileType: SupportedFileType) => {
|
|
83
|
-
// 上传时始终保存原始文件信息,在发送时根据文本模式决定格式
|
|
84
|
-
if (fileType === "image") {
|
|
85
|
-
setMediaUrls((prev) => [...prev, { type: "image_url", image_url: { url }, fileType }]);
|
|
86
|
-
} else if (fileType === "video") {
|
|
87
|
-
setMediaUrls((prev) => [...prev, { type: "video_url", video_url: { url }, fileType }]);
|
|
88
|
-
} else if (fileType === "audio") {
|
|
89
|
-
setMediaUrls((prev) => [...prev, { type: "audio_url", audio_url: { url }, fileType }]);
|
|
90
|
-
} else if (fileType === "other") {
|
|
91
|
-
setMediaUrls((prev) => [...prev, { type: "file_url", file_url: { url }, fileType }]);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const handleFileRemoved = (url: string, fileType: SupportedFileType) => {
|
|
96
|
-
// 删除时移除对应的媒体文件信息
|
|
97
|
-
setMediaUrls((prev) =>
|
|
98
|
-
prev.filter((media) => {
|
|
99
|
-
const mediaUrl = media.image_url?.url || media.video_url?.url || media.audio_url?.url || media.file_url?.url;
|
|
100
|
-
return mediaUrl !== url;
|
|
101
|
-
})
|
|
102
|
-
);
|
|
103
|
-
};
|
|
92
|
+
const { openMonitorWithChat } = useMonitor();
|
|
93
|
+
const { mediaUrls, isFileTextMode, setIsFileTextMode } = useFileList();
|
|
104
94
|
const _setCurrentAgent = (agent: string) => {
|
|
105
95
|
localStorage.setItem("defaultAgent", agent);
|
|
106
96
|
setCurrentAgent(agent);
|
|
@@ -130,7 +120,7 @@ const ChatInput: React.FC = () => {
|
|
|
130
120
|
type: "text",
|
|
131
121
|
text: userInput,
|
|
132
122
|
},
|
|
133
|
-
...processedMediaUrls,
|
|
123
|
+
...(processedMediaUrls as any),
|
|
134
124
|
],
|
|
135
125
|
},
|
|
136
126
|
];
|
|
@@ -146,77 +136,38 @@ const ChatInput: React.FC = () => {
|
|
|
146
136
|
}
|
|
147
137
|
};
|
|
148
138
|
|
|
139
|
+
const [usingSingleMode, setUsingSingleMode] = useState(true);
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (mediaUrls.length > 0) {
|
|
142
|
+
setUsingSingleMode(false);
|
|
143
|
+
} else {
|
|
144
|
+
setUsingSingleMode(true);
|
|
145
|
+
}
|
|
146
|
+
}, [renderMessages.length, mediaUrls.length]);
|
|
149
147
|
return (
|
|
150
|
-
<div className="
|
|
151
|
-
<div className="
|
|
152
|
-
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
<div className=
|
|
158
|
-
<
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
className={`px-2 py-1 rounded ${isFileTextMode.audio ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-500"}`}
|
|
174
|
-
>
|
|
175
|
-
音频
|
|
176
|
-
</button>
|
|
177
|
-
<button
|
|
178
|
-
onClick={() => setIsFileTextMode((prev) => ({ ...prev, other: !prev.other }))}
|
|
179
|
-
className={`px-2 py-1 rounded ${isFileTextMode.other ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-500"}`}
|
|
180
|
-
>
|
|
181
|
-
其他
|
|
182
|
-
</button>
|
|
183
|
-
</div>
|
|
184
|
-
)}
|
|
185
|
-
<div className="flex gap-3">
|
|
186
|
-
<textarea
|
|
187
|
-
className="flex-1 px-5 py-4 text-sm bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-2xl placeholder:text-gray-400 dark:placeholder:text-gray-500 resize-none active:outline-none focus:outline-none"
|
|
188
|
-
rows={3}
|
|
189
|
-
value={userInput}
|
|
190
|
-
onChange={(e) => setUserInput(e.target.value)}
|
|
191
|
-
onKeyDown={handleKeyPress}
|
|
192
|
-
placeholder="请输入消息内容…"
|
|
193
|
-
disabled={loading}
|
|
194
|
-
style={{
|
|
195
|
-
minHeight: "3rem",
|
|
196
|
-
maxHeight: "6rem",
|
|
197
|
-
fontFamily: "inherit",
|
|
198
|
-
}}
|
|
199
|
-
/>
|
|
200
|
-
<button
|
|
201
|
-
className={`px-4 py-3 text-sm font-medium text-white rounded-xl focus:outline-none transition-colors flex items-center justify-center ${
|
|
202
|
-
loading ? "bg-red-500 hover:bg-red-600" : "bg-blue-500 hover:bg-blue-600 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
203
|
-
}`}
|
|
204
|
-
onClick={() => (loading ? stopGeneration() : sendMultiModalMessage())}
|
|
205
|
-
disabled={!loading && !userInput.trim() && mediaUrls.length === 0}
|
|
206
|
-
>
|
|
207
|
-
{loading ? (
|
|
208
|
-
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
209
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
210
|
-
</svg>
|
|
211
|
-
) : (
|
|
212
|
-
<Send className="w-5 h-5" />
|
|
213
|
-
)}
|
|
214
|
-
</button>
|
|
215
|
-
</div>
|
|
216
|
-
<div className="flex mt-4 gap-2 justify-between items-center">
|
|
217
|
-
<UsageMetadata usage_metadata={client?.tokenCounter || {}} />
|
|
218
|
-
<div className="flex items-center gap-2">
|
|
219
|
-
{!!currentChatId && <span className="text-xs text-gray-400 dark:text-gray-500">会话 ID: {currentChatId}</span>}
|
|
148
|
+
<div className=" pb-8 ">
|
|
149
|
+
<div className={"bg-white border border-gray-200 shadow-lg shadow-gray-200 " + (usingSingleMode ? "rounded-full px-3 py-3" : "rounded-4xl px-4 py-3")}>
|
|
150
|
+
{!usingSingleMode && mediaUrls.length > 0 && (
|
|
151
|
+
<div className="flex items-center justify-between mb-2 border-b border-gray-200 pb-2">
|
|
152
|
+
<FileList />
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
<div className={`flex gap-3 ${usingSingleMode ? "items-center" : ""}`}>
|
|
156
|
+
<UploadButton />
|
|
157
|
+
<textarea
|
|
158
|
+
className="flex-1 text-sm resize-none active:outline-none focus:outline-none"
|
|
159
|
+
rows={1}
|
|
160
|
+
value={userInput}
|
|
161
|
+
onChange={(e) => setUserInput(e.target.value)}
|
|
162
|
+
onKeyDown={handleKeyPress}
|
|
163
|
+
placeholder={usingSingleMode ? "请输入消息内容…" : "请输入消息内容…"}
|
|
164
|
+
disabled={loading}
|
|
165
|
+
style={{
|
|
166
|
+
maxHeight: usingSingleMode ? "2rem" : "6rem",
|
|
167
|
+
fontFamily: "inherit",
|
|
168
|
+
lineHeight: usingSingleMode ? "2rem" : "inherit",
|
|
169
|
+
}}
|
|
170
|
+
/>
|
|
220
171
|
<select
|
|
221
172
|
value={currentAgent}
|
|
222
173
|
onChange={(e) => _setCurrentAgent(e.target.value)}
|
|
@@ -230,6 +181,32 @@ const ChatInput: React.FC = () => {
|
|
|
230
181
|
);
|
|
231
182
|
})}
|
|
232
183
|
</select>
|
|
184
|
+
<button
|
|
185
|
+
className={`w-8 h-8 flex items-center justify-center rounded-full focus:outline-none transition-colors ${
|
|
186
|
+
loading ? "bg-red-500 hover:bg-red-600 text-white" : "bg-blue-500 hover:bg-blue-600 text-white disabled:opacity-40 disabled:cursor-not-allowed"
|
|
187
|
+
}`}
|
|
188
|
+
onClick={() => (loading ? stopGeneration() : sendMultiModalMessage())}
|
|
189
|
+
disabled={!loading && !userInput.trim() && mediaUrls.length === 0}
|
|
190
|
+
>
|
|
191
|
+
{loading ? (
|
|
192
|
+
<svg className={"w-4 h-4"} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
193
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
194
|
+
</svg>
|
|
195
|
+
) : (
|
|
196
|
+
<Send className={"w-4 h-4"} />
|
|
197
|
+
)}
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div className={"flex gap-2 px-8 pt-4 justify-between items-center " + (renderMessages.length ? "opacity-100" : "opacity-0")}>
|
|
203
|
+
<UsageMetadata usage_metadata={client?.tokenCounter || {}} />
|
|
204
|
+
<div className="flex items-center gap-2">
|
|
205
|
+
{!!currentChatId && (
|
|
206
|
+
<span className="cursor-pointer text-xs text-gray-300 dark:text-gray-500" onClick={() => openMonitorWithChat(currentChatId)}>
|
|
207
|
+
会话 ID: {currentChatId}
|
|
208
|
+
</span>
|
|
209
|
+
)}
|
|
233
210
|
</div>
|
|
234
211
|
</div>
|
|
235
212
|
</div>
|
|
@@ -242,21 +219,21 @@ const Chat: React.FC = () => {
|
|
|
242
219
|
const { showHistory, toggleHistoryVisible, showGraph, toggleGraphVisible, renderMessages, setTools, client } = useChat();
|
|
243
220
|
const { extraParams, setExtraParams } = useExtraParams();
|
|
244
221
|
const { showArtifact, setShowArtifact } = useChat();
|
|
245
|
-
|
|
222
|
+
const { openMonitor } = useMonitor();
|
|
246
223
|
useEffect(() => {
|
|
247
224
|
setTools([show_form, create_artifacts]);
|
|
248
225
|
}, []);
|
|
249
226
|
return (
|
|
250
227
|
<div className="langgraph-chat-container flex h-full w-full overflow-hidden bg-gray-100">
|
|
251
228
|
{showHistory && (
|
|
252
|
-
<div className="
|
|
229
|
+
<div className="border-r border-gray-200 min-w-64">
|
|
253
230
|
<HistoryList onClose={() => toggleHistoryVisible()} />
|
|
254
231
|
</div>
|
|
255
232
|
)}
|
|
256
233
|
<section className="flex-1 flex flex-col overflow-auto items-center ">
|
|
257
|
-
<header className="flex items-center gap-2 px-
|
|
234
|
+
<header className="flex items-center gap-2 px-3 py-2 justify-end h-16 mt-4 max-w-6xl w-full">
|
|
258
235
|
<button
|
|
259
|
-
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-
|
|
236
|
+
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-200 cursor-pointer rounded-xl hover:bg-gray-100 focus:outline-none transition-colors flex items-center gap-2"
|
|
260
237
|
onClick={() => {
|
|
261
238
|
toggleHistoryVisible();
|
|
262
239
|
setLocalConfig({ showHistory: !showHistory });
|
|
@@ -269,7 +246,7 @@ const Chat: React.FC = () => {
|
|
|
269
246
|
|
|
270
247
|
<button
|
|
271
248
|
onClick={() => setIsPopupOpen(true)}
|
|
272
|
-
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-
|
|
249
|
+
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-200 cursor-pointer rounded-xl hover:bg-gray-100 focus:outline-none transition-colors flex items-center gap-2"
|
|
273
250
|
>
|
|
274
251
|
<FileJson className="w-4 h-4" />
|
|
275
252
|
额外参数
|
|
@@ -277,13 +254,13 @@ const Chat: React.FC = () => {
|
|
|
277
254
|
<button
|
|
278
255
|
id="setting-button"
|
|
279
256
|
onClick={() => setIsSettingsOpen(true)}
|
|
280
|
-
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-
|
|
257
|
+
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-200 cursor-pointer rounded-xl hover:bg-gray-100 focus:outline-none transition-colors flex items-center gap-2"
|
|
281
258
|
>
|
|
282
259
|
<Settings className="w-4 h-4" />
|
|
283
260
|
设置
|
|
284
261
|
</button>
|
|
285
262
|
<button
|
|
286
|
-
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-
|
|
263
|
+
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-200 cursor-pointer rounded-xl hover:bg-gray-100 focus:outline-none transition-colors flex items-center gap-2"
|
|
287
264
|
onClick={() => {
|
|
288
265
|
toast.info("数据已打印到控制台,请 F12 查看");
|
|
289
266
|
console.log(client?.graphState);
|
|
@@ -292,7 +269,7 @@ const Chat: React.FC = () => {
|
|
|
292
269
|
打印 State
|
|
293
270
|
</button>
|
|
294
271
|
<button
|
|
295
|
-
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-
|
|
272
|
+
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-200 cursor-pointer rounded-xl hover:bg-gray-100 focus:outline-none transition-colors flex items-center gap-2"
|
|
296
273
|
onClick={() => {
|
|
297
274
|
toggleGraphVisible();
|
|
298
275
|
setLocalConfig({ showGraph: !showGraph });
|
|
@@ -301,10 +278,21 @@ const Chat: React.FC = () => {
|
|
|
301
278
|
<Network className="w-4 h-4" />
|
|
302
279
|
节点图
|
|
303
280
|
</button>
|
|
281
|
+
<button
|
|
282
|
+
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-200 cursor-pointer rounded-xl hover:bg-gray-100 focus:outline-none transition-colors flex items-center gap-2"
|
|
283
|
+
onClick={() => {
|
|
284
|
+
openMonitor("/api/open-smith/ui/index.html");
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
<Network className="w-4 h-4" />
|
|
288
|
+
控制台
|
|
289
|
+
</button>
|
|
304
290
|
</header>
|
|
305
291
|
<main className="flex-1 overflow-y-auto overflow-x-hidden max-w-6xl w-full h-full flex flex-col">
|
|
306
292
|
<ChatMessages />
|
|
307
|
-
<
|
|
293
|
+
<FileListProvider>
|
|
294
|
+
<ChatInput />
|
|
295
|
+
</FileListProvider>
|
|
308
296
|
</main>
|
|
309
297
|
<JsonEditorPopup
|
|
310
298
|
isOpen={isPopupOpen}
|
|
@@ -347,10 +335,10 @@ const ChatWrapper: React.FC = () => {
|
|
|
347
335
|
defaultHeaders = parsedHeaders;
|
|
348
336
|
}
|
|
349
337
|
}
|
|
350
|
-
|
|
338
|
+
const apiUrl = storedApiUrl?.startsWith("/") ? new URL(storedApiUrl, window.location.origin).toString() : storedApiUrl;
|
|
351
339
|
return {
|
|
352
340
|
defaultAgent: storedDefaultAgent || "",
|
|
353
|
-
apiUrl:
|
|
341
|
+
apiUrl: apiUrl || "http://localhost:8123",
|
|
354
342
|
defaultHeaders,
|
|
355
343
|
withCredentials: storedWithCredentials === "true",
|
|
356
344
|
showHistory: storedShowHistory === "true",
|
|
@@ -372,34 +360,38 @@ const ChatWrapper: React.FC = () => {
|
|
|
372
360
|
const config = getLocalStorageData();
|
|
373
361
|
|
|
374
362
|
return (
|
|
375
|
-
<
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
document.getElementById("
|
|
392
|
-
|
|
363
|
+
<MonitorProvider>
|
|
364
|
+
<ChatProvider
|
|
365
|
+
defaultAgent={config.defaultAgent}
|
|
366
|
+
apiUrl={config.apiUrl}
|
|
367
|
+
defaultHeaders={config.defaultHeaders}
|
|
368
|
+
withCredentials={config.withCredentials}
|
|
369
|
+
showHistory={config.showHistory}
|
|
370
|
+
showGraph={config.showGraph}
|
|
371
|
+
fallbackToAvailableAssistants={true}
|
|
372
|
+
onInitError={(err, currentAgent) => {
|
|
373
|
+
// 默认错误处理
|
|
374
|
+
toast.error("请检查服务器配置: " + currentAgent + "\n" + err, {
|
|
375
|
+
duration: 10000,
|
|
376
|
+
action: {
|
|
377
|
+
label: "去设置",
|
|
378
|
+
onClick: () => {
|
|
379
|
+
document.getElementById("setting-button")?.click();
|
|
380
|
+
setTimeout(() => {
|
|
381
|
+
document.getElementById("server-login-button")?.click();
|
|
382
|
+
}, 300);
|
|
383
|
+
},
|
|
393
384
|
},
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
385
|
+
});
|
|
386
|
+
}}
|
|
387
|
+
>
|
|
388
|
+
<ExtraParamsProvider>
|
|
389
|
+
<Chat />
|
|
390
|
+
<Toaster />
|
|
391
|
+
<Monitor />
|
|
392
|
+
</ExtraParamsProvider>
|
|
393
|
+
</ChatProvider>
|
|
394
|
+
</MonitorProvider>
|
|
403
395
|
);
|
|
404
396
|
};
|
|
405
397
|
|
|
@@ -1,85 +1,41 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import {
|
|
3
|
-
import { File, UploadCloudIcon } from "lucide-react";
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useFileList } from "./FileListContext";
|
|
4
3
|
|
|
5
4
|
export type SupportedFileType = "image" | "video" | "audio" | "other";
|
|
6
5
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
if (file.type === "video/mp4") return "video";
|
|
10
|
-
if (file.type.startsWith("audio/")) return "audio";
|
|
11
|
-
return "other";
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
interface UploadedFile {
|
|
15
|
-
file: File;
|
|
16
|
-
url: string;
|
|
17
|
-
type: SupportedFileType;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface FileListProps {
|
|
21
|
-
onFileUploaded: (url: string, type: SupportedFileType) => void;
|
|
22
|
-
onFileRemoved: (url: string, type: SupportedFileType) => void;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const FileList: React.FC<FileListProps> = ({ onFileUploaded, onFileRemoved }) => {
|
|
26
|
-
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
|
|
27
|
-
const client = new TmpFilesClient();
|
|
28
|
-
const MAX_FILES = 3;
|
|
29
|
-
|
|
30
|
-
const handleFileChange = useCallback(
|
|
31
|
-
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
32
|
-
const selectedFiles = Array.from(event.target.files || []);
|
|
33
|
-
const mediaFiles = selectedFiles;
|
|
34
|
-
|
|
35
|
-
// 检查是否超过最大数量限制
|
|
36
|
-
if (uploadedFiles.length + mediaFiles.length > MAX_FILES) {
|
|
37
|
-
alert(`最多只能上传${MAX_FILES}个文件`);
|
|
38
|
-
event.target.value = "";
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
for (const file of mediaFiles) {
|
|
43
|
-
try {
|
|
44
|
-
const result = await client.upload(file);
|
|
45
|
-
if (result.data?.url) {
|
|
46
|
-
const fileType = getFileType(file);
|
|
47
|
-
if (fileType) {
|
|
48
|
-
const uploadedFile = { file, url: result.data.url, type: fileType };
|
|
49
|
-
setUploadedFiles((prev) => [...prev, uploadedFile]);
|
|
50
|
-
onFileUploaded(result.data.url, fileType);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
} catch (error) {
|
|
54
|
-
console.error("Upload failed:", error);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
event.target.value = "";
|
|
59
|
-
},
|
|
60
|
-
[onFileUploaded, uploadedFiles.length]
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const removeFile = useCallback(
|
|
64
|
-
(index: number) => {
|
|
65
|
-
const fileToRemove = uploadedFiles[index];
|
|
66
|
-
if (fileToRemove) {
|
|
67
|
-
setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
|
|
68
|
-
onFileRemoved(fileToRemove.url, fileToRemove.type);
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
[uploadedFiles, onFileRemoved]
|
|
72
|
-
);
|
|
6
|
+
const FileList: React.FC = () => {
|
|
7
|
+
const { uploadedFiles, removeFile, mediaUrls, setIsFileTextMode, isFileTextMode } = useFileList();
|
|
73
8
|
|
|
74
9
|
return (
|
|
75
|
-
<div
|
|
76
|
-
{
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
10
|
+
<div>
|
|
11
|
+
{mediaUrls.length > 0 && (
|
|
12
|
+
<div className="flex items-center gap-2 text-xs text-gray-600 mb-3" title="文本传输将会把多模态文件转为 XML + URL 的格式传递给大模型">
|
|
13
|
+
<span>启动文本传输:</span>
|
|
14
|
+
<button
|
|
15
|
+
onClick={() => setIsFileTextMode((prev) => ({ ...prev, image: !prev.image }))}
|
|
16
|
+
className={`px-2 py-1 rounded ${isFileTextMode.image ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-500"}`}
|
|
17
|
+
>
|
|
18
|
+
图片
|
|
19
|
+
</button>
|
|
20
|
+
<button
|
|
21
|
+
onClick={() => setIsFileTextMode((prev) => ({ ...prev, video: !prev.video }))}
|
|
22
|
+
className={`px-2 py-1 rounded ${isFileTextMode.video ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-500"}`}
|
|
23
|
+
>
|
|
24
|
+
视频
|
|
25
|
+
</button>
|
|
26
|
+
<button
|
|
27
|
+
onClick={() => setIsFileTextMode((prev) => ({ ...prev, audio: !prev.audio }))}
|
|
28
|
+
className={`px-2 py-1 rounded ${isFileTextMode.audio ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-500"}`}
|
|
29
|
+
>
|
|
30
|
+
音频
|
|
31
|
+
</button>
|
|
32
|
+
<button
|
|
33
|
+
onClick={() => setIsFileTextMode((prev) => ({ ...prev, other: !prev.other }))}
|
|
34
|
+
className={`px-2 py-1 rounded ${isFileTextMode.other ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-500"}`}
|
|
35
|
+
>
|
|
36
|
+
其他
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
83
39
|
)}
|
|
84
40
|
<div className="flex flex-wrap gap-2">
|
|
85
41
|
{uploadedFiles.map((uploadedFile, index) => {
|
|
@@ -113,7 +69,7 @@ const FileList: React.FC<FileListProps> = ({ onFileUploaded, onFileRemoved }) =>
|
|
|
113
69
|
</div>
|
|
114
70
|
);
|
|
115
71
|
})}
|
|
116
|
-
</div>
|
|
72
|
+
</div>{" "}
|
|
117
73
|
</div>
|
|
118
74
|
);
|
|
119
75
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import UploadButton from "./UploadButton";
|
|
3
|
+
import FileList from "./FileList";
|
|
4
|
+
|
|
5
|
+
const FileListContainer: React.FC = () => {
|
|
6
|
+
return (
|
|
7
|
+
<div className="flex gap-2 flex-1">
|
|
8
|
+
<UploadButton />
|
|
9
|
+
<FileList />
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default FileListContainer;
|