fix
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(npm run *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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*
|
||||||
Reference in New Issue
Block a user