Files
paseo/packages/web/app/voice-client.tsx
T
Mohamed Boudra b70c0cfc73 Based on the git changes, this is a major migration from OpenAI's Realtime API to LiveKit for voice infrastructure. Here's the commit message:
```
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
2025-10-14 11:54:17 +02:00

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>
);
}