@langgraph-js/ui 1.1.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.
@@ -1,116 +1,129 @@
1
- .file-list {
2
- padding: 0.5rem;
3
- background-color: #f9fafb;
4
- border-radius: 8px;
5
- margin-bottom: 1rem;
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
- background-color: #3b82f6;
19
- color: white;
20
- border-radius: 6px;
21
- cursor: pointer;
22
- font-size: 2rem;
23
- transition: background-color 0.2s;
24
- }
25
-
26
- .file-upload-button:hover {
27
- background-color: #2563eb;
28
- }
29
-
30
- .file-list-content {
31
- display: flex;
32
- flex-wrap: wrap;
33
- gap: 0.5rem;
34
- }
35
-
36
- .file-item {
37
- position: relative;
38
- width: 80px;
39
- height: 80px;
40
- border-radius: 6px;
41
- overflow: hidden;
42
- }
43
-
44
- .file-preview {
45
- width: 100%;
46
- height: 100%;
47
- object-fit: cover;
48
- }
49
-
50
- .file-info {
51
- padding: 0.75rem;
52
- }
53
-
54
- .file-name {
55
- display: block;
56
- font-size: 0.875rem;
57
- color: #374151;
58
- margin-bottom: 0.5rem;
59
- white-space: nowrap;
60
- overflow: hidden;
61
- text-overflow: ellipsis;
62
- }
63
-
64
- .file-actions {
65
- display: flex;
66
- gap: 0.5rem;
67
- }
68
-
69
- .upload-button,
70
- .remove-button {
71
- flex: 1;
72
- padding: 0.375rem 0.75rem;
73
- border: none;
74
- border-radius: 4px;
75
- font-size: 0.75rem;
76
- cursor: pointer;
77
- transition: all 0.2s;
78
- }
79
-
80
- .upload-button {
81
- background-color: #3b82f6;
82
- color: white;
83
- }
84
-
85
- .upload-button:hover {
86
- background-color: #2563eb;
87
- }
88
-
89
- .remove-button {
90
- position: absolute;
91
- top: 2px;
92
- right: 2px;
93
- width: 20px;
94
- height: 20px;
95
- background-color: rgba(0, 0, 0, 0.5);
96
- color: white;
97
- border: none;
98
- border-radius: 50%;
99
- cursor: pointer;
100
- font-size: 16px;
101
- line-height: 1;
102
- display: flex;
103
- align-items: center;
104
- justify-content: center;
105
- transition: background-color 0.2s;
106
- }
107
-
108
- .remove-button:hover {
109
- background-color: rgba(0, 0, 0, 0.7);
110
- }
111
-
112
- .upload-button:disabled,
113
- .remove-button:disabled {
114
- opacity: 0.5;
115
- cursor: not-allowed;
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;
116
129
  }
@@ -1,70 +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
-
13
- const handleFileChange = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
14
- const selectedFiles = Array.from(event.target.files || []);
15
- const imageFiles = selectedFiles.filter(file => file.type.startsWith('image/'));
16
- setFiles(prev => [...prev, ...imageFiles]);
17
-
18
- for (const file of imageFiles) {
19
- try {
20
- const result = await client.upload(file);
21
- if (result.data?.url) {
22
- onFileUploaded(result.data.url);
23
- }
24
- } catch (error) {
25
- console.error('Upload failed:', error);
26
- }
27
- }
28
-
29
- // 清空 input 值,允许重复选择相同文件
30
- event.target.value = '';
31
- }, [onFileUploaded]);
32
-
33
- const removeFile = useCallback((index: number) => {
34
- setFiles(prev => prev.filter((_, i) => i !== index));
35
- }, []);
36
-
37
- return (
38
- <div className="file-list">
39
- <div className="file-list-content">
40
- <label className="file-upload-button">
41
- <span>+</span>
42
- <input
43
- type="file"
44
- accept="image/*"
45
- multiple
46
- onChange={handleFileChange}
47
- style={{ display: 'none' }}
48
- />
49
- </label>
50
- {files.map((file, index) => (
51
- <div key={index} className="file-item">
52
- <img
53
- src={URL.createObjectURL(file)}
54
- alt={file.name}
55
- className="file-preview"
56
- />
57
- <button
58
- className="remove-button"
59
- onClick={() => removeFile(index)}
60
- >
61
- ×
62
- </button>
63
- </div>
64
- ))}
65
- </div>
66
- </div>
67
- );
68
- };
69
-
70
- export default FileList;
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;