fix env; add agent session hook stop
This commit is contained in:
parent
22c46c133f
commit
f2fd566cc4
@ -1,2 +1,2 @@
|
||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000/api
|
||||
NEXT_PUBLIC_LIVEKIT_URL=ws://localhost:7880
|
||||
|
@ -77,8 +77,9 @@ export default function HomePage() {
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
Найдите работу мечты
|
||||
</h1>
|
||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||
Платформа с искусственным интеллектом для поиска идеальной вакансии
|
||||
<p className="text-lg text-gray-600 max-w-3xl mx-auto">
|
||||
Выберите понравившуюся вам вакансию, заполните форму и прикрепите резюме.<br/>
|
||||
После недолговременной обработки вашего документа мы предоставим вам возможность подключится к сессии для собеседования, если ваше резюме удовлетворит вакансию.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
@ -4,6 +4,7 @@ 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,
|
||||
@ -18,6 +19,7 @@ import {
|
||||
|
||||
interface InterviewSessionProps {
|
||||
resumeId: number
|
||||
sessionId: number
|
||||
onEnd?: () => void
|
||||
}
|
||||
|
||||
@ -80,18 +82,20 @@ export default function InterviewSession({ resumeId, onEnd }: InterviewSessionPr
|
||||
console.error('LiveKit error:', error)
|
||||
}}
|
||||
>
|
||||
<InterviewRoom resumeId={resumeId} onEnd={onEnd} />
|
||||
<InterviewRoom resumeId={resumeId} onEnd={onEnd} sessionId={tokenData.session_id} />
|
||||
</LiveKitRoom>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InterviewRoom({ resumeId, onEnd }: InterviewSessionProps) {
|
||||
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<InterviewState>({
|
||||
isConnected: false,
|
||||
isRecording: false,
|
||||
@ -138,12 +142,10 @@ function InterviewRoom({ resumeId, onEnd }: InterviewSessionProps) {
|
||||
}
|
||||
|
||||
room.on(RoomEvent.Connected, handleConnected)
|
||||
room.on(RoomEvent.Disconnected, handleDisconnected)
|
||||
room.on(RoomEvent.DataReceived, handleDataReceived)
|
||||
|
||||
return () => {
|
||||
room.off(RoomEvent.Connected, handleConnected)
|
||||
room.off(RoomEvent.Disconnected, handleDisconnected)
|
||||
room.off(RoomEvent.DataReceived, handleDataReceived)
|
||||
}
|
||||
}, [room])
|
||||
@ -178,6 +180,8 @@ function InterviewRoom({ resumeId, onEnd }: InterviewSessionProps) {
|
||||
case 'ai_speaking_end':
|
||||
setAiSpeaking(false)
|
||||
break
|
||||
case 'interview_started':
|
||||
break
|
||||
case 'interview_complete':
|
||||
// Собеседование завершено
|
||||
if (onEnd) onEnd()
|
||||
@ -209,6 +213,13 @@ function InterviewRoom({ resumeId, onEnd }: InterviewSessionProps) {
|
||||
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({
|
||||
@ -217,11 +228,26 @@ function InterviewRoom({ resumeId, onEnd }: InterviewSessionProps) {
|
||||
})),
|
||||
{ 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()
|
||||
}
|
||||
@ -305,8 +331,10 @@ function InterviewRoom({ resumeId, onEnd }: InterviewSessionProps) {
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="text-center text-sm text-gray-500 space-y-1">
|
||||
<p>Говорите четко и ждите, пока агент закончит свой вопрос</p>
|
||||
<p>Для завершения собеседования нажмите красную кнопку</p>
|
||||
<p>Для начала диалога поприветствуйте интервьюера</p>
|
||||
<p>В процессе говорите четко и ждите, пока агент закончит свой вопрос</p>
|
||||
<p>Собеседование завершится автоматически</p>
|
||||
<p>Экстренно завершить собеседование можно, нажав красную кнопку</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
// @ts-ignore
|
||||
import InputMask from 'react-input-mask'
|
||||
import { ResumeCreate } from '@/types/api'
|
||||
import { useCreateResume, useResumesByVacancy } from '@/hooks/useResume'
|
||||
@ -63,9 +64,7 @@ export default function ResumeUploadForm({ vacancyId, vacancyTitle, onSuccess }:
|
||||
// Check file type
|
||||
const allowedTypes = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'text/plain'
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
]
|
||||
|
||||
if (!allowedTypes.includes(selectedFile.type)) {
|
||||
@ -162,7 +161,7 @@ export default function ResumeUploadForm({ vacancyId, vacancyTitle, onSuccess }:
|
||||
case 'accepted':
|
||||
return 'Принят'
|
||||
case 'rejected':
|
||||
return 'Отклонен'
|
||||
return 'Отклонено'
|
||||
default:
|
||||
return status
|
||||
}
|
||||
@ -181,7 +180,7 @@ export default function ResumeUploadForm({ vacancyId, vacancyTitle, onSuccess }:
|
||||
Ошибка обработки резюме
|
||||
</h3>
|
||||
<p className="mt-2 text-red-700">
|
||||
Не удалось обработать ваше резюме. Попробуйте загрузить файл в другом формате (PDF, DOC, DOCX, TXT)
|
||||
Не удалось обработать ваше резюме. Попробуйте загрузить файл в другом формате (PDF, DOCX)
|
||||
или обратитесь к нам за помощью.
|
||||
</p>
|
||||
<div className="mt-4 space-y-2">
|
||||
@ -275,7 +274,7 @@ export default function ResumeUploadForm({ vacancyId, vacancyTitle, onSuccess }:
|
||||
? 'bg-green-100 text-green-800'
|
||||
: resume.status === 'parsing' || resume.status === 'pending'
|
||||
? 'bg-yellow-100 text-yellow-800'
|
||||
: resume.status === 'parse_failed'
|
||||
: resume.status === 'parse_failed' || resume.status === 'rejected'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
@ -291,7 +290,10 @@ export default function ResumeUploadForm({ vacancyId, vacancyTitle, onSuccess }:
|
||||
Мы готовы!
|
||||
</h4>
|
||||
<p className="text-sm text-green-700 mb-4">
|
||||
Ваше резюме успешно обработано. Можете приступать к интервью с HR агентом.
|
||||
Ваше резюме успешно обработано. Можете приступать к собеседованию с HR-агентом.
|
||||
<br />
|
||||
<br />
|
||||
* Вы можете пройти собеседование сегодня до 20:00 МСК
|
||||
</p>
|
||||
<a
|
||||
href={`/interview/${resume.id}`}
|
||||
@ -335,7 +337,20 @@ export default function ResumeUploadForm({ vacancyId, vacancyTitle, onSuccess }:
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!['parsed', 'parsing', 'pending', 'parse_failed'].includes(resume.status) && (
|
||||
{resume.status === 'rejected' && (
|
||||
<div className="bg-gradient-to-r from-red-50 to-pink-50 border border-red-200 rounded-lg p-4">
|
||||
<div className="text-center">
|
||||
<h4 className="text-sm font-medium text-red-900 mb-1">
|
||||
Резюме не соответствует вакансии
|
||||
</h4>
|
||||
<p className="text-xs text-red-700">
|
||||
К сожалению, ваш опыт не подходит для данной позиции
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!['parsed', 'parsing', 'pending', 'parse_failed', 'rejected'].includes(resume.status) && (
|
||||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-lg p-4">
|
||||
<div className="text-center">
|
||||
<h4 className="text-sm font-medium text-blue-900 mb-1">
|
||||
@ -357,7 +372,7 @@ export default function ResumeUploadForm({ vacancyId, vacancyTitle, onSuccess }:
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
Откликнуться на вакансию
|
||||
Откликнуться
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
{vacancyTitle}
|
||||
@ -459,14 +474,14 @@ export default function ResumeUploadForm({ vacancyId, vacancyTitle, onSuccess }:
|
||||
name="resume_file"
|
||||
type="file"
|
||||
className="sr-only"
|
||||
accept=".pdf,.doc,.docx,.txt"
|
||||
accept=".pdf,.docx"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</label>
|
||||
<p className="pl-1">или перетащите сюда</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
PDF, DOC, DOCX, TXT до 10 МБ
|
||||
PDF, DOCX до 10 МБ
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -39,4 +39,12 @@ export const useSessionHealth = () => {
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
retry: 2,
|
||||
})
|
||||
}
|
||||
|
||||
export const useForceEndInterview = () => {
|
||||
return useMutation({
|
||||
mutationFn: (sessionId: number) => sessionService.forceEndInterview(sessionId),
|
||||
retry: false, // Не повторяем запрос при ошибке
|
||||
networkMode: 'always',
|
||||
})
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import ky from 'ky'
|
||||
|
||||
// Используем прокси Next.js для избежания CORS проблем
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000/api'
|
||||
|
||||
// Базовый клиент без Content-Type заголовка
|
||||
const baseKyClient = ky.create({
|
||||
|
@ -1,13 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/api/v1/:path*',
|
||||
destination: 'http://localhost:8000/api/v1/:path*',
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
const nextConfig = {}
|
||||
|
||||
module.exports = nextConfig
|
@ -21,17 +21,18 @@ export const resumeService = {
|
||||
|
||||
// Логируем данные для отладки
|
||||
console.log('FormData entries:')
|
||||
// @ts-ignore
|
||||
for (const [key, value] of formData.entries()) {
|
||||
console.log(key, value)
|
||||
}
|
||||
|
||||
return kyFormClient.post('api/v1/resumes/', {
|
||||
return kyFormClient.post('v1/resumes/', {
|
||||
body: formData,
|
||||
}).json<ResumeRead>()
|
||||
},
|
||||
|
||||
async getResume(id: number): Promise<ResumeRead> {
|
||||
return kyClient.get(`api/v1/resumes/${id}`).json<ResumeRead>()
|
||||
return kyClient.get(`v1/resumes/${id}`).json<ResumeRead>()
|
||||
},
|
||||
|
||||
async getResumes(params?: GetResumesParams): Promise<ResumeRead[]> {
|
||||
@ -45,15 +46,15 @@ export const resumeService = {
|
||||
})
|
||||
}
|
||||
|
||||
const endpoint = `api/v1/resumes/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`
|
||||
const endpoint = `v1/resumes/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`
|
||||
return kyClient.get(endpoint).json<ResumeRead[]>()
|
||||
},
|
||||
|
||||
async validateInterview(resumeId: number): Promise<{ can_interview: boolean; message?: string }> {
|
||||
return kyClient.get(`api/v1/interview/${resumeId}/validate-interview`).json()
|
||||
return kyClient.get(`v1/interview/${resumeId}/validate-interview`).json()
|
||||
},
|
||||
|
||||
async getInterviewToken(resumeId: number): Promise<{ token: string; roomName: string; serverUrl: string }> {
|
||||
return kyClient.post(`api/v1/interview/${resumeId}/token`).json()
|
||||
async getInterviewToken(resumeId: number): Promise<{ token: string; roomName: string; serverUrl: string; session_id: number }> {
|
||||
return kyClient.post(`v1/interview/${resumeId}/token`).json()
|
||||
},
|
||||
}
|
@ -3,18 +3,22 @@ import { SessionRead } from '@/types/api'
|
||||
|
||||
export const sessionService = {
|
||||
async getCurrentSession(): Promise<SessionRead> {
|
||||
return kyClient.get('api/v1/sessions/current').json<SessionRead>()
|
||||
return kyClient.get('v1/sessions/current').json<SessionRead>()
|
||||
},
|
||||
|
||||
async refreshSession(): Promise<void> {
|
||||
return kyClient.post('api/v1/sessions/refresh').json<void>()
|
||||
return kyClient.post('v1/sessions/refresh').json<void>()
|
||||
},
|
||||
|
||||
async logout(): Promise<void> {
|
||||
return kyClient.post('api/v1/sessions/logout').json<void>()
|
||||
return kyClient.post('v1/sessions/logout').json<void>()
|
||||
},
|
||||
|
||||
async healthCheck(): Promise<void> {
|
||||
return kyClient.get('api/v1/sessions/health').json<void>()
|
||||
return kyClient.get('v1/sessions/health').json<void>()
|
||||
},
|
||||
|
||||
async forceEndInterview(sessionId: number): Promise<void> {
|
||||
return kyClient.post(`v1/admin/interview/${sessionId}/force-end`).json<void>()
|
||||
},
|
||||
}
|
@ -13,11 +13,11 @@ export const vacancyService = {
|
||||
})
|
||||
}
|
||||
|
||||
const endpoint = `api/v1/vacancies/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`
|
||||
const endpoint = `v1/vacancies/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`
|
||||
return kyClient.get(endpoint).json<VacancyRead[]>()
|
||||
},
|
||||
|
||||
async getVacancy(id: number): Promise<VacancyRead> {
|
||||
return kyClient.get(`api/v1/vacancies/${id}`).json<VacancyRead>()
|
||||
return kyClient.get(`v1/vacancies/${id}`).json<VacancyRead>()
|
||||
},
|
||||
}
|
Loading…
Reference in New Issue
Block a user