@p0llen/speaker-detector-client
Version:
React components and hooks for live speaker detection and mic analysis.
234 lines (230 loc) • 8.47 kB
JavaScript
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;