client/components/App.jsx (158 lines of code) (raw):

import { useEffect, useRef, useState } from "react"; import logo from "/assets/openai-logomark.svg"; import EventLog from "./EventLog"; import SessionControls from "./SessionControls"; import ToolPanel from "./ToolPanel"; export default function App() { const [isSessionActive, setIsSessionActive] = useState(false); const [events, setEvents] = useState([]); const [dataChannel, setDataChannel] = useState(null); const peerConnection = useRef(null); const audioElement = useRef(null); async function startSession() { // Get a session token for OpenAI Realtime API const tokenResponse = await fetch("/token"); const data = await tokenResponse.json(); const EPHEMERAL_KEY = data.client_secret.value; // 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) => (audioElement.current.srcObject = e.streams[0]); // Add local audio track for microphone input in the browser const ms = await navigator.mediaDevices.getUserMedia({ audio: true, }); pc.addTrack(ms.getTracks()[0]); // Set up data channel for sending and receiving 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); const baseUrl = "https://api.openai.com/v1/realtime"; const model = "gpt-4o-realtime-preview-2024-12-17"; const sdpResponse = await fetch(`${baseUrl}?model=${model}`, { method: "POST", body: offer.sdp, headers: { Authorization: `Bearer ${EPHEMERAL_KEY}`, "Content-Type": "application/sdp", }, }); const answer = { type: "answer", sdp: await sdpResponse.text(), }; await pc.setRemoteDescription(answer); peerConnection.current = pc; } // Stop current session, clean up peer connection and data channel function stopSession() { if (dataChannel) { dataChannel.close(); } peerConnection.current.getSenders().forEach((sender) => { if (sender.track) { sender.track.stop(); } }); if (peerConnection.current) { peerConnection.current.close(); } setIsSessionActive(false); setDataChannel(null); peerConnection.current = null; } // Send a message to the model function sendClientEvent(message) { if (dataChannel) { const timestamp = new Date().toLocaleTimeString(); message.event_id = message.event_id || crypto.randomUUID(); // send event before setting timestamp since the backend peer doesn't expect this field dataChannel.send(JSON.stringify(message)); // if guard just in case the timestamp exists by miracle if (!message.timestamp) { message.timestamp = timestamp; } setEvents((prev) => [message, ...prev]); } else { console.error( "Failed to send message - no data channel available", message, ); } } // Send a text message to the model function sendTextMessage(message) { const event = { type: "conversation.item.create", item: { type: "message", role: "user", content: [ { type: "input_text", text: message, }, ], }, }; sendClientEvent(event); sendClientEvent({ type: "response.create" }); } // Attach event listeners to the data channel when a new one is created useEffect(() => { if (dataChannel) { // Append new server events to the list dataChannel.addEventListener("message", (e) => { const event = JSON.parse(e.data); if (!event.timestamp) { event.timestamp = new Date().toLocaleTimeString(); } setEvents((prev) => [event, ...prev]); }); // Set session active when the data channel is opened dataChannel.addEventListener("open", () => { setIsSessionActive(true); setEvents([]); }); } }, [dataChannel]); return ( <> <nav className="absolute top-0 left-0 right-0 h-16 flex items-center"> <div className="flex items-center gap-4 w-full m-4 pb-2 border-0 border-b border-solid border-gray-200"> <img style={{ width: "24px" }} src={logo} /> <h1>realtime console</h1> </div> </nav> <main className="absolute top-16 left-0 right-0 bottom-0"> <section className="absolute top-0 left-0 right-[380px] bottom-0 flex"> <section className="absolute top-0 left-0 right-0 bottom-32 px-4 overflow-y-auto"> <EventLog events={events} /> </section> <section className="absolute h-32 left-0 right-0 bottom-0 p-4"> <SessionControls startSession={startSession} stopSession={stopSession} sendClientEvent={sendClientEvent} sendTextMessage={sendTextMessage} events={events} isSessionActive={isSessionActive} /> </section> </section> <section className="absolute top-0 w-[380px] right-0 bottom-0 p-4 pt-0 overflow-y-auto"> <ToolPanel sendClientEvent={sendClientEvent} sendTextMessage={sendTextMessage} events={events} isSessionActive={isSessionActive} /> </section> </main> </> ); }