branch:
client.tsx
5019 bytesRaw
import type { RealtimeItem } from "@openai/agents/realtime";
import { useAgent } from "agents/react";
import { useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import "./styles.css";
interface Message {
itemId: string;
type: string;
role: "user" | "assistant";
status: "completed" | "in_progress";
content: Array<{
type: string;
transcript: string;
// oxlint-disable-next-line @typescript-eslint/no-explicit-any -- OpenAI SDK audio type not exposed
audio?: any;
}>;
}
function App() {
const [state, setState] = useState<{ history: RealtimeItem[] }>({
history: []
});
const [callStatus, setCallStatus] = useState<
"connecting" | "connected" | "disconnected"
>("connecting");
const [callDuration, setCallDuration] = useState(0);
useAgent<{ history: RealtimeItem[] }>({
agent: "my-agent",
name: "123",
onStateUpdate(newState) {
setState(newState);
// Update call status based on state
if (newState.history && newState.history.length > 0) {
setCallStatus("connected");
}
}
});
// Timer for call duration
useEffect(() => {
let interval: NodeJS.Timeout;
if (callStatus === "connected") {
interval = setInterval(() => {
setCallDuration((prev) => prev + 1);
}, 1000);
}
return () => clearInterval(interval);
}, [callStatus]);
const formatDuration = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, "0")}:${secs
.toString()
.padStart(2, "0")}`;
};
const getStatusColor = (status: string) => {
switch (status) {
case "completed":
return "#22c55e";
case "in_progress":
return "#f59e0b";
default:
return "#6b7280";
}
};
const getStatusText = (status: string) => {
switch (status) {
case "completed":
return "✓";
case "in_progress":
return "●";
default:
return "○";
}
};
return (
<div className="phone-call-container">
{/* Call Header */}
<div className="call-header">
<div className="call-info">
<div className="call-title">Live Call Transcription</div>
<div className="call-duration">{formatDuration(callDuration)}</div>
</div>
<div className="call-status">
<div className={`status-indicator ${callStatus}`}>
<div className="status-dot" />
<span>
{callStatus.charAt(0).toUpperCase() + callStatus.slice(1)}
</span>
</div>
</div>
</div>
{/* Transcription Area */}
<div className="transcription-area">
{!state.history || state.history.length === 0 ? (
<div className="no-messages">
<div className="waiting-indicator">
<div className="pulse-dot" />
<p>Waiting for call to begin...</p>
</div>
</div>
) : (
<div className="messages-container">
{state.history.map((item: RealtimeItem, _index) => {
const message = item as Message;
const isUser = message.role === "user";
const transcript = message.content?.[0]?.transcript || "";
return (
<div
key={message.itemId}
className={`message-bubble ${isUser ? "user" : "assistant"} ${
message.status
}`}
>
<div className="message-header">
<span className="speaker-name">
{isUser ? "You" : "Agent"}
</span>
<span
className="status-indicator"
style={{ color: getStatusColor(message.status) }}
>
{getStatusText(message.status)}
</span>
</div>
<div className="message-content">
{transcript || "Processing..."}
</div>
{message.status === "in_progress" && (
<div className="typing-indicator">
<span />
<span />
<span />
</div>
)}
</div>
);
})}
</div>
)}
</div>
{/* Call Controls */}
<div className="call-controls">
<button type="button" className="control-btn mute-btn">
<span>🔇</span>
</button>
<button type="button" className="control-btn end-call-btn">
<span>📞</span>
</button>
<button type="button" className="control-btn speaker-btn">
<span>🔊</span>
</button>
</div>
</div>
);
}
const root = createRoot(document.getElementById("root")!);
root.render(<App />);