Aplicación Web - Visión General
La aplicación web React de IRIS proporciona una interfaz intuitiva y moderna para interactuar con el sistema de OCR. Permite a los usuarios subir imágenes, ejecutar el pipeline de procesamiento y gestionar los resultados de manera visual y eficiente.
🎯 Características Principales
Funcionalidades Core
- 📤 Subida de Imágenes: Drag & drop, selección múltiple, preview en tiempo real
- ⚙️ Configuración de Pipeline: Selección de fases, parámetros personalizados
- 📊 Monitoreo en Tiempo Real: Progreso de procesamiento, logs en vivo
- 📋 Gestión de Resultados: Visualización, descarga, comparación de resultados
- 🔍 Búsqueda y Filtros: Búsqueda avanzada por tipo, fecha, estado
- 📈 Analytics: Estadísticas de uso, métricas de rendimiento
Experiencia de Usuario
- 🎨 Interfaz Moderna: Material Design con tema claro/oscuro
- 📱 Responsive Design: Optimizada para desktop, tablet y móvil
- ♿ Accesibilidad: Cumple estándares WCAG 2.1 AA
- 🌐 Internacionalización: Soporte para español e inglés
- ⚡ Rendimiento: Lazy loading, optimización de imágenes, cache inteligente
🏗️ Arquitectura Frontend
Stack Tecnológico
Tecnologías Utilizadas
Core Framework
- React 18: Biblioteca para interfaces de usuario con Concurrent Features
- TypeScript: Tipado estático para mejor desarrollo y mantenimiento
- Vite: Build tool rápido con Hot Module Replacement
- ESLint + Prettier: Linting y formateo de código automático
Estado y Datos
- React Query (TanStack Query): Gestión de estado servidor optimizada
- Context API: Estado global para autenticación y configuración
- Zustand: Estado cliente liviano para UI local
- Immer: Actualizaciones inmutables de estado
UI y Styling
- Material-UI (MUI): Componentes React con Material Design
- Emotion: CSS-in-JS para styling dinámico
- React Hook Form: Gestión de formularios con validación
- Framer Motion: Animaciones fluidas y transiciones
Utilidades
- Axios: Cliente HTTP con interceptors automáticos
- date-fns: Manipulación de fechas ligera y moderna
- react-dropzone: Componente drag & drop para archivos
- react-pdf: Visualización de PDFs generados
🎨 Componentes Principales
Dashboard Principal
const Dashboard = () => {
const { data: stats } = useQuery('dashboard-stats', fetchStats);
const { user } = useAuth();
return (
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<StatCard
title="Documentos Procesados"
value={stats?.totalProcessed}
icon={<DocumentIcon />}
/>
</Grid>
{/* Más estadísticas */}
<Grid item xs={12}>
<RecentProcessing />
</Grid>
</Grid>
);
};
Componente de Subida de Archivos
const ImageUploader = ({ onUpload }) => {
const [files, setFiles] = useState([]);
const uploadMutation = useMutation(uploadFiles);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
accept: {
'image/*': ['.jpeg', '.jpg', '.png', '.tiff']
},
maxFiles: 10,
maxSize: 50 * 1024 * 1024, // 50MB
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map(file =>
Object.assign(file, {
preview: URL.createObjectURL(file),
id: uuidv4()
})
));
}
});
return (
<Box
{...getRootProps()}
sx={{
border: 2,
borderColor: isDragActive ? 'primary.main' : 'grey.300',
borderStyle: 'dashed',
borderRadius: 2,
p: 3,
textAlign: 'center',
cursor: 'pointer',
transition: 'border-color 0.2s ease'
}}
>
<input {...getInputProps()} />
<CloudUploadIcon sx={{ fontSize: 48, color: 'grey.400' }} />
<Typography variant="h6" gutterBottom>
{isDragActive ?
'Suelta las imágenes aquí...' :
'Arrastra imágenes o haz click para seleccionar'
}
</Typography>
{files.length > 0 && (
<ImagePreviewGrid files={files} onRemove={removeFile} />
)}
</Box>
);
};
Monitor de Procesamiento
const ProcessingMonitor = ({ processId }) => {
const { data: status, isLoading } = useQuery(
['processing-status', processId],
() => fetchProcessingStatus(processId),
{
refetchInterval: 2000,
enabled: !!processId
}
);
return (
<Card>
<CardHeader
title="Estado del Procesamiento"
action={
<Chip
label={status?.phase || 'Iniciando...'}
color="primary"
variant="outlined"
/>
}
/>
<CardContent>
<LinearProgress
variant="determinate"
value={status?.progress || 0}
sx={{ mb: 2 }}
/>
<Stepper activeStep={status?.currentPhase || 0} orientation="vertical">
{PROCESSING_PHASES.map((phase, index) => (
<Step key={phase.name}>
<StepLabel
optional={phase.description}
error={status?.errors?.[index]}
>
{phase.name}
</StepLabel>
<StepContent>
<Typography variant="body2">
{status?.phaseDetails?.[index]?.message}
</Typography>
{status?.phaseDetails?.[index]?.duration && (
<Chip
size="small"
label={`${status.phaseDetails[index].duration}ms`}
/>
)}
</StepContent>
</Step>
))}
</Stepper>
</CardContent>
</Card>
);
};
Visualizador de Resultados
const ResultsViewer = ({ results }) => {
const [selectedTab, setSelectedTab] = useState(0);
return (
<Box>
<Tabs value={selectedTab} onChange={(e, v) => setSelectedTab(v)}>
<Tab label="JSON Extraído" />
<Tab label="Imagen Original" />
<Tab label="Imagen Procesada" />
<Tab label="Métricas" />
</Tabs>
<TabPanel value={selectedTab} index={0}>
<JsonViewer
data={results.extractedData}
theme="vs-dark"
expandLevel={2}
/>
</TabPanel>
<TabPanel value={selectedTab} index={1}>
<ImageViewer
src={results.originalImage}
annotations={results.detectedRegions}
/>
</TabPanel>
<TabPanel value={selectedTab} index={2}>
<ImageComparison
before={results.originalImage}
after={results.processedImage}
/>
</TabPanel>
<TabPanel value={selectedTab} index={3}>
<MetricsPanel metrics={results.processingMetrics} />
</TabPanel>
</Box>
);
};
🔄 Gestión de Estado
Estructura de Estado
interface AppState {
// Autenticación
auth: {
user: User | null;
token: string | null;
isAuthenticated: boolean;
};
// Configuración de la aplicación
settings: {
theme: 'light' | 'dark';
language: 'es' | 'en';
defaultPipelineConfig: PipelineConfig;
};
// Estado de procesamiento activo
processing: {
activeJobs: ProcessingJob[];
history: ProcessingHistory[];
filters: FilterState;
};
// UI Estado
ui: {
sidebarOpen: boolean;
notifications: Notification[];
modals: ModalState;
};
}
Context Providers
const AppProviders = ({ children }) => {
return (
<BrowserRouter>
<QueryClient client={queryClient}>
<ThemeProvider theme={theme}>
<AuthProvider>
<NotificationProvider>
<CssBaseline />
{children}
</NotificationProvider>
</AuthProvider>
</ThemeProvider>
</QueryClient>
</BrowserRouter>
);
};
Custom Hooks
// Hook para gestión de autenticación
const useAuth = () => {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
};
// Hook para operaciones de archivo
const useFileOperations = () => {
const uploadMutation = useMutation(uploadFile);
const deleteMutation = useMutation(deleteFile);
const uploadFiles = useCallback(async (files: File[]) => {
const uploads = files.map(file => uploadMutation.mutateAsync(file));
return Promise.all(uploads);
}, [uploadMutation]);
return {
uploadFiles,
deleteFile: deleteMutation.mutate,
isUploading: uploadMutation.isLoading,
isDeleting: deleteMutation.isLoading
};
};
// Hook para monitoreo de procesamiento
const useProcessingMonitor = (processId: string) => {
return useQuery(
['processing', processId],
() => api.getProcessingStatus(processId),
{
refetchInterval: (data) =>
data?.status === 'completed' ? false : 2000,
refetchIntervalInBackground: false
}
);
};
🎨 Theming y Diseño
Tema Personalizado
const theme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#1976d2',
light: '#42a5f5',
dark: '#1565c0',
},
secondary: {
main: '#dc004e',
},
background: {
default: '#f5f5f5',
paper: '#ffffff',
},
},
typography: {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
h1: {
fontSize: '2.5rem',
fontWeight: 500,
},
},
components: {
MuiButton: {
styleOverrides: {
root: {
textTransform: 'none',
borderRadius: 8,
},
},
},
MuiCard: {
styleOverrides: {
root: {
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
borderRadius: 12,
},
},
},
},
});
Responsive Design
const useResponsive = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const isTablet = useMediaQuery(theme.breakpoints.between('md', 'lg'));
const isDesktop = useMediaQuery(theme.breakpoints.up('lg'));
return { isMobile, isTablet, isDesktop };
};
🔌 Integración con API
Cliente API
class ApiClient {
private axios: AxiosInstance;
constructor() {
this.axios = axios.create({
baseURL: process.env.REACT_APP_API_URL,
timeout: 30000,
});
this.setupInterceptors();
}
private setupInterceptors() {
// Request interceptor para agregar auth token
this.axios.interceptors.request.use((config) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor para manejo de errores
this.axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Redirect to login
window.location.href = '/login';
}
return Promise.reject(error);
}
);
}
// Métodos de API
async uploadImage(file: File): Promise<UploadResponse> {
const formData = new FormData();
formData.append('file', file);
const response = await this.axios.post('/api/v1/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
return response.data;
}
async processImage(config: ProcessingConfig): Promise<ProcessingResponse> {
return this.axios.post('/api/v1/pipeline/process', config);
}
}
React Query Configuration
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 3,
staleTime: 5 * 60 * 1000, // 5 minutos
cacheTime: 10 * 60 * 1000, // 10 minutos
refetchOnWindowFocus: false,
},
mutations: {
retry: 1,
},
},
});
📱 Características Avanzadas
Notificaciones en Tiempo Real
const NotificationSystem = () => {
const { notifications, dismissNotification } = useNotifications();
return (
<Portal>
<Stack
spacing={1}
sx={{
position: 'fixed',
top: 24,
right: 24,
zIndex: 9999
}}
>
{notifications.map((notification) => (
<Alert
key={notification.id}
severity={notification.type}
onClose={() => dismissNotification(notification.id)}
action={
notification.action && (
<Button color="inherit" size="small">
{notification.action.label}
</Button>
)
}
>
{notification.message}
</Alert>
))}
</Stack>
</Portal>
);
};
Búsqueda Avanzada
const AdvancedSearch = ({ onSearch }) => {
const [filters, setFilters] = useState(defaultFilters);
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Filtros Avanzados</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<FormControl fullWidth>
<InputLabel>Tipo de Documento</InputLabel>
<Select
value={filters.documentType}
onChange={(e) => setFilters({
...filters,
documentType: e.target.value
})}
>
<MenuItem value="all">Todos</MenuItem>
<MenuItem value="cedula">Cédula</MenuItem>
<MenuItem value="factura">Factura</MenuItem>
<MenuItem value="formulario">Formulario</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={6}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DateRangePicker
startText="Fecha Inicio"
endText="Fecha Fin"
value={filters.dateRange}
onChange={(range) => setFilters({
...filters,
dateRange: range
})}
renderInput={(startProps, endProps) => (
<>
<TextField {...startProps} />
<Box sx={{ mx: 2 }}> a </Box>
<TextField {...endProps} />
</>
)}
/>
</LocalizationProvider>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
);
};
🚀 Optimización y Performance
Lazy Loading de Componentes
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Processing = lazy(() => import('./pages/Processing'));
const Results = lazy(() => import('./pages/Results'));
const App = () => (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/processing" element={<Processing />} />
<Route path="/results" element={<Results />} />
</Routes>
</Suspense>
);
Optimización de Imágenes
const useImageOptimization = () => {
const optimizeImage = useCallback(async (file: File): Promise<File> => {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
const img = new Image();
img.onload = () => {
const maxWidth = 1920;
const maxHeight = 1080;
let { width, height } = img;
if (width > height) {
if (width > maxWidth) {
height *= maxWidth / width;
width = maxWidth;
}
} else {
if (height > maxHeight) {
width *= maxHeight / height;
height = maxHeight;
}
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob((blob) => {
resolve(new File([blob!], file.name, {
type: 'image/jpeg',
lastModified: Date.now()
}));
}, 'image/jpeg', 0.9);
};
img.src = URL.createObjectURL(file);
});
}, []);
return { optimizeImage };
};
📚 Próximos Pasos
Explora más sobre la aplicación web:
- Guía de Usuario: Cómo usar la aplicación paso a paso
- Desarrollo: Configuración del entorno de desarrollo
- Componentes: Documentación detallada de componentes React
- API Integration: Cómo integrar con el API Gateway
¿Necesitas ayuda con la aplicación web? Consulta las guías de usuario o revisa la documentación de desarrollo.