diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1ea7c38 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 +NEXT_PUBLIC_LIVEKIT_URL=ws://localhost:7880 diff --git a/README.md b/README.md index dc5e1ea..0cf97e4 100644 --- a/README.md +++ b/README.md @@ -1 +1,109 @@ -# hr-ai \ No newline at end of file +# HR AI Frontend + +Современная платформа для поиска работы с искусственным интеллектом, построенная на Next.js и TypeScript. + +## Возможности + +- 📋 Просмотр списка вакансий с поиском и фильтрацией +- 🔍 Детальная информация о каждой вакансии +- 📄 Загрузка резюме с уведомлением о подготовке сессии собеседования +- 🔐 Авторизация через cookie-сессии (без JWT) +- 🎨 Современный и адаптивный дизайн +- ⚡ Быстрая загрузка и отзывчивый интерфейс + +## Технологический стек + +- **Framework**: Next.js 14 (App Router) +- **Language**: TypeScript +- **Styling**: Tailwind CSS +- **Icons**: Lucide React +- **HTTP Client**: Fetch API с cookie-авторизацией + +## Быстрый старт + +1. Установите зависимости: +```bash +yarn install +``` + +2. Создайте файл `.env.local` (уже создан): +```env +NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 +``` + +3. Запустите development сервер: +```bash +yarn dev +``` + +4. Откройте [http://localhost:3000](http://localhost:3000) в браузере + +## Структура проекта + +``` +├── app/ # Next.js App Router +│ ├── globals.css # Глобальные стили +│ ├── layout.tsx # Корневой layout +│ ├── page.tsx # Главная страница (список вакансий) +│ └── vacancy/[id]/ # Страница детальной информации о вакансии +├── components/ # React компоненты +│ └── ResumeUploadForm.tsx +├── lib/ # Утилиты и API клиент +│ └── api-client.ts +├── types/ # TypeScript типы +│ └── api.ts +└── public/ # Статические файлы +``` + +## API Integration + +Приложение интегрируется с HR AI Backend API: + +### Основные эндпоинты: +- `GET /api/v1/vacancies/` - Получение списка вакансий +- `GET /api/v1/vacancies/{id}` - Получение вакансии по ID +- `POST /api/v1/resumes/` - Загрузка резюме +- `GET /api/v1/sessions/current` - Получение информации о сессии + +### Авторизация: +Все запросы выполняются с `credentials: 'include'` для работы с cookie-сессиями. + +## Особенности реализации + +### Загрузка резюме +- Поддержка файлов: PDF, DOC, DOCX, TXT +- Максимальный размер файла: 10 МБ +- Валидация формы перед отправкой +- Уведомление об успешной отправке + +### Поиск вакансий +- Поиск по названию вакансии +- Фильтрация активных вакансий +- Красивое отображение карточек вакансий + +### Детальная страница вакансии +- Полная информация о вакансии +- Контактные данные +- Форма для отклика прямо на странице + +## Скрипты + +- `yarn dev` - Запуск в режиме разработки +- `yarn build` - Сборка продакшн версии +- `yarn start` - Запуск продакшн версии +- `yarn lint` - Проверка кода с ESLint + +## Стилизация + +Проект использует Tailwind CSS для стилизации с кастомной темой: +- Основной цвет: Blue (primary-*) +- Адаптивный дизайн для всех устройств +- Современные компоненты с hover эффектами + +## Будущие улучшения + +- [ ] Пагинация для списка вакансий +- [ ] Расширенные фильтры (по зарплате, опыту, локации) +- [ ] Избранные вакансии +- [ ] История откликов +- [ ] Уведомления в реальном времени \ No newline at end of file diff --git a/app/interview/[id]/page.tsx b/app/interview/[id]/page.tsx new file mode 100644 index 0000000..a2aede0 --- /dev/null +++ b/app/interview/[id]/page.tsx @@ -0,0 +1,102 @@ +'use client' + +import { useParams, useRouter } from 'next/navigation' +import InterviewSession from '@/components/InterviewSession' +import { useValidateInterview } from '@/hooks/useResume' +import { ArrowLeft, AlertCircle, Loader } from 'lucide-react' + +export default function InterviewPage() { + const params = useParams() + const router = useRouter() + const resumeId = parseInt(params.id as string) + + const { data: validationData, isLoading, error } = useValidateInterview(resumeId) + + const handleInterviewEnd = () => { + // Перенаправляем обратно к вакансии или на главную страницу + router.push('/') + } + + if (isLoading) { + return ( +
+ +

+ Проверяем готовность к собеседованию +

+

+ Пожалуйста, подождите... +

+
+ ) + } + + if (error || !validationData?.can_interview) { + const errorMessage = error?.response?.status === 404 + ? 'Резюме не найдено' + : error?.response?.status === 400 + ? 'Резюме еще не готово к собеседованию' + : validationData?.message || 'Собеседование недоступно' + + return ( +
+ +

+ Собеседование недоступно +

+

+ {errorMessage} +

+
+ + +
+
+ ) + } + + return ( +
+ {/* Navigation Header */} +
+
+ + +
+

+ HR Собеседование +

+

+ Резюме #{resumeId} +

+
+ +
{/* Spacer for centering */} +
+
+ + {/* Interview Session */} + +
+ ) +} \ No newline at end of file diff --git a/components/InterviewSession.tsx b/components/InterviewSession.tsx new file mode 100644 index 0000000..490b165 --- /dev/null +++ b/components/InterviewSession.tsx @@ -0,0 +1,311 @@ +'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 { + Mic, + MicOff, + Phone, + PhoneOff, + Volume2, + VolumeX, + Loader, + CheckCircle, + AlertCircle +} from 'lucide-react' + +interface InterviewSessionProps { + resumeId: 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 }: InterviewSessionProps) { + const room = useRoomContext() + const tracks = useTracks([Track.Source.Microphone, Track.Source.ScreenShare], { + onlySubscribed: false, + }) + + 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' + })) + } + + 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.Disconnected, handleDisconnected) + room.on(RoomEvent.DataReceived, handleDataReceived) + + return () => { + room.off(RoomEvent.Connected, handleConnected) + room.off(RoomEvent.Disconnected, handleDisconnected) + 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_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 { + // Отправляем сигнал серверу о завершении собеседования + await room.localParticipant.publishData( + new TextEncoder().encode(JSON.stringify({ + type: 'end_interview', + resumeId + })), + { reliable: true } + ) + + room.disconnect() + if (onEnd) onEnd() + } catch (error) { + console.error('Error ending interview:', error) + 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 */} +
+
+
+ +
+
+

+ HR Собеседование +

+

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

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

{currentQuestion}

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

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

+

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

+
+
+
+ ) +} \ No newline at end of file diff --git a/components/ResumeUploadForm.tsx b/components/ResumeUploadForm.tsx index c69fbf7..552cff2 100644 --- a/components/ResumeUploadForm.tsx +++ b/components/ResumeUploadForm.tsx @@ -1,10 +1,10 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import InputMask from 'react-input-mask' import { ResumeCreate } from '@/types/api' import { useCreateResume, useResumesByVacancy } from '@/hooks/useResume' -import { Upload, FileText, X, CheckCircle, Clock } from 'lucide-react' +import { Upload, FileText, X, CheckCircle, Clock, Loader, AlertCircle } from 'lucide-react' interface ResumeUploadFormProps { vacancyId: number @@ -24,11 +24,29 @@ export default function ResumeUploadForm({ vacancyId, vacancyTitle, onSuccess }: const [success, setSuccess] = useState(false) const createResumeMutation = useCreateResume() - const { data: existingResumes, isLoading: isLoadingResumes } = useResumesByVacancy(vacancyId) + const { data: existingResumes, isLoading: isLoadingResumes, refetch } = useResumesByVacancy(vacancyId) // Проверяем есть ли уже резюме для этой вакансии в текущей сессии const hasExistingResume = existingResumes && existingResumes.length > 0 + // Находим непарсенные резюме + const pendingResumes = existingResumes?.filter(resume => + resume.status === 'pending' || resume.status === 'parsing' + ) || [] + + const hasPendingResumes = pendingResumes.length > 0 + + // Автообновление для непарсенных резюме + useEffect(() => { + if (hasPendingResumes) { + const interval = setInterval(() => { + refetch() + }, 3000) // 3 секунды + + return () => clearInterval(interval) + } + }, [hasPendingResumes, refetch]) + const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target setFormData(prev => ({ ...prev, [name]: value })) @@ -125,42 +143,212 @@ export default function ResumeUploadForm({ vacancyId, vacancyTitle, onSuccess }: ) } + const getStatusDisplay = (status: string) => { + switch (status) { + case 'pending': + return 'Обрабатывается' + case 'parsing': + return 'Обрабатывается' + case 'parse_failed': + return 'Ошибка обработки' + case 'parsed': + return 'Обработано' + case 'under_review': + return 'На проверке' + case 'interview_scheduled': + return 'Собеседование назначено' + case 'interviewed': + return 'Проведено собеседование' + case 'accepted': + return 'Принят' + case 'rejected': + return 'Отклонен' + default: + return status + } + } + + // Обработка ошибок парсинга + const hasParseFailedResumes = existingResumes?.some(resume => resume.status === 'parse_failed') || false + + if (hasParseFailedResumes) { + return ( +
+
+ +
+

+ Ошибка обработки резюме +

+

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

+
+ {existingResumes?.map((resume) => ( +
+ + Отправлено: {new Date(resume.created_at).toLocaleDateString('ru-RU', { + day: 'numeric', + month: 'long', + hour: '2-digit', + minute: '2-digit' + })} • Статус: {getStatusDisplay(resume.status)} + +
+ ))} +
+ +
+
+
+ ) + } + + // Показываем крутилку для статусов pending/parsing + if (hasPendingResumes) { + return ( +
+ +
+

+ Обрабатываем ваше резюме... +

+
+

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

+
+ {existingResumes?.map((resume) => ( +
+ + Отправлено: {new Date(resume.created_at).toLocaleDateString('ru-RU', { + day: 'numeric', + month: 'long', + hour: '2-digit', + minute: '2-digit' + })} +
Статус: {getStatusDisplay(resume.status)} +
+
+ ))} +
+
+ ) + } + + // Обычное успешное состояние для parsed и других завершенных статусов if (success || hasExistingResume) { return ( -
-
- -
-

- {success ? 'Резюме успешно отправлено!' : 'Ваше резюме уже отправлено!'} -

-

- Готовим для вас сессию для собеседования. Мы свяжемся с вами в ближайшее время. -

- {hasExistingResume && existingResumes && ( -
- {existingResumes.map((resume) => ( -
- - - Отправлено: {new Date(resume.created_at).toLocaleDateString('ru-RU', { - day: 'numeric', - month: 'long', - hour: '2-digit', - minute: '2-digit' - })} • Статус: {resume.status === 'pending' ? 'На рассмотрении' : - resume.status === 'under_review' ? 'На проверке' : - resume.status === 'interview_scheduled' ? 'Собеседование назначено' : - resume.status === 'interviewed' ? 'Проведено собеседование' : - resume.status === 'accepted' ? 'Принят' : - resume.status === 'rejected' ? 'Отклонен' : resume.status} - +
+ {hasExistingResume && existingResumes && existingResumes.map((resume) => ( +
+ {/* Status and Date Row */} +
+
+
+ +
+
+

+ Резюме +

+

+ {new Date(resume.created_at).toLocaleDateString('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} +

+
+
+ + + {getStatusDisplay(resume.status)} + +
+ + {/* Content based on status */} + {resume.status === 'parsed' && ( +
+
+

+ Мы готовы! +

+

+ Ваше резюме успешно обработано. Можете приступать к интервью с HR агентом. +

+ + Начать собеседование + +
+
+ )} + + {(resume.status === 'parsing' || resume.status === 'pending') && ( +
+
+
+
+

+ Обрабатываем ваше резюме +

+

+ Анализируем опыт и готовим персональные вопросы +

- ))} +
+
+ )} + + {resume.status === 'parse_failed' && ( +
+
+
⚠️
+
+

+ Ошибка обработки +

+

+ Попробуйте загрузить файл в другом формате +

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

+ {getStatusDisplay(resume.status)} +

+

+ Мы свяжемся с вами для следующих шагов +

+
)}
-
+ ))}
) } diff --git a/hooks/useResume.ts b/hooks/useResume.ts index a54a864..12ddf64 100644 --- a/hooks/useResume.ts +++ b/hooks/useResume.ts @@ -38,7 +38,28 @@ export const useResumesByVacancy = (vacancyId: number) => { queryKey: ['resumes', 'by-vacancy', vacancyId], queryFn: () => resumeService.getResumes({ vacancy_id: vacancyId }), enabled: !!vacancyId, - staleTime: 2 * 60 * 1000, // 2 minutes + staleTime: 0, // Не кешируем для частых обновлений retry: 2, + refetchInterval: false, // Отключаем автоматический refetch, управляем вручную + }) +} + +export const useValidateInterview = (resumeId: number, enabled: boolean = true) => { + return useQuery({ + queryKey: ['interview', 'validate', resumeId], + queryFn: () => resumeService.validateInterview(resumeId), + enabled: enabled && !!resumeId, + retry: false, + staleTime: 5 * 60 * 1000, // 5 минут + }) +} + +export const useInterviewToken = (resumeId: number, enabled: boolean = false) => { + return useQuery({ + queryKey: ['interview', 'token', resumeId], + queryFn: () => resumeService.getInterviewToken(resumeId), + enabled: enabled && !!resumeId, + retry: false, + staleTime: 30 * 60 * 1000, // 30 минут - токены живут дольше }) } \ No newline at end of file diff --git a/package.json b/package.json index 6877afd..6e73bbd 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,11 @@ "lint": "next lint" }, "dependencies": { + "@livekit/components-react": "^2.9.14", "@tanstack/react-query": "^5.85.6", "@tanstack/react-query-devtools": "^5.85.6", "ky": "^1.9.1", + "livekit-client": "^2.15.6", "lucide-react": "^0.294.0", "next": "14.0.4", "react": "^18.2.0", diff --git a/services/resume.service.ts b/services/resume.service.ts index 19c9a26..8254bbb 100644 --- a/services/resume.service.ts +++ b/services/resume.service.ts @@ -48,4 +48,12 @@ export const resumeService = { const endpoint = `api/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() + }, + + async getInterviewToken(resumeId: number): Promise<{ token: string; roomName: string; serverUrl: string }> { + return kyClient.post(`api/v1/interview/${resumeId}/token`).json() + }, } \ No newline at end of file diff --git a/types/api.ts b/types/api.ts index 678ba17..d5522f0 100644 --- a/types/api.ts +++ b/types/api.ts @@ -1,7 +1,7 @@ export type EmploymentType = 'full' | 'part' | 'project' | 'volunteer' | 'probation' export type Experience = 'noExperience' | 'between1And3' | 'between3And6' | 'moreThan6' export type Schedule = 'fullDay' | 'shift' | 'flexible' | 'remote' | 'flyInFlyOut' -export type ResumeStatus = 'pending' | 'under_review' | 'interview_scheduled' | 'interviewed' | 'rejected' | 'accepted' +export type ResumeStatus = 'pending' | 'parsing' | 'parse_failed' | 'parsed' | 'under_review' | 'interview_scheduled' | 'interviewed' | 'rejected' | 'accepted' export interface VacancyRead { id: number diff --git a/yarn.lock b/yarn.lock index d2ddc28..f9c3452 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== +"@bufbuild/protobuf@^1.10.0": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-1.10.1.tgz#1d76d15290c0212076c15ede94d15157ba0c6344" + integrity sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ== + "@emnapi/core@^1.4.3": version "1.5.0" resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.5.0.tgz#85cd84537ec989cebb2343606a1ee663ce4edaf0" @@ -61,6 +66,26 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@floating-ui/core@^1.6.0": + version "1.7.3" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.3.tgz#462d722f001e23e46d86fd2bd0d21b7693ccb8b7" + integrity sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w== + dependencies: + "@floating-ui/utils" "^0.2.10" + +"@floating-ui/dom@1.6.13": + version "1.6.13" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.13.tgz#a8a938532aea27a95121ec16e667a7cbe8c59e34" + integrity sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w== + dependencies: + "@floating-ui/core" "^1.6.0" + "@floating-ui/utils" "^0.2.9" + +"@floating-ui/utils@^0.2.10", "@floating-ui/utils@^0.2.9": + version "0.2.10" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c" + integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ== + "@humanwhocodes/config-array@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" @@ -118,6 +143,36 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@livekit/components-core@0.12.9": + version "0.12.9" + resolved "https://registry.yarnpkg.com/@livekit/components-core/-/components-core-0.12.9.tgz#87502ed28a61c65db8306b684073e0dd26c86da2" + integrity sha512-bwrZsHf6GaHIO+lLyA6Yps1STTX9YIeL3ixwt+Ufi88OgkNYdp41Ug8oeVDlf7tzdxa+r3Xkfaj/qvIG84Yo6A== + dependencies: + "@floating-ui/dom" "1.6.13" + loglevel "1.9.1" + rxjs "7.8.2" + +"@livekit/components-react@^2.9.14": + version "2.9.14" + resolved "https://registry.yarnpkg.com/@livekit/components-react/-/components-react-2.9.14.tgz#d0b26ccbfe419c7e9191d41a5e5151e10f057cdb" + integrity sha512-fQ3t4PdcM+AORo62FWmJcfqWe7ODwVaU4nsqxse+fp6L5a+0K2uMD7yQ2jrutXIaUQigU/opzTUxPcpdk9+0ow== + dependencies: + "@livekit/components-core" "0.12.9" + clsx "2.1.1" + usehooks-ts "3.1.1" + +"@livekit/mutex@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@livekit/mutex/-/mutex-1.1.1.tgz#72492b611d55be8130ba2271b7a436d94b1bc6d4" + integrity sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw== + +"@livekit/protocol@1.39.3": + version "1.39.3" + resolved "https://registry.yarnpkg.com/@livekit/protocol/-/protocol-1.39.3.tgz#dfbb801f6de232d64d918e0ad796268ff709cc35" + integrity sha512-hfOnbwPCeZBEvMRdRhU2sr46mjGXavQcrb3BFRfG+Gm0Z7WUSeFdy5WLstXJzEepz17Iwp/lkGwJ4ZgOOYfPuA== + dependencies: + "@bufbuild/protobuf" "^1.10.0" + "@napi-rs/wasm-runtime@^0.2.11": version "0.2.12" resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2" @@ -764,6 +819,11 @@ client-only@0.0.1: resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== +clsx@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -1278,6 +1338,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -1959,6 +2024,21 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +livekit-client@^2.15.6: + version "2.15.6" + resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-2.15.6.tgz#6cfd919d36221680b58917d156a1cae6a85ca7da" + integrity sha512-bLdNXklpMfWofw9pCF2XGyYA3OUddXXG4KY+gTN7dh+YvG7TX+YaP/Kt9ugdZ3KziQLqK2HG1ict4s7uD0JAiQ== + dependencies: + "@livekit/mutex" "1.1.1" + "@livekit/protocol" "1.39.3" + events "^3.3.0" + loglevel "^1.9.2" + sdp-transform "^2.15.0" + ts-debounce "^4.0.0" + tslib "2.8.1" + typed-emitter "^2.1.0" + webrtc-adapter "^9.0.1" + locate-path@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" @@ -1966,11 +2046,26 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +loglevel@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.1.tgz#d63976ac9bcd03c7c873116d41c2a85bafff1be7" + integrity sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg== + +loglevel@^1.9.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.2.tgz#c2e028d6c757720107df4e64508530db6621ba08" + integrity sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -2494,6 +2589,13 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rxjs@7.8.2, rxjs@^7.5.2: + version "7.8.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b" + integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== + dependencies: + tslib "^2.1.0" + safe-array-concat@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" @@ -2529,6 +2631,16 @@ scheduler@^0.23.2: dependencies: loose-envify "^1.1.0" +sdp-transform@^2.15.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/sdp-transform/-/sdp-transform-2.15.0.tgz#79d37a2481916f36a0534e07b32ceaa87f71df42" + integrity sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw== + +sdp@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/sdp/-/sdp-3.2.1.tgz#a2f79eecd7c5adb90d54e1bc9812775d80f3c06c" + integrity sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw== + semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" @@ -2880,6 +2992,11 @@ ts-api-utils@^1.0.1: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064" integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== +ts-debounce@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/ts-debounce/-/ts-debounce-4.0.0.tgz#33440ef64fab53793c3d546a8ca6ae539ec15841" + integrity sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg== + ts-interface-checker@^0.1.9: version "0.1.13" resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" @@ -2895,7 +3012,7 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.4.0: +tslib@2.8.1, tslib@^2.1.0, tslib@^2.4.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -2957,6 +3074,13 @@ typed-array-length@^1.0.7: possible-typed-array-names "^1.0.0" reflect.getprototypeof "^1.0.6" +typed-emitter@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/typed-emitter/-/typed-emitter-2.1.0.tgz#ca78e3d8ef1476f228f548d62e04e3d4d3fd77fb" + integrity sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA== + optionalDependencies: + rxjs "^7.5.2" + typescript@^5.3.3: version "5.9.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" @@ -3019,6 +3143,13 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +usehooks-ts@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-3.1.1.tgz#0bb7f38f36f8219ee4509cc5e944ae610fb97656" + integrity sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA== + dependencies: + lodash.debounce "^4.0.8" + util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -3039,6 +3170,13 @@ watchpack@2.4.0: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" +webrtc-adapter@^9.0.1: + version "9.0.3" + resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-9.0.3.tgz#b446ed7cd72129d00c652dd7b9a5716d9ffdd87d" + integrity sha512-5fALBcroIl31OeXAdd1YUntxiZl1eHlZZWzNg3U4Fn+J9/cGL3eT80YlrsWGvj2ojuz1rZr2OXkgCzIxAZ7vRQ== + dependencies: + sdp "^3.2.0" + which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e"