IAweb / index.html.bak
Tu Nombre
Actualización: Cambio a modelo Llama 3.1 8B
3ea4b4c
Raw
History Blame Contribute Delete
33.1 kB
<!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 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>
<!-- PEER ID BOX -->
<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>
<!-- PEER ID BOX -->
<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;
// ── PEERJS ──────────────────────────────────────────
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(() => {
// fallback
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;
// Análisis de audio
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');
// Iniciar PeerJS para compartir cámara
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>