@langgraph-js/ui 1.2.0 → 1.2.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/LICENSE +201 -201
- package/README.md +6 -6
- package/cli.mjs +36 -36
- package/dist/assets/{index-Du4LMUX2.css → index-BlHtM5cu.css} +1 -1
- package/dist/assets/index-C7SfDwhG.js +192 -0
- package/dist/index.html +14 -14
- package/index.html +22 -22
- package/package.json +8 -9
- package/src/chat/Chat.tsx +151 -164
- package/src/chat/FileUpload/index.ts +105 -105
- package/src/chat/chat.css +403 -403
- package/src/chat/components/FileList.css +128 -128
- package/src/chat/components/FileList.tsx +73 -73
- package/src/chat/components/HistoryList.tsx +192 -192
- package/src/chat/components/JsonEditorPopup.css +80 -80
- package/src/chat/components/JsonEditorPopup.tsx +56 -56
- package/src/chat/components/JsonToMessage/JsonToMessage.css +104 -0
- package/src/chat/components/JsonToMessage/JsonToMessage.tsx +114 -0
- package/src/chat/components/JsonToMessage/JsonToMessageButton.tsx +27 -0
- package/src/chat/components/JsonToMessage/index.tsx +5 -0
- package/src/chat/components/MessageAI.tsx +24 -24
- package/src/chat/components/MessageBox.tsx +39 -0
- package/src/chat/components/MessageHuman.tsx +55 -55
- package/src/chat/components/MessageTool.tsx +46 -46
- package/src/chat/components/UsageMetadata.tsx +40 -40
- package/src/chat/context/ChatContext.tsx +31 -29
- package/src/chat/context/ExtraParamsContext.tsx +41 -41
- package/src/chat/store/index.ts +25 -24
- package/src/chat/tools.ts +33 -33
- package/src/chat/types.ts +16 -16
- package/src/hooks/useLocalStorage.ts +27 -27
- package/src/index.ts +1 -1
- package/src/login/Login.css +93 -93
- package/src/login/Login.tsx +92 -92
- package/test/App.tsx +9 -9
- package/test/main.tsx +5 -5
- package/test/vite-env.d.ts +1 -1
- package/tsconfig.json +21 -21
- package/tsconfig.node.json +9 -9
- package/vite.config.ts +17 -17
- package/dist/assets/index-BHPbGlnP.js +0 -192
package/dist/index.html
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>Vite App</title>
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Vite App</title>
|
|
7
7
|
<style>
|
|
8
8
|
body,
|
|
9
9
|
#root,
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
width: 100%;
|
|
14
14
|
height: 100%;
|
|
15
15
|
}
|
|
16
|
-
</style>
|
|
17
|
-
<script type="module" crossorigin src="/assets/index-
|
|
18
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
-
</head>
|
|
20
|
-
<body>
|
|
21
|
-
<div id="root"></div>
|
|
22
|
-
</body>
|
|
23
|
-
</html>
|
|
16
|
+
</style>
|
|
17
|
+
<script type="module" crossorigin src="/assets/index-C7SfDwhG.js"></script>
|
|
18
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BlHtM5cu.css">
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<div id="root"></div>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|
package/index.html
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>Vite App</title>
|
|
7
|
-
<style>
|
|
8
|
-
body,
|
|
9
|
-
#root,
|
|
10
|
-
html {
|
|
11
|
-
margin: 0;
|
|
12
|
-
padding: 0;
|
|
13
|
-
width: 100%;
|
|
14
|
-
height: 100%;
|
|
15
|
-
}
|
|
16
|
-
</style>
|
|
17
|
-
</head>
|
|
18
|
-
<body>
|
|
19
|
-
<div id="root"></div>
|
|
20
|
-
<script type="module" src="./test/main.tsx"></script>
|
|
21
|
-
</body>
|
|
22
|
-
</html>
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Vite App</title>
|
|
7
|
+
<style>
|
|
8
|
+
body,
|
|
9
|
+
#root,
|
|
10
|
+
html {
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
width: 100%;
|
|
14
|
+
height: 100%;
|
|
15
|
+
}
|
|
16
|
+
</style>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<div id="root"></div>
|
|
20
|
+
<script type="module" src="./test/main.tsx"></script>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@langgraph-js/ui",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -12,21 +12,20 @@
|
|
|
12
12
|
"main": "./dist/react/src/index.js",
|
|
13
13
|
"type": "module",
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@langgraph-js/langgraph-pg": "^1.1.2",
|
|
16
15
|
"@nanostores/react": "^1.0.0",
|
|
17
16
|
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
|
18
|
-
"@vitejs/plugin-react": "^4.4
|
|
17
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
19
18
|
"nanostores": "^1.0.1",
|
|
20
|
-
"react": "^19.
|
|
21
|
-
"react-dom": "^19.
|
|
19
|
+
"react": "^19.0.0",
|
|
20
|
+
"react-dom": "^19.0.0",
|
|
21
|
+
"vite": "^6.2.0",
|
|
22
22
|
"react-markdown": "^10.1.0",
|
|
23
23
|
"remark-gfm": "^4.0.1",
|
|
24
|
-
"
|
|
25
|
-
"@langgraph-js/sdk": "1.1.7"
|
|
24
|
+
"@langgraph-js/sdk": "1.1.9"
|
|
26
25
|
},
|
|
27
26
|
"devDependencies": {
|
|
28
|
-
"@types/react": "^19.
|
|
29
|
-
"@types/react-dom": "^19.
|
|
27
|
+
"@types/react": "^19.0.10",
|
|
28
|
+
"@types/react-dom": "^19.0.4"
|
|
30
29
|
},
|
|
31
30
|
"scripts": {
|
|
32
31
|
"dev": "vite",
|
package/src/chat/Chat.tsx
CHANGED
|
@@ -1,164 +1,151 @@
|
|
|
1
|
-
import React, { useState
|
|
2
|
-
import "./chat.css";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
const ChatWrapper: React.FC = () => {
|
|
155
|
-
return (
|
|
156
|
-
<ChatProvider>
|
|
157
|
-
<ExtraParamsProvider>
|
|
158
|
-
<Chat />
|
|
159
|
-
</ExtraParamsProvider>
|
|
160
|
-
</ChatProvider>
|
|
161
|
-
);
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
export default ChatWrapper;
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import "./chat.css";
|
|
3
|
+
import { MessagesBox } from "./components/MessageBox";
|
|
4
|
+
import HistoryList from "./components/HistoryList";
|
|
5
|
+
import { ChatProvider, useChat } from "./context/ChatContext";
|
|
6
|
+
import { ExtraParamsProvider, useExtraParams } from "./context/ExtraParamsContext";
|
|
7
|
+
import { UsageMetadata } from "./components/UsageMetadata";
|
|
8
|
+
import { formatTime, Message } from "@langgraph-js/sdk";
|
|
9
|
+
import FileList from "./components/FileList";
|
|
10
|
+
import JsonEditorPopup from "./components/JsonEditorPopup";
|
|
11
|
+
import { JsonToMessageButton } from "./components/JsonToMessage";
|
|
12
|
+
|
|
13
|
+
const ChatMessages: React.FC = () => {
|
|
14
|
+
const { renderMessages, loading, inChatError, client, collapsedTools, toggleToolCollapse } = useChat();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="chat-messages">
|
|
18
|
+
<MessagesBox renderMessages={renderMessages} collapsedTools={collapsedTools} toggleToolCollapse={toggleToolCollapse} client={client!} />
|
|
19
|
+
{loading && <div className="loading-indicator">正在思考中...</div>}
|
|
20
|
+
{inChatError && <div className="error-message">{JSON.stringify(inChatError)}</div>}
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const ChatInput: React.FC = () => {
|
|
26
|
+
const { userInput, setUserInput, loading, sendMessage, stopGeneration, currentAgent, setCurrentAgent, client } = useChat();
|
|
27
|
+
const { extraParams } = useExtraParams();
|
|
28
|
+
const [imageUrls, setImageUrls] = useState<string[]>([]);
|
|
29
|
+
|
|
30
|
+
const handleFileUploaded = (url: string) => {
|
|
31
|
+
setImageUrls((prev) => [...prev, url]);
|
|
32
|
+
};
|
|
33
|
+
const _setCurrentAgent = (agent: string) => {
|
|
34
|
+
localStorage.setItem("agent_name", agent);
|
|
35
|
+
setCurrentAgent(agent);
|
|
36
|
+
};
|
|
37
|
+
const sendMultiModalMessage = () => {
|
|
38
|
+
const content: Message[] = [
|
|
39
|
+
{
|
|
40
|
+
type: "human",
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: userInput,
|
|
45
|
+
},
|
|
46
|
+
...imageUrls.map((url) => ({
|
|
47
|
+
type: "image_url" as const,
|
|
48
|
+
image_url: { url },
|
|
49
|
+
})),
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
sendMessage(content, {
|
|
55
|
+
extraParams,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// 清空图片列表
|
|
59
|
+
setImageUrls([]);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleKeyPress = (event: React.KeyboardEvent) => {
|
|
63
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
sendMultiModalMessage();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="chat-input">
|
|
71
|
+
<div className="chat-input-header">
|
|
72
|
+
<FileList onFileUploaded={handleFileUploaded} />
|
|
73
|
+
<UsageMetadata usage_metadata={client?.tokenCounter || {}} />
|
|
74
|
+
<select value={currentAgent} onChange={(e) => _setCurrentAgent(e.target.value)}>
|
|
75
|
+
{client?.availableAssistants.map((i) => {
|
|
76
|
+
return (
|
|
77
|
+
<option value={i.graph_id} key={i.graph_id}>
|
|
78
|
+
{i.name}
|
|
79
|
+
</option>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</select>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="input-container">
|
|
85
|
+
<textarea
|
|
86
|
+
className="input-textarea"
|
|
87
|
+
rows={2}
|
|
88
|
+
value={userInput}
|
|
89
|
+
onChange={(e) => setUserInput(e.target.value)}
|
|
90
|
+
onKeyDown={handleKeyPress}
|
|
91
|
+
placeholder="输入消息..."
|
|
92
|
+
disabled={loading}
|
|
93
|
+
/>
|
|
94
|
+
<button
|
|
95
|
+
className={`send-button ${loading ? "interrupt" : ""}`}
|
|
96
|
+
onClick={() => (loading ? stopGeneration() : sendMultiModalMessage())}
|
|
97
|
+
disabled={!loading && !userInput.trim() && imageUrls.length === 0}
|
|
98
|
+
>
|
|
99
|
+
{loading ? "中断" : "发送"}
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const Chat: React.FC = () => {
|
|
107
|
+
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
|
108
|
+
const { showHistory, toggleHistoryVisible } = useChat();
|
|
109
|
+
const { extraParams, setExtraParams } = useExtraParams();
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="chat-container">
|
|
113
|
+
{showHistory && <HistoryList onClose={() => toggleHistoryVisible()} formatTime={formatTime} />}
|
|
114
|
+
<div className="chat-main">
|
|
115
|
+
<div className="chat-header">
|
|
116
|
+
<JsonToMessageButton></JsonToMessageButton>
|
|
117
|
+
<button onClick={() => setIsPopupOpen(true)} className="edit-params-button">
|
|
118
|
+
编辑参数
|
|
119
|
+
</button>
|
|
120
|
+
<button className="history-button" onClick={() => toggleHistoryVisible()}>
|
|
121
|
+
历史记录
|
|
122
|
+
</button>
|
|
123
|
+
<button
|
|
124
|
+
className="history-button"
|
|
125
|
+
onClick={() => {
|
|
126
|
+
localStorage.setItem("code", "");
|
|
127
|
+
location.reload();
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
退出登陆
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
<ChatMessages />
|
|
134
|
+
<ChatInput />
|
|
135
|
+
<JsonEditorPopup isOpen={isPopupOpen} initialJson={extraParams} onClose={() => setIsPopupOpen(false)} onSave={setExtraParams} />
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const ChatWrapper: React.FC = () => {
|
|
142
|
+
return (
|
|
143
|
+
<ChatProvider>
|
|
144
|
+
<ExtraParamsProvider>
|
|
145
|
+
<Chat />
|
|
146
|
+
</ExtraParamsProvider>
|
|
147
|
+
</ChatProvider>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export default ChatWrapper;
|
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File Upload SDK - Base client for file upload services
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
// Base interfaces
|
|
6
|
-
interface FileUploadClientOptions {
|
|
7
|
-
apiUrl?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
interface FileUploadOptions {
|
|
11
|
-
filename?: string;
|
|
12
|
-
signal?: AbortSignal;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface FileUploadResponse {
|
|
16
|
-
status: string;
|
|
17
|
-
data?: {
|
|
18
|
-
url: string;
|
|
19
|
-
delete_url?: string;
|
|
20
|
-
expires_at?: string;
|
|
21
|
-
size?: number;
|
|
22
|
-
[key: string]: any;
|
|
23
|
-
};
|
|
24
|
-
[key: string]: any;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Abstract base class for file upload clients
|
|
28
|
-
abstract class FileUploadClient {
|
|
29
|
-
protected apiUrl: string;
|
|
30
|
-
|
|
31
|
-
constructor(options: FileUploadClientOptions = {}) {
|
|
32
|
-
this.apiUrl = options.apiUrl || "";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
protected abstract getUploadEndpoint(): string;
|
|
36
|
-
protected abstract processResponse(response: FileUploadResponse): FileUploadResponse;
|
|
37
|
-
|
|
38
|
-
protected createFormData(file: File | Blob | string, filename?: string): FormData {
|
|
39
|
-
const formData = new FormData();
|
|
40
|
-
|
|
41
|
-
if (typeof file === "string") {
|
|
42
|
-
const blob = new Blob([file], { type: "text/plain" });
|
|
43
|
-
formData.append("file", blob, filename || "file.txt");
|
|
44
|
-
} else {
|
|
45
|
-
formData.append("file", file, filename || (file instanceof File ? file.name : "file"));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return formData;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
public async upload(file: File | Blob | string, options: FileUploadOptions = {}): Promise<FileUploadResponse> {
|
|
52
|
-
const formData = this.createFormData(file, options.filename);
|
|
53
|
-
|
|
54
|
-
const fetchOptions: RequestInit = {
|
|
55
|
-
method: "POST",
|
|
56
|
-
body: formData,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
if (options.signal) {
|
|
60
|
-
fetchOptions.signal = options.signal;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const response = await fetch(`${this.apiUrl}${this.getUploadEndpoint()}`, fetchOptions);
|
|
65
|
-
|
|
66
|
-
if (!response.ok) {
|
|
67
|
-
throw new Error(`Upload failed with status: ${response.status}`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const result = (await response.json()) as FileUploadResponse;
|
|
71
|
-
return this.processResponse(result);
|
|
72
|
-
} catch (error) {
|
|
73
|
-
throw new Error(`File upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* TmpFiles SDK - A client for uploading files to tmpfiles.org
|
|
80
|
-
*/
|
|
81
|
-
export class TmpFilesClient extends FileUploadClient {
|
|
82
|
-
constructor(options: FileUploadClientOptions = {}) {
|
|
83
|
-
super({
|
|
84
|
-
apiUrl: options.apiUrl || "https://tmpfiles.org/api/v1"
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
protected getUploadEndpoint(): string {
|
|
89
|
-
return "/upload";
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
protected processResponse(response: FileUploadResponse): FileUploadResponse {
|
|
93
|
-
if (response.data?.url) {
|
|
94
|
-
response.data.url = response.data.url.replace("https://tmpfiles.org/", "https://tmpfiles.org/dl/");
|
|
95
|
-
}
|
|
96
|
-
return response;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Export types for external use
|
|
101
|
-
export type {
|
|
102
|
-
FileUploadClientOptions,
|
|
103
|
-
FileUploadOptions,
|
|
104
|
-
FileUploadResponse
|
|
105
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* File Upload SDK - Base client for file upload services
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Base interfaces
|
|
6
|
+
interface FileUploadClientOptions {
|
|
7
|
+
apiUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface FileUploadOptions {
|
|
11
|
+
filename?: string;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface FileUploadResponse {
|
|
16
|
+
status: string;
|
|
17
|
+
data?: {
|
|
18
|
+
url: string;
|
|
19
|
+
delete_url?: string;
|
|
20
|
+
expires_at?: string;
|
|
21
|
+
size?: number;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
};
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Abstract base class for file upload clients
|
|
28
|
+
abstract class FileUploadClient {
|
|
29
|
+
protected apiUrl: string;
|
|
30
|
+
|
|
31
|
+
constructor(options: FileUploadClientOptions = {}) {
|
|
32
|
+
this.apiUrl = options.apiUrl || "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected abstract getUploadEndpoint(): string;
|
|
36
|
+
protected abstract processResponse(response: FileUploadResponse): FileUploadResponse;
|
|
37
|
+
|
|
38
|
+
protected createFormData(file: File | Blob | string, filename?: string): FormData {
|
|
39
|
+
const formData = new FormData();
|
|
40
|
+
|
|
41
|
+
if (typeof file === "string") {
|
|
42
|
+
const blob = new Blob([file], { type: "text/plain" });
|
|
43
|
+
formData.append("file", blob, filename || "file.txt");
|
|
44
|
+
} else {
|
|
45
|
+
formData.append("file", file, filename || (file instanceof File ? file.name : "file"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return formData;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async upload(file: File | Blob | string, options: FileUploadOptions = {}): Promise<FileUploadResponse> {
|
|
52
|
+
const formData = this.createFormData(file, options.filename);
|
|
53
|
+
|
|
54
|
+
const fetchOptions: RequestInit = {
|
|
55
|
+
method: "POST",
|
|
56
|
+
body: formData,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (options.signal) {
|
|
60
|
+
fetchOptions.signal = options.signal;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(`${this.apiUrl}${this.getUploadEndpoint()}`, fetchOptions);
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`Upload failed with status: ${response.status}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const result = (await response.json()) as FileUploadResponse;
|
|
71
|
+
return this.processResponse(result);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new Error(`File upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* TmpFiles SDK - A client for uploading files to tmpfiles.org
|
|
80
|
+
*/
|
|
81
|
+
export class TmpFilesClient extends FileUploadClient {
|
|
82
|
+
constructor(options: FileUploadClientOptions = {}) {
|
|
83
|
+
super({
|
|
84
|
+
apiUrl: options.apiUrl || "https://tmpfiles.org/api/v1"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected getUploadEndpoint(): string {
|
|
89
|
+
return "/upload";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected processResponse(response: FileUploadResponse): FileUploadResponse {
|
|
93
|
+
if (response.data?.url) {
|
|
94
|
+
response.data.url = response.data.url.replace("https://tmpfiles.org/", "https://tmpfiles.org/dl/");
|
|
95
|
+
}
|
|
96
|
+
return response;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Export types for external use
|
|
101
|
+
export type {
|
|
102
|
+
FileUploadClientOptions,
|
|
103
|
+
FileUploadOptions,
|
|
104
|
+
FileUploadResponse
|
|
105
|
+
};
|