This commit is contained in:
arys
2026-04-14 09:10:18 +05:00
parent e7eae6a877
commit 69d1dcd805
9 changed files with 2763 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(npm run *)"
]
}
}
+279
View File
@@ -0,0 +1,279 @@
# AI-генерация маркетинговых стратегий — Документация
## Обзор
Система AI-генерации создаёт профессиональные маркетинговые стратегии на 4 недели с 16 постами, используя строгую валидацию и профессиональные промпты.
## Архитектура
```
┌─────────────────────────────────────────────────────────────┐
│ ФРОНТЕНД (Vue 3) │
├─────────────────────────────────────────────────────────────┤
│ AIStrategyGenerator.vue (UI компонент) │
│ ↓ │
│ strategyPromptGenerator.ts (генерация промпта) │
│ ↓ │
│ AIStrategyService.ts (сервис API + валидация) │
│ ↓ │
│ validateAIStrategy.ts (полная валидация) │
│ ↓ │
│ ai-strategy.types.ts (TypeScript интерфейсы) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ BACKEND API │
│ POST /api/ai/strategy/generate │
│ Тело: { prompt, model, temperature, maxTokens } │
└─────────────────────────────────────────────────────────────┘
```
## Файлы
| Файл | Назначение |
|------|-----------|
| `src/types/ai-strategy.types.ts` | TypeScript интерфейсы и константы |
| `src/utils/validateAIStrategy.ts` | Утилита полной валидации AI-ответа |
| `src/utils/strategyPromptGenerator.ts` | Генератор промпта и парсер JSON |
| `src/service/AIStrategyService.ts` | Сервис работы с AI-стратегиями |
| `src/components/marketing/AIStrategyGenerator.vue` | UI компонент генератора |
| `src/views/pages/marketing/AIStrategyGeneratorPage.vue` | Страница генератора |
## TypeScript интерфейсы
### PostCalendarItem
```typescript
interface PostCalendarItem {
postIndex: number; // 0-15
publishDate: string; // "2026-04-13T09:00:00"
weekNumber: 1 | 2 | 3 | 4;
platform: 'Instagram' | 'TikTok';
contentType: 'фото' | 'видео';
theme: string; // Конкретная тема
postText: string; // 150+ слов
hashtags: string[]; // 5-8 хэштегов
publishTime: '09:00' | '12:00' | '18:00' | '20:00';
imageUrl?: string;
videoUrl?: string;
taskStatus?: string;
}
```
### WeeklyPlan
```typescript
interface WeeklyPlan {
weekNumber: 1 | 2 | 3 | 4;
mainThemes: string[]; // Ровно 4 темы
contentRecommendations: string; // 50+ слов
priorityPlatforms: string[];
}
```
### AIStrategyResponse
```typescript
interface AIStrategyResponse {
selectedModel: string;
scoringRationale: string;
recommendedDurationWeeks: 4;
recommendedPlatforms: string[];
rationale: string; // 60+ слов
weeklyPlans: WeeklyPlan[]; // 4 недели
postCalendar: PostCalendarItem[]; // 16 постов
}
```
## Правила валидации
### Критические ошибки (ERROR)
| Правило | Проверка |
|---------|---------|
| **Количество постов** | Ровно 16 (4 × 4 недели) |
| **Недели** | Каждая из 4 недель представлена |
| **Даты** | ISO формат, правильные дни (Пн, Ср, Пт, Сб) |
| **Время** | Только 09:00, 12:00, 18:00, 20:00 |
| **Видео** | Ровно 4 (первый пост каждой недели) |
| **Фото** | Ровно 12 (остальные посты) |
| **Хэштеги** | 5-8 на пост, каждый начинается с # |
| **Текст** | 150+ слов на пост |
| **Темы** | Конкретные, не абстрактные |
### Предупреждения (WARNING)
| Правило | Проверка |
|---------|---------|
| **Абстрактные темы** | Темы без конкретики |
| **Стиль** | Рекомендации по улучшению |
## Использование
### 1. Через UI
Перейдите на `/marketing-analysis/ai-strategy`
Заполните:
- Ниша бизнеса
- Название бренда
- Город продвижения
- Модель стратегии (ENTRY/AUTHORITY/TRUST/CONVERSION)
- Дата начала (опционально)
### 2. Программно
```typescript
import AIStrategyService from '@/service/AIStrategyService';
const result = await AIStrategyService.generateAIStrategy({
niche: 'Кондитерская',
brand: 'Sweet Dreams',
city: 'Алматы',
model: 'CONVERSION'
});
console.log(result.strategy); // AIStrategyResponse
console.log(result.validation); // ValidationResult
```
### 3. Валидация готовй стратегии
```typescript
import { validateAIStrategy } from '@/utils/validateAIStrategy';
const validation = validateAIStrategy(myStrategy);
if (!validation.isValid) {
console.error(validation.errors);
}
```
### 4. Экспорт
```typescript
// JSON
AIStrategyService.exportStrategyAsJSON(strategy, 'my-strategy');
// CSV
AIStrategyService.exportCalendarAsCSV(strategy, 'my-calendar');
```
## Константы
```typescript
STRATEGY_CONSTANTS = {
TOTAL_POSTS: 16,
POSTS_PER_WEEK: 4,
TOTAL_WEEKS: 4,
TOTAL_VIDEOS: 4,
TOTAL_PHOTOS: 12,
MIN_POST_WORDS: 150,
MIN_HASHTAGS: 5,
MAX_HASHTAGS: 8,
MIN_WEEKLY_RECOMMENDATION_WORDS: 50,
MIN_RATIONALE_WORDS: 60,
VALID_PUBLISH_TIMES: ['09:00', '12:00', '18:00', '20:00']
}
```
## Модели стратегий
| Модель | Описание | Когда использовать |
|--------|---------|-------------------|
| **ENTRY** | Вход / Формирование присутствия | Новые бизнес, узнаваемость |
| **AUTHORITY** | Экспертность / Компетентность | B2B, длинный цикл сделки |
| **TRUST** | Усиление доверия | Высокая конкуренция |
| **CONVERSION** | Прямая конверсия | Быстрые покупки, B2C |
## API Endpoints
### Генерация стратегии
```
POST /api/ai/strategy/generate
Content-Type: application/json
{
"prompt": "...",
"model": "gpt-4",
"temperature": 0.7,
"maxTokens": 8000
}
Response:
{
"data": {
"content": "{...JSON стратегии...}"
}
}
```
## Распределение постов
### Неделя 1-4 (одинаковая структура)
| Пост | День | Время | Тип |
|------|------|-------|-----|
| 1 | Понедельник | 09:00 | 🎥 Видео |
| 2 | Среда | 12:00 | 📸 Фото |
| 3 | Пятница | 18:00 | 📸 Фото |
| 4 | Суббота | 20:00 | 📸 Фото |
## Пример правильного ответа AI
```json
{
"selectedModel": "CONVERSION",
"scoringRationale": "Модель Conversion оптимальна...",
"recommendedDurationWeeks": 4,
"recommendedPlatforms": ["Instagram", "TikTok"],
"rationale": "Instagram и TikTok являются...",
"weeklyPlans": [
{
"weekNumber": 1,
"mainThemes": ["Кейс клиента", "Процесс работы", "Отзыв", "Экспертный"],
"contentRecommendations": "На этой неделе фокусируемся...",
"priorityPlatforms": ["Instagram"]
}
],
"postCalendar": [
{
"postIndex": 0,
"publishDate": "2026-04-13T09:00:00",
"weekNumber": 1,
"platform": "Instagram",
"contentType": "видео",
"theme": "Кейс: Как клиент увеличил продажи на 40%",
"postText": "Хотите узнать, как наш клиент из Алматы...",
"hashtags": ["#кейс", "#алматы", "#бизнес"],
"publishTime": "09:00"
}
]
}
```
## Troubleshooting
### Ошибка: "Невалидный JSON"
AI вернул текст с markdown блоками. Функция `extractJSONFromResponse` автоматически очищает ответ.
### Ошибка валидации: "postCalendar должен содержать ровно 16 постов"
AI вернул меньше или больше постов. Проверьте промпт и температуру (рекомендуется 0.7).
### Предупреждение: "Тема выглядит абстрактной"
AI создал тему без конкретики. Стратегия валидна, но темы можно улучшить.
## Roadmap
- [ ] Сохранение стратегии на бэкенде
- [ ] Интеграция с автопостингом
- [ ] Редактор постов в UI
- [ ] A/B тестирование тем
- [ ] Мульти-язычность
## Лицензия
Внутренний проект Konturai.
@@ -0,0 +1,849 @@
<template>
<div class="ai-strategy-generator">
<!-- HEADER -->
<div class="generator-header">
<div class="header-icon">
<i class="pi pi-sparkles"></i>
</div>
<h2>AI-генерация маркетинговой стратегии</h2>
<p class="header-description">
Заполните данные, и AI создаст профессиональную стратегию на 4 недели с 16 постами
</p>
</div>
<!-- FORM -->
<form @submit.prevent="handleGenerate" class="generator-form">
<!-- Основные поля -->
<div class="form-grid">
<div class="form-group">
<label for="niche">
<i class="pi pi-briefcase"></i> Ниша бизнеса *
</label>
<InputText
id="niche"
v-model="form.niche"
placeholder="Например: Кондитерская, Автосервис, Стоматология"
required
/>
</div>
<div class="form-group">
<label for="brand">
<i class="pi pi-tag"></i> Название бренда *
</label>
<InputText
id="brand"
v-model="form.brand"
placeholder="Например: Sweet Dreams, AutoPro"
required
/>
</div>
<div class="form-group">
<label for="city">
<i class="pi pi-map-marker"></i> Город продвижения *
</label>
<InputText
id="city"
v-model="form.city"
placeholder="Например: Алматы, Астана, Шымкент"
required
/>
</div>
<div class="form-group">
<label for="model">
<i class="pi pi-cog"></i> Модель стратегии *
</label>
<Dropdown
id="model"
v-model="form.model"
:options="modelOptions"
option-label="label"
option-value="value"
placeholder="Выберите модель"
class="w-full"
required
>
<template #option="slotProps">
<div class="model-option">
<span class="model-icon">{{ slotProps.option.icon }}</span>
<div>
<div class="model-label">{{ slotProps.option.label }}</div>
<div class="model-desc">{{ slotProps.option.description }}</div>
</div>
</div>
</template>
</Dropdown>
</div>
</div>
<!-- Опционально: стартовая дата -->
<div class="form-group">
<label for="startDate">
<i class="pi pi-calendar"></i> Дата начала стратегии
</label>
<Calendar
id="startDate"
v-model="form.startDate"
:min-date="minDate"
date-format="dd.mm.yy"
placeholder="По умолчанию — следующий понедельник"
class="w-full"
/>
</div>
<!-- Validation Errors Display -->
<div v-if="validationErrors.length > 0" class="validation-errors">
<div class="error-header">
<i class="pi pi-exclamation-circle"></i>
<span>Ошибки валидации</span>
</div>
<ul>
<li v-for="(error, idx) in validationErrors" :key="idx">
{{ error }}
</li>
</ul>
</div>
<!-- Validation Warnings Display -->
<div v-if="validationWarnings.length > 0" class="validation-warnings">
<div class="warning-header">
<i class="pi pi-exclamation-triangle"></i>
<span>Предупреждения</span>
</div>
<ul>
<li v-for="(warning, idx) in validationWarnings" :key="idx">
{{ warning }}
</li>
</ul>
</div>
<!-- Submit Button -->
<div class="form-actions">
<Button
type="submit"
:label="isGenerating ? 'Генерация стратегии...' : 'Сгенерировать стратегию'"
:icon="isGenerating ? 'pi pi-spin pi-spinner' : 'pi pi-sparkles'"
:disabled="isGenerating || !isFormValid"
severity="primary"
class="generate-btn"
/>
</div>
</form>
<!-- LOADING STATE -->
<div v-if="isGenerating" class="loading-overlay">
<div class="loading-content">
<ProgressSpinner />
<h3>{{ loadingMessage }}</h3>
<ProgressBar :value="progressPercent" :show-value="false" />
<p class="loading-hint">{{ loadingHint }}</p>
</div>
</div>
<!-- SUCCESS: Strategy Preview -->
<div v-if="generatedStrategy" class="strategy-preview">
<div class="preview-header">
<div class="success-icon">
<i class="pi pi-check-circle"></i>
</div>
<h3>Стратегия успешно сгенерирована!</h3>
<p>
{{ previewSummary.totalPosts }} постов на {{ previewSummary.totalWeeks }} недели
{{ previewSummary.videoCount }} видео {{ previewSummary.photoCount }} фото
</p>
</div>
<!-- Weekly Plans -->
<div class="weekly-plans">
<h4>📅 Еженедельные планы</h4>
<div v-for="week in generatedStrategy.weeklyPlans" :key="week.weekNumber" class="week-plan-card">
<div class="week-header">
<span class="week-badge">Неделя {{ week.weekNumber }}</span>
<div class="week-platforms">
<Tag v-for="platform in week.priorityPlatforms" :key="platform" :value="platform" severity="info" />
</div>
</div>
<div class="week-themes">
<div v-for="(theme, idx) in week.mainThemes" :key="idx" class="theme-tag">
{{ theme }}
</div>
</div>
<p class="week-recommendations">{{ week.contentRecommendations }}</p>
</div>
</div>
<!-- Post Calendar by Week -->
<div class="post-calendar">
<h4>📝 Календарь постов</h4>
<div v-for="weekNum in 4" :key="weekNum" class="week-posts">
<h5>Неделя {{ weekNum }}</h5>
<div class="posts-grid">
<div
v-for="post in getWeekPosts(weekNum)"
:key="post.postIndex"
class="post-card"
:class="{ 'video-post': post.contentType === 'видео' }"
>
<div class="post-meta">
<span class="post-date">{{ formatDate(post.publishDate) }}</span>
<span class="post-time">{{ post.publishTime }}</span>
<Tag :value="post.contentType" :severity="post.contentType === 'видео' ? 'warning' : 'success'" />
<Tag :value="post.platform" severity="info" />
</div>
<h6 class="post-theme">{{ post.theme }}</h6>
<p class="post-text-preview">{{ truncateText(post.postText, 150) }}</p>
<div class="post-hashtags">
<span v-for="tag in post.hashtags" :key="tag" class="hashtag">{{ tag }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="preview-actions">
<Button
label="Скачать JSON"
icon="pi pi-download"
@click="handleExportJSON"
outlined
/>
<Button
label="Скачать CSV"
icon="pi pi-file-export"
@click="handleExportCSV"
outlined
/>
<Button
label="Сохранить стратегию"
icon="pi pi-save"
@click="handleSaveStrategy"
severity="success"
/>
<Button
label="Сгенерировать новую"
icon="pi pi-refresh"
@click="handleReset"
text
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { useToast } from 'primevue/usetoast';
import InputText from 'primevue/inputtext';
import Dropdown from 'primevue/dropdown';
import Calendar from 'primevue/calendar';
import Button from 'primevue/button';
import Tag from 'primevue/tag';
import ProgressSpinner from 'primevue/progressspinner';
import ProgressBar from 'primevue/progressbar';
import AIStrategyService from '@/service/AIStrategyService';
import type {
StrategyGenerationRequest,
AIStrategyResponse,
PostCalendarItem,
WeekNumber
} from '@/types/ai-strategy.types';
import { STRATEGY_MODELS } from '@/config/marketingModels';
const toast = useToast();
// ============================================================
// FORM STATE
// ============================================================
const form = ref<StrategyGenerationRequest>({
niche: '',
brand: '',
city: '',
model: 'ENTRY',
startDate: undefined
});
const isGenerating = ref(false);
const progressPercent = ref(0);
const loadingMessage = ref('Генерация промпта...');
const loadingHint = ref('Подготовка данных для AI');
const validationErrors = ref<string[]>([]);
const validationWarnings = ref<string[]>([]);
const generatedStrategy = ref<AIStrategyResponse | null>(null);
// ============================================================
// COMPUTED
// ============================================================
const modelOptions = Object.values(STRATEGY_MODELS).map(model => ({
label: model.title,
value: model.name,
icon: model.icon,
description: model.description
}));
const minDate = computed(() => {
const today = new Date();
today.setHours(0, 0, 0, 0);
return today;
});
const isFormValid = computed(() => {
return form.value.niche.trim() &&
form.value.brand.trim() &&
form.value.city.trim() &&
form.value.model;
});
const previewSummary = computed(() => {
if (!generatedStrategy.value) return { totalPosts: 0, totalWeeks: 0, videoCount: 0, photoCount: 0 };
return AIStrategyService.generateStrategyPreview(generatedStrategy.value);
});
// ============================================================
// METHODS
// ============================================================
function getWeekPosts(weekNum: WeekNumber): PostCalendarItem[] {
if (!generatedStrategy.value) return [];
return AIStrategyService.getPostsForWeek(generatedStrategy.value, weekNum);
}
function formatDate(dateStr: string): string {
const date = new Date(dateStr);
return date.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
weekday: 'short'
});
}
function truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
}
function validateForm(): boolean {
validationErrors.value = [];
validationWarnings.value = [];
if (!form.value.niche.trim()) {
validationErrors.value.push('Ниша бизнеса обязательна');
}
if (!form.value.brand.trim()) {
validationErrors.value.push('Название бренда обязательно');
}
if (!form.value.city.trim()) {
validationErrors.value.push('Город продвижения обязателен');
}
return validationErrors.value.length === 0;
}
async function handleGenerate() {
if (!validateForm()) {
toast.add({
severity: 'error',
summary: 'Ошибка валидации',
detail: 'Пожалуйста, исправьте ошибки в форме',
life: 4000
});
return;
}
isGenerating.value = true;
progressPercent.value = 5;
generatedStrategy.value = null;
validationErrors.value = [];
validationWarnings.value = [];
// Анимация загрузки
const loadingMessages = [
{ msg: 'Генерация промпта...', hint: 'Подготовка данных для AI' },
{ msg: 'Отправка запроса к AI...', hint: 'AI анализирует нишу и конкурентов' },
{ msg: 'Создание стратегии...', hint: 'Формирование контент-плана' },
{ msg: 'Валидация результатов...', hint: 'Проверка соответствия стандартам' }
];
let msgIdx = 0;
const msgInterval = setInterval(() => {
msgIdx = (msgIdx + 1) % loadingMessages.length;
loadingMessage.value = loadingMessages[msgIdx].msg;
loadingHint.value = loadingMessages[msgIdx].hint;
progressPercent.value = Math.min(progressPercent.value + 20, 80);
}, 3000);
try {
const result = await AIStrategyService.generateAIStrategy(form.value);
progressPercent.value = 100;
generatedStrategy.value = result.strategy;
// Показать предупреждения если есть
if (result.validation.warnings.length > 0) {
validationWarnings.value = result.validation.warnings.map(w => w.message);
toast.add({
severity: 'warn',
summary: 'Предупреждения',
detail: `Стратегия имеет ${result.validation.warnings.length} предупреждений`,
life: 5000
});
}
toast.add({
severity: 'success',
summary: 'Успех!',
detail: `Стратегия сгенерирована: ${result.strategy.postCalendar.length} постов`,
life: 4000
});
} catch (error: any) {
validationErrors.value.push(error.message || 'Ошибка при генерации стратегии');
toast.add({
severity: 'error',
summary: 'Ошибка',
detail: error.message || 'Не удалось сгенерировать стратегию',
life: 5000
});
} finally {
clearInterval(msgInterval);
isGenerating.value = false;
}
}
function handleExportJSON() {
if (!generatedStrategy.value) return;
AIStrategyService.exportStrategyAsJSON(
generatedStrategy.value,
`strategy-${form.value.brand}-${new Date().toISOString().split('T')[0]}`
);
toast.add({
severity: 'success',
summary: 'Экспорт',
detail: 'JSON файл скачивается',
life: 3000
});
}
function handleExportCSV() {
if (!generatedStrategy.value) return;
AIStrategyService.exportCalendarAsCSV(
generatedStrategy.value,
`calendar-${form.value.brand}-${new Date().toISOString().split('T')[0]}`
);
toast.add({
severity: 'success',
summary: 'Экспорт',
detail: 'CSV файл скачивается',
life: 3000
});
}
function handleSaveStrategy() {
// TODO: Сохранение стратегии на бэкенде
toast.add({
severity: 'info',
summary: 'Информация',
detail: 'Функция сохранения на бэкенде в разработке',
life: 3000
});
}
function handleReset() {
generatedStrategy.value = null;
validationErrors.value = [];
validationWarnings.value = [];
progressPercent.value = 0;
form.value = {
niche: '',
brand: '',
city: '',
model: 'ENTRY',
startDate: undefined
};
}
</script>
<style scoped>
.ai-strategy-generator {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.generator-header {
text-align: center;
margin-bottom: 3rem;
}
.header-icon {
font-size: 3rem;
color: var(--primary-color);
margin-bottom: 1rem;
}
.generator-header h2 {
font-size: 2rem;
font-weight: 700;
margin: 0.5rem 0;
color: var(--text-color);
}
.header-description {
color: var(--text-color-secondary);
font-size: 1.1rem;
}
/* Form Styles */
.generator-form {
background: var(--surface-card);
padding: 2rem;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-weight: 600;
color: var(--text-color);
display: flex;
align-items: center;
gap: 0.5rem;
}
.form-group label i {
color: var(--primary-color);
}
.model-option {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0;
}
.model-icon {
font-size: 1.5rem;
}
.model-label {
font-weight: 600;
color: var(--text-color);
}
.model-desc {
font-size: 0.85rem;
color: var(--text-color-secondary);
}
/* Validation Messages */
.validation-errors,
.validation-warnings {
margin: 1.5rem 0;
padding: 1rem;
border-radius: 8px;
}
.validation-errors {
background: #fee2e2;
border: 1px solid #ef4444;
}
.validation-warnings {
background: #fef3c7;
border: 1px solid #f59e0b;
}
.error-header,
.warning-header {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.validation-errors ul,
.validation-warnings ul {
margin: 0;
padding-left: 1.5rem;
}
.validation-errors li,
.validation-warnings li {
margin: 0.25rem 0;
font-size: 0.9rem;
}
/* Form Actions */
.form-actions {
margin-top: 2rem;
text-align: center;
}
.generate-btn {
min-width: 250px;
}
/* Loading Overlay */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading-content {
background: var(--surface-card);
padding: 3rem;
border-radius: 12px;
text-align: center;
max-width: 500px;
}
.loading-content h3 {
margin: 1rem 0 0.5rem;
color: var(--text-color);
}
.loading-hint {
color: var(--text-color-secondary);
font-size: 0.9rem;
margin-top: 1rem;
}
/* Strategy Preview */
.strategy-preview {
margin-top: 3rem;
}
.preview-header {
text-align: center;
padding: 2rem;
background: linear-gradient(135deg, var(--primary-color), var(--primary-color-dark));
color: white;
border-radius: 12px;
margin-bottom: 2rem;
}
.success-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.preview-header h3 {
font-size: 1.75rem;
margin: 0.5rem 0;
}
.preview-header p {
font-size: 1.1rem;
opacity: 0.9;
}
/* Weekly Plans */
.weekly-plans {
margin-bottom: 2rem;
}
.weekly-plans h4 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: var(--text-color);
}
.week-plan-card {
background: var(--surface-card);
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 1rem;
border: 1px solid var(--surface-border);
}
.week-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.week-badge {
background: var(--primary-color);
color: white;
padding: 0.5rem 1rem;
border-radius: 6px;
font-weight: 600;
}
.week-platforms {
display: flex;
gap: 0.5rem;
}
.week-themes {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.theme-tag {
background: var(--surface-ground);
padding: 0.4rem 0.8rem;
border-radius: 6px;
font-size: 0.9rem;
color: var(--text-color);
}
.week-recommendations {
color: var(--text-color-secondary);
line-height: 1.6;
margin: 0;
}
/* Post Calendar */
.post-calendar {
margin-bottom: 2rem;
}
.post-calendar h4 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: var(--text-color);
}
.week-posts {
margin-bottom: 2rem;
}
.week-posts h5 {
font-size: 1.25rem;
margin-bottom: 1rem;
color: var(--text-color);
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.post-card {
background: var(--surface-card);
padding: 1.5rem;
border-radius: 8px;
border: 1px solid var(--surface-border);
transition: transform 0.2s, box-shadow 0.2s;
}
.post-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.post-card.video-post {
border-left: 4px solid var(--orange-500);
}
.post-meta {
display: flex;
gap: 0.5rem;
align-items: center;
margin-bottom: 0.75rem;
flex-wrap: wrap;
}
.post-date {
font-weight: 600;
color: var(--text-color);
}
.post-time {
color: var(--text-color-secondary);
font-size: 0.9rem;
}
.post-theme {
font-size: 1.1rem;
font-weight: 600;
margin: 0.75rem 0;
color: var(--text-color);
}
.post-text-preview {
color: var(--text-color-secondary);
line-height: 1.6;
font-size: 0.9rem;
margin-bottom: 1rem;
}
.post-hashtags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.hashtag {
color: var(--primary-color);
font-size: 0.85rem;
background: var(--surface-ground);
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
/* Action Buttons */
.preview-actions {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid var(--surface-border);
}
/* Responsive */
@media (max-width: 768px) {
.ai-strategy-generator {
padding: 1rem;
}
.form-grid {
grid-template-columns: 1fr;
}
.posts-grid {
grid-template-columns: 1fr;
}
.preview-actions {
flex-direction: column;
}
.preview-actions > * {
width: 100%;
}
}
</style>
+178
View File
@@ -0,0 +1,178 @@
/**
* Сервис для работы с AI-генерацией маркетинговых стратегий
* Объединяет генерацию промпта, валидацию и работу с API
*/
import type {
AIStrategyResponse,
StrategyGenerationRequest,
ValidationResult
} from '@/types/ai-strategy.types';
import { generateStrategyPrompt, extractJSONFromResponse, safeParseJSON } from '@/utils/strategyPromptGenerator';
import { validateAIStrategy, getValidationSummary } from '@/utils/validateAIStrategy';
import { API_CONFIG, DEFAULT_REQUEST_CONFIG } from '@/config/api';
import AuthService from '@/service/AuthService';
const API_BASE_URL = API_CONFIG.BASE_URL;
class AIStrategyService {
/**
* Генерация AI-стратегии через внешний AI API
* POST /api/ai/strategy/generate
*/
async generateAIStrategy(request: StrategyGenerationRequest): Promise<{
strategy: AIStrategyResponse;
validation: ValidationResult;
rawResponse: string;
}> {
// 1. Генерация промпта
const prompt = generateStrategyPrompt(request);
console.log('📝 Сгенерирован промпт для AI');
try {
// 2. Вызов AI API
const response = await AuthService.authFetch(`${API_BASE_URL}/api/ai/strategy/generate`, {
method: 'POST',
...DEFAULT_REQUEST_CONFIG,
body: JSON.stringify({
prompt,
model: 'gpt-4', // Или другая модель
temperature: 0.7,
maxTokens: 8000
})
});
const result = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(result?.message || 'Ошибка при генерации AI-стратегии');
}
const rawResponse = result?.data?.content || result?.content || result?.data;
// 3. Извлечение JSON из ответа
const jsonString = extractJSONFromResponse(rawResponse);
console.log('🔍 Извлечён JSON из ответа AI');
// 4. Парсинг JSON
const parsedStrategy = safeParseJSON(jsonString) as AIStrategyResponse;
// 5. Валидация стратегии
const validation = validateAIStrategy(parsedStrategy);
console.log(getValidationSummary(validation));
if (!validation.isValid) {
console.warn('⚠️ Стратегия не прошла валидацию:', validation.errors);
}
return {
strategy: parsedStrategy,
validation,
rawResponse
};
} catch (error) {
console.error('❌ Ошибка при генерации AI-стратегии:', error);
throw error;
}
}
/**
* Валидация готовой стратегии (без вызова API)
*/
validateStrategy(strategy: unknown): ValidationResult {
return validateAIStrategy(strategy);
}
/**
* Получить посты для конкретной недели
*/
getPostsForWeek(strategy: AIStrategyResponse, weekNumber: 1 | 2 | 3 | 4) {
return strategy.postCalendar.filter(post => post.weekNumber === weekNumber);
}
/**
* Получить все видео-посты
*/
getVideoPosts(strategy: AIStrategyResponse) {
return strategy.postCalendar.filter(post => post.contentType === 'видео');
}
/**
* Получить все фото-посты
*/
getPhotoPosts(strategy: AIStrategyResponse) {
return strategy.postCalendar.filter(post => post.contentType === 'фото');
}
/**
* Получить посты для конкретной платформы
*/
getPostsForPlatform(strategy: AIStrategyResponse, platform: 'Instagram' | 'TikTok') {
return strategy.postCalendar.filter(post => post.platform === platform);
}
/**
* Сгенерировать превью стратегии для UI (краткая информация)
*/
generateStrategyPreview(strategy: AIStrategyResponse) {
return {
model: strategy.selectedModel,
totalPosts: strategy.postCalendar.length,
totalWeeks: strategy.weeklyPlans.length,
videoCount: this.getVideoPosts(strategy).length,
photoCount: this.getPhotoPosts(strategy).length,
platforms: strategy.recommendedPlatforms,
weeklyThemes: strategy.weeklyPlans.map(wp => ({
week: wp.weekNumber,
themes: wp.mainThemes
}))
};
}
/**
* Экспорт стратегии в JSON файл для скачивания
*/
exportStrategyAsJSON(strategy: AIStrategyResponse, filename?: string): void {
const jsonString = JSON.stringify(strategy, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename || `strategy-${strategy.selectedModel}-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* Экспорт календаря постов в CSV
*/
exportCalendarAsCSV(strategy: AIStrategyResponse, filename?: string): void {
const headers = ['Дата', 'Время', 'Неделя', 'Платформа', 'Тип', 'Тема', 'Хэштеги'];
const rows = strategy.postCalendar.map(post => [
post.publishDate,
post.publishTime,
`Неделя ${post.weekNumber}`,
post.platform,
post.contentType,
`"${post.theme.replace(/"/g, '""')}"`,
post.hashtags.join(' ')
]);
const csvContent = [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename || `calendar-${strategy.selectedModel}-${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
}
export default new AIStrategyService();
+229
View File
@@ -0,0 +1,229 @@
/**
* Типы и интерфейсы для AI-генерации маркетинговой стратегии
* Соответствуют строгому формату промпта: 16 постов, 4 недели, Пн-Ср-Пт-Сб
*/
// ============================================================
// БАЗОВЫЕ ТИПЫ
// ============================================================
export type Platform = 'Instagram' | 'TikTok';
export type ContentType = 'фото' | 'видео';
export type PublishTime = '09:00' | '12:00' | '18:00' | '20:00';
export type WeekNumber = 1 | 2 | 3 | 4;
// ============================================================
// POST CALENDAR ITEM
// ============================================================
export interface PostCalendarItem {
/** Индекс поста в календаре (0-15) */
postIndex: number;
/** Дата и время публикации в ISO формате */
publishDate: string; // "2026-04-13T09:00:00"
/** Номер недели (1, 2, 3 или 4) */
weekNumber: WeekNumber;
/** Платформа публикации */
platform: Platform;
/** Тип контента */
contentType: ContentType;
/** Конкретная тема поста (не абстракция!) */
theme: string;
/** Текст поста (минимум 150 слов) */
postText: string;
/** Хэштеги (5-8 штук, каждый начинается с #) */
hashtags: string[];
/** Время публикации */
publishTime: PublishTime;
/** Опционально: URL изображения */
imageUrl?: string;
/** Опционально: URL видео */
videoUrl?: string;
/** Опционально: статус задачи */
taskStatus?: 'PENDING' | 'SCHEDULED' | 'PUBLISHED' | 'FAILED';
}
// ============================================================
// WEEKLY PLAN
// ============================================================
export interface WeeklyPlan {
/** Номер недели (1, 2, 3 или 4) */
weekNumber: WeekNumber;
/** Основные темы недели (ровно 4 темы) */
mainThemes: string[];
/** Рекомендации по контенту (минимум 50 слов) */
contentRecommendations: string;
/** Приоритетные платформы */
priorityPlatforms: Platform[];
}
// ============================================================
// STRATEGY RESPONSE (полный ответ от AI)
// ============================================================
export interface AIStrategyResponse {
/** Выбранная модель стратегии */
selectedModel: string;
/** Обоснование выбора модели */
scoringRationale: string;
/** Рекомендуемая длительность в неделях (всегда 4) */
recommendedDurationWeeks: 4;
/** Рекомендуемые платформы */
recommendedPlatforms: Platform[];
/** Обоснование выбора платформ (минимум 60 слов) */
rationale: string;
/** Еженедельные планы (ровно 4) */
weeklyPlans: WeeklyPlan[];
/** Календарь постов (ровно 16) */
postCalendar: PostCalendarItem[];
}
// ============================================================
// VALIDATION TYPES
// ============================================================
export interface ValidationResult {
/** Результат валидации */
isValid: boolean;
/** Список ошибок */
errors: ValidationError[];
/** Список предупреждений */
warnings: ValidationWarning[];
}
export interface ValidationError {
/** Тип ошибки */
type: 'STRUCTURE' | 'COUNT' | 'FORMAT' | 'CONTENT' | 'DATE';
/** Путь к проблемному полю */
field: string;
/** Сообщение об ошибке */
message: string;
}
export interface ValidationWarning {
/** Тип предупреждения */
type: 'CONTENT' | 'STYLE' | 'OPTIMIZATION';
/** Путь к проблемному полю */
field: string;
/** Сообщение о предупреждении */
message: string;
}
// ============================================================
// STRATEGY GENERATION REQUEST
// ============================================================
export interface StrategyGenerationRequest {
/** Ниша бизнеса */
niche: string;
/** Название бренда */
brand: string;
/** Город продвижения */
city: string;
/** Модель стратегии */
model: 'ENTRY' | 'AUTHORITY' | 'TRUST' | 'CONVERSION';
/** Начальная дата стратегии (по умолчанию - следующий понедельник) */
startDate?: string;
}
// ============================================================
// CONSTANTS
// ============================================================
export const STRATEGY_CONSTANTS = {
/** Общее количество постов */
TOTAL_POSTS: 16,
/** Количество постов в неделю */
POSTS_PER_WEEK: 4,
/** Количество недель */
TOTAL_WEEKS: 4,
/** Количество видео за всю стратегию */
TOTAL_VIDEOS: 4,
/** Количество фото за всю стратегию */
TOTAL_PHOTOS: 12,
/** Минимальное количество слов в посте */
MIN_POST_WORDS: 150,
/** Минимальное количество хэштегов */
MIN_HASHTAGS: 5,
/** Максимальное количество хэштегов */
MAX_HASHTAGS: 8,
/** Минимальное количество слов в рекомендациях недели */
MIN_WEEKLY_RECOMMENDATION_WORDS: 50,
/** Минимальное количество слов в rationale */
MIN_RATIONALE_WORDS: 60,
/** Допустимые времена публикации */
VALID_PUBLISH_TIMES: ['09:00', '12:00', '18:00', '20:00'] as const,
/** Паттерн дат: Пн, Ср, Пт, Сб */
WEEKLY_SCHEDULE: [
{ dayOffset: 0, time: '09:00' as const }, // Понедельник
{ dayOffset: 2, time: '12:00' as const }, // Среда
{ dayOffset: 4, time: '18:00' as const }, // Пятница
{ dayOffset: 5, time: '20:00' as const } // Суббота
]
} as const;
// ============================================================
// HELPER FUNCTIONS (TYPE GUARDS)
// ============================================================
/** Проверка: является ли значение допустимым Platform */
export function isPlatform(value: unknown): value is Platform {
return value === 'Instagram' || value === 'TikTok';
}
/** Проверка: является ли значение допустимым ContentType */
export function isContentType(value: unknown): value is ContentType {
return value === 'фото' || value === 'видео';
}
/** Проверка: является ли значение допустимым PublishTime */
export function isPublishTime(value: unknown): value is PublishTime {
return STRATEGY_CONSTANTS.VALID_PUBLISH_TIMES.includes(value as PublishTime);
}
/** Проверка: является ли значение допустимым WeekNumber */
export function isWeekNumber(value: unknown): value is WeekNumber {
return [1, 2, 3, 4].includes(value as WeekNumber);
}
+292
View File
@@ -0,0 +1,292 @@
/**
* Генератор промпта для AI-стратегии
* Создаёт профессиональный промпт на основе данных бизнеса
*/
import type { StrategyGenerationRequest } from '@/types/ai-strategy.types';
import { STRATEGY_MODELS } from '@/config/marketingModels';
/**
* Получить описание типов контента для модели стратегии
*/
function getContentTypesForModel(model: string): string {
const modelConfigs: Record<string, string> = {
ENTRY: `ENTRY-модель: фокус на узнаваемость бренда.
- Видео: короткое приветственное видео о компании, знакомство с командой
- Фото: яркие изображения продуктов/услуг, атмосферные снимки
- Акцент на эмоциях и первом впечатлении`,
AUTHORITY: `AUTHORITY-модель: позиционирование как эксперты.
- Видео: экспертные разборы, объяснения сложных тем простым языком
- Фото: инфографика, схемы, чек-листы, кейсы с цифрами
- Акцент на доказательствах компетентности`,
TRUST: `TRUST-модель: снятие недоверия через социальное доказательство.
- Видео: отзывы клиентов, процесс работы, гарантии
- Фото: реальные результаты, сертификаты, довольные клиенты
- Акцент на отзывах, кейсах, конкретных результатах`,
CONVERSION: `CONVERSION-модель: максимальная конверсия в заявки.
- Видео: демонстрация продукта в действии, до/после
- Фото: привлекательные предложения, акции, конкретные выгоды
- Акцент на призывах к действию и срочности`
};
return modelConfigs[model] || '';
}
/**
* Получить следующий понедельник от указанной даты
*/
function getNextMonday(startDate?: string): Date {
const base = startDate ? new Date(startDate) : new Date();
const dayOfWeek = base.getDay(); // 0=Вс, 1=Пн, ..., 6=Сб
// Если сегодня понедельник, берём сегодня, иначе следующий понедельник
const daysUntilMonday = dayOfWeek === 0 ? 1 : dayOfWeek === 1 ? 0 : 8 - dayOfWeek;
const nextMonday = new Date(base);
nextMonday.setDate(base.getDate() + daysUntilMonday);
nextMonday.setHours(0, 0, 0, 0);
return nextMonday;
}
/**
* Форматировать дату в ISO строку
*/
function formatDateISO(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* Сгенерировать полный промпт для AI
*/
export function generateStrategyPrompt(request: StrategyGenerationRequest): string {
const { niche, brand, city, model, startDate } = request;
const mondayDate = getNextMonday(startDate);
const modelConfig = STRATEGY_MODELS[model as keyof typeof STRATEGY_MODELS] || STRATEGY_MODELS.ENTRY;
const contentTypes = getContentTypesForModel(model);
// Генерация дат для всех 4 недель
const weekDates = [];
for (let week = 0; week < 4; week++) {
const weekStart = new Date(mondayDate);
weekStart.setDate(mondayDate.getDate() + week * 7);
weekDates.push({
monday: new Date(weekStart),
wednesday: new Date(weekStart.getTime() + 2 * 24 * 60 * 60 * 1000),
friday: new Date(weekStart.getTime() + 4 * 24 * 60 * 60 * 1000),
saturday: new Date(weekStart.getTime() + 5 * 24 * 60 * 60 * 1000)
});
}
return `Ты — профессиональный SMM-стратег с 15-летним опытом работы на рынке Казахстана.
ТВОЯ ЗАДАЧА: Создать ПРОФЕССИОНАЛЬНУЮ маркетинговую стратегию на 4 недели для бизнеса:
📌 НИША: ${niche}
📌 БРЕНД: ${brand}
📌 ГОРОД: ${city}
📌 МОДЕЛЬ: ${modelConfig.title} (${model})
════════════════════════════════════════════════════════════════
⚠️ КРИТИЧЕСКИ ВАЖНО: СТРУКТУРА И ФОРМАТ
════════════════════════════════════════════════════════════════
ОБЩЕЕ КОЛИЧЕСТВО: РОВНО 16 ПОСТОВ за 4 недели (4 поста × 4 недели)
РАСПРЕДЕЛЕНИЕ ПО НЕДЕЛЯМ — СТРОГО СОБЛЮДАТЬ:
НЕДЕЛЯ 1 (дни 1-7):
• Пост 1: ${formatDateISO(weekDates[0].monday)}T09:00:00 — понедельник
• Пост 2: ${formatDateISO(weekDates[0].wednesday)}T12:00:00 — среда
• Пост 3: ${formatDateISO(weekDates[0].friday)}T18:00:00 — пятница
• Пост 4: ${formatDateISO(weekDates[0].saturday)}T20:00:00 — суббота
НЕДЕЛЯ 2 (дни 8-14):
• Пост 5: ${formatDateISO(weekDates[1].monday)}T09:00:00 — понедельник
• Пост 6: ${formatDateISO(weekDates[1].wednesday)}T12:00:00 — среда
• Пост 7: ${formatDateISO(weekDates[1].friday)}T18:00:00 — пятница
• Пост 8: ${formatDateISO(weekDates[1].saturday)}T20:00:00 — суббота
НЕДЕЛЯ 3 (дни 15-21):
• Пост 9: ${formatDateISO(weekDates[2].monday)}T09:00:00 — понедельник
• Пост 10: ${formatDateISO(weekDates[2].wednesday)}T12:00:00 — среда
• Пост 11: ${formatDateISO(weekDates[2].friday)}T18:00:00 — пятница
• Пост 12: ${formatDateISO(weekDates[2].saturday)}T20:00:00 — суббота
НЕДЕЛЯ 4 (дни 22-28):
• Пост 13: ${formatDateISO(weekDates[3].monday)}T09:00:00 — понедельник
• Пост 14: ${formatDateISO(weekDates[3].wednesday)}T12:00:00 — среда
• Пост 15: ${formatDateISO(weekDates[3].friday)}T18:00:00 — пятница
• Пост 16: ${formatDateISO(weekDates[3].saturday)}T20:00:00 — суббота
════════════════════════════════════════════════════════════════
ПРАВИЛА СОЗДАНИЯ КОНТЕНТА
════════════════════════════════════════════════════════════════
1. КАЖДЫЙ пост должен иметь:
- publishDate: точная дата и время (формат: "2026-04-13T09:00:00")
- weekNumber: номер недели (1, 2, 3 или 4)
- platform: "Instagram" или "TikTok"
- contentType: "фото" или "видео"
- theme: КОНКРЕТНАЯ тема поста (не абстракция!)
- postText: 150+ слов на русском языке
- hashtags: 5-8 хэштегов (каждый начинается с #)
- publishTime: время публикации ("09:00", "12:00", "18:00", "20:00")
2. ВИДЕО-ПОСТЫ: РОВНО 4 видео за всю стратегию (по 1 в неделю)
- Размести видео на первый пост каждой недели (понедельник 09:00)
- Остальные 12 постов — фото
3. ТЕМЫ ПОСТОВ — КОНКРЕТНЫЕ, НЕ АБСТРАКТНЫЕ:
✅ ПРАВИЛЬНО:
"Кейс: Как мы увеличили продажи клиента на 40% за месяц"
"Процесс: За кадром нашей работы — как готовится самса"
"Отзыв: История довольного клиента из ${city}"
"Экспертный: 5 ошибок при выборе услуги"
❌ НЕПРАВИЛЬНО:
"О нашей компании"
"Введение в продукт"
"Информационный пост"
4. ТЕКСТ ПОСТА (postText) — ПРОДАЮЩИЙ КОНТЕНТ:
- Минимум 150 слов живого текста
- Первые 2 строки — КРЮЧОК, который останавливает скролл
- Конкретные цифры, факты, примеры из казахстанской реальности
- В конце — ОДИН конкретный призыв к действию (запись, звонок, директ)
- Используй тенге, ${city} и казахстанские реалии
- Язык: ТОЛЬКО русский
5. ХЭШТЕГИ:
- От 5 до 8 хэштегов на каждый пост
- Каждый хэштег начинается с ОДНОГО символа #
- ЗАПРЕЩЕНО: ##хэштег, # хэштег, английские хэштеги
- ПРАВИЛЬНО: #${niche.toLowerCase().replace(/\s+/g, '')}${city.toLowerCase()} #${city.toLowerCase()}
════════════════════════════════════════════════════════════════
ЕЖЕНЕДЕЛЬНЫЕ ПЛАНЫ (weeklyPlans)
════════════════════════════════════════════════════════════════
Для каждой недели создай план с РОВНО 4 темами:
{
"weekNumber": 1,
"mainThemes": [
"Конкретная тема 1 для недели 1",
"Конкретная тема 2 для недели 1",
"Конкретная тема 3 для недели 1",
"Конкретная тема 4 для недели 1"
],
"contentRecommendations": "Что конкретно снимать и писать на неделе 1 — минимум 50 слов",
"priorityPlatforms": ["Instagram"]
}
════════════════════════════════════════════════════════════════
ПОЛНАЯ JSON СТРУКТУРА ОТВЕТА
════════════════════════════════════════════════════════════════
{
"selectedModel": "${model}",
"scoringRationale": "Обоснование выбора модели ${modelConfig.title}",
"recommendedDurationWeeks": 4,
"recommendedPlatforms": ["Instagram", "TikTok"],
"rationale": "Минимум 60 слов — почему именно эти платформы для ниши ${niche} в городе ${city}",
"weeklyPlans": [
{
"weekNumber": 1,
"mainThemes": ["Тема 1", "Тема 2", "Тема 3", "Тема 4"],
"contentRecommendations": "Рекомендации для недели 1",
"priorityPlatforms": ["Instagram"]
},
{ "weekNumber": 2, "mainThemes": [...], "contentRecommendations": "...", "priorityPlatforms": ["Instagram"] },
{ "weekNumber": 3, "mainThemes": [...], "contentRecommendations": "...", "priorityPlatforms": ["Instagram"] },
{ "weekNumber": 4, "mainThemes": [...], "contentRecommendations": "...", "priorityPlatforms": ["Instagram"] }
],
"postCalendar": [
{
"publishDate": "${formatDateISO(weekDates[0].monday)}T09:00:00",
"weekNumber": 1,
"platform": "Instagram",
"contentType": "видео",
"theme": "Кейс: Конкретная история успеха клиента ${brand}",
"postText": "150+ слов продающего текста...",
"hashtags": ["#хэштег1", "#хэштег2", "#${city.toLowerCase()}"],
"publishTime": "09:00"
},
{
"publishDate": "${formatDateISO(weekDates[0].wednesday)}T12:00:00",
"weekNumber": 1,
"platform": "Instagram",
"contentType": "фото",
"theme": "Процесс: За кадром работы ${brand}",
"postText": "150+ слов...",
"hashtags": ["#хэштег1", "#хэштег2"],
"publishTime": "12:00"
}
// ... ещё 14 постов
]
}
════════════════════════════════════════════════════════════════
ТИПЫ КОНТЕНТА ПО МОДЕЛИ ${model}
════════════════════════════════════════════════════════════════
${contentTypes}
════════════════════════════════════════════════════════════════
ВАЖНО ДЛЯ ФРОНЕНДА:
════════════════════════════════════════════════════════════════
1. Каждый пост ДОЛЖЕН иметь поле weekNumber (1, 2, 3 или 4)
2. Посты должны быть сгруппированы по неделям
3. Темы постов должны соответствовать темам из weeklyPlans
4. Даты должны быть последовательными без пропусков >2 дней внутри недели
5. postIndex для каждого поста: 0-15
════════════════════════════════════════════════════════════════
ОТВЕТ: ТОЛЬКО JSON объект. Никакого текста вне JSON. Никаких markdown блоков \`\`\`json. Начни сразу с { и закончи }.`;
}
/**
* Извлечь JSON из ответа AI (убрать markdown, комментарии)
*/
export function extractJSONFromResponse(response: string): string {
let cleaned = response.trim();
// Убрать markdown блоки
cleaned = cleaned.replace(/^```json\s*/i, '').replace(/\s*```$/, '');
cleaned = cleaned.replace(/^```\s*/i, '').replace(/\s*```$/, '');
// Убрать комментарии в начале
cleaned = cleaned.replace(/^\/\/.*$/gm, '');
// Найти первый { и последний }
const firstBrace = cleaned.indexOf('{');
const lastBrace = cleaned.lastIndexOf('}');
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
return cleaned.substring(firstBrace, lastBrace + 1);
}
return cleaned;
}
/**
* Безопасно распарсить JSON
*/
export function safeParseJSON(jsonString: string): unknown {
try {
return JSON.parse(jsonString);
} catch (error) {
console.error('Ошибка парсинга JSON:', error);
throw new Error(`Невалидный JSON: ${(error as Error).message}`);
}
}
+587
View File
@@ -0,0 +1,587 @@
/**
* Утилита валидации AI-стратегии
* Проверяет полное соответствие промпту: 16 постов, даты, weekNumber, хэштеги, видео/фото
*/
import type {
AIStrategyResponse,
PostCalendarItem,
WeeklyPlan,
ValidationResult,
ValidationError,
ValidationWarning,
WeekNumber,
PublishTime
} from '@/types/ai-strategy.types';
import { STRATEGY_CONSTANTS } from '@/types/ai-strategy.types';
// ============================================================
// MAIN VALIDATION ENTRY POINT
// ============================================================
/**
* Полная валидация AI-стратегии
* Проверяет структуру, количество постов, даты, контент
*/
export function validateAIStrategy(response: unknown): ValidationResult {
const errors: ValidationError[] = [];
const warnings: ValidationWarning[] = [];
// 1. Проверка: это объект?
if (!response || typeof response !== 'object') {
errors.push({
type: 'STRUCTURE',
field: 'response',
message: 'Ответ AI должен быть объектом'
});
return { isValid: false, errors, warnings };
}
const strategy = response as AIStrategyResponse;
// 2. Проверка обязательных полей верхнего уровня
validateTopLevelFields(strategy, errors);
// 3. Проверка weeklyPlans
validateWeeklyPlans(strategy.weeklyPlans, errors, warnings);
// 4. Проверка postCalendar
validatePostCalendar(strategy.postCalendar, errors, warnings);
// 5. Кросс-валидация: weeklyPlans ↔ postCalendar
validateCrossReferences(strategy, errors, warnings);
return {
isValid: errors.length === 0,
errors,
warnings
};
}
// ============================================================
// TOP LEVEL FIELDS VALIDATION
// ============================================================
function validateTopLevelFields(
strategy: AIStrategyResponse,
errors: ValidationError[]
): void {
// selectedModel
if (!strategy.selectedModel || typeof strategy.selectedModel !== 'string') {
errors.push({
type: 'STRUCTURE',
field: 'selectedModel',
message: 'Поле selectedModel обязательно и должно быть строкой'
});
}
// scoringRationale
if (!strategy.scoringRationale || typeof strategy.scoringRationale !== 'string') {
errors.push({
type: 'STRUCTURE',
field: 'scoringRationale',
message: 'Поле scoringRationale обязательно и должно быть строкой'
});
}
// recommendedDurationWeeks
if (strategy.recommendedDurationWeeks !== 4) {
errors.push({
type: 'STRUCTURE',
field: 'recommendedDurationWeeks',
message: 'Рекомендуемая длительность должна быть ровно 4 недели'
});
}
// recommendedPlatforms
if (
!Array.isArray(strategy.recommendedPlatforms) ||
strategy.recommendedPlatforms.length === 0
) {
errors.push({
type: 'STRUCTURE',
field: 'recommendedPlatforms',
message: 'Поле recommendedPlatforms должно быть непустым массивом'
});
}
// rationale
if (!strategy.rationale || typeof strategy.rationale !== 'string') {
errors.push({
type: 'STRUCTURE',
field: 'rationale',
message: 'Поле rationale обязательно и должно быть строкой'
});
} else {
const rationaleWordCount = countWords(strategy.rationale);
if (rationaleWordCount < STRATEGY_CONSTANTS.MIN_RATIONALE_WORDS) {
errors.push({
type: 'CONTENT',
field: 'rationale',
message: `Rationale должен содержать минимум ${STRATEGY_CONSTANTS.MIN_RATIONALE_WORDS} слов (сейчас: ${rationaleWordCount})`
});
}
}
// weeklyPlans
if (!Array.isArray(strategy.weeklyPlans)) {
errors.push({
type: 'STRUCTURE',
field: 'weeklyPlans',
message: 'weeklyPlans должен быть массивом'
});
} else if (strategy.weeklyPlans.length !== 4) {
errors.push({
type: 'COUNT',
field: 'weeklyPlans',
message: `weeklyPlans должен содержать ровно 4 недели (сейчас: ${strategy.weeklyPlans.length})`
});
}
// postCalendar
if (!Array.isArray(strategy.postCalendar)) {
errors.push({
type: 'STRUCTURE',
field: 'postCalendar',
message: 'postCalendar должен быть массивом'
});
} else if (strategy.postCalendar.length !== STRATEGY_CONSTANTS.TOTAL_POSTS) {
errors.push({
type: 'COUNT',
field: 'postCalendar',
message: `postCalendar должен содержать ровно ${STRATEGY_CONSTANTS.TOTAL_POSTS} постов (сейчас: ${strategy.postCalendar.length})`
});
}
}
// ============================================================
// WEEKLY PLANS VALIDATION
// ============================================================
function validateWeeklyPlans(
plans: WeeklyPlan[] | undefined,
errors: ValidationError[],
warnings: ValidationWarning[]
): void {
if (!plans) return;
// Проверка: каждая неделя 1-4 представлена
const expectedWeekNumbers = [1, 2, 3, 4] as WeekNumber[];
const actualWeekNumbers = plans.map(p => p.weekNumber);
for (const weekNum of expectedWeekNumbers) {
if (!actualWeekNumbers.includes(weekNum)) {
errors.push({
type: 'COUNT',
field: `weeklyPlans[${weekNum}]`,
message: `Отсутствует план для недели ${weekNum}`
});
}
}
// Валидация каждой недели
for (let i = 0; i < plans.length; i++) {
const plan = plans[i];
const prefix = `weeklyPlans[${i}]`;
// weekNumber
if (!isWeekNumber(plan.weekNumber)) {
errors.push({
type: 'FORMAT',
field: `${prefix}.weekNumber`,
message: `weekNumber должен быть 1, 2, 3 или 4 (получено: ${plan.weekNumber})`
});
}
// mainThemes (ровно 4)
if (!Array.isArray(plan.mainThemes)) {
errors.push({
type: 'STRUCTURE',
field: `${prefix}.mainThemes`,
message: 'mainThemes должен быть массивом'
});
} else if (plan.mainThemes.length !== 4) {
errors.push({
type: 'COUNT',
field: `${prefix}.mainThemes`,
message: `mainThemes должен содержать ровно 4 темы (сейчас: ${plan.mainThemes.length})`
});
}
// contentRecommendations
if (!plan.contentRecommendations || typeof plan.contentRecommendations !== 'string') {
errors.push({
type: 'STRUCTURE',
field: `${prefix}.contentRecommendations`,
message: 'contentRecommendations обязателен и должен быть строкой'
});
} else {
const recWordCount = countWords(plan.contentRecommendations);
if (recWordCount < STRATEGY_CONSTANTS.MIN_WEEKLY_RECOMMENDATION_WORDS) {
errors.push({
type: 'CONTENT',
field: `${prefix}.contentRecommendations`,
message: `contentRecommendations должен содержать минимум ${STRATEGY_CONSTANTS.MIN_WEEKLY_RECOMMENDATION_WORDS} слов (сейчас: ${recWordCount})`
});
}
}
// priorityPlatforms
if (!Array.isArray(plan.priorityPlatforms) || plan.priorityPlatforms.length === 0) {
errors.push({
type: 'STRUCTURE',
field: `${prefix}.priorityPlatforms`,
message: 'priorityPlatforms должен быть непустым массивом'
});
}
}
}
// ============================================================
// POST CALENDAR VALIDATION
// ============================================================
function validatePostCalendar(
posts: PostCalendarItem[] | undefined,
errors: ValidationError[],
warnings: ValidationWarning[]
): void {
if (!posts) return;
let totalVideos = 0;
let totalPhotos = 0;
for (let i = 0; i < posts.length; i++) {
const post = posts[i];
const prefix = `postCalendar[${i}]`;
// publishDate
if (!post.publishDate || typeof post.publishDate !== 'string') {
errors.push({
type: 'STRUCTURE',
field: `${prefix}.publishDate`,
message: 'publishDate обязателен и должен быть строкой'
});
} else {
const date = new Date(post.publishDate);
if (isNaN(date.getTime())) {
errors.push({
type: 'DATE',
field: `${prefix}.publishDate`,
message: `Невалидный формат даты: ${post.publishDate}. Ожидается ISO формат (2026-04-13T09:00:00)`
});
}
}
// weekNumber
if (!isWeekNumber(post.weekNumber)) {
errors.push({
type: 'FORMAT',
field: `${prefix}.weekNumber`,
message: `weekNumber должен быть 1, 2, 3 или 4 (получено: ${post.weekNumber})`
});
}
// platform
if (post.platform !== 'Instagram' && post.platform !== 'TikTok') {
errors.push({
type: 'FORMAT',
field: `${prefix}.platform`,
message: `platform должен быть "Instagram" или "TikTok" (получено: ${post.platform})`
});
}
// contentType
if (post.contentType !== 'фото' && post.contentType !== 'видео') {
errors.push({
type: 'FORMAT',
field: `${prefix}.contentType`,
message: `contentType должен быть "фото" или "видео" (получено: ${post.contentType})`
});
} else {
if (post.contentType === 'видео') totalVideos++;
else totalPhotos++;
}
// theme
if (!post.theme || typeof post.theme !== 'string' || post.theme.trim().length === 0) {
errors.push({
type: 'CONTENT',
field: `${prefix}.theme`,
message: 'theme обязателен и не может быть пустым'
});
} else {
// Проверка: тема не абстрактная
const abstractThemes = ['о компании', 'введение', 'информационный', 'пост'];
const lowerTheme = post.theme.toLowerCase();
for (const abstract of abstractThemes) {
if (lowerTheme.includes(abstract) && post.theme.split(':').length <= 1) {
warnings.push({
type: 'CONTENT',
field: `${prefix}.theme`,
message: `Тема "${post.theme}" выглядит абстрактной. Используйте формат "Тип: Конкретика" (например "Кейс: Как мы увеличили продажи на 40%")`
});
break;
}
}
}
// postText (минимум 150 слов)
if (!post.postText || typeof post.postText !== 'string') {
errors.push({
type: 'STRUCTURE',
field: `${prefix}.postText`,
message: 'postText обязателен и должен быть строкой'
});
} else {
const wordCount = countWords(post.postText);
if (wordCount < STRATEGY_CONSTANTS.MIN_POST_WORDS) {
errors.push({
type: 'CONTENT',
field: `${prefix}.postText`,
message: `postText должен содержать минимум ${STRATEGY_CONSTANTS.MIN_POST_WORDS} слов (сейчас: ${wordCount})`
});
}
}
// hashtags
if (!Array.isArray(post.hashtags)) {
errors.push({
type: 'STRUCTURE',
field: `${prefix}.hashtags`,
message: 'hashtags должен быть массивом'
});
} else {
if (
post.hashtags.length < STRATEGY_CONSTANTS.MIN_HASHTAGS ||
post.hashtags.length > STRATEGY_CONSTANTS.MAX_HASHTAGS
) {
errors.push({
type: 'COUNT',
field: `${prefix}.hashtags`,
message: `hashtags должен содержать от ${STRATEGY_CONSTANTS.MIN_HASHTAGS} до ${STRATEGY_CONSTANTS.MAX_HASHTAGS} элементов (сейчас: ${post.hashtags.length})`
});
}
// Проверка формата каждого хэштега
for (let h = 0; h < post.hashtags.length; h++) {
const hashtag = post.hashtags[h];
if (!hashtag.startsWith('#')) {
errors.push({
type: 'FORMAT',
field: `${prefix}.hashtags[${h}]`,
message: `Хэштег должен начинаться с #: "${hashtag}"`
});
}
if (hashtag.startsWith('##')) {
errors.push({
type: 'FORMAT',
field: `${prefix}.hashtags[${h}]`,
message: `Хэштег не должен начинаться с ##: "${hashtag}"`
});
}
if (hashtag.includes(' ')) {
errors.push({
type: 'FORMAT',
field: `${prefix}.hashtags[${h}]`,
message: `Хэштег не должен содержать пробелы: "${hashtag}"`
});
}
}
}
// publishTime
const validTimes = STRATEGY_CONSTANTS.VALID_PUBLISH_TIMES as readonly string[];
if (!validTimes.includes(post.publishTime)) {
errors.push({
type: 'FORMAT',
field: `${prefix}.publishTime`,
message: `publishTime должен быть одним из: ${validTimes.join(', ')} (получено: ${post.publishTime})`
});
}
}
// Проверка общего количества видео/фото
if (totalVideos !== STRATEGY_CONSTANTS.TOTAL_VIDEOS) {
errors.push({
type: 'COUNT',
field: 'postCalendar.totalVideos',
message: `Стратегия должна содержать ровно ${STRATEGY_CONSTANTS.TOTAL_VIDEOS} видео-поста (сейчас: ${totalVideos})`
});
}
if (totalPhotos !== STRATEGY_CONSTANTS.TOTAL_PHOTOS) {
errors.push({
type: 'COUNT',
field: 'postCalendar.totalPhotos',
message: `Стратегия должна содержать ровно ${STRATEGY_CONSTANTS.TOTAL_PHOTOS} фото-поста (сейчас: ${totalPhotos})`
});
}
}
// ============================================================
// CROSS-REFERENCE VALIDATION
// ============================================================
function validateCrossReferences(
strategy: AIStrategyResponse,
errors: ValidationError[],
warnings: ValidationWarning[]
): void {
const { weeklyPlans, postCalendar } = strategy;
if (!weeklyPlans || !postCalendar) return;
// 1. Проверка: посты каждой недели соответствуют plan.mainThemes
for (let weekNum = 1; weekNum <= 4; weekNum++) {
const plan = weeklyPlans.find(p => p.weekNumber === weekNum);
const weekPosts = postCalendar.filter(p => p.weekNumber === weekNum);
if (!plan) continue;
if (weekPosts.length !== STRATEGY_CONSTANTS.POSTS_PER_WEEK) {
errors.push({
type: 'COUNT',
field: `week${weekNum}.posts`,
message: `Неделя ${weekNum} должна содержать ровно ${STRATEGY_CONSTANTS.POSTS_PER_WEEK} поста (сейчас: ${weekPosts.length})`
});
}
// Проверка: первый пост недели — понедельник 09:00 с видео
const firstPost = weekPosts[0];
if (firstPost) {
if (firstPost.contentType !== 'видео') {
errors.push({
type: 'CONTENT',
field: `postCalendar.week${weekNum}.firstPost`,
message: `Первый пост недели ${weekNum} должен быть видео-типом (понедельник 09:00)`
});
}
if (firstPost.publishTime !== '09:00') {
errors.push({
type: 'FORMAT',
field: `postCalendar.week${weekNum}.firstPost.publishTime`,
message: `Первый пост недели ${weekNum} должен иметь publishTime "09:00" (сейчас: ${firstPost.publishTime})`
});
}
}
// Проверка: остальные посты недели — фото
for (let i = 1; i < weekPosts.length; i++) {
if (weekPosts[i].contentType !== 'фото') {
warnings.push({
type: 'CONTENT',
field: `postCalendar.week${weekNum}.post[${i}]`,
message: `Пост ${i + 1} недели ${weekNum} должен быть фото-типом (не видео)`
});
}
}
// Проверка: даты последовательны (Пн, Ср, Пт, Сб)
validateWeeklySchedule(weekPosts, weekNum, errors);
}
}
// ============================================================
// WEEKLY SCHEDULE VALIDATION
// ============================================================
function validateWeeklySchedule(
weekPosts: PostCalendarItem[],
weekNum: number,
errors: ValidationError[]
): void {
if (weekPosts.length !== 4) return;
const expectedSchedule = [
{ dayOfWeek: 1, time: '09:00' }, // Понедельник
{ dayOfWeek: 3, time: '12:00' }, // Среда
{ dayOfWeek: 5, time: '18:00' }, // Пятница
{ dayOfWeek: 6, time: '20:00' } // Суббота
];
for (let i = 0; i < weekPosts.length; i++) {
const post = weekPosts[i];
const expected = expectedSchedule[i];
const date = new Date(post.publishDate);
if (isNaN(date.getTime())) continue;
const actualDayOfWeek = date.getDay(); // 0=Вс, 1=Пн, ..., 6=Сб
const actualTime = post.publishTime;
if (actualDayOfWeek !== expected.dayOfWeek) {
errors.push({
type: 'DATE',
field: `postCalendar.week${weekNum}.post[${i}].publishDate`,
message: `Пост ${i + 1} недели ${weekNum} должен быть в ${getDayName(expected.dayOfWeek)} (получено: ${getDayName(actualDayOfWeek)}, ${post.publishDate})`
});
}
if (actualTime !== expected.time) {
errors.push({
type: 'FORMAT',
field: `postCalendar.week${weekNum}.post[${i}].publishTime`,
message: `Пост ${i + 1} недели ${weekNum} должен иметь время "${expected.time}" (сейчас: "${actualTime}")`
});
}
}
}
// ============================================================
// HELPER FUNCTIONS
// ============================================================
/** Подсчёт слов в тексте */
function countWords(text: string): number {
return text
.trim()
.split(/\s+/)
.filter(word => word.length > 0)
.length;
}
/** Проверка: валидный ли weekNumber */
function isWeekNumber(value: unknown): value is WeekNumber {
return [1, 2, 3, 4].includes(value as WeekNumber);
}
/** Получить название дня недели */
function getDayName(day: number): string {
const names = ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота'];
return names[day] || 'Неизвестно';
}
/** Получить читаемое описание ошибки */
export function formatValidationError(error: ValidationError): string {
return `[${error.type}] ${error.field}: ${error.message}`;
}
/** Получить читаемое описание предупреждения */
export function formatValidationWarning(warning: ValidationWarning): string {
return `[${warning.type}] ${warning.field}: ${warning.message}`;
}
/** Получить сводку валидации для логирования */
export function getValidationSummary(result: ValidationResult): string {
const lines = [
`Валидация: ${result.isValid ? '✅ ПРОЙДЕНА' : '❌ ПРОВАЛЕНА'}`,
`Ошибок: ${result.errors.length}`,
`Предупреждений: ${result.warnings.length}`
];
if (result.errors.length > 0) {
lines.push('\nОшибки:');
for (const error of result.errors) {
lines.push(`${formatValidationError(error)}`);
}
}
if (result.warnings.length > 0) {
lines.push('\nПредупреждения:');
for (const warning of result.warnings) {
lines.push(`${formatValidationWarning(warning)}`);
}
}
return lines.join('\n');
}
@@ -0,0 +1,16 @@
<template>
<div class="ai-strategy-page">
<AIStrategyGenerator />
</div>
</template>
<script setup lang="ts">
import AIStrategyGenerator from '@/components/marketing/AIStrategyGenerator.vue';
</script>
<style scoped>
.ai-strategy-page {
min-height: 100vh;
background: var(--surface-ground);
}
</style>
@@ -0,0 +1,326 @@
# 🎯 Промпт для AI-генерации маркетинговой стратегии
> **Версия:** 1.0
> **Дата:** 11 апреля 2026
> **Назначение:** Профессиональная генерация SMM-стратегии для бизнеса в Казахстане
---
## 📋 Системный промпт
```
Ты — профессиональный SMM-стратег с 15-летним опытом работы на рынке Казахстана.
ТВОЯ ЗАДАЧА: Создать ПРОФЕССИОНАЛЬНУЮ маркетинговую стратегию на 4 недели
для бизнеса в нише "[НИША]" с брендом "[БРЕНД]" в городе "[ГОРОД]".
```
---
## ⚠️ Критически важно: Структура и формат
### 📊 Общее количество постов
| Параметр | Значение |
|----------|----------|
| **Всего постов** | РОВНО 16 |
| **Период** | 4 недели |
| **Постов в неделю** | 4 |
| **Дней публикации** | Пн, Ср, Пт, Сб |
### 📅 Распределение по неделям
#### Неделя 1 (дни 1-7)
| Пост | День | День недели | Время |
|------|------|-------------|-------|
| Пост 1 | День 1 | Понедельник | 09:00 |
| Пост 2 | День 3 | Среда | 12:00 |
| Пост 3 | День 5 | Пятница | 18:00 |
| Пост 4 | День 6 | Суббота | 20:00 |
#### Неделя 2 (дни 8-14)
| Пост | День | День недели | Время |
|------|------|-------------|-------|
| Пост 5 | День 8 | Понедельник | 09:00 |
| Пост 6 | День 10 | Среда | 12:00 |
| Пост 7 | День 12 | Пятница | 18:00 |
| Пост 8 | День 13 | Суббота | 20:00 |
#### Неделя 3 (дни 15-21)
| Пост | День | День недели | Время |
|------|------|-------------|-------|
| Пост 9 | День 15 | Понедельник | 09:00 |
| Пост 10 | День 17 | Среда | 12:00 |
| Пост 11 | День 19 | Пятница | 18:00 |
| Пост 12 | День 20 | Суббота | 20:00 |
#### Неделя 4 (дни 22-28)
| Пост | День | День недели | Время |
|------|------|-------------|-------|
| Пост 13 | День 22 | Понедельник | 09:00 |
| Пост 14 | День 24 | Среда | 12:00 |
| Пост 15 | День 26 | Пятница | 18:00 |
| Пост 16 | День 27 | Суббота | 20:00 |
---
## ✍️ Правила создания контента
### 1️⃣ Обязательные поля каждого поста
Каждый пост **ДОЛЖЕН** содержать следующие поля:
| Поле | Тип | Описание | Пример |
|------|-----|----------|--------|
| `publishDate` | `string` | Точная дата и время (ISO 8601) | `"2026-04-13T09:00:00"` |
| `weekNumber` | `number` | Номер недели (1-4) | `1` |
| `platform` | `string` | Социальная сеть | `"Instagram"`, `"TikTok"` |
| `contentType` | `string` | Тип контента | `"фото"`, `"видео"` |
| `theme` | `string` | Конкретная тема поста | `"Кейс: Как мы увеличили продажи..."` |
| `postText` | `string` | Текст поста (150+ слов) | `"Откройте секрет..."` |
| `hashtags` | `string[]` | Хэштеги (5-8 шт) | `["#алматы", "#бизнес"]` |
| `publishTime` | `string` | Время публикации | `"09:00"` |
---
### 2️⃣ Видео-посты
> 🎬 **Правило:** РОВНО 4 видео за всю стратегию (по 1 в неделю)
- ✅ Размещай видео на **первый пост каждой недели** (понедельник 09:00)
- ✅ Остальные 12 постов — **фото**
---
### 3️⃣ Темы постов — конкретные, не абстрактные
#### ✅ Правильно
- `Кейс: Как мы увеличили продажи клиента на 40% за месяц`
- `Процесс: За кадром нашей работы — как готовится самса`
- `Отзыв: История довольного клиента из Алматы`
- `Экспертный: 5 ошибок при выборе услуги`
#### ❌ Неправильно
- `О нашей компании`
- `Введение в продукт`
- `Информационный пост`
---
### 4️⃣ Текст поста (postText) — продающий контент
**Структура текста:**
1. 🪝 **Первые 2 строки** — КРЮЧОК, который останавливает скролл
2. 📊 **Основная часть** — конкретные цифры, факты, примеры из казахстанской реальности
3. 📣 **Призыв к действию** — ОДИН конкретный CTA (запись, звонок, директ)
**Требования:**
- Минимум **150 слов** живого текста
- Используй **тенге**, алматинские районы, казахстанские реалии
- Язык: **ТОЛЬКО русский**
---
### 5️⃣ Хэштеги
| Параметр | Требование |
|----------|------------|
| **Количество** | 5-8 хэштегов на пост |
| **Формат** | Каждый начинается с ОДНОГО `#` |
#### ❌ Запрещено
- `##хэштег` (двойной символ)
- `# хэштег` (пробел после #)
- `#almatyfood` (английские хэштеги)
#### ✅ Правильно
```
#тортыалматы #выпечкаказахстан #алматы #сладкийстол #тортыназаказ
```
---
## 📆 Еженедельные планы (weeklyPlans)
Для каждой недели создай план с **РОВНО 4 темами**:
```json
{
"weekNumber": 1,
"mainThemes": [
"Конкретная тема 1 для недели 1",
"Конкретная тема 2 для недели 1",
"Конкретная тема 3 для недели 1",
"Конкретная тема 4 для недели 1"
],
"contentRecommendations": "Что конкретно снимать и писать на неделе 1 — минимум 50 слов",
"priorityPlatforms": ["Instagram"]
}
```
---
## 📦 Полная JSON структура ответа
```json
{
"selectedModel": "[МОДЕЛЬ]",
"scoringRationale": "Обоснование выбора модели",
"recommendedDurationWeeks": 4,
"recommendedPlatforms": ["Instagram", "TikTok"],
"rationale": "Минимум 60 слов — почему именно эти платформы для данной ниши",
"weeklyPlans": [
{
"weekNumber": 1,
"mainThemes": ["Тема 1", "Тема 2", "Тема 3", "Тема 4"],
"contentRecommendations": "Рекомендации для недели 1",
"priorityPlatforms": ["Instagram"]
},
{ "weekNumber": 2, "mainThemes": [], "contentRecommendations": "", "priorityPlatforms": [] },
{ "weekNumber": 3, "mainThemes": [], "contentRecommendations": "", "priorityPlatforms": [] },
{ "weekNumber": 4, "mainThemes": [], "contentRecommendations": "", "priorityPlatforms": [] }
],
"postCalendar": [
{
"publishDate": "2026-04-13T09:00:00",
"weekNumber": 1,
"platform": "Instagram",
"contentType": "видео",
"theme": "Кейс: Конкретная история успеха клиента",
"postText": "150+ слов продающего текста...",
"hashtags": ["#хэштег1", "#хэштег2", "#алматы"],
"publishTime": "09:00"
},
{
"publishDate": "2026-04-15T12:00:00",
"weekNumber": 1,
"platform": "Instagram",
"contentType": "фото",
"theme": "Процесс: За кадром работы",
"postText": "150+ слов...",
"hashtags": ["#хэштег1", "#хэштег2"],
"publishTime": "12:00"
}
]
}
```
---
## 📝 Пример правильного распределения (первые 8 постов)
```json
{
"postCalendar": [
{ "publishDate": "2026-04-13T09:00:00", "weekNumber": 1, "contentType": "видео", "theme": "Кейс: ..." },
{ "publishDate": "2026-04-15T12:00:00", "weekNumber": 1, "contentType": "фото", "theme": "Процесс: ..." },
{ "publishDate": "2026-04-17T18:00:00", "weekNumber": 1, "contentType": "фото", "theme": "Отзыв: ..." },
{ "publishDate": "2026-04-18T20:00:00", "weekNumber": 1, "contentType": "фото", "theme": "Экспертный: ..." },
{ "publishDate": "2026-04-20T09:00:00", "weekNumber": 2, "contentType": "видео", "theme": "Кейс: ..." },
{ "publishDate": "2026-04-22T12:00:00", "weekNumber": 2, "contentType": "фото", "theme": "Процесс: ..." },
{ "publishDate": "2026-04-24T18:00:00", "weekNumber": 2, "contentType": "фото", "theme": "Отзыв: ..." },
{ "publishDate": "2026-04-25T20:00:00", "weekNumber": 2, "contentType": "фото", "theme": "Продающий: ..." }
]
}
```
---
## 🔗 Типы контента по модели
> ⚙️ **Здесь укажи типы контента в зависимости от выбранной модели AI**
| Модель | Типы контента |
|--------|---------------|
| GPT-4 | Видео-кейсы, экспертные посты, отзывы |
| GPT-3.5 | Фото-процессы, продающие посты, хэштеги |
| Claude | Глубокие кейсы, аналитика, сравнения |
---
## 🎨 Важно для фронтенда
### Критические требования
1. ✅ Каждый пост **ДОЛЖЕН** иметь поле `weekNumber` (1, 2, 3 или 4)
2. ✅ Посты должны быть **сгруппированы по неделям**
3. ✅ Темы постов должны **соответствовать** темам из `weeklyPlans`
4. ✅ Даты должны быть **последовательными** без пропусков >2 дней внутри недели
### TypeScript интерфейс
```typescript
interface PostCalendarItem {
postIndex: number;
publishDate: string; // "2026-04-13T09:00:00"
weekNumber: number; // 1, 2, 3 или 4
platform: string; // "Instagram" | "TikTok"
contentType: string; // "фото" | "видео"
theme: string; // Конкретная тема
postText: string; // 150+ слов
hashtags: string[]; // ["#тортыалматы", ...]
publishTime: string; // "09:00" | "12:00" | "18:00" | "20:00"
imageUrl?: string;
videoUrl?: string;
taskStatus?: string;
}
```
---
## 🚀 Как использовать
### Шаг 1: Подставь реальные данные
| Переменная | Описание | Пример |
|------------|----------|--------|
| `[НИША]` | Ниша бизнеса | `"выпечка тортов на заказ"` |
| `[БРЕНД]` | Название бренда | `"Сладкий Мир"` |
| `[ГОРОД]` | Город | `"Алматы"` |
| `[МОДЕЛЬ]` | AI модель | `"GPT-4"` |
### Шаг 2: Вставь промпт в систему генерации
```
Скопируй весь текст промпта, замени переменные и отправь в AI
```
### Шаг 3: Получи JSON с профессиональной стратегией
```
AI вернёт валидный JSON объект с готовой маркетинговой стратегией
```
---
## ✅ Преимущества этого промпта
| Преимущество | Описание |
|--------------|----------|
| 📐 **Чёткая структура** | 16 постов, 4/неделю, Пн-Ср-Пт-Сб |
| 🎨 **Красивый фронтенд** | Поле `weekNumber` для группировки |
| 🎯 **Профессионализм** | Конкретные темы, не абстракции |
| ⏰ **Предсказуемость** | Фиксированные времена публикации |
| ✔️ **Валидация** | AI не сможет вернуть меньше 16 постов |
---
## 📞 Поддержка
> При возникновении вопросов по использованию промпта обращайтесь к документации по API генерации отчётов.
---
*📄 Документ создан: 11 апреля 2026*
*🔄 Версия: 1.0*