@langgraph-js/ui 1.4.0 → 1.6.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-7vem5Peg.css +1 -0
- package/dist/assets/index-CZ6k2dGe.js +235 -0
- package/dist/index.html +3 -5
- package/index.html +1 -3
- package/package.json +10 -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 +102 -27
- 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 +57 -45
- 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 +21 -19
- package/src/chat/components/MessageHuman.tsx +13 -10
- package/src/chat/components/MessageTool.tsx +71 -30
- package/src/chat/components/UsageMetadata.tsx +41 -22
- package/src/chat/context/ChatContext.tsx +14 -5
- package/src/chat/store/index.ts +25 -2
- package/src/chat/tools/ask_user_for_approve.tsx +80 -0
- package/src/chat/tools/create_artifacts.tsx +50 -0
- package/src/chat/tools/index.ts +5 -0
- 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 +1 -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-BWndsYW1.js +0 -214
- package/dist/assets/index-LcgERueJ.css +0 -1
- package/src/chat/chat.css +0 -406
- 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/chat/tools.ts +0 -33
- 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-CZ6k2dGe.js"></script>
|
|
16
|
+
<link rel="stylesheet" crossorigin href="/assets/index-7vem5Peg.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.6.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -14,16 +14,24 @@
|
|
|
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",
|
|
21
28
|
"react": "^19.0.0",
|
|
22
29
|
"react-dom": "^19.0.0",
|
|
23
30
|
"react-markdown": "^10.1.0",
|
|
24
31
|
"remark-gfm": "^4.0.1",
|
|
32
|
+
"unocss": "^66.1.3",
|
|
25
33
|
"vite": "^6.2.0",
|
|
26
|
-
"@langgraph-js/sdk": "1.
|
|
34
|
+
"@langgraph-js/sdk": "1.7.6"
|
|
27
35
|
},
|
|
28
36
|
"devDependencies": {
|
|
29
37
|
"@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,15 +10,57 @@ 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";
|
|
14
17
|
|
|
15
18
|
const ChatMessages: React.FC = () => {
|
|
16
|
-
const { renderMessages, loading, inChatError, client, collapsedTools, toggleToolCollapse } = useChat();
|
|
19
|
+
const { renderMessages, loading, inChatError, client, collapsedTools, toggleToolCollapse, isFELocking } = useChat();
|
|
20
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
const MessageContainer = useRef<HTMLDivElement>(null);
|
|
22
|
+
|
|
23
|
+
// 检查是否足够接近底部(距离底部 30% 以内)
|
|
24
|
+
const isNearBottom = () => {
|
|
25
|
+
if (!MessageContainer.current) return false;
|
|
26
|
+
|
|
27
|
+
const container = MessageContainer.current;
|
|
28
|
+
const scrollPosition = container.scrollTop + container.clientHeight;
|
|
29
|
+
const scrollHeight = container.scrollHeight;
|
|
30
|
+
|
|
31
|
+
// 当距离底部不超过容器高度的 30% 时,认为足够接近底部
|
|
32
|
+
return scrollHeight - scrollPosition <= container.clientHeight * 0.3;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const scrollToBottom = () => {
|
|
36
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
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
|
};
|
|
@@ -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,56 +139,79 @@ 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">
|
|
152
|
+
<UsageMetadata usage_metadata={client?.tokenCounter || {}} />
|
|
153
|
+
</div>
|
|
104
154
|
</div>
|
|
105
155
|
);
|
|
106
156
|
};
|
|
107
157
|
|
|
108
158
|
const Chat: React.FC = () => {
|
|
109
159
|
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
|
110
|
-
const { showHistory, toggleHistoryVisible, showGraph, toggleGraphVisible } = useChat();
|
|
160
|
+
const { showHistory, toggleHistoryVisible, showGraph, toggleGraphVisible, renderMessages } = useChat();
|
|
111
161
|
const { extraParams, setExtraParams } = useExtraParams();
|
|
162
|
+
const { showArtifact, setShowArtifact } = useArtifacts();
|
|
112
163
|
|
|
113
164
|
return (
|
|
114
|
-
<div className="
|
|
165
|
+
<div className="flex h-full w-full overflow-hidden">
|
|
115
166
|
{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>
|
|
167
|
+
<div className="flex-1 flex flex-col overflow-auto">
|
|
168
|
+
<div className="flex items-center gap-2 p-4 border-b border-gray-200 justify-end h-16">
|
|
122
169
|
<button
|
|
123
|
-
className="
|
|
170
|
+
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
171
|
onClick={() => {
|
|
125
172
|
toggleHistoryVisible();
|
|
126
173
|
setLocalConfig({ showHistory: !showHistory });
|
|
127
174
|
}}
|
|
128
175
|
>
|
|
176
|
+
<History className="w-4 h-4" />
|
|
129
177
|
历史记录
|
|
130
178
|
</button>
|
|
179
|
+
<div className="flex-1"></div>
|
|
180
|
+
<JsonToMessageButton />
|
|
181
|
+
<button
|
|
182
|
+
onClick={() => setIsPopupOpen(true)}
|
|
183
|
+
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"
|
|
184
|
+
>
|
|
185
|
+
<FileJson className="w-4 h-4" />
|
|
186
|
+
编辑参数
|
|
187
|
+
</button>
|
|
188
|
+
|
|
131
189
|
<button
|
|
132
|
-
className="
|
|
190
|
+
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"
|
|
191
|
+
onClick={() => {
|
|
192
|
+
console.log(renderMessages);
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
打印日志数据
|
|
196
|
+
</button>
|
|
197
|
+
<button
|
|
198
|
+
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
199
|
onClick={() => {
|
|
134
200
|
toggleGraphVisible();
|
|
135
201
|
setLocalConfig({ showGraph: !showGraph });
|
|
136
202
|
}}
|
|
137
203
|
>
|
|
138
|
-
|
|
204
|
+
<Network className="w-4 h-4" />
|
|
205
|
+
节点图
|
|
139
206
|
</button>
|
|
140
207
|
<button
|
|
141
|
-
className="
|
|
208
|
+
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
209
|
onClick={() => {
|
|
143
210
|
localStorage.setItem("code", "");
|
|
144
211
|
location.reload();
|
|
145
212
|
}}
|
|
146
213
|
>
|
|
214
|
+
<LogOut className="w-4 h-4" />
|
|
147
215
|
退出登陆
|
|
148
216
|
</button>
|
|
149
217
|
</div>
|
|
@@ -151,7 +219,12 @@ const Chat: React.FC = () => {
|
|
|
151
219
|
<ChatInput />
|
|
152
220
|
<JsonEditorPopup isOpen={isPopupOpen} initialJson={extraParams} onClose={() => setIsPopupOpen(false)} onSave={setExtraParams} />
|
|
153
221
|
</div>
|
|
154
|
-
{showGraph &&
|
|
222
|
+
{(showGraph || showArtifact) && (
|
|
223
|
+
<div className="overflow-hidden flex-1">
|
|
224
|
+
{showGraph && <GraphPanel />}
|
|
225
|
+
{showArtifact && <ArtifactViewer />}
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
155
228
|
</div>
|
|
156
229
|
);
|
|
157
230
|
};
|
|
@@ -160,7 +233,9 @@ const ChatWrapper: React.FC = () => {
|
|
|
160
233
|
return (
|
|
161
234
|
<ChatProvider>
|
|
162
235
|
<ExtraParamsProvider>
|
|
163
|
-
<
|
|
236
|
+
<ArtifactsProvider>
|
|
237
|
+
<Chat />
|
|
238
|
+
</ArtifactsProvider>
|
|
164
239
|
</ExtraParamsProvider>
|
|
165
240
|
</ChatProvider>
|
|
166
241
|
);
|
|
@@ -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>
|