@langgraph-js/ui 1.0.0 → 1.2.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/LICENSE +201 -201
- package/README.md +6 -6
- package/cli.mjs +36 -36
- package/dist/assets/index-BHPbGlnP.js +192 -0
- package/dist/assets/index-Du4LMUX2.css +1 -0
- package/dist/index.html +14 -14
- package/index.html +22 -22
- package/package.json +11 -8
- package/src/chat/Chat.tsx +164 -112
- package/src/chat/FileUpload/index.ts +105 -0
- package/src/chat/chat.css +403 -361
- package/src/chat/components/FileList.css +129 -0
- package/src/chat/components/FileList.tsx +73 -0
- package/src/chat/components/HistoryList.tsx +192 -192
- package/src/chat/components/JsonEditorPopup.css +81 -0
- package/src/chat/components/JsonEditorPopup.tsx +57 -0
- package/src/chat/components/MessageAI.tsx +24 -20
- package/src/chat/components/MessageHuman.tsx +55 -15
- package/src/chat/components/MessageTool.tsx +46 -47
- package/src/chat/components/UsageMetadata.tsx +40 -34
- package/src/chat/context/ChatContext.tsx +29 -29
- package/src/chat/context/ExtraParamsContext.tsx +42 -0
- package/src/chat/store/index.ts +24 -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-B6G4BLix.js +0 -164
- package/dist/assets/index-BAcH-2-3.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import"https://unpkg.com/github-markdown-css/github-markdown.css";.chat-container{display:flex;height:100vh;width:100%;position:relative}.chat-sidebar{background-color:#f5f5f5;border-right:1px solid #e0e0e0;display:flex;flex-direction:column}.history-header{padding:16px;border-bottom:1px solid #e0e0e0;display:flex;justify-content:space-between;align-items:center}.history-header h3{margin:0;font-size:16px;color:#333}.close-button{background:none;border:none;font-size:20px;cursor:pointer;color:#666}.history-list{flex:none;width:280px;overflow-y:auto;border-right:1px solid #e0e0e0}.history-item{padding:12px;margin-bottom:8px;background-color:#fff;cursor:pointer;transition:background-color .2s}.history-item:hover{background-color:#e8e8e8}.history-item.active{background-color:#e3f2fd;border-left:3px solid #2196f3}.history-title{font-size:14px;color:#333;margin-bottom:4px}.history-time{font-size:12px;color:#666}.chat-main{overflow:hidden;flex:1;display:flex;flex-direction:column}.chat-header{padding:16px;border-bottom:1px solid #e0e0e0;display:flex;justify-content:flex-end;gap:.5rem}.history-button{padding:8px 16px;background-color:#f0f0f0;border:1px solid #e0e0e0;border-radius:4px;cursor:pointer;font-size:14px;color:#333}.history-button:hover{background-color:#e0e0e0}.chat-messages{flex:1;overflow-y:auto;padding:1rem;display:flex;flex-direction:column;gap:1rem}.message{display:flex;max-width:80%}.message.human{margin-left:auto}.message.ai{margin-right:auto}.message-content{padding:.75rem 1rem;border-radius:8px;border:1px solid #e5e7eb;display:flex;flex-direction:column;gap:.5rem;max-width:100%}.message-text{word-break:break-word;line-height:1.5}.message-image{margin:.5rem 0}.message-image img{max-width:100%;border-radius:4px;box-shadow:0 1px 3px #0000001a}.message-audio{margin:.5rem 0}.message-audio audio{width:100%;max-width:300px}.message-meta{display:flex;align-items:center;gap:.75rem;font-size:.75rem;color:#6b7280;margin-top:.25rem}.message-time{font-family:monospace}.token-info{display:flex;gap:.5rem;align-items:center}.token-item{display:flex;align-items:center;gap:.25rem;background-color:#fff;padding:.125rem .375rem;border-radius:4px;font-family:monospace}.token-emoji{font-size:.875rem}.message.human .message-content{background-color:#3b82f6;color:#fff;border-color:#3b82f6}.message.human .message-meta{color:#fffc}.message.ai .message-content{color:#1f2937}.message.tool{width:100%;max-width:100%}.tool-message{width:100%;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden}.tool-header{display:flex;align-items:center;justify-content:space-between;padding:.5rem 1rem;background-color:#f9fafb;border-bottom:1px solid #e5e7eb;cursor:pointer}.tool-header:hover{background-color:#f3f4f6}.tool-title{font-weight:500;color:#374151}.tool-content{padding:1rem}.tool-input{background-color:#f9fafb;padding:.75rem;border-radius:4px;margin-bottom:.5rem;font-family:monospace;white-space:pre-wrap;word-break:break-all}.tool-output{background-color:#fff;padding:.75rem;border-radius:4px;font-family:monospace;border:1px solid #e5e7eb;margin-bottom:.5rem}.chat-input{border-top:1px solid #e5e7eb;padding:.5rem 1rem 1rem;background-color:#fff}.chat-input-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem}.input-container{display:flex;gap:.5rem}.input-textarea{flex:1;padding:.75rem;border:1px solid #e5e7eb;border-radius:8px;resize:none;font-size:.875rem;line-height:1.25rem}.input-textarea:focus{outline:none;border-color:#3b82f6}.send-button{padding:.5rem 1rem;background-color:#3b82f6;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:500;transition:all .2s}.send-button:hover{background-color:#2563eb}.send-button:disabled{background-color:#93c5fd;cursor:not-allowed}.send-button.interrupt{background-color:#ef4444}.send-button.interrupt:hover{background-color:#dc2626}.collapsed .tool-content{display:none}.expand-icon{transition:transform .2s}.collapsed .expand-icon{transform:rotate(-90deg)}.loading-indicator{padding:12px 16px;margin:8px 0;background-color:#f5f5f5;border-radius:8px;color:#666;font-size:14px;text-align:center;animation:pulse 1.5s infinite;display:flex;align-items:center;justify-content:center;gap:12px}.interrupt-button{padding:4px 12px;background-color:#ef4444;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px;transition:background-color .2s}.interrupt-button:hover{background-color:#dc2626}@keyframes pulse{0%{opacity:.6}50%{opacity:1}to{opacity:.6}}.error-message{padding:12px 16px;margin:8px 0;background-color:#fee2e2;border:1px solid #fecaca;border-radius:8px;color:#dc2626;font-size:14px;text-align:center}.markdown-body p{text-align:left}.edit-params-button{padding:6px 12px;background-color:#f0f0f0;border:1px solid #e0e0e0;border-radius:4px;cursor:pointer;font-size:.8rem;color:#333;white-space:nowrap}.edit-params-button:hover{background-color:#e0e0e0}.file-list{display:flex;gap:.5rem;border-radius:8px;flex:1}.file-list-header{margin-bottom:1rem}.file-upload-button{display:inline-flex;align-items:center;justify-content:center;width:80px;height:80px;color:#929292;background-color:#ebebeb;border-radius:6px;cursor:pointer;transition:all .2s}.file-upload-button svg{width:32px;height:32px}.file-upload-button.empty{width:32px;height:32px}.file-upload-button.empty svg{width:20px;height:20px}.file-list-content{display:flex;flex-wrap:wrap;gap:.5rem}.file-item{position:relative;width:80px;height:80px;border-radius:6px;overflow:hidden}.file-item img{border:1px solid #e5e7eb;overflow:hidden}.file-preview{width:100%;height:100%;object-fit:cover}.file-info{padding:.75rem}.file-name{display:block;font-size:.875rem;color:#374151;margin-bottom:.5rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.file-actions{display:flex;gap:.5rem}.upload-button,.remove-button{flex:1;padding:.375rem .75rem;border:none;border-radius:4px;font-size:.75rem;cursor:pointer;transition:all .2s}.upload-button{color:#000}.upload-button:hover{background-color:#2563eb}.remove-button{position:absolute;top:2px;right:2px;width:20px;height:20px;background-color:#00000080;color:#fff;border:none;border-radius:50%;cursor:pointer;font-size:16px;line-height:1;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.remove-button:hover{background-color:#000000b3}.upload-button:disabled,.remove-button:disabled{opacity:.5;cursor:not-allowed}.json-editor-popup-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background-color:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.json-editor-popup-content{background-color:#fff;padding:20px;border-radius:8px;box-shadow:0 2px 10px #0000001a;width:80%;max-width:600px;display:flex;flex-direction:column}.json-editor-popup-content h2{margin-top:0;margin-bottom:15px;font-size:1.5rem;color:#333}.json-editor-popup-content textarea{width:100%;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:.9rem;resize:vertical;box-sizing:border-box}.json-editor-popup-content .error-message{color:red;font-size:.8rem;margin-top:5px;margin-bottom:10px}.popup-actions{display:flex;justify-content:flex-end;margin-top:15px;gap:10px}.popup-actions button{padding:8px 16px;border:none;border-radius:4px;cursor:pointer;font-size:.9rem}.popup-actions .save-button{background-color:#3b82f6;color:#fff}.popup-actions .save-button:hover{background-color:#2563eb}.popup-actions .cancel-button{background-color:#e5e7eb;color:#374151}.popup-actions .cancel-button:hover{background-color:#d1d5db}.login-container{max-width:600px;margin:2rem auto;padding:2rem;background:#fff;border-radius:8px;box-shadow:0 2px 4px #0000001a}.header-group{display:flex;gap:1rem;align-items:flex-start;padding:1rem;background:#f8f9fa;border-radius:4px;position:relative}.form-group{flex:1}.form-group label{display:block;margin-bottom:.5rem;color:#333;font-weight:500}.form-group input{width:100%;padding:.5rem;border:1px solid #ddd;border-radius:4px;font-size:1rem}.form-group input:focus{outline:none;border-color:#007bff;box-shadow:0 0 0 2px #007bff40}.button-group{display:flex;gap:1rem;margin-top:1rem}button{padding:.5rem 1rem;border:none;border-radius:4px;font-size:1rem;cursor:pointer;transition:background-color .2s}button[type=submit]{background-color:#007bff;color:#fff}button[type=submit]:hover{background-color:#0056b3}button[type=button]{background-color:#6c757d;color:#fff}button[type=button]:hover{background-color:#5a6268}.remove-header{background-color:#dc3545;color:#fff;padding:.25rem .5rem;font-size:.875rem}.remove-header:hover{background-color:#c82333}p{margin-bottom:1.5rem;color:#666;text-align:center}
|
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-BHPbGlnP.js"></script>
|
|
18
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Du4LMUX2.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.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -12,18 +12,21 @@
|
|
|
12
12
|
"main": "./dist/react/src/index.js",
|
|
13
13
|
"type": "module",
|
|
14
14
|
"dependencies": {
|
|
15
|
+
"@langgraph-js/langgraph-pg": "^1.1.2",
|
|
15
16
|
"@nanostores/react": "^1.0.0",
|
|
16
17
|
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
|
17
|
-
"@vitejs/plugin-react": "^4.
|
|
18
|
+
"@vitejs/plugin-react": "^4.4.1",
|
|
18
19
|
"nanostores": "^1.0.1",
|
|
19
|
-
"react": "^19.
|
|
20
|
-
"react-dom": "^19.
|
|
21
|
-
"
|
|
22
|
-
"
|
|
20
|
+
"react": "^19.1.0",
|
|
21
|
+
"react-dom": "^19.1.0",
|
|
22
|
+
"react-markdown": "^10.1.0",
|
|
23
|
+
"remark-gfm": "^4.0.1",
|
|
24
|
+
"vite": "^6.3.5",
|
|
25
|
+
"@langgraph-js/sdk": "1.1.7"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {
|
|
25
|
-
"@types/react": "^19.
|
|
26
|
-
"@types/react-dom": "^19.
|
|
28
|
+
"@types/react": "^19.1.3",
|
|
29
|
+
"@types/react-dom": "^19.1.3"
|
|
27
30
|
},
|
|
28
31
|
"scripts": {
|
|
29
32
|
"dev": "vite",
|
package/src/chat/Chat.tsx
CHANGED
|
@@ -1,112 +1,164 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import "./chat.css";
|
|
3
|
-
import MessageHuman from "./components/MessageHuman";
|
|
4
|
-
import MessageAI from "./components/MessageAI";
|
|
5
|
-
import MessageTool from "./components/MessageTool";
|
|
6
|
-
import HistoryList from "./components/HistoryList";
|
|
7
|
-
import { ChatProvider, useChat } from "./context/ChatContext";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
const
|
|
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
|
-
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import "./chat.css";
|
|
3
|
+
import MessageHuman from "./components/MessageHuman";
|
|
4
|
+
import MessageAI from "./components/MessageAI";
|
|
5
|
+
import MessageTool from "./components/MessageTool";
|
|
6
|
+
import HistoryList from "./components/HistoryList";
|
|
7
|
+
import { ChatProvider, useChat } from "./context/ChatContext";
|
|
8
|
+
import { ExtraParamsProvider, useExtraParams } from "./context/ExtraParamsContext";
|
|
9
|
+
import { UsageMetadata } from "./components/UsageMetadata";
|
|
10
|
+
import { formatTime, formatTokens, getMessageContent, Message } from "@langgraph-js/sdk";
|
|
11
|
+
import FileList from "./components/FileList";
|
|
12
|
+
import JsonEditorPopup from "./components/JsonEditorPopup";
|
|
13
|
+
|
|
14
|
+
const ChatMessages: React.FC = () => {
|
|
15
|
+
const { renderMessages, loading, inChatError, client, collapsedTools, toggleToolCollapse } = useChat();
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="chat-messages">
|
|
19
|
+
{renderMessages.map((message) =>
|
|
20
|
+
message.type === "human" ? (
|
|
21
|
+
<MessageHuman content={message.content} key={message.unique_id} />
|
|
22
|
+
) : message.type === "tool" ? (
|
|
23
|
+
<MessageTool
|
|
24
|
+
key={message.unique_id}
|
|
25
|
+
message={message}
|
|
26
|
+
client={client!}
|
|
27
|
+
getMessageContent={getMessageContent}
|
|
28
|
+
formatTokens={formatTokens}
|
|
29
|
+
isCollapsed={collapsedTools.includes(message.id!)}
|
|
30
|
+
onToggleCollapse={() => toggleToolCollapse(message.id!)}
|
|
31
|
+
/>
|
|
32
|
+
) : (
|
|
33
|
+
<MessageAI key={message.unique_id} message={message} />
|
|
34
|
+
)
|
|
35
|
+
)}
|
|
36
|
+
{loading && <div className="loading-indicator">正在思考中...</div>}
|
|
37
|
+
{inChatError && <div className="error-message">{inChatError}</div>}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const ChatInput: React.FC = () => {
|
|
43
|
+
const { userInput, setUserInput, loading, sendMessage, stopGeneration, currentAgent, setCurrentAgent, client } = useChat();
|
|
44
|
+
const { extraParams } = useExtraParams();
|
|
45
|
+
const [imageUrls, setImageUrls] = useState<string[]>([]);
|
|
46
|
+
|
|
47
|
+
const handleFileUploaded = (url: string) => {
|
|
48
|
+
setImageUrls((prev) => [...prev, url]);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const sendMultiModalMessage = () => {
|
|
52
|
+
const content: Message[] = [
|
|
53
|
+
{
|
|
54
|
+
type: "human",
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: userInput,
|
|
59
|
+
},
|
|
60
|
+
...imageUrls.map((url) => ({
|
|
61
|
+
type: "image_url" as const,
|
|
62
|
+
image_url: { url },
|
|
63
|
+
})),
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
sendMessage(content, {
|
|
69
|
+
extraParams,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 清空图片列表
|
|
73
|
+
setImageUrls([]);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleKeyPress = (event: React.KeyboardEvent) => {
|
|
77
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
sendMultiModalMessage();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="chat-input">
|
|
85
|
+
<div className="chat-input-header">
|
|
86
|
+
<FileList onFileUploaded={handleFileUploaded} />
|
|
87
|
+
<UsageMetadata usage_metadata={client?.tokenCounter || {}} />
|
|
88
|
+
<select value={currentAgent} onChange={(e) => setCurrentAgent(e.target.value)}>
|
|
89
|
+
{client?.availableAssistants.map((i) => {
|
|
90
|
+
return (
|
|
91
|
+
<option value={i.graph_id} key={i.graph_id}>
|
|
92
|
+
{i.name}
|
|
93
|
+
</option>
|
|
94
|
+
);
|
|
95
|
+
})}
|
|
96
|
+
</select>
|
|
97
|
+
</div>
|
|
98
|
+
<div className="input-container">
|
|
99
|
+
<textarea
|
|
100
|
+
className="input-textarea"
|
|
101
|
+
rows={2}
|
|
102
|
+
value={userInput}
|
|
103
|
+
onChange={(e) => setUserInput(e.target.value)}
|
|
104
|
+
onKeyDown={handleKeyPress}
|
|
105
|
+
placeholder="输入消息..."
|
|
106
|
+
disabled={loading}
|
|
107
|
+
/>
|
|
108
|
+
<button
|
|
109
|
+
className={`send-button ${loading ? "interrupt" : ""}`}
|
|
110
|
+
onClick={() => (loading ? stopGeneration() : sendMultiModalMessage())}
|
|
111
|
+
disabled={!loading && !userInput.trim() && imageUrls.length === 0}
|
|
112
|
+
>
|
|
113
|
+
{loading ? "中断" : "发送"}
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const Chat: React.FC = () => {
|
|
121
|
+
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
|
122
|
+
const { showHistory, toggleHistoryVisible } = useChat();
|
|
123
|
+
const { extraParams, setExtraParams } = useExtraParams();
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className="chat-container">
|
|
127
|
+
{showHistory && <HistoryList onClose={() => toggleHistoryVisible()} formatTime={formatTime} />}
|
|
128
|
+
<div className="chat-main">
|
|
129
|
+
<div className="chat-header">
|
|
130
|
+
<button onClick={() => setIsPopupOpen(true)} className="edit-params-button">
|
|
131
|
+
编辑参数
|
|
132
|
+
</button>
|
|
133
|
+
<button className="history-button" onClick={() => toggleHistoryVisible()}>
|
|
134
|
+
历史记录
|
|
135
|
+
</button>
|
|
136
|
+
<button
|
|
137
|
+
className="history-button"
|
|
138
|
+
onClick={() => {
|
|
139
|
+
localStorage.setItem("code", "");
|
|
140
|
+
location.reload();
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
退出登陆
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
<ChatMessages />
|
|
147
|
+
<ChatInput />
|
|
148
|
+
<JsonEditorPopup isOpen={isPopupOpen} initialJson={extraParams} onClose={() => setIsPopupOpen(false)} onSave={setExtraParams} />
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
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;
|
|
@@ -0,0 +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
|
+
};
|