«Просто представьте: это уже не просто телефонный звонок, а полноценный ИИ-агент, способный проводить собеседования и выполнять сложные 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 .
Процесс прохождения интервью выглядит следующим образом:
- Голосовое взаимодействие: ИИ-агент, работающий на базе Vapi, приветствует кандидата и задает вопросы естественным голосом .
- Динамическая адаптация: Бот не просто читает список вопросов, а реагирует на ответы. Когда Адриан пытается уйти от прямого ответа, рекламируя свою платформу JS Mastery Pro, ИИ замечает это и просит вернуться к техническим деталям .
- Режим реального времени: Благодаря задержке менее 500 миллисекунд, общение ощущается как разговор с живым человеком .
- Глубокая аналитика: После завершения сессии платформа генерирует подробный отчет. В демо-версии Адриан намеренно «проваливает» интервью, за что получает низкие баллы за коммуникацию и решение проблем, а также конкретные советы по улучшению ответов .
Технологический стек проекта объединяет самые современные инструменты: 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 .
В процессе инициализации выбираются следующие параметры :
- TypeScript: для обеспечения типизации и чистоты кода.
- ESLint: для поддержания стандартов разработки.
- Tailwind CSS: для стилизации интерфейса.
- App Router: современный стандарт маршрутизации Next.js.
- Turbopack: для ускорения сборки в режиме разработки.
После установки зависимостей проводится базовая очистка файловой структуры: удаляется стандартная иконка (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:
- Группа
(auth): Папка, название которой заключено в скобки, что исключает её из URL . Внутри создается собственныйlayout.tsxдля страниц входа и регистрации, где отсутствует стандартная навигационная панель . В этой группе размещаются маршрутыsign-inиsign-up. - Группа
(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 пикселей для десктопных устройств .
Внутренняя структура формы включает:
- Логотип приложения
logo.svgи текстовое название проекта PrepWise ; - Заголовок H3 с призывом к действию («Practice job interviews with AI») ;
- Динамическую ссылку под основной формой, позволяющую пользователю переключаться между экранами входа и регистрации с помощью компонента
Linkизnext/link.
Особое внимание уделяется условному рендерингу: поле для ввода имени отображается только в том случае, если type не равен sign-in . Это реализуется через простую проверку переменной isSignIn, что делает интерфейс лаконичным и интуитивно понятным .
Динамическая валидация и обработка данных 37:40
Для обеспечения безопасности и корректности вводимых данных Адриан Хадждин (Adrian Hajdin) использует связку React Hook Form и библиотеки Zod . Ключевой особенностью реализации является функция AuthFormSchema, которая динамически генерирует схему валидации в зависимости от текущего режима формы . Если пользователь регистрируется (sign-up), поле name становится обязательным строковым параметром с минимальной длиной в три символа . В режиме входа это же поле помечается как необязательное (z.optional()), чтобы не вызывать ошибок при отсутствии данных .
Валидация остальных полей включает:
email: проверка на соответствие формату адреса электронной почты через встроенный метод.email();password: требование минимальной длины (от 3 символов на этапе разработки с возможностью усиления в будущем) .
Обработка отправки формы инкапсулирована в асинхронную функцию 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 включает следующие параметры:
control: объект управления от React Hook Form ;name: имя поля, соответствующее ключу в Zod-схеме ;label: текстовая метка для пользователя ;placeholderиtype: настройки отображения и типа вводимых данных (text, email или password) .
Внутри компонента используется функция 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 пикселей, чтобы текст оставался читабельным на больших экранах .
Интерфейс включает:
- Секцию с кнопкой «Start an interview», которая использует компонент
Buttonиз Shadcn UI с дочерней ссылкойnext/link. - Декоративное изображение робота, которое автоматически скрывается на мобильных устройствах для экономии экранного пространства .
- Два основных подраздела: «Your interviews» (пройденные тесты) и «Take an interview» (доступные варианты) .
Для отображения списка интервью Адриан создаёт компонент 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 . Эта функция выполняет «очистку» названий: она приводит их к нижнему регистру, удаляет точки и лишние пробелы .
Это позволяет системе гибко реагировать на ввод пользователя:
- Ввод «React.js», «ReactJS» или просто «React» будет корректно сопоставлен с официальным логотипом React .
- Утилита обращается к заранее подготовленному объекту
mappings, где зафиксированы пути к иконкам TypeScript, Next.js, Tailwind CSS и других технологий .
Для чистоты кода Адриан выносит эту логику в отдельный компонент 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 и приватным ключом . Особое внимание уделяется обработке приватного ключа:
- Ключ импортируется из
.env.local. - Строка ключа должна быть очищена от некорректных символов переноса строки с помощью метода
.replace(/\\n/g, '\n'), иначе аутентификация на сервере завершится ошибкой . - Admin SDK предоставляет полные привилегии для чтения и записи в Firestore, минуя правила безопасности, настроенные для клиентов .
Эта двухслойная архитектура позволяет безопасно выполнять мутации данных на сервере, в то время как клиентская часть занимается только отображением и инициацией процесса входа .
Реализация безопасной регистрации через Server Actions 1:32:18
После настройки инфраструктуры Адриан приступает к созданию логики регистрации, используя возможности Server Actions в Next.js. Процесс авторизации в PrepWise строится по следующей схеме: пользователь вводит данные на фронтенде, клиентский SDK генерирует ID-токен, а затем серверный экшен проверяет этот токен и управляет сессией . Для этого в папке lib/actions создается файл off.action.ts с директивой 'use server' .
Функция signup спроектирована так, чтобы не просто создавать учетную запись, но и синхронизировать её с базой данных Firestore . Принимая параметры uid, name и email, функция выполняет несколько критических проверок:
- Проверка существования: Перед созданием записи сервер обращается к коллекции
users, используяuidв качестве идентификатора документа . Если пользователь уже существует, процесс прерывается с уведомлением «User already exists» . - Обработка специфических ошибок: В блоке
catchАдриан настраивает проверку кода ошибкиauth/email-already-exists, чтобы возвращать пользователю понятное сообщение на понятном языке вместо системного стека ошибок . - Сохранение профиля: Если проверки пройдены, метод
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
- 60
- 24
- 7
- 1000
миллисекунд <a class="ts" data-seconds="6053" href="#t=6053" title="Смотреть с 1:40:53" aria-label="Смотреть с 1:40:53"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. Хадждин подчёркивает, что в коде важно избегать «магических чисел» (magic numbers), поэтому все расчеты выносятся в понятную переменнуюexpiresIn` .
При настройке куки-стора (cookie store) в Next.js устанавливаются строгие параметры безопасности:
- HTTP-only: устанавливается в значение
true, чтобы предотвратить доступ к куке через JavaScript на стороне клиента . - Secure: активируется только в режиме
production, гарантируя передачу данных исключительно по HTTPS . - SameSite: устанавливается в значение
Laxдля защиты от CSRF-атак .
Адриан объясняет архитектурный выбор в пользу серверной авторизации . Хотя Firebase позволяет управлять пользователями на клиенте, серверный подход в Next.js даёт три ключевых преимущества: невозможность кражи токенов через XSS, полную совместимость с Server Side Rendering (SSR) и упрощённое управление ролями пользователей непосредственно в API-маршрутах . «Вы доверяете серверу, потому что полностью контролируете его», — отмечает он .
Глубокая защита маршрутов и проверка состояния пользователя 1:51:02
Чтобы приложение не оставалось «открытой книгой» для неавторизованных посетителей, Адриан внедряет логику защиты приватных путей . Сердцем этой системы становится асинхронная функция getCurrentUser в файле серверных действий auth.actions.ts .
Процесс проверки пользователя разделен на несколько этапов:
- Извлечение куки: Приложение обращается к
cookies()изnext/headersи пытается найти сессионный токен . Если куки нет, функция мгновенно возвращаетnull. - Верификация через Admin SDK: Если кука найдена, она передаётся в метод
verifySessionCookieсерверного Firebase Admin SDK . Здесь проверяется не только валидность подписи, но и то, не была ли сессия отозвана ранее . - Синхронизация с базой данных: После успешного декодирования 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, которая активируется, когда ИИ-агент начинает говорить .
Особое внимание уделяется отображению транскрипта беседы в реальном времени. Адриан создает систему макетирования сообщений, где текст плавно появляется на экране . Основные элементы этого блока:
- Динамическая проверка длины массива сообщений для отображения блока транскрипта .
- Использование компонента
P(абзац) с уникальным ключом для каждого нового сообщения, что позволяет React корректно обновлять интерфейс . - Применение анимации
animate-fade-inи задержкиduration-500для того, чтобы текст не возникал мгновенно, а плавно проявлялся, имитируя живую речь .
После завершения верстки страницы Адриан делает коммит «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 в будущем, не переписывая основной код . В проект устанавливаются два ключевых пакета:
npm install ai— основной SDK от Vercel .@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», где разработчик вручную определяет переменные, которые ИИ должен извлечь из речи пользователя . Ключевыми параметрами становятся:
- Role: роль, на которую претендует кандидат (front-end, back-end и т.д.) .
- Type: тип интервью (техническое, поведенческое или смешанное) .
- Level: уровень опыта (Junior, Middle, Senior) .
- Tech Stack: список технологий, которые нужно покрыть .
- Amount: количество вопросов .
Адриан Хадждин (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-приложения :
- call-start: устанавливает статус активного вызова .
- message: фильтрует входящие данные и, если тип транскрипта помечен как
final, сохраняет сообщение в массивmessages, обновляя интерфейс . - speech-start / speech-end: управляют визуальной индикацией того, что бот в данный момент говорит .
- 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 имеет свои особенности:
- Для преобразования полученных документов в удобный массив объектов используется метод
.map(), который извлекаетdoc.idи распространяет (spread) остальные данные документа . - При попытке выполнить сложный запрос (одновременная фильтрация
whereи сортировкаorderBy), Firebase выбрасывает ошибку в консоль . - Решение этой проблемы требует создания композитных индексов в панели управления Firebase . Адриан подчеркивает, что разработчику достаточно просто перейти по ссылке из сообщения об ошибке, и Google автоматически предложит создать необходимый индекс .
Интеграция на главной странице (page.tsx) включает проверку наличия сессий через условие hasPastInterviews . Если у пользователя ещё нет созданных интервью, приложение корректно отображает заглушку, предлагая начать первую сессию .
Параллельное извлечение данных с Promise.all 3:02:44
Одной из центральных тем этой части туториала является оптимизация производительности при работе с несколькими независимыми запросами к БД. Помимо личных интервью пользователя, на главной странице должны отображаться «последние интервью», созданные другими участниками платформы . Для этого Адриан создает функцию getLatestInterviews, которая запрашивает завершенные сессии, где userId не совпадает с текущим пользователем .
Вместо последовательного выполнения запросов через await, которое заставило бы второй запрос ждать завершения первого, Адриан внедряет концепцию параллельных запросов :
- Проблема блокировки: Последовательные вызовы увеличивают время загрузки страницы, так как время ожидания суммируется .
- Решение через Promise.all: Метод принимает массив промисов и выполняет их одновременно .
- Эффект: Использование деструктуризации массива
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 .
Логика страницы интервью включает:
- Валидацию: Если интервью с указанным ID не найдено в базе данных, срабатывает серверный редирект обратно на главную страницу .
- Компонентную архитектуру: Для отображения карточки интервью на этой странице повторно используются ранее созданные компоненты, такие как
TechIconsдля вывода стека технологий (например, React, JavaScript, Next.js) . - Динамический UI: На странице отображается обложка интервью, сгенерированная случайным образом, роль (например, Backend Developer) и тип интервью (Behavioral) .
В завершение главы Адриан интегрирует компонент 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, полагаясь на мощь самой модели в интерпретации схемы .
Система оценивает кандидата по пяти фиксированным категориям :
- Коммуникативные навыки: насколько четко и ясно излагаются мысли .
- Технические знания: глубина владения стеком технологий .
- Решение проблем: логика подхода к сложным задачам .
- Культурное соответствие: soft skills и манера общения .
- Уверенность: общее впечатление от подачи ответов .
По каждой категории ИИ выставляет оценку от 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 для быстрой стилизации компонентов .
Интерфейс страницы отчетов строится по следующей иерархии:
- Заголовок и общая оценка: В верхней части страницы располагается крупный заголовок (H1) с общим впечатлением от интервью и датой его прохождения . Общий балл отображается как значение из 100 возможных .
- Форматирование дат: Для работы с временными метками используется библиотека Day.js, которая преобразует системную дату создания записи в читаемый формат .
- Детализация по категориям: Приложение итерирует (mapping) по массиву категорий фидбека, выводя для каждой из них отдельный балл и развернутый комментарий ИИ .
- Сильные и слабые стороны: Отдельно рендерятся списки преимуществ кандидата и зон для его профессионального роста .
- Управляющие элементы: В нижней части страницы размещаются две критически важные кнопки — «Retake Interview» (пересдать) и «Back to Dashboard» (вернуться в панель управления) .
Адриан Хадждин рекомендует просматривать эту страницу на десктопных экранах, так как большой объем аналитических данных требует значительного экранного пространства для комфортного чтения .
Интеграция обратной связи в дашборд 3:48:12
Важным шагом является обновление главной страницы приложения. До этого момента на карточках интервью отображалось заглушка «Вы еще не проходили это интервью» . Чтобы сделать интерфейс динамичным, необходимо обновить компонент InterviewCard .
Логика обновления заключается в проверке наличия существующего фидбека в базе данных. Для этого используется асинхронная функция getFeedbackByInterviewId, в которую передаются userId и interviewId . Если данные найдены, вместо стандартного приглашения пройти опрос, карточка отображает реальный результат — например, «15 из 100» с кратким вердиктом о том, что кандидат справился плохо .
Также на карточке появляется кнопка «Check feedback», которая перенаправляет пользователя на страницу с детальным отчетом . В качестве второго самостоятельного упражнения Адриан предлагает зрителям реализовать логику кнопки пересдачи интервью, которая позволит сбросить текущие результаты и запустить процесс заново .
Финальный деплой на Vercel и проверка системы 3:49:51
Когда UI-часть полностью готова, наступает момент финального развертывания. Поскольку проект изначально настраивался с учетом интеграции с Vercel, процесс публикации сводится к стандартному циклу Git-команд . После выполнения git push изменения автоматически подхватываются платформой .
Процесс сборки (build) обновленной версии занимает около 45 секунд, после чего приложение становится доступно по публичному URL . Адриан Хадждин проводит финальное тестирование в «боевых» условиях:
- Авторизация: Вход в систему выполняется через ранее созданную учетную запись, так как приложение использует общую базу данных Firestore для разработки и продакшена .
- Проверка данных: После входа в дашборде корректно отображаются все созданные интервью и статусы их прохождения .
- Голосовой агент: Тестируется работа ИИ-ассистента в облаке. Адриан проверяет, насколько быстро агент реагирует на голос и корректно ли запускаются рабочие процессы (workflows) .
- Стабильность: Подтверждается, что развернутая версия работает так же безупречно, как и локальная среда разработки .
Итоги курса и экосистема Vapi 3:51:12
Завершая туториал, Адриан Хадждин подводит итоги проделанной работы. Проект объединил в себе мощь Firebase для управления бэкендом (аутентификация и хранилище) и инновационные возможности Vapi для создания голосовых интерфейсов . Ключевой особенностью стало использование человекоподобных голосов и интеграция сложных ИИ-сценариев, где агент не просто задает вопросы, но и собирает данные для последующего глубокого анализа .
Адриан благодарит команду Vapi за предоставление инструментов, которые позволяют разработчикам бесшовно внедрять голосовой ИИ в веб-приложения . Для тех, кто стремится к более глубокому изучению Full Stack разработки, он рекомендует платформу JS Mastery Pro, где представлены оптимизированные программы обучения, помогающие быстрее получить работу или повышение в карьере . Проект PrepWise служит отличным примером того, как современные API могут превратить стандартное Next.js приложение в сложный интеллектуальный продукт .