@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.
Files changed (76) hide show
  1. package/.env +1 -1
  2. package/.langgraph_api/trace.db +0 -0
  3. package/.langgraph_api/trace.db-shm +0 -0
  4. package/.langgraph_api/trace.db-wal +0 -0
  5. package/dist/assets/{arc-CGVbGqUF.js → arc-B4gD06tH.js} +1 -1
  6. package/dist/assets/{architectureDiagram-VXUJARFQ-CqtrDEw9.js → architectureDiagram-VXUJARFQ-BgFtItBV.js} +1 -1
  7. package/dist/assets/{blockDiagram-VD42YOAC-CNqe-K1B.js → blockDiagram-VD42YOAC-BFGzNg6Q.js} +1 -1
  8. package/dist/assets/{c4Diagram-YG6GDRKO-CPTzKGRp.js → c4Diagram-YG6GDRKO-CuVylzXj.js} +1 -1
  9. package/dist/assets/channel-BKsvQ92l.js +1 -0
  10. package/dist/assets/{chunk-4BX2VUAB-CnFclA68.js → chunk-4BX2VUAB-cU_FCghb.js} +1 -1
  11. package/dist/assets/{chunk-55IACEB6-CVyjVz79.js → chunk-55IACEB6-Whk-5ZBd.js} +1 -1
  12. package/dist/assets/{chunk-B4BG7PRW-DiGvJtfO.js → chunk-B4BG7PRW-CreLfPEt.js} +1 -1
  13. package/dist/assets/{chunk-DI55MBZ5-CiqWzq60.js → chunk-DI55MBZ5-DeFeTByd.js} +1 -1
  14. package/dist/assets/{chunk-FMBD7UC4-KfYlm8ba.js → chunk-FMBD7UC4-D8IFRGWy.js} +1 -1
  15. package/dist/assets/{chunk-QN33PNHL-Bwbc5Dxk.js → chunk-QN33PNHL-BtcE2bYr.js} +1 -1
  16. package/dist/assets/{chunk-QZHKN3VN-DlQbvjLB.js → chunk-QZHKN3VN-Dqyf850H.js} +1 -1
  17. package/dist/assets/{chunk-TZMSLE5B-6_Y4HlEU.js → chunk-TZMSLE5B-B9mCm-HN.js} +1 -1
  18. package/dist/assets/classDiagram-2ON5EDUG-DY6hfbKg.js +1 -0
  19. package/dist/assets/classDiagram-v2-WZHVMYZB-DY6hfbKg.js +1 -0
  20. package/dist/assets/clone-DiADKkIv.js +1 -0
  21. package/dist/assets/{cose-bilkent-S5V4N54A-BL_xZw_z.js → cose-bilkent-S5V4N54A-BsyvYd6P.js} +1 -1
  22. package/dist/assets/{dagre-6UL2VRFP-CKK6NwPE.js → dagre-6UL2VRFP-CJWBl4IX.js} +1 -1
  23. package/dist/assets/{diagram-PSM6KHXK-D77jB3aN.js → diagram-PSM6KHXK-s-3r9foi.js} +1 -1
  24. package/dist/assets/{diagram-QEK2KX5R-C3V63KEf.js → diagram-QEK2KX5R-C6wNh_Hf.js} +1 -1
  25. package/dist/assets/{diagram-S2PKOQOG-8vXvyFcA.js → diagram-S2PKOQOG-CpOHanaU.js} +1 -1
  26. package/dist/assets/{erDiagram-Q2GNP2WA-BvdZN7ll.js → erDiagram-Q2GNP2WA-CGoTaByR.js} +1 -1
  27. package/dist/assets/{flowDiagram-NV44I4VS-Cmn4g1Tt.js → flowDiagram-NV44I4VS-BwGzdn6-.js} +1 -1
  28. package/dist/assets/{ganttDiagram-LVOFAZNH-BtK3XTtk.js → ganttDiagram-LVOFAZNH-qaR08rFo.js} +1 -1
  29. package/dist/assets/{gitGraphDiagram-NY62KEGX-Ckd-OTVj.js → gitGraphDiagram-NY62KEGX-MjuWAJcA.js} +1 -1
  30. package/dist/assets/{graph-C8xl-aUs.js → graph-BHPAGsrk.js} +1 -1
  31. package/dist/assets/index-BwQLftC0.css +1 -0
  32. package/dist/assets/index-CeSzlsK_.js +1 -0
  33. package/dist/assets/{index-EdVqpCiz.js → index-Cw_pxs59.js} +171 -177
  34. package/dist/assets/{infoDiagram-F6ZHWCRC-Z0X_yr7r.js → infoDiagram-F6ZHWCRC-DJxuxLb8.js} +1 -1
  35. package/dist/assets/{isUndefined-Daq2Snc8.js → isUndefined-BZle6qE3.js} +1 -1
  36. package/dist/assets/{journeyDiagram-XKPGCS4Q-Ce4GbjgA.js → journeyDiagram-XKPGCS4Q-Bx-5B721.js} +1 -1
  37. package/dist/assets/{kanban-definition-3W4ZIXB7-jKKX8cF-.js → kanban-definition-3W4ZIXB7-BzyJeB-d.js} +1 -1
  38. package/dist/assets/{layout-CehWuyQv.js → layout-D9r9e3x5.js} +1 -1
  39. package/dist/assets/{linear-Do6UjTtT.js → linear-CH-g37GF.js} +1 -1
  40. package/dist/assets/{mermaid.core-DboFnVDg.js → mermaid.core-DQOzsCw4.js} +5 -5
  41. package/dist/assets/min-Bkz53M0y.js +1 -0
  42. package/dist/assets/{mindmap-definition-VGOIOE7T-DHqc52RG.js → mindmap-definition-VGOIOE7T-Dmukcpsg.js} +1 -1
  43. package/dist/assets/{pieDiagram-ADFJNKIX-bG2J_WkI.js → pieDiagram-ADFJNKIX-D2A5t1l4.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-AYHSOK5B-CDEBrcZS.js → quadrantDiagram-AYHSOK5B-fSb36W--.js} +1 -1
  45. package/dist/assets/{requirementDiagram-UZGBJVZJ-D88jc_6e.js → requirementDiagram-UZGBJVZJ-Clv63BU0.js} +1 -1
  46. package/dist/assets/{sankeyDiagram-TZEHDZUN-C93LzfJ0.js → sankeyDiagram-TZEHDZUN-CWJ180dG.js} +1 -1
  47. package/dist/assets/{sequenceDiagram-WL72ISMW-DjpL4ZUq.js → sequenceDiagram-WL72ISMW-DgluEJrO.js} +1 -1
  48. package/dist/assets/{stateDiagram-FKZM4ZOC-Cy4Qc8UW.js → stateDiagram-FKZM4ZOC-Cn6H8hoO.js} +1 -1
  49. package/dist/assets/stateDiagram-v2-4FDKWEC3-CzszURC6.js +1 -0
  50. package/dist/assets/{timeline-definition-IT6M3QCI-DNVlI21w.js → timeline-definition-IT6M3QCI-D650ceX-.js} +1 -1
  51. package/dist/assets/{treemap-KMMF4GRG-Cf7rPol4.js → treemap-KMMF4GRG-DXDGlAhA.js} +1 -1
  52. package/dist/assets/{xychartDiagram-PRI3JC2R-CRudqT7y.js → xychartDiagram-PRI3JC2R-B5EWvgyz.js} +1 -1
  53. package/dist/index.html +2 -2
  54. package/package.json +5 -4
  55. package/src/artifacts/ArtifactViewer.tsx +2 -2
  56. package/src/chat/Chat.tsx +133 -141
  57. package/src/chat/components/FileList.tsx +34 -78
  58. package/src/chat/components/FileListContainer.tsx +14 -0
  59. package/src/chat/components/FileListContext.tsx +157 -0
  60. package/src/chat/components/HistoryList.tsx +1 -1
  61. package/src/chat/components/MessageAI.tsx +1 -1
  62. package/src/chat/components/UploadButton.tsx +20 -0
  63. package/src/chat/components/UsageMetadata.tsx +10 -1
  64. package/src/index.ts +0 -1
  65. package/src/monitor/index.tsx +70 -0
  66. package/src/settings/ConsoleSettings.tsx +48 -0
  67. package/src/settings/LoginSettings.tsx +64 -11
  68. package/src/settings/SettingPanel.tsx +6 -5
  69. package/vite.config.ts +48 -2
  70. package/dist/assets/channel-CbJkpW4g.js +0 -1
  71. package/dist/assets/classDiagram-2ON5EDUG--F4r2fzQ.js +0 -1
  72. package/dist/assets/classDiagram-v2-WZHVMYZB--F4r2fzQ.js +0 -1
  73. package/dist/assets/clone-CxJednSn.js +0 -1
  74. package/dist/assets/index-BpNQAzdK.css +0 -1
  75. package/dist/assets/min-CoD9aTVe.js +0 -1
  76. 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
- <MessagesBox renderMessages={renderMessages} collapsedTools={collapsedTools} toggleToolCollapse={toggleToolCollapse} client={client!} />
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 [mediaUrls, setMediaUrls] = useState<Array<any>>([]);
76
- const [isFileTextMode, setIsFileTextMode] = useState({
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="bg-white rounded-2xl px-4 py-3 mb-4 shadow-lg shadow-gray-200">
151
- <div className="flex items-center justify-between mb-4">
152
- <div className="flex items-center gap-4">
153
- <FileList onFileUploaded={handleFileUploaded} onFileRemoved={handleFileRemoved} />
154
- </div>
155
- </div>
156
- {mediaUrls.length > 0 && (
157
- <div className="flex items-center gap-2 text-xs text-gray-600 mb-3" title="文本传输将会把多模态文件转为 XML + URL 的格式传递给大模型">
158
- <span>启动文本传输:</span>
159
- <button
160
- onClick={() => setIsFileTextMode((prev) => ({ ...prev, image: !prev.image }))}
161
- className={`px-2 py-1 rounded ${isFileTextMode.image ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-500"}`}
162
- >
163
- 图片
164
- </button>
165
- <button
166
- onClick={() => setIsFileTextMode((prev) => ({ ...prev, video: !prev.video }))}
167
- className={`px-2 py-1 rounded ${isFileTextMode.video ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-500"}`}
168
- >
169
- 视频
170
- </button>
171
- <button
172
- onClick={() => setIsFileTextMode((prev) => ({ ...prev, audio: !prev.audio }))}
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="p-4">
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-6 py-4 bg-white/50 backdrop-blur-sm justify-end h-16 mt-4 rounded-2xl shadow-lg shadow-gray-200">
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-50 rounded-xl hover:bg-gray-100 focus:outline-none transition-colors flex items-center gap-2"
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-50 rounded-xl hover:bg-gray-100 focus:outline-none transition-colors flex items-center gap-2"
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-50 rounded-xl hover:bg-gray-100 focus:outline-none transition-colors flex items-center gap-2"
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-50 rounded-xl hover:bg-gray-100 focus:outline-none transition-colors flex items-center gap-2"
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-50 rounded-xl hover:bg-gray-100 focus:outline-none transition-colors flex items-center gap-2"
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
- <ChatInput />
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: storedApiUrl || "http://localhost:8123",
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
- <ChatProvider
376
- defaultAgent={config.defaultAgent}
377
- apiUrl={config.apiUrl}
378
- defaultHeaders={config.defaultHeaders}
379
- withCredentials={config.withCredentials}
380
- showHistory={config.showHistory}
381
- showGraph={config.showGraph}
382
- onInitError={(err, currentAgent) => {
383
- // 默认错误处理
384
- toast.error("请检查服务器配置: " + currentAgent + "\n" + err, {
385
- duration: 10000,
386
- action: {
387
- label: "去设置",
388
- onClick: () => {
389
- document.getElementById("setting-button")?.click();
390
- setTimeout(() => {
391
- document.getElementById("server-login-button")?.click();
392
- }, 300);
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
- <ExtraParamsProvider>
399
- <Chat />
400
- <Toaster />
401
- </ExtraParamsProvider>
402
- </ChatProvider>
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, { useState, useCallback } from "react";
2
- import { TmpFilesClient } from "../FileUpload";
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 getFileType = (file: File): SupportedFileType => {
8
- if (file.type.startsWith("image/")) return "image";
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 className="flex gap-2 flex-1">
76
- {uploadedFiles.length < MAX_FILES && (
77
- <label
78
- className={`inline-flex items-center justify-center text-gray-700 bg-white border border-gray-200 rounded-xl cursor-pointer transition-colors hover:bg-gray-100 hover:border-gray-300 ${uploadedFiles.length === 0 ? "w-10 h-10" : "w-20 h-20"}`}
79
- >
80
- <UploadCloudIcon size={uploadedFiles.length === 0 ? 20 : 32} />
81
- <input type="file" accept="*" multiple onChange={handleFileChange} className="hidden" />
82
- </label>
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;