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