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:

  1. Gemini 2.5 Flash (primario)
  2. Gemini 2.5 Flash-Lite (fallback cuota)
  3. Gemini 2.5 Pro (último recurso Gemini)
  4. Groq Llama 3.3 70B (fallback entre proveedores)
  5. Groq Llama 3.1 8B (fallback Groq)
  6. Clasificador de reglas (último recurso, sin IA)

El campo source en la respuesta indica exactamente qué proveedor respondió.

Output estructurado

  • Gemini: responseMimeType: "application/json" con responseSchema (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

  1. /configuracion → Agrega 2-3 enfermeros
  2. /ingreso → Registra pacientes con síntomas variados
  3. /sala (otra pestaña) → Cola en tiempo real
  4. /medico → Llama al siguiente paciente y finaliza consulta
  5. /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

Share this project:

Updates