Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 31 additions & 20 deletions webapp/app/api/session/route.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
import { NextResponse } from "next/server";

export async function GET() {
try {
const r = await fetch("https://api.openai.com/v1/realtime/sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4o-realtime-preview-2025-06-03",
voice: "verse",
}),
});

if (!r.ok) {
const error = await r.text();
return Response.json({ error }, { status: 500 });
const response = await fetch(
"https://api.openai.com/v1/realtime/sessions",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4o-realtime-preview-2025-06-03",
}),
}
);
if (!response.ok) {
let errorBody;
try {
errorBody = await response.json();
} catch (e) {
errorBody = { error: response.statusText || 'Unknown error' };
}
console.error("Upstream API error:", response.status, errorBody);
return NextResponse.json(errorBody, { status: response.status });
}

const data = await r.json();
return Response.json(data);
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error("Error creating realtime session:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
console.error("Error in /session:", error);
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 }
);
}
}
10 changes: 9 additions & 1 deletion webapp/components/call-interface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import React, { useState, useEffect } from "react";
import TopBar from "@/components/top-bar";
import { Dialog, DialogTrigger, DialogContent } from "@/components/ui/dialog";
import { Dialog, DialogTrigger, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { Settings, Mic } from "lucide-react";
import VoiceMiniApp from "@/components/voice-mini-app";
import SessionConfigurationPanel from "@/components/session-configuration-panel";
Expand Down Expand Up @@ -130,6 +130,10 @@ const CallInterface = () => {
<div className="h-screen bg-white flex flex-col">
<Dialog open={voiceAppOpen} onOpenChange={setVoiceAppOpen}>
<DialogContent className="max-w-sm w-full">
{/* Accessibility: DialogTitle for screen readers */}
<span style={{position: 'absolute', width: 1, height: 1, padding: 0, margin: -1, overflow: 'hidden', clip: 'rect(0,0,0,0)', border: 0}}>
<DialogTitle>Voice Mini App</DialogTitle>
</span>
<VoiceMiniApp />
</DialogContent>
</Dialog>
Expand Down Expand Up @@ -157,6 +161,10 @@ const CallInterface = () => {
</DialogTrigger>
</TopBar>
<DialogContent className="max-w-md w-full sm:max-w-lg">
{/* Accessibility: DialogTitle for screen readers */}
<span style={{position: 'absolute', width: 1, height: 1, padding: 0, margin: -1, overflow: 'hidden', clip: 'rect(0,0,0,0)', border: 0}}>
<DialogTitle>Session Settings</DialogTitle>
</span>
<div className="space-y-5">

<SessionConfigurationPanel
Expand Down
10 changes: 9 additions & 1 deletion webapp/components/voice-mini-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,16 @@ const VoiceMiniApp = () => {
await connect({
getEphemeralKey: async () => {
const res = await fetch("/api/session");
if (!res.ok) {
const error = await res.text();
throw new Error(error || "Failed to fetch session");
}
const data = await res.json();
return data?.client_secret?.value;
const ek = data?.client_secret?.value;
if (!ek) {
throw new Error("No ephemeral key returned");
}
return ek;
},
initialAgents: [agent],
audioElement: audioRef.current ?? undefined,
Expand Down