UNPKG

@p0llen/speaker-detector-client

Version:

React components and hooks for live speaker detection and mic analysis.

234 lines (230 loc) 8.47 kB
import React, { useState, useEffect, useRef } from "react"; import { useSpeakerDetection } from "../hooks/useSpeakerDetection"; import useListeningMode from "../hooks/useListeningMode"; import { CLIENT_VERSION } from "../lib/constants"; import { withBase } from "../lib/apiBase"; import MicStatus from "./MicStatus"; // ⬅️ unified status component var SpeakerStatus = function SpeakerStatus() { var { // legacy props ignored; backend controls tuning now } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; // Backend-controlled settings (via hook) var { mode: listeningMode, setMode: setListeningMode, intervalMs, threshold, sessionLogging, setSessionLogging, syncing } = useListeningMode("off"); // Responsive layout: switch to single column when container is narrow var containerRef = useRef(null); var [isNarrow, setIsNarrow] = useState(false); useEffect(() => { if (!containerRef.current || typeof ResizeObserver === 'undefined') return; var ro = new ResizeObserver(entries => { for (var e of entries) { setIsNarrow(e.contentRect.width < 720); } }); ro.observe(containerRef.current); return () => ro.disconnect(); }, []); var [sessionId, setSessionId] = useState(null); var genSessionId = () => { var host = typeof window !== 'undefined' && window.location && window.location.host ? String(window.location.host).replace(/[^a-zA-Z0-9.-]/g, '_') : 'unknown-host'; var ts = new Date().toISOString().replace(/[:.]/g, '-'); var rand = Math.random().toString(36).slice(2, 8); return "sdc-web_v".concat(CLIENT_VERSION, "_").concat(host, "_").concat(ts, "_").concat(rand); }; useEffect(() => { if (sessionLogging && listeningMode !== 'off' && !sessionId) { setSessionId(genSessionId()); } if (listeningMode === 'off' && sessionId) { setSessionId(null); } }, [sessionLogging, listeningMode]); var { speaker, confidence, isSpeaking, error, status, backendOnline, altSpeaker, altConfidence } = useSpeakerDetection({ log: true, logOnChangeOnly: true, // frontend no longer tunes; rely on backend mode: listeningMode, intervalMs, threshold, sid: sessionLogging ? sessionId : null }); var [backendVersion, setBackendVersion] = useState(null); // --- Fetch helpers --- var fetchBackendVersion = () => { fetch(withBase("/api/version")).then(res => res.json()).then(data => setBackendVersion(data.version)).catch(() => setBackendVersion(null)); }; useEffect(() => { fetchBackendVersion(); }, []); var backendLabel = backendOnline === null ? "checking…" : backendOnline ? "online" : "unreachable"; // If backend is truly unreachable and the hook flagged error, show a compact error if (error && backendOnline === false) { return /*#__PURE__*/React.createElement("p", { className: "speaker-error" }, "\u274C ", error); } return /*#__PURE__*/React.createElement("div", { className: "speaker-status-panel", ref: containerRef }, /*#__PURE__*/React.createElement("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } }, /*#__PURE__*/React.createElement("h4", { className: "speaker-heading", style: { margin: 0 } }, "\uD83D\uDD0A Speaker Status")), /*#__PURE__*/React.createElement("p", { style: { marginBottom: 8 } }, /*#__PURE__*/React.createElement("strong", null, "Backend:"), " ", backendLabel, backendVersion && /*#__PURE__*/React.createElement(React.Fragment, null, " ", "\xA0|\xA0 ", /*#__PURE__*/React.createElement("strong", null, "Backend v"), backendVersion), "\xA0|\xA0 ", /*#__PURE__*/React.createElement("strong", null, "Client v"), CLIENT_VERSION), /*#__PURE__*/React.createElement("div", { style: { display: 'grid', gap: 32, gridTemplateColumns: isNarrow ? '1fr' : '2fr 1fr' } }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(MicStatus, { backendOnline: backendOnline, status: status, listeningMode: listeningMode, isSpeaking: isSpeaking }), /*#__PURE__*/React.createElement("div", { style: { marginTop: 12, opacity: 0.8 } }, /*#__PURE__*/React.createElement("strong", null, "Mode:"), " ", listeningMode, " \xA0|\xA0 ", /*#__PURE__*/React.createElement("strong", null, "Interval:"), " ", intervalMs, " ms \xA0|\xA0 ", /*#__PURE__*/React.createElement("strong", null, "Threshold:"), " ", typeof threshold === 'number' ? threshold.toFixed(2) : '—', syncing && /*#__PURE__*/React.createElement("span", { style: { marginLeft: 8, fontStyle: 'italic' } }, "syncing\u2026")), /*#__PURE__*/React.createElement("div", { style: { marginTop: 10, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } }, /*#__PURE__*/React.createElement("div", { role: "group", "aria-label": "Detection Mode", style: { display: 'inline-flex', gap: 6 } }, /*#__PURE__*/React.createElement("button", { onClick: () => setListeningMode('off'), disabled: listeningMode === 'off', title: "Stop detection" }, /*#__PURE__*/React.createElement("span", { style: { display: 'inline-block', width: 10, height: 10, background: '#c62828', borderRadius: 2, marginRight: 6, verticalAlign: -1 } }), "Off"), /*#__PURE__*/React.createElement("button", { onClick: () => { if (sessionLogging && !sessionId) setSessionId(genSessionId()); setListeningMode('single'); }, disabled: listeningMode === 'single', title: "Start detection (single mode)" }, "\u25B6\uFE0F Single"), /*#__PURE__*/React.createElement("button", { onClick: () => { if (sessionLogging && !sessionId) setSessionId(genSessionId()); setListeningMode('multi'); }, disabled: listeningMode === 'multi', title: "Start detection (multi mode)" }, "\u25B6\uFE0F Multi")), /*#__PURE__*/React.createElement("label", { style: { marginLeft: 6, display: 'inline-flex', alignItems: 'center', gap: 6 }, title: "When enabled, tag polls with a session id and request backend session logging" }, /*#__PURE__*/React.createElement("input", { type: "checkbox", checked: !!sessionLogging, onChange: e => { var next = e.target.checked; setSessionLogging(next); if (next && listeningMode !== 'off' && !sessionId) { setSessionId(genSessionId()); } } }), "Session logging")), sessionLogging && sessionId && /*#__PURE__*/React.createElement("div", { style: { marginTop: 6, fontSize: 12, opacity: 0.8 } }, /*#__PURE__*/React.createElement("strong", null, "Session ID:"), " ", sessionId)), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { style: { marginBottom: 8, opacity: 0.8 } }, "Status: ", status, " ", isSpeaking ? '• speaking' : ''), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { style: { fontSize: 14, opacity: 0.8 } }, "Detected Speaker"), /*#__PURE__*/React.createElement("div", { style: { fontSize: 28, fontWeight: 700, lineHeight: 1.2, marginTop: 4 } }, speaker || 'None', altSpeaker && (!speaker || speaker === 'unknown') && /*#__PURE__*/React.createElement("span", { style: { fontSize: 14, fontWeight: 500, marginLeft: 8, opacity: 0.8 } }, "Suggestion: ", altSpeaker, typeof altConfidence === 'number' ? " (".concat(Math.round(altConfidence * 100), "%)") : '')), /*#__PURE__*/React.createElement("div", { style: { marginTop: 6, fontSize: 16 } }, /*#__PURE__*/React.createElement("strong", null, "Confidence:"), " ", confidence !== null ? "".concat(Math.round(confidence * 100), "%") : 'N/A'), altSpeaker && /*#__PURE__*/React.createElement("div", { style: { marginTop: 12, fontSize: 14, opacity: 0.8 } }, /*#__PURE__*/React.createElement("span", { style: { fontWeight: 600 } }, "Suggestion:"), ' ', altSpeaker, " ", typeof altConfidence === 'number' ? "(".concat(Math.round(altConfidence * 100), "%)") : ''))))); }; export default SpeakerStatus;