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

Источник: https://www.youtube.com/watch?v=8GK8R77Bd7g
Канал: JavaScript Mastery
Опубликовано: 21.03.2025

---

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

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

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

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

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

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

*   **Голосовое взаимодействие:** ИИ-агент, работающий на базе Vapi, приветствует кандидата и задает вопросы естественным голосом [3:40].
*   **Динамическая адаптация:** Бот не просто читает список вопросов, а реагирует на ответы. Когда Адриан пытается уйти от прямого ответа, рекламируя свою платформу JS Mastery Pro, ИИ замечает это и просит вернуться к техническим деталям [4:19].
*   **Режим реального времени:** Благодаря задержке менее 500 миллисекунд, общение ощущается как разговор с живым человеком [8:20].
*   **Глубокая аналитика:** После завершения сессии платформа генерирует подробный отчет. В демо-версии Адриан намеренно «проваливает» интервью, за что получает низкие баллы за коммуникацию и решение проблем, а также конкретные советы по улучшению ответов [6:11].

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

### Инициализация проекта и настройка окружения
[[JUMP:09:25]]

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

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

*   **TypeScript:** для обеспечения типизации и чистоты кода.
*   **ESLint:** для поддержания стандартов разработки.
*   **Tailwind CSS:** для стилизации интерфейса.
*   **App Router:** современный стандарт маршрутизации Next.js.
*   **Turbopack:** для ускорения сборки в режиме разработки.

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

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

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

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

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

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

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

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

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

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

[[JUMP:25:44]]

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

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

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

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

*   Логотип приложения `logo.svg` и текстовое название проекта PrepWise [31:03];
*   Заголовок H3 с призывом к действию («Practice job interviews with AI») [32:26];
*   Динамическую ссылку под основной формой, позволяющую пользователю переключаться между экранами входа и регистрации с помощью компонента `Link` из `next/link` [36:32].

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

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

[[JUMP:37:40]]

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

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

*   `email`: проверка на соответствие формату адреса электронной почты через встроенный метод `.email()` [38:37];
*   `password`: требование минимальной длины (от 3 символов на этапе разработки с возможностью усиления в будущем) [38:43].

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

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

[[JUMP:41:56]]

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

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

*   `control`: объект управления от React Hook Form [43:53];
*   `name`: имя поля, соответствующее ключу в Zod-схеме [43:57];
*   `label`: текстовая метка для пользователя [44:03];
*   `placeholder` и `type`: настройки отображения и типа вводимых данных (text, email или password) [44:09].

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

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

[[JUMP:50:05]]

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

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

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

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

*   Секцию с кнопкой «Start an interview», которая использует компонент `Button` из Shadcn UI с дочерней ссылкой `next/link` [55:04].
*   Декоративное изображение робота, которое автоматически скрывается на мобильных устройствах для экономии экранного пространства [56:02].
*   Два основных подраздела: «Your interviews» (пройденные тесты) и «Take an interview» (доступные варианты) [57:28].

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

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

Для работы с временными метками в проект добавляется библиотека Day.js [58:47]. Она позволяет преобразовывать технические даты создания записей в человекочитаемый формат, например, «March 15, 2024» [1:03:30]. Внутри `InterviewCard` Адриан Хадждин применяет деструктуризацию пропсов, извлекая роль, стек технологий и дату создания интервью [1:01:24]. 

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

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

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

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

*   Ввод «React.js», «ReactJS» или просто «React» будет корректно сопоставлен с официальным логотипом React [1:14:01].
*   Утилита обращается к заранее подготовленному объекту `mappings`, где зафиксированы пути к иконкам TypeScript, Next.js, Tailwind CSS и других технологий [1:07:25].

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

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

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

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

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

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

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

*   Ключ импортируется из `.env.local` [1:23:38].
*   Строка ключа должна быть очищена от некорректных символов переноса строки с помощью метода `.replace(/\\n/g, '\n')`, иначе аутентификация на сервере завершится ошибкой [1:27:14].
*   Admin SDK предоставляет полные привилегии для чтения и записи в Firestore, минуя правила безопасности, настроенные для клиентов [1:27:40].

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

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

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

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

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

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

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

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

[[JUMP:1:40:13]]

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

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

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

*   60
*   24
*   7
*   1000` миллисекунд [1:40:53]. Хадждин подчёркивает, что в коде важно избегать «магических чисел» (magic numbers), поэтому все расчеты выносятся в понятную переменную `expiresIn` [1:41:31].

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

*   **HTTP-only**: устанавливается в значение `true`, чтобы предотвратить доступ к куке через JavaScript на стороне клиента [1:41:44].
*   **Secure**: активируется только в режиме `production`, гарантируя передачу данных исключительно по HTTPS [1:41:59].
*   **SameSite**: устанавливается в значение `Lax` для защиты от CSRF-атак [1:41:59].

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

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

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

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

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

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

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

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

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

После настройки защиты Адриан проводит финальный аудит конфигурации, напоминая о важности чистоты `.env` файлов — лишние запятые или пробелы в ключах Firebase могут привести к трудноотловимым ошибкам авторизации [1:48:12]. Убедившись в корректности работы редиректов и создании записей в Firebase Console [1:50:37], он приступает к подготовке интерфейса для генерации интервью, который станет доступен только авторизованным пользователям [1:58:26]. Для этого создается новый маршрут `/interview` и базовый компонент `Agent`, который в будущем будет отвечать за голосовое взаимодействие [1:59:21].

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

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

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

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

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

*   Динамическая проверка длины массива сообщений для отображения блока транскрипта [2:09:17].
*   Использование компонента `P` (абзац) с уникальным ключом для каждого нового сообщения, что позволяет React корректно обновлять интерфейс [2:09:43].
*   Применение анимации `animate-fade-in` и задержки `duration-500` для того, чтобы текст не возникал мгновенно, а плавно проявлялся, имитируя живую речь [2:10:22].

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

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

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

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

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

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

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

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

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

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

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

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

*   Необходимость составить вопросы для конкретной роли и уровня опыта [2:22:53].
*   Акцент на поведенческих или технических аспектах [2:23:08].
*   Строгое требование возвращать только вопросы без лишнего текста, так как их будет зачитывать голосовой помощник [2:23:08].
*   Запрет на использование спецсимволов, которые могут «сломать» синтез речи [2:23:20].

После того как Gemini возвращает текст, вопросы парсятся в массив с помощью `JSON.parse` [2:24:23]. Затем создается объект интервью, который включает в себя очищенный стек технологий (через `.split(',')`), сгенерированные вопросы, ID пользователя и случайную обложку [2:24:50]. Эти данные сохраняются в коллекцию `interviews` базы данных Firestore [2:25:31]. Проверка через HTTP-клиент показывает, что в базе успешно создается документ с полным списком вопросов, готовых к использованию голосовым агентом [2:26:56].

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

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

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

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

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

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

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

*   **Role**: роль, на которую претендует кандидат (front-end, back-end и т.д.) [2:32:05].
*   **Type**: тип интервью (техническое, поведенческое или смешанное) [2:33:01].
*   **Level**: уровень опыта (Junior, Middle, Senior) [2:33:15].
*   **Tech Stack**: список технологий, которые нужно покрыть [2:33:29].
*   **Amount**: количество вопросов [2:33:42].

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

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

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

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

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

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

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

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

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

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

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

[[JUMP:2:55:34]]

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

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

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

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

*   Для преобразования полученных документов в удобный массив объектов используется метод `.map()`, который извлекает `doc.id` и распространяет (`spread`) остальные данные документа [2:59:03].
*   При попытке выполнить сложный запрос (одновременная фильтрация `where` и сортировка `orderBy`), Firebase выбрасывает ошибку в консоль [3:01:49].
*   Решение этой проблемы требует создания композитных индексов в панели управления Firebase [3:02:03]. Адриан подчеркивает, что разработчику достаточно просто перейти по ссылке из сообщения об ошибке, и Google автоматически предложит создать необходимый индекс [3:02:18].

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

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

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

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

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

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

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

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

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

*   **Валидацию:** Если интервью с указанным ID не найдено в базе данных, срабатывает серверный редирект обратно на главную страницу [3:14:26].
*   **Компонентную архитектуру:** Для отображения карточки интервью на этой странице повторно используются ранее созданные компоненты, такие как `TechIcons` для вывода стека технологий (например, React, JavaScript, Next.js) [3:18:00].
*   **Динамический UI:** На странице отображается обложка интервью, сгенерированная случайным образом, роль (например, Backend Developer) и тип интервью (Behavioral) [3:16:52].

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

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

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

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

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

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

*   Использование заранее сгенерированного списка вопросов, которые передаются агенту в виде форматированного списка с маркерами [3:24:44].
*   Естественное реагирование на ответы кандидата: ИИ не должен просто зачитывать вопросы, он обязан проявлять гибкость и профессиональную вежливость [3:27:21].
*   Соблюдение баланса между официальностью и дружелюбием, использование коротких и понятных формулировок [3:27:35].

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

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

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

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

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

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

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

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

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

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

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

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

[[JUMP:3:45:55]]

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

### Визуализация аналитики и вёрстка страницы отзывов

[[JUMP:3:46:36]]

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

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

*   **Заголовок и общая оценка:** В верхней части страницы располагается крупный заголовок (H1) с общим впечатлением от интервью и датой его прохождения [3:47:17]. Общий балл отображается как значение из 100 возможных [3:47:17].
*   **Форматирование дат:** Для работы с временными метками используется библиотека Day.js, которая преобразует системную дату создания записи в читаемый формат [3:47:17].
*   **Детализация по категориям:** Приложение итерирует (mapping) по массиву категорий фидбека, выводя для каждой из них отдельный балл и развернутый комментарий ИИ [3:47:31].
*   **Сильные и слабые стороны:** Отдельно рендерятся списки преимуществ кандидата и зон для его профессионального роста [3:47:45].
*   **Управляющие элементы:** В нижней части страницы размещаются две критически важные кнопки — «Retake Interview» (пересдать) и «Back to Dashboard» (вернуться в панель управления) [3:47:45].

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

### Интеграция обратной связи в дашборд

[[JUMP:3:48:12]]

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

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

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

### Финальный деплой на Vercel и проверка системы

[[JUMP:3:49:51]]

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

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

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

### Итоги курса и экосистема Vapi

[[JUMP:3:51:12]]

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

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