// vita-telemed.jsx — Telemedicine (patient POV): orchestrator + waiting room + camera hook
// HealthSings modeled as the underlying RTC/API layer. Vita is the patient's copilot.
// ── Camera hook (real getUserMedia, graceful fallback) ──────────
function useCamera(active) {
const videoRef = React.useRef(null);
const streamRef = React.useRef(null);
const [status, setStatus] = React.useState('idle'); // idle | granted | denied | unavailable
const [camOn, setCamOn] = React.useState(true);
const [micOn, setMicOn] = React.useState(true);
React.useEffect(() => {
let cancelled = false;
if (!active) return;
async function start() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { setStatus('unavailable'); return; }
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 } }, audio: true });
if (cancelled) { stream.getTracks().forEach(t => t.stop()); return; }
streamRef.current = stream;
setStatus('granted');
if (videoRef.current) { videoRef.current.srcObject = stream; videoRef.current.play().catch(() => {}); }
} catch (e) { setStatus('denied'); }
}
start();
return () => { cancelled = true; if (streamRef.current) { streamRef.current.getTracks().forEach(t => t.stop()); streamRef.current = null; } };
}, [active]);
const toggleCam = () => {
const s = streamRef.current; if (!s) { setCamOn(c => !c); return; }
s.getVideoTracks().forEach(t => { t.enabled = !t.enabled; }); setCamOn(s.getVideoTracks()[0] ? s.getVideoTracks()[0].enabled : false);
};
const toggleMic = () => {
const s = streamRef.current; if (!s) { setMicOn(m => !m); return; }
s.getAudioTracks().forEach(t => { t.enabled = !t.enabled; }); setMicOn(s.getAudioTracks()[0] ? s.getAudioTracks()[0].enabled : false);
};
return { videoRef, status, camOn, micOn, toggleCam, toggleMic };
}
// ── HealthSings connection chip (technical layer) ───────────────
function HealthSingsChip({ t, state = 'secure', compact }) {
const map = {
connecting: { label: 'Conectando · HealthSings', color: '#E6A93C', bars: 1 },
secure: { label: 'Sala segura · HealthSings RTC', color: '#73C58E', bars: 3 },
e2e: { label: 'Cifrado E2E · HealthSings', color: '#73C58E', bars: 3 },
};
const s = map[state] || map.secure;
return (
{[5, 8, 11].map((h, i) => )}
{s.label}
);
}
// ── Waiting room ────────────────────────────────────────────────
function WaitingRoom({ t, persona, profile, prep, prepLoading, cam, cita, onEnter, onCancel }) {
const accent = persona.accent;
const [phase, setPhase] = React.useState('prep'); // prep -> ready
const steps = ['Estableciendo sala segura con HealthSings', 'Enviando tu resumen clínico al médico', 'Preparando tu acompañamiento con Vita'];
const [stepN, setStepN] = React.useState(0);
React.useEffect(() => {
const iv = setInterval(() => setStepN(x => { if (x >= steps.length) { clearInterval(iv); return x; } return x + 1; }), 850);
const tm = setTimeout(() => setPhase('ready'), 3100);
return () => { clearInterval(iv); clearTimeout(tm); };
}, []);
return (
{/* camera preview as background */}
{cam.status !== 'granted' && (
{Icon.video({ s: 38, c: 'rgba(255,255,255,0.3)' })}
{cam.status === 'denied' ? 'Cámara desactivada' : 'Activando cámara…'}
)}
{/* top */}
{/* doctor card */}
LG
Dra. Liliana Gómez
{cita ? cita.esp || 'Cardiología' : 'Cardiología'} · Telemedicina
{/* prep / vita brief */}
{phase === 'prep' ? (
Vita está preparando tu consulta
{steps.map((s, i) => (
= stepN ? '2px solid rgba(255,255,255,0.2)' : 'none' }}>{i < stepN ? Icon.check({ s: 12, c: '#fff', w: 3 }) : }
{s}
))}
) : (
Todo listo. Yo te acompaño.
Le compartí tu historia a la doctora.
{/* what to tell the doctor — the patient's cheat sheet */}
No olvides contarle
{(prep ? prep.paraContar : []).slice(0, 3).map((p, i) => (
{p.sintoma}{p.detalle ? ' — ' + p.detalle : ''}
))}
)}
{/* mic/cam pre-toggles */}
);
}
function PreToggle({ on, onClick, icon, off, label }) {
return (
);
}
Object.assign(window, { useCamera, HealthSingsChip, WaitingRoom, PreToggle });