v0-german: генератор документов для воронки продаж

Представь себе такую воронку: потенциальный клиент приходит через лид-магниты, проходит трёхдневный прогрев, потом попадает на диагностику с бизнес-консультантом, и только после этого — на предложение войти в программу за несколько сотен тысяч рублей. Каждый шаг этой воронки требует персонализированных документов: подробного отчёта о диагностике, коммерческого предложения под конкретный тариф, дорожной карты на несколько месяцев вперёд. Раньше всё это делалось вручную — часами. Мы это автоматизировали.
Проект v0-german — это внутренний инструмент для команды Германа Юна, бизнес-консультанта, который работает с предпринимателями над систематизацией их компаний. Проект живёт на germanyun.online и уже включал в себя CJM-модуль (Customer Journey Map) для визуализации воронок, транскрибацию аудио через Whisper и интеграцию с Supabase. Новый модуль — Generator — стал логичным продолжением: взять транскрипцию диагностического звонка и автоматически сгенерировать три PDF-документа, готовых к отправке клиенту.
Зачем это вообще нужно
Диагностика с клиентом — это разговор на 1-2 часа. После него консультант должен подготовить:
- Отчёт «Точка А» — подробное описание текущей ситуации в бизнесе клиента: отдел продаж, найм, дебиторка, управление, ключевые проблемы и их корневые причины.
- Коммерческое предложение — персонализированный оффер с выбранным тарифом (три варианта: 250К, 750К и 1М рублей), программой и условиями.
- Дорожная карта — помесячный план работы под конкретный запрос клиента.
Без автоматизации это занимает несколько часов работы. С генератором — несколько минут: загрузил аудио или вставил текст транскрипции, заполнил пару полей, нажал кнопку — и три PDF готовы к отправке.
Архитектура: пошаговый визард
Мы приняли решение не делать одну большую форму, а разбить процесс на четыре чётких шага. Это снижает когнитивную нагрузку на менеджера и позволяет валидировать данные на каждом этапе.
Шаг 1 — Данные клиента. Имя, описание бизнеса, локация, дата встречи, дата следующего касания с Германом.
Шаг 2 — Транскрипция. Два режима: загрузка аудиофайла (обрабатывается через Whisper API) или прямая вставка текста транскрипции. Переиспользовали логику из существующего /api/otchet роута.
Шаг 3 — Выбор тарифа. Три карточки с подробным описанием каждого тарифа, что включено, цена. Менеджер выбирает то, что обсуждалось на диагностике.
Шаг 4 — Генерация документов. Три карточки документов с кнопками «Сгенерировать» и «Скачать PDF». Плюс кнопки «Сгенерировать все» и «Скачать все PDF» для удобства.
Структура файлов получилась такая:
app/generator/
page.tsx — Страница-визард (клиентский компонент)
layout.tsx — Layout с password-gate
app/api/generator/
transcribe/route.ts — Whisper транскрибация
report/route.ts — Генерация отчёта через GPT-4o
kp/route.ts — Генерация КП
roadmap/route.ts — Генерация дорожной карты
pdf/route.ts — Рендеринг PDF через @react-pdf/renderer
components/generator/
client-form.tsx — Шаг 1: данные клиента
transcription-step.tsx — Шаг 2: транскрипция
tariff-select.tsx — Шаг 3: выбор тарифа
documents-step.tsx — Шаг 4: генерация и скачивание
Выбор стека для PDF
Один из ключевых технических вопросов — как генерировать красивые PDF. У нас было три варианта:
@react-pdf/renderer — настоящие векторные PDF с кастомными шрифтами, работает в API routes Next.js, лёгкий (~5MB). Минус — свой синтаксис, не Tailwind, нужно переписывать layout.
Puppeteer — pixel-perfect из HTML, Tailwind работает как в браузере. Но ~400MB Chromium в зависимостях, медленный cold start, проблемы в Docker Alpine-контейнерах.
window.print() — самый простой вариант, работает мгновенно, но результат непредсказуем и зависит от браузера пользователя.
Мы выбрали @react-pdf/renderer — он даёт предсказуемый результат в серверном окружении, не раздувает образ Docker и позволяет точно контролировать оформление. Да, пришлось написать отдельные layout-компоненты для PDF, но это разовая работа.
Брендинг взяли с germanyun.online: бордовый #9b2c2c как основной цвет, золотой акцент #c9a959, тёплый белый фон, шрифты Inter + Noto Serif JP. Японская эстетика — строго, благородно, дорого.
AI-промпты: структура важнее красоты
Для генерации отчёта использовали GPT-4o с детальным системным промптом. Ключевой принцип — жёсткая структура вывода, которую можно парсить и рендерить в PDF:
// Структура промпта для отчёта «Точка А»
const REPORT_SYSTEM_PROMPT = `
Ты — эксперт по диагностике бизнесов, обученный в школе Германа Юна.
На основе текста транскрипции составь отчет строго по структуре:
## Точка А
### Отдел продаж
[анализ]
### Найм и HR
[анализ]
### Финансы и дебиторка
[анализ]
### Управление и процессы
[анализ]
## Ключевые выводы
### Корневые причины
[список]
### Ограничения роста
[список]
### Особенности мышления собственника
[анализ]
`;Для КП и дорожной карты промпты учитывают выбранный тариф — разный объём программы, разные акценты, разные сроки.
Отдельно пришлось поработать над промптами для слайдов (проект параллельно включает PowerPoint-like презентацию из 15 слайдов). Несколько итераций потребовало слайд 03 — список симптомов и ограничений: изначально AI давал 2-3 пункта, а нам нужно 5-7. Решили явным требованием в промпте: require minimum 5 symptoms and 3 current_constraints from AI. Помогло.
CJM-модуль: аудит и рефакторинг
Параллельно с разработкой генератора мы провели глубокий аудит CJM-модуля — визуального редактора Customer Journey Map на базе React Flow. Этот модуль уже был в продакшене, но работал нестабильно: баги в отображении, потери данных, неправильная логика каскадных расчётов.
Автоматизированный анализ 50+ компонентов и 15 API-роутов выявил около 40 проблем разной степени критичности. Самые серьёзные:
SQL Injection — UUID'ы вставлялись через string interpolation в запросах типа .not("id", "in", ...). В Supabase это безопасно благодаря PostgREST, но паттерн всё равно плохой и потенциально опасный при изменении ORM.
Потеря данных при сохранении — поле weight для рёбер графа не передавалось в API при сохранении, из-за чего веса связей между этапами воронки сбрасывались.
Были и UX-проблемы: несинхронизированное состояние между вкладками «Конструктор», «Таблица» и «Дашборд», race conditions при быстром переключении, некорректный рендеринг на мобильных.
Решение: поэтапный рефакторинг с планом из 11 задач. К моменту написания этой статьи 9 из 11 задач завершены.
Password Gate: защита внутренних страниц
И генератор, и CJM — внутренние инструменты, не для публичного доступа. Поэтому обе страницы закрыты простым password gate: при первом заходе показывается форма с полем пароля, при успехе — токен сохраняется в localStorage, и следующие заходы не требуют повторного ввода.
Паттерн реализован через общий компонент PasswordGate с параметрами title, endpoint и storageKey. Для генератора добавили отдельный env var и соответствующий API endpoint — /api/generator/auth.
// layout.tsx для /generator
import { PasswordGate } from '@/components/password-gate';
export default function GeneratorLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<PasswordGate
title="Generator — Команда Германа Юна"
endpoint="/api/generator/auth"
storageKey="generator_auth"
>
{children}
</PasswordGate>
);
}Просто, надёжно, не перегружено. Для внутреннего инструмента этого достаточно.
Деплой и CI
Проект деплоится через стандартный пайплайн на VPS: git push → автоматический npm run build → pm2 restart. В git-истории видно регулярные auto-sync коммиты каждые 15 минут — это фоновый процесс, который сохраняет промежуточное состояние файлов.
Build перед финальным мёржем прошёл успешно: все роуты на месте, TypeScript без ошибок, @react-pdf/renderer совместим с App Router через серверные компоненты.
Отдельный момент — работа с Supabase: тарифы хранятся в БД и подгружаются динамически. Это позволяет менеджерам обновлять условия тарифов без деплоя кода. Была баг: при передаче tariffId из формы приходил не UUID, а строковое название тарифа. Фикс — добавить предварительную выборку всех тарифов и резолвить по обоим полям.
// До фикса
const tariff = await supabase
.from('tariffs')
.select('*')
.eq('id', tariffId) // падало если tariffId не UUID
.single();
// После фикса
const { data: tariffs } = await supabase
.from('tariffs')
.select('*');
const tariff = tariffs?.find(
t => t.id === tariffId || t.name === tariffId
);Результат
Генератор документов полностью функционален: четырёхшаговый визард, транскрибация аудио через OpenAI Whisper, генерация трёх типов документов через GPT-4o, экспорт в PDF с фирменным брендингом.
Время подготовки документов после диагностики: с нескольких часов до 5-7 минут. Менеджер загружает запись звонка, выбирает тариф, нажимает «Сгенерировать все» — и три персонализированных PDF готовы к отправке клиенту.
CJM-модуль прошёл аудит и рефакторинг: критические баги с потерей данных устранены, логика каскадных расчётов выправлена, UX на трёх вкладках синхронизирован.
Выводы и уроки
Первый важный урок: не нужно изобретать велосипед там, где уже есть рабочий паттерн. Когда появилась идея использовать AFFiNE (мощный open-source workspace) для решения задач проекта, мы трезво оценили ситуацию: AFFiNE — это general-purpose инструмент, он не умеет воронки продаж, каскадные расчёты и генерацию персонализированных PDF. Пришлось бы писать всё заново внутри чужой платформы. Правильное решение — дорабатывать то, что уже есть и работает.
Второй урок: аудит перед фичами. Когда пользователь говорит «инструмент работает с багами и неправильной логикой», первый шаг — не добавлять новые возможности, а найти и зафиксировать все существующие проблемы. Систематический аудит 50+ файлов дал конкретный список из 40 проблем с приоритетами — это несравнимо лучше, чем хаотичное «починить то, что сломалось».
Третий урок: переполнение контекста — нормально, главное — восстановление. В процессе длинных сессий разработки контекстное окно переполнялось, и работу приходилось начинать заново. Решение — сохранять план в файл (PLAN.md), вести KNOWLEDGE.md с описанием архитектуры, делать атомарные коммиты с понятными сообщениями. Когда сессия оборвалась на Task 10 из 11, мы восстановили контекст за пару минут по git log и файлу плана.
Четвёртый урок: промпты нужно тестировать итеративно. Казалось бы, написал промпт — и готово. Но AI генерировал то 2 симптома вместо 5, то неправильный заголовок слайда, то смешивал данные из разных секций. Каждый слайд в презентационном модуле потребовал 2-4 итерации промпта с явными ограничениями (minimum 5 symptoms, use lead_text from financial_risk, not executive_summary). Это нормальная часть разработки AI-фич, не стоит недооценивать этот этап при планировании.
Проект продолжает развиваться: следующие шаги — автоматическая отправка документов клиенту через email или Telegram, версионирование сгенерированных отчётов и аналитика по тарифам.
Технологии в проекте: Next.js 14 App Router, React Flow, @react-pdf/renderer, OpenAI API, Supabase, TypeScript, Tailwind CSS.

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.
Связанный проект
CJM Designer →