@langgraph-js/ui 3.1.2 → 4.0.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/.env +1 -1
- package/.langgraph_api/trace.db +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 +4 -3
- 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 +47 -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
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useCallback } from "react";
|
|
2
|
+
import { TmpFilesClient } from "../FileUpload";
|
|
3
|
+
import type { SupportedFileType } from "./FileList";
|
|
4
|
+
|
|
5
|
+
interface UploadedFile {
|
|
6
|
+
file: File;
|
|
7
|
+
url: string;
|
|
8
|
+
type: SupportedFileType;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface MediaUrl {
|
|
12
|
+
type: "image_url" | "video_url" | "audio_url" | "file_url";
|
|
13
|
+
image_url?: { url: string };
|
|
14
|
+
video_url?: { url: string };
|
|
15
|
+
audio_url?: { url: string };
|
|
16
|
+
file_url?: { url: string };
|
|
17
|
+
fileType: SupportedFileType;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface FileListContextType {
|
|
21
|
+
uploadedFiles: UploadedFile[];
|
|
22
|
+
mediaUrls: MediaUrl[];
|
|
23
|
+
isFileTextMode: {
|
|
24
|
+
image: boolean;
|
|
25
|
+
video: boolean;
|
|
26
|
+
audio: boolean;
|
|
27
|
+
other: boolean;
|
|
28
|
+
};
|
|
29
|
+
setIsFileTextMode: React.Dispatch<
|
|
30
|
+
React.SetStateAction<{
|
|
31
|
+
image: boolean;
|
|
32
|
+
video: boolean;
|
|
33
|
+
audio: boolean;
|
|
34
|
+
other: boolean;
|
|
35
|
+
}>
|
|
36
|
+
>;
|
|
37
|
+
handleFileUploaded: (url: string, fileType: SupportedFileType) => void;
|
|
38
|
+
handleFileRemoved: (url: string, fileType: SupportedFileType) => void;
|
|
39
|
+
handleFileChange: (event: React.ChangeEvent<HTMLInputElement>) => Promise<void>;
|
|
40
|
+
removeFile: (index: number) => void;
|
|
41
|
+
MAX_FILES: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const FileListContext = createContext<FileListContextType | undefined>(undefined);
|
|
45
|
+
|
|
46
|
+
export const useFileList = () => {
|
|
47
|
+
const context = useContext(FileListContext);
|
|
48
|
+
if (!context) {
|
|
49
|
+
throw new Error("useFileList must be used within a FileListProvider");
|
|
50
|
+
}
|
|
51
|
+
return context;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
interface FileListProviderProps {
|
|
55
|
+
children: React.ReactNode;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const FileListProvider: React.FC<FileListProviderProps> = ({ children }) => {
|
|
59
|
+
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
|
|
60
|
+
const [mediaUrls, setMediaUrls] = useState<MediaUrl[]>([]);
|
|
61
|
+
const [isFileTextMode, setIsFileTextMode] = useState({
|
|
62
|
+
image: false,
|
|
63
|
+
video: false,
|
|
64
|
+
audio: false,
|
|
65
|
+
other: true,
|
|
66
|
+
});
|
|
67
|
+
const MAX_FILES = 3;
|
|
68
|
+
const client = new TmpFilesClient();
|
|
69
|
+
|
|
70
|
+
const handleFileUploaded = useCallback((url: string, fileType: SupportedFileType) => {
|
|
71
|
+
// 上传时始终保存原始文件信息,在发送时根据文本模式决定格式
|
|
72
|
+
if (fileType === "image") {
|
|
73
|
+
setMediaUrls((prev) => [...prev, { type: "image_url", image_url: { url }, fileType }]);
|
|
74
|
+
} else if (fileType === "video") {
|
|
75
|
+
setMediaUrls((prev) => [...prev, { type: "video_url", video_url: { url }, fileType }]);
|
|
76
|
+
} else if (fileType === "audio") {
|
|
77
|
+
setMediaUrls((prev) => [...prev, { type: "audio_url", audio_url: { url }, fileType }]);
|
|
78
|
+
} else if (fileType === "other") {
|
|
79
|
+
setMediaUrls((prev) => [...prev, { type: "file_url", file_url: { url }, fileType }]);
|
|
80
|
+
}
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const handleFileRemoved = useCallback((url: string, fileType: SupportedFileType) => {
|
|
84
|
+
// 删除时移除对应的媒体文件信息
|
|
85
|
+
setMediaUrls((prev) =>
|
|
86
|
+
prev.filter((media) => {
|
|
87
|
+
const mediaUrl = media.image_url?.url || media.video_url?.url || media.audio_url?.url || media.file_url?.url;
|
|
88
|
+
return mediaUrl !== url;
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const handleFileChange = useCallback(
|
|
94
|
+
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
95
|
+
const selectedFiles = Array.from(event.target.files || []);
|
|
96
|
+
const mediaFiles = selectedFiles;
|
|
97
|
+
|
|
98
|
+
// 检查是否超过最大数量限制
|
|
99
|
+
if (uploadedFiles.length + mediaFiles.length > MAX_FILES) {
|
|
100
|
+
alert(`最多只能上传${MAX_FILES}个文件`);
|
|
101
|
+
event.target.value = "";
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const file of mediaFiles) {
|
|
106
|
+
try {
|
|
107
|
+
const result = await client.upload(file);
|
|
108
|
+
if (result.data?.url) {
|
|
109
|
+
const fileType = getFileType(file);
|
|
110
|
+
if (fileType) {
|
|
111
|
+
const uploadedFile = { file, url: result.data.url, type: fileType };
|
|
112
|
+
setUploadedFiles((prev) => [...prev, uploadedFile]);
|
|
113
|
+
handleFileUploaded(result.data.url, fileType);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error("Upload failed:", error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
event.target.value = "";
|
|
122
|
+
},
|
|
123
|
+
[uploadedFiles.length, handleFileUploaded]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const removeFile = useCallback(
|
|
127
|
+
(index: number) => {
|
|
128
|
+
const fileToRemove = uploadedFiles[index];
|
|
129
|
+
if (fileToRemove) {
|
|
130
|
+
setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
|
|
131
|
+
handleFileRemoved(fileToRemove.url, fileToRemove.type);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
[uploadedFiles, handleFileRemoved]
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const value: FileListContextType = {
|
|
138
|
+
uploadedFiles,
|
|
139
|
+
mediaUrls,
|
|
140
|
+
isFileTextMode,
|
|
141
|
+
setIsFileTextMode,
|
|
142
|
+
handleFileUploaded,
|
|
143
|
+
handleFileRemoved,
|
|
144
|
+
handleFileChange,
|
|
145
|
+
removeFile,
|
|
146
|
+
MAX_FILES,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return <FileListContext.Provider value={value}>{children}</FileListContext.Provider>;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const getFileType = (file: File): SupportedFileType => {
|
|
153
|
+
if (file.type.startsWith("image/")) return "image";
|
|
154
|
+
if (file.type === "video/mp4") return "video";
|
|
155
|
+
if (file.type.startsWith("audio/")) return "audio";
|
|
156
|
+
return "other";
|
|
157
|
+
};
|
|
@@ -10,7 +10,7 @@ interface HistoryListProps {
|
|
|
10
10
|
const HistoryList: React.FC<HistoryListProps> = ({ onClose }) => {
|
|
11
11
|
const { historyList, currentChatId, refreshHistoryList, createNewChat, deleteHistoryChat, toHistoryChat } = useChat();
|
|
12
12
|
return (
|
|
13
|
-
<section className="bg-white h-full flex flex-col
|
|
13
|
+
<section className="bg-white h-full flex flex-col ">
|
|
14
14
|
<div className="px-5 flex justify-between items-center py-6">
|
|
15
15
|
<div className="flex items-center gap-3">
|
|
16
16
|
<h3 className="m-0 text-base font-semibold text-gray-700">历史记录</h3>
|
|
@@ -10,7 +10,7 @@ interface MessageAIProps {
|
|
|
10
10
|
|
|
11
11
|
const MessageAI: React.FC<MessageAIProps> = ({ message }) => {
|
|
12
12
|
return (
|
|
13
|
-
<div className="flex flex-col w-[80%] bg-white rounded-2xl px-5 py-4">
|
|
13
|
+
<div className="flex flex-col w-[80%] bg-white rounded-2xl px-5 py-4 border border-gray-200">
|
|
14
14
|
<div className="text-xs font-medium text-gray-500 mb-3">{message.name}</div>
|
|
15
15
|
<div className="markdown-body max-w-none">
|
|
16
16
|
<Response>{getMessageContent(message.content)}</Response>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { PlusIcon, UploadCloudIcon } from "lucide-react";
|
|
3
|
+
import { useFileList } from "./FileListContext";
|
|
4
|
+
|
|
5
|
+
const UploadButton: React.FC = () => {
|
|
6
|
+
const { uploadedFiles, handleFileChange, MAX_FILES } = useFileList();
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
{uploadedFiles.length < MAX_FILES && (
|
|
11
|
+
<label className={`inline-flex items-center justify-center text-gray-700 bg-white rounded-xl cursor-pointer transition-colors hover:bg-gray-200 w-8 h-8`}>
|
|
12
|
+
<PlusIcon size={20} />
|
|
13
|
+
<input type="file" accept="*" multiple onChange={handleFileChange} className="hidden" />
|
|
14
|
+
</label>
|
|
15
|
+
)}
|
|
16
|
+
</>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default UploadButton;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { useMonitor } from "@/monitor";
|
|
2
|
+
import { useChat } from "@langgraph-js/sdk/react";
|
|
3
|
+
|
|
1
4
|
interface UsageMetadataProps {
|
|
2
5
|
usage_metadata: Partial<{
|
|
3
6
|
input_tokens: number;
|
|
@@ -21,6 +24,8 @@ const formatTokens = (tokens: number) => {
|
|
|
21
24
|
export const UsageMetadata: React.FC<UsageMetadataProps> = ({ usage_metadata, spend_time, response_metadata, id, tool_call_id }) => {
|
|
22
25
|
const speed = spend_time ? ((usage_metadata.output_tokens || 0) * 1000) / (spend_time || 1) : 0;
|
|
23
26
|
spend_time = spend_time && !isNaN(spend_time) ? spend_time : 0;
|
|
27
|
+
const { currentChatId } = useChat();
|
|
28
|
+
const { openMonitorWithChat } = useMonitor();
|
|
24
29
|
return (
|
|
25
30
|
<div className="flex items-center justify-between text-xs text-gray-400 mt-3">
|
|
26
31
|
<div className="flex items-center gap-3">
|
|
@@ -31,7 +36,11 @@ export const UsageMetadata: React.FC<UsageMetadataProps> = ({ usage_metadata, sp
|
|
|
31
36
|
<div className="flex items-center gap-2">
|
|
32
37
|
{response_metadata?.model_name && <span className="text-gray-500">{response_metadata.model_name}</span>}
|
|
33
38
|
{tool_call_id && <span className="text-gray-400">Tool: {tool_call_id}</span>}
|
|
34
|
-
{id &&
|
|
39
|
+
{id && (
|
|
40
|
+
<span className="text-gray-400" onClick={() => openMonitorWithChat(currentChatId!, id)}>
|
|
41
|
+
ID: {id}
|
|
42
|
+
</span>
|
|
43
|
+
)}
|
|
35
44
|
</div>
|
|
36
45
|
</div>
|
|
37
46
|
);
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
// Context 类型定义
|
|
4
|
+
interface MonitorContextValue {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
url: string;
|
|
7
|
+
openMonitor: (url: string) => void;
|
|
8
|
+
closeModal: () => void;
|
|
9
|
+
openMonitorWithChat: (thread_id: string, trace_id?: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// 创建 Context
|
|
13
|
+
const MonitorContext = createContext<MonitorContextValue | undefined>(undefined);
|
|
14
|
+
|
|
15
|
+
// Modal Provider 组件
|
|
16
|
+
export const MonitorProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
|
17
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
18
|
+
const [url, setUrl] = useState("");
|
|
19
|
+
|
|
20
|
+
const openModal = (newUrl: string) => {
|
|
21
|
+
setUrl(newUrl);
|
|
22
|
+
|
|
23
|
+
setIsOpen(true);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const closeModal = () => {
|
|
27
|
+
setIsOpen(false);
|
|
28
|
+
setUrl("");
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const openMonitorWithChat = (thread_id: string, trace_id?: string) => {
|
|
32
|
+
const url = new URL("/api/open-smith/ui/index.html", window.location.origin);
|
|
33
|
+
const qs = new URLSearchParams();
|
|
34
|
+
qs.set("thread_id", thread_id);
|
|
35
|
+
if (trace_id) {
|
|
36
|
+
qs.set("trace_id", trace_id);
|
|
37
|
+
}
|
|
38
|
+
url.hash = "/?" + qs.toString();
|
|
39
|
+
openModal(url.toString());
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return <MonitorContext.Provider value={{ isOpen, url, openMonitor: openModal, closeModal, openMonitorWithChat }}>{children}</MonitorContext.Provider>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Hook 用于在组件中使用 modal
|
|
46
|
+
export const useMonitor = () => {
|
|
47
|
+
const context = useContext(MonitorContext);
|
|
48
|
+
if (context === undefined) {
|
|
49
|
+
throw new Error("useModal must be used within a ModalProvider");
|
|
50
|
+
}
|
|
51
|
+
return context;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Modal 组件
|
|
55
|
+
export const Monitor: React.FC = () => {
|
|
56
|
+
const { isOpen, url, closeModal } = useMonitor();
|
|
57
|
+
|
|
58
|
+
if (!isOpen) return null;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="fixed inset-0 flex items-center justify-center z-50">
|
|
62
|
+
<div className="bg-white rounded-lg shadow-xl max-w-7xl w-full mx-4 max-h-[90vh] h-full flex flex-col border border-gray-200 relative">
|
|
63
|
+
<button onClick={closeModal} className="text-gray-400 hover:text-gray-600 text-2xl leading-none absolute top-2 right-2 z-50 cursor-pointer">
|
|
64
|
+
×
|
|
65
|
+
</button>
|
|
66
|
+
<div className="flex-1 p-4 min-h-0">{url && <iframe src={url} className="w-full h-full border rounded" title="Monitor" sandbox="allow-scripts allow-same-origin" />}</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, ChangeEvent } from "react";
|
|
2
|
+
import { toast } from "sonner";
|
|
3
|
+
|
|
4
|
+
interface ConsoleSettingsData {}
|
|
5
|
+
|
|
6
|
+
const initialConsoleSettings: ConsoleSettingsData = {};
|
|
7
|
+
|
|
8
|
+
const ConsoleSettings: React.FC = () => {
|
|
9
|
+
const [formData, setFormData] = useState<ConsoleSettingsData>(() => {
|
|
10
|
+
try {
|
|
11
|
+
const storedSettings = localStorage.getItem("consoleSettings");
|
|
12
|
+
return storedSettings ? JSON.parse(storedSettings) : initialConsoleSettings;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error("Error reading console settings from localStorage:", error);
|
|
15
|
+
return initialConsoleSettings;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
try {
|
|
21
|
+
localStorage.setItem("consoleSettings", JSON.stringify(formData));
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error("Error writing console settings to localStorage:", error);
|
|
24
|
+
}
|
|
25
|
+
}, [formData]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<form className="space-y-6">
|
|
29
|
+
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-800">
|
|
30
|
+
<h4 className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">控制台说明</h4>
|
|
31
|
+
<p className="text-sm text-blue-800 dark:text-blue-200 leading-relaxed">
|
|
32
|
+
控制台用于监控应用程序的运行状态。我们的控制台提供一个兼容 LangChain 的数据上报接口,您可以通过下面的配置来启用控制台输出。
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
<ol>
|
|
36
|
+
<li>请在 LangGraph 或者 LangChain 项目中使用以下环境变量来启用控制台输出:</li>
|
|
37
|
+
<li>
|
|
38
|
+
LANGSMITH_TRACING=true
|
|
39
|
+
<br />
|
|
40
|
+
LANGSMITH_ENDPOINT= {new URL("/api/open-smith", window.location.origin).toString()}
|
|
41
|
+
</li>
|
|
42
|
+
<li>然后可以在主面板的 控制台 按钮中查看控制台输出。</li>
|
|
43
|
+
</ol>
|
|
44
|
+
</form>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default ConsoleSettings;
|
|
@@ -16,11 +16,14 @@ interface LoginSettingsData {
|
|
|
16
16
|
const initialLoginSettings: LoginSettingsData = {
|
|
17
17
|
headers: [{ key: "authorization", value: "" }],
|
|
18
18
|
withCredentials: false,
|
|
19
|
-
apiUrl: "",
|
|
20
|
-
defaultAgent: "
|
|
19
|
+
apiUrl: "/api-langgraph",
|
|
20
|
+
defaultAgent: "agent",
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
const apiUrlShortcuts = ["/api/langgraph", "http://localhost:8123", "http://localhost:3000", "http://localhost:8000", "http://localhost:5000"];
|
|
24
|
+
|
|
23
25
|
const LoginSettings: React.FC = () => {
|
|
26
|
+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
24
27
|
const [formData, setFormData] = useState<LoginSettingsData>(() => {
|
|
25
28
|
try {
|
|
26
29
|
const storedHeaders = localStorage.getItem("code");
|
|
@@ -62,6 +65,21 @@ const LoginSettings: React.FC = () => {
|
|
|
62
65
|
}
|
|
63
66
|
}, [formData]);
|
|
64
67
|
|
|
68
|
+
// 点击外部关闭下拉菜单
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
71
|
+
const target = event.target as Element;
|
|
72
|
+
if (isDropdownOpen && !target.closest(".api-url-dropdown")) {
|
|
73
|
+
setIsDropdownOpen(false);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
78
|
+
return () => {
|
|
79
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
80
|
+
};
|
|
81
|
+
}, [isDropdownOpen]);
|
|
82
|
+
|
|
65
83
|
const addHeader = useCallback(() => {
|
|
66
84
|
setFormData((prevData) => ({
|
|
67
85
|
...prevData,
|
|
@@ -100,6 +118,14 @@ const LoginSettings: React.FC = () => {
|
|
|
100
118
|
}));
|
|
101
119
|
}, []);
|
|
102
120
|
|
|
121
|
+
const handleApiUrlShortcutSelect = useCallback((url: string) => {
|
|
122
|
+
setFormData((prevData) => ({
|
|
123
|
+
...prevData,
|
|
124
|
+
apiUrl: url,
|
|
125
|
+
}));
|
|
126
|
+
setIsDropdownOpen(false);
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
103
129
|
const handleSave = () => {
|
|
104
130
|
// 保存逻辑已经通过 useEffect 处理了,这里可以添加其他保存后的操作,例如提示用户保存成功
|
|
105
131
|
toast.success("配置已保存!重启程序");
|
|
@@ -129,15 +155,42 @@ const LoginSettings: React.FC = () => {
|
|
|
129
155
|
<label htmlFor="api-url" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
130
156
|
API URL
|
|
131
157
|
</label>
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
158
|
+
<div className="relative api-url-dropdown">
|
|
159
|
+
<div className="flex">
|
|
160
|
+
<input
|
|
161
|
+
type="text"
|
|
162
|
+
id="apiUrl"
|
|
163
|
+
name="apiUrl"
|
|
164
|
+
value={formData.apiUrl}
|
|
165
|
+
onChange={handleInputChange}
|
|
166
|
+
placeholder="例如: http://localhost:8123"
|
|
167
|
+
className="flex-1 px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
168
|
+
/>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
|
172
|
+
className="px-3 py-3 border border-l-0 border-gray-300 dark:border-gray-600 rounded-r-lg bg-gray-50 dark:bg-gray-600 hover:bg-gray-100 dark:hover:bg-gray-500 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
173
|
+
>
|
|
174
|
+
<svg className="w-5 h-5 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
175
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
|
176
|
+
</svg>
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
{isDropdownOpen && (
|
|
180
|
+
<div className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg">
|
|
181
|
+
{apiUrlShortcuts.map((url, index) => (
|
|
182
|
+
<button
|
|
183
|
+
key={index}
|
|
184
|
+
type="button"
|
|
185
|
+
onClick={() => handleApiUrlShortcutSelect(url)}
|
|
186
|
+
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors first:rounded-t-lg last:rounded-b-lg text-gray-900 dark:text-white"
|
|
187
|
+
>
|
|
188
|
+
{url}
|
|
189
|
+
</button>
|
|
190
|
+
))}
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
141
194
|
</div>
|
|
142
195
|
|
|
143
196
|
<div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect, ReactNode } from "react";
|
|
2
2
|
import LoginSettings from "./LoginSettings"; // 导入新的 LoginSettings 组件
|
|
3
|
+
import ConsoleSettings from "./ConsoleSettings"; // 导入控制台设置组件
|
|
3
4
|
|
|
4
5
|
interface SettingPanelProps {
|
|
5
6
|
isOpen: boolean;
|
|
@@ -16,16 +17,16 @@ export interface SettingTab {
|
|
|
16
17
|
const SettingPanel: React.FC<SettingPanelProps> = ({ isOpen, onClose, tabs: externalTabs }) => {
|
|
17
18
|
// 内部默认的 tabs
|
|
18
19
|
const defaultTabs: SettingTab[] = [
|
|
19
|
-
{
|
|
20
|
-
id: "general",
|
|
21
|
-
title: "通用",
|
|
22
|
-
component: <div className="text-gray-500 dark:text-gray-400">通用设置内容将在此显示</div>,
|
|
23
|
-
},
|
|
24
20
|
{
|
|
25
21
|
id: "server-login",
|
|
26
22
|
title: "服务器",
|
|
27
23
|
component: <LoginSettings />,
|
|
28
24
|
},
|
|
25
|
+
{
|
|
26
|
+
id: "console",
|
|
27
|
+
title: "控制台",
|
|
28
|
+
component: <ConsoleSettings />,
|
|
29
|
+
},
|
|
29
30
|
// 更多设置页面将在这里添加
|
|
30
31
|
];
|
|
31
32
|
|
package/vite.config.ts
CHANGED
|
@@ -1,12 +1,50 @@
|
|
|
1
1
|
import react from "@vitejs/plugin-react";
|
|
2
|
-
import { defineConfig } from "vite";
|
|
2
|
+
import { defineConfig, Plugin } from "vite";
|
|
3
3
|
import basicSsl from "@vitejs/plugin-basic-ssl";
|
|
4
4
|
import tailwindcss from "@tailwindcss/vite";
|
|
5
|
+
import { Readable } from "stream";
|
|
6
|
+
const OpenSmithPlugin = () =>
|
|
7
|
+
({
|
|
8
|
+
name: "open-smith",
|
|
9
|
+
configureServer(server) {
|
|
10
|
+
server.middlewares.use("/api/open-smith", async (req, res, next) => {
|
|
11
|
+
const { app } = await import("@langgraph-js/open-smith/dist/app.js");
|
|
12
|
+
try {
|
|
13
|
+
const body = Readable.toWeb(req);
|
|
14
|
+
// Build a compatible Request for Fetch API
|
|
15
|
+
const url = `http://localhost${req.url}`;
|
|
16
|
+
const fetchRequest = new Request(url, {
|
|
17
|
+
method: req.method,
|
|
18
|
+
headers: req.headers as any,
|
|
19
|
+
body: req.method && !["GET", "HEAD"].includes(req.method.toUpperCase()) && body ? body : undefined,
|
|
20
|
+
...(req.method && !["GET", "HEAD"].includes(req.method.toUpperCase()) && body ? { duplex: "half" as const } : {}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Proxy the request to app.basePath handler
|
|
24
|
+
const response = await app.basePath("/api/open-smith").fetch(fetchRequest);
|
|
25
|
+
|
|
26
|
+
// Set status and headers
|
|
27
|
+
res.statusCode = response.status;
|
|
28
|
+
// @ts-ignore
|
|
29
|
+
for (const [key, value] of response.headers.entries()) {
|
|
30
|
+
res.setHeader(key, value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Send the response body
|
|
34
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
35
|
+
res.end(Buffer.from(arrayBuffer));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.log(error);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
}) as Plugin;
|
|
42
|
+
|
|
5
43
|
// https://vitejs.dev/config/
|
|
6
44
|
export default defineConfig(({ mode }) => {
|
|
7
45
|
const isHttps = mode === "https";
|
|
8
46
|
return {
|
|
9
|
-
plugins: [react(), tailwindcss(), isHttps ? basicSsl() : undefined],
|
|
47
|
+
plugins: [react(), tailwindcss(), isHttps ? basicSsl() : undefined, process.env.NODE_ENV === "development" && OpenSmithPlugin()],
|
|
10
48
|
resolve: {
|
|
11
49
|
alias: {
|
|
12
50
|
"@langgraph-js/sdk": new URL("../langgraph-client/src", import.meta.url).pathname,
|
|
@@ -17,6 +55,13 @@ export default defineConfig(({ mode }) => {
|
|
|
17
55
|
exclude: ["@langgraph-js/ui", "@langgraph-js/sdk"],
|
|
18
56
|
},
|
|
19
57
|
server: {
|
|
58
|
+
proxy: {
|
|
59
|
+
"/api/langgraph": {
|
|
60
|
+
target: "http://localhost:8123",
|
|
61
|
+
changeOrigin: true,
|
|
62
|
+
rewrite: (path) => path.replace(/^\/api\/langgraph/, ""),
|
|
63
|
+
},
|
|
64
|
+
},
|
|
20
65
|
// headers: {
|
|
21
66
|
// "Cross-Origin-Opener-Policy": "same-origin",
|
|
22
67
|
// "Cross-Origin-Embedder-Policy": "require-corp",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{U as a,C as n}from"./mermaid.core-DboFnVDg.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-DiGvJtfO.js";import{_ as i}from"./mermaid.core-DboFnVDg.js";import"./index-EdVqpCiz.js";import"./chunk-FMBD7UC4-KfYlm8ba.js";import"./chunk-55IACEB6-CVyjVz79.js";import"./chunk-QN33PNHL-Bwbc5Dxk.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-DiGvJtfO.js";import{_ as i}from"./mermaid.core-DboFnVDg.js";import"./index-EdVqpCiz.js";import"./chunk-FMBD7UC4-KfYlm8ba.js";import"./chunk-55IACEB6-CVyjVz79.js";import"./chunk-QN33PNHL-Bwbc5Dxk.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{b as r}from"./index-EdVqpCiz.js";var e=4;function a(o){return r(o,e)}export{a as c};
|