Saltar al contenido principal

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:


¿Necesitas ayuda con la aplicación web? Consulta las guías de usuario o revisa la documentación de desarrollo.