mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-27 05:38:51 +01:00
✨ feat: enhance call connection quality metrics with detailed statistics display; fix: package-lock
This commit is contained in:
parent
25bd556933
commit
20a2a20a6b
3 changed files with 113 additions and 6164 deletions
|
|
@ -9,6 +9,9 @@ import {
|
|||
Volume2,
|
||||
VolumeX,
|
||||
Activity,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Wifi,
|
||||
} from 'lucide-react';
|
||||
import { OGDialog, OGDialogContent, Button } from '~/components';
|
||||
import { useWebSocket, useCall } from '~/hooks';
|
||||
|
|
@ -26,6 +29,7 @@ export const Call: React.FC = () => {
|
|||
localStream,
|
||||
remoteStream,
|
||||
connectionQuality,
|
||||
connectionMetrics,
|
||||
isMuted,
|
||||
toggleMute,
|
||||
} = useCall();
|
||||
|
|
@ -33,6 +37,7 @@ export const Call: React.FC = () => {
|
|||
const [open, setOpen] = useRecoilState(store.callDialogOpen(0));
|
||||
const [eventLog, setEventLog] = React.useState<string[]>([]);
|
||||
const [isAudioEnabled, setIsAudioEnabled] = React.useState(true);
|
||||
const [showMetrics, setShowMetrics] = React.useState(false);
|
||||
|
||||
const remoteAudioRef = useRef<HTMLAudioElement>(null);
|
||||
|
||||
|
|
@ -101,6 +106,38 @@ export const Call: React.FC = () => {
|
|||
const isActive = callState === CallState.ACTIVE;
|
||||
const isError = callState === CallState.ERROR;
|
||||
|
||||
const getQualityColor = (quality: string) => {
|
||||
switch (quality) {
|
||||
case 'excellent':
|
||||
return 'bg-emerald-100 text-emerald-700';
|
||||
case 'good':
|
||||
return 'bg-green-100 text-green-700';
|
||||
case 'fair':
|
||||
return 'bg-yellow-100 text-yellow-700';
|
||||
case 'poor':
|
||||
return 'bg-orange-100 text-orange-700';
|
||||
case 'bad':
|
||||
return 'bg-red-100 text-red-700';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-700';
|
||||
}
|
||||
};
|
||||
|
||||
const getQualityIcon = (quality: string) => {
|
||||
switch (quality) {
|
||||
case 'excellent':
|
||||
case 'good':
|
||||
return <Wifi size={16} />;
|
||||
case 'fair':
|
||||
case 'poor':
|
||||
return <Wifi size={16} className="opacity-75" />;
|
||||
case 'bad':
|
||||
return <Wifi size={16} className="opacity-50" />;
|
||||
default:
|
||||
return <Activity size={16} />;
|
||||
}
|
||||
};
|
||||
|
||||
// TESTS
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -152,18 +189,41 @@ export const Call: React.FC = () => {
|
|||
|
||||
{isActive && (
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-full px-4 py-2 ${
|
||||
(connectionQuality === 'good' && 'bg-green-100 text-green-700') ||
|
||||
(connectionQuality === 'poor' && 'bg-yellow-100 text-yellow-700') ||
|
||||
'bg-gray-100 text-gray-700'
|
||||
}`}
|
||||
className={`flex items-center gap-2 rounded-full px-4 py-2 ${getQualityColor(connectionQuality)}`}
|
||||
onClick={() => setShowMetrics(!showMetrics)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
title="Click to show detailed metrics"
|
||||
>
|
||||
<Activity size={16} />
|
||||
{getQualityIcon(connectionQuality)}
|
||||
<span className="text-sm font-medium capitalize">{connectionQuality} Quality</span>
|
||||
{showMetrics ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quality Metrics Panel */}
|
||||
{isActive && showMetrics && (
|
||||
<div className="w-full rounded-md bg-surface-secondary p-3 text-sm shadow-inner">
|
||||
<h4 className="mb-2 font-medium">Connection Metrics</h4>
|
||||
<ul className="space-y-1 text-text-secondary">
|
||||
<li className="flex justify-between">
|
||||
<span>Round Trip Time:</span>
|
||||
<span className="font-mono">{(connectionMetrics.rtt * 1000).toFixed(1)} ms</span>
|
||||
</li>
|
||||
<li className="flex justify-between">
|
||||
<span>Packet Loss:</span>
|
||||
<span className="font-mono">{connectionMetrics.packetsLost?.toFixed(2)}%</span>
|
||||
</li>
|
||||
<li className="flex justify-between">
|
||||
<span>Jitter:</span>
|
||||
<span className="font-mono">
|
||||
{((connectionMetrics.jitter ?? 0) * 1000).toFixed(1)} ms
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="flex w-full items-center gap-2 rounded-md bg-red-100 p-3 text-red-700">
|
||||
|
|
|
|||
|
|
@ -21,7 +21,12 @@ interface CallStatus {
|
|||
error: CallError | null;
|
||||
localStream: MediaStream | null;
|
||||
remoteStream: MediaStream | null;
|
||||
connectionQuality: 'good' | 'poor' | 'unknown';
|
||||
connectionQuality: 'excellent' | 'good' | 'fair' | 'poor' | 'bad' | 'unknown';
|
||||
connectionMetrics: {
|
||||
rtt?: number;
|
||||
packetsLost?: number;
|
||||
jitter?: number;
|
||||
};
|
||||
isUserSpeaking: boolean;
|
||||
remoteAISpeaking: boolean;
|
||||
}
|
||||
|
|
@ -33,6 +38,7 @@ const INITIAL_STATUS: CallStatus = {
|
|||
localStream: null,
|
||||
remoteStream: null,
|
||||
connectionQuality: 'unknown',
|
||||
connectionMetrics: {},
|
||||
isUserSpeaking: false,
|
||||
remoteAISpeaking: false,
|
||||
};
|
||||
|
|
@ -133,18 +139,56 @@ const useCall = () => {
|
|||
}
|
||||
|
||||
let totalRoundTripTime = 0;
|
||||
let totalPacketsLost = 0;
|
||||
let totalPackets = 0;
|
||||
let totalJitter = 0;
|
||||
let samplesCount = 0;
|
||||
let samplesJitterCount = 0;
|
||||
|
||||
stats.forEach((report) => {
|
||||
if (report.type === 'candidate-pair' && report.currentRoundTripTime) {
|
||||
totalRoundTripTime += report.currentRoundTripTime;
|
||||
samplesCount++;
|
||||
}
|
||||
|
||||
if (report.type === 'inbound-rtp' && report.kind === 'audio') {
|
||||
if (report.packetsLost !== undefined && report.packetsReceived !== undefined) {
|
||||
totalPacketsLost += report.packetsLost;
|
||||
totalPackets += report.packetsReceived + report.packetsLost;
|
||||
}
|
||||
|
||||
if (report.jitter !== undefined) {
|
||||
totalJitter += report.jitter;
|
||||
samplesJitterCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const averageRTT = samplesCount > 0 ? totalRoundTripTime / samplesCount : 0;
|
||||
const packetLossRate = totalPackets > 0 ? (totalPacketsLost / totalPackets) * 100 : 0;
|
||||
const averageJitter = samplesJitterCount > 0 ? totalJitter / samplesJitterCount : 0;
|
||||
|
||||
let quality: CallStatus['connectionQuality'] = 'unknown';
|
||||
|
||||
if (averageRTT < 0.15 && packetLossRate < 0.5 && averageJitter < 0.015) {
|
||||
quality = 'excellent';
|
||||
} else if (averageRTT < 0.25 && packetLossRate < 2 && averageJitter < 0.025) {
|
||||
quality = 'good';
|
||||
} else if (averageRTT < 0.4 && packetLossRate < 5 && averageJitter < 0.04) {
|
||||
quality = 'fair';
|
||||
} else if (averageRTT < 0.6 && packetLossRate < 10 && averageJitter < 0.06) {
|
||||
quality = 'poor';
|
||||
} else if (averageRTT >= 0.6 || packetLossRate >= 10 || averageJitter >= 0.06) {
|
||||
quality = 'bad';
|
||||
}
|
||||
|
||||
updateStatus({
|
||||
connectionQuality: averageRTT < 0.3 ? 'good' : 'poor',
|
||||
connectionQuality: quality,
|
||||
connectionMetrics: {
|
||||
rtt: averageRTT,
|
||||
packetsLost: packetLossRate,
|
||||
jitter: averageJitter,
|
||||
},
|
||||
});
|
||||
}, 2000);
|
||||
}, [updateStatus]);
|
||||
|
|
|
|||
6157
package-lock.json
generated
6157
package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue