legal-doc-masker/frontend/src/components/FileList.tsx

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;