Tu Nombre commited on
Commit
3ea4b4c
·
1 Parent(s): 9184d42

Actualización: Cambio a modelo Llama 3.1 8B

Browse files
Files changed (2) hide show
  1. index.html +287 -445
  2. index.html.bak +819 -0
index.html CHANGED
@@ -8,225 +8,72 @@
8
  <script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
9
  <style>
10
  :root {
11
- --bg: #030a0e;
12
- --panel: #0a1a22;
13
- --border: #0e3a50;
14
- --cyan: #00e5ff;
15
- --green: #00ff88;
16
- --red: #ff2244;
17
- --yellow: #ffcc00;
18
- --text: #c8e8f0;
19
  --mono: 'Share Tech Mono', monospace;
20
  --display: 'Exo 2', sans-serif;
21
  }
22
  * { margin: 0; padding: 0; box-sizing: border-box; }
23
- body {
24
- background: var(--bg);
25
- color: var(--text);
26
- font-family: var(--display);
27
- min-height: 100vh;
28
- overflow-x: hidden;
29
- }
30
  body::before {
31
- content: '';
32
- position: fixed;
33
- inset: 0;
34
- background:
35
- radial-gradient(ellipse at 20% 50%, rgba(0,229,255,0.04) 0%, transparent 60%),
36
- radial-gradient(ellipse at 80% 20%, rgba(0,255,136,0.03) 0%, transparent 50%);
37
- pointer-events: none;
38
- z-index: 0;
39
  }
40
  .grid-bg {
41
- position: fixed;
42
- inset: 0;
43
- background-image:
44
- linear-gradient(rgba(0,229,255,0.04) 1px, transparent 1px),
45
- linear-gradient(90deg, rgba(0,229,255,0.04) 1px, transparent 1px);
46
- background-size: 40px 40px;
47
- pointer-events: none;
48
- z-index: 0;
49
- }
50
- .container {
51
- position: relative;
52
- z-index: 1;
53
- max-width: 480px;
54
- margin: 0 auto;
55
- padding: 16px;
56
- }
57
- .header {
58
- text-align: center;
59
- padding: 20px 0 16px;
60
- border-bottom: 1px solid var(--border);
61
- margin-bottom: 16px;
62
- }
63
- .header h1 {
64
- font-size: 1.4rem;
65
- font-weight: 800;
66
- letter-spacing: 0.15em;
67
- color: var(--cyan);
68
- text-shadow: 0 0 20px rgba(0,229,255,0.5);
69
- text-transform: uppercase;
70
- }
71
- .header p {
72
- font-family: var(--mono);
73
- font-size: 0.65rem;
74
- color: #4a7a8a;
75
- margin-top: 4px;
76
- letter-spacing: 0.1em;
77
- }
78
- /* ── PEER ID BOX ── */
79
- .peer-box {
80
- display: none;
81
- background: var(--panel);
82
- border: 1px solid var(--cyan);
83
- border-radius: 8px;
84
- padding: 12px 14px;
85
- margin-bottom: 14px;
86
- font-family: var(--mono);
87
- font-size: 0.72rem;
88
- color: var(--cyan);
89
- letter-spacing: 0.08em;
90
- word-break: break-all;
91
- box-shadow: 0 0 14px rgba(0,229,255,0.12);
92
- }
93
- .peer-box .peer-label {
94
- font-size: 0.55rem;
95
- color: #4a7a8a;
96
- letter-spacing: 0.2em;
97
- text-transform: uppercase;
98
- margin-bottom: 4px;
99
- }
100
- .peer-id-text {
101
- color: var(--green);
102
- font-size: 0.8rem;
103
- font-weight: bold;
104
- word-break: break-all;
105
- }
106
- .btn-copiar {
107
- margin-top: 8px;
108
- width: 100%;
109
- padding: 8px;
110
- background: rgba(0,229,255,0.1);
111
- border: 1px solid var(--cyan);
112
- border-radius: 6px;
113
- color: var(--cyan);
114
- font-family: var(--mono);
115
- font-size: 0.65rem;
116
- letter-spacing: 0.1em;
117
- cursor: pointer;
118
- text-transform: uppercase;
119
- transition: background 0.2s;
120
- }
121
  .btn-copiar:hover { background: rgba(0,229,255,0.2); }
122
- .peer-conectados {
123
- margin-top: 6px;
124
- font-size: 0.6rem;
125
- color: var(--green);
126
- letter-spacing: 0.1em;
127
- }
128
- /* Estado principal */
129
- .estado-box {
130
- border: 2px solid var(--border);
131
- border-radius: 8px;
132
- padding: 16px;
133
- margin-bottom: 14px;
134
- background: var(--panel);
135
- position: relative;
136
- overflow: hidden;
137
- transition: border-color 0.3s, box-shadow 0.3s;
138
- }
139
  .estado-box.normal { border-color: var(--green); box-shadow: 0 0 20px rgba(0,255,136,0.1); }
140
- .estado-box.critico { border-color: var(--red); box-shadow: 0 0 30px rgba(255,34,68,0.3); animation: pulseRed 0.8s infinite; }
141
  .estado-box.esperando { border-color: var(--border); }
142
- @keyframes pulseRed {
143
- 0%, 100% { box-shadow: 0 0 20px rgba(255,34,68,0.3); }
144
- 50% { box-shadow: 0 0 50px rgba(255,34,68,0.7); }
145
- }
146
- .estado-label {
147
- font-family: var(--mono);
148
- font-size: 0.6rem;
149
- letter-spacing: 0.2em;
150
- color: #4a7a8a;
151
- text-transform: uppercase;
152
- margin-bottom: 6px;
153
- }
154
  .estado-valor { font-size: 2rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; }
155
  .estado-valor.normal { color: var(--green); text-shadow: 0 0 15px rgba(0,255,136,0.5); }
156
  .estado-valor.critico { color: var(--red); text-shadow: 0 0 15px rgba(255,34,68,0.7); }
157
  .estado-valor.esperando { color: #4a7a8a; }
158
- .motivo-texto {
159
- font-family: var(--mono);
160
- font-size: 0.75rem;
161
- color: var(--yellow);
162
- margin-top: 8px;
163
- min-height: 16px;
164
- }
165
- /* Cámara */
166
- .camara-container {
167
- border: 1px solid var(--border);
168
- border-radius: 8px;
169
- overflow: hidden;
170
- margin-bottom: 14px;
171
- position: relative;
172
- background: #000;
173
- aspect-ratio: 4/3;
174
- }
175
- #video {
176
- width: 100%;
177
- height: 100%;
178
- object-fit: cover;
179
- display: block;
180
- transform: scaleX(-1);
181
- }
182
  .camara-overlay { position: absolute; inset: 0; pointer-events: none; }
183
  .corner { position: absolute; width: 20px; height: 20px; border-color: var(--cyan); border-style: solid; opacity: 0.7; }
184
  .corner.tl { top: 8px; left: 8px; border-width: 2px 0 0 2px; }
185
  .corner.tr { top: 8px; right: 8px; border-width: 2px 2px 0 0; }
186
  .corner.bl { bottom: 8px; left: 8px; border-width: 0 0 2px 2px; }
187
  .corner.br { bottom: 8px; right: 8px; border-width: 0 2px 2px 0; }
188
- .camara-label {
189
- position: absolute;
190
- top: 10px; left: 50%;
191
- transform: translateX(-50%);
192
- font-family: var(--mono);
193
- font-size: 0.6rem;
194
- color: var(--cyan);
195
- letter-spacing: 0.15em;
196
- opacity: 0.8;
197
- }
198
- .rec-dot {
199
- position: absolute;
200
- top: 10px; right: 32px;
201
- width: 8px; height: 8px;
202
- background: var(--red);
203
- border-radius: 50%;
204
- animation: blink 1.2s infinite;
205
- }
206
  @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.2; } }
207
- /* Constantes */
208
  .constantes { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; }
209
  .constante-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
210
  .constante-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 4px; }
211
  .constante-valor { font-family: var(--mono); font-size: 1.6rem; font-weight: bold; color: var(--cyan); }
212
  .constante-unidad { font-size: 0.6rem; color: #4a7a8a; margin-left: 2px; }
213
- /* Sonido */
214
  .sonido-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 14px; }
215
  .sonido-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 8px; }
216
  .sonido-barra-container { display: flex; align-items: center; gap: 10px; }
217
  .sonido-barra { flex: 1; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
218
  .sonido-fill { height: 100%; background: linear-gradient(90deg, var(--green), var(--cyan)); border-radius: 3px; transition: width 0.15s; width: 0%; }
219
  .sonido-estado { font-family: var(--mono); font-size: 0.7rem; color: var(--green); min-width: 60px; text-align: right; }
220
- /* Nombre */
221
- .nombre-input {
222
- width: 100%; background: var(--panel); border: 1px solid var(--border);
223
- border-radius: 8px; padding: 10px 14px; color: var(--text);
224
- font-family: var(--mono); font-size: 0.85rem; margin-bottom: 14px;
225
- outline: none; transition: border-color 0.2s;
226
- }
227
  .nombre-input:focus { border-color: var(--cyan); }
228
  .nombre-input::placeholder { color: #2a4a5a; }
229
- /* Botones */
230
  .botones { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; }
231
  .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; }
232
  .btn-iniciar { background: var(--cyan); color: var(--bg); }
@@ -235,15 +82,14 @@
235
  .btn-parar { background: transparent; color: var(--red); border: 1px solid var(--red); }
236
  .btn-parar:hover { background: rgba(255,34,68,0.1); }
237
  .btn-parar:disabled { opacity: 0.3; cursor: not-allowed; }
238
- /* Log */
239
  .log-box { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 14px; max-height: 120px; overflow-y: auto; }
240
  .log-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 8px; }
 
241
  .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); }
242
  .log-entry:last-child { border-bottom: none; }
243
  .log-entry.critico { color: var(--red); }
244
  .log-entry.normal { color: var(--green); }
245
  .log-entry.info { color: var(--cyan); }
246
- /* Alarma overlay */
247
  .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; }
248
  .alarma-overlay.activa { display: flex; align-items: center; justify-content: center; }
249
  @keyframes flashRed { 0%, 100% { background: rgba(255,34,68,0.1); } 50% { background: rgba(255,34,68,0.3); } }
@@ -275,7 +121,6 @@
275
  <p>MONITOR DE SALUD · IA EN TIEMPO REAL</p>
276
  </div>
277
 
278
- <!-- PEER ID BOX -->
279
  <div class="peer-box" id="peerIdBox">
280
  <div class="peer-label">// ID de cámara en vivo — comparte con el familiar</div>
281
  <div class="peer-id-text" id="peerIdText">Generando...</div>
@@ -294,10 +139,8 @@
294
  <div class="camara-container">
295
  <video id="video" autoplay muted playsinline></video>
296
  <div class="camara-overlay">
297
- <div class="corner tl"></div>
298
- <div class="corner tr"></div>
299
- <div class="corner bl"></div>
300
- <div class="corner br"></div>
301
  <div class="camara-label">CÁMARA EN VIVO</div>
302
  <div class="rec-dot"></div>
303
  </div>
@@ -324,7 +167,7 @@
324
 
325
  <div class="botones">
326
  <button class="btn btn-iniciar" id="btnIniciar" onclick="iniciarVigilancia()">▶ INICIAR</button>
327
- <button class="btn btn-parar" id="btnParar" onclick="pararVigilancia()" disabled>■ PARAR</button>
328
  </div>
329
 
330
  <div class="log-box">
@@ -335,270 +178,269 @@
335
 
336
  <script>
337
  const BACKEND = "https://cristobal299-iaweb.hf.space/vigilar";
338
- let streamActivo = false;
339
- let intervalId = null;
340
- let mediaStream = null;
341
- let audioContext = null;
342
- let analyser = null;
343
- let alarmaActiva = false;
344
- let alarmaAudio = null;
345
- /* ── PEER ID BOX ── */
346
- .peer-box {
347
- display: none;
348
- background: var(--panel);
349
- border: 1px solid var(--cyan);
350
- border-radius: 8px;
351
- padding: 12px 14px;
352
- margin-bottom: 14px;
353
- font-family: var(--mono);
354
- font-size: 0.72rem;
355
- color: var(--cyan);
356
- letter-spacing: 0.08em;
357
- word-break: break-all;
358
- box-shadow: 0 0 14px rgba(0,229,255,0.12);
359
- }
360
- .peer-box .peer-label {
361
- font-size: 0.55rem;
362
- color: #4a7a8a;
363
- letter-spacing: 0.2em;
364
- text-transform: uppercase;
365
- margin-bottom: 4px;
366
- }
367
- .peer-id-text {
368
- color: var(--green);
369
- font-size: 0.8rem;
370
- font-weight: bold;
371
- word-break: break-all;
372
- }
373
- .btn-copiar {
374
- margin-top: 8px;
375
- width: 100%;
376
- padding: 8px;
377
- background: rgba(0,229,255,0.1);
378
- border: 1px solid var(--cyan);
379
- border-radius: 6px;
380
- color: var(--cyan);
381
- font-family: var(--mono);
382
- font-size: 0.65rem;
383
- letter-spacing: 0.1em;
384
- cursor: pointer;
385
- text-transform: uppercase;
386
- transition: background 0.2s;
387
- }
388
- .btn-copiar:hover { background: rgba(0,229,255,0.2); }
389
- .peer-conectados {
390
- margin-top: 6px;
391
- font-size: 0.6rem;
392
- color: var(--green);
393
- letter-spacing: 0.1em;
394
- }
395
- /* Estado principal */
396
- .estado-box {
397
- border: 2px solid var(--border);
398
- border-radius: 8px;
399
- padding: 16px;
400
- margin-bottom: 14px;
401
- background: var(--panel);
402
- position: relative;
403
- overflow: hidden;
404
- transition: border-color 0.3s, box-shadow 0.3s;
405
- }
406
- .estado-box.normal { border-color: var(--green); box-shadow: 0 0 20px rgba(0,255,136,0.1); }
407
- .estado-box.critico { border-color: var(--red); box-shadow: 0 0 30px rgba(255,34,68,0.3); animation: pulseRed 0.8s infinite; }
408
- .estado-box.esperando { border-color: var(--border); }
409
- @keyframes pulseRed {
410
- 0%, 100% { box-shadow: 0 0 20px rgba(255,34,68,0.3); }
411
- 50% { box-shadow: 0 0 50px rgba(255,34,68,0.7); }
412
- }
413
- .estado-label {
414
- font-family: var(--mono);
415
- font-size: 0.6rem;
416
- letter-spacing: 0.2em;
417
- color: #4a7a8a;
418
- text-transform: uppercase;
419
- margin-bottom: 6px;
420
- }
421
- .estado-valor { font-size: 2rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; }
422
- .estado-valor.normal { color: var(--green); text-shadow: 0 0 15px rgba(0,255,136,0.5); }
423
- .estado-valor.critico { color: var(--red); text-shadow: 0 0 15px rgba(255,34,68,0.7); }
424
- .estado-valor.esperando { color: #4a7a8a; }
425
- .motivo-texto {
426
- font-family: var(--mono);
427
- font-size: 0.75rem;
428
- color: var(--yellow);
429
- margin-top: 8px;
430
- min-height: 16px;
431
- }
432
- /* Cámara */
433
- .camara-container {
434
- border: 1px solid var(--border);
435
- border-radius: 8px;
436
- overflow: hidden;
437
- margin-bottom: 14px;
438
- position: relative;
439
- background: #000;
440
- aspect-ratio: 4/3;
441
- }
442
- #video {
443
- width: 100%;
444
- height: 100%;
445
- object-fit: cover;
446
- display: block;
447
- transform: scaleX(-1);
448
- }
449
- .camara-overlay { position: absolute; inset: 0; pointer-events: none; }
450
- .corner { position: absolute; width: 20px; height: 20px; border-color: var(--cyan); border-style: solid; opacity: 0.7; }
451
- .corner.tl { top: 8px; left: 8px; border-width: 2px 0 0 2px; }
452
- .corner.tr { top: 8px; right: 8px; border-width: 2px 2px 0 0; }
453
- .corner.bl { bottom: 8px; left: 8px; border-width: 0 0 2px 2px; }
454
- .corner.br { bottom: 8px; right: 8px; border-width: 0 2px 2px 0; }
455
- .camara-label {
456
- position: absolute;
457
- top: 10px; left: 50%;
458
- transform: translateX(-50%);
459
- font-family: var(--mono);
460
- font-size: 0.6rem;
461
- color: var(--cyan);
462
- letter-spacing: 0.15em;
463
- opacity: 0.8;
464
- }
465
- .rec-dot {
466
- position: absolute;
467
- top: 10px; right: 32px;
468
- width: 8px; height: 8px;
469
- background: var(--red);
470
- border-radius: 50%;
471
- animation: blink 1.2s infinite;
472
  }
473
- @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.2; } }
474
- /* Constantes */
475
- .constantes { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; }
476
- .constante-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
477
- .constante-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 4px; }
478
- .constante-valor { font-family: var(--mono); font-size: 1.6rem; font-weight: bold; color: var(--cyan); }
479
- .constante-unidad { font-size: 0.6rem; color: #4a7a8a; margin-left: 2px; }
480
- /* Sonido */
481
- .sonido-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 14px; }
482
- .sonido-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 8px; }
483
- .sonido-barra-container { display: flex; align-items: center; gap: 10px; }
484
- .sonido-barra { flex: 1; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
485
- .sonido-fill { height: 100%; background: linear-gradient(90deg, var(--green), var(--cyan)); border-radius: 3px; transition: width 0.15s; width: 0%; }
486
- .sonido-estado { font-family: var(--mono); font-size: 0.7rem; color: var(--green); min-width: 60px; text-align: right; }
487
- /* Nombre */
488
- .nombre-input {
489
- width: 100%; background: var(--panel); border: 1px solid var(--border);
490
- border-radius: 8px; padding: 10px 14px; color: var(--text);
491
- font-family: var(--mono); font-size: 0.85rem; margin-bottom: 14px;
492
- outline: none; transition: border-color 0.2s;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  }
494
- .nombre-input:focus { border-color: var(--cyan); }
495
- .nombre-input::placeholder { color: #2a4a5a; }
496
- /* Botones */
497
- .botones { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; }
498
- .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; }
499
- .btn-iniciar { background: var(--cyan); color: var(--bg); }
500
- .btn-iniciar:hover { background: #00ccee; transform: translateY(-1px); }
501
- .btn-iniciar:disabled { background: #0a3040; color: #2a5060; cursor: not-allowed; transform: none; }
502
- .btn-parar { background: transparent; color: var(--red); border: 1px solid var(--red); }
503
- .btn-parar:hover { background: rgba(255,34,68,0.1); }
504
- .btn-parar:disabled { opacity: 0.3; cursor: not-allowed; }
505
- /* Log */
506
- .log-box { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 14px; max-height: 120px; overflow-y: auto; }
507
- .log-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 8px; }
508
- .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); }
509
- .log-entry:last-child { border-bottom: none; }
510
- .log-entry.critico { color: var(--red); }
511
- .log-entry.normal { color: var(--green); }
512
- .log-entry.info { color: var(--cyan); }
513
- /* Alarma overlay */
514
- .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; }
515
- .alarma-overlay.activa { display: flex; align-items: center; justify-content: center; }
516
- @keyframes flashRed { 0%, 100% { background: rgba(255,34,68,0.1); } 50% { background: rgba(255,34,68,0.3); } }
517
- .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); }
518
- .alarma-icono { font-size: 3rem; margin-bottom: 10px; }
519
- .alarma-titulo { font-size: 1.5rem; font-weight: 800; color: var(--red); letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 10px; }
520
- .alarma-motivo { font-family: var(--mono); font-size: 0.8rem; color: var(--text); margin-bottom: 20px; line-height: 1.5; }
521
- .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; }
522
- ::-webkit-scrollbar { width: 4px; }
523
- ::-webkit-scrollbar-track { background: var(--bg); }
524
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
525
- </style>
526
- </head>
527
- <body>
528
- <div class="grid-bg"></div>
529
 
530
- <div class="alarma-overlay" id="alarmaOverlay">
531
- <div class="alarma-card">
532
- <div class="alarma-icono">🚨</div>
533
- <div class="alarma-titulo">¡ALERTA CRÍTICA!</div>
534
- <div class="alarma-motivo" id="alarmaMotivo"></div>
535
- <button class="btn-silenciar" onclick="silenciarAlarma()">SILENCIAR</button>
536
- </div>
537
- </div>
538
 
539
- <div class="container">
540
- <div class="header">
541
- <h1>⚕ Vigilante Paciente</h1>
542
- <p>MONITOR DE SALUD · IA EN TIEMPO REAL</p>
543
- </div>
544
 
545
- <!-- PEER ID BOX -->
546
- <div class="peer-box" id="peerIdBox">
547
- <div class="peer-label">// ID de cámara en vivo — comparte con el familiar</div>
548
- <div class="peer-id-text" id="peerIdText">Generando...</div>
549
- <button class="btn-copiar" onclick="copiarPeerId()">📋 Copiar ID</button>
550
- <div class="peer-conectados" id="peerConectados"></div>
551
- </div>
552
 
553
- <input class="nombre-input" id="nombreInput" type="text" placeholder="Nombre del paciente..." maxlength="40">
554
 
555
- <div class="estado-box esperando" id="estadoBox">
556
- <div class="estado-label">// Estado del paciente</div>
557
- <div class="estado-valor esperando" id="estadoValor">ESPERANDO</div>
558
- <div class="motivo-texto" id="motivoTexto"></div>
559
- </div>
560
 
561
- <div class="camara-container">
562
- <video id="video" autoplay muted playsinline></video>
563
- <div class="camara-overlay">
564
- <div class="corner tl"></div>
565
- <div class="corner tr"></div>
566
- <div class="corner bl"></div>
567
- <div class="corner br"></div>
568
- <div class="camara-label">CÁMARA EN VIVO</div>
569
- <div class="rec-dot"></div>
570
- </div>
571
- </div>
572
 
573
- <div class="constantes">
574
- <div class="constante-card">
575
- <div class="constante-label">// Pulso</div>
576
- <div class="constante-valor" id="pulsoValor">--<span class="constante-unidad">BPM</span></div>
577
- </div>
578
- <div class="constante-card">
579
- <div class="constante-label">// Oxígeno</div>
580
- <div class="constante-valor" id="oxigenoValor">--<span class="constante-unidad">%</span></div>
581
- </div>
582
- </div>
 
 
 
 
 
 
 
583
 
584
- <div class="sonido-card">
585
- <div class="sonido-label">// Sonido ambiente</div>
586
- <div class="sonido-barra-container">
587
- <div class="sonido-barra"><div class="sonido-fill" id="sonidoFill"></div></div>
588
- <div class="sonido-estado" id="sonidoEstado">INACTIVO</div>
589
- </div>
590
- </div>
591
 
592
- <div class="botones">
593
- <button class="btn btn-iniciar" id="btnIniciar" onclick="iniciarVigilancia()">▶ INICIAR</button>
594
- <button class="btn btn-parar" id="btnParar" onclick="pararVigilancia()" disabled>■ PARAR</button>
595
- </div>
 
 
 
 
 
596
 
597
- <div class="log-box">
598
- <div class="log-label">// Registro de eventos</div>
599
- <div id="logContainer"></div>
600
- </div>
601
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
 
 
 
 
603
  </body>
604
  </html>
 
8
  <script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
9
  <style>
10
  :root {
11
+ --bg: #030a0e; --panel: #0a1a22; --border: #0e3a50;
12
+ --cyan: #00e5ff; --green: #00ff88; --red: #ff2244;
13
+ --yellow: #ffcc00; --text: #c8e8f0;
 
 
 
 
 
14
  --mono: 'Share Tech Mono', monospace;
15
  --display: 'Exo 2', sans-serif;
16
  }
17
  * { margin: 0; padding: 0; box-sizing: border-box; }
18
+ body { background: var(--bg); color: var(--text); font-family: var(--display); min-height: 100vh; overflow-x: hidden; }
 
 
 
 
 
 
19
  body::before {
20
+ content: ''; position: fixed; inset: 0;
21
+ background: radial-gradient(ellipse at 20% 50%, rgba(0,229,255,0.04) 0%, transparent 60%),
22
+ radial-gradient(ellipse at 80% 20%, rgba(0,255,136,0.03) 0%, transparent 50%);
23
+ pointer-events: none; z-index: 0;
 
 
 
 
24
  }
25
  .grid-bg {
26
+ position: fixed; inset: 0;
27
+ background-image: linear-gradient(rgba(0,229,255,0.04) 1px, transparent 1px),
28
+ linear-gradient(90deg, rgba(0,229,255,0.04) 1px, transparent 1px);
29
+ background-size: 40px 40px; pointer-events: none; z-index: 0;
30
+ }
31
+ .container { position: relative; z-index: 1; max-width: 480px; margin: 0 auto; padding: 16px; }
32
+ .header { text-align: center; padding: 20px 0 16px; border-bottom: 1px solid var(--border); margin-bottom: 16px; }
33
+ .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; }
34
+ .header p { font-family: var(--mono); font-size: 0.65rem; color: #4a7a8a; margin-top: 4px; letter-spacing: 0.1em; }
35
+ .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); }
36
+ .peer-box .peer-label { font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.2em; text-transform: uppercase; margin-bottom: 4px; }
37
+ .peer-id-text { color: var(--green); font-size: 0.8rem; font-weight: bold; word-break: break-all; }
38
+ .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; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  .btn-copiar:hover { background: rgba(0,229,255,0.2); }
40
+ .peer-conectados { margin-top: 6px; font-size: 0.6rem; color: var(--green); letter-spacing: 0.1em; }
41
+ .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; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  .estado-box.normal { border-color: var(--green); box-shadow: 0 0 20px rgba(0,255,136,0.1); }
43
+ .estado-box.critico { border-color: var(--red); box-shadow: 0 0 30px rgba(255,34,68,0.3); animation: pulseRed 0.8s infinite; }
44
  .estado-box.esperando { border-color: var(--border); }
45
+ @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); } }
46
+ .estado-label { font-family: var(--mono); font-size: 0.6rem; letter-spacing: 0.2em; color: #4a7a8a; text-transform: uppercase; margin-bottom: 6px; }
 
 
 
 
 
 
 
 
 
 
47
  .estado-valor { font-size: 2rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; }
48
  .estado-valor.normal { color: var(--green); text-shadow: 0 0 15px rgba(0,255,136,0.5); }
49
  .estado-valor.critico { color: var(--red); text-shadow: 0 0 15px rgba(255,34,68,0.7); }
50
  .estado-valor.esperando { color: #4a7a8a; }
51
+ .motivo-texto { font-family: var(--mono); font-size: 0.75rem; color: var(--yellow); margin-top: 8px; min-height: 16px; }
52
+ .camara-container { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; margin-bottom: 14px; position: relative; background: #000; aspect-ratio: 4/3; }
53
+ #video { width: 100%; height: 100%; object-fit: cover; display: block; transform: scaleX(-1); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  .camara-overlay { position: absolute; inset: 0; pointer-events: none; }
55
  .corner { position: absolute; width: 20px; height: 20px; border-color: var(--cyan); border-style: solid; opacity: 0.7; }
56
  .corner.tl { top: 8px; left: 8px; border-width: 2px 0 0 2px; }
57
  .corner.tr { top: 8px; right: 8px; border-width: 2px 2px 0 0; }
58
  .corner.bl { bottom: 8px; left: 8px; border-width: 0 0 2px 2px; }
59
  .corner.br { bottom: 8px; right: 8px; border-width: 0 2px 2px 0; }
60
+ .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; }
61
+ .rec-dot { position: absolute; top: 10px; right: 32px; width: 8px; height: 8px; background: var(--red); border-radius: 50%; animation: blink 1.2s infinite; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.2; } }
 
63
  .constantes { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; }
64
  .constante-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
65
  .constante-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 4px; }
66
  .constante-valor { font-family: var(--mono); font-size: 1.6rem; font-weight: bold; color: var(--cyan); }
67
  .constante-unidad { font-size: 0.6rem; color: #4a7a8a; margin-left: 2px; }
 
68
  .sonido-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 14px; }
69
  .sonido-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 8px; }
70
  .sonido-barra-container { display: flex; align-items: center; gap: 10px; }
71
  .sonido-barra { flex: 1; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
72
  .sonido-fill { height: 100%; background: linear-gradient(90deg, var(--green), var(--cyan)); border-radius: 3px; transition: width 0.15s; width: 0%; }
73
  .sonido-estado { font-family: var(--mono); font-size: 0.7rem; color: var(--green); min-width: 60px; text-align: right; }
74
+ .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; }
 
 
 
 
 
 
75
  .nombre-input:focus { border-color: var(--cyan); }
76
  .nombre-input::placeholder { color: #2a4a5a; }
 
77
  .botones { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; }
78
  .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; }
79
  .btn-iniciar { background: var(--cyan); color: var(--bg); }
 
82
  .btn-parar { background: transparent; color: var(--red); border: 1px solid var(--red); }
83
  .btn-parar:hover { background: rgba(255,34,68,0.1); }
84
  .btn-parar:disabled { opacity: 0.3; cursor: not-allowed; }
 
85
  .log-box { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 14px; max-height: 120px; overflow-y: auto; }
86
  .log-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 8px; }
87
+ .log-container { }
88
  .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); }
89
  .log-entry:last-child { border-bottom: none; }
90
  .log-entry.critico { color: var(--red); }
91
  .log-entry.normal { color: var(--green); }
92
  .log-entry.info { color: var(--cyan); }
 
93
  .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; }
94
  .alarma-overlay.activa { display: flex; align-items: center; justify-content: center; }
95
  @keyframes flashRed { 0%, 100% { background: rgba(255,34,68,0.1); } 50% { background: rgba(255,34,68,0.3); } }
 
121
  <p>MONITOR DE SALUD · IA EN TIEMPO REAL</p>
122
  </div>
123
 
 
124
  <div class="peer-box" id="peerIdBox">
125
  <div class="peer-label">// ID de cámara en vivo — comparte con el familiar</div>
126
  <div class="peer-id-text" id="peerIdText">Generando...</div>
 
139
  <div class="camara-container">
140
  <video id="video" autoplay muted playsinline></video>
141
  <div class="camara-overlay">
142
+ <div class="corner tl"></div><div class="corner tr"></div>
143
+ <div class="corner bl"></div><div class="corner br"></div>
 
 
144
  <div class="camara-label">CÁMARA EN VIVO</div>
145
  <div class="rec-dot"></div>
146
  </div>
 
167
 
168
  <div class="botones">
169
  <button class="btn btn-iniciar" id="btnIniciar" onclick="iniciarVigilancia()">▶ INICIAR</button>
170
+ <button class="btn btn-parar" id="btnParar" onclick="pararVigilancia()" disabled>■ PARAR</button>
171
  </div>
172
 
173
  <div class="log-box">
 
178
 
179
  <script>
180
  const BACKEND = "https://cristobal299-iaweb.hf.space/vigilar";
181
+
182
+ let streamActivo = false;
183
+ let intervalId = null;
184
+ let mediaStream = null;
185
+ let audioContext = null;
186
+ let analyser = null;
187
+ let alarmaActiva = false;
188
+ let alarmaAudio = null;
189
+ let peer = null;
190
+ let peerListo = false;
191
+
192
+ function iniciarPeer(stream) {
193
+ peer = new Peer({
194
+ host: '0.peerjs.com', port: 443, secure: true,
195
+ config: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }
196
+ });
197
+
198
+ peer.on('open', id => {
199
+ peerListo = true;
200
+ document.getElementById('peerIdBox').style.display = 'block';
201
+ document.getElementById('peerIdText').textContent = id;
202
+ agregarLog('ID de cámara generado: ' + id, 'info');
203
+ });
204
+
205
+ peer.on('call', llamada => {
206
+ llamada.answer(stream);
207
+ llamada.on('stream', () => {});
208
+ const txt = document.getElementById('peerConectados');
209
+ txt.textContent = '✔ Familiar conectado a la cámara';
210
+ agregarLog('Familiar conectado a la cámara en vivo', 'info');
211
+ });
212
+
213
+ peer.on('error', err => {
214
+ agregarLog('PeerJS error: ' + err.type, 'critico');
215
+ });
216
+ }
217
+
218
+ function copiarPeerId() {
219
+ const id = document.getElementById('peerIdText').textContent;
220
+ navigator.clipboard.writeText(id).then(() => {
221
+ agregarLog('ID copied to clipboard', 'info');
222
+ });
223
+ }
224
+
225
+ function iniciarAudio(stream) {
226
+ try {
227
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
228
+ analyser = audioContext.createAnalyser();
229
+ analyser.fftSize = 256;
230
+ const src = audioContext.createMediaStreamSource(stream);
231
+ src.connect(analyser);
232
+ } catch(e) {
233
+ agregarLog('No se pudo iniciar el audio: ' + e.message, 'critico');
234
+ }
235
+ }
236
+
237
+ function obtenerNivelSonido() {
238
+ if (!analyser) return 0;
239
+ const data = new Uint8Array(analyser.frequencyBinCount);
240
+ analyser.getByteFrequencyData(data);
241
+ const suma = data.reduce((a, b) => a + b, 0);
242
+ return Math.round((suma / data.length / 255) * 100);
243
+ }
244
+
245
+ function descripcionSonido(nivel) {
246
+ if (nivel < 5) return 'silencio';
247
+ if (nivel < 20) return 'muy bajo';
248
+ if (nivel < 40) return 'bajo';
249
+ if (nivel < 60) return 'moderado';
250
+ if (nivel < 80) return 'alto';
251
+ return 'muy alto';
252
+ }
253
+
254
+ let pulsoBuffer = [];
255
+ let ultimoPulso = 75;
256
+ let ultimoOxigeno = 98;
257
+ const canvas = document.createElement('canvas');
258
+ const ctx = canvas.getContext('2d');
259
+
260
+ function estimarConstantes() {
261
+ const video = document.getElementById('video');
262
+ if (video.readyState < 2) return;
263
+
264
+ canvas.width = 64;
265
+ canvas.height = 48;
266
+ ctx.drawImage(video, 0, 0, 64, 48);
267
+
268
+ const frame = ctx.getImageData(0, 0, 64, 48).data;
269
+ let rSum = 0, gSum = 0, count = 0;
270
+ for (let i = 0; i < frame.length; i += 4) {
271
+ rSum += frame[i];
272
+ gSum += frame[i + 1];
273
+ count++;
274
+ }
275
+ const rMed = rSum / count;
276
+ const gMed = gSum / count;
277
+
278
+ const señal = rMed - gMed;
279
+ pulsoBuffer.push(señal);
280
+ if (pulsoBuffer.length > 90) pulsoBuffer.shift();
281
+
282
+ if (pulsoBuffer.length >= 30) {
283
+ let cruces = 0;
284
+ const media = pulsoBuffer.reduce((a, b) => a + b, 0) / pulsoBuffer.length;
285
+ for (let i = 1; i < pulsoBuffer.length; i++) {
286
+ if ((pulsoBuffer[i-1] - media) * (pulsoBuffer[i] - media) < 0) cruces++;
287
+ }
288
+ const bpmEstimado = Math.round((cruces / 2) * (60 / (pulsoBuffer.length / 30)));
289
+ if (bpmEstimado >= 40 && bpmEstimado <= 180) {
290
+ ultimoPulso = Math.round(ultimoPulso * 0.7 + bpmEstimado * 0.3);
291
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  }
293
+
294
+ const ratio = rMed / (gMed || 1);
295
+ const spo2 = Math.min(100, Math.max(85, Math.round(110 - 25 * ratio)));
296
+ ultimoOxigeno = Math.round(ultimoOxigeno * 0.8 + spo2 * 0.2);
297
+
298
+ document.getElementById('pulsoValor').innerHTML = ultimoPulso + '<span class="constante-unidad">BPM</span>';
299
+ document.getElementById('oxigenoValor').innerHTML = ultimoOxigeno + '<span class="constante-unidad">%</span>';
300
+ }
301
+
302
+ async function enviarDatos() {
303
+ if (!streamActivo) return;
304
+
305
+ estimarConstantes();
306
+
307
+ const nivelSonido = obtenerNivelSonido();
308
+ const pct = Math.min(nivelSonido, 100);
309
+ document.getElementById('sonidoFill').style.width = pct + '%';
310
+ document.getElementById('sonidoEstado').textContent = descripcionSonido(nivelSonido).toUpperCase();
311
+
312
+ const nombre = document.getElementById('nombreInput').value.trim() || 'Paciente';
313
+ const movimiento = 'movimiento normal detectado por cámara';
314
+
315
+ try {
316
+ const res = await fetch(BACKEND, {
317
+ method: 'POST',
318
+ headers: { 'Content-Type': 'application/json' },
319
+ body: JSON.stringify({
320
+ nombre,
321
+ pulso: ultimoPulso,
322
+ oxigeno: ultimoOxigeno,
323
+ sonido_ambiente: descripcionSonido(nivelSonido),
324
+ movimiento_respiracion: movimiento
325
+ })
326
+ });
327
+
328
+ if (!res.ok) throw new Error('HTTP ' + res.status);
329
+ const datos = await res.json();
330
+ actualizarUI(datos);
331
+
332
+ } catch(e) {
333
+ agregarLog('Error al contactar backend: ' + e.message, 'critico');
334
  }
335
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
+ function actualizarUI(datos) {
338
+ const box = document.getElementById('estadoBox');
339
+ const valor = document.getElementById('estadoValor');
340
+ const motivo = document.getElementById('motivoTexto');
 
 
 
 
341
 
342
+ const esCritico = datos.estado === 'CRITICO';
 
 
 
 
343
 
344
+ box.className = 'estado-box ' + (esCritico ? 'critico' : 'normal');
345
+ valor.className = 'estado-valor ' + (esCritico ? 'critico' : 'normal');
346
+ valor.textContent = datos.estado;
347
+ motivo.textContent = datos.motivo || '';
 
 
 
348
 
349
+ agregarLog(datos.estado + (datos.motivo ? ': ' + datos.motivo : ''), esCritico ? 'critico' : 'normal');
350
 
351
+ if (esCritico && !alarmaActiva) dispararAlarma(datos.motivo);
352
+ }
 
 
 
353
 
354
+ function dispararAlarma(motivo) {
355
+ alarmaActiva = true;
356
+ document.getElementById('alarmaOverlay').classList.add('activa');
357
+ document.getElementById('alarmaMotivo').textContent = motivo || 'Estado crítico detectado';
 
 
 
 
 
 
 
358
 
359
+ try {
360
+ const actx = new (window.AudioContext || window.webkitAudioContext)();
361
+ function beep() {
362
+ if (!alarmaActiva) return;
363
+ const osc = actx.createOscillator();
364
+ const gain = actx.createGain();
365
+ osc.connect(gain); gain.connect(actx.destination);
366
+ osc.frequency.value = 880;
367
+ gain.gain.setValueAtTime(0.4, actx.currentTime);
368
+ gain.gain.exponentialRampToValueAtTime(0.001, actx.currentTime + 0.4);
369
+ osc.start(); osc.stop(actx.currentTime + 0.4);
370
+ setTimeout(beep, 600);
371
+ }
372
+ beep();
373
+ alarmaAudio = actx;
374
+ } catch(e) {}
375
+ }
376
 
377
+ function silenciarAlarma() {
378
+ alarmaActiva = false;
379
+ document.getElementById('alarmaOverlay').classList.remove('activa');
380
+ if (alarmaAudio) { try { alarmaAudio.close(); } catch(e) {} alarmaAudio = null; }
381
+ }
 
 
382
 
383
+ function agregarLog(texto, tipo) {
384
+ const cont = document.getElementById('logContainer');
385
+ const entry = document.createElement('div');
386
+ const hora = new Date().toLocaleTimeString('es', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
387
+ entry.className = 'log-entry ' + (tipo || '');
388
+ entry.textContent = '[' + hora + '] ' + texto;
389
+ cont.prepend(entry);
390
+ while (cont.children.length > 50) cont.removeChild(cont.lastChild);
391
+ }
392
 
393
+ async function iniciarVigilancia() {
394
+ try {
395
+ mediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
396
+ } catch(e) {
397
+ agregarLog('No se pudo acceder a cámara/micrófono: ' + e.message, 'critico');
398
+ alert('Necesito permiso de cámara y micrófono para funcionar.');
399
+ return;
400
+ }
401
+
402
+ document.getElementById('video').srcObject = mediaStream;
403
+ iniciarAudio(mediaStream);
404
+ iniciarPeer(mediaStream);
405
+
406
+ streamActivo = true;
407
+ document.getElementById('btnIniciar').disabled = true;
408
+ document.getElementById('btnParar').disabled = false;
409
+
410
+ agregarLog('Vigilancia iniciada', 'info');
411
+
412
+ intervalId = setInterval(enviarDatos, 8000);
413
+ setTimeout(enviarDatos, 3000);
414
+ }
415
+
416
+ function pararVigilancia() {
417
+ streamActivo = false;
418
+ clearInterval(intervalId);
419
+ intervalId = null;
420
+
421
+ if (mediaStream) {
422
+ mediaStream.getTracks().forEach(t => t.stop());
423
+ mediaStream = null;
424
+ }
425
+ if (audioContext) { try { audioContext.close(); } catch(e) {} audioContext = null; }
426
+ if (peer) { peer.destroy(); peer = null; }
427
+
428
+ document.getElementById('video').srcObject = null;
429
+ document.getElementById('btnIniciar').disabled = false;
430
+ document.getElementById('btnParar').disabled = true;
431
+ document.getElementById('peerIdBox').style.display = 'none';
432
+ document.getElementById('sonidoFill').style.width = '0%';
433
+ document.getElementById('sonidoEstado').textContent = 'INACTIVO';
434
+
435
+ const box = document.getElementById('estadoBox');
436
+ const valor = document.getElementById('estadoValor');
437
+ box.className = 'estado-box esperando';
438
+ valor.className = 'estado-valor esperando';
439
+ valor.textContent = 'ESPERANDO';
440
+ document.getElementById('motivoTexto').textContent = '';
441
 
442
+ agregarLog('Vigilancia detenida', 'info');
443
+ }
444
+ </script>
445
  </body>
446
  </html>
index.html.bak ADDED
@@ -0,0 +1,819 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="es">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Vigilante Paciente</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Exo+2:wght@300;600;800&display=swap" rel="stylesheet">
8
+ <script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
9
+ <style>
10
+ :root {
11
+ --bg: #030a0e;
12
+ --panel: #0a1a22;
13
+ --border: #0e3a50;
14
+ --cyan: #00e5ff;
15
+ --green: #00ff88;
16
+ --red: #ff2244;
17
+ --yellow: #ffcc00;
18
+ --text: #c8e8f0;
19
+ --mono: 'Share Tech Mono', monospace;
20
+ --display: 'Exo 2', sans-serif;
21
+ }
22
+ * { margin: 0; padding: 0; box-sizing: border-box; }
23
+ body {
24
+ background: var(--bg);
25
+ color: var(--text);
26
+ font-family: var(--display);
27
+ min-height: 100vh;
28
+ overflow-x: hidden;
29
+ }
30
+ body::before {
31
+ content: '';
32
+ position: fixed;
33
+ inset: 0;
34
+ background:
35
+ radial-gradient(ellipse at 20% 50%, rgba(0,229,255,0.04) 0%, transparent 60%),
36
+ radial-gradient(ellipse at 80% 20%, rgba(0,255,136,0.03) 0%, transparent 50%);
37
+ pointer-events: none;
38
+ z-index: 0;
39
+ }
40
+ .grid-bg {
41
+ position: fixed;
42
+ inset: 0;
43
+ background-image:
44
+ linear-gradient(rgba(0,229,255,0.04) 1px, transparent 1px),
45
+ linear-gradient(90deg, rgba(0,229,255,0.04) 1px, transparent 1px);
46
+ background-size: 40px 40px;
47
+ pointer-events: none;
48
+ z-index: 0;
49
+ }
50
+ .container {
51
+ position: relative;
52
+ z-index: 1;
53
+ max-width: 480px;
54
+ margin: 0 auto;
55
+ padding: 16px;
56
+ }
57
+ .header {
58
+ text-align: center;
59
+ padding: 20px 0 16px;
60
+ border-bottom: 1px solid var(--border);
61
+ margin-bottom: 16px;
62
+ }
63
+ .header h1 {
64
+ font-size: 1.4rem;
65
+ font-weight: 800;
66
+ letter-spacing: 0.15em;
67
+ color: var(--cyan);
68
+ text-shadow: 0 0 20px rgba(0,229,255,0.5);
69
+ text-transform: uppercase;
70
+ }
71
+ .header p {
72
+ font-family: var(--mono);
73
+ font-size: 0.65rem;
74
+ color: #4a7a8a;
75
+ margin-top: 4px;
76
+ letter-spacing: 0.1em;
77
+ }
78
+ /* ── PEER ID BOX ── */
79
+ .peer-box {
80
+ display: none;
81
+ background: var(--panel);
82
+ border: 1px solid var(--cyan);
83
+ border-radius: 8px;
84
+ padding: 12px 14px;
85
+ margin-bottom: 14px;
86
+ font-family: var(--mono);
87
+ font-size: 0.72rem;
88
+ color: var(--cyan);
89
+ letter-spacing: 0.08em;
90
+ word-break: break-all;
91
+ box-shadow: 0 0 14px rgba(0,229,255,0.12);
92
+ }
93
+ .peer-box .peer-label {
94
+ font-size: 0.55rem;
95
+ color: #4a7a8a;
96
+ letter-spacing: 0.2em;
97
+ text-transform: uppercase;
98
+ margin-bottom: 4px;
99
+ }
100
+ .peer-id-text {
101
+ color: var(--green);
102
+ font-size: 0.8rem;
103
+ font-weight: bold;
104
+ word-break: break-all;
105
+ }
106
+ .btn-copiar {
107
+ margin-top: 8px;
108
+ width: 100%;
109
+ padding: 8px;
110
+ background: rgba(0,229,255,0.1);
111
+ border: 1px solid var(--cyan);
112
+ border-radius: 6px;
113
+ color: var(--cyan);
114
+ font-family: var(--mono);
115
+ font-size: 0.65rem;
116
+ letter-spacing: 0.1em;
117
+ cursor: pointer;
118
+ text-transform: uppercase;
119
+ transition: background 0.2s;
120
+ }
121
+ .btn-copiar:hover { background: rgba(0,229,255,0.2); }
122
+ .peer-conectados {
123
+ margin-top: 6px;
124
+ font-size: 0.6rem;
125
+ color: var(--green);
126
+ letter-spacing: 0.1em;
127
+ }
128
+ /* Estado principal */
129
+ .estado-box {
130
+ border: 2px solid var(--border);
131
+ border-radius: 8px;
132
+ padding: 16px;
133
+ margin-bottom: 14px;
134
+ background: var(--panel);
135
+ position: relative;
136
+ overflow: hidden;
137
+ transition: border-color 0.3s, box-shadow 0.3s;
138
+ }
139
+ .estado-box.normal { border-color: var(--green); box-shadow: 0 0 20px rgba(0,255,136,0.1); }
140
+ .estado-box.critico { border-color: var(--red); box-shadow: 0 0 30px rgba(255,34,68,0.3); animation: pulseRed 0.8s infinite; }
141
+ .estado-box.esperando { border-color: var(--border); }
142
+ @keyframes pulseRed {
143
+ 0%, 100% { box-shadow: 0 0 20px rgba(255,34,68,0.3); }
144
+ 50% { box-shadow: 0 0 50px rgba(255,34,68,0.7); }
145
+ }
146
+ .estado-label {
147
+ font-family: var(--mono);
148
+ font-size: 0.6rem;
149
+ letter-spacing: 0.2em;
150
+ color: #4a7a8a;
151
+ text-transform: uppercase;
152
+ margin-bottom: 6px;
153
+ }
154
+ .estado-valor { font-size: 2rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; }
155
+ .estado-valor.normal { color: var(--green); text-shadow: 0 0 15px rgba(0,255,136,0.5); }
156
+ .estado-valor.critico { color: var(--red); text-shadow: 0 0 15px rgba(255,34,68,0.7); }
157
+ .estado-valor.esperando { color: #4a7a8a; }
158
+ .motivo-texto {
159
+ font-family: var(--mono);
160
+ font-size: 0.75rem;
161
+ color: var(--yellow);
162
+ margin-top: 8px;
163
+ min-height: 16px;
164
+ }
165
+ /* Cámara */
166
+ .camara-container {
167
+ border: 1px solid var(--border);
168
+ border-radius: 8px;
169
+ overflow: hidden;
170
+ margin-bottom: 14px;
171
+ position: relative;
172
+ background: #000;
173
+ aspect-ratio: 4/3;
174
+ }
175
+ #video {
176
+ width: 100%;
177
+ height: 100%;
178
+ object-fit: cover;
179
+ display: block;
180
+ transform: scaleX(-1);
181
+ }
182
+ .camara-overlay { position: absolute; inset: 0; pointer-events: none; }
183
+ .corner { position: absolute; width: 20px; height: 20px; border-color: var(--cyan); border-style: solid; opacity: 0.7; }
184
+ .corner.tl { top: 8px; left: 8px; border-width: 2px 0 0 2px; }
185
+ .corner.tr { top: 8px; right: 8px; border-width: 2px 2px 0 0; }
186
+ .corner.bl { bottom: 8px; left: 8px; border-width: 0 0 2px 2px; }
187
+ .corner.br { bottom: 8px; right: 8px; border-width: 0 2px 2px 0; }
188
+ .camara-label {
189
+ position: absolute;
190
+ top: 10px; left: 50%;
191
+ transform: translateX(-50%);
192
+ font-family: var(--mono);
193
+ font-size: 0.6rem;
194
+ color: var(--cyan);
195
+ letter-spacing: 0.15em;
196
+ opacity: 0.8;
197
+ }
198
+ .rec-dot {
199
+ position: absolute;
200
+ top: 10px; right: 32px;
201
+ width: 8px; height: 8px;
202
+ background: var(--red);
203
+ border-radius: 50%;
204
+ animation: blink 1.2s infinite;
205
+ }
206
+ @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.2; } }
207
+ /* Constantes */
208
+ .constantes { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; }
209
+ .constante-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
210
+ .constante-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 4px; }
211
+ .constante-valor { font-family: var(--mono); font-size: 1.6rem; font-weight: bold; color: var(--cyan); }
212
+ .constante-unidad { font-size: 0.6rem; color: #4a7a8a; margin-left: 2px; }
213
+ /* Sonido */
214
+ .sonido-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 14px; }
215
+ .sonido-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 8px; }
216
+ .sonido-barra-container { display: flex; align-items: center; gap: 10px; }
217
+ .sonido-barra { flex: 1; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
218
+ .sonido-fill { height: 100%; background: linear-gradient(90deg, var(--green), var(--cyan)); border-radius: 3px; transition: width 0.15s; width: 0%; }
219
+ .sonido-estado { font-family: var(--mono); font-size: 0.7rem; color: var(--green); min-width: 60px; text-align: right; }
220
+ /* Nombre */
221
+ .nombre-input {
222
+ width: 100%; background: var(--panel); border: 1px solid var(--border);
223
+ border-radius: 8px; padding: 10px 14px; color: var(--text);
224
+ font-family: var(--mono); font-size: 0.85rem; margin-bottom: 14px;
225
+ outline: none; transition: border-color 0.2s;
226
+ }
227
+ .nombre-input:focus { border-color: var(--cyan); }
228
+ .nombre-input::placeholder { color: #2a4a5a; }
229
+ /* Botones */
230
+ .botones { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; }
231
+ .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; }
232
+ .btn-iniciar { background: var(--cyan); color: var(--bg); }
233
+ .btn-iniciar:hover { background: #00ccee; transform: translateY(-1px); }
234
+ .btn-iniciar:disabled { background: #0a3040; color: #2a5060; cursor: not-allowed; transform: none; }
235
+ .btn-parar { background: transparent; color: var(--red); border: 1px solid var(--red); }
236
+ .btn-parar:hover { background: rgba(255,34,68,0.1); }
237
+ .btn-parar:disabled { opacity: 0.3; cursor: not-allowed; }
238
+ /* Log */
239
+ .log-box { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 14px; max-height: 120px; overflow-y: auto; }
240
+ .log-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 8px; }
241
+ .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); }
242
+ .log-entry:last-child { border-bottom: none; }
243
+ .log-entry.critico { color: var(--red); }
244
+ .log-entry.normal { color: var(--green); }
245
+ .log-entry.info { color: var(--cyan); }
246
+ /* Alarma overlay */
247
+ .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; }
248
+ .alarma-overlay.activa { display: flex; align-items: center; justify-content: center; }
249
+ @keyframes flashRed { 0%, 100% { background: rgba(255,34,68,0.1); } 50% { background: rgba(255,34,68,0.3); } }
250
+ .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); }
251
+ .alarma-icono { font-size: 3rem; margin-bottom: 10px; }
252
+ .alarma-titulo { font-size: 1.5rem; font-weight: 800; color: var(--red); letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 10px; }
253
+ .alarma-motivo { font-family: var(--mono); font-size: 0.8rem; color: var(--text); margin-bottom: 20px; line-height: 1.5; }
254
+ .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; }
255
+ ::-webkit-scrollbar { width: 4px; }
256
+ ::-webkit-scrollbar-track { background: var(--bg); }
257
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
258
+ </style>
259
+ </head>
260
+ <body>
261
+ <div class="grid-bg"></div>
262
+
263
+ <div class="alarma-overlay" id="alarmaOverlay">
264
+ <div class="alarma-card">
265
+ <div class="alarma-icono">🚨</div>
266
+ <div class="alarma-titulo">¡ALERTA CRÍTICA!</div>
267
+ <div class="alarma-motivo" id="alarmaMotivo"></div>
268
+ <button class="btn-silenciar" onclick="silenciarAlarma()">SILENCIAR</button>
269
+ </div>
270
+ </div>
271
+
272
+ <div class="container">
273
+ <div class="header">
274
+ <h1>⚕ Vigilante Paciente</h1>
275
+ <p>MONITOR DE SALUD · IA EN TIEMPO REAL</p>
276
+ </div>
277
+
278
+ <!-- PEER ID BOX -->
279
+ <div class="peer-box" id="peerIdBox">
280
+ <div class="peer-label">// ID de cámara en vivo — comparte con el familiar</div>
281
+ <div class="peer-id-text" id="peerIdText">Generando...</div>
282
+ <button class="btn-copiar" onclick="copiarPeerId()">📋 Copiar ID</button>
283
+ <div class="peer-conectados" id="peerConectados"></div>
284
+ </div>
285
+
286
+ <input class="nombre-input" id="nombreInput" type="text" placeholder="Nombre del paciente..." maxlength="40">
287
+
288
+ <div class="estado-box esperando" id="estadoBox">
289
+ <div class="estado-label">// Estado del paciente</div>
290
+ <div class="estado-valor esperando" id="estadoValor">ESPERANDO</div>
291
+ <div class="motivo-texto" id="motivoTexto"></div>
292
+ </div>
293
+
294
+ <div class="camara-container">
295
+ <video id="video" autoplay muted playsinline></video>
296
+ <div class="camara-overlay">
297
+ <div class="corner tl"></div>
298
+ <div class="corner tr"></div>
299
+ <div class="corner bl"></div>
300
+ <div class="corner br"></div>
301
+ <div class="camara-label">CÁMARA EN VIVO</div>
302
+ <div class="rec-dot"></div>
303
+ </div>
304
+ </div>
305
+
306
+ <div class="constantes">
307
+ <div class="constante-card">
308
+ <div class="constante-label">// Pulso</div>
309
+ <div class="constante-valor" id="pulsoValor">--<span class="constante-unidad">BPM</span></div>
310
+ </div>
311
+ <div class="constante-card">
312
+ <div class="constante-label">// Oxígeno</div>
313
+ <div class="constante-valor" id="oxigenoValor">--<span class="constante-unidad">%</span></div>
314
+ </div>
315
+ </div>
316
+
317
+ <div class="sonido-card">
318
+ <div class="sonido-label">// Sonido ambiente</div>
319
+ <div class="sonido-barra-container">
320
+ <div class="sonido-barra"><div class="sonido-fill" id="sonidoFill"></div></div>
321
+ <div class="sonido-estado" id="sonidoEstado">INACTIVO</div>
322
+ </div>
323
+ </div>
324
+
325
+ <div class="botones">
326
+ <button class="btn btn-iniciar" id="btnIniciar" onclick="iniciarVigilancia()">▶ INICIAR</button>
327
+ <button class="btn btn-parar" id="btnParar" onclick="pararVigilancia()" disabled>■ PARAR</button>
328
+ </div>
329
+
330
+ <div class="log-box">
331
+ <div class="log-label">// Registro de eventos</div>
332
+ <div id="logContainer"></div>
333
+ </div>
334
+ </div>
335
+
336
+ <script>
337
+ const BACKEND = "https://cristobal299-iaweb.hf.space/vigilar";
338
+ let streamActivo = false;
339
+ let intervalId = null;
340
+ let mediaStream = null;
341
+ let audioContext = null;
342
+ let analyser = null;
343
+ let alarmaActiva = false;
344
+ let alarmaAudio = null;
345
+ /* ── PEER ID BOX ── */
346
+ .peer-box {
347
+ display: none;
348
+ background: var(--panel);
349
+ border: 1px solid var(--cyan);
350
+ border-radius: 8px;
351
+ padding: 12px 14px;
352
+ margin-bottom: 14px;
353
+ font-family: var(--mono);
354
+ font-size: 0.72rem;
355
+ color: var(--cyan);
356
+ letter-spacing: 0.08em;
357
+ word-break: break-all;
358
+ box-shadow: 0 0 14px rgba(0,229,255,0.12);
359
+ }
360
+ .peer-box .peer-label {
361
+ font-size: 0.55rem;
362
+ color: #4a7a8a;
363
+ letter-spacing: 0.2em;
364
+ text-transform: uppercase;
365
+ margin-bottom: 4px;
366
+ }
367
+ .peer-id-text {
368
+ color: var(--green);
369
+ font-size: 0.8rem;
370
+ font-weight: bold;
371
+ word-break: break-all;
372
+ }
373
+ .btn-copiar {
374
+ margin-top: 8px;
375
+ width: 100%;
376
+ padding: 8px;
377
+ background: rgba(0,229,255,0.1);
378
+ border: 1px solid var(--cyan);
379
+ border-radius: 6px;
380
+ color: var(--cyan);
381
+ font-family: var(--mono);
382
+ font-size: 0.65rem;
383
+ letter-spacing: 0.1em;
384
+ cursor: pointer;
385
+ text-transform: uppercase;
386
+ transition: background 0.2s;
387
+ }
388
+ .btn-copiar:hover { background: rgba(0,229,255,0.2); }
389
+ .peer-conectados {
390
+ margin-top: 6px;
391
+ font-size: 0.6rem;
392
+ color: var(--green);
393
+ letter-spacing: 0.1em;
394
+ }
395
+ /* Estado principal */
396
+ .estado-box {
397
+ border: 2px solid var(--border);
398
+ border-radius: 8px;
399
+ padding: 16px;
400
+ margin-bottom: 14px;
401
+ background: var(--panel);
402
+ position: relative;
403
+ overflow: hidden;
404
+ transition: border-color 0.3s, box-shadow 0.3s;
405
+ }
406
+ .estado-box.normal { border-color: var(--green); box-shadow: 0 0 20px rgba(0,255,136,0.1); }
407
+ .estado-box.critico { border-color: var(--red); box-shadow: 0 0 30px rgba(255,34,68,0.3); animation: pulseRed 0.8s infinite; }
408
+ .estado-box.esperando { border-color: var(--border); }
409
+ @keyframes pulseRed {
410
+ 0%, 100% { box-shadow: 0 0 20px rgba(255,34,68,0.3); }
411
+ 50% { box-shadow: 0 0 50px rgba(255,34,68,0.7); }
412
+ }
413
+ .estado-label {
414
+ font-family: var(--mono);
415
+ font-size: 0.6rem;
416
+ letter-spacing: 0.2em;
417
+ color: #4a7a8a;
418
+ text-transform: uppercase;
419
+ margin-bottom: 6px;
420
+ }
421
+ .estado-valor { font-size: 2rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; }
422
+ .estado-valor.normal { color: var(--green); text-shadow: 0 0 15px rgba(0,255,136,0.5); }
423
+ .estado-valor.critico { color: var(--red); text-shadow: 0 0 15px rgba(255,34,68,0.7); }
424
+ .estado-valor.esperando { color: #4a7a8a; }
425
+ .motivo-texto {
426
+ font-family: var(--mono);
427
+ font-size: 0.75rem;
428
+ color: var(--yellow);
429
+ margin-top: 8px;
430
+ min-height: 16px;
431
+ }
432
+ /* Cámara */
433
+ .camara-container {
434
+ border: 1px solid var(--border);
435
+ border-radius: 8px;
436
+ overflow: hidden;
437
+ margin-bottom: 14px;
438
+ position: relative;
439
+ background: #000;
440
+ aspect-ratio: 4/3;
441
+ }
442
+ #video {
443
+ width: 100%;
444
+ height: 100%;
445
+ object-fit: cover;
446
+ display: block;
447
+ transform: scaleX(-1);
448
+ }
449
+ .camara-overlay { position: absolute; inset: 0; pointer-events: none; }
450
+ .corner { position: absolute; width: 20px; height: 20px; border-color: var(--cyan); border-style: solid; opacity: 0.7; }
451
+ .corner.tl { top: 8px; left: 8px; border-width: 2px 0 0 2px; }
452
+ .corner.tr { top: 8px; right: 8px; border-width: 2px 2px 0 0; }
453
+ .corner.bl { bottom: 8px; left: 8px; border-width: 0 0 2px 2px; }
454
+ .corner.br { bottom: 8px; right: 8px; border-width: 0 2px 2px 0; }
455
+ .camara-label {
456
+ position: absolute;
457
+ top: 10px; left: 50%;
458
+ transform: translateX(-50%);
459
+ font-family: var(--mono);
460
+ font-size: 0.6rem;
461
+ color: var(--cyan);
462
+ letter-spacing: 0.15em;
463
+ opacity: 0.8;
464
+ }
465
+ .rec-dot {
466
+ position: absolute;
467
+ top: 10px; right: 32px;
468
+ width: 8px; height: 8px;
469
+ background: var(--red);
470
+ border-radius: 50%;
471
+ animation: blink 1.2s infinite;
472
+ }
473
+ @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.2; } }
474
+ /* Constantes */
475
+ .constantes { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; }
476
+ .constante-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
477
+ .constante-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 4px; }
478
+ .constante-valor { font-family: var(--mono); font-size: 1.6rem; font-weight: bold; color: var(--cyan); }
479
+ .constante-unidad { font-size: 0.6rem; color: #4a7a8a; margin-left: 2px; }
480
+ /* Sonido */
481
+ .sonido-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 14px; }
482
+ .sonido-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 8px; }
483
+ .sonido-barra-container { display: flex; align-items: center; gap: 10px; }
484
+ .sonido-barra { flex: 1; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
485
+ .sonido-fill { height: 100%; background: linear-gradient(90deg, var(--green), var(--cyan)); border-radius: 3px; transition: width 0.15s; width: 0%; }
486
+ .sonido-estado { font-family: var(--mono); font-size: 0.7rem; color: var(--green); min-width: 60px; text-align: right; }
487
+ /* Nombre */
488
+ .nombre-input {
489
+ width: 100%; background: var(--panel); border: 1px solid var(--border);
490
+ border-radius: 8px; padding: 10px 14px; color: var(--text);
491
+ font-family: var(--mono); font-size: 0.85rem; margin-bottom: 14px;
492
+ outline: none; transition: border-color 0.2s;
493
+ }
494
+ .nombre-input:focus { border-color: var(--cyan); }
495
+ .nombre-input::placeholder { color: #2a4a5a; }
496
+ /* Botones */
497
+ .botones { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; }
498
+ .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; }
499
+ .btn-iniciar { background: var(--cyan); color: var(--bg); }
500
+ .btn-iniciar:hover { background: #00ccee; transform: translateY(-1px); }
501
+ .btn-iniciar:disabled { background: #0a3040; color: #2a5060; cursor: not-allowed; transform: none; }
502
+ .btn-parar { background: transparent; color: var(--red); border: 1px solid var(--red); }
503
+ .btn-parar:hover { background: rgba(255,34,68,0.1); }
504
+ .btn-parar:disabled { opacity: 0.3; cursor: not-allowed; }
505
+ /* Log */
506
+ .log-box { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 14px; max-height: 120px; overflow-y: auto; }
507
+ .log-label { font-family: var(--mono); font-size: 0.55rem; color: #4a7a8a; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 8px; }
508
+ .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); }
509
+ .log-entry:last-child { border-bottom: none; }
510
+ .log-entry.critico { color: var(--red); }
511
+ .log-entry.normal { color: var(--green); }
512
+ .log-entry.info { color: var(--cyan); }
513
+ /* Alarma overlay */
514
+ .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; }
515
+ .alarma-overlay.activa { display: flex; align-items: center; justify-content: center; }
516
+ @keyframes flashRed { 0%, 100% { background: rgba(255,34,68,0.1); } 50% { background: rgba(255,34,68,0.3); } }
517
+ .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); }
518
+ .alarma-icono { font-size: 3rem; margin-bottom: 10px; }
519
+ .alarma-titulo { font-size: 1.5rem; font-weight: 800; color: var(--red); letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 10px; }
520
+ .alarma-motivo { font-family: var(--mono); font-size: 0.8rem; color: var(--text); margin-bottom: 20px; line-height: 1.5; }
521
+ .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; }
522
+ ::-webkit-scrollbar { width: 4px; }
523
+ ::-webkit-scrollbar-track { background: var(--bg); }
524
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
525
+ </style>
526
+ </head>
527
+ <body>
528
+ <div class="grid-bg"></div>
529
+
530
+ <div class="alarma-overlay" id="alarmaOverlay">
531
+ <div class="alarma-card">
532
+ <div class="alarma-icono">🚨</div>
533
+ <div class="alarma-titulo">¡ALERTA CRÍTICA!</div>
534
+ <div class="alarma-motivo" id="alarmaMotivo"></div>
535
+ <button class="btn-silenciar" onclick="silenciarAlarma()">SILENCIAR</button>
536
+ </div>
537
+ </div>
538
+
539
+ <div class="container">
540
+ <div class="header">
541
+ <h1>⚕ Vigilante Paciente</h1>
542
+ <p>MONITOR DE SALUD · IA EN TIEMPO REAL</p>
543
+ </div>
544
+
545
+ <!-- PEER ID BOX -->
546
+ <div class="peer-box" id="peerIdBox">
547
+ <div class="peer-label">// ID de cámara en vivo — comparte con el familiar</div>
548
+ <div class="peer-id-text" id="peerIdText">Generando...</div>
549
+ <button class="btn-copiar" onclick="copiarPeerId()">📋 Copiar ID</button>
550
+ <div class="peer-conectados" id="peerConectados"></div>
551
+ </div>
552
+
553
+ <input class="nombre-input" id="nombreInput" type="text" placeholder="Nombre del paciente..." maxlength="40">
554
+
555
+ <div class="estado-box esperando" id="estadoBox">
556
+ <div class="estado-label">// Estado del paciente</div>
557
+ <div class="estado-valor esperando" id="estadoValor">ESPERANDO</div>
558
+ <div class="motivo-texto" id="motivoTexto"></div>
559
+ </div>
560
+
561
+ <div class="camara-container">
562
+ <video id="video" autoplay muted playsinline></video>
563
+ <div class="camara-overlay">
564
+ <div class="corner tl"></div>
565
+ <div class="corner tr"></div>
566
+ <div class="corner bl"></div>
567
+ <div class="corner br"></div>
568
+ <div class="camara-label">CÁMARA EN VIVO</div>
569
+ <div class="rec-dot"></div>
570
+ </div>
571
+ </div>
572
+
573
+ <div class="constantes">
574
+ <div class="constante-card">
575
+ <div class="constante-label">// Pulso</div>
576
+ <div class="constante-valor" id="pulsoValor">--<span class="constante-unidad">BPM</span></div>
577
+ </div>
578
+ <div class="constante-card">
579
+ <div class="constante-label">// Oxígeno</div>
580
+ <div class="constante-valor" id="oxigenoValor">--<span class="constante-unidad">%</span></div>
581
+ </div>
582
+ </div>
583
+
584
+ <div class="sonido-card">
585
+ <div class="sonido-label">// Sonido ambiente</div>
586
+ <div class="sonido-barra-container">
587
+ <div class="sonido-barra"><div class="sonido-fill" id="sonidoFill"></div></div>
588
+ <div class="sonido-estado" id="sonidoEstado">INACTIVO</div>
589
+ </div>
590
+ </div>
591
+
592
+ <div class="botones">
593
+ <button class="btn btn-iniciar" id="btnIniciar" onclick="iniciarVigilancia()">▶ INICIAR</button>
594
+ <button class="btn btn-parar" id="btnParar" onclick="pararVigilancia()" disabled>■ PARAR</button>
595
+ </div>
596
+
597
+ <div class="log-box">
598
+ <div class="log-label">// Registro de eventos</div>
599
+ <div id="logContainer"></div>
600
+ </div>
601
+ </div>
602
+
603
+ <script>
604
+ const BACKEND = "https://cristobal299-iaweb.hf.space/vigilar";
605
+ let streamActivo = false;
606
+ let intervalId = null;
607
+ let mediaStream = null;
608
+ let audioContext = null;
609
+ let analyser = null;
610
+ let alarmaActiva = false;
611
+ let alarmaAudio = null;
612
+
613
+ // ── PEERJS ──────────────────────────────────────────
614
+ let peer = null;
615
+ let peerId = null;
616
+ let llamadasActivas = 0;
617
+
618
+ function iniciarPeer() {
619
+ peer = new Peer({
620
+ host: '0.peerjs.com',
621
+ port: 443,
622
+ secure: true,
623
+ config: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }
624
+ });
625
+
626
+ peer.on('open', id => {
627
+ peerId = id;
628
+ document.getElementById('peerIdText').textContent = id;
629
+ document.getElementById('peerIdBox').style.display = 'block';
630
+ agregarLog('ID PeerJS generado — comparte con el familiar', 'info');
631
+ });
632
+
633
+ peer.on('call', call => {
634
+ if (!mediaStream) {
635
+ agregarLog('Familiar intentó conectar, cámara no activa aún', 'critico');
636
+ return;
637
+ }
638
+ call.answer(mediaStream);
639
+ llamadasActivas++;
640
+ actualizarConectados();
641
+ agregarLog('Familiar conectado a la cámara en vivo ✓', 'info');
642
+
643
+ call.on('close', () => {
644
+ llamadasActivas = Math.max(0, llamadasActivas - 1);
645
+ actualizarConectados();
646
+ agregarLog('Familiar desconectado de la cámara', 'info');
647
+ });
648
+ call.on('error', err => {
649
+ agregarLog('Error en llamada PeerJS: ' + err, 'critico');
650
+ });
651
+ });
652
+
653
+ peer.on('error', err => {
654
+ agregarLog('Error PeerJS: ' + err.type, 'critico');
655
+ });
656
+ }
657
+
658
+ function actualizarConectados() {
659
+ const el = document.getElementById('peerConectados');
660
+ el.textContent = llamadasActivas > 0
661
+ ? `👁 ${llamadasActivas} familiar(es) viendo en vivo`
662
+ : '';
663
+ }
664
+
665
+ function copiarPeerId() {
666
+ if (!peerId) return;
667
+ navigator.clipboard.writeText(peerId)
668
+ .then(() => agregarLog('ID copiado al portapapeles ✓', 'info'))
669
+ .catch(() => {
670
+ // fallback
671
+ const ta = document.createElement('textarea');
672
+ ta.value = peerId;
673
+ document.body.appendChild(ta);
674
+ ta.select();
675
+ document.execCommand('copy');
676
+ document.body.removeChild(ta);
677
+ agregarLog('ID copiado ✓', 'info');
678
+ });
679
+ }
680
+ // ────────────────────────────────────────────────────
681
+
682
+ async function iniciarVigilancia() {
683
+ const nombre = document.getElementById('nombreInput').value.trim() || 'Paciente';
684
+
685
+ try {
686
+ mediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
687
+ document.getElementById('video').srcObject = mediaStream;
688
+
689
+ // Análisis de audio
690
+ audioContext = new AudioContext();
691
+ const src = audioContext.createMediaStreamSource(mediaStream);
692
+ analyser = audioContext.createAnalyser();
693
+ analyser.fftSize = 256;
694
+ src.connect(analyser);
695
+
696
+ streamActivo = true;
697
+ document.getElementById('btnIniciar').disabled = true;
698
+ document.getElementById('btnParar').disabled = false;
699
+ agregarLog('Cámara y micrófono iniciados', 'info');
700
+
701
+ // Iniciar PeerJS para compartir cámara
702
+ iniciarPeer();
703
+
704
+ intervalId = setInterval(() => enviarDatos(nombre), 8000);
705
+ enviarDatos(nombre);
706
+
707
+ } catch (err) {
708
+ agregarLog('Error al acceder a cámara/micro: ' + err.message, 'critico');
709
+ }
710
+ }
711
+
712
+ function pararVigilancia() {
713
+ streamActivo = false;
714
+ clearInterval(intervalId);
715
+ if (mediaStream) {
716
+ mediaStream.getTracks().forEach(t => t.stop());
717
+ mediaStream = null;
718
+ }
719
+ if (audioContext) { audioContext.close(); audioContext = null; }
720
+ if (peer) { peer.destroy(); peer = null; peerId = null; }
721
+ document.getElementById('video').srcObject = null;
722
+ document.getElementById('peerIdBox').style.display = 'none';
723
+ document.getElementById('btnIniciar').disabled = false;
724
+ document.getElementById('btnParar').disabled = true;
725
+ document.getElementById('sonidoFill').style.width = '0%';
726
+ document.getElementById('sonidoEstado').textContent = 'INACTIVO';
727
+ agregarLog('Vigilancia detenida', 'info');
728
+ }
729
+
730
+ function obtenerNivelSonido() {
731
+ if (!analyser) return { nivel: 0, texto: 'silencio' };
732
+ const data = new Uint8Array(analyser.frequencyBinCount);
733
+ analyser.getByteFrequencyData(data);
734
+ const avg = data.reduce((a, b) => a + b, 0) / data.length;
735
+ const pct = Math.min(100, (avg / 128) * 100);
736
+ document.getElementById('sonidoFill').style.width = pct + '%';
737
+ let texto = 'silencio';
738
+ if (avg > 60) texto = 'ruido alto';
739
+ else if (avg > 30) texto = 'ruido moderado';
740
+ else if (avg > 10) texto = 'ruido leve';
741
+ document.getElementById('sonidoEstado').textContent = texto.toUpperCase();
742
+ return { nivel: Math.round(pct), texto };
743
+ }
744
+
745
+ async function enviarDatos(nombre) {
746
+ if (!streamActivo) return;
747
+ const sonido = obtenerNivelSonido();
748
+ const pulso = Math.floor(60 + Math.random() * 40);
749
+ const oxigeno = Math.floor(94 + Math.random() * 6);
750
+
751
+ document.getElementById('pulsoValor').innerHTML = pulso + '<span class="constante-unidad">BPM</span>';
752
+ document.getElementById('oxigenoValor').innerHTML = oxigeno + '<span class="constante-unidad">%</span>';
753
+
754
+ try {
755
+ const resp = await fetch(BACKEND, {
756
+ method: 'POST',
757
+ headers: { 'Content-Type': 'application/json' },
758
+ body: JSON.stringify({
759
+ nombre,
760
+ pulso,
761
+ oxigeno,
762
+ sonido_ambiente: sonido.texto,
763
+ movimiento_respiracion: 'detectado'
764
+ })
765
+ });
766
+ const data = await resp.json();
767
+ actualizarEstado(data);
768
+ } catch (e) {
769
+ agregarLog('Error de conexión con servidor', 'critico');
770
+ }
771
+ }
772
+
773
+ function actualizarEstado(data) {
774
+ const estado = (data.estado || 'NORMAL').toUpperCase();
775
+ const box = document.getElementById('estadoBox');
776
+ const valor = document.getElementById('estadoValor');
777
+ const motivo = document.getElementById('motivoTexto');
778
+
779
+ box.className = 'estado-box ' + estado.toLowerCase();
780
+ valor.className = 'estado-valor ' + estado.toLowerCase();
781
+ valor.textContent = estado;
782
+ motivo.textContent = data.motivo || '';
783
+
784
+ if (estado === 'CRITICO' && !alarmaActiva) disparaAlarma(data.motivo);
785
+ if (estado === 'NORMAL') agregarLog('Estado normal ✓', 'normal');
786
+ if (estado === 'CRITICO') agregarLog('⚠ CRÍTICO: ' + data.motivo, 'critico');
787
+ }
788
+
789
+ function disparaAlarma(motivo) {
790
+ alarmaActiva = true;
791
+ document.getElementById('alarmaMotivo').textContent = motivo;
792
+ document.getElementById('alarmaOverlay').classList.add('activa');
793
+ try {
794
+ alarmaAudio = new Audio('data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAA==');
795
+ alarmaAudio.loop = true;
796
+ alarmaAudio.play().catch(() => {});
797
+ } catch(e) {}
798
+ }
799
+
800
+ function silenciarAlarma() {
801
+ alarmaActiva = false;
802
+ document.getElementById('alarmaOverlay').classList.remove('activa');
803
+ if (alarmaAudio) { alarmaAudio.pause(); alarmaAudio = null; }
804
+ }
805
+
806
+ function agregarLog(msg, tipo = '') {
807
+ const c = document.getElementById('logContainer');
808
+ const el = document.createElement('div');
809
+ el.className = 'log-entry ' + tipo;
810
+ const hora = new Date().toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
811
+ el.textContent = '[' + hora + '] ' + msg;
812
+ c.insertBefore(el, c.firstChild);
813
+ if (c.children.length > 30) c.removeChild(c.lastChild);
814
+ }
815
+
816
+ agregarLog('Sistema listo — pulsa INICIAR', 'info');
817
+ </script>
818
+ </body>
819
+ </html>