feat: migrar dashboard a React 19 + TypeScript + Vite + Tailwind v4
- Módulo HITL (/cases): 6 patrones de formularios dinámicos para 53 tipos de caso con validación Zod - Módulo Monitor (/monitor): streaming token-a-token en tiempo real, auto-scroll y notas internas vía WebSocket - Arquitectura híbrida: REST (canal autoritativo) + WebSocket (difusión/streaming) - MSW para desarrollo sin backend, hooks de notificaciones/sonido/título preservados - Backend legacy movido a legacy/, archivos residuales eliminados de raíz
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import { useRef, useEffect, useCallback } from 'react';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Constants
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** C5 frequency in Hz (523.25) */
|
||||
const C5 = 523.25;
|
||||
|
||||
/** E5 frequency in Hz (659.25) */
|
||||
const E5 = 659.25;
|
||||
|
||||
/** Duration of each tone in seconds (120 ms) */
|
||||
const TONE_DURATION = 0.12;
|
||||
|
||||
/** Overall gain / volume (0.08 = 8%) */
|
||||
const VOLUME = 0.08;
|
||||
|
||||
/** Exponential fade-out duration in seconds (450 ms) */
|
||||
const FADE_DURATION = 0.45;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// useSound
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Hook for playing a notification chime using the Web Audio API.
|
||||
*
|
||||
* - Initialises an `AudioContext` lazily on the first user gesture
|
||||
* (click or keydown) to comply with browser autoplay policies.
|
||||
* - Exposes `playNotificationSound()` which plays a two‑tone chime:
|
||||
* C5 (523.25 Hz) for 120 ms → E5 (659.25 Hz) for 120 ms,
|
||||
* with a shared exponential fade‑out envelope (0.08 → 0.001 over 450 ms).
|
||||
* - If the `AudioContext` is suspended, a warning is logged and the
|
||||
* call is silently ignored.
|
||||
*/
|
||||
export function useSound() {
|
||||
const audioCtxRef = useRef<AudioContext | null>(null);
|
||||
const initializedRef = useRef(false);
|
||||
|
||||
// ── Lazy initialisation on first user gesture ──────────────
|
||||
useEffect(() => {
|
||||
const initAudio = () => {
|
||||
if (initializedRef.current) return;
|
||||
|
||||
try {
|
||||
audioCtxRef.current = new AudioContext();
|
||||
initializedRef.current = true;
|
||||
} catch (err) {
|
||||
console.warn('[useSound] Web Audio API is not available:', err);
|
||||
}
|
||||
|
||||
// Remove both listeners after the first gesture
|
||||
window.removeEventListener('click', initAudio);
|
||||
window.removeEventListener('keydown', initAudio);
|
||||
};
|
||||
|
||||
// Attach listeners for first gesture
|
||||
window.addEventListener('click', initAudio, { once: true });
|
||||
window.addEventListener('keydown', initAudio, { once: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('click', initAudio);
|
||||
window.removeEventListener('keydown', initAudio);
|
||||
|
||||
// Close AudioContext on unmount
|
||||
const ctx = audioCtxRef.current;
|
||||
if (ctx) {
|
||||
ctx.close().catch(() => {});
|
||||
audioCtxRef.current = null;
|
||||
}
|
||||
initializedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// ── Play the two‑tone chime ────────────────────────────────
|
||||
const playNotificationSound = useCallback((): void => {
|
||||
const ctx = audioCtxRef.current;
|
||||
|
||||
if (!ctx) {
|
||||
console.warn('[useSound] AudioContext has not been initialised yet');
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.state === 'suspended') {
|
||||
console.warn('[useSound] AudioContext is suspended — cannot play sound');
|
||||
return;
|
||||
}
|
||||
|
||||
const now = ctx.currentTime;
|
||||
|
||||
// Single shared gain node for both oscillators → unified fade‑out
|
||||
const gain = ctx.createGain();
|
||||
gain.gain.setValueAtTime(VOLUME, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + FADE_DURATION);
|
||||
gain.connect(ctx.destination);
|
||||
|
||||
// Helper: create and schedule a single sine‑wave tone
|
||||
const playTone = (frequency: number, startTime: number): void => {
|
||||
const osc = ctx.createOscillator();
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(frequency, startTime);
|
||||
osc.connect(gain);
|
||||
osc.start(startTime);
|
||||
osc.stop(startTime + TONE_DURATION);
|
||||
};
|
||||
|
||||
// Schedule the two tones
|
||||
playTone(C5, now); // C5 starts immediately
|
||||
playTone(E5, now + TONE_DURATION); // E5 starts after C5 ends
|
||||
}, []);
|
||||
|
||||
return { playNotificationSound };
|
||||
}
|
||||
Reference in New Issue
Block a user