'use client' import { useState, useEffect, useRef } from 'react' import { Room, RoomEvent, Track, RemoteTrack, LocalTrack } from 'livekit-client' import { useTracks, RoomAudioRenderer, LiveKitRoom, useRoomContext } from '@livekit/components-react' import { useInterviewToken } from '@/hooks/useResume' import { useForceEndInterview } from '@/hooks/useSession' import { Mic, MicOff, Phone, PhoneOff, Volume2, VolumeX, Loader, CheckCircle, AlertCircle } from 'lucide-react' interface InterviewSessionProps { resumeId: number sessionId: number onEnd?: () => void } interface InterviewState { isConnected: boolean isRecording: boolean isMuted: boolean isSpeaking: boolean connectionState: 'connecting' | 'connected' | 'disconnected' | 'failed' error?: string } export default function InterviewSession({ resumeId, onEnd }: InterviewSessionProps) { const { data: tokenData, isLoading, error } = useInterviewToken(resumeId, true) if (isLoading) { return (

Подключаемся к собеседованию

Пожалуйста, подождите, мы подготавливаем для вас сессию

) } if (error || !tokenData?.token) { return (

Ошибка подключения

Не удалось подключиться к сессии собеседования

) } return (
console.log('Connected to LiveKit')} onDisconnected={() => console.log('Disconnected from LiveKit')} onError={(error) => { console.error('LiveKit error:', error) }} >
) } function InterviewRoom({ resumeId, onEnd, sessionId }: InterviewSessionProps) { const room = useRoomContext() const tracks = useTracks([Track.Source.Microphone, Track.Source.ScreenShare], { onlySubscribed: false, }) const forceEndMutation = useForceEndInterview() const [state, setState] = useState({ isConnected: false, isRecording: false, isMuted: false, isSpeaking: false, connectionState: 'connecting' }) const [interviewStarted, setInterviewStarted] = useState(false) const [currentQuestion, setCurrentQuestion] = useState('') const [aiSpeaking, setAiSpeaking] = useState(false) useEffect(() => { if (!room) return const handleConnected = () => { setState(prev => ({ ...prev, isConnected: true, connectionState: 'connected' })) // Начинаем собеседование startInterview() } const handleDisconnected = () => { setState(prev => ({ ...prev, isConnected: false, connectionState: 'disconnected' })) if (onEnd) { onEnd() } } const handleDataReceived = (payload: Uint8Array, participant: any) => { try { const message = JSON.parse(new TextDecoder().decode(payload)) handleServerMessage(message) } catch (error) { console.error('Error parsing server message:', error) } } room.on(RoomEvent.Connected, handleConnected) room.on(RoomEvent.DataReceived, handleDataReceived) return () => { room.off(RoomEvent.Connected, handleConnected) room.off(RoomEvent.DataReceived, handleDataReceived) } }, [room]) const startInterview = async () => { if (!room) return try { // Отправляем сигнал серверу о начале собеседования await room.localParticipant.publishData( new TextEncoder().encode(JSON.stringify({ type: 'start_interview', resumeId })), { reliable: true } ) setInterviewStarted(true) } catch (error) { console.error('Error starting interview:', error) } } const handleServerMessage = (message: any) => { switch (message.type) { case 'question': setCurrentQuestion(message.text) setAiSpeaking(true) break case 'ai_speaking_start': setAiSpeaking(true) break case 'ai_speaking_end': setAiSpeaking(false) break case 'interview_started': break case 'interview_complete': // Собеседование завершено if (onEnd) onEnd() break default: console.log('Unknown message type:', message.type) } } const toggleMute = async () => { if (!room) return try { const audioTrack = room.localParticipant.getTrackPublication(Track.Source.Microphone) if (audioTrack) { if (state.isMuted) { await audioTrack.unmute() } else { await audioTrack.mute() } setState(prev => ({ ...prev, isMuted: !prev.isMuted })) } } catch (error) { console.error('Error toggling mute:', error) } } const endInterview = async () => { if (!room) return try { // Если есть sessionId, используем force-end API if (sessionId) { console.log('Starting force-end mutation for sessionId:', sessionId) await forceEndMutation.mutateAsync(sessionId) console.log('Force-end mutation completed successfully') } // Отправляем сигнал серверу о завершении собеседования await room.localParticipant.publishData( new TextEncoder().encode(JSON.stringify({ type: 'end_interview', resumeId })), { reliable: true } ) setState(prev => ({ ...prev, isConnected: false, connectionState: 'disconnected' })) // Отключение происходит только после успешного выполнения всех операций room.disconnect() console.log('About to call onEnd - this will cause redirect') // Временно отключаем редирект для проверки логов if (onEnd) onEnd() } catch (error) { console.error('Error ending interview:', error) // В случае ошибки всё равно отключаемся setState(prev => ({ ...prev, isConnected: false, connectionState: 'disconnected' })) room.disconnect() if (onEnd) onEnd() } } const getConnectionStatusColor = () => { switch (state.connectionState) { case 'connected': return 'text-green-600' case 'connecting': return 'text-yellow-600' case 'failed': return 'text-red-600' default: return 'text-gray-600' } } return (
{/* Header */}

Собеседование со Стефани

{state.connectionState === 'connected' && 'Подключено'} {state.connectionState === 'connecting' && 'Подключение...'} {state.connectionState === 'disconnected' && 'Отключено'} {state.connectionState === 'failed' && 'Ошибка подключения'}

{/* Current Question */} {currentQuestion && (
{aiSpeaking && } {aiSpeaking ? 'HR Агент говорит...' : 'Текущий вопрос:'}

{currentQuestion}

)} {!interviewStarted && state.isConnected && (
Ожидаем начала собеседования...
)} {/* Controls */}
{/* Instructions */}

Для начала диалога поприветствуйте интервьюера

В процессе говорите четко и ждите, пока агент закончит свой вопрос

Собеседование завершится автоматически

Экстренно завершить собеседование можно, нажав красную кнопку

) }