| <!DOCTYPE html> |
| <html lang="es"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Vigilante Paciente</title> |
| <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Exo+2:wght@300;600;800&display=swap" rel="stylesheet"> |
| <script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script> |
| <style> |
| :root { |
| --bg: #030a0e; |
| --panel: #0a1a22; |
| --border: #0e3a50; |
| --cyan: #00e5ff; |
| --green: #00ff88; |
| --red: #ff2244; |
| --yellow: #ffcc00; |
| --text: #c8e8f0; |
| --mono: 'Share Tech Mono', monospace; |
| --display: 'Exo 2', sans-serif; |
| } |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| body { |
| background: var(--bg); |
| color: var(--text); |
| font-family: var(--display); |
| min-height: 100vh; |
| overflow-x: hidden; |
| } |
| body::before { |
| content: ''; |
| position: fixed; |
| inset: 0; |
| background: |
| radial-gradient(ellipse at 20% 50%, rgba(0,229,255,0.04) 0%, transparent 60%), |
| radial-gradient(ellipse at 80% 20%, rgba(0,255,136,0.03) 0%, transparent 50%); |
| pointer-events: none; |
| z-index: 0; |
| } |
| .grid-bg { |
| position: fixed; |
| inset: 0; |
| background-image: |
| linear-gradient(rgba(0,229,255,0.04) 1px, transparent 1px), |
| linear-gradient(90deg, rgba(0,229,255,0.04) 1px, transparent 1px); |
| background-size: 40px 40px; |
| pointer-events: none; |
| z-index: 0; |
| } |
| .container { |
| position: relative; |
| z-index: 1; |
| max-width: 480px; |
| margin: 0 auto; |
| padding: 16px; |
| } |
| .header { |
| text-align: center; |
| padding: 20px 0 16px; |
| border-bottom: 1px solid var(--border); |
| margin-bottom: 16px; |
| } |
| .header h1 { |
| font-size: 1.4rem; |
| font-weight: 800; |
| letter-spacing: 0.15em; |
| color: var(--cyan); |
| text-shadow: 0 0 20px rgba(0,229,255,0.5); |
| text-transform: uppercase; |
| } |
| .header p { |
| font-family: var(--mono); |
| font-size: 0.65rem; |
| color: #4a7a8a; |
| margin-top: 4px; |
| letter-spacing: 0.1em; |
| } |
| |
| .peer-box { |
| display: none; |
| background: var(--panel); |
| border: 1px solid var(--cyan); |
| border-radius: 8px; |
| padding: 12px 14px; |
| margin-bottom: 14px; |
| font-family: var(--mono); |
| font-size: 0.72rem; |
| color: var(--cyan); |
| letter-spacing: 0.08em; |
| word-break: break-all; |
| box-shadow: 0 0 14px rgba(0,229,255,0.12); |
| } |
| .peer-box .peer-label { |
| font-size: 0.55rem; |
| color: #4a7a8a; |
| letter-spacing: 0.2em; |
| text-transform: uppercase; |
| margin-bottom: 4px; |
| } |
| .peer-id-text { |
| color: var(--green); |
| font-size: 0.8rem; |
| font-weight: bold; |
| word-break: break-all; |
| } |
| .btn-copiar { |
| margin-top: 8px; |
| width: 100%; |
| padding: 8px; |
| background: rgba(0,229,255,0.1); |
| border: 1px solid var(--cyan); |
| border-radius: 6px; |
| color: var(--cyan); |
| font-family: var(--mono); |
| font-size: 0.65rem; |
| letter-spacing: 0.1em; |
| cursor: pointer; |
| text-transform: uppercase; |
| transition: background 0.2s; |
| } |
| .btn-copiar:hover { background: rgba(0,229,255,0.2); } |
| .peer-conectados { |
| margin-top: 6px; |
| font-size: 0.6rem; |
| color: var(--green); |
| letter-spacing: 0.1em; |
| } |
| |
| .estado-box { |
| border: 2px solid var(--border); |
| border-radius: 8px; |
| padding: 16px; |
| margin-bottom: 14px; |
| background: var(--panel); |
| position: relative; |
| overflow: hidden; |
| transition: border-color 0.3s, box-shadow 0.3s; |
| } |
| .estado-box.normal { border-color: var(--green); box-shadow: 0 0 20px rgba(0,255,136,0.1); } |
| .estado-box.critico { border-color: var(--red); box-shadow: 0 0 30px rgba(255,34,68,0.3); animation: pulseRed 0.8s infinite; } |
| .estado-box.esperando { border-color: var(--border); } |
| @keyframes pulseRed { |
| 0%, 100% { box-shadow: 0 0 20px rgba(255,34,68,0.3); } |
| 50% { box-shadow: 0 0 50px rgba(255,34,68,0.7); } |
| } |
| .estado-label { |
| font-family: var(--mono); |
| font-size: 0.6rem; |
| letter-spacing: 0.2em; |
| color: #4a7a8a; |
| text-transform: uppercase; |
| margin-bottom: 6px; |
| } |
| .estado-valor { font-size: 2rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; } |
| .estado-valor.normal { color: var(--green); text-shadow: 0 0 15px rgba(0,255,136,0.5); } |
| .estado-valor.critico { color: var(--red); text-shadow: 0 0 15px rgba(255,34,68,0.7); } |
| .estado-valor.esperando { color: #4a7a8a; } |
| .motivo-texto { |
| font-family: var(--mono); |
| font-size: 0.75rem; |
| color: var(--yellow); |
| margin-top: 8px; |
| min-height: 16px; |
| } |
| |
| .camara-container { |
| border: 1px solid var(--border); |
| border-radius: 8px; |
| overflow: hidden; |
| margin-bottom: 14px; |
| position: relative; |
| background: #000; |
| aspect-ratio: 4/3; |
| } |
| #video { |
| width: 100%; |
| height: 100%; |
| object-fit: cover; |
| display: block; |
| transform: scaleX(-1); |
| } |
| .camara-overlay { position: absolute; inset: 0; pointer-events: none; } |
| .corner { position: absolute; width: 20px; height: 20px; border-color: var(--cyan); border-style: solid; opacity: 0.7; } |
| .corner.tl { top: 8px; left: 8px; border-width: 2px 0 0 2px; } |
| .corner.tr { top: 8px; right: 8px; border-width: 2px 2px 0 0; } |
| .corner.bl { bottom: 8px; left: 8px; border-width: 0 0 2px 2px; } |
| .corner.br { bottom: 8px; right: 8px; border-width: 0 2px 2px 0; } |
| .camara-label { |
| position: absolute; |
| top: 10px; left: 50%; |
| transform: translateX(-50%); |
| font-family: var(--mono); |
| font-size: 0.6rem; |
| color: var(--cyan); |
| letter-spacing: 0.15em; |
| opacity: 0.8; |
| } |
| .rec-dot { |
| position: absolute; |
| top: 10px; right: 32px; |
| width: 8px; height: 8px; |
| background: var(--red); |
| border-radius: 50%; |
| animation: blink 1.2s infinite; |
| } |
| @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.2; } } |
| |
| .constantes { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; } |
| .constante-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; } |
| .constante-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 4px; } |
| .constante-valor { font-family: var(--mono); font-size: 1.6rem; font-weight: bold; color: var(--cyan); } |
| .constante-unidad { font-size: 0.6rem; color: #4a7a8a; margin-left: 2px; } |
| |
| .sonido-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 14px; } |
| .sonido-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 8px; } |
| .sonido-barra-container { display: flex; align-items: center; gap: 10px; } |
| .sonido-barra { flex: 1; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; } |
| .sonido-fill { height: 100%; background: linear-gradient(90deg, var(--green), var(--cyan)); border-radius: 3px; transition: width 0.15s; width: 0%; } |
| .sonido-estado { font-family: var(--mono); font-size: 0.7rem; color: var(--green); min-width: 60px; text-align: right; } |
| |
| .nombre-input { |
| width: 100%; background: var(--panel); border: 1px solid var(--border); |
| border-radius: 8px; padding: 10px 14px; color: var(--text); |
| font-family: var(--mono); font-size: 0.85rem; margin-bottom: 14px; |
| outline: none; transition: border-color 0.2s; |
| } |
| .nombre-input:focus { border-color: var(--cyan); } |
| .nombre-input::placeholder { color: #2a4a5a; } |
| |
| .botones { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; } |
| .btn { padding: 14px; border: none; border-radius: 8px; font-family: var(--display); font-size: 0.8rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; cursor: pointer; transition: all 0.2s; } |
| .btn-iniciar { background: var(--cyan); color: var(--bg); } |
| .btn-iniciar:hover { background: #00ccee; transform: translateY(-1px); } |
| .btn-iniciar:disabled { background: #0a3040; color: #2a5060; cursor: not-allowed; transform: none; } |
| .btn-parar { background: transparent; color: var(--red); border: 1px solid var(--red); } |
| .btn-parar:hover { background: rgba(255,34,68,0.1); } |
| .btn-parar:disabled { opacity: 0.3; cursor: not-allowed; } |
| |
| .log-box { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 14px; max-height: 120px; overflow-y: auto; } |
| .log-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 8px; } |
| .log-entry { font-family: var(--mono); font-size: 0.7rem; color: #5a8a9a; padding: 2px 0; border-bottom: 1px solid rgba(14,58,80,0.4); } |
| .log-entry:last-child { border-bottom: none; } |
| .log-entry.critico { color: var(--red); } |
| .log-entry.normal { color: var(--green); } |
| .log-entry.info { color: var(--cyan); } |
| |
| .alarma-overlay { display: none; position: fixed; inset: 0; background: rgba(255,34,68,0.15); z-index: 100; backdrop-filter: blur(2px); animation: flashRed 0.5s infinite; } |
| .alarma-overlay.activa { display: flex; align-items: center; justify-content: center; } |
| @keyframes flashRed { 0%, 100% { background: rgba(255,34,68,0.1); } 50% { background: rgba(255,34,68,0.3); } } |
| .alarma-card { background: #1a0508; border: 2px solid var(--red); border-radius: 12px; padding: 30px; text-align: center; max-width: 300px; box-shadow: 0 0 60px rgba(255,34,68,0.5); } |
| .alarma-icono { font-size: 3rem; margin-bottom: 10px; } |
| .alarma-titulo { font-size: 1.5rem; font-weight: 800; color: var(--red); letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 10px; } |
| .alarma-motivo { font-family: var(--mono); font-size: 0.8rem; color: var(--text); margin-bottom: 20px; line-height: 1.5; } |
| .btn-silenciar { background: var(--red); color: white; border: none; border-radius: 6px; padding: 12px 24px; font-family: var(--display); font-weight: 600; font-size: 0.85rem; cursor: pointer; letter-spacing: 0.1em; text-transform: uppercase; } |
| ::-webkit-scrollbar { width: 4px; } |
| ::-webkit-scrollbar-track { background: var(--bg); } |
| ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; } |
| </style> |
| </head> |
| <body> |
| <div class="grid-bg"></div> |
|
|
| <div class="alarma-overlay" id="alarmaOverlay"> |
| <div class="alarma-card"> |
| <div class="alarma-icono">🚨</div> |
| <div class="alarma-titulo">¡ALERTA CRÍTICA!</div> |
| <div class="alarma-motivo" id="alarmaMotivo"></div> |
| <button class="btn-silenciar" onclick="silenciarAlarma()">SILENCIAR</button> |
| </div> |
| </div> |
|
|
| <div class="container"> |
| <div class="header"> |
| <h1>⚕ Vigilante Paciente</h1> |
| <p>MONITOR DE SALUD · IA EN TIEMPO REAL</p> |
| </div> |
|
|
| |
| <div class="peer-box" id="peerIdBox"> |
| <div class="peer-label">// ID de cámara en vivo — comparte con el familiar</div> |
| <div class="peer-id-text" id="peerIdText">Generando...</div> |
| <button class="btn-copiar" onclick="copiarPeerId()">📋 Copiar ID</button> |
| <div class="peer-conectados" id="peerConectados"></div> |
| </div> |
|
|
| <input class="nombre-input" id="nombreInput" type="text" placeholder="Nombre del paciente..." maxlength="40"> |
|
|
| <div class="estado-box esperando" id="estadoBox"> |
| <div class="estado-label">// Estado del paciente</div> |
| <div class="estado-valor esperando" id="estadoValor">ESPERANDO</div> |
| <div class="motivo-texto" id="motivoTexto"></div> |
| </div> |
|
|
| <div class="camara-container"> |
| <video id="video" autoplay muted playsinline></video> |
| <div class="camara-overlay"> |
| <div class="corner tl"></div> |
| <div class="corner tr"></div> |
| <div class="corner bl"></div> |
| <div class="corner br"></div> |
| <div class="camara-label">CÁMARA EN VIVO</div> |
| <div class="rec-dot"></div> |
| </div> |
| </div> |
|
|
| <div class="constantes"> |
| <div class="constante-card"> |
| <div class="constante-label">// Pulso</div> |
| <div class="constante-valor" id="pulsoValor">--<span class="constante-unidad">BPM</span></div> |
| </div> |
| <div class="constante-card"> |
| <div class="constante-label">// Oxígeno</div> |
| <div class="constante-valor" id="oxigenoValor">--<span class="constante-unidad">%</span></div> |
| </div> |
| </div> |
|
|
| <div class="sonido-card"> |
| <div class="sonido-label">// Sonido ambiente</div> |
| <div class="sonido-barra-container"> |
| <div class="sonido-barra"><div class="sonido-fill" id="sonidoFill"></div></div> |
| <div class="sonido-estado" id="sonidoEstado">INACTIVO</div> |
| </div> |
| </div> |
|
|
| <div class="botones"> |
| <button class="btn btn-iniciar" id="btnIniciar" onclick="iniciarVigilancia()">▶ INICIAR</button> |
| <button class="btn btn-parar" id="btnParar" onclick="pararVigilancia()" disabled>■ PARAR</button> |
| </div> |
|
|
| <div class="log-box"> |
| <div class="log-label">// Registro de eventos</div> |
| <div id="logContainer"></div> |
| </div> |
| </div> |
|
|
| <script> |
| const BACKEND = "https://cristobal299-iaweb.hf.space/vigilar"; |
| let streamActivo = false; |
| let intervalId = null; |
| let mediaStream = null; |
| let audioContext = null; |
| let analyser = null; |
| let alarmaActiva = false; |
| let alarmaAudio = null; |
| /* ── PEER ID BOX ── */ |
| .peer-box { |
| display: none; |
| background: var(--panel); |
| border: 1px solid var(--cyan); |
| border-radius: 8px; |
| padding: 12px 14px; |
| margin-bottom: 14px; |
| font-family: var(--mono); |
| font-size: 0.72rem; |
| color: var(--cyan); |
| letter-spacing: 0.08em; |
| word-break: break-all; |
| box-shadow: 0 0 14px rgba(0,229,255,0.12); |
| } |
| .peer-box .peer-label { |
| font-size: 0.55rem; |
| color: #4a7a8a; |
| letter-spacing: 0.2em; |
| text-transform: uppercase; |
| margin-bottom: 4px; |
| } |
| .peer-id-text { |
| color: var(--green); |
| font-size: 0.8rem; |
| font-weight: bold; |
| word-break: break-all; |
| } |
| .btn-copiar { |
| margin-top: 8px; |
| width: 100%; |
| padding: 8px; |
| background: rgba(0,229,255,0.1); |
| border: 1px solid var(--cyan); |
| border-radius: 6px; |
| color: var(--cyan); |
| font-family: var(--mono); |
| font-size: 0.65rem; |
| letter-spacing: 0.1em; |
| cursor: pointer; |
| text-transform: uppercase; |
| transition: background 0.2s; |
| } |
| .btn-copiar:hover { background: rgba(0,229,255,0.2); } |
| .peer-conectados { |
| margin-top: 6px; |
| font-size: 0.6rem; |
| color: var(--green); |
| letter-spacing: 0.1em; |
| } |
| /* Estado principal */ |
| .estado-box { |
| border: 2px solid var(--border); |
| border-radius: 8px; |
| padding: 16px; |
| margin-bottom: 14px; |
| background: var(--panel); |
| position: relative; |
| overflow: hidden; |
| transition: border-color 0.3s, box-shadow 0.3s; |
| } |
| .estado-box.normal { border-color: var(--green); box-shadow: 0 0 20px rgba(0,255,136,0.1); } |
| .estado-box.critico { border-color: var(--red); box-shadow: 0 0 30px rgba(255,34,68,0.3); animation: pulseRed 0.8s infinite; } |
| .estado-box.esperando { border-color: var(--border); } |
| @keyframes pulseRed { |
| 0%, 100% { box-shadow: 0 0 20px rgba(255,34,68,0.3); } |
| 50% { box-shadow: 0 0 50px rgba(255,34,68,0.7); } |
| } |
| .estado-label { |
| font-family: var(--mono); |
| font-size: 0.6rem; |
| letter-spacing: 0.2em; |
| color: #4a7a8a; |
| text-transform: uppercase; |
| margin-bottom: 6px; |
| } |
| .estado-valor { font-size: 2rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; } |
| .estado-valor.normal { color: var(--green); text-shadow: 0 0 15px rgba(0,255,136,0.5); } |
| .estado-valor.critico { color: var(--red); text-shadow: 0 0 15px rgba(255,34,68,0.7); } |
| .estado-valor.esperando { color: #4a7a8a; } |
| .motivo-texto { |
| font-family: var(--mono); |
| font-size: 0.75rem; |
| color: var(--yellow); |
| margin-top: 8px; |
| min-height: 16px; |
| } |
| /* Cámara */ |
| .camara-container { |
| border: 1px solid var(--border); |
| border-radius: 8px; |
| overflow: hidden; |
| margin-bottom: 14px; |
| position: relative; |
| background: #000; |
| aspect-ratio: 4/3; |
| } |
| #video { |
| width: 100%; |
| height: 100%; |
| object-fit: cover; |
| display: block; |
| transform: scaleX(-1); |
| } |
| .camara-overlay { position: absolute; inset: 0; pointer-events: none; } |
| .corner { position: absolute; width: 20px; height: 20px; border-color: var(--cyan); border-style: solid; opacity: 0.7; } |
| .corner.tl { top: 8px; left: 8px; border-width: 2px 0 0 2px; } |
| .corner.tr { top: 8px; right: 8px; border-width: 2px 2px 0 0; } |
| .corner.bl { bottom: 8px; left: 8px; border-width: 0 0 2px 2px; } |
| .corner.br { bottom: 8px; right: 8px; border-width: 0 2px 2px 0; } |
| .camara-label { |
| position: absolute; |
| top: 10px; left: 50%; |
| transform: translateX(-50%); |
| font-family: var(--mono); |
| font-size: 0.6rem; |
| color: var(--cyan); |
| letter-spacing: 0.15em; |
| opacity: 0.8; |
| } |
| .rec-dot { |
| position: absolute; |
| top: 10px; right: 32px; |
| width: 8px; height: 8px; |
| background: var(--red); |
| border-radius: 50%; |
| animation: blink 1.2s infinite; |
| } |
| @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.2; } } |
| /* Constantes */ |
| .constantes { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; } |
| .constante-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; } |
| .constante-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 4px; } |
| .constante-valor { font-family: var(--mono); font-size: 1.6rem; font-weight: bold; color: var(--cyan); } |
| .constante-unidad { font-size: 0.6rem; color: #4a7a8a; margin-left: 2px; } |
| /* Sonido */ |
| .sonido-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 14px; } |
| .sonido-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 8px; } |
| .sonido-barra-container { display: flex; align-items: center; gap: 10px; } |
| .sonido-barra { flex: 1; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; } |
| .sonido-fill { height: 100%; background: linear-gradient(90deg, var(--green), var(--cyan)); border-radius: 3px; transition: width 0.15s; width: 0%; } |
| .sonido-estado { font-family: var(--mono); font-size: 0.7rem; color: var(--green); min-width: 60px; text-align: right; } |
| /* Nombre */ |
| .nombre-input { |
| width: 100%; background: var(--panel); border: 1px solid var(--border); |
| border-radius: 8px; padding: 10px 14px; color: var(--text); |
| font-family: var(--mono); font-size: 0.85rem; margin-bottom: 14px; |
| outline: none; transition: border-color 0.2s; |
| } |
| .nombre-input:focus { border-color: var(--cyan); } |
| .nombre-input::placeholder { color: #2a4a5a; } |
| /* Botones */ |
| .botones { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; } |
| .btn { padding: 14px; border: none; border-radius: 8px; font-family: var(--display); font-size: 0.8rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; cursor: pointer; transition: all 0.2s; } |
| .btn-iniciar { background: var(--cyan); color: var(--bg); } |
| .btn-iniciar:hover { background: #00ccee; transform: translateY(-1px); } |
| .btn-iniciar:disabled { background: #0a3040; color: #2a5060; cursor: not-allowed; transform: none; } |
| .btn-parar { background: transparent; color: var(--red); border: 1px solid var(--red); } |
| .btn-parar:hover { background: rgba(255,34,68,0.1); } |
| .btn-parar:disabled { opacity: 0.3; cursor: not-allowed; } |
| /* Log */ |
| .log-box { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 14px; max-height: 120px; overflow-y: auto; } |
| .log-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 8px; } |
| .log-entry { font-family: var(--mono); font-size: 0.7rem; color: #5a8a9a; padding: 2px 0; border-bottom: 1px solid rgba(14,58,80,0.4); } |
| .log-entry:last-child { border-bottom: none; } |
| .log-entry.critico { color: var(--red); } |
| .log-entry.normal { color: var(--green); } |
| .log-entry.info { color: var(--cyan); } |
| /* Alarma overlay */ |
| .alarma-overlay { display: none; position: fixed; inset: 0; background: rgba(255,34,68,0.15); z-index: 100; backdrop-filter: blur(2px); animation: flashRed 0.5s infinite; } |
| .alarma-overlay.activa { display: flex; align-items: center; justify-content: center; } |
| @keyframes flashRed { 0%, 100% { background: rgba(255,34,68,0.1); } 50% { background: rgba(255,34,68,0.3); } } |
| .alarma-card { background: #1a0508; border: 2px solid var(--red); border-radius: 12px; padding: 30px; text-align: center; max-width: 300px; box-shadow: 0 0 60px rgba(255,34,68,0.5); } |
| .alarma-icono { font-size: 3rem; margin-bottom: 10px; } |
| .alarma-titulo { font-size: 1.5rem; font-weight: 800; color: var(--red); letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 10px; } |
| .alarma-motivo { font-family: var(--mono); font-size: 0.8rem; color: var(--text); margin-bottom: 20px; line-height: 1.5; } |
| .btn-silenciar { background: var(--red); color: white; border: none; border-radius: 6px; padding: 12px 24px; font-family: var(--display); font-weight: 600; font-size: 0.85rem; cursor: pointer; letter-spacing: 0.1em; text-transform: uppercase; } |
| ::-webkit-scrollbar { width: 4px; } |
| ::-webkit-scrollbar-track { background: var(--bg); } |
| ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; } |
| </style> |
| </head> |
| <body> |
| <div class="grid-bg"></div> |
| |
| <div class="alarma-overlay" id="alarmaOverlay"> |
| <div class="alarma-card"> |
| <div class="alarma-icono">🚨</div> |
| <div class="alarma-titulo">¡ALERTA CRÍTICA!</div> |
| <div class="alarma-motivo" id="alarmaMotivo"></div> |
| <button class="btn-silenciar" onclick="silenciarAlarma()">SILENCIAR</button> |
| </div> |
| </div> |
| |
| <div class="container"> |
| <div class="header"> |
| <h1>⚕ Vigilante Paciente</h1> |
| <p>MONITOR DE SALUD · IA EN TIEMPO REAL</p> |
| </div> |
| |
| |
| <div class="peer-box" id="peerIdBox"> |
| <div class="peer-label">// ID de cámara en vivo — comparte con el familiar</div> |
| <div class="peer-id-text" id="peerIdText">Generando...</div> |
| <button class="btn-copiar" onclick="copiarPeerId()">📋 Copiar ID</button> |
| <div class="peer-conectados" id="peerConectados"></div> |
| </div> |
| |
| <input class="nombre-input" id="nombreInput" type="text" placeholder="Nombre del paciente..." maxlength="40"> |
| |
| <div class="estado-box esperando" id="estadoBox"> |
| <div class="estado-label">// Estado del paciente</div> |
| <div class="estado-valor esperando" id="estadoValor">ESPERANDO</div> |
| <div class="motivo-texto" id="motivoTexto"></div> |
| </div> |
| |
| <div class="camara-container"> |
| <video id="video" autoplay muted playsinline></video> |
| <div class="camara-overlay"> |
| <div class="corner tl"></div> |
| <div class="corner tr"></div> |
| <div class="corner bl"></div> |
| <div class="corner br"></div> |
| <div class="camara-label">CÁMARA EN VIVO</div> |
| <div class="rec-dot"></div> |
| </div> |
| </div> |
| |
| <div class="constantes"> |
| <div class="constante-card"> |
| <div class="constante-label">// Pulso</div> |
| <div class="constante-valor" id="pulsoValor">--<span class="constante-unidad">BPM</span></div> |
| </div> |
| <div class="constante-card"> |
| <div class="constante-label">// Oxígeno</div> |
| <div class="constante-valor" id="oxigenoValor">--<span class="constante-unidad">%</span></div> |
| </div> |
| </div> |
| |
| <div class="sonido-card"> |
| <div class="sonido-label">// Sonido ambiente</div> |
| <div class="sonido-barra-container"> |
| <div class="sonido-barra"><div class="sonido-fill" id="sonidoFill"></div></div> |
| <div class="sonido-estado" id="sonidoEstado">INACTIVO</div> |
| </div> |
| </div> |
| |
| <div class="botones"> |
| <button class="btn btn-iniciar" id="btnIniciar" onclick="iniciarVigilancia()">▶ INICIAR</button> |
| <button class="btn btn-parar" id="btnParar" onclick="pararVigilancia()" disabled>■ PARAR</button> |
| </div> |
| |
| <div class="log-box"> |
| <div class="log-label">// Registro de eventos</div> |
| <div id="logContainer"></div> |
| </div> |
| </div> |
| |
| <script> |
| const BACKEND = "https://cristobal299-iaweb.hf.space/vigilar"; |
| let streamActivo = false; |
| let intervalId = null; |
| let mediaStream = null; |
| let audioContext = null; |
| let analyser = null; |
| let alarmaActiva = false; |
| let alarmaAudio = null; |
| |
| |
| let peer = null; |
| let peerId = null; |
| let llamadasActivas = 0; |
| |
| function iniciarPeer() { |
| peer = new Peer({ |
| host: '0.peerjs.com', |
| port: 443, |
| secure: true, |
| config: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] } |
| }); |
| |
| peer.on('open', id => { |
| peerId = id; |
| document.getElementById('peerIdText').textContent = id; |
| document.getElementById('peerIdBox').style.display = 'block'; |
| agregarLog('ID PeerJS generado — comparte con el familiar', 'info'); |
| }); |
| |
| peer.on('call', call => { |
| if (!mediaStream) { |
| agregarLog('Familiar intentó conectar, cámara no activa aún', 'critico'); |
| return; |
| } |
| call.answer(mediaStream); |
| llamadasActivas++; |
| actualizarConectados(); |
| agregarLog('Familiar conectado a la cámara en vivo ✓', 'info'); |
| |
| call.on('close', () => { |
| llamadasActivas = Math.max(0, llamadasActivas - 1); |
| actualizarConectados(); |
| agregarLog('Familiar desconectado de la cámara', 'info'); |
| }); |
| call.on('error', err => { |
| agregarLog('Error en llamada PeerJS: ' + err, 'critico'); |
| }); |
| }); |
| |
| peer.on('error', err => { |
| agregarLog('Error PeerJS: ' + err.type, 'critico'); |
| }); |
| } |
| |
| function actualizarConectados() { |
| const el = document.getElementById('peerConectados'); |
| el.textContent = llamadasActivas > 0 |
| ? `👁 ${llamadasActivas} familiar(es) viendo en vivo` |
| : ''; |
| } |
| |
| function copiarPeerId() { |
| if (!peerId) return; |
| navigator.clipboard.writeText(peerId) |
| .then(() => agregarLog('ID copiado al portapapeles ✓', 'info')) |
| .catch(() => { |
| |
| const ta = document.createElement('textarea'); |
| ta.value = peerId; |
| document.body.appendChild(ta); |
| ta.select(); |
| document.execCommand('copy'); |
| document.body.removeChild(ta); |
| agregarLog('ID copiado ✓', 'info'); |
| }); |
| } |
| |
| |
| async function iniciarVigilancia() { |
| const nombre = document.getElementById('nombreInput').value.trim() || 'Paciente'; |
| |
| try { |
| mediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); |
| document.getElementById('video').srcObject = mediaStream; |
| |
| |
| audioContext = new AudioContext(); |
| const src = audioContext.createMediaStreamSource(mediaStream); |
| analyser = audioContext.createAnalyser(); |
| analyser.fftSize = 256; |
| src.connect(analyser); |
| |
| streamActivo = true; |
| document.getElementById('btnIniciar').disabled = true; |
| document.getElementById('btnParar').disabled = false; |
| agregarLog('Cámara y micrófono iniciados', 'info'); |
| |
| |
| iniciarPeer(); |
| |
| intervalId = setInterval(() => enviarDatos(nombre), 8000); |
| enviarDatos(nombre); |
| |
| } catch (err) { |
| agregarLog('Error al acceder a cámara/micro: ' + err.message, 'critico'); |
| } |
| } |
| |
| function pararVigilancia() { |
| streamActivo = false; |
| clearInterval(intervalId); |
| if (mediaStream) { |
| mediaStream.getTracks().forEach(t => t.stop()); |
| mediaStream = null; |
| } |
| if (audioContext) { audioContext.close(); audioContext = null; } |
| if (peer) { peer.destroy(); peer = null; peerId = null; } |
| document.getElementById('video').srcObject = null; |
| document.getElementById('peerIdBox').style.display = 'none'; |
| document.getElementById('btnIniciar').disabled = false; |
| document.getElementById('btnParar').disabled = true; |
| document.getElementById('sonidoFill').style.width = '0%'; |
| document.getElementById('sonidoEstado').textContent = 'INACTIVO'; |
| agregarLog('Vigilancia detenida', 'info'); |
| } |
| |
| function obtenerNivelSonido() { |
| if (!analyser) return { nivel: 0, texto: 'silencio' }; |
| const data = new Uint8Array(analyser.frequencyBinCount); |
| analyser.getByteFrequencyData(data); |
| const avg = data.reduce((a, b) => a + b, 0) / data.length; |
| const pct = Math.min(100, (avg / 128) * 100); |
| document.getElementById('sonidoFill').style.width = pct + '%'; |
| let texto = 'silencio'; |
| if (avg > 60) texto = 'ruido alto'; |
| else if (avg > 30) texto = 'ruido moderado'; |
| else if (avg > 10) texto = 'ruido leve'; |
| document.getElementById('sonidoEstado').textContent = texto.toUpperCase(); |
| return { nivel: Math.round(pct), texto }; |
| } |
| |
| async function enviarDatos(nombre) { |
| if (!streamActivo) return; |
| const sonido = obtenerNivelSonido(); |
| const pulso = Math.floor(60 + Math.random() * 40); |
| const oxigeno = Math.floor(94 + Math.random() * 6); |
| |
| document.getElementById('pulsoValor').innerHTML = pulso + '<span class="constante-unidad">BPM</span>'; |
| document.getElementById('oxigenoValor').innerHTML = oxigeno + '<span class="constante-unidad">%</span>'; |
| |
| try { |
| const resp = await fetch(BACKEND, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| nombre, |
| pulso, |
| oxigeno, |
| sonido_ambiente: sonido.texto, |
| movimiento_respiracion: 'detectado' |
| }) |
| }); |
| const data = await resp.json(); |
| actualizarEstado(data); |
| } catch (e) { |
| agregarLog('Error de conexión con servidor', 'critico'); |
| } |
| } |
| |
| function actualizarEstado(data) { |
| const estado = (data.estado || 'NORMAL').toUpperCase(); |
| const box = document.getElementById('estadoBox'); |
| const valor = document.getElementById('estadoValor'); |
| const motivo = document.getElementById('motivoTexto'); |
| |
| box.className = 'estado-box ' + estado.toLowerCase(); |
| valor.className = 'estado-valor ' + estado.toLowerCase(); |
| valor.textContent = estado; |
| motivo.textContent = data.motivo || ''; |
| |
| if (estado === 'CRITICO' && !alarmaActiva) disparaAlarma(data.motivo); |
| if (estado === 'NORMAL') agregarLog('Estado normal ✓', 'normal'); |
| if (estado === 'CRITICO') agregarLog('⚠ CRÍTICO: ' + data.motivo, 'critico'); |
| } |
| |
| function disparaAlarma(motivo) { |
| alarmaActiva = true; |
| document.getElementById('alarmaMotivo').textContent = motivo; |
| document.getElementById('alarmaOverlay').classList.add('activa'); |
| try { |
| alarmaAudio = new Audio('data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAA=='); |
| alarmaAudio.loop = true; |
| alarmaAudio.play().catch(() => {}); |
| } catch(e) {} |
| } |
| |
| function silenciarAlarma() { |
| alarmaActiva = false; |
| document.getElementById('alarmaOverlay').classList.remove('activa'); |
| if (alarmaAudio) { alarmaAudio.pause(); alarmaAudio = null; } |
| } |
| |
| function agregarLog(msg, tipo = '') { |
| const c = document.getElementById('logContainer'); |
| const el = document.createElement('div'); |
| el.className = 'log-entry ' + tipo; |
| const hora = new Date().toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); |
| el.textContent = '[' + hora + '] ' + msg; |
| c.insertBefore(el, c.firstChild); |
| if (c.children.length > 30) c.removeChild(c.lastChild); |
| } |
| |
| agregarLog('Sistema listo — pulsa INICIAR', 'info'); |
| </script> |
| </body> |
| </html> |
|
|