275 lines
8.7 KiB
TypeScript
275 lines
8.7 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
Paper,
|
|
IconButton,
|
|
Checkbox,
|
|
Button,
|
|
Chip,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
Typography,
|
|
Tooltip,
|
|
} from '@mui/material';
|
|
import { Download as DownloadIcon, Delete as DeleteIcon, Error as ErrorIcon } from '@mui/icons-material';
|
|
import { File, FileStatus } from '../types/file';
|
|
import { api } from '../services/api';
|
|
|
|
interface FileListProps {
|
|
files: File[];
|
|
onFileStatusChange: () => void;
|
|
}
|
|
|
|
const FileList: React.FC<FileListProps> = ({ files, onFileStatusChange }) => {
|
|
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
|
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
const [fileToDelete, setFileToDelete] = useState<string | null>(null);
|
|
|
|
const handleSelectFile = (fileId: string) => {
|
|
setSelectedFiles((prev) =>
|
|
prev.includes(fileId)
|
|
? prev.filter((id) => id !== fileId)
|
|
: [...prev, fileId]
|
|
);
|
|
};
|
|
|
|
const handleSelectAll = () => {
|
|
setSelectedFiles((prev) =>
|
|
prev.length === files.length ? [] : files.map((file) => file.id)
|
|
);
|
|
};
|
|
|
|
const handleDownload = async (fileId: string) => {
|
|
try {
|
|
console.log('=== FRONTEND DOWNLOAD START ===');
|
|
console.log('File ID:', fileId);
|
|
|
|
const file = files.find((f) => f.id === fileId);
|
|
console.log('File object:', file);
|
|
|
|
const blob = await api.downloadFile(fileId);
|
|
console.log('Blob received:', blob);
|
|
console.log('Blob type:', blob.type);
|
|
console.log('Blob size:', blob.size);
|
|
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
|
|
// Match backend behavior: change extension to .md
|
|
const originalFilename = file?.filename || 'downloaded-file';
|
|
const filenameWithoutExt = originalFilename.replace(/\.[^/.]+$/, ''); // Remove extension
|
|
const downloadFilename = `${filenameWithoutExt}.md`;
|
|
|
|
console.log('Original filename:', originalFilename);
|
|
console.log('Filename without extension:', filenameWithoutExt);
|
|
console.log('Download filename:', downloadFilename);
|
|
|
|
a.download = downloadFilename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
|
|
console.log('=== FRONTEND DOWNLOAD END ===');
|
|
} catch (error) {
|
|
console.error('Error downloading file:', error);
|
|
}
|
|
};
|
|
|
|
const handleDownloadSelected = async () => {
|
|
for (const fileId of selectedFiles) {
|
|
await handleDownload(fileId);
|
|
}
|
|
};
|
|
|
|
const handleDeleteClick = (fileId: string) => {
|
|
setFileToDelete(fileId);
|
|
setDeleteDialogOpen(true);
|
|
};
|
|
|
|
const handleDeleteConfirm = async () => {
|
|
if (fileToDelete) {
|
|
try {
|
|
await api.deleteFile(fileToDelete);
|
|
onFileStatusChange();
|
|
} catch (error) {
|
|
console.error('Error deleting file:', error);
|
|
}
|
|
}
|
|
setDeleteDialogOpen(false);
|
|
setFileToDelete(null);
|
|
};
|
|
|
|
const handleDeleteCancel = () => {
|
|
setDeleteDialogOpen(false);
|
|
setFileToDelete(null);
|
|
};
|
|
|
|
const getStatusColor = (status: FileStatus) => {
|
|
switch (status) {
|
|
case FileStatus.SUCCESS:
|
|
return 'success';
|
|
case FileStatus.FAILED:
|
|
return 'error';
|
|
case FileStatus.PROCESSING:
|
|
return 'warning';
|
|
default:
|
|
return 'default';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div style={{ marginBottom: '1rem' }}>
|
|
<Button
|
|
variant="contained"
|
|
color="primary"
|
|
onClick={handleDownloadSelected}
|
|
disabled={selectedFiles.length === 0}
|
|
sx={{ mr: 1 }}
|
|
>
|
|
Download Selected
|
|
</Button>
|
|
</div>
|
|
<TableContainer component={Paper}>
|
|
<Table>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell padding="checkbox">
|
|
<Checkbox
|
|
checked={selectedFiles.length === files.length}
|
|
indeterminate={selectedFiles.length > 0 && selectedFiles.length < files.length}
|
|
onChange={handleSelectAll}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>Filename</TableCell>
|
|
<TableCell>Status</TableCell>
|
|
<TableCell>Created At</TableCell>
|
|
<TableCell>Finished At</TableCell>
|
|
<TableCell>Actions</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{files.map((file) => (
|
|
<TableRow key={file.id}>
|
|
<TableCell padding="checkbox">
|
|
<Checkbox
|
|
checked={selectedFiles.includes(file.id)}
|
|
onChange={() => handleSelectFile(file.id)}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>{file.filename}</TableCell>
|
|
<TableCell>
|
|
<Chip
|
|
label={file.status}
|
|
color={getStatusColor(file.status) as any}
|
|
size="small"
|
|
/>
|
|
{file.status === FileStatus.FAILED && file.error_message && (
|
|
<div style={{ marginTop: '4px' }}>
|
|
<Tooltip
|
|
title={file.error_message}
|
|
placement="top-start"
|
|
arrow
|
|
sx={{ maxWidth: '400px' }}
|
|
>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'flex-start',
|
|
gap: '4px',
|
|
padding: '4px 8px',
|
|
backgroundColor: '#ffebee',
|
|
borderRadius: '4px',
|
|
border: '1px solid #ffcdd2'
|
|
}}
|
|
>
|
|
<ErrorIcon
|
|
color="error"
|
|
sx={{ fontSize: '16px', marginTop: '1px', flexShrink: 0 }}
|
|
/>
|
|
<Typography
|
|
variant="caption"
|
|
color="error"
|
|
sx={{
|
|
display: 'block',
|
|
wordBreak: 'break-word',
|
|
maxWidth: '300px',
|
|
lineHeight: '1.2',
|
|
cursor: 'help',
|
|
fontWeight: 500
|
|
}}
|
|
>
|
|
{file.error_message.length > 50
|
|
? `${file.error_message.substring(0, 50)}...`
|
|
: file.error_message
|
|
}
|
|
</Typography>
|
|
</div>
|
|
</Tooltip>
|
|
</div>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{new Date(file.created_at).toLocaleString()}
|
|
</TableCell>
|
|
<TableCell>
|
|
{(file.status === FileStatus.SUCCESS || file.status === FileStatus.FAILED)
|
|
? new Date(file.updated_at).toLocaleString()
|
|
: '—'}
|
|
</TableCell>
|
|
<TableCell>
|
|
<IconButton
|
|
onClick={() => handleDeleteClick(file.id)}
|
|
size="small"
|
|
color="error"
|
|
sx={{ mr: 1 }}
|
|
>
|
|
<DeleteIcon />
|
|
</IconButton>
|
|
{file.status === FileStatus.SUCCESS && (
|
|
<IconButton
|
|
onClick={() => handleDownload(file.id)}
|
|
size="small"
|
|
color="primary"
|
|
>
|
|
<DownloadIcon />
|
|
</IconButton>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
|
|
<Dialog
|
|
open={deleteDialogOpen}
|
|
onClose={handleDeleteCancel}
|
|
>
|
|
<DialogTitle>Confirm Delete</DialogTitle>
|
|
<DialogContent>
|
|
<Typography>
|
|
Are you sure you want to delete this file? This action cannot be undone.
|
|
</Typography>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={handleDeleteCancel}>Cancel</Button>
|
|
<Button onClick={handleDeleteConfirm} color="error" variant="contained">
|
|
Delete
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FileList;
|