@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.
@@ -0,0 +1,129 @@
1
+ .file-list {
2
+ display: flex;
3
+ gap: 0.5rem;
4
+ border-radius: 8px;
5
+ flex:1;
6
+ }
7
+
8
+ .file-list-header {
9
+ margin-bottom: 1rem;
10
+ }
11
+
12
+ .file-upload-button {
13
+ display: inline-flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ width: 80px;
17
+ height: 80px;
18
+ color: #929292;
19
+ background-color: #ebebeb;
20
+ border-radius: 6px;
21
+ cursor: pointer;
22
+ transition: all 0.2s;
23
+ }
24
+
25
+ .file-upload-button svg {
26
+ width: 32px;
27
+ height: 32px;
28
+ }
29
+
30
+ .file-upload-button.empty {
31
+ width: 32px;
32
+ height: 32px;
33
+ }
34
+
35
+ .file-upload-button.empty svg {
36
+ width: 20px;
37
+ height: 20px;
38
+ }
39
+
40
+ .file-list-content {
41
+ display: flex;
42
+ flex-wrap: wrap;
43
+ gap: 0.5rem;
44
+ }
45
+
46
+ .file-item {
47
+ position: relative;
48
+ width: 80px;
49
+ height: 80px;
50
+ border-radius: 6px;
51
+ overflow: hidden;
52
+ }
53
+ .file-item img{
54
+ border: 1px solid #e5e7eb;
55
+ overflow: hidden;
56
+ }
57
+
58
+ .file-preview {
59
+ width: 100%;
60
+ height: 100%;
61
+ object-fit: cover;
62
+ }
63
+
64
+ .file-info {
65
+ padding: 0.75rem;
66
+ }
67
+
68
+ .file-name {
69
+ display: block;
70
+ font-size: 0.875rem;
71
+ color: #374151;
72
+ margin-bottom: 0.5rem;
73
+ white-space: nowrap;
74
+ overflow: hidden;
75
+ text-overflow: ellipsis;
76
+ }
77
+
78
+ .file-actions {
79
+ display: flex;
80
+ gap: 0.5rem;
81
+ }
82
+
83
+ .upload-button,
84
+ .remove-button {
85
+ flex: 1;
86
+ padding: 0.375rem 0.75rem;
87
+ border: none;
88
+ border-radius: 4px;
89
+ font-size: 0.75rem;
90
+ cursor: pointer;
91
+ transition: all 0.2s;
92
+ }
93
+
94
+ .upload-button {
95
+ color: black;
96
+ }
97
+
98
+ .upload-button:hover {
99
+ background-color: #2563eb;
100
+ }
101
+
102
+ .remove-button {
103
+ position: absolute;
104
+ top: 2px;
105
+ right: 2px;
106
+ width: 20px;
107
+ height: 20px;
108
+ background-color: rgba(0, 0, 0, 0.5);
109
+ color: white;
110
+ border: none;
111
+ border-radius: 50%;
112
+ cursor: pointer;
113
+ font-size: 16px;
114
+ line-height: 1;
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: center;
118
+ transition: background-color 0.2s;
119
+ }
120
+
121
+ .remove-button:hover {
122
+ background-color: rgba(0, 0, 0, 0.7);
123
+ }
124
+
125
+ .upload-button:disabled,
126
+ .remove-button:disabled {
127
+ opacity: 0.5;
128
+ cursor: not-allowed;
129
+ }
@@ -0,0 +1,73 @@
1
+ import React, { useState, useCallback } from "react";
2
+ import { TmpFilesClient } from "../FileUpload";
3
+ import "./FileList.css";
4
+
5
+ interface FileListProps {
6
+ onFileUploaded: (url: string) => void;
7
+ }
8
+
9
+ const FileList: React.FC<FileListProps> = ({ onFileUploaded }) => {
10
+ const [files, setFiles] = useState<File[]>([]);
11
+ const client = new TmpFilesClient();
12
+ const MAX_FILES = 3;
13
+
14
+ const handleFileChange = useCallback(
15
+ async (event: React.ChangeEvent<HTMLInputElement>) => {
16
+ const selectedFiles = Array.from(event.target.files || []);
17
+ const imageFiles = selectedFiles.filter((file) => file.type.startsWith("image/"));
18
+
19
+ // 检查是否超过最大数量限制
20
+ if (files.length + imageFiles.length > MAX_FILES) {
21
+ alert(`最多只能上传${MAX_FILES}张图片`);
22
+ event.target.value = "";
23
+ return;
24
+ }
25
+
26
+ setFiles((prev) => [...prev, ...imageFiles]);
27
+
28
+ for (const file of imageFiles) {
29
+ try {
30
+ const result = await client.upload(file);
31
+ if (result.data?.url) {
32
+ onFileUploaded(result.data.url);
33
+ }
34
+ } catch (error) {
35
+ console.error("Upload failed:", error);
36
+ }
37
+ }
38
+
39
+ event.target.value = "";
40
+ },
41
+ [onFileUploaded, files.length]
42
+ );
43
+
44
+ const removeFile = useCallback((index: number) => {
45
+ setFiles((prev) => prev.filter((_, i) => i !== index));
46
+ }, []);
47
+
48
+ return (
49
+ <div className="file-list">
50
+ {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"/>
55
+ </svg>
56
+ <input type="file" accept="image/*" multiple onChange={handleFileChange} style={{ display: "none" }} />
57
+ </label>
58
+ )}
59
+ <div className="file-list-content">
60
+ {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)}>
64
+ ×
65
+ </button>
66
+ </div>
67
+ ))}
68
+ </div>
69
+ </div>
70
+ );
71
+ };
72
+
73
+ export default FileList;
@@ -1,192 +1,192 @@
1
- import React from "react";
2
- import { useChat } from "../context/ChatContext";
3
-
4
- interface HistoryListProps {
5
- onClose: () => void;
6
- formatTime: (date: Date) => string;
7
- }
8
-
9
- const HistoryList: React.FC<HistoryListProps> = ({ onClose, formatTime }) => {
10
- const { historyList, currentChatId, refreshHistoryList, createNewChat, deleteHistoryChat, toHistoryChat } = useChat();
11
-
12
- return (
13
- <div className="history-list">
14
- <div className="history-header">
15
- <div className="header-left">
16
- <h3>历史记录</h3>
17
- <button className="refresh-button" onClick={refreshHistoryList} title="刷新列表">
18
- 🔁
19
- </button>
20
- </div>
21
- <button className="close-button" onClick={onClose} title="关闭">
22
-
23
- </button>
24
- </div>
25
- <div className="history-content">
26
- <div
27
- className="history-items"
28
- onClick={() => {
29
- createNewChat();
30
- }}
31
- >
32
- <div className="history-item">
33
- <div className="history-title"> New Chat</div>
34
- </div>
35
- </div>
36
- {historyList.length === 0 ? (
37
- <div className="empty-history">暂无历史记录</div>
38
- ) : (
39
- <div className="history-items">
40
- {historyList
41
- .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
42
- .map((thread) => (
43
- <div className={`history-item ${thread.thread_id === currentChatId ? "active" : ""}`} key={thread.thread_id}>
44
- <div className="history-info">
45
- <div className="history-title">{thread?.values?.messages?.[0]?.content as string}</div>
46
- <div className="history-meta">
47
- <span className="history-time">{formatTime(new Date(thread.created_at))}</span>
48
- <span className="history-status">{thread.status}</span>
49
- </div>
50
- </div>
51
- <div className="history-actions">
52
- <button
53
- className="action-button"
54
- onClick={() => {
55
- toHistoryChat(thread);
56
- }}
57
- title="恢复对话"
58
- >
59
-
60
- </button>
61
- <button
62
- className="action-button"
63
- onClick={async () => {
64
- await deleteHistoryChat(thread);
65
- }}
66
- title="删除对话"
67
- >
68
-
69
- </button>
70
- </div>
71
- </div>
72
- ))}
73
- </div>
74
- )}
75
- </div>
76
- <style>{`
77
- .history-list {
78
- background: #fff;
79
- border-radius: 8px;
80
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
81
- height: 100%;
82
- display: flex;
83
- flex-direction: column;
84
- }
85
-
86
- .history-header {
87
- padding: 16px;
88
- border-bottom: 1px solid #eee;
89
- display: flex;
90
- justify-content: space-between;
91
- align-items: center;
92
- }
93
-
94
- .header-left {
95
- display: flex;
96
- align-items: center;
97
- gap: 12px;
98
- }
99
-
100
- .history-header h3 {
101
- margin: 0;
102
- font-size: 18px;
103
- color: #333;
104
- }
105
-
106
- .history-content {
107
- flex: 1;
108
- overflow-y: auto;
109
- padding: 16px;
110
- }
111
-
112
- .history-items {
113
- display: flex;
114
- flex-direction: column;
115
- gap: 12px;
116
- }
117
-
118
- .history-item {
119
- display: flex;
120
- justify-content: space-between;
121
- align-items: center;
122
- padding: 12px;
123
- border-radius: 6px;
124
- background: #f8f9fa;
125
- transition: all 0.2s ease;
126
- }
127
-
128
- .history-item:hover {
129
- background: #f0f2f5;
130
- }
131
-
132
- .history-item.active {
133
- background: #e6f7ff;
134
- border: 1px solid #91d5ff;
135
- }
136
-
137
- .history-info {
138
- flex: 1;
139
- min-width: 0;
140
- }
141
-
142
- .history-title {
143
- font-size: 14px;
144
- color: #333;
145
- margin-bottom: 4px;
146
- white-space: nowrap;
147
- overflow: hidden;
148
- text-overflow: ellipsis;
149
- }
150
-
151
- .history-meta {
152
- display: flex;
153
- gap: 12px;
154
- color: #666;
155
- font-size: 12px;
156
- }
157
-
158
- .history-actions {
159
- display: flex;
160
- gap: 8px;
161
- margin-left: 12px;
162
- }
163
-
164
- .action-button, .close-button, .refresh-button {
165
- background: none;
166
- border: none;
167
- cursor: pointer;
168
- padding: 6px;
169
- font-size: 16px;
170
- border-radius: 4px;
171
- transition: all 0.2s ease;
172
- display: flex;
173
- align-items: center;
174
- justify-content: center;
175
- }
176
-
177
- .action-button:hover, .close-button:hover, .refresh-button:hover {
178
- background: rgba(0, 0, 0, 0.05);
179
- transform: scale(1.1);
180
- }
181
-
182
- .empty-history {
183
- text-align: center;
184
- color: #999;
185
- padding: 32px 0;
186
- }
187
- `}</style>
188
- </div>
189
- );
190
- };
191
-
192
- export default HistoryList;
1
+ import React from "react";
2
+ import { useChat } from "../context/ChatContext";
3
+ import { getHistoryContent } from "@langgraph-js/sdk";
4
+
5
+ interface HistoryListProps {
6
+ onClose: () => void;
7
+ formatTime: (date: Date) => string;
8
+ }
9
+
10
+ const HistoryList: React.FC<HistoryListProps> = ({ onClose, formatTime }) => {
11
+ const { historyList, currentChatId, refreshHistoryList, createNewChat, deleteHistoryChat, toHistoryChat } = useChat();
12
+ return (
13
+ <div className="history-list">
14
+ <div className="history-header">
15
+ <div className="header-left">
16
+ <h3>历史记录</h3>
17
+ <button className="refresh-button" onClick={refreshHistoryList} title="刷新列表">
18
+ 🔁
19
+ </button>
20
+ </div>
21
+ <button className="close-button" onClick={onClose} title="关闭">
22
+
23
+ </button>
24
+ </div>
25
+ <div className="history-content">
26
+ <div
27
+ className="history-items"
28
+ onClick={() => {
29
+ createNewChat();
30
+ }}
31
+ >
32
+ <div className="history-item">
33
+ <div className="history-title"> New Chat</div>
34
+ </div>
35
+ </div>
36
+ {historyList.length === 0 ? (
37
+ <div className="empty-history">暂无历史记录</div>
38
+ ) : (
39
+ <div className="history-items">
40
+ {historyList
41
+ .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
42
+ .map((thread) => (
43
+ <div className={`history-item ${thread.thread_id === currentChatId ? "active" : ""}`} key={thread.thread_id}>
44
+ <div className="history-info">
45
+ <div className="history-title">{getHistoryContent(thread)}</div>
46
+ <div className="history-meta">
47
+ <span className="history-time">{formatTime(new Date(thread.created_at))}</span>
48
+ <span className="history-status">{thread.status}</span>
49
+ </div>
50
+ </div>
51
+ <div className="history-actions">
52
+ <button
53
+ className="action-button"
54
+ onClick={() => {
55
+ toHistoryChat(thread);
56
+ }}
57
+ title="恢复对话"
58
+ >
59
+
60
+ </button>
61
+ <button
62
+ className="action-button"
63
+ onClick={async () => {
64
+ await deleteHistoryChat(thread);
65
+ }}
66
+ title="删除对话"
67
+ >
68
+
69
+ </button>
70
+ </div>
71
+ </div>
72
+ ))}
73
+ </div>
74
+ )}
75
+ </div>
76
+ <style>{`
77
+ .history-list {
78
+ background: #fff;
79
+ border-radius: 8px;
80
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
81
+ height: 100%;
82
+ display: flex;
83
+ flex-direction: column;
84
+ }
85
+
86
+ .history-header {
87
+ padding: 16px;
88
+ border-bottom: 1px solid #eee;
89
+ display: flex;
90
+ justify-content: space-between;
91
+ align-items: center;
92
+ }
93
+
94
+ .header-left {
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 12px;
98
+ }
99
+
100
+ .history-header h3 {
101
+ margin: 0;
102
+ font-size: 18px;
103
+ color: #333;
104
+ }
105
+
106
+ .history-content {
107
+ flex: 1;
108
+ overflow-y: auto;
109
+ padding: 16px;
110
+ }
111
+
112
+ .history-items {
113
+ display: flex;
114
+ flex-direction: column;
115
+ gap: 12px;
116
+ }
117
+
118
+ .history-item {
119
+ display: flex;
120
+ justify-content: space-between;
121
+ align-items: center;
122
+ padding: 12px;
123
+ border-radius: 6px;
124
+ background: #f8f9fa;
125
+ transition: all 0.2s ease;
126
+ }
127
+
128
+ .history-item:hover {
129
+ background: #f0f2f5;
130
+ }
131
+
132
+ .history-item.active {
133
+ background: #e6f7ff;
134
+ border: 1px solid #91d5ff;
135
+ }
136
+
137
+ .history-info {
138
+ flex: 1;
139
+ min-width: 0;
140
+ }
141
+
142
+ .history-title {
143
+ font-size: 14px;
144
+ color: #333;
145
+ margin-bottom: 4px;
146
+ white-space: nowrap;
147
+ overflow: hidden;
148
+ text-overflow: ellipsis;
149
+ }
150
+
151
+ .history-meta {
152
+ display: flex;
153
+ gap: 12px;
154
+ color: #666;
155
+ font-size: 12px;
156
+ }
157
+
158
+ .history-actions {
159
+ display: flex;
160
+ gap: 8px;
161
+ margin-left: 12px;
162
+ }
163
+
164
+ .action-button, .close-button, .refresh-button {
165
+ background: none;
166
+ border: none;
167
+ cursor: pointer;
168
+ padding: 6px;
169
+ font-size: 16px;
170
+ border-radius: 4px;
171
+ transition: all 0.2s ease;
172
+ display: flex;
173
+ align-items: center;
174
+ justify-content: center;
175
+ }
176
+
177
+ .action-button:hover, .close-button:hover, .refresh-button:hover {
178
+ background: rgba(0, 0, 0, 0.05);
179
+ transform: scale(1.1);
180
+ }
181
+
182
+ .empty-history {
183
+ text-align: center;
184
+ color: #999;
185
+ padding: 32px 0;
186
+ }
187
+ `}</style>
188
+ </div>
189
+ );
190
+ };
191
+
192
+ export default HistoryList;
@@ -0,0 +1,81 @@
1
+ .json-editor-popup-overlay {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ right: 0;
6
+ bottom: 0;
7
+ background-color: rgba(0, 0, 0, 0.5);
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ z-index: 1000;
12
+ }
13
+
14
+ .json-editor-popup-content {
15
+ background-color: white;
16
+ padding: 20px;
17
+ border-radius: 8px;
18
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
19
+ width: 80%;
20
+ max-width: 600px;
21
+ display: flex;
22
+ flex-direction: column;
23
+ }
24
+
25
+ .json-editor-popup-content h2 {
26
+ margin-top: 0;
27
+ margin-bottom: 15px;
28
+ font-size: 1.5rem;
29
+ color: #333;
30
+ }
31
+
32
+ .json-editor-popup-content textarea {
33
+ width: 100%;
34
+ padding: 10px;
35
+ border: 1px solid #ccc;
36
+ border-radius: 4px;
37
+ font-family: monospace;
38
+ font-size: 0.9rem;
39
+ resize: vertical;
40
+ box-sizing: border-box;
41
+ }
42
+
43
+ .json-editor-popup-content .error-message {
44
+ color: red;
45
+ font-size: 0.8rem;
46
+ margin-top: 5px;
47
+ margin-bottom: 10px;
48
+ }
49
+
50
+ .popup-actions {
51
+ display: flex;
52
+ justify-content: flex-end;
53
+ margin-top: 15px;
54
+ gap: 10px;
55
+ }
56
+
57
+ .popup-actions button {
58
+ padding: 8px 16px;
59
+ border: none;
60
+ border-radius: 4px;
61
+ cursor: pointer;
62
+ font-size: 0.9rem;
63
+ }
64
+
65
+ .popup-actions .save-button {
66
+ background-color: #3b82f6;
67
+ color: white;
68
+ }
69
+
70
+ .popup-actions .save-button:hover {
71
+ background-color: #2563eb;
72
+ }
73
+
74
+ .popup-actions .cancel-button {
75
+ background-color: #e5e7eb;
76
+ color: #374151;
77
+ }
78
+
79
+ .popup-actions .cancel-button:hover {
80
+ background-color: #d1d5db;
81
+ }