ИИ-технологФлагман NP
ИИ-технолог

Промпты для верстальщика. 25 готовых примеров для генерации

От NP_Article

ИИ-технолог

Обучение управлению ИИ с 0 до мастерского уровня

250+ уроков Начато наполнение Без инфоцыган и воды

Многие специалисты ищут качественные промпты для верстальщика, но в итоге находят лишь поверхностные списки, которые не работают на реальных задачах. Результат – разочарование в технологии и потерянное время. Кажется, что нейросеть просто не способна генерировать хороший код.

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

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

  • Создание чистой HTML-разметки по методологии БЭМ.
  • Написание современных и адаптивных CSS-стилей с использованием Flexbox и Grid.
  • Генерация JavaScript-кода для интерактивных элементов интерфейса.
  • Оптимизация, исправление и документирование существующего кода.

Материал построен так, чтобы вы не просто скопировали текст, а поняли логику составления эффективных запросов. Это поможет в будущем самостоятельно формулировать команды для любых, даже самых сложных задач верстки.

Типичные ошибки при составлении запросов к ИИ

Прежде чем переходить к готовым решениям, важно понять, какие ошибки чаще всего допускают специалисты. Избегая этих ловушек, вы сразу повысите качество генерируемого кода в несколько раз. Многие наступают на эти грабли, теряя часы на исправление того, что можно было сразу сделать правильно.

  1. Использовать абстрактные команды вроде “сделай красиво” или “улучши дизайн”. Нейросеть – это исполнитель кода, а не дизайнер с врожденным чувством стиля. Она не понимает субъективных оценок и выдаст клишированный или совершенно неподходящий результат.
  2. Забывать о контексте всего проекта. Если попросить сверстать кнопку, не указав, где она будет использоваться (в шапке, в форме, в карточке товара), вы получите универсальный, но безликий элемент, который придется долго дорабатывать напильником.
  3. Слепо копировать сгенерированный код, не проводя проверку. ИИ может допускать неочевидные ошибки, использовать устаревшие подходы или создавать уязвимости. Любой код, особенно JavaScript, требует внимательного код-ревью перед добавлением в проект.
  4. Просить написать код, игнорируя принятые в проекте методологии и стандарты. Если вы используете БЭМ, а в промпте это не указали, нейросеть сгенерирует набор классов вразнобой, нарушая всю структуру и консистентность проекта. Конечно, если ваша цель – хаос в стилях, то это отличный способ.
  5. Пытаться сгенерировать всю страницу или весь сайт одним запросом. Это верный путь к провалу. Модель плохо справляется с огромными задачами, ее ответ будет неполным, запутанным и полным ошибок. Работайте итерационно, по одному компоненту за раз.
  6. Не указывать технические ограничения и требования к совместимости. Без указания “использовать только ES6 синтаксис” или “обеспечить совместимость с Safari 14” вы рискуете получить код, который просто не будет работать у части ваших пользователей.

Сравнение формулировок: от слабого запроса к сильному результату

Разница между бесполезным и рабочим кодом часто кроется в деталях формулировки. В этой таблице наглядно показано, как превратить расплывчатую просьбу в точную техническую задачу, которая принесет ожидаемый результат. Обратите внимание, как добавление контекста, ограничений и формата меняет итог.

Задача Слабый промпт (частая ошибка) Сильный промпт (как надо)
Создать кнопку Напиши HTML и CSS для кнопки. Создай HTML и CSS для основной кнопки (call-to-action). Используй тег button. Стиль: синий фон #007bff, белый текст, скругление углов 8px, внутренние отступы 12px 24px. При наведении курсора фон должен становиться темнее (#0056b3). Не использовать сторонние библиотеки. Код должен быть в одном блоке.
Сверстать карточку товара Сделай карточку товара с картинкой, названием и ценой. Ты – опытный верстальщик. Создай HTML и CSS для карточки товара. Используй семантические теги. Структура: изображение сверху, под ним заголовок h3, абзац с коротким описанием и цена. Цена должна быть выделена жирным. Внизу – кнопка “Добавить в корзину”. Используй Flexbox для выравнивания. Карточка должна иметь тень и скругленные углы. Ширина карточки – 300px.
Сделать адаптивное меню Сверстай навигационное меню для сайта. Создай HTML, CSS и JS для адаптивного навигационного меню. На экранах шире 768px меню должно быть горизонтальным. На экранах уже 768px – скрываться под иконкой “бургер”. При клике на “бургер” меню плавно выезжает сбоку. Используй CSS-переходы для анимации. JavaScript должен быть написан на чистом ES6, без jQuery.
Написать стили для сетки Нужна сетка из 3 колонок. Напиши CSS-код для создания адаптивной сетки с помощью CSS Grid. На десктопах (от 1024px) должно быть 3 колонки одинаковой ширины. На планшетах (от 768px до 1023px) – 2 колонки. На мобильных устройствах (до 767px) – 1 колонка. Расстояние между элементами сетки (gap) – 20px.

Рабочие промпты для верстальщика: 25 готовых примеров

Ниже представлены 25 детализированных промптов для решения повседневных задач верстальщика. Каждый шаблон – это результат отсева неудачных формулировок и поиска оптимальной структуры. Используйте их как основу, подставляя свои данные в квадратные скобки.

1. Генерация HTML-структуры компонента по БЭМ

Проблема: Нужно быстро создать HTML-каркас для нового компонента, соблюдая строгую структуру именования классов по методологии БЭМ, чтобы избежать хаоса в проекте.

Решение: Промпт генерирует чистую и валидную HTML-разметку с классами, полностью соответствующими БЭМ-нотации.

Текст промпта:

*****

Ты – эксперт по HTML-верстке и методологии БЭМ. Твоя задача – создать HTML-структуру для компонента “[укажите название компонента, например, ‘карточка профиля пользователя’]”.
Название блока: “[укажите название блока, например, ‘profile-card’]”.
Компонент должен включать следующие элементы:
[перечислите элементы компонента, например:
– Аватар пользователя (изображение)
– Имя пользователя (заголовок)
– Короткая биография (абзац)
– Кнопка “Подписаться” (кнопка)] Сгенерируй только HTML-код. Используй теги с максимальной семантической точностью. Все классы должны строго соответствовать БЭМ-нотации (блок__элемент, блок–модификатор). Не добавляй CSS или JavaScript.

*****

Почему это работает: Четкое указание роли (“эксперт по БЭМ”), названия блока и перечня элементов не оставляет нейросети пространства для импровизации. Требование строго следовать нотации гарантирует правильное именование классов.

2. Создание CSS-стилей для Flexbox-контейнера

Проблема: Необходимо быстро стилизовать блок с помощью Flexbox, но помнить все свойства и их значения бывает утомительно.

Решение: Промпт генерирует готовый CSS-код для Flexbox-контейнера с нужными параметрами выравнивания, распределения и переноса элементов.

Текст промпта:

*****

Ты – frontend-разработчик, специалист по CSS Flexbox. Напиши CSS-код для класса “[укажите имя класса, например, ‘.card-container’]”.
Этот класс должен определять Flexbox-контейнер со следующими свойствами:
– Направление основной оси: [укажите направление, например, “горизонтальное (row)”] – Выравнивание элементов по основной оси: [укажите свойство justify-content, например, “пространство между элементами (space-between)”] – Выравнивание элементов по поперечной оси: [укажите свойство align-items, например, “по центру (center)”] – Перенос элементов: [укажите, нужен ли перенос, например, “разрешить перенос на новую строку (wrap)”] Сгенерируй только CSS-код. Добавь короткие комментарии к каждому свойству, объясняющие его назначение.

*****

Почему это работает: Запрос оперирует конкретными свойствами Flexbox (justify-content, align-items). Указание роли и просьба добавить комментарии заставляют модель выдать не просто код, а хорошо документированный и понятный фрагмент стилей.

3. Написание стилей для CSS Grid-раскладки

Проблема: Создание сложной сеточной раскладки с помощью CSS Grid требует написания большого количества кода, особенно для адаптивности.

Решение: Промпт позволяет быстро сгенерировать CSS для сложной Grid-сетки, включая определение колонок, рядов и областей.

Текст промпта:

*****

Ты – эксперт по CSS Grid Layout. Создай CSS для контейнера с классом “[укажите класс, например, ‘.page-layout’]”.
Сетка должна иметь следующую структуру:
– Колонки: [опишите колонки, например, “три колонки: первая и третья по 1fr, центральная – 3fr”] – Ряды: [опишите ряды, например, “два ряда: верхний ‘auto’, нижний ‘1fr'”] – Области (grid-template-areas): [опишите области, например, “‘header header header’, ‘sidebar content content’, ‘footer footer footer'”] – Расстояние между ячейками (gap): “[укажите значение, например, ’15px’]”.
Напиши только CSS-код для родительского контейнера. Предоставь также примеры CSS-правил для дочерних элементов, чтобы разместить их в областях (header, sidebar, content, footer).

*****

Почему это работает: Промпт использует специфическую терминологию CSS Grid (grid-template-areas, fr, gap), что направляет модель на генерацию точного кода. Просьба предоставить примеры для дочерних элементов делает ответ исчерпывающим и готовым к использованию.

4. Создание адаптивной сетки для страницы

Проблема: Нужно создать CSS-код для сетки элементов (например, карточек товаров), которая корректно отображается на всех типах устройств.

Решение: Этот запрос генерирует CSS с медиазапросами, которые изменяют количество колонок в сетке в зависимости от ширины экрана.

Текст промпта:

*****

Ты – верстальщик, специализирующийся на адаптивной верстке. Напиши CSS-код для создания адаптивной сетки элементов.
Используй [выберите технологию, например, “CSS Grid” или “Flexbox”].
Селектор контейнера: “[укажите селектор, например, ‘.product-grid’]”.
Поведение сетки:
– На экранах шире 1200px: [укажите количество колонок, например, “4 колонки”] – На экранах от 992px до 1199px: [укажите количество колонок, например, “3 колонки”] – На экранах от 768px до 991px: [укажите количество колонок, например, “2 колонки”] – На экранах уже 767px: [укажите количество колонок, например, “1 колонка”] Расстояние между элементами сетки: “[укажите значение, например, ’24px’]”.
Сгенерируй полный CSS-код с медиазапросами (media queries).

*****

Почему это работает: Запрос четко определяет “точки останова” (breakpoints) и желаемое поведение на каждом из них. Это позволяет ИИ сгенерировать точные медиазапросы, избавляя от рутинной работы по их написанию.

5. Генерация JS для открытия/закрытия модального окна

Проблема: Написание JavaScript для управления видимостью модального окна – частая, но требующая внимания задача (обработка кликов, закрытие по Esc и т.д.).

Решение: Промпт генерирует готовый, чистый JavaScript-код для полного цикла работы модального окна.

Текст промпта:

*****

Ты – JavaScript-разработчик. Напиши код на чистом JavaScript (ES6+) для управления модальным окном.
HTML-структура:
– Кнопка открытия с id: “[укажите id, например, ‘openModalBtn’]”
– Модальное окно с id: “[укажите id, например, ‘myModal’]”
– Кнопка закрытия внутри окна с классом: “[укажите класс, например, ‘.close-button’]”
Требуемая функциональность:
1. При клике на кнопку открытия модальное окно должно появляться (добавлять класс ‘is-open’).
2. При клике на кнопку закрытия или на фон вне окна, оно должно скрываться (убирать класс ‘is-open’).
3. При нажатии клавиши ‘Escape’ окно также должно скрываться.
Сгенерируй только JavaScript-код. Код должен быть хорошо прокомментирован.

*****

Почему это работает: Промпт не просто просит “сделать модалку”, а предоставляет точные селекторы (id, классы) и перечисляет все необходимые триггеры (клик по кнопке, клик вне, клавиша Esc). Это гарантирует полнофункциональный и продуманный скрипт.

6. Рефакторинг “грязного” CSS-кода

Проблема: Имеется фрагмент CSS с重复рующимися стилями, длинными селекторами и магическими числами, который сложно поддерживать.

Решение: Этот промпт анализирует предоставленный CSS и предлагает оптимизированную версию, следуя лучшим практикам.

Текст промпта:

*****

Ты – опытный frontend-архитектор, специалист по рефакторингу CSS. Проанализируй следующий CSS-код и предложи его улучшенную версию.
Цели рефакторинга:
– Уменьшить вложенность селекторов.
– Сгруппировать общие свойства для похожих элементов.
– Заменить “магические числа” (например, `margin-left: 13.5px;`) на более универсальные значения или переменные.
– Улучшить читаемость и структуру кода.
Объясни, какие именно изменения ты внес и почему они делают код лучше.
Исходный CSS-код для рефакторинга:
“`css
[вставьте сюда ваш “грязный” CSS-код] “`
Формат ответа: сначала исправленный CSS, затем список внесенных улучшений с объяснениями.

*****

Почему это работает: Роль “архитектора” и четко поставленные цели (уменьшить вложенность, сгруппировать) направляют ИИ на глубокий анализ, а не поверхностное форматирование. Требование объяснить изменения превращает ответ в обучающий материал.

7. Поиск и исправление ошибок в JavaScript

Проблема: Скрипт не работает, как ожидалось, но ошибка не очевидна.

Решение: Промпт дает ИИ задачу выступить в роли отладчика: найти логические или синтаксические ошибки в коде и предложить исправление.

Текст промпта:

*****

Ты – опытный JavaScript-разработчик и отладчик. Проанализируй этот код. Он должен [опишите, что должен делать код, например, “скрывать элемент при клике”], но он не работает.
Найди ошибку(и) в коде, исправь ее и подробно объясни, в чем была проблема и почему предложенное тобой решение ее исправляет.
Код для анализа:
“`javascript
[вставьте сюда ваш неработающий JS-код] “`
Формат ответа:
1. Описание найденной ошибки.
2. Исправленный код.
3. Подробное объяснение.

*****

Почему это работает: Запрос не просто просит “исправить код”, а требует выступить в роли отладчика и объяснить причину. Описание ожидаемого поведения помогает ИИ понять цель кода и быстрее найти логическую ошибку.

8. Добавление ARIA-атрибутов для доступности

Проблема: Компонент сверстан, но не доступен для пользователей с экранными ридерами.

Решение: Промпт анализирует HTML-структуру и добавляет необходимые WAI-ARIA атрибуты для улучшения доступности (a11y).

Текст промпта:

*****

Ты – эксперт по веб-доступности (a11y). Твоя задача – улучшить доступность следующего HTML-компонента, добавив необходимые WAI-ARIA роли и атрибуты.
Компонент представляет собой [опишите компонент, например, “аккордеон” или “вкладки (табы)”].
Добавь атрибуты, такие как `role`, `aria-expanded`, `aria-controls`, `aria-labelledby`, и другие, где это необходимо.
Вот HTML-код для улучшения:
[вставьте сюда ваш HTML-код без ARIA-атрибутов] Верни обновленный HTML-код и добавь краткие комментарии, объясняющие, для чего нужен каждый добавленный атрибут.

*****

Почему это работает: Роль “эксперта по доступности” заставляет модель сфокусироваться на специфических атрибутах ARIA. Описание типа компонента (“аккордеон”) дает ключ к пониманию его интерактивного поведения и нужных ролей.

9. Оптимизация SVG-иконки

Проблема: SVG-иконка, выгруженная из графического редактора, содержит много лишнего кода: метаданные, комментарии, избыточные теги.

Решение: Промпт “чистит” SVG-код, удаляя все ненужное и оптимизируя его для использования на веб-странице.

Текст промпта:

*****

Ты – инструмент для оптимизации SVG. Проанализируй следующий SVG-код и оптимизируй его для использования в вебе.
Задачи оптимизации:
– Удалить метаданные редактора (например, комментарии, теги ``, ``).
– Удалить XML-инструкции и DOCTYPE.
– Удалить ненужные атрибуты, такие как `id`, `class`, если они не используются.
– Сократить числовые значения до необходимой точности (2-3 знака после запятой).
– Преобразовать стили из атрибутов в единый `style` или наоборот, если это уменьшит размер.
Вот SVG-код для оптимизации:
“`xml
[вставьте сюда ваш исходный SVG-код] “`
Верни только оптимизированный SVG-код.

*****

Почему это работает: Запрос перечисляет конкретные шаги оптимизации, которым должна следовать модель. Это превращает ее в аналог специализированного инструмента вроде SVGO, но с гибкой настройкой через текст.

10. Создание CSS-анимации при наведении

Проблема: Нужно добавить простой, но элегантный интерактивный эффект для элемента, например, для кнопки или ссылки.

Решение: Промпт генерирует CSS-код для создания анимационного эффекта при наведении курсора, используя `transition` и `transform`.

Текст промпта:

*****

Ты – CSS-аниматор. Напиши CSS-код для элемента с классом “[укажите класс, например, ‘.animated-button’]”.
При наведении курсора на этот элемент должен происходить следующий эффект: [опишите эффект, например, “элемент плавно сдвигается вверх на 5px и его тень становится более выраженной”].
Длительность анимации: [укажите время, например, “0.3 секунды”].
Функция плавности: [укажите функцию, например, “ease-in-out”].
Используй свойства `transition` и `transform`. Не используй JavaScript.
Сгенерируй только CSS-код.

*****

Почему это работает: Конкретное описание желаемого визуального эффекта, указание длительности и функции плавности дают ИИ все необходимые параметры для создания точного и аккуратного CSS-кода с переходами.

11. Написание стилей для кастомного чекбокса

Проблема: Стандартные чекбоксы выглядят по-разному в браузерах и не вписываются в дизайн проекта. Нужно создать свой, стилизованный чекбокс.

Решение: Промпт генерирует HTML и CSS для создания полностью кастомного чекбокса с использованием псевдоэлементов.

Текст промпта:

*****

Ты – frontend-разработчик. Создай HTML и CSS для стилизации чекбокса.
Задача: полностью скрыть стандартный `` и нарисовать кастомный вид с помощью `

*****

Почему это работает: Промпт описывает конкретную технику (скрыть input, использовать label и псевдоэлементы), что является стандартным подходом. Описание вида в двух состояниях дает четкие инструкции для стилизации.

12. Генерация HTML и CSS для формы обратной связи

Проблема: Нужно быстро сверстать стандартную форму с полями и кнопкой, обеспечив базовую структуру и стилизацию.

Решение: Запрос генерирует готовую разметку и стили для простой, но аккуратной формы обратной связи.

Текст промпта:

*****

Ты – верстальщик. Создай HTML и CSS для формы обратной связи.
Форма должна содержать следующие поля:
– Имя (текстовое поле, обязательное)
– Email (поле для email, обязательное)
– Сообщение (текстовая область, textarea)
– Кнопка “Отправить”
Используй тег `

` и семантически верные теги для полей (``, `
Готовность
Выберите тип, чтобы начать
≈ 1 мин
Собираю структуру 0%
✓ Ответ генератора

Технический промпт скрыт. Вы видите только свою конфигурацию, введённые данные и готовый результат.

`; if(anchor&&anchor.parentNode){anchor.parentNode.insertBefore(WRAP,anchor);}else{scope.insertBefore(WRAP,scope.firstChild);} // Гарантированный отступ НАД виджетом (распорка — не схлопывается, тему не задевает). try{ if(WRAP && WRAP.parentNode){ var gap=document.createElement("div"); gap.setAttribute("aria-hidden","true"); gap.style.cssText="height:24px;flex:0 0 auto;pointer-events:none;"; WRAP.parentNode.insertBefore(gap, WRAP); } }catch(e){} // Перенос блока «ИИ-технолог» ПОД виджет: переносим его исходную обёртку // (.entry-top) ЦЕЛИКОМ и ВНЕ .entry-content — иначе H2 подхватывает стили темы. try{ var promo=document.getElementById("kursblock"); var mover=promo?(promo.closest('.entry-top')||promo):null; var ec=document.querySelector('.entry-content,.post-content,.article-content,.td-post-content'); if(mover && ec && ec.parentNode){ ec.parentNode.insertBefore(mover, ec.nextSibling); mover.style.setProperty('margin-top','32px','important'); mover.style.setProperty('margin-bottom','0','important'); } }catch(e){} startEngine(WRAP); } if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',boot);}else{boot();} function startEngine(WRAP){ /* ========================================================= ICONS ========================================================= */ const CHECK_ICON = ` `; const AI_ICON = ` `; const FLIP_ICON = ` `; /* ========================================================= DATA ========================================================= */ /* ========================================================= РАЗМЕТКА ОТ ИИ (CyberSEO вставляет сюда свой ответ) Движок сам парсит её в CARDS / LAYERS / CLASSIC_PRESET / PAGE. ========================================================= */ /* демо-разметка (фолбэк, если на странице нет блока #generator) */ const DEMO_MARKUP = ` [[ТЕМА]] коммерческих предложений [[ПОДЗАГОЛОВОК]] Собери промпт: выбери тип, усили слоями и впиши данные. [[ЛИД]] Бесплатный генератор коммерческих предложений на нейросети: соберите сильный промпт под свою задачу за минуту. [[КАРТА]] [НАЗВАНИЕ] Холодное КП [ЯРЛЫК] Первый контакт [ХИТ] да [ОПИСАНИЕ] Цепляет того, кто о вас ещё не слышал. [КОГДА] Когда пишете тому, кто вас пока не знает: холодная база, первый выход на ЛПР. [ФИЧА] Сильный первый абзац [ФИЧА] Снимает недоверие [ФИЧА] Без длинных вступлений [МЕТРИКА] Свобода ИИ | 45 [МЕТРИКА] Простота ввода | 70 [МЕТРИКА] Глубина | 55 [ПРОМПТ] Составь холодное КП от [КОМПАНИЯ] для [АУДИТОРИЯ], которая пока не знает о нас. Продукт: [ПРОДУКТ]. Зацепи с первой строки, сними недоверие. [[/КАРТА]] [[КАРТА]] [НАЗВАНИЕ] Тёплое КП [ЯРЛЫК] Уже знают вас [ОПИСАНИЕ] Опирается на прошлый контакт и укрепляет доверие. [КОГДА] Когда клиент уже общался с вами: после встречи, заявки, пробного периода. [ФИЧА] Личное обращение [ФИЧА] Опора на контакт [ФИЧА] Мягкое дожатие [МЕТРИКА] Свобода ИИ | 40 [МЕТРИКА] Простота ввода | 60 [МЕТРИКА] Глубина | 60 [ПРОМПТ] Составь тёплое КП от [КОМПАНИЯ] для [АУДИТОРИЯ], уже знакомой с нами. Продукт: [ПРОДУКТ]. Опирайся на прошлый контакт [КОНТЕКСТ]. [[/КАРТА]] [[КАРТА]] [НАЗВАНИЕ] Длинное со списками [ЯРЛЫК] Детальное [ОПИСАНИЕ] Полный разбор по разделам. Для сложных сделок. [КОГДА] Когда сделка сложная или дорогая: услуги с этапами, B2B-проекты. [ФИЧА] Разделы и списки [ФИЧА] Полнота [ФИЧА] Под сложный B2B [МЕТРИКА] Свобода ИИ | 35 [МЕТРИКА] Простота ввода | 55 [МЕТРИКА] Глубина | 90 [ПРОМПТ] Составь развёрнутое КП от [КОМПАНИЯ] для [АУДИТОРИЯ] по [ПРОДУКТ]. Раздели на секции со списками: проблема, решение, этапы, результат. [[/КАРТА]] [[КАРТА]] [НАЗВАНИЕ] Короткое КП [ЯРЛЫК] На один экран [ОПИСАНИЕ] Суть, выгода, оффер и шаг — без воды. [КОГДА] Когда нужно быстро: мессенджер, короткое письмо, занятый руководитель. [ФИЧА] На один экран [ФИЧА] Без воды [ФИЧА] Быстрое чтение [МЕТРИКА] Свобода ИИ | 50 [МЕТРИКА] Простота ввода | 85 [МЕТРИКА] Глубина | 30 [ПРОМПТ] Составь короткое КП от [КОМПАНИЯ] для [АУДИТОРИЯ] по [ПРОДУКТ] — не длиннее экрана: суть, выгода, оффер, шаг. [[/КАРТА]] [[КАРТА]] [НАЗВАНИЕ] Через кейс [ЯРЛЫК] Доказательное [ХИТ] да [ОПИСАНИЕ] Показывает результат другого клиента. [КОГДА] Когда есть сильный кейс, цифры результата и надо снизить риск. [ФИЧА] Социальное доказательство [ФИЧА] Цифры результата [ФИЧА] Снимает риск [МЕТРИКА] Свобода ИИ | 40 [МЕТРИКА] Простота ввода | 45 [МЕТРИКА] Глубина | 75 [ПРОМПТ] Составь КП от [КОМПАНИЯ] для [АУДИТОРИЯ] на основе кейса [КЕЙС] с результатом [РЕЗУЛЬТАТ]. Перенеси логику успеха на клиента. [[/КАРТА]] [[КАРТА]] [НАЗВАНИЕ] Под тендер [ЯРЛЫК] Формальное [ОПИСАНИЕ] Строгий формат: требования, сроки и гарантии. [КОГДА] Когда участвуете в закупке, конкурсе, тендере. [ФИЧА] Соответствие ТЗ [ФИЧА] Сроки и гарантии [ФИЧА] Строгий тон [МЕТРИКА] Свобода ИИ | 25 [МЕТРИКА] Простота ввода | 50 [МЕТРИКА] Глубина | 70 [ПРОМПТ] Составь формальное КП от [КОМПАНИЯ] для закупки [АУДИТОРИЯ] по [ПРОДУКТ]: требования, сроки, гарантии, цена. [[/КАРТА]] [[СЛОЙ]] Кто пишет [ВАРИАНТ] Психолог коммуникаций | Учитывает мотивы и страхи ЛПР. | Пиши как психолог деловых коммуникаций: учитывай мотивы и страхи ЛПР, снимай возражения заранее. [ВАРИАНТ] Маркетолог со стажем | Выгоды и оффер на первом плане. | Пиши как опытный маркетолог: цепляющий заход, выгоды на первом плане, чёткий оффер. [ВАРИАНТ] Руководитель руководителю | По делу, через цифры. | Пиши как управленец управленцу: по делу, цифры, ответственность, без воды. [[/СЛОЙ]] [[СЛОЙ]] Мышление [ВАРИАНТ] Латеральное | Неочевидный угол подачи. | Найди неочевидный угол подачи, которого нет у конкурентов. [ВАРИАНТ] Первопринципы | От коренной потребности. | Иди от коренной потребности клиента, а не от шаблона. [ВАРИАНТ] Композиционное | Аргументы по нарастанию. | Выстрой аргументы по нарастанию — от слабого к сильному. [[/СЛОЙ]] [[СЛОЙ]] Упор [ВАРИАНТ] Психология | Триггеры доверия и дефицита. | Сделай упор на триггеры: доверие, принадлежность, страх упустить. [ВАРИАНТ] Выгоды | Экономия и рост в цифрах. | Сделай упор на измеримые выгоды: экономия, рост, окупаемость в цифрах. [ВАРИАНТ] Надёжность | Гарантии и снижение рисков. | Сделай упор на надёжность: гарантии, опыт, снижение рисков. [[/СЛОЙ]] [[КЛАССИКА]] карта=1 | слои=Кто пишет:2, Упор:2 [[FAQ]] [ВОПРОС] Что вписать в «Дополнительно», чтобы КП стало сильнее? [ОТВЕТ] Добавьте конкретику: цифры, сроки, кейс и главное возражение клиента — детали отличают убедительное предложение от шаблонного. [ВОПРОС] Как правильно заполнять поля? [ОТВЕТ] Пишите конкретно: не «бизнесу», а «руководителю продаж сети из 20 филиалов». Чем точнее вводные, тем точнее результат. [ВОПРОС] Чем усилители реально помогают? [ОТВЕТ] Они меняют логику подачи: заставляют считать экономику решения, снимать главное возражение или усиливать доказательства. [ВОПРОС] Что делать с готовым промптом? [ОТВЕТ] Скопируйте его в любую нейросеть, чтобы создать КП с ИИ, оцените черновик и при желании прогоните ещё раз с другим усилителем. [[/FAQ]] `; /* ========================================================= ЧТЕНИЕ РАЗМЕТКИ ИЗ ТЕЛА СТАТЬИ (маркер #generator ... #generator) Как у движка аналогов: ИИ выдаёт разметку в текст статьи между двумя строками-маркерами #generator. Движок находит блок, парсит и скрывает исходный текст, оставляя только конструктор. ========================================================= */ function readMarkupFromPage(){ // Читалка и пряталка на ОДНИХ узлах. Блок находится по всему дереву, // и прячется весь диапазон его верхнеуровневых контейнеров между // маркерами (кроме виджета). Устойчиво к: любой вложенности (.wp-block-*), // склейке блока в один абзац, съеденной решётке (#generator->generator), // висячим   в пустых абзацах, тексту статьи рядом с маркером. const root = document.querySelector('.entry-content, .post-content, .article-content, .td-post-content') || document.body; const widget = document.querySelector('.app-layout'); const widgetBox = document.getElementById('ai-generator-widget'); const BLANK = /^[\s\u00A0\u200B\u200C\u200D\uFEFF]*$/; const blank = (s) => BLANK.test(s || ''); const hasWidget = (el) => (widget && el.contains(widget)) || (widgetBox && el.contains(widgetBox)); const inWidget = (n) => { let p = n; while(p){ const t = p.nodeName; if(t === 'SCRIPT' || t === 'STYLE') return true; if(widget && p === widget) return true; if(widgetBox && p === widgetBox) return true; p = p.parentNode; } return false; }; // 1. Глубоко собираем текстовые узлы статьи (кроме виджета) + карту смещений. const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null); const nodes = []; let node; while((node = walker.nextNode())){ if(inWidget(node)) continue; nodes.push(node); } if(!nodes.length) return null; let full = ''; const ranges = []; for(let i = 0; i < nodes.length; i++){ if(i) full += '\n'; const start = full.length; full += (nodes[i].nodeValue || ''); ranges.push([start, full.length]); } // 2. Маркеры (учитываем съеденную решётку: #generator -> generator). const reMark = /(^|\n|\s)#?generator(\s|\n|$)/gi; const marks = []; let m; while((m = reMark.exec(full)) !== null){ const wStart = m.index + m[1].length; const wEnd = wStart + (m[0].length - m[1].length - m[2].length); marks.push({ start: wStart, end: wEnd }); } if(marks.length < 2) return null; // блока нет -> демо/выключение const first = marks[0], last = marks[marks.length - 1]; // 3. Текст блока между маркерами + нормализация: перенос строки ТОЛЬКО // перед структурными тегами. Плейсхолдеры внутри промптов не трогаем. let block = full.slice(first.start, last.start).replace(/^#?generator/i, ''); const TAGS = ['НАЗВАНИЕ','ЯРЛЫК','ХИТ','ОПИСАНИЕ','КОГДА','ФИЧА','МЕТРИКА','ПРОМПТ','ВАРИАНТ','ВОПРОС','ОТВЕТ']; block = block.replace(/\s*(\[\[[^\]]+\]\])\s*/g, '\n$1 '); block = block.replace(new RegExp('\\s*(\\[(?:' + TAGS.join('|') + ')\\])\\s*', 'g'), '\n$1 '); if(block.indexOf('[[КАРТА]]') === -1) return null; // Узлы маркеров и их верхнеуровневые (прямые дети root) контейнеры. const nodeAt = (pos) => { for(let i = 0; i < ranges.length; i++){ if(pos >= ranges[i][0] && pos < ranges[i][1]) return nodes[i]; } return nodes[nodes.length - 1]; }; const topChild = (n) => { let el = n; while(el && el.parentNode && el.parentNode !== root){ el = el.parentNode; } return (el && el.parentNode === root) ? el : null; }; const firstTop = topChild(nodeAt(first.start)); const lastTop = topChild(nodeAt(Math.max(first.start, last.end - 1))); // 4a. Стираем текст блока (границы режем посимвольно — текст статьи в том // же узле не заденем). const from = first.start, to = last.end; const touched = new Set(); for(let i = 0; i < nodes.length; i++){ const [s, e] = ranges[i]; if(e <= from || s >= to) continue; const v = nodes[i].nodeValue; const lf = Math.max(0, from - s), lt = Math.min(v.length, to - s); nodes[i].nodeValue = v.slice(0, lf) + v.slice(lt); if(nodes[i].parentNode) touched.add(nodes[i].parentNode); } // 4b. Прячем внутренние опустевшие элементы (в т.ч. внутри обёртки с виджетом). touched.forEach(p0 => { let el = p0; while(el && el !== root && el.nodeType === 1){ if(hasWidget(el)) break; if(blank(el.textContent)) el.style.display = 'none'; el = el.parentNode; } }); // 4c. Жёстко прячем верхнеуровневые контейнеры блока [firstTop..lastTop]: // между границами — целиком; сами границы — только если опустели // (чтобы не съесть текст статьи у маркера); обёртку с виджетом не трогаем. if(firstTop && lastTop){ const kids = [].slice.call(root.children); let started = false; for(const el of kids){ const isFirst = el === firstTop, isLast = el === lastTop; if(isFirst) started = true; if(started && !hasWidget(el)){ if(isFirst || isLast){ if(blank(el.textContent)) el.style.display = 'none'; } else { el.style.display = 'none'; } } if(isLast) break; } } return block; } let MARKUP = DEMO_MARKUP; // реально прочитается в initEngine() после загрузки DOM /* ========================================================= ПАРСЕР РАЗМЕТКИ → CARDS / LAYERS / CLASSIC_PRESET / PAGE ========================================================= */ function parseMarkup(text){ const lines = text.split(/\r?\n/); const page = { theme:"", subtitle:"", lead:"" }; const cards = []; const layers = []; const faq = []; let faqItem = null; let classic = { card:0, layers:{} }; let mode = null; // "card" | "layer" let card = null; // текущая карта let layer = null; // текущий слой const val = (line, tag) => line.slice(tag.length).trim(); for(let raw of lines){ const line = raw.trim(); if(!line) continue; // шапка if(line.startsWith("[[ТЕМА]]")){ page.theme = val(line,"[[ТЕМА]]"); continue; } if(line.startsWith("[[ПОДЗАГОЛОВОК]]")){ page.subtitle = val(line,"[[ПОДЗАГОЛОВОК]]"); continue; } if(line.startsWith("[[ЛИД]]")){ page.lead = val(line,"[[ЛИД]]"); continue; } // карта if(line === "[[КАРТА]]"){ mode = "card"; card = { name:"", badge:"", hot:false, description:"", when:"", features:[], metrics:[], prompt:"" }; continue; } if(line === "[[/КАРТА]]"){ if(card) cards.push(card); card=null; mode=null; continue; } // слой if(line.startsWith("[[СЛОЙ]]")){ mode = "layer"; const name = val(line,"[[СЛОЙ]]"); layer = { key:"layer"+(layers.length+1), name:name, options:[] }; continue; } if(line === "[[/СЛОЙ]]"){ if(layer) layers.push(layer); layer=null; mode=null; continue; } if(line === "[[FAQ]]"){ mode="faq"; faqItem=null; continue; } if(line === "[[/FAQ]]"){ mode=null; faqItem=null; continue; } // классика if(line.startsWith("[[КЛАССИКА]]")){ const body = val(line,"[[КЛАССИКА]]"); // карта=1 | слои=Кто пишет:2, Упор:2 const parts = body.split("|").map(s=>s.trim()); parts.forEach(pt=>{ if(pt.startsWith("карта=")){ classic.card = Math.max(0, parseInt(pt.slice(6),10)-1 || 0); } else if(pt.startsWith("слои=")){ pt.slice(5).split(",").forEach(pair=>{ const [lname, opt] = pair.split(":").map(x=>x.trim()); classic._pending = classic._pending || []; classic._pending.push([lname, parseInt(opt,10)-1]); }); } }); continue; } // поля карты if(mode === "card" && card){ if(line.startsWith("[НАЗВАНИЕ]")){ card.name = val(line,"[НАЗВАНИЕ]"); continue; } if(line.startsWith("[ЯРЛЫК]")){ card.badge = val(line,"[ЯРЛЫК]"); continue; } if(line.startsWith("[ХИТ]")){ card.hot = /да|true|1/i.test(val(line,"[ХИТ]")); continue; } if(line.startsWith("[ОПИСАНИЕ]")){ card.description = val(line,"[ОПИСАНИЕ]"); continue; } if(line.startsWith("[КОГДА]")){ card.when = val(line,"[КОГДА]"); continue; } if(line.startsWith("[ФИЧА]")){ card.features.push(val(line,"[ФИЧА]")); continue; } if(line.startsWith("[МЕТРИКА]")){ const m = val(line,"[МЕТРИКА]").split("|").map(x=>x.trim()); card.metrics.push([ m[0]||"", parseInt(m[1],10)||0 ]); continue; } if(line.startsWith("[ПРОМПТ]")){ card.prompt = val(line,"[ПРОМПТ]"); continue; } } // варианты слоя if(mode === "layer" && layer){ if(line.startsWith("[ВАРИАНТ]")){ const parts = val(line,"[ВАРИАНТ]").split("|").map(x=>x.trim()); layer.options.push([ parts[0]||"", parts[1]||"", parts[2]||"" ]); continue; } } // FAQ (вопрос+ответ на одной строке "[ВОПРОС] .. [ОТВЕТ] .." или на разных) if(mode === "faq"){ if(line.startsWith("[ВОПРОС]")){ const qa = val(line,"[ВОПРОС]").split("[ОТВЕТ]"); faqItem = { q:(qa[0]||"").trim(), a:(qa[1]||"").trim() }; faq.push(faqItem); continue; } if(line.startsWith("[ОТВЕТ]")){ if(!faqItem){ faqItem={q:"",a:""}; faq.push(faqItem); } faqItem.a = val(line,"[ОТВЕТ]"); continue; } } } // разрешить _pending слои классики (по имени → key) if(classic._pending){ classic._pending.forEach(([lname, opt])=>{ const L = layers.find(x=>x.name === lname); if(L) classic.layers[L.key] = opt; }); delete classic._pending; } return { page, cards, layers, classic, faq }; } let PARSED = parseMarkup(MARKUP); let PAGE = PARSED.page; let CARDS = PARSED.cards; let LAYERS = PARSED.layers; let CLASSIC_PRESET = PARSED.classic; let FAQ = PARSED.faq; // перечитать разметку из статьи и пересобрать структуры (вызывается после DOM готов) function loadMarkupAndBuild(){ let fromPage = null; try{ fromPage = readMarkupFromPage(); }catch(e){ fromPage = null; } // НЕТ блока #generator в этой статье → движок не работает здесь. // Это защищает от показа на всех страницах сайта (в шапке/подвале/других статьях). // Исключение: страница-превью, где на самой странице лежит атрибут data-generator-demo. if(!fromPage){ const allowDemo = document.querySelector('[data-generator-demo]'); if(!allowDemo){ const box = document.getElementById('ai-generator-widget'); if(box) box.style.display = 'none'; return false; // сигнал: не рисовать } MARKUP = DEMO_MARKUP; } else { MARKUP = fromPage; } try{ PARSED = parseMarkup(MARKUP); if(!PARSED.cards || PARSED.cards.length === 0) throw new Error('no cards'); }catch(e){ const box = document.getElementById('ai-generator-widget'); if(box) box.style.display = 'none'; return false; } PAGE = PARSED.page; CARDS = PARSED.cards; LAYERS = PARSED.layers; CLASSIC_PRESET = PARSED.classic; FAQ = PARSED.faq; return true; } const state = { card:null, layers:{}, activeLayer:0, ai:{}, values:{}, additional:"", cardSides:{} }; /* ========================================================= HELPERS ========================================================= */ const byId = id => document.getElementById(id); function escapeHTML(value){ return String(value ?? "").replace( /[&<>"']/g, character => ({ "&":"&", "<":"<", ">":">", '"':""", "'":"'" })[character] ); } function pulseElement(element){ if(!element){ return; } element.classList.remove("pulse"); void element.offsetWidth; element.classList.add("pulse"); } function animateElement(element,className){ if(!element){ return; } element.classList.remove(className); void element.offsetWidth; element.classList.add(className); element.addEventListener( "animationend", () => element.classList.remove(className), {once:true} ); } function selectedLayerCount(){ return LAYERS.filter(layer => { return state.layers[layer.key] != null; }).length; } function variantCount(){ let total = CARDS.length; LAYERS.forEach(layer => { total *= layer.options.length + 1; }); return total; } /* ========================================================= SOUND ========================================================= */ let soundEnabled = true; const Sound = (() => { let context = null; let master = null; const notes = { C:523.25, D:587.33, E:659.25, G:783.99, A:880 }; function ensure(){ if(!context){ context = new ( window.AudioContext || window.webkitAudioContext )(); master = context.createGain(); master.gain.value = .18; master.connect(context.destination); } if(context.state === "suspended"){ context.resume(); } return context; } function tone(frequency,duration = .12,delay = 0,gain = 1){ if(!soundEnabled){ return; } try{ const ctx = ensure(); const start = ctx.currentTime + delay; const oscillator = ctx.createOscillator(); const envelope = ctx.createGain(); oscillator.type = "sine"; oscillator.frequency.value = frequency; envelope.gain.setValueAtTime(.0001,start); envelope.gain.exponentialRampToValueAtTime( .22 * gain, start + .01 ); envelope.gain.exponentialRampToValueAtTime( .0001, start + duration ); oscillator.connect(envelope).connect(master); oscillator.start(start); oscillator.stop(start + duration + .03); }catch(error){} } return { click(){ tone(notes.D,.055,0,.43); tone(notes.E,.075,.06,.38); }, open(){ tone(notes.E,.11,0,.72); tone(notes.G,.18,.075,.66); }, close(){ tone(notes.G,.1,0,.62); tone(notes.E,.16,.07,.58); }, flip(){ tone(notes.C,.07,0,.45); tone(notes.G,.12,.06,.56); }, fill(){ tone(notes.G,.075,0,.44); tone(notes.A,.11,.055,.54); }, success(){ [notes.C,notes.E,notes.G,notes.A].forEach( (note,index) => { tone( note, index === 3 ? .18 : .09, index * .085, .62 ); } ); } }; })(); /* ========================================================= CARD CONTENT ========================================================= */ function metricClass(index){ return [ "originality", "simplicity", "persuasion" ][index]; } function cardFrontHTML(card,index){ const selected = state.card === index; return `
${escapeHTML(card.badge)} ${card.hot ? ` Хит ` : ""} ${selected ? ` ${CHECK_ICON} ` : ""}

${escapeHTML(card.name)}

${escapeHTML(card.description)}
${card.metrics.map((metric,metricIndex) => `
${escapeHTML(metric[0])} ${metric[1]}
`).join("")}
`; } function cardBackHTML(card,index){ const selected = state.card === index; return `
Когда брать · чем сильна
${escapeHTML(card.name)}

${escapeHTML(card.when)}

${card.features.map(feature => ` ${escapeHTML(feature)} `).join("")}
`; } /* ========================================================= CONTROLLED CLICK FLIP ========================================================= */ const turningCards = new Set(); function switchCardSide(cardElement,index){ if(turningCards.has(index)){ return; } const shell = cardElement.querySelector(".card-shell"); const button = cardElement.querySelector(".flip-button"); if(!shell || !button){ return; } const currentSide = state.cardSides[index] || "front"; const targetSide = currentSide === "front" ? "back" : "front"; turningCards.add(index); button.classList.add("spinning"); cardElement.classList.add("turn-out"); Sound.flip(); setTimeout(() => { state.cardSides[index] = targetSide; shell.innerHTML = targetSide === "back" ? cardBackHTML(CARDS[index],index) : cardFrontHTML(CARDS[index],index); cardElement.classList.toggle( "showing-back", targetSide === "back" ); button.setAttribute( "aria-label", targetSide === "back" ? "Вернуться к карточке" : "Показать, когда использовать" ); cardElement.classList.remove("turn-out"); cardElement.classList.add("turn-in"); setTimeout(() => { cardElement.classList.remove("turn-in"); button.classList.remove("spinning"); turningCards.delete(index); if(targetSide === "front"){ requestAnimationFrame(() => { byId("cardsGrid").classList.add("metrics-ready"); }); } },230); },180); } /* ========================================================= RENDER CARDS ========================================================= */ function renderCards(){ const root = byId("cardsGrid"); root.classList.remove("metrics-ready"); root.innerHTML = CARDS.map((card,index) => { const side = state.cardSides[index] || "front"; const selected = state.card === index; return `
${ side === "back" ? cardBackHTML(card,index) : cardFrontHTML(card,index) }
`; }).join(""); root.querySelectorAll("[data-card]").forEach(cardElement => { const index = Number(cardElement.dataset.card); cardElement.addEventListener("click",event => { if(event.target.closest("[data-flip]")){ return; } selectCard(index); }); cardElement.addEventListener("keydown",event => { if(event.key === "Enter" || event.key === " "){ event.preventDefault(); selectCard(index); } }); }); root.querySelectorAll("[data-flip]").forEach(button => { button.addEventListener("click",event => { event.stopPropagation(); const index = Number(button.dataset.flip); const cardElement = button.closest(".proposal-card"); switchCardSide(cardElement,index); }); }); requestAnimationFrame(() => { requestAnimationFrame(() => { root.classList.add("metrics-ready"); }); }); } function selectCard(index){ state.card = state.card === index ? null : index; if(state.card == null){ Sound.close(); }else{ Sound.open(); } pulseElement(byId("cardCounter")); hideGeneratedResult(); renderInterface(); } /* ========================================================= LAYERS ========================================================= */ function renderLayerTabs(){ const root = byId("layerTabs"); root.innerHTML = LAYERS.map((layer,index) => ` `).join(""); root.querySelectorAll("[data-layer-tab]").forEach(button => { button.addEventListener("click",() => { state.activeLayer = Number(button.dataset.layerTab); Sound.click(); renderLayerTabs(); renderLayerOptions(); }); }); } function renderLayerOptions(){ const root = byId("layerOptions"); const layer = LAYERS[state.activeLayer]; if(!layer){ root.innerHTML = ""; return; } root.innerHTML = layer.options.map((option,index) => ` `).join(""); root.querySelectorAll("[data-layer-option]").forEach(button => { button.addEventListener("click",() => { const index = Number(button.dataset.layerOption); state.layers[layer.key] = state.layers[layer.key] === index ? null : index; Sound.click(); pulseElement(byId("layerCounter")); hideGeneratedResult(); renderInterface(); }); }); } /* ========================================================= ASSEMBLY ========================================================= */ function assemblySegments(){ const result = []; if(state.card != null){ const card = CARDS[state.card]; result.push({ key:"Тип", value:card.name, prompt:card.prompt }); } LAYERS.forEach(layer => { const optionIndex = state.layers[layer.key]; if(optionIndex != null){ const option = layer.options[optionIndex]; result.push({ key:layer.name, value:option[0], prompt:option[2] }); } }); return result; } function placeholders(){ const hiddenText = assemblySegments() .map(segment => segment.prompt) .join("\n"); const result = []; for(const match of hiddenText.matchAll(/\[([^\]]+)\]/g)){ if(!result.includes(match[1])){ result.push(match[1]); } } return result; } function internalPrompt(){ let prompt = assemblySegments() .map(segment => segment.prompt) .join("\n\n"); placeholders().forEach(placeholder => { let replacement = ""; if(state.ai[placeholder]){ replacement = `(подбери подходящее значение для «${placeholder.toLowerCase()}» самостоятельно)`; }else{ replacement = String( state.values[placeholder] || "" ).trim(); } if(replacement){ prompt = prompt .split(`[${placeholder}]`) .join(replacement); } }); if(state.additional.trim()){ prompt += "\n\nДополнительно: " + state.additional.trim(); } return prompt; } /* ========================================================= STATUS ========================================================= */ function isFieldComplete(placeholder){ return Boolean( state.ai[placeholder] || String(state.values[placeholder] || "").trim() ); } function completedFieldCount(){ return placeholders().filter(isFieldComplete).length; } function missingFields(){ return placeholders().filter(item => { return !isFieldComplete(item); }); } function originalityScore(){ let score = 0; if(state.card != null){ // база зависит от КОНКРЕТНОЙ карты — от её метрик, // поэтому разные карты дают разную оригинальность. const card = CARDS[state.card]; let avg = 40; if(card && card.metrics && card.metrics.length){ let sum = 0, n = 0; card.metrics.forEach(function(m){ var v = parseInt(m[1], 10); if(!isNaN(v)){ sum += v; n++; } }); if(n){ avg = sum / n; } } // база карты 22..52 в зависимости от её метрик score += Math.round(22 + (avg / 100) * 30); } // каждый усилитель заметно двигает шкалу score += selectedLayerCount() * 15; if(state.additional.trim()){ score += 7; } return Math.min(100, score); } /* ========================================================= GENERATOR UI ========================================================= */ function renderOriginality(){ const score = originalityScore(); byId("originalityValue").textContent = score + "%"; var fill = byId("originalityFill"); fill.style.width = score + "%"; // один спокойный цвет — просто ползунок заполнения, без "плохо/хорошо" const badges = []; if(state.card != null){ badges.push("Основа"); } LAYERS.forEach(layer => { if(state.layers[layer.key] != null){ badges.push(layer.name); } }); byId("assemblyBadges").innerHTML = badges.map(title => ` ${escapeHTML(title)} `).join(""); } function renderSummary(){ const segments = assemblySegments(); const root = byId("summaryBox"); if(!segments.length){ root.innerHTML = `
Выберите тип предложения слева —
ваша сборка появится здесь.
`; return; } root.innerHTML = segments.map(segment => `
${escapeHTML(segment.key)} ${escapeHTML(segment.value)}
`).join(""); } // Разбирает плейсхолдер на короткую метку + подсказку для тултипа. // Вход: полный текст внутри [ ], например: // 'укажите роль, например, "опытный автор постов"' // 'опишите аудиторию, например, "владельцы студий"' // 'АУДИТОРИЯ' (старый короткий формат — тоже поддержан) // Возвращает { label, hint }. function parsePlaceholder(raw){ const full = String(raw || "").trim(); // старый формат: короткое ИМЯ капсом без запятых/кавычек → метка как есть, без подсказки if(/^[A-ZА-ЯЁ0-9 _-]+$/.test(full) && full.length <= 24 && full.indexOf(",") === -1){ return { label: capitalizeFirst(full.toLowerCase()), hint: "" }; } // новый формат: "<действие> <предмет>, например, "<пример>"" // 1) отделяем пример (после "например") let hint = ""; const exMatch = full.match(/,?\s*например[,:]?\s*(.+)$/i); let head = full; if(exMatch){ hint = exMatch[1].trim().replace(/^["«]|["»]$/g, "").trim(); head = full.slice(0, exMatch.index).trim(); } // 2) из "укажите роль" / "опишите аудиторию" / "дайте цель" убираем глагол-действие let label = head .replace(/^(укажите|опишите|дайте|впишите|введите|добавьте|напишите|перечислите|уточните)\s+/i, "") .trim(); // 3) если после чистки пусто — берём первое существительное из head if(!label){ label = head; } // 4) обрезаем хвост после запятой (лишние уточнения) label = label.split(",")[0].trim(); // 5) капитализация label = capitalizeFirst(label); // если подсказки не было, но head длинный — саму инструкцию в подсказку if(!hint && head && head.toLowerCase() !== label.toLowerCase()){ hint = capitalizeFirst(head); } return { label: label || full, hint: hint }; } function capitalizeFirst(s){ s = String(s || "").trim(); return s ? s.charAt(0).toUpperCase() + s.slice(1) : s; } function renderFields(){ const list = placeholders(); const root = byId("fieldsList"); byId("fieldsCount").textContent = list.length; if(!list.length){ root.innerHTML = `
Поля появятся после выбора типа предложения.
`; updateFieldStatus(); return; } root.innerHTML = list.map(placeholder => { const aiMode = Boolean(state.ai[placeholder]); const value = state.values[placeholder] || ""; const done = isFieldComplete(placeholder); const parsed = parsePlaceholder(placeholder); const hintHTML = parsed.hint ? ` Например: ${escapeHTML(parsed.hint)} ` : ""; return `
${hintHTML}
`; }).join(""); root.querySelectorAll("[data-field]").forEach(input => { input.addEventListener("input",() => { const placeholder = input.dataset.field; const wasComplete = isFieldComplete(placeholder); state.values[placeholder] = input.value; const nowComplete = isFieldComplete(placeholder); const row = input.closest(".field-row"); if(row){ row.classList.toggle("done",nowComplete); } hideGeneratedResult(); updateFieldStatus(); if(!wasComplete && nowComplete){ Sound.fill(); animateReadinessSegment(placeholder); } }); }); root.querySelectorAll("[data-ai-field]").forEach(button => { button.addEventListener("click",() => { const placeholder = button.dataset.aiField; const wasComplete = isFieldComplete(placeholder); state.ai[placeholder] = !state.ai[placeholder]; const nowComplete = isFieldComplete(placeholder); if(!wasComplete && nowComplete){ Sound.fill(); }else{ Sound.click(); } hideGeneratedResult(); renderFields(); }); }); updateFieldStatus(); } function animateReadinessSegment(placeholder){ requestAnimationFrame(() => { const segments = document.querySelectorAll( "[data-ready-field]" ); const segment = Array.from(segments).find(item => { return item.dataset.readyField === placeholder; }); if(segment && segment.animate){ segment.animate( [ { transform:"scale(.82)", boxShadow:"0 0 0 0 rgba(255,210,63,.65)" }, { transform:"scale(1.08)", boxShadow:"0 0 0 6px rgba(255,210,63,0)" }, { transform:"scale(1)", boxShadow:"none" } ], { duration:420, easing:"cubic-bezier(.2,.8,.2,1)" } ); } }); } function renderReadinessSegments(){ const list = placeholders(); const root = byId("readinessSegments"); if(!list.length){ root.style.gridTemplateColumns = "1fr"; root.innerHTML = `
Выберите тип — здесь появятся сегменты полей.
`; return; } root.style.gridTemplateColumns = `repeat(${list.length},minmax(14px,1fr))`; root.innerHTML = list.map(placeholder => { const complete = isFieldComplete(placeholder); const aiMode = Boolean(state.ai[placeholder]); return ` `; }).join(""); } function updateFieldStatus(){ const list = placeholders(); const completed = completedFieldCount(); const missing = list.length - completed; const progress = list.length ? Math.round(completed / list.length * 100) : 0; byId("fieldsProgressFill").style.width = progress + "%"; if(state.card == null){ byId("readinessMessage").textContent = "Выберите тип, чтобы начать"; }else if(!list.length || missing === 0){ byId("readinessMessage").textContent = "Все поля заполнены — можно генерировать"; }else{ byId("readinessMessage").textContent = `Осталось вписать: ${missing}`; } renderReadinessSegments(); } function renderGenerator(){ renderOriginality(); renderSummary(); renderFields(); byId("variantCounter").textContent = `из ${variantCount().toLocaleString("ru-RU")} вариантов`; } /* ========================================================= COUNTERS ========================================================= */ function renderCounters(){ byId("cardCounter").textContent = state.card == null ? `${CARDS.length} вариантов` : "1 выбран"; const count = selectedLayerCount(); const total = LAYERS.length; const canAdd = total - count; byId("layerCounter").textContent = canAdd > 0 ? `можно выбрать ещё: ${canAdd}` : "выбрано максимум"; } /* ========================================================= PRESETS ========================================================= */ function applyPreset(card,layers){ state.card = card; state.layers = {...layers}; state.activeLayer = 0; state.ai = {}; state.values = {}; state.additional = ""; state.cardSides = {}; byId("additionalField").value = ""; hideGeneratedResult(); Sound.success(); renderInterface(); } byId("luckyButton").addEventListener("click",event => { animateElement( event.currentTarget.querySelector(".dice-icon"), "dice-spin" ); const randomLayers = {}; LAYERS.forEach(layer => { if(Math.random() < .82){ randomLayers[layer.key] = Math.floor(Math.random() * layer.options.length); } }); applyPreset( Math.floor(Math.random() * CARDS.length), randomLayers ); showToast("Собрана случайная комбинация"); }); byId("classicButton").addEventListener("click",event => { animateElement( event.currentTarget.querySelector(".classic-icon"), "classic-play" ); animateElement( event.currentTarget, "classic-flash" ); applyPreset( CLASSIC_PRESET.card, CLASSIC_PRESET.layers ); showToast("Применена классическая сборка"); }); byId("resetButton").addEventListener("click",() => { state.card = null; state.layers = {}; state.activeLayer = 0; state.ai = {}; state.values = {}; state.additional = ""; state.cardSides = {}; byId("additionalField").value = ""; hideGeneratedResult(); Sound.close(); renderInterface(); }); byId("additionalField").addEventListener("input",event => { state.additional = event.target.value; hideGeneratedResult(); renderOriginality(); }); /* ========================================================= GENERATION ========================================================= */ let generationTimers = []; let typingTimer = null; function clearGenerationTimers(){ generationTimers.forEach(clearTimeout); generationTimers = []; if(typingTimer){ clearInterval(typingTimer); typingTimer = null; } } function renderProcessBlocks(){ byId("processBlocks").innerHTML = Array.from( {length:10}, () => `` ).join(""); } function hideGeneratedResult(){ clearGenerationTimers(); byId("generationProcess").classList.remove("show"); byId("resultBox").classList.remove("show"); byId("resultOutput").classList.remove("typing-cursor"); byId("resultOutput").textContent = ""; byId("processPercent").textContent = "0%"; byId("processText").textContent = "Собираю структуру"; renderProcessBlocks(); } function buildVisibleResult(){ const card = CARDS[state.card]; const company = state.values["КОМПАНИЯ"] || "вашей компании"; const audience = state.values["АУДИТОРИЯ"] || "целевой аудитории"; const product = state.values["ПРОДУКТ"] || "предлагаемого решения"; const roleIndex = state.layers.role; const focusIndex = state.layers.focus; const role = roleIndex != null ? LAYERS[0].options[roleIndex][0] : "Эксперт компании"; const focus = focusIndex != null ? LAYERS[2].options[focusIndex][0].toLowerCase() : "практическую ценность"; const openings = [ `Здравствуйте! Мы подготовили предложение, которое поможет ${audience} получить измеримый результат без лишнего усложнения процессов.`, `Спасибо за предыдущий контакт. Ниже — конкретный вариант продолжения работы с учётом ваших задач и обсуждённого контекста.`, `Предлагаем комплексное решение с понятной структурой работ, ответственностью на каждом этапе и прогнозируемым результатом.`, `Коротко: ${product} помогает ${audience} быстрее прийти к нужному результату и сократить лишние затраты.`, `Мы уже решали похожую задачу и предлагаем перенести проверенную механику результата на вашу ситуацию.`, `В соответствии с требованиями предлагаем структурированное решение с обозначенными сроками, гарантиями и составом работ.` ]; const additional = state.additional.trim() ? `\n\nДополнительные пожелания:\n${state.additional.trim()}` : ""; return `${card.name} ${openings[state.card]} Предложение от ${company} Что предлагаем: • решение на базе «${product}»; • понятный сценарий запуска; • прозрачные этапы взаимодействия; • акцент на ${focus}; • конкретный следующий шаг. Подход к коммуникации: ${role}. Текст выстроен так, чтобы клиент быстро понял суть предложения, практическую пользу и логику дальнейшего действия. Ожидаемый эффект: клиент получает понятную связь между своей задачей, предлагаемым решением и ожидаемым результатом. Следующий шаг: предлагаем коротко сверить исходные данные и подготовить финальную версию под конкретную ситуацию.${additional}`; } function typeResult(text){ const output = byId("resultOutput"); output.textContent = ""; output.classList.add("typing-cursor"); let position = 0; let soundCounter = 0; typingTimer = setInterval(() => { const step = text.length > 600 ? 4 : 3; position += step; soundCounter += step; output.textContent = text.slice(0,position); if(soundCounter >= 46){ soundCounter = 0; Sound.click(); } if(position >= text.length){ clearInterval(typingTimer); typingTimer = null; output.textContent = text; output.classList.remove("typing-cursor"); Sound.success(); } },13); } /* ========================================================= ОТПРАВКА НА БЭКЕНД (реальная генерация через твой WordPress) ========================================================= */ // Эндпоинт PHP-бэкенда (сниппет generator-backend.php). Относительный путь — // работает на том же домене, что и статья. var GENAI_ENDPOINT = "/wp-json/genai/v1/run"; // шлём промпт на бэкенд, возвращаем {text} или бросаем ошибку function requestGeneration(promptText){ return fetch(GENAI_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: promptText }) }).then(function(res){ return res.json().then(function(data){ if(!res.ok){ var msg = (data && data.error) ? data.error : "Не удалось сгенерировать. Попробуйте позже."; var err = new Error(msg); err.isLimit = !!(data && data.limit); throw err; } return data; // { text, left } }); }); } /* ─── МОДАЛКА РЕЗУЛЬТАТА ─── */ /* ─── Лёгкий Markdown → HTML (без библиотек) ─── */ function mdToHtml(md){ var s = String(md || ""); // экранируем HTML s = s.replace(/&/g,"&").replace(//g,">"); var lines = s.split(/\r?\n/); var html = []; var inUl = false, inOl = false; function closeLists(){ if(inUl){ html.push(""); inUl = false; } if(inOl){ html.push(""); inOl = false; } } function inline(t){ // жирный **...** и __...__ t = t.replace(/\*\*([^*]+)\*\*/g, "$1"); t = t.replace(/__([^_]+)__/g, "$1"); // курсив *...* и _..._ t = t.replace(/(^|[^*])\*([^*]+)\*/g, "$1$2"); t = t.replace(/(^|[^_])_([^_]+)_/g, "$1$2"); // `код` t = t.replace(/`([^`]+)`/g, "$1"); return t; } for(var i=0;i"+inline(h[2])+""); continue; } // маркированный список var ul = raw.match(/^[-*•]\s+(.*)$/); if(ul){ if(inOl){ html.push(""); inOl=false; } if(!inUl){ html.push("
    "); inUl=true; } html.push("
  • "+inline(ul[1])+"
  • "); continue; } // нумерованный список var ol = raw.match(/^\d+[.)]\s+(.*)$/); if(ol){ if(inUl){ html.push("
"); inUl=false; } if(!inOl){ html.push("
    "); inOl=true; } html.push("
  1. "+inline(ol[1])+"
  2. "); continue; } // обычный абзац closeLists(); html.push("

    "+inline(raw)+"

    "); } closeLists(); return html.join(""); } function ensureResultModal(){ var box = document.getElementById("genai-modal"); if(box) return box; box = document.createElement("div"); box.id = "genai-modal"; box.className = "genai-modal"; box.innerHTML = '
    ' + ''; // монтируем в сам виджет (чтобы стили изоляции действовали) var host = document.getElementById("ai-generator-widget") || document.body; host.appendChild(box); // закрытие по фону/крестику box.addEventListener("click", function(e){ if(e.target && e.target.getAttribute("data-close")){ closeResultModal(); } }); document.addEventListener("keydown", function(e){ if(e.key === "Escape"){ closeResultModal(); } }); // копирование: кладём и HTML (красиво в Google Docs/Word), и чистый текст box.querySelector(".genai-modal-copy").addEventListener("click", function(){ var body = box.querySelector(".genai-modal-body"); if(!body) return; var html = body.innerHTML; var plain = body.getAttribute("data-raw") || body.textContent || ""; var b = box.querySelector(".genai-modal-copy"); var done = function(){ var old = b.getAttribute("data-label") || b.textContent || "Копировать"; b.setAttribute("data-label", old); b.innerHTML = 'Скопировано'; b.classList.add("copied"); setTimeout(function(){ b.textContent = old; b.classList.remove("copied"); }, 1600); }; // современный способ: ClipboardItem с двумя форматами if(window.ClipboardItem && navigator.clipboard && navigator.clipboard.write){ try{ var item = new ClipboardItem({ "text/html": new Blob([html], { type: "text/html" }), "text/plain": new Blob([plain], { type: "text/plain" }) }); navigator.clipboard.write([item]).then(done).catch(function(){ navigator.clipboard.writeText(plain).then(done).catch(function(){}); }); return; }catch(e){ /* упадём в fallback ниже */ } } // fallback: только текст if(navigator.clipboard){ navigator.clipboard.writeText(plain).then(done).catch(function(){}); } }); return box; } function openResultModal(text, left, errorMsg){ var box = ensureResultModal(); var body = box.querySelector(".genai-modal-body"); var foot = box.querySelector(".genai-modal-foot"); var leftEl = box.querySelector(".genai-modal-left"); var title = box.querySelector(".genai-modal-title"); var copyBtn = box.querySelector(".genai-modal-copy"); if(errorMsg){ title.textContent = "Не получилось"; body.className = "genai-modal-body is-error"; body.textContent = errorMsg; leftEl.textContent = ""; if(copyBtn) copyBtn.style.display = "none"; // копировать нечего foot.style.display = ""; // но плашку курса оставляем } else { title.textContent = "Готовый результат"; body.className = "genai-modal-body genai-md"; body.innerHTML = mdToHtml(text || ""); body.setAttribute("data-raw", text || ""); // оригинал MD для копирования if(copyBtn) copyBtn.style.display = ""; foot.style.display = ""; leftEl.style.setProperty("color", "#7c8398", "important"); if(typeof left === "number"){ leftEl.textContent = left > 0 ? ("Осталось генераций сегодня: " + left) : "Это была последняя генерация на сегодня"; } else { leftEl.textContent = ""; } } box.classList.add("show"); document.documentElement.style.overflow = "hidden"; body.scrollTop = 0; } function closeResultModal(){ var box = document.getElementById("genai-modal"); if(box){ box.classList.remove("show"); } document.documentElement.style.overflow = ""; } function startGeneration(){ if(state.card == null){ showToast("Сначала выберите тип предложения"); return; } const missing = missingFields(); if(missing.length){ showToast(`Осталось заполнить: ${missing.length}`); byId("fieldsSection").scrollIntoView({ behavior:"smooth", block:"center" }); return; } clearGenerationTimers(); renderProcessBlocks(); const process = byId("generationProcess"); const blocks = process.querySelectorAll(".process-block"); byId("resultBox").classList.remove("show"); process.classList.add("show"); Sound.success(); const labels = [ "Собираю структуру", "Проверяю вводные", "Усиливаю аргументы", "Настраиваю тон", "Формирую результат" ]; blocks.forEach((block,index) => { const timer = setTimeout(() => { block.classList.add("running"); byId("processPercent").textContent = Math.round((index + 1) / blocks.length * 100) + "%"; byId("processText").textContent = labels[ Math.min( labels.length - 1, Math.floor(index / 2) ) ]; Sound.click(); },index * 115); generationTimers.push(timer); }); const finishTimer = setTimeout(() => { // реальный промпт (собранный из карты + слоёв + полей + дополнительно) const promptText = internalPrompt(); // держим анимацию процесса, пока ждём ответ byId("processText").textContent = "Генерирую ответ"; requestGeneration(promptText) .then(function(data){ process.classList.remove("show"); clearGenerationTimers(); openResultModal(data.text, data.left); launchConfetti(); }) .catch(function(err){ process.classList.remove("show"); clearGenerationTimers(); openResultModal(null, null, err.message || "Ошибка генерации."); }); },blocks.length * 115 + 250); generationTimers.push(finishTimer); } byId("generateButton").addEventListener( "click", startGeneration ); /* ========================================================= COPY ========================================================= */ byId("copyButton").addEventListener("click",async () => { const text = byId("resultOutput").textContent; if(!text){ return; } try{ await navigator.clipboard.writeText(text); showToast("Результат скопирован"); Sound.fill(); }catch(error){ const temporary = document.createElement("textarea"); temporary.value = text; temporary.style.position = "fixed"; temporary.style.opacity = "0"; document.body.appendChild(temporary); temporary.select(); try{ document.execCommand("copy"); showToast("Результат скопирован"); }catch(copyError){ showToast("Не удалось скопировать текст"); } temporary.remove(); } }); /* ========================================================= SOUND BUTTON ========================================================= */ const SOUND_ON_ICON = ` `; const SOUND_OFF_ICON = ` `; byId("soundButton").addEventListener("click",() => { soundEnabled = !soundEnabled; byId("soundButton").classList.toggle( "off", !soundEnabled ); byId("soundText").textContent = soundEnabled ? "Звук" : "Тихо"; byId("soundIcon").innerHTML = soundEnabled ? SOUND_ON_ICON : SOUND_OFF_ICON; if(soundEnabled){ Sound.fill(); } }); /* ========================================================= TOAST ========================================================= */ let toastTimer = null; function showToast(message){ const toast = byId("toast"); toast.textContent = message; toast.classList.add("show"); clearTimeout(toastTimer); toastTimer = setTimeout(() => { toast.classList.remove("show"); },1900); } /* ========================================================= CONFETTI ========================================================= */ function launchConfetti(){ try{ let canvas = byId("confetti"); // canvas может отсутствовать в DOM — создаём его на лету if(!canvas){ canvas = document.createElement("canvas"); canvas.id = "confetti"; canvas.className = "confetti"; var host = document.getElementById("ai-generator-widget") || document.body; host.appendChild(canvas); } if(!canvas || typeof canvas.getContext !== "function") return; const context = canvas.getContext("2d"); if(!context) return; canvas.width = window.innerWidth; canvas.height = window.innerHeight; const colors = [ "#ffd23f", "#182036", "#7765bd", "#16a174", "#8e98b2" ]; const originX = window.innerWidth > 820 ? window.innerWidth * .83 : window.innerWidth * .5; const originY = window.innerHeight * .54; const particles = Array.from( {length:76}, (_,index) => ({ x:originX, y:originY, vx:(Math.random() - .5) * 10, vy:Math.random() * -11 - 3, size:Math.random() * 4 + 2.5, color:colors[index % colors.length], alpha:1, rotation:Math.random() * Math.PI * 2, vr:(Math.random() - .5) * .35 }) ); function animate(){ context.clearRect( 0, 0, canvas.width, canvas.height ); let alive = false; particles.forEach(particle => { particle.vy += .34; particle.x += particle.vx; particle.y += particle.vy; particle.rotation += particle.vr; particle.alpha -= .012; if(particle.alpha > 0){ alive = true; context.save(); context.globalAlpha = particle.alpha; context.translate(particle.x,particle.y); context.rotate(particle.rotation); context.fillStyle = particle.color; context.fillRect( -particle.size, -particle.size, particle.size * 2, particle.size * 1.15 ); context.restore(); } }); if(alive){ requestAnimationFrame(animate); }else{ context.clearRect( 0, 0, canvas.width, canvas.height ); } } animate(); }catch(e){ /* конфетти не критичны — молча пропускаем */ } } /* ========================================================= MASTER RENDER ========================================================= */ function firstSentence(text){ var t = String(text || "").trim(); if(!t) return t; // берём до первой точки/!/? с последующим пробелом или концом строки var m = t.match(/^[\s\S]*?[.!?](?=\s|$)/); var out = m ? m[0].trim() : t; // если после обрезки осталась висячая точка-сокращение — вернём как есть return out; } function applyPage(){ if(PAGE.theme){document.title='Генератор '+PAGE.theme+' с ИИ';} var ld=document.getElementById('pageLead');if(ld){ if(PAGE.lead){ ld.textContent=firstSentence(PAGE.lead); ld.style.display=''; } else { ld.style.display='none'; } } var cc=document.getElementById('cardCounter');if(cc)cc.textContent=CARDS.length+' вариантов'; } function renderFaq(){ var sec=document.getElementById('npFaq'); var host=document.getElementById('npFaqList'); if(!sec||!host) return; if(!FAQ || !FAQ.length){ sec.style.display='none'; return; } var h=sec.querySelector('h2'); if(h){ h.textContent = (PAGE && PAGE.theme) ? ('Фишки и вопросы по генерации '+PAGE.theme+' с ИИ') : 'Фишки и вопросы'; } host.innerHTML=''; FAQ.forEach(function(item){ var it=document.createElement('div'); it.className='faq-item'; var btn=document.createElement('button'); btn.type='button'; btn.className='faq-q'; btn.setAttribute('aria-expanded','false'); var q=document.createElement('span'); q.className='faq-q-text'; q.textContent=item.q||''; var ic=document.createElement('span'); ic.className='faq-ic'; ic.setAttribute('aria-hidden','true'); btn.appendChild(q); btn.appendChild(ic); var a=document.createElement('div'); a.className='faq-a'; var ain=document.createElement('div'); ain.className='faq-a-in'; ain.textContent=item.a||''; a.appendChild(ain); btn.addEventListener('click',function(){ var open=it.classList.toggle('open'); btn.setAttribute('aria-expanded',open?'true':'false'); }); it.appendChild(btn); it.appendChild(a); host.appendChild(it); }); sec.style.display=''; } function renderInterface(){ applyPage(); renderCards(); renderLayerTabs(); renderLayerOptions(); renderCounters(); renderGenerator(); renderFaq(); } var __engineStarted = false; function initEngine(){ if(__engineStarted) return; __engineStarted = true; const ok = loadMarkupAndBuild(); if(!ok) return; // нет блока #generator в этой статье — не рисуем renderProcessBlocks(); renderInterface(); } initEngine(); } })();