feat(client): do a better details page
This commit is contained in:
parent
06d5e583d0
commit
14f5a998f9
|
|
@ -10,6 +10,7 @@ import Dashboard from './components/Dashboard/Dashboard';
|
|||
import UploadPage from './pages/UploadPage';
|
||||
import DocumentsPage from './pages/DocumentsPage';
|
||||
import SearchPage from './pages/SearchPage';
|
||||
import DocumentDetailsPage from './pages/DocumentDetailsPage';
|
||||
|
||||
function App() {
|
||||
const { user, loading } = useAuth();
|
||||
|
|
@ -59,6 +60,7 @@ function App() {
|
|||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/upload" element={<UploadPage />} />
|
||||
<Route path="/documents" element={<DocumentsPage />} />
|
||||
<Route path="/documents/:id" element={<DocumentDetailsPage />} />
|
||||
<Route path="/search" element={<SearchPage />} />
|
||||
<Route path="/watch" element={<div>Watch Folder Page - Coming Soon</div>} />
|
||||
<Route path="/settings" element={<div>Settings Page - Coming Soon</div>} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,412 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Card,
|
||||
CardContent,
|
||||
Button,
|
||||
Chip,
|
||||
Stack,
|
||||
Grid,
|
||||
Divider,
|
||||
IconButton,
|
||||
Paper,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
Tooltip,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ArrowBack as BackIcon,
|
||||
Download as DownloadIcon,
|
||||
PictureAsPdf as PdfIcon,
|
||||
Image as ImageIcon,
|
||||
Description as DocIcon,
|
||||
TextSnippet as TextIcon,
|
||||
CalendarToday as DateIcon,
|
||||
Storage as SizeIcon,
|
||||
Tag as TagIcon,
|
||||
Visibility as ViewIcon,
|
||||
Search as SearchIcon,
|
||||
Edit as EditIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { documentService } from '../services/api';
|
||||
|
||||
const DocumentDetailsPage = () => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [document, setDocument] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [ocrText, setOcrText] = useState('');
|
||||
const [showOcrDialog, setShowOcrDialog] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchDocumentDetails();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const fetchDocumentDetails = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Since we don't have a direct document details endpoint,
|
||||
// we'll fetch the document from the list and find the matching one
|
||||
const response = await documentService.list(1000, 0);
|
||||
const foundDoc = response.data.find(doc => doc.id === id);
|
||||
|
||||
if (foundDoc) {
|
||||
setDocument(foundDoc);
|
||||
// If the document has OCR text, we could fetch it here
|
||||
// For now, we'll show a placeholder
|
||||
if (foundDoc.has_ocr_text) {
|
||||
setOcrText('OCR text extraction feature would be available here. The document has been processed and text content is available for search.');
|
||||
}
|
||||
} else {
|
||||
setError('Document not found');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to load document details');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const response = await documentService.download(document.id);
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', document.original_filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
console.error('Download failed:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const getFileIcon = (mimeType) => {
|
||||
if (mimeType?.includes('pdf')) return <PdfIcon color="error" sx={{ fontSize: 64 }} />;
|
||||
if (mimeType?.includes('image')) return <ImageIcon color="primary" sx={{ fontSize: 64 }} />;
|
||||
if (mimeType?.includes('text')) return <TextIcon color="info" sx={{ fontSize: 64 }} />;
|
||||
return <DocIcon color="secondary" sx={{ fontSize: 64 }} />;
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes) => {
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !document) {
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Button
|
||||
startIcon={<BackIcon />}
|
||||
onClick={() => navigate('/documents')}
|
||||
sx={{ mb: 3 }}
|
||||
>
|
||||
Back to Documents
|
||||
</Button>
|
||||
<Alert severity="error">
|
||||
{error || 'Document not found'}
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Button
|
||||
startIcon={<BackIcon />}
|
||||
onClick={() => navigate('/documents')}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
Back to Documents
|
||||
</Button>
|
||||
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
Document Details
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
View and manage document information
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{/* Document Preview */}
|
||||
<Grid item xs={12} md={4}>
|
||||
<Card sx={{ height: 'fit-content' }}>
|
||||
<CardContent sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
mb: 3,
|
||||
p: 3,
|
||||
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%)',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
{getFileIcon(document.mime_type)}
|
||||
</Box>
|
||||
|
||||
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
|
||||
{document.original_filename}
|
||||
</Typography>
|
||||
|
||||
<Stack direction="row" spacing={1} justifyContent="center" sx={{ mb: 3 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<DownloadIcon />}
|
||||
onClick={handleDownload}
|
||||
sx={{ borderRadius: 2 }}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
{document.has_ocr_text && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<SearchIcon />}
|
||||
onClick={() => setShowOcrDialog(true)}
|
||||
sx={{ borderRadius: 2 }}
|
||||
>
|
||||
View OCR
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{document.has_ocr_text && (
|
||||
<Chip
|
||||
label="OCR Processed"
|
||||
color="success"
|
||||
variant="outlined"
|
||||
icon={<TextIcon />}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Document Information */}
|
||||
<Grid item xs={12} md={8}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" sx={{ mb: 3, fontWeight: 600 }}>
|
||||
Document Information
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Paper sx={{ p: 2, height: '100%' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<DocIcon color="primary" sx={{ mr: 1 }} />
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
Filename
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||
{document.original_filename}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Paper sx={{ p: 2, height: '100%' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<SizeIcon color="primary" sx={{ mr: 1 }} />
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
File Size
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||
{formatFileSize(document.file_size)}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Paper sx={{ p: 2, height: '100%' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<DateIcon color="primary" sx={{ mr: 1 }} />
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
Upload Date
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||
{formatDate(document.created_at)}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Paper sx={{ p: 2, height: '100%' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<ViewIcon color="primary" sx={{ mr: 1 }} />
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
File Type
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||
{document.mime_type}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{document.tags && document.tags.length > 0 && (
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<TagIcon color="primary" sx={{ mr: 1 }} />
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
Tags
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
|
||||
{document.tags.map((tag, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={tag}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
|
||||
Processing Status
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'success.main',
|
||||
mr: 2,
|
||||
}}
|
||||
/>
|
||||
<Typography variant="body2">
|
||||
Document uploaded successfully
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: document.has_ocr_text ? 'success.main' : 'warning.main',
|
||||
mr: 2,
|
||||
}}
|
||||
/>
|
||||
<Typography variant="body2">
|
||||
{document.has_ocr_text ? 'OCR processing completed' : 'OCR processing pending'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* OCR Text Dialog */}
|
||||
<Dialog
|
||||
open={showOcrDialog}
|
||||
onClose={() => setShowOcrDialog(false)}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
Extracted Text (OCR)
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Paper
|
||||
sx={{
|
||||
p: 2,
|
||||
backgroundColor: 'grey.50',
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.200',
|
||||
maxHeight: 400,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
color: ocrText ? 'text.primary' : 'text.secondary',
|
||||
}}
|
||||
>
|
||||
{ocrText || 'OCR text extraction is not yet implemented in the frontend. The backend processes documents and stores extracted text for search functionality.'}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setShowOcrDialog(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentDetailsPage;
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
|
|
@ -41,6 +42,7 @@ import {
|
|||
import { documentService } from '../services/api';
|
||||
|
||||
const DocumentsPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [documents, setDocuments] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
|
@ -263,7 +265,10 @@ const DocumentsPage = () => {
|
|||
<ListItemIcon><DownloadIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Download</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => setDocMenuAnchor(null)}>
|
||||
<MenuItem onClick={() => {
|
||||
navigate(`/documents/${selectedDoc.id}`);
|
||||
setDocMenuAnchor(null);
|
||||
}}>
|
||||
<ListItemIcon><ViewIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>View Details</ListItemText>
|
||||
</MenuItem>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
|
|
@ -48,6 +49,7 @@ import {
|
|||
import { documentService } from '../services/api';
|
||||
|
||||
const SearchPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -609,6 +611,14 @@ const SearchPage = () => {
|
|||
)}
|
||||
</Box>
|
||||
|
||||
<Tooltip title="View Details">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => navigate(`/documents/${doc.id}`)}
|
||||
>
|
||||
<ViewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Download">
|
||||
<IconButton
|
||||
size="small"
|
||||
|
|
|
|||
Loading…
Reference in New Issue