130 lines
4.0 KiB
TypeScript
130 lines
4.0 KiB
TypeScript
import {useState, useRef, useEffect} from 'react';
|
|
import useLoginToken from "../Hooks/useLoginToken.tsx";
|
|
import { z } from 'zod/v4';
|
|
|
|
function SessionActive({stopSession} : {stopSession: () => void})
|
|
{
|
|
return <button onClick={stopSession}>Stop session</button>
|
|
}
|
|
|
|
function SessionStopped({startSession} : {startSession: () => void})
|
|
{
|
|
return <button onClick={startSession}>Start session</button>
|
|
}
|
|
|
|
function SessionControl( { isSessionActive, startSession, stopSession }: { isSessionActive: boolean, startSession : () => void, stopSession : () => void })
|
|
{
|
|
return (isSessionActive
|
|
? <SessionActive stopSession={stopSession}/>
|
|
: <SessionStopped startSession={startSession}/>);
|
|
}
|
|
|
|
const EphemeralTokenResponse = z.object({
|
|
ephemeralToken: z.string(),
|
|
expiresAt: z.number(),
|
|
});
|
|
|
|
export function ChatControl()
|
|
{
|
|
const { token } = useLoginToken();
|
|
|
|
const [isSessionActive, setSessionActive] = useState<boolean>(false);
|
|
const [dataChannel, setDataChannel] = useState<RTCDataChannel | null>(null);
|
|
const audioElement = useRef<HTMLAudioElement>(null);
|
|
const peerConnection = useRef<RTCPeerConnection>(null);
|
|
|
|
async function StartSession()
|
|
{
|
|
// Get a session token for OpenAI Realtime API
|
|
const response = await fetch('ephemeral_token', {
|
|
method: "GET",
|
|
headers: {
|
|
Authorization: `Bearer ${token}asdasd`
|
|
},
|
|
});
|
|
if (!response.ok)
|
|
{
|
|
throw new Error(response.statusText);
|
|
}
|
|
|
|
const responseJson:unknown = await response.json();
|
|
const parsedToken = EphemeralTokenResponse.parse(responseJson);
|
|
const ephemeralToken = parsedToken.ephemeralToken;
|
|
|
|
// Create a peer connection
|
|
const pc = new RTCPeerConnection();
|
|
|
|
// Set up to play remote audio from the model
|
|
audioElement.current = document.createElement("audio");
|
|
audioElement.current.autoplay = true;
|
|
pc.ontrack = (e) =>
|
|
{
|
|
if (audioElement.current !== null)
|
|
{
|
|
audioElement.current.srcObject = e.streams[0];
|
|
}
|
|
}
|
|
|
|
// Add local audio track for microphone input in the browser
|
|
const microphoneStream = await navigator.mediaDevices.getUserMedia({audio: true});
|
|
pc.addTrack(microphoneStream.getTracks()[0]);
|
|
|
|
// Set up data channel for sending and receving events
|
|
const dc = pc.createDataChannel("oai-events");
|
|
setDataChannel(dc)
|
|
|
|
// Start the session using the Session Description Protocol (SDP)
|
|
const offer = await pc.createOffer();
|
|
await pc.setLocalDescription(offer);
|
|
|
|
// Start Realtime Session
|
|
const baseUrl = "https://api.openai.com/v1/realtime";
|
|
const model = "gpt-4o-realtime-preview";
|
|
const sdpResponse = await fetch(`${baseUrl}?model=${model}`, {
|
|
method: "POST",
|
|
body: offer.sdp,
|
|
headers: {
|
|
Authorization: `Bearer ${ephemeralToken}`,
|
|
"Content-Type": "application/sdp",
|
|
},
|
|
});
|
|
|
|
const answer:RTCSessionDescriptionInit = {
|
|
type: "answer",
|
|
sdp: await sdpResponse.text(),
|
|
};
|
|
await pc.setRemoteDescription(answer);
|
|
|
|
peerConnection.current = pc;
|
|
}
|
|
|
|
function stopSession()
|
|
{
|
|
if (dataChannel)
|
|
{
|
|
dataChannel.close();
|
|
setDataChannel(null);
|
|
}
|
|
|
|
if (peerConnection.current !== null)
|
|
{
|
|
peerConnection.current.close();
|
|
peerConnection.current = null;
|
|
}
|
|
|
|
setSessionActive(false);
|
|
}
|
|
|
|
useEffect(() =>
|
|
{
|
|
if (dataChannel)
|
|
{
|
|
dataChannel.addEventListener("open", () =>
|
|
{
|
|
setSessionActive(true);
|
|
});
|
|
}
|
|
}, [dataChannel])
|
|
|
|
return <SessionControl isSessionActive={isSessionActive} startSession={() => void StartSession()} stopSession={stopSession}/>;
|
|
} |