@langgraph-js/ui 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +2 -0
- package/.env.example +2 -0
- package/dist/assets/index-C0dczJ0v.css +1 -0
- package/dist/assets/index-JlVlMqZ-.js +248 -0
- package/dist/index.html +3 -5
- package/index.html +1 -3
- package/package.json +11 -2
- package/src/artifacts/ArtifactViewer.tsx +158 -0
- package/src/artifacts/ArtifactsContext.tsx +99 -0
- package/src/artifacts/SourceCodeViewer.tsx +15 -0
- package/src/chat/Chat.tsx +100 -32
- package/src/chat/FileUpload/index.ts +3 -7
- package/src/chat/components/FileList.tsx +16 -12
- package/src/chat/components/HistoryList.tsx +39 -137
- package/src/chat/components/JsonEditorPopup.tsx +196 -48
- package/src/chat/components/JsonToMessage/JsonToMessage.tsx +20 -14
- package/src/chat/components/JsonToMessage/JsonToMessageButton.tsx +9 -16
- package/src/chat/components/JsonToMessage/index.tsx +1 -1
- package/src/chat/components/MessageAI.tsx +5 -4
- package/src/chat/components/MessageBox.tsx +20 -19
- package/src/chat/components/MessageHuman.tsx +13 -10
- package/src/chat/components/MessageTool.tsx +85 -8
- package/src/chat/components/UsageMetadata.tsx +41 -22
- package/src/chat/context/ChatContext.tsx +14 -5
- package/src/chat/index.css +4 -0
- package/src/chat/store/index.ts +25 -2
- package/src/chat/tools/ask_user_for_approve.tsx +25 -13
- package/src/chat/tools/create_artifacts.tsx +50 -0
- package/src/chat/tools/index.ts +3 -2
- package/src/chat/tools/update_plan.tsx +75 -0
- package/src/chat/tools/web_search_tool.tsx +89 -0
- package/src/graph/index.tsx +9 -6
- package/src/index.ts +2 -0
- package/src/login/Login.tsx +155 -47
- package/src/memory/BaseDB.ts +92 -0
- package/src/memory/db.ts +232 -0
- package/src/memory/fulltext-search.ts +191 -0
- package/src/memory/index.ts +4 -0
- package/src/memory/tools.ts +170 -0
- package/test/main.tsx +2 -2
- package/vite.config.ts +7 -1
- package/dist/assets/index-CLyKQAUN.js +0 -214
- package/dist/assets/index-D80TEgwy.css +0 -1
- package/src/chat/chat.css +0 -552
- package/src/chat/components/FileList.css +0 -129
- package/src/chat/components/JsonEditorPopup.css +0 -81
- package/src/chat/components/JsonToMessage/JsonToMessage.css +0 -104
- package/src/login/Login.css +0 -93
package/dist/index.html
CHANGED
|
@@ -10,14 +10,12 @@
|
|
|
10
10
|
html {
|
|
11
11
|
margin: 0;
|
|
12
12
|
padding: 0;
|
|
13
|
-
width: 100%;
|
|
14
|
-
height: 100%;
|
|
15
13
|
}
|
|
16
14
|
</style>
|
|
17
|
-
<script type="module" crossorigin src="/assets/index-
|
|
18
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<script type="module" crossorigin src="/assets/index-JlVlMqZ-.js"></script>
|
|
16
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C0dczJ0v.css">
|
|
19
17
|
</head>
|
|
20
18
|
<body>
|
|
21
|
-
<div id="root"></div>
|
|
19
|
+
<div id="root" class="h-screen w-screen"></div>
|
|
22
20
|
</body>
|
|
23
21
|
</html>
|
package/index.html
CHANGED
|
@@ -10,13 +10,11 @@
|
|
|
10
10
|
html {
|
|
11
11
|
margin: 0;
|
|
12
12
|
padding: 0;
|
|
13
|
-
width: 100%;
|
|
14
|
-
height: 100%;
|
|
15
13
|
}
|
|
16
14
|
</style>
|
|
17
15
|
</head>
|
|
18
16
|
<body>
|
|
19
|
-
<div id="root"></div>
|
|
17
|
+
<div id="root" class="h-screen w-screen"></div>
|
|
20
18
|
<script type="module" src="./test/main.tsx"></script>
|
|
21
19
|
</body>
|
|
22
20
|
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@langgraph-js/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -14,16 +14,25 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@dagrejs/dagre": "^1.1.4",
|
|
16
16
|
"@nanostores/react": "^1.0.0",
|
|
17
|
+
"@unocss/reset": "^66.1.3",
|
|
17
18
|
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
|
18
19
|
"@vitejs/plugin-react": "^4.3.4",
|
|
19
20
|
"@xyflow/react": "^12.6.4",
|
|
21
|
+
"comlink": "^4.4.2",
|
|
22
|
+
"github-markdown-css": "^5.8.1",
|
|
23
|
+
"idb": "^8.0.3",
|
|
24
|
+
"lucide-react": "^0.511.0",
|
|
25
|
+
"minisearch": "^7.1.2",
|
|
26
|
+
"motion": "^12.16.0",
|
|
20
27
|
"nanostores": "^1.0.1",
|
|
28
|
+
"prism-react-renderer": "^2.4.1",
|
|
21
29
|
"react": "^19.0.0",
|
|
22
30
|
"react-dom": "^19.0.0",
|
|
23
31
|
"react-markdown": "^10.1.0",
|
|
24
32
|
"remark-gfm": "^4.0.1",
|
|
33
|
+
"unocss": "^66.1.3",
|
|
25
34
|
"vite": "^6.2.0",
|
|
26
|
-
"@langgraph-js/sdk": "1.
|
|
35
|
+
"@langgraph-js/sdk": "1.10.4"
|
|
27
36
|
},
|
|
28
37
|
"devDependencies": {
|
|
29
38
|
"@types/react": "^19.0.10",
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { wrap, windowEndpoint } from "comlink";
|
|
3
|
+
import { useArtifacts } from "./ArtifactsContext";
|
|
4
|
+
import { SourceCodeViewer } from "./SourceCodeViewer";
|
|
5
|
+
import { ChevronDown } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
type ViewMode = "preview" | "source";
|
|
8
|
+
|
|
9
|
+
export const ArtifactViewer: React.FC = () => {
|
|
10
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
11
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
12
|
+
const { currentArtifact, getArtifactVersions, setCurrentArtifactById, artifacts } = useArtifacts();
|
|
13
|
+
const [iframeKey, setIframeKey] = useState(0);
|
|
14
|
+
const [viewMode, setViewMode] = useState<ViewMode>("preview");
|
|
15
|
+
const [isFileSelectOpen, setIsFileSelectOpen] = useState(false);
|
|
16
|
+
|
|
17
|
+
const getIframeAPI = async (iframe: HTMLIFrameElement) => {
|
|
18
|
+
const iframeApi = wrap(windowEndpoint(iframe.contentWindow!));
|
|
19
|
+
|
|
20
|
+
// 5 秒内,每 50 ms 检测一次 init 函数
|
|
21
|
+
const index = await Promise.race(
|
|
22
|
+
Array(100)
|
|
23
|
+
.fill(0)
|
|
24
|
+
.map((_, index) => {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
setTimeout(async () => {
|
|
27
|
+
/* @ts-ignore */
|
|
28
|
+
if (await iframeApi.init()) {
|
|
29
|
+
resolve(index);
|
|
30
|
+
}
|
|
31
|
+
}, 100 * index);
|
|
32
|
+
});
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return iframeApi;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const runCode = async () => {
|
|
40
|
+
console.log(iframeKey);
|
|
41
|
+
if (!iframeRef.current) return;
|
|
42
|
+
|
|
43
|
+
setIsLoading(true);
|
|
44
|
+
try {
|
|
45
|
+
const iframeApi: any = await getIframeAPI(iframeRef.current);
|
|
46
|
+
await iframeApi.run(currentArtifact?.code, currentArtifact?.filename, currentArtifact?.filetype);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error("Failed to run code:", error);
|
|
49
|
+
} finally {
|
|
50
|
+
setIsLoading(false);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const refresh = () => {
|
|
55
|
+
setIframeKey((prev) => prev + 1);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// useEffect(() => {
|
|
59
|
+
// if (currentArtifact && iframeRef.current) {
|
|
60
|
+
// setIframeKey((prev) => prev + 1);
|
|
61
|
+
// }
|
|
62
|
+
// }, [currentArtifact?.id]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (iframeRef.current) {
|
|
66
|
+
runCode();
|
|
67
|
+
}
|
|
68
|
+
}, [iframeKey]);
|
|
69
|
+
|
|
70
|
+
if (!currentArtifact) {
|
|
71
|
+
return <div className="h-full w-full flex items-center justify-center text-gray-500">请选择一个文件</div>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const versions = getArtifactVersions(currentArtifact.filename);
|
|
75
|
+
|
|
76
|
+
// 获取所有唯一的文件名
|
|
77
|
+
const uniqueFilenames = Array.from(new Set(artifacts.map((a) => a.filename)));
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className="h-full w-full flex flex-col">
|
|
81
|
+
<div className="flex items-center justify-between p-2 border-b">
|
|
82
|
+
<div className="flex items-center space-x-4">
|
|
83
|
+
<div className="flex space-x-2">
|
|
84
|
+
<button
|
|
85
|
+
className={`px-3 py-1 rounded ${viewMode === "preview" ? "bg-blue-500 text-white" : "bg-gray-100 text-gray-700"}`}
|
|
86
|
+
onClick={() => {
|
|
87
|
+
setViewMode("preview");
|
|
88
|
+
refresh();
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
预览
|
|
92
|
+
</button>
|
|
93
|
+
<button className={`px-3 py-1 rounded ${viewMode === "source" ? "bg-blue-500 text-white" : "bg-gray-100 text-gray-700"}`} onClick={() => setViewMode("source")}>
|
|
94
|
+
源代码
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="relative">
|
|
98
|
+
<button className="flex items-center space-x-2 px-3 py-1 bg-gray-100 rounded hover:bg-gray-200" onClick={() => setIsFileSelectOpen(!isFileSelectOpen)}>
|
|
99
|
+
<span>{currentArtifact.filename}</span>
|
|
100
|
+
<ChevronDown className="w-4 h-4" />
|
|
101
|
+
</button>
|
|
102
|
+
{isFileSelectOpen && (
|
|
103
|
+
<div className="absolute top-full left-0 mt-1 w-48 bg-white border rounded-md shadow-lg z-10">
|
|
104
|
+
{uniqueFilenames.map((filename) => {
|
|
105
|
+
const fileVersions = getArtifactVersions(filename);
|
|
106
|
+
const latestVersion = fileVersions[fileVersions.length - 1];
|
|
107
|
+
return (
|
|
108
|
+
<button
|
|
109
|
+
key={filename}
|
|
110
|
+
className="w-full text-left px-3 py-2 hover:bg-gray-100"
|
|
111
|
+
onClick={() => {
|
|
112
|
+
setCurrentArtifactById(latestVersion.id);
|
|
113
|
+
setIsFileSelectOpen(false);
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
{filename}
|
|
117
|
+
</button>
|
|
118
|
+
);
|
|
119
|
+
})}
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
<div className="flex items-center space-x-2">
|
|
124
|
+
<span className="text-sm text-gray-500">版本:</span>
|
|
125
|
+
<div className="flex space-x-1">
|
|
126
|
+
{versions.map((version) => (
|
|
127
|
+
<button
|
|
128
|
+
key={version.id}
|
|
129
|
+
className={`px-2 py-1 text-sm rounded ${version.id === currentArtifact.id ? "bg-blue-500 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200"}`}
|
|
130
|
+
onClick={() => setCurrentArtifactById(version.id)}
|
|
131
|
+
>
|
|
132
|
+
v{version.version}
|
|
133
|
+
</button>
|
|
134
|
+
))}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
{viewMode === "preview" && (
|
|
139
|
+
<div className="flex space-x-2">
|
|
140
|
+
<button disabled={isLoading} className="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200 disabled:opacity-50">
|
|
141
|
+
{isLoading ? "Running..." : "Run"}
|
|
142
|
+
</button>
|
|
143
|
+
<button onClick={refresh} disabled={isLoading} className="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200 disabled:opacity-50">
|
|
144
|
+
Refresh
|
|
145
|
+
</button>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
<div className="flex-1 overflow-auto">
|
|
150
|
+
{viewMode === "preview" ? (
|
|
151
|
+
<iframe key={iframeKey} ref={iframeRef} src="https://langgraph-artifacts.netlify.app/index.html" className="w-full h-full border border-gray-300" />
|
|
152
|
+
) : (
|
|
153
|
+
<SourceCodeViewer />
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, ReactNode, useEffect } from "react";
|
|
2
|
+
import { useChat } from "../chat/context/ChatContext";
|
|
3
|
+
import { Message } from "@langgraph-js/sdk";
|
|
4
|
+
|
|
5
|
+
interface Artifact {
|
|
6
|
+
id: string;
|
|
7
|
+
code: string;
|
|
8
|
+
filename: string;
|
|
9
|
+
filetype: string;
|
|
10
|
+
version: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ArtifactsContextType {
|
|
14
|
+
artifacts: Artifact[];
|
|
15
|
+
currentArtifact: Artifact | null;
|
|
16
|
+
setCurrentArtifactById: (id: string) => void;
|
|
17
|
+
getArtifactVersions: (filename: string) => Artifact[];
|
|
18
|
+
showArtifact: boolean;
|
|
19
|
+
setShowArtifact: (show: boolean) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ArtifactsContext = createContext<ArtifactsContextType>({
|
|
23
|
+
artifacts: [],
|
|
24
|
+
currentArtifact: null,
|
|
25
|
+
setCurrentArtifactById: () => {},
|
|
26
|
+
getArtifactVersions: () => [],
|
|
27
|
+
showArtifact: false,
|
|
28
|
+
setShowArtifact: () => {},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const useArtifacts = () => useContext(ArtifactsContext);
|
|
32
|
+
|
|
33
|
+
interface ArtifactsProviderProps {
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const ArtifactsProvider: React.FC<ArtifactsProviderProps> = ({ children }) => {
|
|
38
|
+
const [artifacts, setArtifacts] = useState<Artifact[]>([]);
|
|
39
|
+
const [showArtifact, setShowArtifact] = useState(false);
|
|
40
|
+
const { renderMessages } = useChat();
|
|
41
|
+
const [currentArtifact, setCurrentArtifact] = useState<Artifact | null>(null);
|
|
42
|
+
|
|
43
|
+
// 获取指定文件名的所有版本
|
|
44
|
+
const getArtifactVersions = (filename: string) => {
|
|
45
|
+
return artifacts.filter((artifact) => artifact.filename === filename).sort((a, b) => a.version - b.version);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!renderMessages) return;
|
|
50
|
+
|
|
51
|
+
const createArtifacts = renderMessages.filter((message) => message.type === "tool").filter((message) => message.name === "create_artifacts");
|
|
52
|
+
|
|
53
|
+
// 创建文件名到最新版本的映射
|
|
54
|
+
const filenameToLatestVersion = new Map<string, number>();
|
|
55
|
+
|
|
56
|
+
// 处理每个 artifact,分配版本号
|
|
57
|
+
const processedArtifacts = createArtifacts.map((message) => {
|
|
58
|
+
const content = JSON.parse(message.tool_input as string);
|
|
59
|
+
const filename = content.filename;
|
|
60
|
+
|
|
61
|
+
// 获取当前文件名的最新版本号
|
|
62
|
+
const currentVersion = filenameToLatestVersion.get(filename) || 0;
|
|
63
|
+
const newVersion = currentVersion + 1;
|
|
64
|
+
|
|
65
|
+
// 更新最新版本号
|
|
66
|
+
filenameToLatestVersion.set(filename, newVersion);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
id: message.id!,
|
|
70
|
+
code: content.code,
|
|
71
|
+
filename: filename,
|
|
72
|
+
version: newVersion,
|
|
73
|
+
filetype: content.filetype,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
setArtifacts(processedArtifacts);
|
|
78
|
+
}, [renderMessages]);
|
|
79
|
+
|
|
80
|
+
const setCurrentArtifactById = (id: string) => {
|
|
81
|
+
setShowArtifact(true);
|
|
82
|
+
setCurrentArtifact(artifacts.find((artifact) => artifact.id === id) || null);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<ArtifactsContext.Provider
|
|
87
|
+
value={{
|
|
88
|
+
artifacts,
|
|
89
|
+
currentArtifact,
|
|
90
|
+
setCurrentArtifactById,
|
|
91
|
+
getArtifactVersions,
|
|
92
|
+
showArtifact,
|
|
93
|
+
setShowArtifact,
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
{children}
|
|
97
|
+
</ArtifactsContext.Provider>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useArtifacts } from "./ArtifactsContext";
|
|
2
|
+
|
|
3
|
+
export const SourceCodeViewer: React.FC = () => {
|
|
4
|
+
const { currentArtifact } = useArtifacts();
|
|
5
|
+
|
|
6
|
+
if (!currentArtifact) {
|
|
7
|
+
return <div className="h-full w-full flex items-center justify-center text-gray-500">请选择一个文件</div>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="h-full w-full overflow-auto">
|
|
12
|
+
<pre>{currentArtifact.code}</pre>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
};
|
package/src/chat/Chat.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
2
|
-
import "./chat.css";
|
|
1
|
+
import React, { useState, useRef, useEffect } from "react";
|
|
3
2
|
import { MessagesBox } from "./components/MessageBox";
|
|
4
3
|
import HistoryList from "./components/HistoryList";
|
|
5
4
|
import { ChatProvider, useChat } from "./context/ChatContext";
|
|
@@ -11,21 +10,63 @@ import JsonEditorPopup from "./components/JsonEditorPopup";
|
|
|
11
10
|
import { JsonToMessageButton } from "./components/JsonToMessage";
|
|
12
11
|
import { GraphPanel } from "../graph/GraphPanel";
|
|
13
12
|
import { setLocalConfig } from "./store";
|
|
13
|
+
import { History, Network, LogOut, FileJson, Code } from "lucide-react";
|
|
14
|
+
import { ArtifactViewer } from "../artifacts/ArtifactViewer";
|
|
15
|
+
import "github-markdown-css/github-markdown.css";
|
|
16
|
+
import { ArtifactsProvider, useArtifacts } from "../artifacts/ArtifactsContext";
|
|
17
|
+
import "./index.css";
|
|
14
18
|
|
|
15
19
|
const ChatMessages: React.FC = () => {
|
|
16
|
-
const { renderMessages, loading, inChatError, client, collapsedTools, toggleToolCollapse } = useChat();
|
|
20
|
+
const { renderMessages, loading, inChatError, client, collapsedTools, toggleToolCollapse, isFELocking } = useChat();
|
|
21
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
22
|
+
const MessageContainer = useRef<HTMLDivElement>(null);
|
|
23
|
+
|
|
24
|
+
// 检查是否足够接近底部(距离底部 30% 以内)
|
|
25
|
+
const isNearBottom = () => {
|
|
26
|
+
if (!MessageContainer.current) return false;
|
|
27
|
+
|
|
28
|
+
const container = MessageContainer.current;
|
|
29
|
+
const scrollPosition = container.scrollTop + container.clientHeight;
|
|
30
|
+
const scrollHeight = container.scrollHeight;
|
|
31
|
+
|
|
32
|
+
return scrollHeight - scrollPosition <= 50;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const scrollToBottom = () => {
|
|
36
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "instant" });
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (renderMessages.length > 0 && MessageContainer.current) {
|
|
41
|
+
// 切换消息时,自动滚动到底部
|
|
42
|
+
if (!loading) {
|
|
43
|
+
scrollToBottom();
|
|
44
|
+
}
|
|
45
|
+
// 只有当用户已经滚动到接近底部时,才自动滚动到底部
|
|
46
|
+
if (loading && isNearBottom()) {
|
|
47
|
+
scrollToBottom();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}, [renderMessages]);
|
|
17
51
|
|
|
18
52
|
return (
|
|
19
|
-
<div className="
|
|
53
|
+
<div className="flex-1 overflow-y-auto overflow-x-hidden p-4" ref={MessageContainer}>
|
|
20
54
|
<MessagesBox renderMessages={renderMessages} collapsedTools={collapsedTools} toggleToolCollapse={toggleToolCollapse} client={client!} />
|
|
21
|
-
{
|
|
22
|
-
{
|
|
55
|
+
{/* {isFELocking() && <div className="flex items-center justify-center py-4 text-gray-500">请你继续操作</div>} */}
|
|
56
|
+
{loading && !isFELocking() && (
|
|
57
|
+
<div className="flex items-center justify-center py-4 text-gray-500">
|
|
58
|
+
<div className="animate-spin rounded-full h-4 w-4 border-2 border-blue-500 border-t-transparent mr-2"></div>
|
|
59
|
+
正在思考中...
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
{inChatError && <div className="p-4 text-sm text-red-600 bg-red-50 rounded-lg border border-red-200">{JSON.stringify(inChatError)}</div>}
|
|
63
|
+
<div ref={messagesEndRef} />
|
|
23
64
|
</div>
|
|
24
65
|
);
|
|
25
66
|
};
|
|
26
67
|
|
|
27
68
|
const ChatInput: React.FC = () => {
|
|
28
|
-
const { userInput, setUserInput, loading, sendMessage, stopGeneration, currentAgent, setCurrentAgent, client } = useChat();
|
|
69
|
+
const { userInput, setUserInput, loading, sendMessage, stopGeneration, currentAgent, setCurrentAgent, client, currentChatId } = useChat();
|
|
29
70
|
const { extraParams } = useExtraParams();
|
|
30
71
|
const [imageUrls, setImageUrls] = useState<string[]>([]);
|
|
31
72
|
|
|
@@ -69,11 +110,15 @@ const ChatInput: React.FC = () => {
|
|
|
69
110
|
};
|
|
70
111
|
|
|
71
112
|
return (
|
|
72
|
-
<div className="
|
|
73
|
-
<div className="
|
|
113
|
+
<div className="border-t border-gray-200 p-4">
|
|
114
|
+
<div className="flex items-center justify-between mb-4">
|
|
74
115
|
<FileList onFileUploaded={handleFileUploaded} />
|
|
75
|
-
|
|
76
|
-
<select
|
|
116
|
+
|
|
117
|
+
<select
|
|
118
|
+
value={currentAgent}
|
|
119
|
+
onChange={(e) => _setCurrentAgent(e.target.value)}
|
|
120
|
+
className="px-3 py-1.5 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
121
|
+
>
|
|
77
122
|
{client?.availableAssistants.map((i) => {
|
|
78
123
|
return (
|
|
79
124
|
<option value={i.graph_id} key={i.graph_id}>
|
|
@@ -83,9 +128,9 @@ const ChatInput: React.FC = () => {
|
|
|
83
128
|
})}
|
|
84
129
|
</select>
|
|
85
130
|
</div>
|
|
86
|
-
<div className="
|
|
131
|
+
<div className="flex gap-2">
|
|
87
132
|
<textarea
|
|
88
|
-
className="
|
|
133
|
+
className="flex-1 p-3 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
|
89
134
|
rows={2}
|
|
90
135
|
value={userInput}
|
|
91
136
|
onChange={(e) => setUserInput(e.target.value)}
|
|
@@ -94,13 +139,19 @@ const ChatInput: React.FC = () => {
|
|
|
94
139
|
disabled={loading}
|
|
95
140
|
/>
|
|
96
141
|
<button
|
|
97
|
-
className={`
|
|
142
|
+
className={`px-4 py-2 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 ${
|
|
143
|
+
loading ? "bg-red-500 hover:bg-red-600 focus:ring-red-500" : "bg-blue-500 hover:bg-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
144
|
+
}`}
|
|
98
145
|
onClick={() => (loading ? stopGeneration() : sendMultiModalMessage())}
|
|
99
146
|
disabled={!loading && !userInput.trim() && imageUrls.length === 0}
|
|
100
147
|
>
|
|
101
148
|
{loading ? "中断" : "发送"}
|
|
102
149
|
</button>
|
|
103
150
|
</div>
|
|
151
|
+
<div className="flex border-b border-gray-200 mt-4 gap-2 justify-between">
|
|
152
|
+
<UsageMetadata usage_metadata={client?.tokenCounter || {}} />
|
|
153
|
+
<span className="text-sm text-gray-500">会话 ID: {currentChatId}</span>
|
|
154
|
+
</div>
|
|
104
155
|
</div>
|
|
105
156
|
);
|
|
106
157
|
};
|
|
@@ -109,49 +160,59 @@ const Chat: React.FC = () => {
|
|
|
109
160
|
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
|
110
161
|
const { showHistory, toggleHistoryVisible, showGraph, toggleGraphVisible, renderMessages } = useChat();
|
|
111
162
|
const { extraParams, setExtraParams } = useExtraParams();
|
|
163
|
+
const { showArtifact, setShowArtifact } = useArtifacts();
|
|
112
164
|
|
|
113
165
|
return (
|
|
114
|
-
<div className="chat-container">
|
|
166
|
+
<div className="langgraph-chat-container flex h-full w-full overflow-hidden">
|
|
115
167
|
{showHistory && <HistoryList onClose={() => toggleHistoryVisible()} formatTime={formatTime} />}
|
|
116
|
-
<div className="
|
|
117
|
-
<div className="
|
|
118
|
-
<JsonToMessageButton></JsonToMessageButton>
|
|
119
|
-
<button onClick={() => setIsPopupOpen(true)} className="edit-params-button">
|
|
120
|
-
编辑参数
|
|
121
|
-
</button>
|
|
168
|
+
<div className="flex-1 flex flex-col overflow-auto">
|
|
169
|
+
<div className="flex items-center gap-2 p-4 border-b border-gray-200 justify-end h-16">
|
|
122
170
|
<button
|
|
123
|
-
className="
|
|
171
|
+
className="px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center gap-1.5"
|
|
124
172
|
onClick={() => {
|
|
125
173
|
toggleHistoryVisible();
|
|
126
174
|
setLocalConfig({ showHistory: !showHistory });
|
|
127
175
|
}}
|
|
128
176
|
>
|
|
177
|
+
<History className="w-4 h-4" />
|
|
129
178
|
历史记录
|
|
130
179
|
</button>
|
|
180
|
+
<div className="flex-1"></div>
|
|
181
|
+
<JsonToMessageButton />
|
|
182
|
+
<button
|
|
183
|
+
onClick={() => setIsPopupOpen(true)}
|
|
184
|
+
className="px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center gap-1.5"
|
|
185
|
+
>
|
|
186
|
+
<FileJson className="w-4 h-4" />
|
|
187
|
+
编辑参数
|
|
188
|
+
</button>
|
|
189
|
+
|
|
131
190
|
<button
|
|
132
|
-
className="
|
|
191
|
+
className="px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center gap-1.5"
|
|
133
192
|
onClick={() => {
|
|
134
|
-
|
|
135
|
-
setLocalConfig({ showGraph: !showGraph });
|
|
193
|
+
console.log(renderMessages);
|
|
136
194
|
}}
|
|
137
195
|
>
|
|
138
|
-
|
|
196
|
+
打印日志数据
|
|
139
197
|
</button>
|
|
140
198
|
<button
|
|
141
|
-
className="
|
|
199
|
+
className="px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center gap-1.5"
|
|
142
200
|
onClick={() => {
|
|
143
|
-
|
|
201
|
+
toggleGraphVisible();
|
|
202
|
+
setLocalConfig({ showGraph: !showGraph });
|
|
144
203
|
}}
|
|
145
204
|
>
|
|
146
|
-
|
|
205
|
+
<Network className="w-4 h-4" />
|
|
206
|
+
节点图
|
|
147
207
|
</button>
|
|
148
208
|
<button
|
|
149
|
-
className="
|
|
209
|
+
className="px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center gap-1.5"
|
|
150
210
|
onClick={() => {
|
|
151
211
|
localStorage.setItem("code", "");
|
|
152
212
|
location.reload();
|
|
153
213
|
}}
|
|
154
214
|
>
|
|
215
|
+
<LogOut className="w-4 h-4" />
|
|
155
216
|
退出登陆
|
|
156
217
|
</button>
|
|
157
218
|
</div>
|
|
@@ -159,7 +220,12 @@ const Chat: React.FC = () => {
|
|
|
159
220
|
<ChatInput />
|
|
160
221
|
<JsonEditorPopup isOpen={isPopupOpen} initialJson={extraParams} onClose={() => setIsPopupOpen(false)} onSave={setExtraParams} />
|
|
161
222
|
</div>
|
|
162
|
-
{showGraph &&
|
|
223
|
+
{(showGraph || showArtifact) && (
|
|
224
|
+
<div className="overflow-hidden flex-1">
|
|
225
|
+
{showGraph && <GraphPanel />}
|
|
226
|
+
{showArtifact && <ArtifactViewer />}
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
163
229
|
</div>
|
|
164
230
|
);
|
|
165
231
|
};
|
|
@@ -168,7 +234,9 @@ const ChatWrapper: React.FC = () => {
|
|
|
168
234
|
return (
|
|
169
235
|
<ChatProvider>
|
|
170
236
|
<ExtraParamsProvider>
|
|
171
|
-
<
|
|
237
|
+
<ArtifactsProvider>
|
|
238
|
+
<Chat />
|
|
239
|
+
</ArtifactsProvider>
|
|
172
240
|
</ExtraParamsProvider>
|
|
173
241
|
</ChatProvider>
|
|
174
242
|
);
|
|
@@ -81,7 +81,7 @@ abstract class FileUploadClient {
|
|
|
81
81
|
export class TmpFilesClient extends FileUploadClient {
|
|
82
82
|
constructor(options: FileUploadClientOptions = {}) {
|
|
83
83
|
super({
|
|
84
|
-
apiUrl: options.apiUrl || "https://tmpfiles.org/api/v1"
|
|
84
|
+
apiUrl: options.apiUrl || "https://tmpfiles.org/api/v1",
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -91,15 +91,11 @@ export class TmpFilesClient extends FileUploadClient {
|
|
|
91
91
|
|
|
92
92
|
protected processResponse(response: FileUploadResponse): FileUploadResponse {
|
|
93
93
|
if (response.data?.url) {
|
|
94
|
-
response.data.url = response.data.url.replace("
|
|
94
|
+
response.data.url = response.data.url.replace("//tmpfiles.org/", "//tmpfiles.org/dl/");
|
|
95
95
|
}
|
|
96
96
|
return response;
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// Export types for external use
|
|
101
|
-
export type {
|
|
102
|
-
FileUploadClientOptions,
|
|
103
|
-
FileUploadOptions,
|
|
104
|
-
FileUploadResponse
|
|
105
|
-
};
|
|
101
|
+
export type { FileUploadClientOptions, FileUploadOptions, FileUploadResponse };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { useState, useCallback } from "react";
|
|
2
2
|
import { TmpFilesClient } from "../FileUpload";
|
|
3
|
-
import "./FileList.css";
|
|
4
3
|
|
|
5
4
|
interface FileListProps {
|
|
6
5
|
onFileUploaded: (url: string) => void;
|
|
@@ -15,7 +14,7 @@ const FileList: React.FC<FileListProps> = ({ onFileUploaded }) => {
|
|
|
15
14
|
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
16
15
|
const selectedFiles = Array.from(event.target.files || []);
|
|
17
16
|
const imageFiles = selectedFiles.filter((file) => file.type.startsWith("image/"));
|
|
18
|
-
|
|
17
|
+
|
|
19
18
|
// 检查是否超过最大数量限制
|
|
20
19
|
if (files.length + imageFiles.length > MAX_FILES) {
|
|
21
20
|
alert(`最多只能上传${MAX_FILES}张图片`);
|
|
@@ -46,21 +45,26 @@ const FileList: React.FC<FileListProps> = ({ onFileUploaded }) => {
|
|
|
46
45
|
}, []);
|
|
47
46
|
|
|
48
47
|
return (
|
|
49
|
-
<div className="
|
|
48
|
+
<div className="flex gap-2 rounded-lg flex-1">
|
|
50
49
|
{files.length < MAX_FILES && (
|
|
51
|
-
<label
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
<label
|
|
51
|
+
className={`inline-flex items-center justify-center w-20 h-20 text-gray-500 bg-gray-100 rounded-lg cursor-pointer transition-all duration-200 hover:bg-gray-200 ${files.length === 0 ? "w-8 h-8" : ""}`}
|
|
52
|
+
>
|
|
53
|
+
<svg viewBox="0 0 24 24" width={files.length === 0 ? "20" : "32"} height={files.length === 0 ? "20" : "32"} fill="currentColor">
|
|
54
|
+
<path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z" />
|
|
55
|
+
<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
56
|
</svg>
|
|
56
|
-
<input type="file" accept="image/*" multiple onChange={handleFileChange}
|
|
57
|
+
<input type="file" accept="image/*" multiple onChange={handleFileChange} className="hidden" />
|
|
57
58
|
</label>
|
|
58
59
|
)}
|
|
59
|
-
<div className="
|
|
60
|
+
<div className="flex flex-wrap gap-2">
|
|
60
61
|
{files.map((file, index) => (
|
|
61
|
-
<div key={index} className="
|
|
62
|
-
<img src={URL.createObjectURL(file)} alt={file.name} className="
|
|
63
|
-
<button
|
|
62
|
+
<div key={index} className="relative w-20 h-20 rounded-lg overflow-hidden">
|
|
63
|
+
<img src={URL.createObjectURL(file)} alt={file.name} className="w-full h-full object-cover border border-gray-200" />
|
|
64
|
+
<button
|
|
65
|
+
className="absolute top-0.5 right-0.5 w-5 h-5 bg-black/50 text-white rounded-full flex items-center justify-center text-base leading-none hover:bg-black/70 transition-colors"
|
|
66
|
+
onClick={() => removeFile(index)}
|
|
67
|
+
>
|
|
64
68
|
×
|
|
65
69
|
</button>
|
|
66
70
|
</div>
|