fix(client): correctly style the document details page for light/dark mode

This commit is contained in:
perf3ct 2025-07-10 20:27:51 +00:00
parent 1178493030
commit 63e3035bac
No known key found for this signature in database
GPG Key ID: 569C4EEC436F5232
5 changed files with 408 additions and 303 deletions

View File

@ -19,7 +19,8 @@ import {
Error as ErrorIcon,
Info as InfoIcon,
} from '@mui/icons-material';
import { modernTokens } from '../theme';
import { useTheme } from '../contexts/ThemeContext';
import { useTheme as useMuiTheme } from '@mui/material/styles';
interface FileIntegrityDisplayProps {
fileHash?: string;
@ -43,6 +44,8 @@ const FileIntegrityDisplay: React.FC<FileIntegrityDisplayProps> = ({
compact = false,
}) => {
const [copied, setCopied] = useState(false);
const { modernTokens } = useTheme();
const theme = useMuiTheme();
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
@ -60,7 +63,7 @@ const FileIntegrityDisplay: React.FC<FileIntegrityDisplayProps> = ({
return {
status: 'unknown',
icon: <InfoIcon />,
color: modernTokens.colors.neutral[500],
color: theme.palette.text.secondary,
message: 'Hash not available',
};
}
@ -70,7 +73,7 @@ const FileIntegrityDisplay: React.FC<FileIntegrityDisplayProps> = ({
return {
status: 'verified',
icon: <CheckIcon />,
color: modernTokens.colors.success[500],
color: theme.palette.success.main,
message: 'File integrity verified',
};
}
@ -78,7 +81,7 @@ const FileIntegrityDisplay: React.FC<FileIntegrityDisplayProps> = ({
return {
status: 'warning',
icon: <WarningIcon />,
color: modernTokens.colors.warning[500],
color: theme.palette.warning.main,
message: 'Hash format unusual',
};
};
@ -107,8 +110,8 @@ const FileIntegrityDisplay: React.FC<FileIntegrityDisplayProps> = ({
<Paper
sx={{
p: 2,
background: `linear-gradient(135deg, ${modernTokens.colors.neutral[50]} 0%, ${modernTokens.colors.primary[50]} 100%)`,
border: `1px solid ${modernTokens.colors.neutral[200]}`,
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
@ -183,8 +186,8 @@ const FileIntegrityDisplay: React.FC<FileIntegrityDisplayProps> = ({
<Paper
sx={{
p: 3,
background: `linear-gradient(135deg, ${modernTokens.colors.neutral[50]} 0%, ${modernTokens.colors.primary[50]} 100%)`,
border: `1px solid ${modernTokens.colors.neutral[200]}`,
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
}}
>
{/* Header */}
@ -194,7 +197,7 @@ const FileIntegrityDisplay: React.FC<FileIntegrityDisplayProps> = ({
sx={{
fontSize: 24,
mr: 1.5,
color: modernTokens.colors.primary[500]
color: theme.palette.primary.main
}}
/>
<Typography variant="h6" sx={{ fontWeight: 600 }}>
@ -221,7 +224,7 @@ const FileIntegrityDisplay: React.FC<FileIntegrityDisplayProps> = ({
sx={{
fontSize: 18,
mr: 1,
color: modernTokens.colors.neutral[600]
color: theme.palette.text.secondary
}}
/>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
@ -235,9 +238,9 @@ const FileIntegrityDisplay: React.FC<FileIntegrityDisplayProps> = ({
display: 'flex',
alignItems: 'center',
p: 2,
backgroundColor: modernTokens.colors.neutral[100],
backgroundColor: theme.palette.action.hover,
borderRadius: 1,
border: `1px solid ${modernTokens.colors.neutral[200]}`,
border: `1px solid ${theme.palette.divider}`,
}}
>
<Typography
@ -247,7 +250,7 @@ const FileIntegrityDisplay: React.FC<FileIntegrityDisplayProps> = ({
flex: 1,
wordBreak: 'break-all',
fontSize: '0.8rem',
color: modernTokens.colors.neutral[700],
color: theme.palette.text.primary,
}}
>
{fileHash}
@ -294,8 +297,8 @@ const FileIntegrityDisplay: React.FC<FileIntegrityDisplayProps> = ({
size="small"
sx={{
fontSize: '0.75rem',
backgroundColor: modernTokens.colors.neutral[100],
border: `1px solid ${modernTokens.colors.neutral[300]}`,
backgroundColor: theme.palette.action.hover,
border: `1px solid ${theme.palette.divider}`,
}}
/>
</Box>
@ -329,9 +332,9 @@ const FileIntegrityDisplay: React.FC<FileIntegrityDisplayProps> = ({
size="small"
sx={{
fontSize: '0.75rem',
backgroundColor: modernTokens.colors.primary[50],
color: modernTokens.colors.primary[700],
border: `1px solid ${modernTokens.colors.primary[200]}`,
backgroundColor: theme.palette.primary.light,
color: theme.palette.primary.dark,
border: `1px solid ${theme.palette.primary.main}`,
}}
/>
</Box>

View File

@ -29,7 +29,8 @@ import {
Schedule as ScheduleIcon,
Person as PersonIcon,
} from '@mui/icons-material';
import { modernTokens } from '../theme';
import { useTheme } from '../contexts/ThemeContext';
import { useTheme as useMuiTheme } from '@mui/material/styles';
import { documentService } from '../services/api';
interface ProcessingTimelineProps {
@ -70,6 +71,8 @@ const ProcessingTimeline: React.FC<ProcessingTimelineProps> = ({
const [expanded, setExpanded] = useState(!compact);
const [retryHistory, setRetryHistory] = useState<any[]>([]);
const [loadingHistory, setLoadingHistory] = useState(false);
const { modernTokens } = useTheme();
const theme = useMuiTheme();
const getStatusIcon = (type: string, status: string) => {
switch (type) {
@ -90,13 +93,13 @@ const ProcessingTimeline: React.FC<ProcessingTimelineProps> = ({
const getStatusColor = (status: string) => {
switch (status) {
case 'success':
return modernTokens.colors.success[500];
return theme.palette.success.main;
case 'error':
return modernTokens.colors.error[500];
return theme.palette.error.main;
case 'warning':
return modernTokens.colors.warning[500];
return theme.palette.warning.main;
default:
return modernTokens.colors.info[500];
return theme.palette.info.main;
}
};
@ -213,8 +216,8 @@ const ProcessingTimeline: React.FC<ProcessingTimelineProps> = ({
<Paper
sx={{
p: 2,
background: `linear-gradient(135deg, ${modernTokens.colors.neutral[50]} 0%, ${modernTokens.colors.info[50]} 100%)`,
border: `1px solid ${modernTokens.colors.neutral[200]}`,
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
@ -223,7 +226,7 @@ const ProcessingTimeline: React.FC<ProcessingTimelineProps> = ({
sx={{
fontSize: 18,
mr: 1,
color: modernTokens.colors.primary[500]
color: theme.palette.primary.main
}}
/>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
@ -274,8 +277,8 @@ const ProcessingTimeline: React.FC<ProcessingTimelineProps> = ({
<Paper
sx={{
p: 3,
background: `linear-gradient(135deg, ${modernTokens.colors.neutral[50]} 0%, ${modernTokens.colors.info[50]} 100%)`,
border: `1px solid ${modernTokens.colors.neutral[200]}`,
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
}}
>
{/* Header */}
@ -297,7 +300,7 @@ const ProcessingTimeline: React.FC<ProcessingTimelineProps> = ({
<Chip
label={`${events.length} events`}
size="small"
sx={{ backgroundColor: modernTokens.colors.neutral[100] }}
sx={{ backgroundColor: theme.palette.action.hover }}
/>
{ocrRetryCount > 0 && (
<Chip
@ -324,7 +327,7 @@ const ProcessingTimeline: React.FC<ProcessingTimelineProps> = ({
</TimelineDot>
{index < events.length - 1 && (
<TimelineConnector
sx={{ backgroundColor: modernTokens.colors.neutral[300] }}
sx={{ backgroundColor: theme.palette.action.selected }}
/>
)}
</TimelineSeparator>
@ -347,7 +350,7 @@ const ProcessingTimeline: React.FC<ProcessingTimelineProps> = ({
{event.metadata?.userId && (
<Box sx={{ display: 'flex', alignItems: 'center', mt: 1 }}>
<PersonIcon sx={{ fontSize: 14, mr: 0.5, color: modernTokens.colors.neutral[500] }} />
<PersonIcon sx={{ fontSize: 14, mr: 0.5, color: theme.palette.text.secondary }} />
<Typography variant="caption" color="text.secondary">
User: {event.metadata.userId.substring(0, 8)}...
</Typography>
@ -366,7 +369,7 @@ const ProcessingTimeline: React.FC<ProcessingTimelineProps> = ({
{/* Retry History Section */}
{ocrRetryCount > 0 && (
<Box sx={{ mt: 3, pt: 2, borderTop: `1px solid ${modernTokens.colors.neutral[200]}` }}>
<Box sx={{ mt: 3, pt: 2, borderTop: `1px solid ${theme.palette.divider}` }}>
<Button
onClick={() => setExpanded(!expanded)}
endIcon={<ExpandIcon sx={{ transform: expanded ? 'rotate(180deg)' : 'none' }} />}
@ -390,8 +393,8 @@ const ProcessingTimeline: React.FC<ProcessingTimelineProps> = ({
key={retry.id}
sx={{
p: 2,
backgroundColor: modernTokens.colors.neutral[50],
border: `1px solid ${modernTokens.colors.neutral[200]}`,
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.divider}`,
}}
>
<Typography variant="subtitle2">

View File

@ -1,10 +1,13 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { createTheme, Theme, ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
import { PaletteMode } from '@mui/material';
import { modernTokens } from '../theme';
interface ThemeContextType {
mode: PaletteMode;
toggleTheme: () => void;
modernTokens: typeof modernTokens;
glassEffect: (alphaValue?: number) => object;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
@ -21,19 +24,53 @@ interface ThemeProviderProps {
children: ReactNode;
}
// Glassmorphism effect that adapts to theme mode
const createGlassEffect = (mode: PaletteMode) => (alphaValue: number = 0.1) => ({
background: mode === 'light'
? `rgba(255, 255, 255, ${alphaValue})`
: `rgba(30, 30, 30, ${alphaValue})`,
backdropFilter: 'blur(10px)',
border: mode === 'light'
? '1px solid rgba(255, 255, 255, 0.2)'
: '1px solid rgba(255, 255, 255, 0.1)',
boxShadow: mode === 'light'
? modernTokens.shadows.glass
: '0 8px 32px 0 rgba(0, 0, 0, 0.37)',
});
const createAppTheme = (mode: PaletteMode): Theme => {
return createTheme({
palette: {
mode,
primary: {
main: '#667eea',
light: '#9bb5ff',
dark: '#304ffe',
main: modernTokens.colors.primary[500],
light: modernTokens.colors.primary[300],
dark: modernTokens.colors.primary[700],
50: modernTokens.colors.primary[50],
100: modernTokens.colors.primary[100],
200: modernTokens.colors.primary[200],
300: modernTokens.colors.primary[300],
400: modernTokens.colors.primary[400],
500: modernTokens.colors.primary[500],
600: modernTokens.colors.primary[600],
700: modernTokens.colors.primary[700],
800: modernTokens.colors.primary[800],
900: modernTokens.colors.primary[900],
},
secondary: {
main: '#764ba2',
light: '#a777d9',
dark: '#4c1e74',
main: modernTokens.colors.secondary[500],
light: modernTokens.colors.secondary[300],
dark: modernTokens.colors.secondary[700],
50: modernTokens.colors.secondary[50],
100: modernTokens.colors.secondary[100],
200: modernTokens.colors.secondary[200],
300: modernTokens.colors.secondary[300],
400: modernTokens.colors.secondary[400],
500: modernTokens.colors.secondary[500],
600: modernTokens.colors.secondary[600],
700: modernTokens.colors.secondary[700],
800: modernTokens.colors.secondary[800],
900: modernTokens.colors.secondary[900],
},
background: {
default: mode === 'light' ? '#fafafa' : '#121212',
@ -43,6 +80,66 @@ const createAppTheme = (mode: PaletteMode): Theme => {
primary: mode === 'light' ? '#333333' : '#f8fafc',
secondary: mode === 'light' ? '#666666' : '#cbd5e1',
},
success: {
main: modernTokens.colors.success[500],
light: modernTokens.colors.success[50],
dark: modernTokens.colors.success[600],
50: modernTokens.colors.success[50],
100: mode === 'light' ? '#dcfce7' : '#14532d',
200: mode === 'light' ? '#bbf7d0' : '#166534',
300: mode === 'light' ? '#86efac' : '#15803d',
400: mode === 'light' ? '#4ade80' : '#16a34a',
500: modernTokens.colors.success[500],
600: modernTokens.colors.success[600],
700: mode === 'light' ? '#15803d' : '#4ade80',
800: mode === 'light' ? '#166534' : '#86efac',
900: mode === 'light' ? '#14532d' : '#dcfce7',
},
warning: {
main: modernTokens.colors.warning[500],
light: modernTokens.colors.warning[50],
dark: modernTokens.colors.warning[600],
50: modernTokens.colors.warning[50],
100: mode === 'light' ? '#fef3c7' : '#78350f',
200: mode === 'light' ? '#fde68a' : '#92400e',
300: mode === 'light' ? '#fcd34d' : '#b45309',
400: mode === 'light' ? '#fbbf24' : '#d97706',
500: modernTokens.colors.warning[500],
600: modernTokens.colors.warning[600],
700: mode === 'light' ? '#b45309' : '#fbbf24',
800: mode === 'light' ? '#92400e' : '#fcd34d',
900: mode === 'light' ? '#78350f' : '#fef3c7',
},
error: {
main: modernTokens.colors.error[500],
light: modernTokens.colors.error[50],
dark: modernTokens.colors.error[600],
50: modernTokens.colors.error[50],
100: mode === 'light' ? '#fee2e2' : '#7f1d1d',
200: mode === 'light' ? '#fecaca' : '#991b1b',
300: mode === 'light' ? '#fca5a5' : '#b91c1c',
400: mode === 'light' ? '#f87171' : '#dc2626',
500: modernTokens.colors.error[500],
600: modernTokens.colors.error[600],
700: mode === 'light' ? '#b91c1c' : '#f87171',
800: mode === 'light' ? '#991b1b' : '#fca5a5',
900: mode === 'light' ? '#7f1d1d' : '#fee2e2',
},
info: {
main: modernTokens.colors.info[500],
light: modernTokens.colors.info[50],
dark: modernTokens.colors.info[600],
50: modernTokens.colors.info[50],
100: mode === 'light' ? '#dbeafe' : '#1e3a8a',
200: mode === 'light' ? '#bfdbfe' : '#1e40af',
300: mode === 'light' ? '#93c5fd' : '#1d4ed8',
400: mode === 'light' ? '#60a5fa' : '#2563eb',
500: modernTokens.colors.info[500],
600: modernTokens.colors.info[600],
700: mode === 'light' ? '#1d4ed8' : '#60a5fa',
800: mode === 'light' ? '#1e40af' : '#93c5fd',
900: mode === 'light' ? '#1e3a8a' : '#dbeafe',
},
divider: mode === 'light' ? 'rgba(0, 0, 0, 0.12)' : 'rgba(255, 255, 255, 0.12)',
},
typography: {
@ -148,6 +245,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
};
const theme = createAppTheme(mode);
const glassEffect = createGlassEffect(mode);
// Listen for system theme changes
useEffect(() => {
@ -164,7 +262,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
}, []);
return (
<ThemeContext.Provider value={{ mode, toggleTheme }}>
<ThemeContext.Provider value={{ mode, toggleTheme, modernTokens, glassEffect }}>
<MuiThemeProvider theme={theme}>
{children}
</MuiThemeProvider>

View File

@ -56,12 +56,15 @@ import MetadataParser from '../components/MetadataParser';
import FileIntegrityDisplay from '../components/FileIntegrityDisplay';
import ProcessingTimeline from '../components/ProcessingTimeline';
import { RetryHistoryModal } from '../components/RetryHistoryModal';
import { modernTokens, glassEffect } from '../theme';
import { useTheme } from '../contexts/ThemeContext';
import { useTheme as useMuiTheme } from '@mui/material/styles';
import api from '../services/api';
const DocumentDetailsPage: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { mode, modernTokens, glassEffect } = useTheme();
const theme = useMuiTheme();
const [document, setDocument] = useState<Document | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
@ -335,7 +338,7 @@ const DocumentDetailsPage: React.FC = () => {
<Box
sx={{
minHeight: '100vh',
background: `linear-gradient(135deg, ${modernTokens.colors.primary[50]} 0%, ${modernTokens.colors.secondary[50]} 100%)`,
backgroundColor: theme.palette.background.default,
}}
>
<Container maxWidth="xl" sx={{ py: 4 }}>
@ -347,9 +350,9 @@ const DocumentDetailsPage: React.FC = () => {
onClick={() => navigate('/documents')}
sx={{
mb: 3,
color: modernTokens.colors.neutral[600],
color: theme.palette.text.secondary,
'&:hover': {
backgroundColor: modernTokens.colors.neutral[100],
backgroundColor: theme.palette.action.hover,
},
}}
>
@ -358,10 +361,10 @@ const DocumentDetailsPage: React.FC = () => {
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography
variant="h2"
variant="h4"
sx={{
fontWeight: 800,
background: `linear-gradient(135deg, ${modernTokens.colors.primary[600]} 0%, ${modernTokens.colors.secondary[600]} 100%)`,
fontWeight: 700,
background: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.secondary.main} 100%)`,
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
color: 'transparent',
@ -377,11 +380,12 @@ const DocumentDetailsPage: React.FC = () => {
<IconButton
onClick={handleDownload}
sx={{
...glassEffect(0.2),
color: modernTokens.colors.primary[600],
backgroundColor: theme.palette.action.hover,
backdropFilter: 'blur(10px)',
color: theme.palette.primary.main,
'&:hover': {
transform: 'scale(1.05)',
backgroundColor: modernTokens.colors.primary[100],
backgroundColor: theme.palette.primary.light,
},
}}
>
@ -393,11 +397,12 @@ const DocumentDetailsPage: React.FC = () => {
<IconButton
onClick={handleViewDocument}
sx={{
...glassEffect(0.2),
color: modernTokens.colors.primary[600],
backgroundColor: theme.palette.action.hover,
backdropFilter: 'blur(10px)',
color: theme.palette.primary.main,
'&:hover': {
transform: 'scale(1.05)',
backgroundColor: modernTokens.colors.primary[100],
backgroundColor: theme.palette.primary.light,
},
}}
>
@ -410,11 +415,12 @@ const DocumentDetailsPage: React.FC = () => {
<IconButton
onClick={handleViewOcr}
sx={{
...glassEffect(0.2),
color: modernTokens.colors.secondary[600],
backgroundColor: theme.palette.action.hover,
backdropFilter: 'blur(10px)',
color: theme.palette.secondary.main,
'&:hover': {
transform: 'scale(1.05)',
backgroundColor: modernTokens.colors.secondary[100],
backgroundColor: theme.palette.secondary.light,
},
}}
>
@ -438,9 +444,9 @@ const DocumentDetailsPage: React.FC = () => {
<Grid item xs={12} lg={5}>
<Card
sx={{
...glassEffect(0.3),
backgroundColor: theme.palette.background.paper,
backdropFilter: 'blur(10px)',
height: 'fit-content',
background: `linear-gradient(135deg, ${modernTokens.colors.neutral[0]} 0%, ${modernTokens.colors.primary[50]} 100%)`,
}}
>
<CardContent sx={{ p: 4 }}>
@ -452,7 +458,7 @@ const DocumentDetailsPage: React.FC = () => {
justifyContent: 'center',
mb: 4,
p: 4,
background: `linear-gradient(135deg, ${modernTokens.colors.primary[100]} 0%, ${modernTokens.colors.secondary[100]} 100%)`,
background: `linear-gradient(135deg, ${theme.palette.primary.light} 0%, ${theme.palette.secondary.light} 100%)`,
borderRadius: 3,
minHeight: 280,
position: 'relative',
@ -481,15 +487,15 @@ const DocumentDetailsPage: React.FC = () => {
objectFit: 'contain',
cursor: 'pointer',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: modernTokens.shadows.lg,
boxShadow: theme.shadows[8],
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'scale(1.05) rotateY(5deg)';
e.currentTarget.style.boxShadow = modernTokens.shadows.xl;
e.currentTarget.style.boxShadow = theme.shadows[12];
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'scale(1) rotateY(0deg)';
e.currentTarget.style.boxShadow = modernTokens.shadows.lg;
e.currentTarget.style.boxShadow = theme.shadows[8];
}}
/>
) : (
@ -503,7 +509,7 @@ const DocumentDetailsPage: React.FC = () => {
}
}}
>
<Box sx={{ fontSize: 120, color: modernTokens.colors.primary[400], display: 'flex' }}>
<Box sx={{ fontSize: 120, color: theme.palette.primary.main, display: 'flex' }}>
{getFileIcon(document.mime_type)}
</Box>
</Box>
@ -515,10 +521,10 @@ const DocumentDetailsPage: React.FC = () => {
<Chip
label={document.mime_type}
sx={{
backgroundColor: modernTokens.colors.primary[100],
color: modernTokens.colors.primary[700],
backgroundColor: theme.palette.primary.light,
color: theme.palette.primary.dark,
fontWeight: 600,
border: `1px solid ${modernTokens.colors.primary[300]}`,
border: `1px solid ${theme.palette.primary.main}`,
}}
/>
</Box>
@ -566,10 +572,10 @@ const DocumentDetailsPage: React.FC = () => {
onClick={handleViewProcessedImage}
disabled={processedImageLoading}
sx={{
backgroundColor: modernTokens.colors.secondary[100],
color: modernTokens.colors.secondary[600],
backgroundColor: theme.palette.secondary.light,
color: theme.palette.secondary.dark,
'&:hover': {
backgroundColor: modernTokens.colors.secondary[200],
backgroundColor: theme.palette.secondary[200],
transform: 'scale(1.1)',
},
}}
@ -588,10 +594,10 @@ const DocumentDetailsPage: React.FC = () => {
onClick={handleRetryOcr}
disabled={retryingOcr}
sx={{
backgroundColor: modernTokens.colors.warning[100],
color: modernTokens.colors.warning[600],
backgroundColor: theme.palette.warning.light,
color: theme.palette.warning.dark,
'&:hover': {
backgroundColor: modernTokens.colors.warning[200],
backgroundColor: theme.palette.warning[200],
transform: 'scale(1.1)',
},
}}
@ -608,10 +614,10 @@ const DocumentDetailsPage: React.FC = () => {
<IconButton
onClick={handleShowRetryHistory}
sx={{
backgroundColor: modernTokens.colors.info[100],
color: modernTokens.colors.info[600],
backgroundColor: theme.palette.info.light,
color: theme.palette.info.dark,
'&:hover': {
backgroundColor: modernTokens.colors.info[200],
backgroundColor: theme.palette.info[200],
transform: 'scale(1.1)',
},
}}
@ -622,21 +628,194 @@ const DocumentDetailsPage: React.FC = () => {
</Stack>
</CardContent>
</Card>
{/* File Integrity Display - Moved here */}
<Box sx={{ mt: 3 }}>
<FileIntegrityDisplay
fileHash={document.file_hash}
fileName={document.original_filename}
fileSize={document.file_size}
mimeType={document.mime_type}
createdAt={document.created_at}
updatedAt={document.updated_at}
userId={document.user_id}
/>
</Box>
</Grid>
{/* Main Content Area */}
<Grid item xs={12} lg={7}>
<Stack spacing={4}>
{/* File Integrity Display */}
<FileIntegrityDisplay
fileHash={document.file_hash}
fileName={document.original_filename}
fileSize={document.file_size}
mimeType={document.mime_type}
createdAt={document.created_at}
updatedAt={document.updated_at}
userId={document.user_id}
/>
<Stack spacing={4}>
{/* OCR Text Section - Moved higher */}
{document.has_ocr_text && (
<Card
sx={{
backgroundColor: theme.palette.background.paper,
backdropFilter: 'blur(10px)',
}}
>
<CardContent sx={{ p: 4 }}>
<Typography variant="h5" sx={{ mb: 3, fontWeight: 700 }}>
🔍 Extracted Text (OCR)
</Typography>
{ocrLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 6 }}>
<CircularProgress size={32} sx={{ mr: 2 }} />
<Typography variant="h6" color="text.secondary">
Loading OCR analysis...
</Typography>
</Box>
) : ocrData ? (
<>
{/* Enhanced OCR Stats */}
<Box sx={{ mb: 4, display: 'flex', gap: 2, flexWrap: 'wrap' }}>
{ocrData.ocr_confidence && (
<Box
sx={{
p: 2,
borderRadius: 2,
backgroundColor: mode === 'light' ? modernTokens.colors.primary[100] : modernTokens.colors.primary[800],
border: `1px solid ${mode === 'light' ? modernTokens.colors.primary[300] : modernTokens.colors.primary[600]}`,
textAlign: 'center',
minWidth: 120,
}}
>
<Typography variant="h5" sx={{ fontWeight: 700, color: mode === 'light' ? modernTokens.colors.primary[700] : modernTokens.colors.primary[300] }}>
{Math.round(ocrData.ocr_confidence)}%
</Typography>
<Typography variant="caption" color="text.secondary">
Confidence
</Typography>
</Box>
)}
{ocrData.ocr_word_count && (
<Box
sx={{
p: 2,
borderRadius: 2,
backgroundColor: mode === 'light' ? modernTokens.colors.secondary[100] : modernTokens.colors.secondary[800],
border: `1px solid ${mode === 'light' ? modernTokens.colors.secondary[300] : modernTokens.colors.secondary[600]}`,
textAlign: 'center',
minWidth: 120,
}}
>
<Typography variant="h5" sx={{ fontWeight: 700, color: mode === 'light' ? modernTokens.colors.secondary[700] : modernTokens.colors.secondary[300] }}>
{ocrData.ocr_word_count.toLocaleString()}
</Typography>
<Typography variant="caption" color="text.secondary">
Words
</Typography>
</Box>
)}
{ocrData.ocr_processing_time_ms && (
<Box
sx={{
p: 2,
borderRadius: 2,
backgroundColor: mode === 'light' ? modernTokens.colors.info[100] : modernTokens.colors.info[800],
border: `1px solid ${mode === 'light' ? modernTokens.colors.info[300] : modernTokens.colors.info[600]}`,
textAlign: 'center',
minWidth: 120,
}}
>
<Typography variant="h5" sx={{ fontWeight: 700, color: mode === 'light' ? modernTokens.colors.info[700] : modernTokens.colors.info[300] }}>
{ocrData.ocr_processing_time_ms}ms
</Typography>
<Typography variant="caption" color="text.secondary">
Processing Time
</Typography>
</Box>
)}
</Box>
{/* OCR Error Display */}
{ocrData.ocr_error && (
<Alert
severity="error"
sx={{
mb: 3,
borderRadius: 2,
}}
>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
OCR Processing Error
</Typography>
<Typography variant="body2">{ocrData.ocr_error}</Typography>
</Alert>
)}
{/* Full OCR Text Display */}
<Paper
sx={{
p: 4,
backgroundColor: theme.palette.background.default,
borderRadius: 3,
maxHeight: 400,
overflow: 'auto',
// Custom scrollbar styling
'&::-webkit-scrollbar': {
width: '8px',
},
'&::-webkit-scrollbar-track': {
backgroundColor: mode === 'light' ? modernTokens.colors.neutral[100] : modernTokens.colors.neutral[800],
borderRadius: '4px',
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: mode === 'light' ? modernTokens.colors.neutral[300] : modernTokens.colors.neutral[600],
borderRadius: '4px',
'&:hover': {
backgroundColor: mode === 'light' ? modernTokens.colors.neutral[400] : modernTokens.colors.neutral[500],
},
},
// Firefox scrollbar styling
scrollbarWidth: 'thin',
scrollbarColor: mode === 'light'
? `${modernTokens.colors.neutral[300]} ${modernTokens.colors.neutral[100]}`
: `${modernTokens.colors.neutral[600]} ${modernTokens.colors.neutral[800]}`,
}}
>
{ocrData.ocr_text ? (
<Typography
variant="body1"
sx={{
fontFamily: '"Inter", monospace',
whiteSpace: 'pre-wrap',
lineHeight: 1.8,
fontSize: '0.95rem',
}}
>
{ocrData.ocr_text}
</Typography>
) : (
<Typography variant="body1" color="text.secondary" sx={{ fontStyle: 'italic', textAlign: 'center', py: 4 }}>
No OCR text available for this document.
</Typography>
)}
</Paper>
{/* Processing Info */}
{ocrData.ocr_completed_at && (
<Box sx={{ mt: 3, pt: 3, borderTop: `1px solid ${theme.palette.divider}` }}>
<Typography variant="body2" color="text.secondary">
Processing completed: {new Date(ocrData.ocr_completed_at).toLocaleString()}
</Typography>
</Box>
)}
</>
) : (
<Alert
severity="info"
sx={{
borderRadius: 2,
}}
>
OCR text is available but failed to load. Please try refreshing the page.
</Alert>
)}
</CardContent>
</Card>
)}
{/* Processing Timeline */}
<ProcessingTimeline
@ -654,8 +833,8 @@ const DocumentDetailsPage: React.FC = () => {
{document.source_metadata && Object.keys(document.source_metadata).length > 0 && (
<Card
sx={{
...glassEffect(0.2),
background: `linear-gradient(135deg, ${modernTokens.colors.neutral[0]} 0%, ${modernTokens.colors.info[50]} 100%)`,
backgroundColor: theme.palette.background.paper,
backdropFilter: 'blur(10px)',
}}
>
<CardContent sx={{ p: 4 }}>
@ -673,8 +852,8 @@ const DocumentDetailsPage: React.FC = () => {
{/* Tags and Labels */}
<Card
sx={{
...glassEffect(0.2),
background: `linear-gradient(135deg, ${modernTokens.colors.neutral[0]} 0%, ${modernTokens.colors.secondary[50]} 100%)`,
backgroundColor: theme.palette.background.paper,
backdropFilter: 'blur(10px)',
}}
>
<CardContent sx={{ p: 4 }}>
@ -686,10 +865,10 @@ const DocumentDetailsPage: React.FC = () => {
startIcon={<EditIcon />}
onClick={() => setShowLabelDialog(true)}
sx={{
backgroundColor: modernTokens.colors.secondary[100],
color: modernTokens.colors.secondary[700],
backgroundColor: theme.palette.secondary.light,
color: theme.palette.secondary.dark,
'&:hover': {
backgroundColor: modernTokens.colors.secondary[200],
backgroundColor: theme.palette.secondary[200],
},
}}
>
@ -709,9 +888,9 @@ const DocumentDetailsPage: React.FC = () => {
key={index}
label={tag}
sx={{
backgroundColor: modernTokens.colors.primary[100],
color: modernTokens.colors.primary[700],
border: `1px solid ${modernTokens.colors.primary[300]}`,
backgroundColor: theme.palette.primary.light,
color: theme.palette.primary.dark,
border: `1px solid ${theme.palette.primary.main}`,
fontWeight: 500,
}}
/>
@ -753,212 +932,6 @@ const DocumentDetailsPage: React.FC = () => {
</Grid>
</Fade>
{/* OCR Text Section */}
{document.has_ocr_text && (
<Fade in timeout={1000}>
<Card
sx={{
mt: 4,
...glassEffect(0.2),
background: `linear-gradient(135deg, ${modernTokens.colors.neutral[0]} 0%, ${modernTokens.colors.success[50]} 100%)`,
}}
>
<CardContent sx={{ p: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Typography variant="h4" sx={{ fontWeight: 700 }}>
🔍 Extracted Text (OCR)
</Typography>
<Button
startIcon={<SpeedIcon />}
onClick={handleViewOcr}
sx={{
backgroundColor: modernTokens.colors.success[100],
color: modernTokens.colors.success[700],
'&:hover': {
backgroundColor: modernTokens.colors.success[200],
},
}}
>
View Full Text
</Button>
</Box>
{ocrLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 6 }}>
<CircularProgress size={32} sx={{ mr: 2 }} />
<Typography variant="h6" color="text.secondary">
Loading OCR analysis...
</Typography>
</Box>
) : ocrData ? (
<>
{/* Enhanced OCR Stats */}
<Box sx={{ mb: 4, display: 'flex', gap: 2, flexWrap: 'wrap' }}>
{ocrData.ocr_confidence && (
<Box
sx={{
p: 2,
borderRadius: 2,
backgroundColor: modernTokens.colors.primary[100],
border: `1px solid ${modernTokens.colors.primary[300]}`,
textAlign: 'center',
minWidth: 120,
}}
>
<Typography variant="h5" sx={{ fontWeight: 700, color: modernTokens.colors.primary[700] }}>
{Math.round(ocrData.ocr_confidence)}%
</Typography>
<Typography variant="caption" color="text.secondary">
Confidence
</Typography>
</Box>
)}
{ocrData.ocr_word_count && (
<Box
sx={{
p: 2,
borderRadius: 2,
backgroundColor: modernTokens.colors.secondary[100],
border: `1px solid ${modernTokens.colors.secondary[300]}`,
textAlign: 'center',
minWidth: 120,
}}
>
<Typography variant="h5" sx={{ fontWeight: 700, color: modernTokens.colors.secondary[700] }}>
{ocrData.ocr_word_count.toLocaleString()}
</Typography>
<Typography variant="caption" color="text.secondary">
Words
</Typography>
</Box>
)}
{ocrData.ocr_processing_time_ms && (
<Box
sx={{
p: 2,
borderRadius: 2,
backgroundColor: modernTokens.colors.info[100],
border: `1px solid ${modernTokens.colors.info[300]}`,
textAlign: 'center',
minWidth: 120,
}}
>
<Typography variant="h5" sx={{ fontWeight: 700, color: modernTokens.colors.info[700] }}>
{ocrData.ocr_processing_time_ms}ms
</Typography>
<Typography variant="caption" color="text.secondary">
Processing Time
</Typography>
</Box>
)}
</Box>
{/* OCR Error Display */}
{ocrData.ocr_error && (
<Alert
severity="error"
sx={{
mb: 3,
borderRadius: 2,
backgroundColor: modernTokens.colors.error[50],
border: `1px solid ${modernTokens.colors.error[200]}`,
}}
>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
OCR Processing Error
</Typography>
<Typography variant="body2">{ocrData.ocr_error}</Typography>
</Alert>
)}
{/* OCR Text Preview */}
<Paper
sx={{
p: 4,
background: `linear-gradient(135deg, ${modernTokens.colors.neutral[50]} 0%, ${modernTokens.colors.neutral[100]} 100%)`,
border: `1px solid ${modernTokens.colors.neutral[300]}`,
borderRadius: 3,
maxHeight: 300,
overflow: 'auto',
position: 'relative',
}}
>
{ocrData.ocr_text ? (
<Typography
variant="body1"
sx={{
fontFamily: '"Inter", monospace',
whiteSpace: 'pre-wrap',
lineHeight: 1.8,
color: modernTokens.colors.neutral[800],
fontSize: '0.95rem',
}}
>
{ocrData.ocr_text.length > 500
? `${ocrData.ocr_text.substring(0, 500)}...`
: ocrData.ocr_text
}
</Typography>
) : (
<Typography variant="body1" color="text.secondary" sx={{ fontStyle: 'italic', textAlign: 'center', py: 4 }}>
No OCR text available for this document.
</Typography>
)}
{ocrData.ocr_text && ocrData.ocr_text.length > 500 && (
<Box
sx={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 60,
background: `linear-gradient(transparent, ${modernTokens.colors.neutral[100]})`,
display: 'flex',
alignItems: 'end',
justifyContent: 'center',
pb: 2,
}}
>
<Button
onClick={handleViewOcr}
size="small"
sx={{
backgroundColor: modernTokens.colors.neutral[0],
boxShadow: modernTokens.shadows.sm,
}}
>
View Full Text
</Button>
</Box>
)}
</Paper>
{/* Processing Info */}
{ocrData.ocr_completed_at && (
<Box sx={{ mt: 3, pt: 3, borderTop: `1px solid ${modernTokens.colors.neutral[200]}` }}>
<Typography variant="body2" color="text.secondary">
Processing completed: {new Date(ocrData.ocr_completed_at).toLocaleString()}
</Typography>
</Box>
)}
</>
) : (
<Alert
severity="info"
sx={{
borderRadius: 2,
backgroundColor: modernTokens.colors.info[50],
border: `1px solid ${modernTokens.colors.info[200]}`,
}}
>
OCR text is available but failed to load. Try clicking the "View Full Text" button above.
</Alert>
)}
</CardContent>
</Card>
</Fade>
)}
</Container>
{/* OCR Text Dialog */}

View File

@ -43,23 +43,51 @@ export const modernTokens = {
},
success: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
},
warning: {
50: '#fffbeb',
100: '#fef3c7',
200: '#fde68a',
300: '#fcd34d',
400: '#fbbf24',
500: '#f59e0b',
600: '#d97706',
700: '#b45309',
800: '#92400e',
900: '#78350f',
},
error: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
},
info: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
},
},
shadows: {