SarahXia0405 commited on
Commit
384b968
·
verified ·
1 Parent(s): 0226509

Update web/src/components/FileUploadArea.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/FileUploadArea.tsx +89 -62
web/src/components/FileUploadArea.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import React, { useRef, useState } from 'react';
2
  import { Button } from './ui/button';
3
- import { Upload, File, X, FileText, FileSpreadsheet, Presentation } from 'lucide-react';
4
  import { Card } from './ui/card';
5
  import { Badge } from './ui/badge';
6
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
@@ -12,6 +12,11 @@ interface FileUploadAreaProps {
12
  onFileUpload: (files: File[]) => void;
13
  onRemoveFile: (index: number) => void;
14
  onFileTypeChange: (index: number, type: FileType) => void;
 
 
 
 
 
15
  disabled?: boolean;
16
  }
17
 
@@ -25,6 +30,8 @@ export function FileUploadArea({
25
  onFileUpload,
26
  onRemoveFile,
27
  onFileTypeChange,
 
 
28
  disabled = false,
29
  }: FileUploadAreaProps) {
30
  const [isDragging, setIsDragging] = useState(false);
@@ -51,7 +58,7 @@ export function FileUploadArea({
51
  );
52
 
53
  if (files.length > 0) {
54
- setPendingFiles(files.map(file => ({ file, type: 'other' as FileType })));
55
  setShowTypeDialog(true);
56
  }
57
  };
@@ -59,21 +66,24 @@ export function FileUploadArea({
59
  const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
60
  const files = Array.from(e.target.files || []);
61
  if (files.length > 0) {
62
- setPendingFiles(files.map(file => ({ file, type: 'other' as FileType })));
63
  setShowTypeDialog(true);
64
  }
65
  e.target.value = '';
66
  };
67
 
68
  const handleConfirmUpload = () => {
69
- onFileUpload(pendingFiles.map(pf => pf.file));
70
- // Update the parent's file types
 
 
71
  const startIndex = uploadedFiles.length;
72
  pendingFiles.forEach((pf, idx) => {
73
  setTimeout(() => {
74
  onFileTypeChange(startIndex + idx, pf.type);
75
  }, 0);
76
  });
 
77
  setPendingFiles([]);
78
  setShowTypeDialog(false);
79
  };
@@ -84,15 +94,14 @@ export function FileUploadArea({
84
  };
85
 
86
  const handlePendingFileTypeChange = (index: number, type: FileType) => {
87
- setPendingFiles(prev => prev.map((pf, i) =>
88
- i === index ? { ...pf, type } : pf
89
- ));
90
  };
91
 
92
  const getFileIcon = (filename: string) => {
93
- if (filename.endsWith('.pdf')) return FileText;
94
- if (filename.endsWith('.docx')) return File;
95
- if (filename.endsWith('.pptx')) return Presentation;
 
96
  return File;
97
  };
98
 
@@ -102,22 +111,33 @@ export function FileUploadArea({
102
  return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
103
  };
104
 
105
- const getFileTypeLabel = (type: FileType) => {
106
- const labels: Record<FileType, string> = {
107
- 'syllabus': 'Syllabus',
108
- 'lecture-slides': 'Lecture Slides / PPT',
109
- 'literature-review': 'Literature Review / Paper',
110
- 'other': 'Other Course Document',
111
- };
112
- return labels[type];
113
- };
114
 
115
  return (
116
  <Card className="p-4 space-y-3">
117
  <div className="flex items-center justify-between">
118
- <h4 className="text-sm">Course Materials</h4>
119
- {uploadedFiles.length > 0 && (
120
- <Badge variant="secondary">{uploadedFiles.length} file(s)</Badge>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  )}
122
  </div>
123
 
@@ -134,12 +154,8 @@ export function FileUploadArea({
134
  onClick={() => !disabled && fileInputRef.current?.click()}
135
  >
136
  <Upload className="h-6 w-6 mx-auto mb-2 text-muted-foreground" />
137
- <p className="text-sm text-muted-foreground mb-1">
138
- {disabled ? 'Please log in to upload' : 'Drop files or click to upload'}
139
- </p>
140
- <p className="text-xs text-muted-foreground">
141
- .pdf, .docx, .pptx
142
- </p>
143
  <input
144
  ref={fileInputRef}
145
  type="file"
@@ -156,19 +172,42 @@ export function FileUploadArea({
156
  <div className="space-y-3 max-h-64 overflow-y-auto">
157
  {uploadedFiles.map((uploadedFile, index) => {
158
  const Icon = getFileIcon(uploadedFile.file.name);
 
 
159
  return (
160
- <div
161
- key={index}
162
- className="p-3 bg-muted rounded-md space-y-2"
163
- >
164
  <div className="flex items-center gap-2 group">
165
  <Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
166
  <div className="flex-1 min-w-0">
167
  <p className="text-sm truncate">{uploadedFile.file.name}</p>
168
- <p className="text-xs text-muted-foreground">
169
- {formatFileSize(uploadedFile.file.size)}
170
- </p>
171
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  <Button
173
  variant="ghost"
174
  size="icon"
@@ -177,16 +216,15 @@ export function FileUploadArea({
177
  e.stopPropagation();
178
  onRemoveFile(index);
179
  }}
 
180
  >
181
  <X className="h-3 w-3" />
182
  </Button>
183
  </div>
 
184
  <div className="space-y-1">
185
  <label className="text-xs text-muted-foreground">File Type</label>
186
- <Select
187
- value={uploadedFile.type}
188
- onValueChange={(value) => onFileTypeChange(index, value as FileType)}
189
- >
190
  <SelectTrigger className="h-8 text-xs">
191
  <SelectValue />
192
  </SelectTrigger>
@@ -210,31 +248,26 @@ export function FileUploadArea({
210
  <DialogContent className="sm:max-w-[425px]">
211
  <DialogHeader>
212
  <DialogTitle>Select File Types</DialogTitle>
213
- <DialogDescription>
214
- Please select the type for each file you are uploading.
215
- </DialogDescription>
216
  </DialogHeader>
 
217
  <div className="space-y-3 max-h-64 overflow-y-auto">
218
  {pendingFiles.map((pendingFile, index) => {
219
  const Icon = getFileIcon(pendingFile.file.name);
220
  return (
221
- <div
222
- key={index}
223
- className="p-3 bg-muted rounded-md space-y-2"
224
- >
225
  <div className="flex items-center gap-2 group">
226
  <Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
227
  <div className="flex-1 min-w-0">
228
  <p className="text-sm truncate">{pendingFile.file.name}</p>
229
- <p className="text-xs text-muted-foreground">
230
- {formatFileSize(pendingFile.file.size)}
231
- </p>
232
  </div>
233
  </div>
 
234
  <div className="space-y-1">
235
  <label className="text-xs text-muted-foreground">File Type</label>
236
- <Select
237
- value={pendingFile.type}
238
  onValueChange={(value) => handlePendingFileTypeChange(index, value as FileType)}
239
  >
240
  <SelectTrigger className="h-8 text-xs">
@@ -252,22 +285,16 @@ export function FileUploadArea({
252
  );
253
  })}
254
  </div>
 
255
  <DialogFooter>
256
- <Button
257
- variant="outline"
258
- onClick={handleCancelUpload}
259
- >
260
  Cancel
261
  </Button>
262
- <Button
263
- onClick={handleConfirmUpload}
264
- >
265
- Upload
266
- </Button>
267
  </DialogFooter>
268
  </DialogContent>
269
  </Dialog>
270
  )}
271
  </Card>
272
  );
273
- }
 
1
  import React, { useRef, useState } from 'react';
2
  import { Button } from './ui/button';
3
+ import { Upload, File, X, FileText, Presentation, CloudUpload } from 'lucide-react';
4
  import { Card } from './ui/card';
5
  import { Badge } from './ui/badge';
6
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
 
12
  onFileUpload: (files: File[]) => void;
13
  onRemoveFile: (index: number) => void;
14
  onFileTypeChange: (index: number, type: FileType) => void;
15
+
16
+ // ✅ 新增:真正触发后端上传(App 里实现)
17
+ onUploadFile?: (index: number) => void;
18
+ onUploadAll?: () => void;
19
+
20
  disabled?: boolean;
21
  }
22
 
 
30
  onFileUpload,
31
  onRemoveFile,
32
  onFileTypeChange,
33
+ onUploadFile,
34
+ onUploadAll,
35
  disabled = false,
36
  }: FileUploadAreaProps) {
37
  const [isDragging, setIsDragging] = useState(false);
 
58
  );
59
 
60
  if (files.length > 0) {
61
+ setPendingFiles(files.map((file) => ({ file, type: 'other' as FileType })));
62
  setShowTypeDialog(true);
63
  }
64
  };
 
66
  const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
67
  const files = Array.from(e.target.files || []);
68
  if (files.length > 0) {
69
+ setPendingFiles(files.map((file) => ({ file, type: 'other' as FileType })));
70
  setShowTypeDialog(true);
71
  }
72
  e.target.value = '';
73
  };
74
 
75
  const handleConfirmUpload = () => {
76
+ // 这里只“入库”,不触发后端上传(符合你现在的逻辑)
77
+ onFileUpload(pendingFiles.map((pf) => pf.file));
78
+
79
+ // 把用户在弹窗里选的 type 同步到父组件列表(通过 index 偏移)
80
  const startIndex = uploadedFiles.length;
81
  pendingFiles.forEach((pf, idx) => {
82
  setTimeout(() => {
83
  onFileTypeChange(startIndex + idx, pf.type);
84
  }, 0);
85
  });
86
+
87
  setPendingFiles([]);
88
  setShowTypeDialog(false);
89
  };
 
94
  };
95
 
96
  const handlePendingFileTypeChange = (index: number, type: FileType) => {
97
+ setPendingFiles((prev) => prev.map((pf, i) => (i === index ? { ...pf, type } : pf)));
 
 
98
  };
99
 
100
  const getFileIcon = (filename: string) => {
101
+ const lower = filename.toLowerCase();
102
+ if (lower.endsWith('.pdf')) return FileText;
103
+ if (lower.endsWith('.docx')) return File;
104
+ if (lower.endsWith('.pptx')) return Presentation;
105
  return File;
106
  };
107
 
 
111
  return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
112
  };
113
 
114
+ const hasPendingUploads = uploadedFiles.some((f) => !f.uploaded);
 
 
 
 
 
 
 
 
115
 
116
  return (
117
  <Card className="p-4 space-y-3">
118
  <div className="flex items-center justify-between">
119
+ <div className="flex items-center gap-2">
120
+ <h4 className="text-sm">Course Materials</h4>
121
+ {uploadedFiles.length > 0 && <Badge variant="secondary">{uploadedFiles.length} file(s)</Badge>}
122
+ </div>
123
+
124
+ {/* ✅ Upload All Pending(只在存在 pending 且提供了回调时显示) */}
125
+ {uploadedFiles.length > 0 && hasPendingUploads && !!onUploadAll && (
126
+ <Button
127
+ variant="outline"
128
+ size="sm"
129
+ className="h-8 text-xs gap-2"
130
+ disabled={disabled}
131
+ onClick={(e) => {
132
+ e.preventDefault();
133
+ e.stopPropagation();
134
+ onUploadAll();
135
+ }}
136
+ title="Upload all pending files"
137
+ >
138
+ <CloudUpload className="h-4 w-4" />
139
+ Upload All
140
+ </Button>
141
  )}
142
  </div>
143
 
 
154
  onClick={() => !disabled && fileInputRef.current?.click()}
155
  >
156
  <Upload className="h-6 w-6 mx-auto mb-2 text-muted-foreground" />
157
+ <p className="text-sm text-muted-foreground mb-1">{disabled ? 'Please log in to upload' : 'Drop files or click to upload'}</p>
158
+ <p className="text-xs text-muted-foreground">.pdf, .docx, .pptx</p>
 
 
 
 
159
  <input
160
  ref={fileInputRef}
161
  type="file"
 
172
  <div className="space-y-3 max-h-64 overflow-y-auto">
173
  {uploadedFiles.map((uploadedFile, index) => {
174
  const Icon = getFileIcon(uploadedFile.file.name);
175
+ const isUploaded = !!uploadedFile.uploaded;
176
+
177
  return (
178
+ <div key={index} className="p-3 bg-muted rounded-md space-y-2">
 
 
 
179
  <div className="flex items-center gap-2 group">
180
  <Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
181
  <div className="flex-1 min-w-0">
182
  <p className="text-sm truncate">{uploadedFile.file.name}</p>
183
+ <p className="text-xs text-muted-foreground">{formatFileSize(uploadedFile.file.size)}</p>
 
 
184
  </div>
185
+
186
+ {/* ✅ 单文件 Upload(仅未上传时显示 & 必须有回调) */}
187
+ {!isUploaded && !!onUploadFile && (
188
+ <Button
189
+ variant="secondary"
190
+ size="sm"
191
+ className="h-7 text-xs px-2"
192
+ disabled={disabled}
193
+ onClick={(e) => {
194
+ e.preventDefault();
195
+ e.stopPropagation();
196
+ onUploadFile(index);
197
+ }}
198
+ title="Upload this file to backend"
199
+ >
200
+ Upload
201
+ </Button>
202
+ )}
203
+
204
+ {/* ✅ 已上传状态 */}
205
+ {isUploaded && (
206
+ <Badge variant="secondary" className="text-[10px]">
207
+ Uploaded{typeof uploadedFile.uploadedChunks === 'number' ? ` (+${uploadedFile.uploadedChunks})` : ''}
208
+ </Badge>
209
+ )}
210
+
211
  <Button
212
  variant="ghost"
213
  size="icon"
 
216
  e.stopPropagation();
217
  onRemoveFile(index);
218
  }}
219
+ title="Remove"
220
  >
221
  <X className="h-3 w-3" />
222
  </Button>
223
  </div>
224
+
225
  <div className="space-y-1">
226
  <label className="text-xs text-muted-foreground">File Type</label>
227
+ <Select value={uploadedFile.type} onValueChange={(value) => onFileTypeChange(index, value as FileType)}>
 
 
 
228
  <SelectTrigger className="h-8 text-xs">
229
  <SelectValue />
230
  </SelectTrigger>
 
248
  <DialogContent className="sm:max-w-[425px]">
249
  <DialogHeader>
250
  <DialogTitle>Select File Types</DialogTitle>
251
+ <DialogDescription>Please select the type for each file you are uploading.</DialogDescription>
 
 
252
  </DialogHeader>
253
+
254
  <div className="space-y-3 max-h-64 overflow-y-auto">
255
  {pendingFiles.map((pendingFile, index) => {
256
  const Icon = getFileIcon(pendingFile.file.name);
257
  return (
258
+ <div key={index} className="p-3 bg-muted rounded-md space-y-2">
 
 
 
259
  <div className="flex items-center gap-2 group">
260
  <Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
261
  <div className="flex-1 min-w-0">
262
  <p className="text-sm truncate">{pendingFile.file.name}</p>
263
+ <p className="text-xs text-muted-foreground">{formatFileSize(pendingFile.file.size)}</p>
 
 
264
  </div>
265
  </div>
266
+
267
  <div className="space-y-1">
268
  <label className="text-xs text-muted-foreground">File Type</label>
269
+ <Select
270
+ value={pendingFile.type}
271
  onValueChange={(value) => handlePendingFileTypeChange(index, value as FileType)}
272
  >
273
  <SelectTrigger className="h-8 text-xs">
 
285
  );
286
  })}
287
  </div>
288
+
289
  <DialogFooter>
290
+ <Button variant="outline" onClick={handleCancelUpload}>
 
 
 
291
  Cancel
292
  </Button>
293
+ <Button onClick={handleConfirmUpload}>Upload</Button>
 
 
 
 
294
  </DialogFooter>
295
  </DialogContent>
296
  </Dialog>
297
  )}
298
  </Card>
299
  );
300
+ }