diff --git a/.env.example b/.env.example index 1ea7c38..d616a23 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/app/page.tsx b/app/page.tsx index 6a810bf..e6db225 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -77,8 +77,9 @@ export default function HomePage() {

Найдите работу мечты

-

- Платформа с искусственным интеллектом для поиска идеальной вакансии +

+ Выберите понравившуюся вам вакансию, заполните форму и прикрепите резюме.
+ После недолговременной обработки вашего документа мы предоставим вам возможность подключится к сессии для собеседования, если ваше резюме удовлетворит вакансию.

diff --git a/components/InterviewSession.tsx b/components/InterviewSession.tsx index 4be489a..6da80a2 100644 --- a/components/InterviewSession.tsx +++ b/components/InterviewSession.tsx @@ -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) }} > - + ) } -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({ 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 */}
-

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

-

Для завершения собеседования нажмите красную кнопку

+

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

+

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

+

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

+

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

diff --git a/components/ResumeUploadForm.tsx b/components/ResumeUploadForm.tsx index 552cff2..76662d9 100644 --- a/components/ResumeUploadForm.tsx +++ b/components/ResumeUploadForm.tsx @@ -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 }: Ошибка обработки резюме

- Не удалось обработать ваше резюме. Попробуйте загрузить файл в другом формате (PDF, DOC, DOCX, TXT) + Не удалось обработать ваше резюме. Попробуйте загрузить файл в другом формате (PDF, DOCX) или обратитесь к нам за помощью.

@@ -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 }: Мы готовы!

- Ваше резюме успешно обработано. Можете приступать к интервью с HR агентом. + Ваше резюме успешно обработано. Можете приступать к собеседованию с HR-агентом. +
+
+ * Вы можете пройти собеседование сегодня до 20:00 МСК

)} - {!['parsed', 'parsing', 'pending', 'parse_failed'].includes(resume.status) && ( + {resume.status === 'rejected' && ( +
+
+

+ Резюме не соответствует вакансии +

+

+ К сожалению, ваш опыт не подходит для данной позиции +

+
+
+ )} + + {!['parsed', 'parsing', 'pending', 'parse_failed', 'rejected'].includes(resume.status) && (

@@ -357,7 +372,7 @@ export default function ResumeUploadForm({ vacancyId, vacancyTitle, onSuccess }:

- Откликнуться на вакансию + Откликнуться

{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} />

или перетащите сюда

- PDF, DOC, DOCX, TXT до 10 МБ + PDF, DOCX до 10 МБ

diff --git a/hooks/useSession.ts b/hooks/useSession.ts index 7fe9904..9da45fe 100644 --- a/hooks/useSession.ts +++ b/hooks/useSession.ts @@ -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', + }) } \ No newline at end of file diff --git a/lib/ky-client.ts b/lib/ky-client.ts index 76570f2..b314ba4 100644 --- a/lib/ky-client.ts +++ b/lib/ky-client.ts @@ -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({ diff --git a/next.config.js b/next.config.js index 32d4f41..0db6252 100644 --- a/next.config.js +++ b/next.config.js @@ -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 \ No newline at end of file diff --git a/services/resume.service.ts b/services/resume.service.ts index 8254bbb..451b2dc 100644 --- a/services/resume.service.ts +++ b/services/resume.service.ts @@ -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() }, async getResume(id: number): Promise { - return kyClient.get(`api/v1/resumes/${id}`).json() + return kyClient.get(`v1/resumes/${id}`).json() }, async getResumes(params?: GetResumesParams): Promise { @@ -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() }, 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() }, } \ No newline at end of file diff --git a/services/session.service.ts b/services/session.service.ts index f2b4e2a..ec015f1 100644 --- a/services/session.service.ts +++ b/services/session.service.ts @@ -3,18 +3,22 @@ import { SessionRead } from '@/types/api' export const sessionService = { async getCurrentSession(): Promise { - return kyClient.get('api/v1/sessions/current').json() + return kyClient.get('v1/sessions/current').json() }, async refreshSession(): Promise { - return kyClient.post('api/v1/sessions/refresh').json() + return kyClient.post('v1/sessions/refresh').json() }, async logout(): Promise { - return kyClient.post('api/v1/sessions/logout').json() + return kyClient.post('v1/sessions/logout').json() }, async healthCheck(): Promise { - return kyClient.get('api/v1/sessions/health').json() + return kyClient.get('v1/sessions/health').json() + }, + + async forceEndInterview(sessionId: number): Promise { + return kyClient.post(`v1/admin/interview/${sessionId}/force-end`).json() }, } \ No newline at end of file diff --git a/services/vacancy.service.ts b/services/vacancy.service.ts index ac7d060..c33a93f 100644 --- a/services/vacancy.service.ts +++ b/services/vacancy.service.ts @@ -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() }, async getVacancy(id: number): Promise { - return kyClient.get(`api/v1/vacancies/${id}`).json() + return kyClient.get(`v1/vacancies/${id}`).json() }, } \ No newline at end of file