``` feat: migrate from OpenAI Realtime API to LiveKit for voice infrastructure Replace direct OpenAI Realtime API WebRTC connection with LiveKit Cloud infrastructure. Add LiveKit agent package with OpenAI and Silero plugins for voice-to-voice conversations. Update web client to use LiveKit React components and session token authentication. Maintain backward compatibility for OpenAI event types during transition. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> ``` This commit message: - Uses `feat:` type since this is a new feature/major change - Describes the main change (migration from OpenAI to LiveKit) - Provides context in the body about what was added/changed - Mentions backward compatibility consideration - Includes the Claude Code attribution as requested
132 lines
4.0 KiB
TypeScript
132 lines
4.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useLiveKitVoice } from './hooks/use-livekit-voice';
|
|
import { useAudioLevel } from './hooks/use-audio-level';
|
|
import { useAudioDevices } from './hooks/use-audio-devices';
|
|
import { useEventLog } from './hooks/use-event-log';
|
|
import VolumeBar from './components/volume-bar';
|
|
import MuteButton from './components/mute-button';
|
|
import DeviceSelector from './components/device-selector';
|
|
import AgentStatus from './components/agent-status';
|
|
import ActivityLogPanel from './components/activity-log-panel';
|
|
|
|
export default function VoiceClient() {
|
|
const [isMuted, setIsMuted] = useState(false);
|
|
|
|
// Initialize event logging
|
|
const { logs, agentStatus, addLog, clearLogs } = useEventLog();
|
|
|
|
const {
|
|
isConnected,
|
|
isConnecting,
|
|
error,
|
|
stream,
|
|
connect,
|
|
disconnect
|
|
} = useLiveKitVoice({
|
|
onEvent: addLog
|
|
});
|
|
|
|
const {
|
|
devices,
|
|
selectedDeviceId,
|
|
setSelectedDeviceId,
|
|
error: devicesError,
|
|
} = useAudioDevices();
|
|
|
|
const volume = useAudioLevel(stream);
|
|
|
|
function handleConnect() {
|
|
connect(selectedDeviceId);
|
|
}
|
|
|
|
function handleMuteToggle() {
|
|
if (!stream) return;
|
|
|
|
const newMutedState = !isMuted;
|
|
|
|
stream.getAudioTracks().forEach(track => {
|
|
track.enabled = !newMutedState;
|
|
});
|
|
|
|
setIsMuted(newMutedState);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col items-center justify-center min-h-screen gap-4 md:gap-8 p-4 md:p-8">
|
|
<h1 className="text-2xl md:text-4xl font-bold text-center">Real-Time Voice</h1>
|
|
|
|
{/* Agent Status */}
|
|
<AgentStatus status={agentStatus} />
|
|
|
|
{devicesError && (
|
|
<div className="bg-yellow-50 border border-yellow-400 text-yellow-800 px-4 py-3 rounded max-w-md text-sm md:text-base">
|
|
<p className="font-medium">⚠️ {devicesError}</p>
|
|
{devicesError.includes('HTTPS') && (
|
|
<p className="mt-2 text-xs md:text-sm">
|
|
For mobile access, use HTTPS or a tunneling service like ngrok or Cloudflare Tunnel.
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded max-w-md text-sm md:text-base">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-col md:flex-row items-center md:items-end gap-4 md:gap-8 w-full max-w-2xl">
|
|
<div className="flex justify-center w-full md:w-auto">
|
|
<VolumeBar volume={volume} isMuted={isMuted} />
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-4 w-full md:w-auto md:min-w-[240px]">
|
|
{!isConnected && (
|
|
<DeviceSelector
|
|
devices={devices}
|
|
selectedDeviceId={selectedDeviceId}
|
|
onDeviceChange={setSelectedDeviceId}
|
|
disabled={isConnecting}
|
|
/>
|
|
)}
|
|
|
|
{!isConnected ? (
|
|
<button
|
|
onClick={handleConnect}
|
|
disabled={isConnecting || !!devicesError}
|
|
className="w-full px-6 md:px-8 py-3 md:py-4 bg-green-500 hover:bg-green-600 text-white rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{isConnecting ? 'Connecting...' : 'Start Voice Chat'}
|
|
</button>
|
|
) : (
|
|
<>
|
|
<MuteButton
|
|
isMuted={isMuted}
|
|
onToggle={handleMuteToggle}
|
|
disabled={!isConnected}
|
|
/>
|
|
<button
|
|
onClick={disconnect}
|
|
className="w-full px-6 md:px-8 py-3 md:py-4 bg-gray-500 hover:bg-gray-600 text-white rounded-lg font-medium"
|
|
>
|
|
Disconnect
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{isConnected && (
|
|
<p className="text-green-600 font-medium text-center text-sm md:text-base">
|
|
Connected - Speak to interact with AI
|
|
</p>
|
|
)}
|
|
|
|
{/* Activity Log Panel */}
|
|
<ActivityLogPanel logs={logs} onClear={clearLogs} />
|
|
</div>
|
|
);
|
|
}
|