+
+
+
-
-
-
-
-
-
-
-
-
-
Сделать анализ
-
Опишите свой бизнес. Нейросеть соберёт данные по конкурентам, ЦА и предложит лучшее позиционирование.
+
+
+
+
+
+
+
+
Сделать анализ
+
Опишите бизнес — ИИ соберёт данные по конкурентам, ЦА и предложит лучшее позиционирование
+
-
-
-
-
-
-
-
-
Готовые анализы
-
Библиотека ваших предыдущих исследований с развернутой аналитикой и инсайтами.
+
+
+
+
+
Готовые анализы
+
Библиотека ваших исследований с развёрнутой аналитикой, инсайтами и экспортом
+
-
-
-
-
-
-
-
-
-
-
Сгенерировать стратегию
-
- Контент-фабрика. ИИ выберет соцсети, напишет посты и отрисует картинки на основе вашего анализа.
- Требуется готовый анализ
-
+
+
+
+
+
+
Сгенерировать стратегию
+
Контент-фабрика. ИИ выберет соцсети, напишет посты и отрисует картинки на основе анализа
+
Требуется анализ
-
-
-
-
-
-
-
-
Мои стратегии
-
Список сгенерированных контент-планов, медиа-активов и AI-обоснований.
+
+
+
+
+
Мои стратегии
+
Список сгенерированных контент-планов, медиа-активов и AI-обоснований
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Beta API
+
+
ИИ-Таргетолог
+
Развёртывание рекламных кампаний — ИИ создаст аудитории, оптимизирует бюджет и запустит рекламу в Facebook, Instagram и TikTok
+
+
+
+ Открыть кабинет
+
+
-
+
diff --git a/src/views/pages/marketing/MarketingPromotion.vue b/src/views/pages/marketing/MarketingPromotion.vue
index 593deb6..019257a 100644
--- a/src/views/pages/marketing/MarketingPromotion.vue
+++ b/src/views/pages/marketing/MarketingPromotion.vue
@@ -1,5 +1,5 @@
-
-
+
@@ -779,6 +864,18 @@ onUnmounted(() => {
+
+
+
+
+
+
+
@@ -996,8 +1093,8 @@ onUnmounted(() => {
-
-
+
+
{
@click="handlePublishNow(post)"
/>
+
+
@@ -1017,20 +1123,20 @@ onUnmounted(() => {
-
+
- Запустить стратегию в работу
+ Запустить таргет по новой логике
- Автопостинг уже активен
+ Переходим к запуску таргета
@@ -1080,39 +1186,91 @@ onUnmounted(() => {
diff --git a/src/views/pages/marketing/MarketingStrategyV3List.vue b/src/views/pages/marketing/MarketingStrategyV3List.vue
index 7a87e30..8c79ba4 100644
--- a/src/views/pages/marketing/MarketingStrategyV3List.vue
+++ b/src/views/pages/marketing/MarketingStrategyV3List.vue
@@ -23,7 +23,6 @@ const STATUS_FILTERS = [
{ label: 'Ошибка', value: 'FAILED' }
];
-// Load
const load = async () => {
loading.value = true;
try {
@@ -36,13 +35,12 @@ const load = async () => {
}
};
-// Filtered & Paginated
const filtered = computed(() => {
let list = strategies.value;
- if (filterStatus.value) list = list.filter((a) => a.status === filterStatus.value);
+ if (filterStatus.value) list = list.filter(a => a.status === filterStatus.value);
if (searchQuery.value.trim()) {
const q = searchQuery.value.trim().toLowerCase();
- list = list.filter((a) => {
+ list = list.filter(a => {
const niche = (a.analysisData?.requestData?.businessNiche || '').toLowerCase();
const id = (a.strategyId || a.id || '').toLowerCase();
return niche.includes(q) || id.includes(q);
@@ -55,64 +53,52 @@ const paginated = computed(() => {
return filtered.value.slice(start, start + pageSize);
});
-const countByStatus = (s) => strategies.value.filter((a) => a.status === s).length;
+const countByStatus = s => strategies.value.filter(a => a.status === s).length;
+const formatDate = dt => dt ? new Date(dt).toLocaleString('ru-RU', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—';
+const statusLabel = s => ({ COMPLETED: 'Завершена', PROCESSING: 'В работе', QUEUED: 'В очереди', FAILED: 'Ошибка' })[(s||'').toUpperCase()] || s;
-// Helpers
-const formatDate = (dt) => (dt ? new Date(dt).toLocaleString('ru-RU', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—');
+const statusStyle = s => ({
+ COMPLETED: { color: 'var(--kai-green)', bg: 'rgba(0,212,139,0.1)', border: 'rgba(0,212,139,0.25)' },
+ PROCESSING: { color: 'var(--kai-blue)', bg: 'rgba(45,123,255,0.1)', border: 'rgba(45,123,255,0.25)' },
+ QUEUED: { color: 'var(--kai-yellow)', bg: 'rgba(245,158,11,0.1)', border: 'rgba(245,158,11,0.25)' },
+ FAILED: { color: 'var(--kai-red)', bg: 'rgba(239,68,68,0.1)', border: 'rgba(239,68,68,0.25)' }
+})[(s||'').toUpperCase()] || { color: 'var(--kai-txt3)', bg: 'rgba(255,255,255,0.05)', border: 'rgba(255,255,255,0.1)' };
-const statusLabel = (s) => ({ COMPLETED: 'Завершена', PROCESSING: 'В работе', QUEUED: 'В очереди', FAILED: 'Ошибка' })[(s || '').toUpperCase()] || s;
+const statusIcon = s => ({
+ COMPLETED: 'pi pi-check-circle',
+ PROCESSING: 'pi pi-spin pi-spinner',
+ QUEUED: 'pi pi-clock',
+ FAILED: 'pi pi-times-circle'
+})[(s||'').toUpperCase()] || 'pi pi-circle';
-const statusBg = (s) =>
- ({
- COMPLETED: 'bg-emerald-50 dark:bg-emerald-900/20',
- PROCESSING: 'bg-blue-50 dark:bg-blue-900/20',
- QUEUED: 'bg-amber-50 dark:bg-amber-900/20',
- FAILED: 'bg-red-50 dark:bg-red-900/20'
- })[(s || '').toUpperCase()] || 'bg-surface-100 dark:bg-surface-800';
-
-const statusIcon = (s) =>
- ({
- COMPLETED: 'pi pi-check-circle text-emerald-500',
- PROCESSING: 'pi pi-spin pi-spinner text-blue-500',
- QUEUED: 'pi pi-clock text-amber-500',
- FAILED: 'pi pi-times-circle text-red-500'
- })[(s || '').toUpperCase()] || 'pi pi-circle text-surface-400';
-
-const statusTagClass = (s) =>
- ({
- COMPLETED: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
- PROCESSING: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
- QUEUED: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
- FAILED: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
- })[(s || '').toUpperCase()] || 'bg-surface-200 text-surface-600 dark:bg-surface-700 dark:text-surface-300';
-
-const getPlatformIcon = (platform) => {
- const p = platform?.toLowerCase() || '';
- if (p.includes('insta')) return 'pi pi-instagram';
- if (p.includes('tik')) return 'pi pi-tiktok';
- if (p.includes('you')) return 'pi pi-youtube';
- if (p.includes('tg') || p.includes('tele')) return 'pi pi-telegram';
- if (p.includes('link')) return 'pi pi-linkedin';
- if (p.includes('vk')) return 'pi pi-vk';
+const getPlatformIcon = p => {
+ const pl = (p||'').toLowerCase();
+ if (pl.includes('insta')) return 'pi pi-instagram';
+ if (pl.includes('tik')) return 'pi pi-tiktok';
+ if (pl.includes('you')) return 'pi pi-youtube';
+ if (pl.includes('tg') || pl.includes('tele')) return 'pi pi-telegram';
return 'pi pi-share-alt';
};
-const getModelInfo = (modelName) => {
+const getModelInfo = modelName => {
if (!modelName) return DEFAULT_STRATEGY_MODEL;
return STRATEGY_MODELS[modelName.toUpperCase()] || DEFAULT_STRATEGY_MODEL;
};
-const getLatestStatusMessage = (item) => {
- if (item.statusHistory && item.statusHistory.length > 0) {
- return item.statusHistory[item.statusHistory.length - 1].message;
- }
+const getStrategyName = item =>
+ item.analysisTitle ||
+ (item.analysisData?.requestData?.productName
+ ? `${item.analysisData.requestData.productName} — ${item.analysisData.requestData.businessNiche}`
+ : (item.analysisData?.requestData?.businessNiche || 'Стратегия #' + (item.strategyId || item.id || '').slice(-6)));
+
+const getLatestMsg = item => {
+ if (item.statusHistory?.length > 0) return item.statusHistory[item.statusHistory.length - 1].message;
return item.status === 'QUEUED' ? 'В очереди...' : 'Фабрика генерирует контент...';
};
-// Actions
-const openStrategy = (item) => {
+const openStrategy = item => {
const s = (item.status || '').toUpperCase();
- if (s === 'COMPLETED' || s === 'PROCESSING' || s === 'QUEUED') {
+ if (['COMPLETED', 'PROCESSING', 'QUEUED'].includes(s)) {
const id = item.strategyId || item.id;
router.push(`/marketing-analysis/v3/strategy?strategyId=${id}&analysisId=${item.analysisId}`);
}
@@ -122,172 +108,223 @@ onMounted(load);
-
+
-
-
-
-
-
- CONTENT FACTORY V3
-
-
Мои стратегии
-
Сгенерированные контент-планы и медиа-активы
-
-
-
+
-
-
-
-
-
-
-
-
-
- {{ f.label }}
- ({{ countByStatus(f.value) }})
+
+
+
+
+
+
+ {{ countByStatus(f.value) }}
+ {{ f.label }}
+
+
+ {{ strategies.length }}
+ Всего
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ {{ f.label }}
+ {{ countByStatus(f.value) }}
+
+
+
+
+
-
-
-
-
-
+
+
-
- {{ filterStatus ? 'Нет стратегий с таким статусом' : 'Стратегий пока нет' }}
-
-
Сгенерируйте первую стратегию ИИ-маркетологом.
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
{{ filterStatus ? 'Нет стратегий с таким статусом' : 'Стратегий пока нет' }}
+
Сгенерируйте первую стратегию ИИ-маркетологом на основе готового анализа.
+
+ К анализам
+
+
-
-
-
-
-
-
- {{ statusLabel(item.status) }}
-
-
-
- {{ getModelInfo(item.scoringModelName).icon }}
- {{ getModelInfo(item.scoringModelName).badge }}
-
-
-
-
-
{{ item.durationWeeks || item.strategyData?.durationWeeks }} недель
+
+
+
-
- Платформы:
-
- {{ p }}
-
+
+
+
+
+
+
+
+
+
{{ getStrategyName(item) }}
+
+
+ {{ statusLabel(item.status) }}
+
+
+ {{ getModelInfo(item.scoringModelName).icon }} {{ getModelInfo(item.scoringModelName).badge }}
- Создана: {{ formatDate(item.createdAt) }}
-
-
-
+
+
+ {{ item.durationWeeks || item.strategyData?.durationWeeks }} нед.
+
+
+
+
+ {{ p }}
+
+
+ {{ formatDate(item.createdAt) }}
+
+
+
+
+
{{ getLatestMsg(item) }}
+
-
-
-
- {{ getLatestStatusMessage(item) }}
-
-
+
+
+
+
+
-
-
-
Показано {{ (currentPage - 1) * pageSize + 1 }}–{{ Math.min(currentPage * pageSize, filtered.length) }} из {{ filtered.length }}
-
+
+
+
diff --git a/src/views/pages/marketing/targetologist/CampaignInsightsDetail.vue b/src/views/pages/marketing/targetologist/CampaignInsightsDetail.vue
new file mode 100644
index 0000000..500c241
--- /dev/null
+++ b/src/views/pages/marketing/targetologist/CampaignInsightsDetail.vue
@@ -0,0 +1,575 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Кампания успешно запущена {{ formatDateTime(launchTimestamp) }}
+
+
+
+
+
+
Общий бюджет
+
{{ formatKzt(budget.totalBudget.value) }}
+
+
+
Дневной расход
+
{{ formatKzt(budget.dailyBudget.value) }}
+
+
+
Длительность
+
{{ budget.campaignDays.value ?? '—' }} дн.
+
+
+
Целевой охват
+
{{ formatLargeNumber(budget.targetReach.value) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Профиль аудитории
+
+
Сегмент {{ audienceProfile?.primarySegment || '—' }}
+
+
Возраст {{ audienceProfile?.ageRange || '—' }}
+
Гендер {{ audienceProfile?.gender || '—' }}
+
+
География {{ audienceProfile?.geography ? formatGeo(audienceProfile.geography) : '—' }}
+
Интересы {{ formatArrayField(audienceProfile?.interests ?? null) }}
+
+
+
+
Плейсменты и форматы
+
+
Рекумендованные площадки {{ formatArrayField(recommendation?.recommendedPlacements ?? null) }}
+
Форматы {{ formatArrayField(recommendation?.recommendedAdFormats ?? null) }}
+
Тип таргетинга {{ targetingTypeLabel(recommendation?.recommendedType ?? null) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/pages/marketing/targetologist/CampaignWizard.vue b/src/views/pages/marketing/targetologist/CampaignWizard.vue
new file mode 100644
index 0000000..969fa76
--- /dev/null
+++ b/src/views/pages/marketing/targetologist/CampaignWizard.vue
@@ -0,0 +1,545 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Маркетинговая стратегия
+
+
+
+
+ {{ slotProps.value.displayTitle }}
+
+ {{ slotProps.placeholder }}
+
+
+
+
+
{{ slotProps.option.displayTitle }}
+
+
+
+
+
+
+
+
+
ИИ автоматически проанализирует рынок и сегменты из стратегии для настройки точного таргетинга.
+
+
+
+
+
+
+
+
+ Название кампании
+
+
+
+
+
+ Бюджет (KZT)
+
+
+
+ Цель
+
+
+
+
+
+
Платформы размещения
+
+
+
+
+
+
+
+
+
+
+
A/B Тестирование
+
Автоматический сплит бюджета между вариантами креативов.
+
+
+
+
+
+
+
+
ИИ-разбор концептов
+
Генерация рекламных офферов на базе текстов стратегии.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Ошибка запуска
+
{{ errorMessage }}
+
Вернуться
+
+
+
+
+
+
+
+
+
+
+
{{ phase.label }}
+
Обработка...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/pages/marketing/targetologist/TargetologistDashboard.vue b/src/views/pages/marketing/targetologist/TargetologistDashboard.vue
new file mode 100644
index 0000000..346c7c2
--- /dev/null
+++ b/src/views/pages/marketing/targetologist/TargetologistDashboard.vue
@@ -0,0 +1,615 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ campaigns.length }}
+
Кампаний всего
+
+
+
+
+
+
{{ activeCnt() }}
+
Активно сейчас
+
+
+
+
+
+
{{ formatCurrency(totalSpent()) }}
+
Всего потрачено
+
+
+
+
+
+
{{ formatCurrency(totalBudget()) }}
+
Общий лимит
+
+
+
+
+
+
+ Рекламные кабинеты
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Нет запущенных кампаний
+
ИИ-Таргетолог готов автоматизировать ваши расходы на рекламу, протестировать креативы и найти идеальную аудиторию.
+
+ Запустить первую кампанию
+
+
+
+
+
+
+
+
+
+
+
+
{{ cmp.name }}
+
+ {{ cmp.goal }}
+
+
+
+
+
+
+
+
+
+
+ {{ sm(cmp.status).label }}
+
+
+
+
+
+
Потрачено
+
{{ formatCurrency(cmp.spentKzt || 0) }}
+
+
+
+
CTR
+
{{ cmp.ctr || '0.00' }}%
+
+
+
Охват
+
{{ new Intl.NumberFormat('ru-RU').format(cmp.reach || 0) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/pages/marketing/targetologist/components/CampaignAdSets.vue b/src/views/pages/marketing/targetologist/components/CampaignAdSets.vue
new file mode 100644
index 0000000..49644cd
--- /dev/null
+++ b/src/views/pages/marketing/targetologist/components/CampaignAdSets.vue
@@ -0,0 +1,506 @@
+
+
+
+
+
+ Структура кампании пока формируется. AdSet-ы появятся после публикации.
+
+
+
+
+
+
+
+
+
+
+
+ {{ mediaTypeLabel(ad) }}
+ {{ ad.mediaSize }}
+
+
+
+ {{ ad.headline }}
+
+ {{ truncateText(ad.primaryText, getAdKey(ad, adIndex)) }}
+
+
+ {{ expandedText[getAdKey(ad, adIndex)] ? 'Свернуть' : 'Читать полностью' }}
+
+
+
+
+
+
+ Детали
+
+
adSetId: {{ adSet.adSetId ?? adSet.id }}
+
+ adId: {{ ad.adId ?? ad.id ?? `ad-${adIndex + 1}` }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/pages/marketing/targetologist/components/CampaignInsights.vue b/src/views/pages/marketing/targetologist/components/CampaignInsights.vue
new file mode 100644
index 0000000..3650a02
--- /dev/null
+++ b/src/views/pages/marketing/targetologist/components/CampaignInsights.vue
@@ -0,0 +1,311 @@
+
+
+
+
+
+
+
+
+
+
ИИ анализирует кампанию. Первые инсайты появятся через 24–48 часов после запуска.
+
+
+ Синхронизировать сейчас
+
+
+
+
+
+
+
+
+
+
+
{{ insight.description }}
+
+
+ Принять меры
+
+
+
+
+
+
+
+
{{ selectedInsight.title }}
+
{{ selectedInsight.description }}
+
+
+ {{ selectedInsight.suggestedAction || 'Рекомендация будет добавлена позже.' }}
+
+
+
+
+
+
+
diff --git a/src/views/pages/marketing/targetologist/components/CampaignMetrics.vue b/src/views/pages/marketing/targetologist/components/CampaignMetrics.vue
new file mode 100644
index 0000000..b4a0b00
--- /dev/null
+++ b/src/views/pages/marketing/targetologist/components/CampaignMetrics.vue
@@ -0,0 +1,251 @@
+
+
+
+
+
+
+ Данные появятся через 24–48 часов после запуска кампании
+
+
+
+
+
+
+
+
+
+
+ {{ card.value }}
+
+
{{ card.rangeLabel }}
+
+ {{ card.helperText }}
+
+
+
+
+
+
+
+
diff --git a/src/views/pages/marketing/targetologist/components/CampaignPrediction.vue b/src/views/pages/marketing/targetologist/components/CampaignPrediction.vue
new file mode 100644
index 0000000..5e19e46
--- /dev/null
+++ b/src/views/pages/marketing/targetologist/components/CampaignPrediction.vue
@@ -0,0 +1,930 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
AI-прогноз недоступен
+
Прогноз пока не готов. Запустите генерацию вручную.
+
Сервис готовит прогноз. Обычно это занимает 15-30 секунд.
+
+ Запустить расчет
+
+
+
+
+
+
+
+
+
+
+
+
Оценка прогноза
+
+
{{ predictionLabel }}
+
Уверенность: {{ confidence }}%
+
Обновлено: {{ formatDateTime(lastUpdated) }}
+
+
+ Обновить
+
+
+
+
+
+
Прогнозируемые метрики
+
+
+
+
+
+
+
Прогноз по неделям
+
+
+
+
+
+
{{ phase.name }}
+
{{ phase.spend != null ? formatMetricValue(phase.spend, 'KZT') : '—' }}
+
+
+
+
+
+
+
+
+
+
Топ рекомендации от ИИ
+
+
+
+
+ {{ rec.priority }}
+
+
+ {{ rec.category }}
+
+
+
{{ rec.title }}
+
{{ rec.description }}
+
{{ rec.expectedImpact }}
+
+
Рекомендации отсутствуют
+
+
+
+
+
+
+
Лучший сегмент
+
{{ audienceInsights.bestPerformingSegment || '—' }}
+
+
+
Расширение аудитории
+
{{ audienceInsights.recommendedExpansion || '—' }}
+
+
+
Кого исключить
+
{{ audienceInsights.exclusionAdvice || '—' }}
+
+
+
+
+
+
Оптимизация бюджета
+
+
+
Текущее распределение
+
{{ budgetOptimization.currentAllocation || '—' }}
+
+
+
Рекомендация
+
{{ budgetOptimization.recommendedReallocation || '—' }}
+
+
+
+
+ Потенциальное снижение CPL: {{ budgetOptimization.potentialCplReduction || '—' }}
+
+
+
+
+
+
Конкурентный анализ
+
+
+
+
+
+
+
+
+
{{ differentiationScore }}/100
+
+
+
+ Насыщенность рынка:
+ {{ marketSaturationMeta.label }}
+
+
Конкурентные преимущества
+
+
+ {{ advantage }}
+
+ —
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/pages/marketing/targetologist/components/OAuthConnectionWidget.vue b/src/views/pages/marketing/targetologist/components/OAuthConnectionWidget.vue
new file mode 100644
index 0000000..ad14a90
--- /dev/null
+++ b/src/views/pages/marketing/targetologist/components/OAuthConnectionWidget.vue
@@ -0,0 +1,289 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ title }}
+
{{ subtitle }}
+
+
+
+
+
+
+ Подключено
+ {{ accountName || 'Аккаунт' }}
+
+
+
+
+ {{ isConnecting ? 'Ожидание...' : 'Авторизовать' }}
+
+
+
+
+
+
+
+ Мы нашли несколько рекламных аккаунтов. Выберите тот, который будет использоваться ИИ для запуска рекламы.
+
+
+
+
+
+ {{ acc.name }}
+ ID: {{ acc.id }}
+
+
+
+
+
+ Отмена
+ Подтвердить
+
+
+
+
+
+
+
diff --git a/src/views/pages/marketing/targetologist/components/PipelineTimeline.vue b/src/views/pages/marketing/targetologist/components/PipelineTimeline.vue
new file mode 100644
index 0000000..acbd3f2
--- /dev/null
+++ b/src/views/pages/marketing/targetologist/components/PipelineTimeline.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ step.label }}
+
+
+ {{ fmtTime(historyEntry(step.code).timestamp) }}
+
+
+
+ {{ historyEntry(step.code).message }}
+
+
+ В процессе...
+
+
+
+
+
+
+
diff --git a/src/views/pages/marketing/targetologist/components/TargetingRecommendationPanel.vue b/src/views/pages/marketing/targetologist/components/TargetingRecommendationPanel.vue
new file mode 100644
index 0000000..6e51750
--- /dev/null
+++ b/src/views/pages/marketing/targetologist/components/TargetingRecommendationPanel.vue
@@ -0,0 +1,315 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
Тип таргетинга
+
{{ fmt(recommendation.recommendedTypeLabel || recommendation.recommendedType) }}
+
{{ recommendation.typeRationale }}
+
+
+
Цель кампании
+
{{ fmt(recommendation.campaignObjectiveLabel || recommendation.campaignObjective) }}
+
{{ recommendation.campaignObjectiveRationale }}
+
+
+
+
+
+
CTR
{{ fmt(recommendation.estimatedCtr) }}%
+
CPL
{{ fmtKzt(recommendation.estimatedCpl) }}
+
Охват
{{ fmt(recommendation.estimatedReach) }}
+
Частота
{{ fmt(recommendation.estimatedFrequency) }}
+
Конверсии
{{ fmt(recommendation.estimatedConversions) }}
+
+
+
+
+
Портрет аудитории
+
+
+
Основной сегмент
+
{{ recommendation.audienceProfile.primarySegment }}
+
+
+
Возраст
+
{{ recommendation.audienceProfile.ageRange }}
+
+
+
Гендер
+
{{ recommendation.audienceProfile.gender }}
+
+
+
География
+
{{ recommendation.audienceProfile.geography }}
+
+
+
Вторичный сегмент
+
{{ recommendation.audienceProfile.secondarySegment }}
+
+
+
Язык
+
{{ recommendation.audienceProfile.language }}
+
+
+
+
+
+
+
+
+
+
+
+
Форматы
+
+ {{ f }}
+
+
+
+
Плейсменты
+
+ {{ pl }}
+
+
+
+
+
+
+
Бюджетная рекомендация
+
+
+
Дневной бюджет
+
{{ fmtKzt(recommendation.budgetRecommendation.dailyBudget) }}
+
+
+
Месячный бюджет
+
{{ fmtKzt(recommendation.budgetRecommendation.monthlyBudget) }}
+
+
+
Стратегия ставок
+
{{ fmt(recommendation.budgetRecommendation.bidStrategy) }}
+
+
+
Дневной охват
+
{{ fmt(recommendation.budgetRecommendation.estimatedDailyReach) }}
+
+
+
+ {{ recommendation.budgetRecommendation.rationale }}
+
+
+
+
+
+
Фазы кампании
+
+
+
+
+
+
Фаза {{ phase.phase }}
+
{{ fmt(phase.name) }}
+
+
{{ fmt(phase.duration) }}
+
+
+
Цель
{{ phase.objective }}
+
+
Тип
{{ phase.targetingType }}
+
+
+
+ {{ t }}
+
+
+
+
+
+
+
+
+
+
+
+ Конкурентный контекст
+
+ CII: {{ recommendation.ciiLevel }}
+
+
+
{{ recommendation.competitiveContext }}
+
+ Дифференциация: {{ recommendation.differentiationAdvice }}
+
+
+
+
+
+
Воронка и CTA
+
+
+
Основной CTA
+
{{ recommendation.primaryCta }}
+
+
+
Стадия воронки
+
{{ recommendation.funnelStage }}
+
+
+
Механизм конверсии
+
{{ recommendation.conversionMechanism }}
+
+
+
+
+
+
+
+
diff --git a/src/views/pages/marketing/targetologist/composables/useTargeting.ts b/src/views/pages/marketing/targetologist/composables/useTargeting.ts
new file mode 100644
index 0000000..22ffe26
--- /dev/null
+++ b/src/views/pages/marketing/targetologist/composables/useTargeting.ts
@@ -0,0 +1,199 @@
+import { reactive } from 'vue';
+import { useToast } from 'primevue/usetoast';
+import TargetingService from '@/service/TargetingService';
+
+// --- Types Mocks (will be matched with API) ---
+export interface AdAccountStatus {
+ facebook: boolean;
+ tiktok: boolean;
+}
+
+export interface Campaign {
+ id: string;
+ name: string;
+ goal: string;
+ status: 'ACTIVE' | 'PAUSED' | 'FAILED' | 'GENERATING' | 'DRAFT' | 'COMPLETED';
+ budgetKzt: number;
+ spentKzt: number;
+ reach: number;
+ cpm: number;
+ ctr: number;
+ cpc: number;
+ roas?: number;
+ conversions?: number;
+ platforms: string[];
+ createdAt: string;
+}
+
+export interface AdCreative {
+ id: string;
+ headline: string;
+ primaryText: string;
+ mediaUrl: string;
+ contentType: 'image' | 'video';
+}
+
+export interface AdSet {
+ id: string;
+ name: string;
+ targetAudience: string;
+ ads: AdCreative[];
+}
+
+export interface CampaignDetails extends Campaign {
+ adSets: AdSet[];
+ statusHistory?: Array<{ status: string; message?: string }>;
+}
+
+export interface TargetAudience {
+ trustedAI: boolean;
+ ageMin?: number;
+ ageMax?: number;
+ locations?: string[];
+}
+
+// --- Centralized State ---
+const state = reactive({
+ accounts: {
+ facebook: false,
+ tiktok: false,
+ } as AdAccountStatus,
+ accountNames: {
+ facebook: '',
+ tiktok: '',
+ },
+ campaigns: [] as Campaign[],
+ currentCampaignDetails: null as any,
+ isGenerating: false,
+});
+
+// --- Actions (Composables) ---
+export function useTargeting() {
+ const toast = useToast();
+
+ const connectAccount = async (platform: 'facebook' | 'tiktok', accountId: string, accountName: string) => {
+ try {
+ await TargetingService.selectAdAccount(platform, accountId);
+ state.accounts[platform] = true;
+ state.accountNames[platform] = accountName || `Account ${accountId}`;
+ toast.add({ severity: 'success', summary: 'Синхронизация завершена', detail: `Успешно подключен кабинет: ${state.accountNames[platform]}`, life: 4000 });
+ } catch (error: any) {
+ toast.add({ severity: 'error', summary: 'Ошибка привязки', detail: error.message || 'Не удалось привязать кабинет', life: 5000 });
+ }
+ };
+
+ const fetchCampaigns = async () => {
+ try {
+ const campaignsData = await TargetingService.getCampaigns(0, 50);
+ state.campaigns = Array.isArray(campaignsData) ? campaignsData : [];
+ return state.campaigns;
+ } catch (error) {
+ console.error(error);
+ toast.add({ severity: 'error', summary: 'Ошибка', detail: 'Не удалось загрузить кампании', life: 5000 });
+ return [];
+ }
+ };
+
+ const getCampaignById = async (id: string, noStateUpdate = false) => {
+ try {
+ const details = await TargetingService.getCampaignById(id);
+ // mock ad structure if backend lacks it for now
+ if (!details.adSets) {
+ details.adSets = [
+ {
+ id: 'set-1',
+ name: 'IT-Специалисты 18-34',
+ targetAudience: 'Lookalike, AI-matched',
+ ads: [
+ { id: 'ad1', headline: 'Оффер для Tech', primaryText: 'Текст', mediaUrl: 'https://images.unsplash.com/photo-1558002038-1055907df827', contentType: 'image' }
+ ]
+ }
+ ];
+ }
+ if (!noStateUpdate) {
+ state.currentCampaignDetails = details;
+ }
+ return details;
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+ };
+
+ const createAiCampaign = async (payload: { name: string, goal: string, budgetKzt: number, platforms: string[], targetAudiences?: TargetAudience, strategyId?: string }) => {
+ state.isGenerating = true;
+
+ try {
+ // Mapping frontend model to backend DTO Request
+ // analysisId намеренно не передаём — бэкенд подтягивает его из стратегии сам
+ // Map frontend platform keys → backend enum values
+ const mapPlatforms = (platforms: string[]): string[] => {
+ const result: string[] = [];
+ for (const p of platforms) {
+ const up = p.toUpperCase();
+ if (up === 'FB_IG') { result.push('FACEBOOK', 'INSTAGRAM'); }
+ else if (up === 'FACEBOOK') { result.push('FACEBOOK'); }
+ else if (up === 'INSTAGRAM') { result.push('INSTAGRAM'); }
+ else if (up === 'TIKTOK') { result.push('TIKTOK'); }
+ else { result.push(up); }
+ }
+ return result;
+ };
+
+ const apiPayload = {
+ strategyId: payload.strategyId,
+ campaignName: payload.name || 'Новая AI Кампания',
+ objective: getObjectiveEnum(payload.goal),
+ platforms: mapPlatforms(payload.platforms),
+ totalBudgetKzt: payload.budgetKzt,
+ startDate: new Date().toISOString(),
+ endDate: new Date(Date.now() + 30 * 86400000).toISOString(),
+ };
+
+ const response = await TargetingService.createCampaign(apiPayload);
+
+ // Response typically returns { id, status, statusHistory }
+ // Fetch immediately to put into state
+ if (response && response.id) {
+ return response.id;
+ } else {
+ throw new Error("Невалидный ответ от сервера при создании кампании");
+ }
+ } catch (error: any) {
+ toast.add({ severity: 'error', summary: 'Сбой оркестратора', detail: error.message || 'Критическая ошибка запуска', life: 8000 });
+ throw error;
+ } finally {
+ state.isGenerating = false;
+ }
+ };
+
+ // Map Russian UI labels OR direct enum values → backend objective enum
+ const getObjectiveEnum = (goalRu: string) => {
+ const v = goalRu.toUpperCase();
+ // Direct enum pass-through
+ if (['CONVERSIONS', 'TRAFFIC', 'REACH', 'LEADS', 'AWARENESS', 'ENGAGEMENT', 'SALES'].includes(v)) return v;
+ // Russian UI labels
+ switch(goalRu.toLowerCase()) {
+ case 'продажи': return 'CONVERSIONS';
+ case 'трафик': return 'TRAFFIC';
+ case 'охват': return 'REACH';
+ case 'лиды': return 'LEADS';
+ default: return 'CONVERSIONS';
+ }
+ }
+
+ const formatCurrency = (amount: number) => {
+ if (!amount || isNaN(amount)) amount = 0;
+ return new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'KZT', maximumFractionDigits: 0 }).format(amount);
+ };
+
+ return {
+ state,
+ connectAccount,
+ fetchCampaigns,
+ getCampaignById,
+ createAiCampaign,
+ formatCurrency,
+ TargetingService // Expose raw service if needed directly
+ };
+}
diff --git a/src/views/targeting/TargetingAnalysisView.vue b/src/views/targeting/TargetingAnalysisView.vue
new file mode 100644
index 0000000..9693df9
--- /dev/null
+++ b/src/views/targeting/TargetingAnalysisView.vue
@@ -0,0 +1,214 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ analysis.marketOverview }}
+
+
+
+
+
+
+
Основная
+
{{ analysis.targetAudience?.primary }}
+
+
+
Вторичная
+
{{ analysis.targetAudience?.secondary }}
+
+
+
+
Возраст
+
{{ analysis.targetAudience?.ageRange }}
+
+
+
Пол
+
{{ analysis.targetAudience?.gender }}
+
+
+
Доход
+
{{ analysis.targetAudience?.income }}
+
+
+
+
Интересы
+
+
+ {{ interest }}
+
+
+
+
+
+
+
+
+
+
+
+
{{ platform.name }}
+
{{ platform.reason }}
+
+
+ {{ platform.priority }}
+
+
+
+
+
+
+
+
+
Мин/день
+
{{ analysis.budgetRecommendation?.minDaily }}
+
+
+
Оптимум/день
+
{{ analysis.budgetRecommendation?.optimalDaily }}
+
+
+
В месяц
+
{{ analysis.budgetRecommendation?.monthly }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ index + 1 }}
+
+ {{ message }}
+
+
+
+
+
+
+ Форматы
+
+
+ {{ format }}
+
+
+
+
+
+
+ {{ analysis.competitorsInsight }}
+
+
+
+ {{ analysis.recommendedVisuals }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/targeting/TargetingCreativeView.vue b/src/views/targeting/TargetingCreativeView.vue
new file mode 100644
index 0000000..0572240
--- /dev/null
+++ b/src/views/targeting/TargetingCreativeView.vue
@@ -0,0 +1,291 @@
+
+
+
+
+
+
+
+
+
+
+
+
✅
+
Реклама запущена в Facebook
+
Первый пост из стратегии опубликован. Ожидайте первые результаты через 24-48 часов.
+
+
+
+
ID публикации: {{ launchResult.facebookPostId || '—' }}
+
Платформа: {{ launchResult.platform || 'Instagram' }}
+
Бюджет/день: {{ launchResult.targetingSettings?.budgetPerDay || '—' }}
+
Охват/день: {{ launchResult.targetingSettings?.estimatedReach || '—' }}
+
+
+
+
+
+
+
+
+ {{ statusBadge.text }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Стратегия уже готова, поэтому мы пропускаем генерацию анализа и креатива и сразу запускаем таргет через `POST /api/targeting/launch/{strategyId}`.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ creative.headline }}
+ {{ creative.caption }}
+ {{ (creative.hashtags || []).join(' ') }}
+
+
+
+
+
+
+
+
+
+
+
Аудитория: {{ creative.targetingSettings?.audience }}
+
Возраст: {{ creative.targetingSettings?.age }}
+
Размещение: {{ creative.targetingSettings?.placement }}
+
Бюджет/день: {{ creative.targetingSettings?.budgetPerDay }}
+
Охват/день: {{ creative.targetingSettings?.estimatedReach }}
+
+
+
+
+ {{ interest }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/targeting/TargetingInputView.vue b/src/views/targeting/TargetingInputView.vue
new file mode 100644
index 0000000..b264345
--- /dev/null
+++ b/src/views/targeting/TargetingInputView.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
Тема бизнеса *
+
+
+
+
Быстрые примеры
+
+
+ {{ example }}
+
+
+
+
+
+
+
Что получим
+
+
+
01
+
Анализ рынка, аудитории, платформ и бюджета.
+
+
+
02
+
Стратегию по фазам с KPI и набором креативов.
+
+
+
03
+
Готовый рекламный пост и запуск по новой targeting API логике.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/targeting/TargetingLayout.vue b/src/views/targeting/TargetingLayout.vue
new file mode 100644
index 0000000..f7b2ca6
--- /dev/null
+++ b/src/views/targeting/TargetingLayout.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
AI Targeting System
+
Единый поток: анализ, стратегия, креатив и запуск рекламы.
+
+
+ Шаг {{ currentStepIndex + 1 }} из 4
+
+
+
+
+
+
+
+
+
+
+ ✓
+ {{ index + 1 }}
+
+
+ {{ step.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/targeting/TargetingStrategyView.vue b/src/views/targeting/TargetingStrategyView.vue
new file mode 100644
index 0000000..7287026
--- /dev/null
+++ b/src/views/targeting/TargetingStrategyView.vue
@@ -0,0 +1,202 @@
+
+
+
+
+
+
+
+
+
+
+ {{ strategy.strategyName }}
+ •
+ {{ strategy.duration }}
+ •
+ {{ strategy.totalBudget }}
+
+
+
+
+
+
+ {{ tab.label }}
+
+
+
+
+
+
+
+ {{ strategy.goal }}
+
+
+
+
+
+
+
+ Цель: {{ phase.objective }}
+
+
+
+ KPI: {{ phase.kpi }}
+
+
+
+
+
+
+
+
Главный раздел
+
Выберите нужный креатив, и мы сразу сгенерируем готовый рекламный пост с настройками таргетинга.
+
+
+
+
+
+
+
+
+
Идея подписи
+
{{ creativeNeed.captionIdea }}
+
+
+ {{ creativeNeed.targetSegment }}
+
+
+
+
+
Описание изображения
+
{{ creativeNeed.imageDescription }}
+
+
+
+
CTA: {{ creativeNeed.cta }}
+
+
+
+
+
+
+
+
+ {{ strategy.totalBudget }}
+
+
+
+
+
Охват: {{ strategy.expectedResults?.reach }}
+
Клики: {{ strategy.expectedResults?.clicks }}
+
Лиды: {{ strategy.expectedResults?.conversions }}
+
CPL: {{ strategy.expectedResults?.cpl }}
+
+
+
+
+
+
+
+
+
+
diff --git a/vite.config.mjs.timestamp-1775383080692-2b97e170850468.mjs b/vite.config.mjs.timestamp-1775383080692-2b97e170850468.mjs
new file mode 100644
index 0000000..390a73b
--- /dev/null
+++ b/vite.config.mjs.timestamp-1775383080692-2b97e170850468.mjs
@@ -0,0 +1,47 @@
+// vite.config.mjs
+import { fileURLToPath, URL } from "node:url";
+import { PrimeVueResolver } from "file:///C:/Users/777/IdeaProjects/marketing/node_modules/@primevue/auto-import-resolver/index.mjs";
+import vue from "file:///C:/Users/777/IdeaProjects/marketing/node_modules/@vitejs/plugin-vue/dist/index.mjs";
+import Components from "file:///C:/Users/777/IdeaProjects/marketing/node_modules/unplugin-vue-components/dist/vite.js";
+import { defineConfig } from "file:///C:/Users/777/IdeaProjects/marketing/node_modules/vite/dist/node/index.js";
+var __vite_injected_original_import_meta_url = "file:///C:/Users/777/IdeaProjects/marketing/vite.config.mjs";
+var vite_config_default = defineConfig({
+ optimizeDeps: {
+ noDiscovery: true
+ },
+ plugins: [
+ vue(),
+ Components({
+ resolvers: [PrimeVueResolver()]
+ })
+ ],
+ resolve: {
+ alias: {
+ "@": fileURLToPath(new URL("./src", __vite_injected_original_import_meta_url))
+ }
+ },
+ server: {
+ proxy: {
+ "/api": {
+ target: "https://api.konturai.kz",
+ changeOrigin: true,
+ secure: true,
+ configure: (proxy, _options) => {
+ proxy.on("error", (err, _req, _res) => {
+ console.log("proxy error", err);
+ });
+ proxy.on("proxyReq", (proxyReq, req, _res) => {
+ console.log("Sending Request to the Target:", req.method, req.url);
+ });
+ proxy.on("proxyRes", (proxyRes, req, _res) => {
+ console.log("Received Response from the Target:", proxyRes.statusCode, req.url);
+ });
+ }
+ }
+ }
+ }
+});
+export {
+ vite_config_default as default
+};
+//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcubWpzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiQzpcXFxcVXNlcnNcXFxcNzc3XFxcXElkZWFQcm9qZWN0c1xcXFxtYXJrZXRpbmdcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIkM6XFxcXFVzZXJzXFxcXDc3N1xcXFxJZGVhUHJvamVjdHNcXFxcbWFya2V0aW5nXFxcXHZpdGUuY29uZmlnLm1qc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vQzovVXNlcnMvNzc3L0lkZWFQcm9qZWN0cy9tYXJrZXRpbmcvdml0ZS5jb25maWcubWpzXCI7aW1wb3J0IHsgZmlsZVVSTFRvUGF0aCwgVVJMIH0gZnJvbSAnbm9kZTp1cmwnO1xuXG5pbXBvcnQgeyBQcmltZVZ1ZVJlc29sdmVyIH0gZnJvbSAnQHByaW1ldnVlL2F1dG8taW1wb3J0LXJlc29sdmVyJztcbmltcG9ydCB2dWUgZnJvbSAnQHZpdGVqcy9wbHVnaW4tdnVlJztcbmltcG9ydCBDb21wb25lbnRzIGZyb20gJ3VucGx1Z2luLXZ1ZS1jb21wb25lbnRzL3ZpdGUnO1xuaW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSc7XG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICAgIG9wdGltaXplRGVwczoge1xuICAgICAgICBub0Rpc2NvdmVyeTogdHJ1ZVxuICAgIH0sXG4gICAgcGx1Z2luczogW1xuICAgICAgICB2dWUoKSxcbiAgICAgICAgQ29tcG9uZW50cyh7XG4gICAgICAgICAgICByZXNvbHZlcnM6IFtQcmltZVZ1ZVJlc29sdmVyKCldXG4gICAgICAgIH0pXG4gICAgXSxcbiAgICByZXNvbHZlOiB7XG4gICAgICAgIGFsaWFzOiB7XG4gICAgICAgICAgICAnQCc6IGZpbGVVUkxUb1BhdGgobmV3IFVSTCgnLi9zcmMnLCBpbXBvcnQubWV0YS51cmwpKVxuICAgICAgICB9XG4gICAgfSxcbiAgICBzZXJ2ZXI6IHtcbiAgICAgICAgcHJveHk6IHtcbiAgICAgICAgICAgICcvYXBpJzoge1xuICAgICAgICAgICAgICAgIHRhcmdldDogJ2h0dHBzOi8vYXBpLmtvbnR1cmFpLmt6JyxcbiAgICAgICAgICAgICAgICBjaGFuZ2VPcmlnaW46IHRydWUsXG4gICAgICAgICAgICAgICAgc2VjdXJlOiB0cnVlLFxuICAgICAgICAgICAgICAgIGNvbmZpZ3VyZTogKHByb3h5LCBfb3B0aW9ucykgPT4ge1xuICAgICAgICAgICAgICAgICAgICBwcm94eS5vbignZXJyb3InLCAoZXJyLCBfcmVxLCBfcmVzKSA9PiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBjb25zb2xlLmxvZygncHJveHkgZXJyb3InLCBlcnIpO1xuICAgICAgICAgICAgICAgICAgICB9KTtcbiAgICAgICAgICAgICAgICAgICAgcHJveHkub24oJ3Byb3h5UmVxJywgKHByb3h5UmVxLCByZXEsIF9yZXMpID0+IHtcbiAgICAgICAgICAgICAgICAgICAgICAgIGNvbnNvbGUubG9nKCdTZW5kaW5nIFJlcXVlc3QgdG8gdGhlIFRhcmdldDonLCByZXEubWV0aG9kLCByZXEudXJsKTtcbiAgICAgICAgICAgICAgICAgICAgfSk7XG4gICAgICAgICAgICAgICAgICAgIHByb3h5Lm9uKCdwcm94eVJlcycsIChwcm94eVJlcywgcmVxLCBfcmVzKSA9PiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBjb25zb2xlLmxvZygnUmVjZWl2ZWQgUmVzcG9uc2UgZnJvbSB0aGUgVGFyZ2V0OicsIHByb3h5UmVzLnN0YXR1c0NvZGUsIHJlcS51cmwpO1xuICAgICAgICAgICAgICAgICAgICB9KTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICB9XG59KTtcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBdVMsU0FBUyxlQUFlLFdBQVc7QUFFMVUsU0FBUyx3QkFBd0I7QUFDakMsT0FBTyxTQUFTO0FBQ2hCLE9BQU8sZ0JBQWdCO0FBQ3ZCLFNBQVMsb0JBQW9CO0FBTDJKLElBQU0sMkNBQTJDO0FBUXpPLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQ3hCLGNBQWM7QUFBQSxJQUNWLGFBQWE7QUFBQSxFQUNqQjtBQUFBLEVBQ0EsU0FBUztBQUFBLElBQ0wsSUFBSTtBQUFBLElBQ0osV0FBVztBQUFBLE1BQ1AsV0FBVyxDQUFDLGlCQUFpQixDQUFDO0FBQUEsSUFDbEMsQ0FBQztBQUFBLEVBQ0w7QUFBQSxFQUNBLFNBQVM7QUFBQSxJQUNMLE9BQU87QUFBQSxNQUNILEtBQUssY0FBYyxJQUFJLElBQUksU0FBUyx3Q0FBZSxDQUFDO0FBQUEsSUFDeEQ7QUFBQSxFQUNKO0FBQUEsRUFDQSxRQUFRO0FBQUEsSUFDSixPQUFPO0FBQUEsTUFDSCxRQUFRO0FBQUEsUUFDSixRQUFRO0FBQUEsUUFDUixjQUFjO0FBQUEsUUFDZCxRQUFRO0FBQUEsUUFDUixXQUFXLENBQUMsT0FBTyxhQUFhO0FBQzVCLGdCQUFNLEdBQUcsU0FBUyxDQUFDLEtBQUssTUFBTSxTQUFTO0FBQ25DLG9CQUFRLElBQUksZUFBZSxHQUFHO0FBQUEsVUFDbEMsQ0FBQztBQUNELGdCQUFNLEdBQUcsWUFBWSxDQUFDLFVBQVUsS0FBSyxTQUFTO0FBQzFDLG9CQUFRLElBQUksa0NBQWtDLElBQUksUUFBUSxJQUFJLEdBQUc7QUFBQSxVQUNyRSxDQUFDO0FBQ0QsZ0JBQU0sR0FBRyxZQUFZLENBQUMsVUFBVSxLQUFLLFNBQVM7QUFDMUMsb0JBQVEsSUFBSSxzQ0FBc0MsU0FBUyxZQUFZLElBQUksR0FBRztBQUFBLFVBQ2xGLENBQUM7QUFBQSxRQUNMO0FBQUEsTUFDSjtBQUFBLElBQ0o7QUFBQSxFQUNKO0FBQ0osQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K