UrgenSys — Sistema de Triage Inteligente
Herramienta clínica para urgencias hospitalarias que prioriza pacientes automáticamente usando IA (Gemini + Groq con fallback a reglas) basado en el estándar Emergency Severity Index (ESI) v4. Construida para el Hackathon 2026.
🚀 Quick Start
git clone https://github.com/ChrisAvens/UrgenSys && cd UrgenSys
npm install
echo "GOOGLE_API_KEY=tu_key_de_google_ai_studio" > .env.local
npm run dev
# → http://localhost:3000
Sin API keys configuradas, el sistema igual funciona usando un clasificador basado en reglas.
Stack tecnológico
| Capa | Tecnología |
|---|---|
| Framework | Next.js 16 (App Router) |
| Lenguaje | TypeScript 5 |
| Estilos | Tailwind CSS v4 + shadcn/ui |
| Estado cliente | Zustand v5 + localStorage (persistencia reactiva) |
| Persistencia servidor | SQLite (better-sqlite3) con WAL |
| IA (cadena con fallbacks) | Gemini 2.5 Flash → Flash-Lite → Pro → Groq Llama 3.3 70B → Llama 3.1 8B → reglas |
| Gráficas | Recharts |
| Animaciones | Framer Motion |
Variables de entorno
| Variable | Requerida | Descripción |
|---|---|---|
GOOGLE_API_KEY |
Recomendada | Gemini (proveedor primario). Sin ésta se usa Groq o reglas |
GROQ_API_KEY |
Opcional | Fallback automático si Gemini falla o agota cuota |
Sin ninguna de las dos, el sistema funciona con el clasificador de reglas. Útil para desarrollo y demos offline, no para producción clínica.
Roles y rutas
| Ruta | Rol | Función |
|---|---|---|
/ |
Todos | Landing con acceso a cada sección |
/ingreso |
Enfermera de triage | Registra al paciente y obtiene clasificación ESI de la IA (con evaluación clínica profunda) |
/sala |
Pantalla pública | Cola priorizada proyectable en TV; consultorios en tiempo real |
/medico |
Médico / Enfermero | Asigna pacientes, revisa evaluación IA en accordion, dicta notas clínicas por voz |
/pacientes |
Médico / Enfermero | Historial completo del turno con búsqueda, filtros y notas por voz |
/dashboard |
Jefe de turno | Métricas del turno, gráficas, exportación completa y cierre de turno |
/configuracion |
Administrador | Gestión de personal (enfermeros) y número de consultorios |
Arquitectura de datos
UrgenSys usa persistencia dual:
- Cliente (Zustand + localStorage): estado reactivo instantáneo, funciona offline, ideal para UI responsiva
- Servidor (SQLite vía better-sqlite3): fuente de verdad, sincronización entre pestañas/dispositivos, exportación
El hook useRealtimeSync empuja cambios locales al servidor y hace polling cada 3s para detectar cambios de otras sesiones.
Estructura del store cliente (store/triageStore.ts)
interface TriageState {
patients: Patient[]; // Todos los pacientes del turno
consultorios: Consultorio[]; // 1-10 consultorios, configurable
nurses: Nurse[]; // Personal registrado (persiste entre turnos)
shiftStartedAt: number; // Timestamp de inicio del turno
}
Schema SQLite (lib/db/schema.ts)
turnos (id, started_at, ended_at, closed)
patients (id, turno_id, name, age, sex, chief_complaint, vital_signs JSON,
esi_level, justification, red_flags JSON, extracted_symptoms JSON,
treatment_notes JSON, ai_evaluation JSON, arrival_time, triage_time,
triage_by, status, consult_start_time, consult_end_time,
attended_by, total_wait_minutes, estimated_wait_minutes)
consultorios (id, name, current_patient_id, consult_started_at)
reports (id, patient_id, turno_id, template_id, title, transcript,
structured_data JSON, missing_fields JSON, created_at, created_by)
nurses (id, name, role)
La DB se crea en .data/triage.db con journal_mode = WAL y foreign_keys = ON.
Ciclo de vida de un paciente
WAITING ──► IN-CONSULT ──► DISCHARGED
│ │ │
│ assignPatient │ dischargePatient
│ │ │
arrivalTime consultStartTime consultEndTime
totalWaitMinutes calculado
Persistencia cliente entre recargas
El middleware persist de Zustand serializa el estado en cada cambio. Al recargar la página, rehidrata el store desde localStorage. Para SSR (Next.js), se usa un storage wrapper seguro:
getItem: (name) => typeof window !== 'undefined' ? localStorage.getItem(name) : null,
Cómo funciona el triage con IA
Endpoint
POST /api/triage
Request body
{
"chiefComplaint": "Dolor en el pecho desde hace 2 horas, irradiado al brazo izquierdo",
"age": 58,
"sex": "M",
"vitalSigns": {
"heartRate": 125,
"oxygenSaturation": 93,
"systolicBP": 140
}
}
Respuesta — AIEvaluation completa
AIEvaluation {
esiLevel, confidence, justification,
clinicalAssessment: {
chiefComplaintSummary, // Síntesis en terminología médica formal
symptomAnalysis[],
vitalSignsInterpretation,
possibleDiagnoses[]: { diagnosis, likelihood: 'alta'|'media'|'baja', reasoning },
redFlags[],
riskFactors[]
},
recommendedActions: {
immediate[],
diagnosticTests[],
specialistReferral,
monitoringPriority // 'continuo' | 'frecuente' | 'estándar'
},
warnings: {
deteriorationSigns[],
contraindications[],
criticalWindow // Ej. "trombolisis <4.5h"
},
extractedSymptoms[],
treatmentNotes[],
source // "gemini:<model>" | "groq:<model>" | "rules" | "rules-fallback"
}
En /ingreso se muestra un preview completo antes de confirmar. En /medico se presenta en secciones colapsables (shadcn Accordion).
Cadena de proveedores (fallback automático)
El endpoint intenta en este orden hasta que uno responda:
- Gemini 2.5 Flash (primario)
- Gemini 2.5 Flash-Lite (fallback cuota)
- Gemini 2.5 Pro (último recurso Gemini)
- Groq Llama 3.3 70B (fallback entre proveedores)
- Groq Llama 3.1 8B (fallback Groq)
- Clasificador de reglas (último recurso, sin IA)
El campo source en la respuesta indica exactamente qué proveedor respondió.
Output estructurado
- Gemini:
responseMimeType: "application/json"conresponseSchema(JSON schema tipado). - Groq:
response_format: { type: 'json_object' }con instrucciones de schema en el system prompt.
Clasificador de reglas (fallback final)
| ESI | Criterio de activación |
|---|---|
| 1 | SatO2 < 88%, FC < 40 o > 150, texto: "paro", "inconsciente", "convulsión", "apnea" |
| 2 | FC > 120, SatO2 < 94%, TAS < 90, FR > 28, Temp > 40°C, texto: "pecho", "torácico", "brazo izquierdo", "desmayo" |
| 3 | FC > 100, Temp > 38.5°C, texto: "fractura", "abdomen", "vómito", "cefalea" |
| 4 | Texto: "fiebre", "dolor", "lumbar", "torcedura", "cortada" |
| 5 | Ninguno de los anteriores |
Algoritmo de tiempo de espera estimado (lib/wait-time.ts)
estimatedWait = patientsAhead × avgConsultDuration / totalConsultorios
- patientsAhead: pacientes con ESI menor, o mismo ESI pero llegaron antes
- avgConsultDuration: promedio calculado de pacientes ya dados de alta (
consultEndTime - consultStartTime). Default: 15 min si no hay historial - totalConsultorios: número total de consultorios configurados
Alertas en tiempo real (components/AlertsBanner.tsx)
Se actualiza cada segundo y genera alertas cuando:
| Condición | Tipo |
|---|---|
| Paciente ESI 1 en espera (cualquier tiempo) | 🔴 Crítica (pulso animado) |
| Paciente ESI 2 con > 10 min esperando | 🟠 Alerta |
| Consultorio libre + pacientes en cola | 🟡 Info — consultorio ocioso |
Gestión de personal (/configuracion)
Personal registrado en /configuracion se persiste en localStorage. En el formulario de ingreso aparece como dropdown para selección rápida, eliminando escritura manual y errores.
interface Nurse {
id: string;
name: string;
role: 'Enfermera' | 'Enfermero' | 'Médico' | 'Residente' | 'Técnico';
}
El personal no se borra al cerrar el turno — solo los pacientes y estados de consultorios se limpian.
Gestión de consultorios
- Inicio con 3 consultorios por defecto; configurable hasta 10 desde
/configuracion - Solo se pueden eliminar si están libres
- Desde
/medico:- Llamar siguiente: asigna el paciente más urgente automáticamente
- Selección manual: dropdown para elegir paciente específico
- Reasignar: mover un paciente en consulta a otro consultorio libre
Notas clínicas por voz
Flujo: seleccionar template → grabar con MediaRecorder → Gemini transcribe + extrae campos → revisar/completar → guardar en SQLite.
| Template | Campos principales |
|---|---|
| Nota de Evolución | SOAP (Subjetivo, Objetivo, Evaluación, Plan) |
| Nota de Alta | Diagnóstico final, instrucciones, signos de alarma |
| Interconsulta | Especialidad, motivo, pregunta clínica |
| Nota Triage Complementaria | Cambios clínicos, reevaluación ESI |
Audio enviado como base64 inline a Gemini 2.0 Flash. El endpoint es POST /api/reports/voice.
API REST interna
| Método | Ruta | Función |
|---|---|---|
| GET/POST | /api/patients |
Listar y upsert pacientes por turno |
| GET/PATCH | /api/patients/[id] |
Detalle y actualización de estado |
| GET/POST/DELETE | /api/consultorios |
Gestión de consultorios |
| POST | /api/consultorios/[id]/assign |
Asignar paciente |
| POST | /api/consultorios/[id]/release |
Dar de alta / liberar consultorio |
| GET/POST | /api/reports |
Reportes por turno |
| GET | /api/reports/patient/[id] |
Reportes de un paciente |
| POST | /api/reports/voice |
Procesar audio con Gemini → structured data |
| GET/POST/DELETE | /api/turno |
Gestión del turno activo |
| POST | /api/triage |
Clasificación ESI con cadena Gemini → Groq → reglas |
Flujo de demostración rápida
/configuracion→ Agrega 2-3 enfermeros/ingreso→ Registra pacientes con síntomas variados/sala(otra pestaña) → Cola en tiempo real/medico→ Llama al siguiente paciente y finaliza consulta/dashboard→ Revisa métricas o carga "Cargar demo" para el pitch
Arquitectura de archivos
UrgenSys/
├── app/
│ ├── api/
│ │ ├── triage/route.ts # Cadena Gemini → Groq → reglas
│ │ ├── patients/route.ts # GET/POST pacientes
│ │ ├── patients/[id]/route.ts # GET/PATCH paciente individual
│ │ ├── consultorios/route.ts # GET/POST/DELETE consultorios
│ │ ├── consultorios/[id]/assign/ # POST asignar paciente
│ │ ├── consultorios/[id]/release/ # POST dar de alta
│ │ ├── reports/route.ts # GET/POST reportes
│ │ ├── reports/patient/[id]/ # GET reportes por paciente
│ │ ├── reports/voice/route.ts # POST audio → Gemini
│ │ └── turno/route.ts # GET/POST/DELETE turno
│ ├── ingreso/page.tsx # Triage con preview AI completo
│ ├── sala/page.tsx # Cola pública (TV)
│ ├── medico/page.tsx # Consultorio + accordion AI + reportes voz
│ ├── pacientes/page.tsx # Historial + búsqueda + fichas + reportes
│ ├── dashboard/page.tsx # Métricas, gráficas, exportación completa
│ └── configuracion/page.tsx # Personal y consultorios
├── components/
│ ├── reports/VoiceReportRecorder.tsx # Grabadora de voz con Framer Motion
│ ├── AlertsBanner.tsx # Alertas ESI tiempo real
│ ├── ESIBadge.tsx # Badge ESI 1-5
│ └── LogoLink.tsx # Logo → home
├── hooks/
│ ├── useClientTime.ts # SSR-safe clock
│ └── useRealtimeSync.ts # Polling SQLite ↔ Zustand (3s)
├── lib/
│ ├── db/
│ │ ├── client.ts # Singleton better-sqlite3
│ │ ├── schema.ts # DDL: patients, consultorios, reports, turnos, nurses
│ │ └── repositories/
│ │ ├── patientsRepo.ts
│ │ ├── consultoriosRepo.ts
│ │ ├── reportsRepo.ts
│ │ └── turnosRepo.ts
│ ├── types.ts # AIEvaluation, Patient, Consultorio, Nurse…
│ ├── report-templates.ts # 4 templates clínicos + voice prompts
│ ├── wait-time.ts # Estimación y formato de tiempos
│ └── demo-data.ts # 10 pacientes demostración
└── store/
└── triageStore.ts # Zustand v5 + persist localStorage
Built With
- gemini
- grok
- next.js
- sqlite
- tailwind
- typescript
- zustand
Log in or sign up for Devpost to join the conversation.