@langgraph-js/ui 1.5.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.
Files changed (47) hide show
  1. package/.env +2 -0
  2. package/.env.example +2 -0
  3. package/dist/assets/index-7vem5Peg.css +1 -0
  4. package/dist/assets/index-CZ6k2dGe.js +235 -0
  5. package/dist/index.html +3 -5
  6. package/index.html +1 -3
  7. package/package.json +10 -2
  8. package/src/artifacts/ArtifactViewer.tsx +158 -0
  9. package/src/artifacts/ArtifactsContext.tsx +99 -0
  10. package/src/artifacts/SourceCodeViewer.tsx +15 -0
  11. package/src/chat/Chat.tsx +98 -31
  12. package/src/chat/FileUpload/index.ts +3 -7
  13. package/src/chat/components/FileList.tsx +16 -12
  14. package/src/chat/components/HistoryList.tsx +39 -137
  15. package/src/chat/components/JsonEditorPopup.tsx +57 -45
  16. package/src/chat/components/JsonToMessage/JsonToMessage.tsx +20 -14
  17. package/src/chat/components/JsonToMessage/JsonToMessageButton.tsx +9 -16
  18. package/src/chat/components/JsonToMessage/index.tsx +1 -1
  19. package/src/chat/components/MessageAI.tsx +5 -4
  20. package/src/chat/components/MessageBox.tsx +21 -19
  21. package/src/chat/components/MessageHuman.tsx +13 -10
  22. package/src/chat/components/MessageTool.tsx +55 -8
  23. package/src/chat/components/UsageMetadata.tsx +41 -22
  24. package/src/chat/context/ChatContext.tsx +14 -5
  25. package/src/chat/store/index.ts +25 -2
  26. package/src/chat/tools/ask_user_for_approve.tsx +25 -13
  27. package/src/chat/tools/create_artifacts.tsx +50 -0
  28. package/src/chat/tools/index.ts +3 -2
  29. package/src/chat/tools/update_plan.tsx +75 -0
  30. package/src/chat/tools/web_search_tool.tsx +89 -0
  31. package/src/graph/index.tsx +9 -6
  32. package/src/index.ts +1 -0
  33. package/src/login/Login.tsx +155 -47
  34. package/src/memory/BaseDB.ts +92 -0
  35. package/src/memory/db.ts +232 -0
  36. package/src/memory/fulltext-search.ts +191 -0
  37. package/src/memory/index.ts +4 -0
  38. package/src/memory/tools.ts +170 -0
  39. package/test/main.tsx +2 -2
  40. package/vite.config.ts +7 -1
  41. package/dist/assets/index-CLyKQAUN.js +0 -214
  42. package/dist/assets/index-D80TEgwy.css +0 -1
  43. package/src/chat/chat.css +0 -552
  44. package/src/chat/components/FileList.css +0 -129
  45. package/src/chat/components/JsonEditorPopup.css +0 -81
  46. package/src/chat/components/JsonToMessage/JsonToMessage.css +0 -104
  47. 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-CLyKQAUN.js"></script>
18
- <link rel="stylesheet" crossorigin href="/assets/index-D80TEgwy.css">
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.5.0",
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.6.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="chat-messages">
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
- {loading && <div className="loading-indicator">正在思考中...</div>}
22
- {inChatError && <div className="error-message">{JSON.stringify(inChatError)}</div>}
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="chat-input">
73
- <div className="chat-input-header">
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
- <UsageMetadata usage_metadata={client?.tokenCounter || {}} />
76
- <select value={currentAgent} onChange={(e) => _setCurrentAgent(e.target.value)}>
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="input-container">
131
+ <div className="flex gap-2">
87
132
  <textarea
88
- className="input-textarea"
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,18 @@ const ChatInput: React.FC = () => {
94
139
  disabled={loading}
95
140
  />
96
141
  <button
97
- className={`send-button ${loading ? "interrupt" : ""}`}
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
  };
@@ -109,49 +159,59 @@ const Chat: React.FC = () => {
109
159
  const [isPopupOpen, setIsPopupOpen] = useState(false);
110
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="chat-container">
165
+ <div className="flex h-full w-full overflow-hidden">
115
166
  {showHistory && <HistoryList onClose={() => toggleHistoryVisible()} formatTime={formatTime} />}
116
- <div className="chat-main">
117
- <div className="chat-header">
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="history-button"
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="graph-button"
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"
133
191
  onClick={() => {
134
- toggleGraphVisible();
135
- setLocalConfig({ showGraph: !showGraph });
192
+ console.log(renderMessages);
136
193
  }}
137
194
  >
138
-
195
+ 打印日志数据
139
196
  </button>
140
197
  <button
141
- className="graph-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"
142
199
  onClick={() => {
143
- console.log(renderMessages);
200
+ toggleGraphVisible();
201
+ setLocalConfig({ showGraph: !showGraph });
144
202
  }}
145
203
  >
146
- 日志数据
204
+ <Network className="w-4 h-4" />
205
+ 节点图
147
206
  </button>
148
207
  <button
149
- className="history-button"
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"
150
209
  onClick={() => {
151
210
  localStorage.setItem("code", "");
152
211
  location.reload();
153
212
  }}
154
213
  >
214
+ <LogOut className="w-4 h-4" />
155
215
  退出登陆
156
216
  </button>
157
217
  </div>
@@ -159,7 +219,12 @@ const Chat: React.FC = () => {
159
219
  <ChatInput />
160
220
  <JsonEditorPopup isOpen={isPopupOpen} initialJson={extraParams} onClose={() => setIsPopupOpen(false)} onSave={setExtraParams} />
161
221
  </div>
162
- {showGraph && <GraphPanel />}
222
+ {(showGraph || showArtifact) && (
223
+ <div className="overflow-hidden flex-1">
224
+ {showGraph && <GraphPanel />}
225
+ {showArtifact && <ArtifactViewer />}
226
+ </div>
227
+ )}
163
228
  </div>
164
229
  );
165
230
  };
@@ -168,7 +233,9 @@ const ChatWrapper: React.FC = () => {
168
233
  return (
169
234
  <ChatProvider>
170
235
  <ExtraParamsProvider>
171
- <Chat />
236
+ <ArtifactsProvider>
237
+ <Chat />
238
+ </ArtifactsProvider>
172
239
  </ExtraParamsProvider>
173
240
  </ChatProvider>
174
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("https://tmpfiles.org/", "https://tmpfiles.org/dl/");
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="file-list">
48
+ <div className="flex gap-2 rounded-lg flex-1">
50
49
  {files.length < MAX_FILES && (
51
- <label className={`file-upload-button ${files.length === 0 ? "empty" : ""}`}>
52
- <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
53
- <path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/>
54
- <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"/>
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} style={{ display: "none" }} />
57
+ <input type="file" accept="image/*" multiple onChange={handleFileChange} className="hidden" />
57
58
  </label>
58
59
  )}
59
- <div className="file-list-content">
60
+ <div className="flex flex-wrap gap-2">
60
61
  {files.map((file, index) => (
61
- <div key={index} className="file-item">
62
- <img src={URL.createObjectURL(file)} alt={file.name} className="file-preview" />
63
- <button className="remove-button" onClick={() => removeFile(index)}>
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>