RADIA — Arquitectura
Última actualización: 2026-04-14
Visión general
┌─────────────────────────────────────────────────────────┐
│ Cloudflare Pages │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Astro SSG │ │ React SPA │ │ Workers API │ │
│ │ (páginas) │ │ (componentes)│ │ (functions/) │ │
│ └──────────────┘ └──────────────┘ └───────┬───────┘ │
│ │ │
│ ┌────────────────────────────┼────┐ │
│ │ Bindings │ │ │
│ │ ┌─────┐ ┌──────┐ ┌─────┐│ │ │
│ │ │ D1 │ │ R2 │ │ KV ││ │ │
│ │ │(SQL)│ │(blob)│ │(opt)││ │ │
│ │ └─────┘ └──────┘ └─────┘│ │ │
│ └────────────────────────────┘ │ │
└─────────────────────────────────────────────────────────┘
│
┌──────────────┴──────────────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ Gemini │ │ Groq │
│ 2.5 Flash│ │ Llama 4 │
└──────────┘ └──────────┘
Capas
1. Frontend (Astro + React)
Astro genera páginas estáticas (SSG) que montan componentes React interactivos. No hay SSR — toda la lógica dinámica se ejecuta en el cliente o en Workers.
| Página | Componente principal | Descripción |
|---|---|---|
/ |
— (HTML estático) | Landing page |
/study?id=xxx |
StudyExplorer |
Visor principal completo |
/dashboard |
Script inline | Lista de estudios del usuario |
/shared?token=xxx |
SharedViewer |
Visor público sin autenticación |
/collab?token=xxx |
Script inline | Visor colaborador autenticado |
/upgrade |
Script inline | Planes y gestión de suscripción |
2. Backend (Cloudflare Workers)
Cada archivo en functions/api/ se convierte automáticamente en un Worker endpoint gracias a Cloudflare Pages Functions.
Autenticación: JWT HS256 en cabecera Authorization: Bearer <token>. Google OAuth genera el JWT tras verificar el id_token de Google con JWKS.
Patrón de respuesta: Todos los endpoints usan json(), error(), unauthorized() de lib/response.js con CORS abierto.
3. Base de datos (D1)
5 tablas en una única base D1 compartida (projectos-db):
radia_users ──< radia_studies ──< radia_findings
│──< radia_chat_messages
│──< radia_analysis_jobs
└──< radia_share_links
└──< radia_reports
Todas las relaciones usan ON DELETE CASCADE para limpieza automática.
4. Almacenamiento (R2)
Estructura de keys en el bucket radia-dicom:
{user_id}/{study_id}/
├── slices/
│ ├── 0000.dcm # DICOM originales
│ ├── 0001.dcm
│ └── ...
├── thumbs/
│ ├── 0000.png # Thumbnails PNG (generados en upload)
│ └── ...
└── viewports/
└── {capture_id}.png # Capturas 3D/MPR
Flujos de datos
Subida de estudio
1. Usuario arrastra ZIP → DicomUploader
2. Client-side: JSZip extrae → dicomParser parsea headers
3. POST /api/studies/upload (metadata)
4. Loop: PUT /api/dicom/{key} (binarios, 5 paralelos)
5. POST /api/studies/{id} action=finalize
6. Redirect a /study?id=xxx
Scan 360°
1. Usuario pulsa botón Scan → Se abre Diálogo de Pre-análisis
→ Modos: fresh / complement / review / delete
→ Toggle Asteroide, campo razón opcional
→ Usuario confirma
2. POST /api/analysis/scan360 action=init
→ Recibe reason (con prefijo [COMPLEMENT]/[REVIEW] según modo)
→ Si modo complement/review: preserva hallazgos existentes
→ Si modo fresh/delete: elimina hallazgos previos
→ Calcula sliceIndices (sampleRate=8, o =2 en Asteroide)
→ Crea job en radia_analysis_jobs
→ Retorna: { jobId, sliceIndices, totalBatches }
3. Loop por cada batch (frontend controla):
POST /api/analysis/scan360 action=batch
→ Lee PNG de R2 por cada slice del batch
→ Envía a Gemini 2.5 Flash (+ Groq Specialist en Asteroide)
→ Parsea JSON de hallazgos
→ INSERT INTO radia_findings
→ Retorna: { batchFindings, processedSlices }
4. POST /api/analysis/scan360 action=finalize
→ Genera impresión global con todos los hallazgos
→ (Asteroide) Auto deep-analysis en top 5 hallazgos
→ Actualiza status estudio → 'analyzed'
→ Retorna: { totalFindings, impression }
Chat multimodal
1. POST /api/chat { studyId, messages, imageBase64? }
2. Backend construye context: findings + study metadata
3. Si hay imagen: Gemini vision con base64
4. Si no: Gemini text con historial
5. Guarda mensajes en radia_chat_messages
6. Retorna respuesta + metadata
Compartir estudio
1. POST /api/share { studyId, expiresInDays? }
→ Genera token único, guarda en radia_share_links
2. Frontend construye URL: /shared?token=xxx
3. SharedViewer: GET /api/public/study?token=xxx
4. DICOM: GET /api/public/dicom/{key}?token=xxx
5. Sin JWT requerido — solo token de share
Componentes React — Responsabilidades
StudyExplorer (orquestador)
El componente central que maneja:
- Estado global del visor (study, findings, currentSlice, modo 3D/MPR)
- Todas las interacciones de IA (scan360, deep, pointask, viewport, suggestions)
- Tabs: viewer / findings / chat / reports
- Toolbar: modo visor, controles 3D, botón Asteroide
- Navegación entre vistas (DicomViewer ↔ Volume3D ↔ MPR)
- Diálogo de pre-análisis — Antes de ejecutar Scan 360°, presenta un diálogo con modos:
fresh(nuevo análisis),complement(añadir hallazgos preservando los existentes),review(revisión crítica que preserva hallazgos),delete(borrar hallazgos y re-analizar). Incluye toggle de Asteroide y campo de razón. - Gestión de hallazgos — Eliminar hallazgos individuales o grupos deduplicados, ocultar/mostrar hallazgos del informe (
user_confirmed = -1). - Modo Navegar — Al activar, click en el visor navega al hallazgo más cercano por distancia euclídea.
- Deep filter — Filtro de chip para mostrar solo hallazgos con deep analysis.
- Hallazgos agrupados (dedup) — Hallazgos en cortes adyacentes con misma categoría/ubicación se agrupan como
GroupedFindingconsliceRangeStart/EndygroupedIds. - Badge Asteroide — Indicador visual en el header del estudio si fue analizado con Modo Asteroide.
DicomViewer
- Carga lazy de slices PNG (prefetch ±5)
- Canvas rendering con window/level
- Zoom (Ctrl+scroll), pan (drag), crosshair
- Anotaciones DICOM en esquinas
- Point-Ask: click derecho → coordenadas → API
- Modo Navegar: crosshair cyan, click navega al hallazgo más cercano
Volume3DViewer
- WebGL2 ray marching (shaders GLSL inline)
- 3 modos: Bone (transfer function), MIP, X-Ray
- Reconstrucción de volumen 3D desde PNGs
- Cache en memoria + IndexedDB (7 días)
- Marcadores de hallazgos proyectados 3D→2D
- Popup interactivo por hallazgo con navegación
- Rotación libre / bloqueada por eje (X/Y/Z)
MPRViewer
- Grid 2×2 siempre (responsive mobile)
- Panels: Axial, 3D, Coronal, Sagital
- Crosshair sincronizado entre vistas
- Click en panel → expandir a pantalla completa
ChatPanel
- Chat tipo mensajería con markdown rendering
- Captura automática de viewport 3D si está visible
- Quick prompts contextuales
- Soporte de imagen pegada (paste/file)
- Guardar hallazgos desde respuestas del chat
Modelos de IA — Cadena de prioridad
Petición de visión:
1. Gemini 2.5 Flash (GEMINI_API_KEY) ← primario
2. Groq Llama 4 Scout (GROQ_API_KEY) ← fallback
Modo Asteroide (scan360):
→ Gemini (generalista) + Groq Specialist (segunda opinión) en paralelo
→ Cross-reference por category+location+slice
→ Consenso = ai_model con "+" (ej: "gemini-2.5-flash+llama-4-scout-specialist")
→ Confidence boost ×1.15 para consenso
Decisiones técnicas
| Decisión | Razón |
|---|---|
| Astro SSG (no SSR) | Máximo rendimiento, Cloudflare Pages free tier |
| D1 compartida | Una sola DB para todo ProjectOS, prefijo radia_ |
| R2 para DICOM | Almacenamiento blob ilimitado, costo mínimo |
| PNG thumbnails | Evita parsear DICOM en cada vista, cache-friendly |
| WebGL2 inline shaders | Sin dependencias 3D pesadas (Three.js = 400KB+) |
| JWT sin refresh | Simplicidad, sesión de 24h, re-login con Google |
| Client-side DICOM parsing | Reduce carga del Worker, mejor UX |
| Batch scan (frontend-driven) | Evita timeouts de Workers (30s free tier) |