diff --git a/Cargo.lock b/Cargo.lock index f147635..85bf3aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2454,6 +2454,7 @@ dependencies = [ "chrono", "clap", "dotenvy", + "futures", "futures-util", "hostname", "image", diff --git a/Cargo.toml b/Cargo.toml index edc9e47..46e3f2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ tracing = "0.1" tracing-subscriber = "0.3" tokio-util = { version = "0.7", features = ["io"] } futures-util = "0.3" +futures = "0.3" notify = "6" mime_guess = "2" tesseract = { version = "0.15", optional = true } diff --git a/frontend/src/components/DocumentThumbnail.tsx b/frontend/src/components/DocumentThumbnail.tsx new file mode 100644 index 0000000..2cb6965 --- /dev/null +++ b/frontend/src/components/DocumentThumbnail.tsx @@ -0,0 +1,118 @@ +import React, { useState, useEffect } from 'react'; +import { Box } from '@mui/material'; +import { + PictureAsPdf as PdfIcon, + Image as ImageIcon, + Description as DocIcon, + TextSnippet as TextIcon, +} from '@mui/icons-material'; +import { documentService } from '../services/api'; + +interface DocumentThumbnailProps { + documentId: string; + mimeType: string; + size?: 'small' | 'medium' | 'large'; + fallbackIcon?: boolean; +} + +const DocumentThumbnail: React.FC = ({ + documentId, + mimeType, + size = 'medium', + fallbackIcon = true, +}) => { + const [thumbnailUrl, setThumbnailUrl] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + useEffect(() => { + loadThumbnail(); + + // Cleanup URL when component unmounts + return () => { + if (thumbnailUrl) { + window.URL.revokeObjectURL(thumbnailUrl); + } + }; + }, [documentId]); + + const loadThumbnail = async (): Promise => { + try { + setLoading(true); + setError(false); + + const response = await documentService.getThumbnail(documentId); + const url = window.URL.createObjectURL(new Blob([response.data])); + setThumbnailUrl(url); + } catch (err) { + setError(true); + } finally { + setLoading(false); + } + }; + + const getFileIcon = (mimeType: string): React.ReactElement => { + const iconProps = { + sx: { + fontSize: size === 'small' ? 24 : size === 'medium' ? 48 : 64, + color: 'action.active', + } + }; + + if (mimeType.includes('pdf')) return ; + if (mimeType.includes('image')) return ; + if (mimeType.includes('text')) return ; + return ; + }; + + const dimensions = { + small: { width: 40, height: 40 }, + medium: { width: 80, height: 80 }, + large: { width: 120, height: 120 }, + }; + + if (thumbnailUrl && !error) { + return ( + + Document thumbnail + + ); + } + + if (fallbackIcon) { + return ( + + {getFileIcon(mimeType)} + + ); + } + + return null; +}; + +export default DocumentThumbnail; \ No newline at end of file diff --git a/frontend/src/components/DocumentViewer.tsx b/frontend/src/components/DocumentViewer.tsx new file mode 100644 index 0000000..9081c72 --- /dev/null +++ b/frontend/src/components/DocumentViewer.tsx @@ -0,0 +1,216 @@ +import React, { useState, useEffect } from 'react'; +import { + Box, + Typography, + CircularProgress, + Alert, + Paper, +} from '@mui/material'; +import { documentService } from '../services/api'; + +interface DocumentViewerProps { + documentId: string; + filename: string; + mimeType: string; +} + +const DocumentViewer: React.FC = ({ + documentId, + filename, + mimeType, +}) => { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [documentUrl, setDocumentUrl] = useState(null); + + useEffect(() => { + loadDocument(); + + // Cleanup URL when component unmounts + return () => { + if (documentUrl) { + window.URL.revokeObjectURL(documentUrl); + } + }; + }, [documentId]); + + const loadDocument = async (): Promise => { + try { + setLoading(true); + setError(null); + + const response = await documentService.view(documentId); + const url = window.URL.createObjectURL(new Blob([response.data], { type: mimeType })); + setDocumentUrl(url); + } catch (err) { + console.error('Failed to load document:', err); + setError('Failed to load document for viewing'); + } finally { + setLoading(false); + } + }; + + const renderDocumentContent = (): React.ReactElement => { + if (!documentUrl) return <>; + + // Handle images + if (mimeType.startsWith('image/')) { + return ( + + {filename} + + ); + } + + // Handle PDFs + if (mimeType === 'application/pdf') { + return ( + +