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

View File

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

View File

@ -1,10 +1,13 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { createTheme, Theme, ThemeProvider as MuiThemeProvider } from '@mui/material/styles'; import { createTheme, Theme, ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
import { PaletteMode } from '@mui/material'; import { PaletteMode } from '@mui/material';
import { modernTokens } from '../theme';
interface ThemeContextType { interface ThemeContextType {
mode: PaletteMode; mode: PaletteMode;
toggleTheme: () => void; toggleTheme: () => void;
modernTokens: typeof modernTokens;
glassEffect: (alphaValue?: number) => object;
} }
const ThemeContext = createContext<ThemeContextType | undefined>(undefined); const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
@ -21,19 +24,53 @@ interface ThemeProviderProps {
children: ReactNode; 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 => { const createAppTheme = (mode: PaletteMode): Theme => {
return createTheme({ return createTheme({
palette: { palette: {
mode, mode,
primary: { primary: {
main: '#667eea', main: modernTokens.colors.primary[500],
light: '#9bb5ff', light: modernTokens.colors.primary[300],
dark: '#304ffe', 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: { secondary: {
main: '#764ba2', main: modernTokens.colors.secondary[500],
light: '#a777d9', light: modernTokens.colors.secondary[300],
dark: '#4c1e74', 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: { background: {
default: mode === 'light' ? '#fafafa' : '#121212', default: mode === 'light' ? '#fafafa' : '#121212',
@ -43,6 +80,66 @@ const createAppTheme = (mode: PaletteMode): Theme => {
primary: mode === 'light' ? '#333333' : '#f8fafc', primary: mode === 'light' ? '#333333' : '#f8fafc',
secondary: mode === 'light' ? '#666666' : '#cbd5e1', 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)', divider: mode === 'light' ? 'rgba(0, 0, 0, 0.12)' : 'rgba(255, 255, 255, 0.12)',
}, },
typography: { typography: {
@ -148,6 +245,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
}; };
const theme = createAppTheme(mode); const theme = createAppTheme(mode);
const glassEffect = createGlassEffect(mode);
// Listen for system theme changes // Listen for system theme changes
useEffect(() => { useEffect(() => {
@ -164,7 +262,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
}, []); }, []);
return ( return (
<ThemeContext.Provider value={{ mode, toggleTheme }}> <ThemeContext.Provider value={{ mode, toggleTheme, modernTokens, glassEffect }}>
<MuiThemeProvider theme={theme}> <MuiThemeProvider theme={theme}>
{children} {children}
</MuiThemeProvider> </MuiThemeProvider>

View File

@ -56,12 +56,15 @@ import MetadataParser from '../components/MetadataParser';
import FileIntegrityDisplay from '../components/FileIntegrityDisplay'; import FileIntegrityDisplay from '../components/FileIntegrityDisplay';
import ProcessingTimeline from '../components/ProcessingTimeline'; import ProcessingTimeline from '../components/ProcessingTimeline';
import { RetryHistoryModal } from '../components/RetryHistoryModal'; 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'; import api from '../services/api';
const DocumentDetailsPage: React.FC = () => { const DocumentDetailsPage: React.FC = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const { mode, modernTokens, glassEffect } = useTheme();
const theme = useMuiTheme();
const [document, setDocument] = useState<Document | null>(null); const [document, setDocument] = useState<Document | null>(null);
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -335,7 +338,7 @@ const DocumentDetailsPage: React.FC = () => {
<Box <Box
sx={{ sx={{
minHeight: '100vh', 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 }}> <Container maxWidth="xl" sx={{ py: 4 }}>
@ -347,9 +350,9 @@ const DocumentDetailsPage: React.FC = () => {
onClick={() => navigate('/documents')} onClick={() => navigate('/documents')}
sx={{ sx={{
mb: 3, mb: 3,
color: modernTokens.colors.neutral[600], color: theme.palette.text.secondary,
'&:hover': { '&: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 }}> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography <Typography
variant="h2" variant="h4"
sx={{ sx={{
fontWeight: 800, fontWeight: 700,
background: `linear-gradient(135deg, ${modernTokens.colors.primary[600]} 0%, ${modernTokens.colors.secondary[600]} 100%)`, background: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.secondary.main} 100%)`,
backgroundClip: 'text', backgroundClip: 'text',
WebkitBackgroundClip: 'text', WebkitBackgroundClip: 'text',
color: 'transparent', color: 'transparent',
@ -377,11 +380,12 @@ const DocumentDetailsPage: React.FC = () => {
<IconButton <IconButton
onClick={handleDownload} onClick={handleDownload}
sx={{ sx={{
...glassEffect(0.2), backgroundColor: theme.palette.action.hover,
color: modernTokens.colors.primary[600], backdropFilter: 'blur(10px)',
color: theme.palette.primary.main,
'&:hover': { '&:hover': {
transform: 'scale(1.05)', transform: 'scale(1.05)',
backgroundColor: modernTokens.colors.primary[100], backgroundColor: theme.palette.primary.light,
}, },
}} }}
> >
@ -393,11 +397,12 @@ const DocumentDetailsPage: React.FC = () => {
<IconButton <IconButton
onClick={handleViewDocument} onClick={handleViewDocument}
sx={{ sx={{
...glassEffect(0.2), backgroundColor: theme.palette.action.hover,
color: modernTokens.colors.primary[600], backdropFilter: 'blur(10px)',
color: theme.palette.primary.main,
'&:hover': { '&:hover': {
transform: 'scale(1.05)', transform: 'scale(1.05)',
backgroundColor: modernTokens.colors.primary[100], backgroundColor: theme.palette.primary.light,
}, },
}} }}
> >
@ -410,11 +415,12 @@ const DocumentDetailsPage: React.FC = () => {
<IconButton <IconButton
onClick={handleViewOcr} onClick={handleViewOcr}
sx={{ sx={{
...glassEffect(0.2), backgroundColor: theme.palette.action.hover,
color: modernTokens.colors.secondary[600], backdropFilter: 'blur(10px)',
color: theme.palette.secondary.main,
'&:hover': { '&:hover': {
transform: 'scale(1.05)', 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}> <Grid item xs={12} lg={5}>
<Card <Card
sx={{ sx={{
...glassEffect(0.3), backgroundColor: theme.palette.background.paper,
backdropFilter: 'blur(10px)',
height: 'fit-content', height: 'fit-content',
background: `linear-gradient(135deg, ${modernTokens.colors.neutral[0]} 0%, ${modernTokens.colors.primary[50]} 100%)`,
}} }}
> >
<CardContent sx={{ p: 4 }}> <CardContent sx={{ p: 4 }}>
@ -452,7 +458,7 @@ const DocumentDetailsPage: React.FC = () => {
justifyContent: 'center', justifyContent: 'center',
mb: 4, mb: 4,
p: 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, borderRadius: 3,
minHeight: 280, minHeight: 280,
position: 'relative', position: 'relative',
@ -481,15 +487,15 @@ const DocumentDetailsPage: React.FC = () => {
objectFit: 'contain', objectFit: 'contain',
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: modernTokens.shadows.lg, boxShadow: theme.shadows[8],
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.transform = 'scale(1.05) rotateY(5deg)'; 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) => { onMouseLeave={(e) => {
e.currentTarget.style.transform = 'scale(1) rotateY(0deg)'; 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)} {getFileIcon(document.mime_type)}
</Box> </Box>
</Box> </Box>
@ -515,10 +521,10 @@ const DocumentDetailsPage: React.FC = () => {
<Chip <Chip
label={document.mime_type} label={document.mime_type}
sx={{ sx={{
backgroundColor: modernTokens.colors.primary[100], backgroundColor: theme.palette.primary.light,
color: modernTokens.colors.primary[700], color: theme.palette.primary.dark,
fontWeight: 600, fontWeight: 600,
border: `1px solid ${modernTokens.colors.primary[300]}`, border: `1px solid ${theme.palette.primary.main}`,
}} }}
/> />
</Box> </Box>
@ -566,10 +572,10 @@ const DocumentDetailsPage: React.FC = () => {
onClick={handleViewProcessedImage} onClick={handleViewProcessedImage}
disabled={processedImageLoading} disabled={processedImageLoading}
sx={{ sx={{
backgroundColor: modernTokens.colors.secondary[100], backgroundColor: theme.palette.secondary.light,
color: modernTokens.colors.secondary[600], color: theme.palette.secondary.dark,
'&:hover': { '&:hover': {
backgroundColor: modernTokens.colors.secondary[200], backgroundColor: theme.palette.secondary[200],
transform: 'scale(1.1)', transform: 'scale(1.1)',
}, },
}} }}
@ -588,10 +594,10 @@ const DocumentDetailsPage: React.FC = () => {
onClick={handleRetryOcr} onClick={handleRetryOcr}
disabled={retryingOcr} disabled={retryingOcr}
sx={{ sx={{
backgroundColor: modernTokens.colors.warning[100], backgroundColor: theme.palette.warning.light,
color: modernTokens.colors.warning[600], color: theme.palette.warning.dark,
'&:hover': { '&:hover': {
backgroundColor: modernTokens.colors.warning[200], backgroundColor: theme.palette.warning[200],
transform: 'scale(1.1)', transform: 'scale(1.1)',
}, },
}} }}
@ -608,10 +614,10 @@ const DocumentDetailsPage: React.FC = () => {
<IconButton <IconButton
onClick={handleShowRetryHistory} onClick={handleShowRetryHistory}
sx={{ sx={{
backgroundColor: modernTokens.colors.info[100], backgroundColor: theme.palette.info.light,
color: modernTokens.colors.info[600], color: theme.palette.info.dark,
'&:hover': { '&:hover': {
backgroundColor: modernTokens.colors.info[200], backgroundColor: theme.palette.info[200],
transform: 'scale(1.1)', transform: 'scale(1.1)',
}, },
}} }}
@ -622,21 +628,194 @@ const DocumentDetailsPage: React.FC = () => {
</Stack> </Stack>
</CardContent> </CardContent>
</Card> </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> </Grid>
{/* Main Content Area */} {/* Main Content Area */}
<Grid item xs={12} lg={7}> <Grid item xs={12} lg={7}>
<Stack spacing={4}> <Stack spacing={4}>
{/* File Integrity Display */} {/* OCR Text Section - Moved higher */}
<FileIntegrityDisplay {document.has_ocr_text && (
fileHash={document.file_hash} <Card
fileName={document.original_filename} sx={{
fileSize={document.file_size} backgroundColor: theme.palette.background.paper,
mimeType={document.mime_type} backdropFilter: 'blur(10px)',
createdAt={document.created_at} }}
updatedAt={document.updated_at} >
userId={document.user_id} <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 */} {/* Processing Timeline */}
<ProcessingTimeline <ProcessingTimeline
@ -654,8 +833,8 @@ const DocumentDetailsPage: React.FC = () => {
{document.source_metadata && Object.keys(document.source_metadata).length > 0 && ( {document.source_metadata && Object.keys(document.source_metadata).length > 0 && (
<Card <Card
sx={{ sx={{
...glassEffect(0.2), backgroundColor: theme.palette.background.paper,
background: `linear-gradient(135deg, ${modernTokens.colors.neutral[0]} 0%, ${modernTokens.colors.info[50]} 100%)`, backdropFilter: 'blur(10px)',
}} }}
> >
<CardContent sx={{ p: 4 }}> <CardContent sx={{ p: 4 }}>
@ -673,8 +852,8 @@ const DocumentDetailsPage: React.FC = () => {
{/* Tags and Labels */} {/* Tags and Labels */}
<Card <Card
sx={{ sx={{
...glassEffect(0.2), backgroundColor: theme.palette.background.paper,
background: `linear-gradient(135deg, ${modernTokens.colors.neutral[0]} 0%, ${modernTokens.colors.secondary[50]} 100%)`, backdropFilter: 'blur(10px)',
}} }}
> >
<CardContent sx={{ p: 4 }}> <CardContent sx={{ p: 4 }}>
@ -686,10 +865,10 @@ const DocumentDetailsPage: React.FC = () => {
startIcon={<EditIcon />} startIcon={<EditIcon />}
onClick={() => setShowLabelDialog(true)} onClick={() => setShowLabelDialog(true)}
sx={{ sx={{
backgroundColor: modernTokens.colors.secondary[100], backgroundColor: theme.palette.secondary.light,
color: modernTokens.colors.secondary[700], color: theme.palette.secondary.dark,
'&:hover': { '&:hover': {
backgroundColor: modernTokens.colors.secondary[200], backgroundColor: theme.palette.secondary[200],
}, },
}} }}
> >
@ -709,9 +888,9 @@ const DocumentDetailsPage: React.FC = () => {
key={index} key={index}
label={tag} label={tag}
sx={{ sx={{
backgroundColor: modernTokens.colors.primary[100], backgroundColor: theme.palette.primary.light,
color: modernTokens.colors.primary[700], color: theme.palette.primary.dark,
border: `1px solid ${modernTokens.colors.primary[300]}`, border: `1px solid ${theme.palette.primary.main}`,
fontWeight: 500, fontWeight: 500,
}} }}
/> />
@ -753,212 +932,6 @@ const DocumentDetailsPage: React.FC = () => {
</Grid> </Grid>
</Fade> </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> </Container>
{/* OCR Text Dialog */} {/* OCR Text Dialog */}

View File

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