mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-15 06:58:51 +01:00
✨ feat: move to Socket.IO
This commit is contained in:
parent
7717d3a514
commit
9c0c341dee
9 changed files with 413 additions and 134 deletions
|
|
@ -97,6 +97,7 @@
|
|||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"remark-supersub": "^1.0.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"sse.js": "^2.5.0",
|
||||
"tailwind-merge": "^1.9.1",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,51 @@
|
|||
import React from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { Mic, Phone, PhoneOff } from 'lucide-react';
|
||||
import { Phone, PhoneOff } from 'lucide-react';
|
||||
import { OGDialog, OGDialogContent, Button } from '~/components';
|
||||
import { useWebRTC, useWebSocket, useCall } from '~/hooks';
|
||||
import { useWebSocket, useCall } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
export const Call: React.FC = () => {
|
||||
const { isConnected } = useWebSocket();
|
||||
const { isCalling, startCall, hangUp } = useCall();
|
||||
const { isConnected, sendMessage } = useWebSocket();
|
||||
const { isCalling, isProcessing, startCall, hangUp } = useCall();
|
||||
|
||||
const [open, setOpen] = useRecoilState(store.callDialogOpen(0));
|
||||
|
||||
const [eventLog, setEventLog] = React.useState<string[]>([]);
|
||||
|
||||
const logEvent = (message: string) => {
|
||||
console.log(message);
|
||||
setEventLog((prev) => [...prev, message]);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isConnected) {
|
||||
logEvent('Connected to server.');
|
||||
} else {
|
||||
logEvent('Disconnected from server.');
|
||||
}
|
||||
}, [isConnected]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isCalling) {
|
||||
logEvent('Call started.');
|
||||
} else if (isProcessing) {
|
||||
logEvent('Processing audio...');
|
||||
} else {
|
||||
logEvent('Call ended.');
|
||||
}
|
||||
}, [isCalling, isProcessing]);
|
||||
|
||||
const handleStartCall = () => {
|
||||
logEvent('Attempting to start call...');
|
||||
startCall();
|
||||
};
|
||||
|
||||
const handleHangUp = () => {
|
||||
logEvent('Attempting to hang up call...');
|
||||
hangUp();
|
||||
};
|
||||
|
||||
return (
|
||||
<OGDialog open={open} onOpenChange={setOpen}>
|
||||
<OGDialogContent className="w-96 p-8">
|
||||
|
|
@ -27,24 +63,34 @@ export const Call: React.FC = () => {
|
|||
</span>
|
||||
</div>
|
||||
|
||||
{!isCalling ? (
|
||||
{isCalling ? (
|
||||
<Button
|
||||
onClick={startCall}
|
||||
onClick={handleHangUp}
|
||||
className="flex items-center gap-2 rounded-full bg-red-500 px-6 py-3 text-white hover:bg-red-600"
|
||||
>
|
||||
<PhoneOff size={20} />
|
||||
<span>End Call</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleStartCall}
|
||||
disabled={!isConnected}
|
||||
className="flex items-center gap-2 rounded-full bg-green-500 px-6 py-3 text-white hover:bg-green-600 disabled:opacity-50"
|
||||
>
|
||||
<Phone size={20} />
|
||||
<span>Start Call</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={hangUp}
|
||||
className="flex items-center gap-2 rounded-full bg-red-500 px-6 py-3 text-white hover:bg-red-600"
|
||||
>
|
||||
<PhoneOff size={20} />
|
||||
<span>End Call</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Debugging Information */}
|
||||
<div className="mt-4 w-full rounded-md bg-gray-100 p-4 shadow-sm">
|
||||
<h3 className="mb-2 text-lg font-medium">Event Log</h3>
|
||||
<ul className="h-32 overflow-y-auto text-xs text-gray-600">
|
||||
{eventLog.map((log, index) => (
|
||||
<li key={index}>{log}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const SILENCE_THRESHOLD = -50;
|
|||
const SILENCE_DURATION = 1000;
|
||||
|
||||
const useCall = () => {
|
||||
const { sendMessage: wsMessage } = useWebSocket();
|
||||
const { sendMessage: wsMessage, isConnected } = useWebSocket();
|
||||
const [isCalling, setIsCalling] = useState(false);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
|
|
@ -23,9 +23,7 @@ const useCall = () => {
|
|||
}
|
||||
|
||||
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
|
||||
// Send audio through WebRTC data channel
|
||||
webrtcServiceRef.current?.sendAudioChunk(audioBlob);
|
||||
// Signal processing start via WebSocket
|
||||
wsMessage({ type: 'processing-start' });
|
||||
|
||||
audioChunksRef.current = [];
|
||||
|
|
@ -34,17 +32,18 @@ const useCall = () => {
|
|||
|
||||
const handleRTCMessage = useCallback((message: RTCMessage) => {
|
||||
if (message.type === 'audio-received') {
|
||||
// Backend confirmed audio receipt
|
||||
setIsProcessing(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const startCall = useCallback(async () => {
|
||||
// Initialize WebRTC with message handler
|
||||
if (!isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
webrtcServiceRef.current = new WebRTCService(handleRTCMessage);
|
||||
await webrtcServiceRef.current.initializeCall();
|
||||
|
||||
// Signal call start via WebSocket
|
||||
wsMessage({ type: 'call-start' });
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
|
|
@ -53,7 +52,6 @@ const useCall = () => {
|
|||
analyserRef.current = audioContextRef.current.createAnalyser();
|
||||
source.connect(analyserRef.current);
|
||||
|
||||
// Start VAD monitoring
|
||||
intervalRef.current = window.setInterval(() => {
|
||||
if (!analyserRef.current || !isCalling) {
|
||||
return;
|
||||
|
|
@ -76,7 +74,7 @@ const useCall = () => {
|
|||
}, 100);
|
||||
|
||||
setIsCalling(true);
|
||||
}, [handleRTCMessage, wsMessage, sendAudioChunk]);
|
||||
}, [handleRTCMessage, isConnected, wsMessage, sendAudioChunk, isCalling]);
|
||||
|
||||
const hangUp = useCallback(async () => {
|
||||
if (intervalRef.current) {
|
||||
|
|
|
|||
|
|
@ -1,49 +1,58 @@
|
|||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useGetWebsocketUrlQuery } from 'librechat-data-provider/react-query';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import type { RTCMessage } from '~/common';
|
||||
|
||||
const useWebSocket = () => {
|
||||
const { data: data } = useGetWebsocketUrlQuery();
|
||||
const { data } = useGetWebsocketUrlQuery();
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const socketRef = useRef<Socket | null>(null);
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (!data || !data.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
wsRef.current = new WebSocket(data.url);
|
||||
wsRef.current.onopen = () => setIsConnected(true);
|
||||
wsRef.current.onclose = () => setIsConnected(false);
|
||||
wsRef.current.onerror = (err) => console.error('WebSocket error:', err);
|
||||
socketRef.current = io(data.url, { transports: ['websocket'] });
|
||||
|
||||
wsRef.current.onmessage = (event) => {
|
||||
const msg: RTCMessage = JSON.parse(event.data);
|
||||
switch (msg.type) {
|
||||
case 'transcription':
|
||||
// TODO: Handle transcription update
|
||||
break;
|
||||
case 'llm-response':
|
||||
// TODO: Handle LLM streaming response
|
||||
break;
|
||||
case 'tts-chunk':
|
||||
if (typeof msg.data === 'string') {
|
||||
const audio = new Audio(`data:audio/mp3;base64,${msg.data}`);
|
||||
audio.play().catch(console.error);
|
||||
}
|
||||
break;
|
||||
socketRef.current.on('connect', () => {
|
||||
setIsConnected(true);
|
||||
});
|
||||
|
||||
socketRef.current.on('disconnect', () => {
|
||||
setIsConnected(false);
|
||||
});
|
||||
|
||||
socketRef.current.on('error', (err) => {
|
||||
console.error('Socket.IO error:', err);
|
||||
});
|
||||
|
||||
socketRef.current.on('transcription', (msg: RTCMessage) => {
|
||||
// TODO: Handle transcription update
|
||||
});
|
||||
|
||||
socketRef.current.on('llm-response', (msg: RTCMessage) => {
|
||||
// TODO: Handle LLM streaming response
|
||||
});
|
||||
|
||||
socketRef.current.on('tts-chunk', (msg: RTCMessage) => {
|
||||
if (typeof msg.data === 'string') {
|
||||
const audio = new Audio(`data:audio/mp3;base64,${msg.data}`);
|
||||
audio.play().catch(console.error);
|
||||
}
|
||||
};
|
||||
});
|
||||
}, [data?.url]);
|
||||
|
||||
useEffect(() => {
|
||||
connect();
|
||||
return () => wsRef.current?.close();
|
||||
return () => {
|
||||
socketRef.current?.disconnect();
|
||||
};
|
||||
}, [connect]);
|
||||
|
||||
const sendMessage = useCallback((message: Record<string, unknown>) => {
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.send(JSON.stringify(message));
|
||||
if (socketRef.current?.connected) {
|
||||
socketRef.current.emit('message', message);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue