Создание ИИ-интервьюера: архитектура и стек PrepWise

JavaScript Mastery 1,1 млн 3 ч 52 мин 33 мин
Главное

«Просто представьте: это уже не просто телефонный звонок, а полноценный ИИ-агент, способный проводить собеседования и выполнять сложные API-запросы за кулисами в реальном времени. Адриан Хадждин разбирает архитектуру современного приложения, где Next.js 19, Firebase и Gemini 2.0 объединяются для автоматизации процесса найма — от генерации вопросов до глубокой аналитики ответов кандидата».

🎙️ Обзор платформы PrepWise и старт разработки на Next.js 19 0:00

Забыть ответ на технический вопрос, который вы точно знали — кошмар любого разработчика на собеседовании . Адриан Хадждин (Adrian Hajdin) представляет решение этой проблемы: PrepWise, интерактивную платформу для тренировки интервью с использованием искусственного интеллекта . Это не просто проект для портфолио, а полноценный инструмент, имитирующий реальное общение с рекрутером в режиме реального времени . Платформа включает в себя защищенную аутентификацию, панель управления с историей сессий и, что самое важное, голосового ИИ-агента, способного задавать уточняющие вопросы и оценивать ответы кандидата .

Демонстрация возможностей ИИ-интервьюера 0:00

Основная ценность PrepWise заключается в реалистичности симуляции. В ходе демонстрации Адриан Хадждин (Adrian Hajdin) показывает процесс создания интервью: вместо заполнения скучных форм пользователь общается с ИИ-ассистентом . В демо-версии Адриан выбирает роль Senior Full Stack разработчика, смешанный тип интервью (поведенческое и техническое) и список технологий, включающий JavaScript, React и Next.js .

Процесс прохождения интервью выглядит следующим образом:

Технологический стек проекта объединяет самые современные инструменты: Next.js 19, Tailwind CSS, Shadcn UI, Firebase для базы данных и аутентификации, а также модели Gemini AI и GPT-4 для обработки логики . Перед началом кодинга автор настраивает аккаунт в Vapi — платформе для голосового ИИ, где создается базовый ассистент «Interview Prep» и настраивается рабочий процесс (workflow) .

Инициализация проекта и настройка окружения 9:25

Разработка начинается с создания папки проекта и выбора IDE. Адриан использует WebStorm, отмечая, что недавно этот инструмент стал бесплатным для некоммерческого использования . Для развертывания каркаса приложения используется стандартная команда npx create-next-app@latest .

В процессе инициализации выбираются следующие параметры :

После установки зависимостей проводится базовая очистка файловой структуры: удаляется стандартная иконка (favicon), очищается файл global.css (сохраняются только директивы Tailwind) и переписывается page.tsx с использованием сниппета rafc для создания чистого функционального компонента . Важным этапом является настройка шрифтов в корневом макете layout.tsx. Адриан заменяет стандартные шрифты на Mona Sans из библиотеки next/font/google, устанавливая переменную окружения для шрифта и подключая латинский поднабор . Также в тег html принудительно добавляется класс dark . Это гарантирует, что приложение всегда будет использовать темную тему, что критично для корректного отображения компонентов библиотек вроде Shadcn UI в связке с Tailwind .

Интеграция Shadcn UI и подготовка ресурсов 13:25

Для создания качественного интерфейса используется Shadcn UI — набор доступных и кастомизируемых компонентов . Процесс интеграции начинается с команды npx shadcn@latest init . Адриан обращает внимание на важный нюанс: при использовании React 19 могут возникнуть конфликты зависимостей . Для их решения при установке необходимо выбрать опцию «Use Legacy Peer Deps», что позволит корректно завершить инсталляцию пакетов в актуальной среде Node.js .

После настройки UI-библиотеки проект наполняется графическими активами и константами. Адриан заменяет стандартную папку public на подготовленный набор ресурсов, включающий иконки технологий (JavaScript, React, Next.js) и изображения для главной страницы . Также в корень проекта добавляются папки constants и types . В этих файлах содержится информация о технологическом стеке, которая понадобится для обучения ИИ выдавать релевантные вопросы, а также объекты для сопоставления названий технологий с их изображениями .

Архитектура маршрутизации через Route Groups 17:39

Для эффективного управления интерфейсом Адриан Хадждин (Adrian Hajdin) применяет механизм Route Groups (группы маршрутов) в Next.js . Это позволяет логически разделить приложение на части с разными макетами без изменения URL-структуры. Основная цель здесь — отделить страницы авторизации от основной части дашборда .

Настраивается следующая структура директорий в папке app:

  1. Группа (auth): Папка, название которой заключено в скобки, что исключает её из URL . Внутри создается собственный layout.tsx для страниц входа и регистрации, где отсутствует стандартная навигационная панель . В этой группе размещаются маршруты sign-in и sign-up .
  2. Группа (root): Группа для основного функционала приложения. Сюда переносится главная страница и создается отдельный root layout . В дальнейшем здесь будет реализована общая навигация (Navbar), доступная на всех страницах дашборда .

Такое разделение предотвращает конфликты маршрутов и позволяет гибко настраивать вложенные макеты . Завершая начальную настройку, Адриан обновляет global.css, импортируя расширенные стили из видео-кита . В файл добавляются специфические цветовые переменные (для статусов успеха или ошибок), фоновые паттерны и слои Tailwind (layer base, layer components), которые упрощают повторное использование стилей кнопок и карточек во всем приложении . Напоследок к тегу body в главном макете применяется класс pattern, задающий характерный фоновый рисунок платформы .

Универсальная система аутентификации: разработка AuthForm и FormField 25:44

Архитектура универсальной формы AuthForm

На этапе создания интерфейса аутентификации Адриан Хадждин (Adrian Hajdin) придерживается принципа DRY (Don't Repeat Yourself), объединяя логику входа и регистрации в одном гибком компоненте. Для этого в папке components создается файл AuthForm.tsx . Основным механизмом управления состоянием формы становится проп type, который принимает значения sign-in или sign-up . Это позволяет использовать один и тот же визуальный каркас для обеих страниц, просто переключая набор полей и текст на кнопках. Ранее в проекте уже были настроены группы маршрутов, что упрощает интеграцию этого компонента в структуру Next.js .

Для ускорения разработки Адриан Хадждин (Adrian Hajdin) интегрирует библиотеку Shadcn UI, устанавливая необходимые элементы через терминал: кнопки, формы, инпуты и библиотеку уведомлений Sonner . Визуальная составляющая формы базируется на CSS-классах, определенных в глобальных стилях проекта. Контейнер формы получает класс card-border и фиксированную минимальную ширину 566 пикселей для десктопных устройств .

Внутренняя структура формы включает:

Особое внимание уделяется условному рендерингу: поле для ввода имени отображается только в том случае, если type не равен sign-in . Это реализуется через простую проверку переменной isSignIn, что делает интерфейс лаконичным и интуитивно понятным .

Динамическая валидация и обработка данных 37:40

Для обеспечения безопасности и корректности вводимых данных Адриан Хадждин (Adrian Hajdin) использует связку React Hook Form и библиотеки Zod . Ключевой особенностью реализации является функция AuthFormSchema, которая динамически генерирует схему валидации в зависимости от текущего режима формы . Если пользователь регистрируется (sign-up), поле name становится обязательным строковым параметром с минимальной длиной в три символа . В режиме входа это же поле помечается как необязательное (z.optional()), чтобы не вызывать ошибок при отсутствии данных .

Валидация остальных полей включает:

Обработка отправки формы инкапсулирована в асинхронную функцию onSubmit . Внутри нее используется блок try/catch для перехвата возможных ошибок. В случае успеха Адриан использует toast.success из библиотеки Sonner для вывода всплывающих уведомлений о создании аккаунта или успешном входе . Для навигации между страницами применяется хук useRouter из пакета next/navigation . Например, после успешной регистрации пользователь автоматически перенаправляется на страницу входа, а после авторизации — на главную страницу дашборда . Для корректной работы уведомлений в корневой макет приложения (layout.tsx) добавляется компонент Toaster .

Разработка контролируемого компонента FormField 41:56

Чтобы избежать дублирования громоздкого JSX-кода для каждого поля ввода, Адриан Хадждин (Adrian Hajdin) выносит логику в отдельный переиспользуемый компонент FormField.tsx . В основе этого компонента лежит контроллер из React Hook Form, который связывает визуальный инпут с общим состоянием формы . Использование дженерика FormFieldProps<T> позволяет компоненту быть типобезопасным и адаптироваться к любым значениям, которые передаются в схему валидации .

Интерфейс FormFieldProps включает следующие параметры:

Внутри компонента используется функция render, которая предоставляет доступ к объекту field. Это позволяет автоматически передавать все необходимые обработчики событий (onChange, onBlur, value) в стандартный Shadcn-инпут через деструктуризацию пропсов {...field} . Адриан подчеркивает важность правильной настройки типа password: если забыть передать type="password" в компонент инпута, вводимые символы останутся видимыми, что является критической ошибкой безопасности . После фикса этой детали форма становится полностью функциональной и готовой к интеграции с серверной частью . В завершение этапа разработки Адриан создает GitHub-репозиторий и фиксирует изменения, чтобы зрители могли отслеживать прогресс по отдельным коммитам .

🧠 Проектирование интерфейса дашборда и динамических компонентов 50:05

После завершения базовой настройки проекта и маршрутизации, Адриан Хадждин переходит к активной фазе разработки пользовательского интерфейса. В качестве важного дополнения к рабочему процессу, он внедряет систему атомарных коммитов в GitHub: каждое логическое изменение в коде теперь фиксируется отдельным сохранением . Это решение было вдохновлено его курсом «Ultimate Next.js», где более 100 коммитов позволяют детально отследить эволюцию приложения . Работа над визуальной частью начинается с создания универсальной навигационной панели (Navbar) в корневом макете layout.tsx, которая содержит логотип PrepWise и ссылку на главную страницу .

Разработка структуры главной страницы и карточек интервью 53:47

Главная страница дашборда проектируется как центральный хаб для пользователя. Адриан Хадждин оборачивает содержимое в React-фрагменты и использует секции с Tailwind-классами для создания отзывчивого макета . Основной акцент сделан на призыве к действию (CTA): блок с заголовком «Get interview ready...» ограничивается максимальной шириной 500 пикселей, чтобы текст оставался читабельным на больших экранах .

Интерфейс включает:

Для отображения списка интервью Адриан создаёт компонент InterviewCard. Поскольку реальная логика создания интервью будет реализована позже, на данном этапе используются заглушки из файла констант (dummyInterviews) . Каждая карточка получает уникальный ключ через interview.id, что критически важно для корректного рендеринга списков в React .

Интеграция Day.js и нормализация данных карточки 58:32

Для работы с временными метками в проект добавляется библиотека Day.js . Она позволяет преобразовывать технические даты создания записей в человекочитаемый формат, например, «March 15, 2024» . Внутри InterviewCard Адриан Хадждин применяет деструктуризацию пропсов, извлекая роль, стек технологий и дату создания интервью .

Особое внимание уделяется нормализации типов интервью. Адриан использует регулярные выражения, чтобы привести различные варианты написания (например, «Technical» или «Mixed») к единому стандарту отображения в бейджах на карточке . Визуально карточка дополняется случайно сгенерированным логотипом компании через функцию getRandomInterviewCover, которая выбирает изображения из локальной папки public . Если интервью ещё не было пройдено, вместо оценки отображается прочерк, а пользователю предлагается описание «You haven't taken the interview yet» .

Динамическое сопоставление технологий через Devicon 1:12:45

Финальным этапом текущей главы становится реализация системы отображения иконок технологий. Вместо ручного подбора изображений для каждой библиотеки или языка, Адриан Хадждин внедряет утилиту getTechLogos, которая работает в связке с внешним CDN Devicon . Эта функция выполняет «очистку» названий: она приводит их к нижнему регистру, удаляет точки и лишние пробелы .

Это позволяет системе гибко реагировать на ввод пользователя:

Для чистоты кода Адриан выносит эту логику в отдельный компонент DisplayTechIcons . На данном этапе компонент принимает массив технологий (Tech Stack) и готовит почву для рендеринга ряда иконок под описанием интервью, что делает дашборд визуально информативным и профессиональным .

🔐 Интеграция Firebase: архитектура двух SDK и серверная регистрация 1:15:21

Завершив работу над визуальной составляющей дашборда и динамическим отображением иконок технологий через Devicon , Адриан Хадждин (Adrian Hajdin) переходит к реализации бизнес-логики приложения. Центром управления данными и аутентификацией в PrepWise становится Firebase — облачная платформа от Google, работающая по модели «бэкенд как сервис» (BaaS) . Выбор этого инструмента обусловлен его популярностью среди стартапов и крупных компаний, таких как Duolingo и Alibaba, благодаря возможности быстро развертывать базы данных и системы безопасности без необходимости вручную настраивать серверную инфраструктуру .

Инициализация серверного и клиентского Firebase SDK 1:19:59

Одной из ключевых особенностей работы с Firebase в современных фреймворках вроде Next.js является необходимость разделения уровней доступа. Адриан подчеркивает, что проект требует инициализации двух разных SDK: Client SDK для взаимодействия из браузера и Admin SDK для защищенных операций на стороне сервера . Настройка начинается в консоли Firebase с создания проекта под названием PrepWise, где активируются два основных модуля: Authentication (с провайдером Email/Password) и Firestore Database в производственном режиме .

Для клиентской части создается файл firebase/client.ts, куда копируются конфигурационные данные веб-приложения . Адриан обращает внимание на то, что инициализация должна быть защищена от дублирования: код проверяет наличие уже запущенных инстансов Firebase-приложения, прежде чем создавать новое, что критично для стабильной работы в режиме горячей перезагрузки (HMR) . В итоге клиентский SDK экспортирует объекты auth и db, которые будут использоваться для базовых запросов и входа пользователей .

Серверная инициализация (Admin SDK) требует более строгого подхода к безопасности. Вместо публичных ключей здесь используются секретные переменные окружения, полученные через сервисный аккаунт . В файле firebase/admin.ts Адриан настраивает функцию initFirebaseAdmin, которая использует сертификат с идентификатором проекта, клиентским email и приватным ключом . Особое внимание уделяется обработке приватного ключа:

Эта двухслойная архитектура позволяет безопасно выполнять мутации данных на сервере, в то время как клиентская часть занимается только отображением и инициацией процесса входа .

Реализация безопасной регистрации через Server Actions 1:32:18

После настройки инфраструктуры Адриан приступает к созданию логики регистрации, используя возможности Server Actions в Next.js. Процесс авторизации в PrepWise строится по следующей схеме: пользователь вводит данные на фронтенде, клиентский SDK генерирует ID-токен, а затем серверный экшен проверяет этот токен и управляет сессией . Для этого в папке lib/actions создается файл off.action.ts с директивой 'use server' .

Функция signup спроектирована так, чтобы не просто создавать учетную запись, но и синхронизировать её с базой данных Firestore . Принимая параметры uid, name и email, функция выполняет несколько критических проверок:

  1. Проверка существования: Перед созданием записи сервер обращается к коллекции users, используя uid в качестве идентификатора документа . Если пользователь уже существует, процесс прерывается с уведомлением «User already exists» .
  2. Обработка специфических ошибок: В блоке catch Адриан настраивает проверку кода ошибки auth/email-already-exists, чтобы возвращать пользователю понятное сообщение на понятном языке вместо системного стека ошибок .
  3. Сохранение профиля: Если проверки пройдены, метод db.collection('users').doc(uid).set(...) создает профиль пользователя в Firestore, сохраняя его имя и адрес электронной почты .

На стороне пользовательского интерфейса в компоненте AuthForm (ранее разработанном во второй главе) Адриан интегрирует вызов метода createUserWithEmailAndPassword из Firebase Client SDK . Этот метод регистрирует пользователя непосредственно в системе аутентификации Firebase, возвращая уникальный uid, который затем передается в вышеупомянутый серверный экшен signup .

Для визуальной обратной связи используется библиотека Sonner: если серверный экшен возвращает ошибку, на экране мгновенно всплывает «toast»-уведомление с детальным описанием проблемы . В случае успеха пользователь перенаправляется на страницу входа, где на следующем этапе будет реализована система сессионных куки для защиты приватных маршрутов .

🔐 Безопасность и архитектура сессий: защита маршрутов и серверная авторизация 1:40:13

После настройки базовой регистрации Адриан Хадждин (Adrian Hajdin) переходит к критически важному этапу — созданию надежной системы управления сессиями. Вместо того чтобы полагаться на хранение токенов в localStorage, что делает приложение уязвимым для XSS-атак, проект переводится на использование защищённых кук (cookies) на стороне сервера .

Безопасное управление сессиями через куки 1:40:13

Для реализации механизма сессий Адриан создаёт функцию setSessionCookie, которая генерирует зашифрованный идентификатор . Срок жизни сессии устанавливается ровно на одну неделю — разработчик специально рассчитывает это значение как `60

При настройке куки-стора (cookie store) в Next.js устанавливаются строгие параметры безопасности:

Адриан объясняет архитектурный выбор в пользу серверной авторизации . Хотя Firebase позволяет управлять пользователями на клиенте, серверный подход в Next.js даёт три ключевых преимущества: невозможность кражи токенов через XSS, полную совместимость с Server Side Rendering (SSR) и упрощённое управление ролями пользователей непосредственно в API-маршрутах . «Вы доверяете серверу, потому что полностью контролируете его», — отмечает он .

Глубокая защита маршрутов и проверка состояния пользователя 1:51:02

Чтобы приложение не оставалось «открытой книгой» для неавторизованных посетителей, Адриан внедряет логику защиты приватных путей . Сердцем этой системы становится асинхронная функция getCurrentUser в файле серверных действий auth.actions.ts .

Процесс проверки пользователя разделен на несколько этапов:

  1. Извлечение куки: Приложение обращается к cookies() из next/headers и пытается найти сессионный токен . Если куки нет, функция мгновенно возвращает null .
  2. Верификация через Admin SDK: Если кука найдена, она передаётся в метод verifySessionCookie серверного Firebase Admin SDK . Здесь проверяется не только валидность подписи, но и то, не была ли сессия отозвана ранее .
  3. Синхронизация с базой данных: После успешного декодирования claims (заявлений) токена, система запрашивает актуальные данные пользователя из коллекции users в Firestore, используя его уникальный uid .

Для удобства использования в интерфейсах Адриан создаёт вспомогательную функцию isAuthenticated . Здесь он демонстрирует полезный «трюк» с двойным восклицательным знаком (!!), который быстро преобразует наличие или отсутствие объекта пользователя в булево значение (true/false) .

Автоматические перенаправления на уровне макетов Next.js 1:55:54

Финальный штрих в системе безопасности — интеграция проверок в корневые макеты (layouts) приложения. В основном layout.tsx добавляется проверка isUserAuthenticated . Если пользователь не авторизован, функция redirect из next/navigation автоматически отправляет его на страницу входа /sign-in .

Интересный подход применяется и к страницам самой авторизации. В auth/layout.tsx реализуется обратная логика: если уже вошедший в систему пользователь пытается снова открыть страницу регистрации, приложение бесшовно перенаправляет его на главную страницу (dashboard) . «Им не нужно логиниться дважды», — комментирует Адриан .

После настройки защиты Адриан проводит финальный аудит конфигурации, напоминая о важности чистоты .env файлов — лишние запятые или пробелы в ключах Firebase могут привести к трудноотловимым ошибкам авторизации . Убедившись в корректности работы редиректов и создании записей в Firebase Console , он приступает к подготовке интерфейса для генерации интервью, который станет доступен только авторизованным пользователям . Для этого создается новый маршрут /interview и базовый компонент Agent, который в будущем будет отвечать за голосовое взаимодействие .

🤖 Интеграция Gemini AI и создание API для генерации вопросов 2:05:21

Для того чтобы платформа PrepWise могла проводить качественные интервью, ей необходим «мозг», способный генерировать персонализированные вопросы на основе стека технологий и опыта кандидата. Адриан Хадждин (Adrian Hajdin) переходит к реализации серверной логики, которая свяжет пользовательский интерфейс, нейросеть Gemini от Google и базу данных Firestore . Этот этап превращает статический интерфейс в динамическое приложение, способное подстраиваться под конкретную вакансию.

Интерфейс взаимодействия и визуализация транскрибации 2:05:36

Прежде чем переходить к серверной части, Адриан завершает работу над фронтендом страницы интервью. Он реализует логику кнопки вызова, состояние которой зависит от статуса звонка: если сессия неактивна или завершена, отображается текст «Call», в противном случае — индикатор загрузки или кнопка завершения . Для визуального отклика добавляется пульсирующая анимация (ping animation) с использованием утилит Tailwind CSS, которая активируется, когда ИИ-агент начинает говорить .

Особое внимание уделяется отображению транскрипта беседы в реальном времени. Адриан создает систему макетирования сообщений, где текст плавно появляется на экране . Основные элементы этого блока:

После завершения верстки страницы Адриан делает коммит «Implement interview generation page UI», фиксируя готовность клиентской части к интеграции с ИИ .

Настройка Gemini AI и Vercel AI SDK 2:11:25

Для генерации вопросов Адриан выбирает Gemini AI, так как эта модель предоставляет бесплатный уровень использования, что идеально подходит для обучения и небольших проектов . Процесс настройки начинается с получения API-ключа в Google AI Studio . Этот ключ сохраняется в файле .env.local под переменной GOOGLE_GENERATIVE_AI_API_KEY .

Для взаимодействия с моделью используется Vercel AI SDK — библиотека, которая абстрагирует работу с различными LLM (Large Language Models). Адриан поясняет, что такой подход позволяет легко переключиться, например, с Gemini на OpenAI или Anthropic в будущем, не переписывая основной код . В проект устанавливаются два ключевых пакета:

  1. npm install ai — основной SDK от Vercel .
  2. @ai-sdk/google — специфический провайдер для моделей Gemini .

Дополнительно в папке lib создается файл vapi.sdk.ts, где инициализируется клиент Vapi для работы с голосовым интерфейсом, используя публичный токен из настроек организации в панели Vapi .

Разработка API-эндпоинта для генерации интервью 2:17:40

Центральным элементом этой главы является создание Next.js Route Handler. Это серверный маршрут, который будет принимать данные о желаемом интервью и возвращать список вопросов от ИИ. Адриан структурирует проект, создавая путь app/api/vapi/generate/route.ts .

Сначала реализуется простой GET-запрос для проверки работоспособности маршрута, возвращающий JSON с сообщением о статусе 200 . Тестирование проводится через легковесный HTTP-клиент (аналог Postman), что подтверждает корректность настройки серверной части Next.js .

Основная логика ложится на POST-запрос. Из тела запроса (request.json()) извлекаются ключевые параметры: роль (role), уровень (level), технологический стек (tech stack), тип интервью и количество вопросов . Эти данные станут основой для формирования промпта.

Промпт-инжиниринг и сохранение данных в Firestore 2:21:29

Для получения качественного результата Адриан использует функцию generateText из Vercel AI SDK, указывая модель gemini-2.0-flash . Ключевой момент здесь — составление промпта. Адриан подчеркивает, что промпт-инжиниринг — это искусство . В инструкции для ИИ указывается:

После того как Gemini возвращает текст, вопросы парсятся в массив с помощью JSON.parse . Затем создается объект интервью, который включает в себя очищенный стек технологий (через .split(',')), сгенерированные вопросы, ID пользователя и случайную обложку . Эти данные сохраняются в коллекцию interviews базы данных Firestore . Проверка через HTTP-клиент показывает, что в базе успешно создается документ с полным списком вопросов, готовых к использованию голосовым агентом .

Деплой на Vercel для внешней интеграции 2:27:11

Чтобы сервис Vapi мог обращаться к созданному API-маршруту, приложение должно быть доступно в интернете. Адриан инициирует процесс деплоя на платформу Vercel . При импорте проекта из GitHub критически важно перенести все переменные окружения, включая ключи Firebase и Gemini .

В процессе сборки Адриан сталкивается с тем, что ошибки ESLint или TypeScript могут блокировать билд на Vercel. Чтобы ускорить тестирование, он вносит изменения в next.config.js, добавляя параметры ignoreDuringBuilds: true для ESLint и ignoreBuildErrors: true для TypeScript . Это временная мера, позволяющая получить рабочий URL для настройки вебхуков и продолжить разработку голосового взаимодействия . После успешного деплоя и появления «конфетти» на экране Vercel, проект готов к самой захватывающей части — настройке ИИ-агента .

🎙️ Проектирование голосового интеллекта: Workflow в Vapi и интеграция SDK 2:30:28

Проектирование голосового сценария в панели управления Vapi 2:30:28

Работа над ИИ-ассистентом начинается в панели управления Vapi, где Адриан Хадждин (Adrian Hajdin) выстраивает логику взаимодействия через систему узлов (nodes) . Первым шагом создаётся узел типа «Say», который приветствует пользователя, используя синтаксис двойных фигурных скобок для обращения по имени: Hello {{username}} . Это позволяет сделать начало диалога персонализированным, после чего бот сообщает о готовности подготовить идеальное интервью .

Для сбора параметров будущего интервью используется узел «Gather», где разработчик вручную определяет переменные, которые ИИ должен извлечь из речи пользователя . Ключевыми параметрами становятся:

Адриан Хадждин (Adrian Hajdin) подчёркивает, что описания (descriptions) для этих полей критически важны, так как они дают контекст ассистенту для ведения естественного диалога . Он рекомендует делать вопросы открытыми, чтобы бот не просто зачитывал список опций, а общался как живой человек . После того как данные собраны, в цепочку добавляется узел «API Request» . Этот узел отправляет POST-запрос на развёрнутый в Vercel эндпоинт приложения (/api/vapi/generate), передавая все собранные переменные и userId в теле запроса . Завершается сценарий финальным узлом «Say», подтверждающим генерацию интервью, и командой «Hang Up» для завершения вызова .

Тонкая настройка личности ассистента и окружения 2:37:14

После создания рабочего процесса (workflow) необходимо привязать его к конкретному ассистенту. В разделе Assistants Адриан создаёт нового агента под названием «Interview Generator», выбирая Vapi в качестве провайдера и подключая ранее настроенный JSM-workflow . Платформа позволяет кастомизировать не только логику, но и само «звучание» бота. Например, можно включить эффект фонового шума офиса (office background sound) для придания реалистичности или активировать функцию «back channeling» .

Функция back-channeling заставляет бота вставлять в речь междометия вроде «м-хм» или «понимаю», что делает диалог менее механическим . Адриан Хадждин (Adrian Hajdin) экспериментирует с различными голосами, выбирая между мягким тоном Hannah и более жизнерадостным Lily, проверяя их через встроенный симулятор разговора . После финальной настройки крайне важно нажать кнопку «Publish» и скопировать Assistant ID . Этот ID, наряду с базовым URL приложения, добавляется в переменные окружения Vercel (NEXT_PUBLIC_VAPI_WORKFLOW_ID), после чего проект отправляется на пересборку (redeploy) для активации изменений .

Интеграция Vapi SDK и обработка событий звонка в коде 2:41:52

Переходя к фронтенд-части на Next.js, Адриан настраивает компонент Agent, который должен обрабатывать звонок в реальном времени. В клиентском компоненте ('use client') инициализируются состояния для отслеживания статуса вызова, процесса речи (isSpeaking) и массива сообщений транскрипта . Для хранения истории диалога создаётся интерфейс SavedMessage, включающий роли (user, system, assistant) и текстовый контент .

Основная логика взаимодействия с Vapi Web SDK сосредоточена в хуке useEffect. Адриан Хадждин (Adrian Hajdin) настраивает слушателей событий, которые сопоставляют внутренние процессы SDK с состоянием React-приложения :

  1. call-start: устанавливает статус активного вызова .
  2. message: фильтрует входящие данные и, если тип транскрипта помечен как final, сохраняет сообщение в массив messages, обновляя интерфейс .
  3. speech-start / speech-end: управляют визуальной индикацией того, что бот в данный момент говорит .
  4. error: выводит ошибки в консоль для отладки .

Особое внимание уделяется очистке ресурсов: при размонтировании компонента все слушатели должны быть отключены через vapi.off(), чтобы избежать утечек памяти и замедления работы приложения .

Для запуска процесса используется функция handleCall, которая вызывает vapi.start(), передавая ID воркфлоу и переменные пользователя (имя и ID) . Когда разговор завершён и статус меняется на finished, другой useEffect автоматически перенаправляет пользователя на главную страницу, где его уже ждёт сгенерированное интервью . В ходе живого теста Адриан демонстрирует, как агент успешно собирает данные о роли фронтенд-разработчика и стеке React/Next.js, после чего прощается и кладет трубку . Ранее в разговоре они касались структуры API для генерации вопросов, и теперь эта связка начинает работать как единый механизм .

📊 Интеграция данных и оптимизация производительности 2:55:34

После успешного тестирования процесса генерации вопросов, Адриан Хадждин (Adrian Hajdin) переходит к этапу отображения данных на главной странице . На этом этапе приложение превращается из набора разрозненных функций в полноценную платформу, где пользователь может видеть свою историю интервью и доступные сессии других участников. Ключевым аспектом этой главы становится не только корректный вывод данных из Firebase Firestore, но и оптимизация серверных запросов для обеспечения максимальной скорости отклика интерфейса .

Получение данных из Firestore и настройка индексов 2:56:58

Первым шагом к динамическому интерфейсу становится создание серверных экшенов для извлечения интервью из базы данных. Адриан Хадждин (Adrian Hajdin) переносит логику в файл off.action.ts (позже реорганизованный в general.action.ts), где реализует функцию getInterviewsByUserId . Функция выполняет запрос к коллекции Firestore, фильтруя документы по идентификатору пользователя и сортируя их по дате создания в порядке убывания (desc) .

Процесс извлечения данных в Firebase имеет свои особенности:

Интеграция на главной странице (page.tsx) включает проверку наличия сессий через условие hasPastInterviews . Если у пользователя ещё нет созданных интервью, приложение корректно отображает заглушку, предлагая начать первую сессию .

Параллельное извлечение данных с Promise.all 3:02:44

Одной из центральных тем этой части туториала является оптимизация производительности при работе с несколькими независимыми запросами к БД. Помимо личных интервью пользователя, на главной странице должны отображаться «последние интервью», созданные другими участниками платформы . Для этого Адриан создает функцию getLatestInterviews, которая запрашивает завершенные сессии, где userId не совпадает с текущим пользователем .

Вместо последовательного выполнения запросов через await, которое заставило бы второй запрос ждать завершения первого, Адриан внедряет концепцию параллельных запросов :

  1. Проблема блокировки: Последовательные вызовы увеличивают время загрузки страницы, так как время ожидания суммируется .
  2. Решение через Promise.all: Метод принимает массив промисов и выполняет их одновременно .
  3. Эффект: Использование деструктуризации массива const [userInterviews, latestInterviews] = await Promise.all([...]) позволяет сократить время загрузки данных практически вдвое .

Адриан Хадждин (Adrian Hajdin) отмечает, что такая оптимизация является стандартом индустрии и ранее подробно рассматривалась им в курсе «Ultimate Next.js» на примере приложения Dev Overflow . Это критически важно для Server Components в Next.js, так как позволяет минимизировать время отрисовки страницы на стороне сервера.

Создание динамических маршрутов для прохождения интервью 3:10:48

После настройки главной страницы фокус смещается на создание индивидуальных страниц для каждого интервью. Для этого используется механизм динамических роутов в Next.js: создается папка interview/[id], где id — уникальный идентификатор документа в Firestore . Внутри файла page.tsx этого раздела Адриан реализует получение параметров маршрута через объект params .

Логика страницы интервью включает:

В завершение главы Адриан интегрирует компонент Agent, который ранее использовался только для этапа генерации . Теперь этот же компонент адаптируется для проведения самого интервью. Ключевое отличие заключается в передаче пропса type="interview" и списка вопросов, полученных из базы данных . Это подготавливает почву для настройки логики Vapi, которая будет отвечать за озвучивание конкретных технических вопросов, а не просто сбор требований .

🤖 Промпт-инжиниринг и интеллектуальный анализ интервью 3:20:42

После того как техническая база для голосовых вызовов была заложена в предыдущих частях работы с Vapi SDK, Адриан Хадждин переходит к критически важному этапу: настройке «личности» ИИ-интервьюера и механизмов автоматизированной оценки кандидата . На этом этапе приложение превращается из простого инструмента для звонков в полноценную образовательную платформу, способную не только слушать, но и глубоко анализировать профессиональные навыки пользователя .

Промпт-инжиниринг ИИ-интервьюера на GPT-4 3:25:50

Для того чтобы ИИ вел себя как настоящий профессиональный рекрутер, Адриан настраивает объект конфигурации interviewer в файле констант . В качестве «мозга» агента выбрана модель GPT-4 от OpenAI, которая обеспечивает наиболее естественное и контекстное ведение беседы . Однако одной модели недостаточно — Адриан подчеркивает, что решающее значение имеет промпт-инжиниринг, задающий строгое системное поведение .

Инструкция для ИИ-агента превращает его в профессионального интервьюера, цель которого — оценить квалификацию, мотивацию и соответствие кандидата роли . Промпт включает следующие ключевые директивы:

Помимо текстовых инструкций, Адриан детально настраивает параметры голоса: стабильность (stability), усиление сходства (similarity boost) и стиль . Это позволяет избежать роботизированного звучания и сделать ИИ-агента более похожим на живого человека, что критично для снижения стресса у пользователя во время тренировки . В ходе демонстрационного звонка Адриан показывает, как агент реагирует на технические ответы и даже на попытку кандидата досрочно завершить интервью, сохраняя профессиональный тон до последней секунды .

Анализ транскрипта и структурированный фидбек Gemini 3:29:52

Самая ценная часть платформы PrepWise — это автоматическая генерация отчета после завершения звонка. Для реализации этой функции Адриан создает серверный экшен createFeedback в файле general.action.ts . Основная сложность заключается в том, что ИИ должен проанализировать весь массив сообщений (транскрипт) и выдать не просто текст, а структурированные данные .

Для решения этой задачи используется функция generateObject из Vercel AI SDK в связке с моделью Gemini 2.0 Flash от Google . В отличие от стандартной генерации текста, этот подход гарантирует получение объекта, строго соответствующего заданной схеме (Zod schema) . Адриан отмечает, что для повышения стабильности результатов он отключает параметр structuredOutputs, полагаясь на мощь самой модели в интерпретации схемы .

Система оценивает кандидата по пяти фиксированным категориям :

  1. Коммуникативные навыки: насколько четко и ясно излагаются мысли .
  2. Технические знания: глубина владения стеком технологий .
  3. Решение проблем: логика подхода к сложным задачам .
  4. Культурное соответствие: soft skills и манера общения .
  5. Уверенность: общее впечатление от подачи ответов .

По каждой категории ИИ выставляет оценку от 0 до 100 и пишет развернутый комментарий . Кроме того, Gemini формирует общий список сильных сторон кандидата, зоны для роста и финальное заключение . Весь этот массив данных сохраняется в базу данных Firestore в коллекцию feedback, привязываясь к конкретному интервью и пользователю .

Интеграция логики сохранения и получения отчетов 3:37:33

Чтобы связать фронтенд с логикой анализа, Адриан обновляет клиентский компонент Agent, заменяя моковые данные на реальный вызов экшена createFeedback . Теперь, как только статус звонка меняется на «завершено», приложение автоматически собирает все сообщения из чата, отправляет их на анализ Gemini и получает уникальный ID созданного фидбека . После успешного сохранения происходит автоматический редирект на страницу отчета .

Для отображения данных на странице /feedback Адриан разрабатывает вспомогательную функцию getFeedbackByInterviewId . Она извлекает документ из Firestore, выполняя фильтрацию по ID интервью и ID пользователя, чтобы обеспечить безопасность данных . Примечательно, что Адриан использует метод limit(1), так как для каждого сеанса интервью может существовать только один итоговый отчет .

В завершение главы Адриан тестирует полный цикл: проводит короткое интервью, завершает вызов и демонстрирует в консоли детальный объект фидбека, который уже содержит баллы за коммуникацию, технические замечания и зоны для развития . Этот объект станет основой для финальной верстки интерфейса отчетов в следующей главе . Ранее в разговоре они касались структуры Firestore, что позволило бесшовно интегрировать новую коллекцию для отзывов.

🏁 Завершение разработки: интерфейс отчетов и финальный деплой 3:45:55

Заключительный этап создания платформы PrepWise посвящен визуализации результатов интервью и выводу приложения в продакшен. Адриан Хадждин (Adrian Hajdin) подчеркивает, что для закрепления материала курса он подготовил два практических упражнения: одно касается верстки интерфейса на JSX, второе — логики повторного прохождения интервью . Основная задача этой главы — превратить сырые данные, полученные от ИИ в предыдущих разделах, в презентабельную страницу фидбека и убедиться, что всё корректно работает на живом сервере .

Визуализация аналитики и вёрстка страницы отзывов 3:46:36

После того как ИИ-ассистент завершил анализ транскрипта (о чем шла речь в предыдущих главах), приложение получает структурированный объект с данными. Первая задача разработчика — отобразить этот отчет пользователю . Адриан Хадждин демонстрирует решение, основанное на использовании Tailwind CSS для быстрой стилизации компонентов .

Интерфейс страницы отчетов строится по следующей иерархии:

Адриан Хадждин рекомендует просматривать эту страницу на десктопных экранах, так как большой объем аналитических данных требует значительного экранного пространства для комфортного чтения .

Интеграция обратной связи в дашборд 3:48:12

Важным шагом является обновление главной страницы приложения. До этого момента на карточках интервью отображалось заглушка «Вы еще не проходили это интервью» . Чтобы сделать интерфейс динамичным, необходимо обновить компонент InterviewCard .

Логика обновления заключается в проверке наличия существующего фидбека в базе данных. Для этого используется асинхронная функция getFeedbackByInterviewId, в которую передаются userId и interviewId . Если данные найдены, вместо стандартного приглашения пройти опрос, карточка отображает реальный результат — например, «15 из 100» с кратким вердиктом о том, что кандидат справился плохо .

Также на карточке появляется кнопка «Check feedback», которая перенаправляет пользователя на страницу с детальным отчетом . В качестве второго самостоятельного упражнения Адриан предлагает зрителям реализовать логику кнопки пересдачи интервью, которая позволит сбросить текущие результаты и запустить процесс заново .

Финальный деплой на Vercel и проверка системы 3:49:51

Когда UI-часть полностью готова, наступает момент финального развертывания. Поскольку проект изначально настраивался с учетом интеграции с Vercel, процесс публикации сводится к стандартному циклу Git-команд . После выполнения git push изменения автоматически подхватываются платформой .

Процесс сборки (build) обновленной версии занимает около 45 секунд, после чего приложение становится доступно по публичному URL . Адриан Хадждин проводит финальное тестирование в «боевых» условиях:

  1. Авторизация: Вход в систему выполняется через ранее созданную учетную запись, так как приложение использует общую базу данных Firestore для разработки и продакшена .
  2. Проверка данных: После входа в дашборде корректно отображаются все созданные интервью и статусы их прохождения .
  3. Голосовой агент: Тестируется работа ИИ-ассистента в облаке. Адриан проверяет, насколько быстро агент реагирует на голос и корректно ли запускаются рабочие процессы (workflows) .
  4. Стабильность: Подтверждается, что развернутая версия работает так же безупречно, как и локальная среда разработки .

Итоги курса и экосистема Vapi 3:51:12

Завершая туториал, Адриан Хадждин подводит итоги проделанной работы. Проект объединил в себе мощь Firebase для управления бэкендом (аутентификация и хранилище) и инновационные возможности Vapi для создания голосовых интерфейсов . Ключевой особенностью стало использование человекоподобных голосов и интеграция сложных ИИ-сценариев, где агент не просто задает вопросы, но и собирает данные для последующего глубокого анализа .

Адриан благодарит команду Vapi за предоставление инструментов, которые позволяют разработчикам бесшовно внедрять голосовой ИИ в веб-приложения . Для тех, кто стремится к более глубокому изучению Full Stack разработки, он рекомендует платформу JS Mastery Pro, где представлены оптимизированные программы обучения, помогающие быстрее получить работу или повышение в карьере . Проект PrepWise служит отличным примером того, как современные API могут превратить стандартное Next.js приложение в сложный интеллектуальный продукт .

💬 Цитаты

«Просто представьте, сколько полезных сценариев использования есть у этой технологии. Это не просто звонок, это агент, способный выполнять API-запросы за кулисами.»

Адриан Хадждин 40:58

«Формы — это одна из самых распространенных вещей, которые вы будете создавать в веб-приложении, но в то же время и одна из самых сложных.»

Адриан Хадждин 28:09

«Я решил делать по одному коммиту на каждый урок, чтобы вы могли видеть точные изменения в коде на каждом этапе.»

Адриан Хадждин 50:32

«Managing the authentication on the server side is the recommended approach... it is the right way to do things.»

Адриан Хадждин 47:21

«Использование Promise.all по сути удваивает скорость, с которой мы выполняем оба этих запроса.»

Адриан Хадждин 05:26
👥 Спикер
📖 Термины
Vapi
Платформа для создания и интеграции голосовых ИИ-агентов с низкой задержкой ответа.
Route Groups
Функция Next.js, позволяющая группировать маршруты в папках без влияния на структуру URL.
Server Actions
Метод выполнения функций на сервере прямо из компонентов React в Next.js.
Firebase Admin SDK
Библиотека для безопасных серверных операций с сервисами Firebase, включая авторизацию и базу данных.
Технологии и IT Next.js 19 Vapi Firebase Gemini 2.0 ИИ-агенты