feat: enhance file upload progress tracking in UploadDatasetEntriesModal
This commit is contained in:
@@ -16,7 +16,7 @@ export default function UploadDatasetEntriesModal({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
const [uploadQueue, setUploadQueue] = useState<Array<{ file: File; status: 'pending' | 'uploading' | 'completed' | 'failed'; message?: string }>>([]);
|
const [uploadQueue, setUploadQueue] = useState<Array<{ file: File; status: 'pending' | 'uploading' | 'completed' | 'failed'; message?: string; progress: number }>>([]);
|
||||||
const [status, setStatus] = useState<string>('');
|
const [status, setStatus] = useState<string>('');
|
||||||
|
|
||||||
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
@@ -24,8 +24,8 @@ export default function UploadDatasetEntriesModal({
|
|||||||
if (selectedFiles.length > 0) {
|
if (selectedFiles.length > 0) {
|
||||||
setFiles(selectedFiles);
|
setFiles(selectedFiles);
|
||||||
setError(null);
|
setError(null);
|
||||||
// Initialize queue with pending status
|
// Initialize queue with pending status and 0 progress
|
||||||
setUploadQueue(selectedFiles.map(file => ({ file, status: 'pending' })));
|
setUploadQueue(selectedFiles.map(file => ({ file, status: 'pending', progress: 0 })));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,37 +48,23 @@ export default function UploadDatasetEntriesModal({
|
|||||||
for (let i = 0; i < newQueue.length; i++) {
|
for (let i = 0; i < newQueue.length; i++) {
|
||||||
const item = newQueue[i];
|
const item = newQueue[i];
|
||||||
item.status = 'uploading';
|
item.status = 'uploading';
|
||||||
|
item.progress = 0;
|
||||||
setUploadQueue([...newQueue]);
|
setUploadQueue([...newQueue]);
|
||||||
setStatus(`Processing file ${i + 1} of ${newQueue.length}: ${item.file.name}`);
|
setStatus(`Processing file ${i + 1} of ${newQueue.length}: ${item.file.name}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
// Upload with progress tracking
|
||||||
formData.append('file', item.file);
|
const uploadResult = await uploadFileWithProgress(item.file, i, newQueue);
|
||||||
formData.append('datasetId', datasetId.toString());
|
|
||||||
|
|
||||||
const uploadResponse = await fetch('/api/upload', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!uploadResponse.ok) {
|
|
||||||
const errorData = await uploadResponse.json();
|
|
||||||
throw new Error(errorData.error || 'Failed to upload ZIP file');
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadResult = await uploadResponse.json();
|
|
||||||
|
|
||||||
if (!uploadResult.success) {
|
|
||||||
throw new Error('Failed to upload ZIP file');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the extracted data
|
// Process the extracted data
|
||||||
const processResult = await processDatasetEntries(datasetId, uploadResult.tempDir);
|
const processResult = await processDatasetEntries(datasetId, uploadResult.tempDir);
|
||||||
|
|
||||||
item.status = 'completed';
|
item.status = 'completed';
|
||||||
|
item.progress = 100;
|
||||||
item.message = `✓ Processed (${processResult.entriesCreated} entries)`;
|
item.message = `✓ Processed (${processResult.entriesCreated} entries)`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
item.status = 'failed';
|
item.status = 'failed';
|
||||||
|
item.progress = 0;
|
||||||
item.message = err instanceof Error ? err.message : 'Unknown error';
|
item.message = err instanceof Error ? err.message : 'Unknown error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +103,62 @@ export default function UploadDatasetEntriesModal({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uploadFileWithProgress(
|
||||||
|
file: File,
|
||||||
|
index: number,
|
||||||
|
queue: Array<{ file: File; status: 'pending' | 'uploading' | 'completed' | 'failed'; progress: number; message?: string }>
|
||||||
|
): Promise<{ tempDir: string }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('datasetId', datasetId.toString());
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
// Track upload progress
|
||||||
|
xhr.upload.addEventListener('progress', (event) => {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
const progress = Math.round((event.loaded / event.total) * 100);
|
||||||
|
queue[index].progress = progress;
|
||||||
|
setUploadQueue([...queue]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('load', () => {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(xhr.responseText);
|
||||||
|
if (result.success) {
|
||||||
|
resolve(result);
|
||||||
|
} else {
|
||||||
|
reject(new Error(result.error || 'Upload failed'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('Failed to parse response'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const error = JSON.parse(xhr.responseText);
|
||||||
|
reject(new Error(error.error || `Upload failed with status ${xhr.status}`));
|
||||||
|
} catch {
|
||||||
|
reject(new Error(`Upload failed with status ${xhr.status}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('error', () => {
|
||||||
|
reject(new Error('Upload failed'));
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('abort', () => {
|
||||||
|
reject(new Error('Upload aborted'));
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.open('POST', '/api/upload');
|
||||||
|
xhr.send(formData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
@@ -215,11 +257,12 @@ export default function UploadDatasetEntriesModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{uploadQueue.length > 0 && (
|
{uploadQueue.length > 0 && (
|
||||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 space-y-2">
|
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 space-y-3">
|
||||||
<p className="font-semibold text-sm text-gray-700">Upload Queue:</p>
|
<p className="font-semibold text-sm text-gray-700">Upload Queue:</p>
|
||||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
<div className="space-y-2 max-h-48 overflow-y-auto">
|
||||||
{uploadQueue.map((item, idx) => (
|
{uploadQueue.map((item, idx) => (
|
||||||
<div key={idx} className="flex items-center gap-2 text-xs">
|
<div key={idx} className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<span className={`font-medium ${
|
<span className={`font-medium ${
|
||||||
item.status === 'completed' ? 'text-green-600' :
|
item.status === 'completed' ? 'text-green-600' :
|
||||||
item.status === 'failed' ? 'text-red-600' :
|
item.status === 'failed' ? 'text-red-600' :
|
||||||
@@ -231,8 +274,30 @@ export default function UploadDatasetEntriesModal({
|
|||||||
item.status === 'uploading' ? '⟳' :
|
item.status === 'uploading' ? '⟳' :
|
||||||
'○'}
|
'○'}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex-1">{item.file.name}</span>
|
<span className="flex-1 truncate">{item.file.name}</span>
|
||||||
{item.message && <span className="text-gray-500">{item.message}</span>}
|
{item.progress > 0 && item.progress < 100 && (
|
||||||
|
<span className="text-gray-500">{item.progress}%</span>
|
||||||
|
)}
|
||||||
|
{item.message && <span className="text-gray-500 text-xs">{item.message}</span>}
|
||||||
|
</div>
|
||||||
|
{item.status === 'uploading' && (
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="bg-blue-500 h-full transition-all duration-300"
|
||||||
|
style={{ width: `${item.progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{item.status === 'completed' && (
|
||||||
|
<div className="w-full bg-green-200 rounded-full h-2 overflow-hidden">
|
||||||
|
<div className="bg-green-500 h-full w-full" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{item.status === 'failed' && (
|
||||||
|
<div className="w-full bg-red-200 rounded-full h-2 overflow-hidden">
|
||||||
|
<div className="bg-red-500 h-full w-full" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user