This commit is contained in:
root
2025-12-03 09:05:00 +05:00
parent 24cc534fed
commit 0b074d6281
6 changed files with 1023 additions and 98 deletions
@@ -0,0 +1,700 @@
# API Документация: Обновленные поля для генерации бизнес-анализа (Frontend/AI Agent)
## Обзор изменений
API для генерации маркетингового анализа был обновлен с новыми полями, которые более точно отражают требования бизнес-анализа. Все старые поля были заменены новыми для улучшения качества анализа.
---
## Изменения в структуре запроса
### Удаленные поля (больше не используются)
Следующие поля были **удалены** из API и больше не принимаются:
-`location` - заменено на `region`
-`client` - заменено на `targetAudience`
-`differentiator` - заменено на комбинацию `businessNiche`, `strongSide`, `weakSide`
### Новые обязательные поля
| Поле | Тип | Обязательный | Описание | Пример значения |
| ---------------- | ------ | ------------ | -------------------------------------- | ------------------------------------------------------ |
| `businessNiche` | string | ✅ | Ниша бизнеса | "E-commerce платформы" |
| `product` | string | ✅ | Продукт или услуга | "Разработка мобильных приложений" |
| `targetAudience` | string | ✅ | Целевая аудитория (детальное описание) | "Малый и средний бизнес, владельцы интернет-магазинов" |
| `region` | string | ✅ | Регион (город Казахстана) | "Алматы" |
| `goal` | string | ✅ | Цель на 6-12 месяцев | "Увеличить количество клиентов на 50%" |
| `detailLevel` | string | ✅ | Уровень детализации анализа | "СТАНДАРТНО" |
| `analysisType` | string | ✅ | Тип анализа | "РЫНОК" |
### Новые опциональные поля
| Поле | Тип | Обязательный | Описание | Пример значения |
| ------------ | ------ | ------------ | ----------------------- | ------------------------------------------------- |
| `strongSide` | string | ❌ | Сильная сторона бизнеса | "Опытная команда разработчиков, быстрая доставка" |
| `weakSide` | string | ❌ | Слабая сторона бизнеса | "Ограниченный маркетинговый бюджет" |
---
## Детальное описание полей
### 1. `businessNiche` (обязательное)
**Описание**: Ниша бизнеса, в которой работает компания.
**Валидация**:
- Минимальная длина: 3 символа
- Максимальная длина: 200 символов
- Разрешены: буквы, цифры, пробелы, дефисы, запятые
- Паттерн: `^[\p{L}\p{N}\s\-,]+$`
**Примеры**:
```json
"businessNiche": "E-commerce платформы"
"businessNiche": "Образовательные технологии"
"businessNiche": "Финансовые услуги для малого бизнеса"
```
---
### 2. `product` (обязательное)
**Описание**: Конкретный продукт или услуга, которую предоставляет компания.
**Валидация**:
- Минимальная длина: 3 символа
- Максимальная длина: 200 символов
- Разрешены: буквы, цифры, пробелы, дефисы, запятые
- Паттерн: `^[\p{L}\p{N}\s\-,]+$`
**Примеры**:
```json
"product": "Разработка мобильных приложений"
"product": "Консультации по маркетингу"
"product": "Веб-разработка и дизайн"
```
---
### 3. `targetAudience` (обязательное)
**Описание**: Детальное описание целевой аудитории. Это поле заменяет старое поле `client` и должно содержать более подробную информацию.
**Валидация**:
- Минимальная длина: 3 символа
- Максимальная длина: 300 символов
- Разрешены любые символы
**Примеры**:
```json
"targetAudience": "Малый и средний бизнес, владельцы интернет-магазинов в возрасте 30-50 лет"
"targetAudience": "Стартапы и технологические компании, нуждающиеся в быстрой разработке MVP"
"targetAudience": "Частные лица, желающие создать личный бренд в социальных сетях"
```
**Примечание**: В отличие от старого поля `client`, которое принимало только фиксированные значения, `targetAudience` принимает свободный текст для более гибкого описания.
---
### 4. `region` (обязательное)
**Описание**: Регион (город Казахстана), в котором работает бизнес. Это поле заменяет старое поле `location`.
**Валидация**:
- Должно быть одним из допустимых городов Казахстана
- Проверка выполняется через валидатор `@ValidRegion`
**Допустимые значения** (точное совпадение):
- `"Алматы"`
- `"Астана"`
- `"Шымкент"`
- `"Караганда"`
- `"Актобе"`
- `"Тараз"`
- `"Павлодар"`
- `"Усть-Каменогорск"`
- `"Семей"`
- `"Костанай"`
- `"Кызылорда"`
- `"Уральск"`
- `"Петропавловск"`
- `"Атырау"`
- `"Актау"`
- `"Туркестан"`
- `"Кокшетау"`
- `"Талдыкорган"`
- `"Экибастуз"`
- `"Рудный"`
**Примеры**:
```json
"region": "Алматы"
"region": "Астана"
"region": "Шымкент"
```
**Важно**: Значение должно точно совпадать с одним из допустимых городов (регистр важен).
---
### 5. `goal` (обязательное)
**Описание**: Цель бизнеса на период 6-12 месяцев. Это новое поле, которое помогает AI лучше понять приоритеты бизнеса.
**Валидация**:
- Минимальная длина: 10 символов
- Максимальная длина: 500 символов
- Разрешены любые символы
**Примеры**:
```json
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев"
"goal": "Выйти на рынок соседних регионов и открыть 3 новых филиала"
"goal": "Повысить узнаваемость бренда и увеличить продажи через онлайн-каналы на 30%"
```
---
### 6. `detailLevel` (обязательное)
**Описание**: Уровень детализации анализа. Влияет на объем и глубину генерируемого анализа.
**Валидация**:
- Должно быть одним из допустимых значений
- Проверка выполняется через валидатор `@ValidDetailLevel`
**Допустимые значения** (точное совпадение, регистр важен):
- `"КРАТКО"` - Краткий анализ (1-2 абзаца)
- `"СТАНДАРТНО"` - Стандартный анализ (3-5 абзацев) - **рекомендуется по умолчанию**
- `"ПОДРОБНО"` - Подробный анализ (5-8 абзацев)
**Влияние на анализ**:
| Уровень | Длина ответов | Количество рекомендаций | Детализация стратегии |
| ------------ | ------------- | ----------------------- | ------------------------ |
| `КРАТКО` | 1-2 абзаца | 3-4 рекомендации | Краткая |
| `СТАНДАРТНО` | 3-5 абзацев | 4-6 рекомендаций | Стандартная |
| `ПОДРОБНО` | 5-8 абзацев | 6-8 рекомендаций | Детальная с обоснованием |
**Примеры**:
```json
"detailLevel": "КРАТКО"
"detailLevel": "СТАНДАРТНО"
"detailLevel": "ПОДРОБНО"
```
---
### 7. `strongSide` (опциональное)
**Описание**: Сильная сторона бизнеса. Помогает AI лучше понять конкурентные преимущества.
**Валидация**:
- Максимальная длина: 500 символов
- Разрешены любые символы
- Может быть пустым или отсутствовать
**Примеры**:
```json
"strongSide": "Опытная команда разработчиков с 10+ летним опытом, быстрая доставка проектов"
"strongSide": "Уникальная технология, низкие цены, отличная поддержка клиентов"
```
**Примечание**: Если поле не указано, AI будет анализировать сильные стороны на основе других данных.
---
### 8. `weakSide` (опциональное)
**Описание**: Слабая сторона бизнеса. Помогает AI лучше понять области для улучшения.
**Валидация**:
- Максимальная длина: 500 символов
- Разрешены любые символы
- Может быть пустым или отсутствовать
**Примеры**:
```json
"weakSide": "Ограниченный маркетинговый бюджет, недостаточная узнаваемость бренда"
"weakSide": "Небольшая команда, ограниченные ресурсы для масштабирования"
```
**Примечание**: Если поле не указано, AI будет анализировать слабые стороны на основе других данных.
---
### 9. `analysisType` (обязательное)
**Описание**: Тип анализа, который необходимо провести. Поле осталось без изменений.
**Допустимые значения**:
- `"РЫНОК"` - Анализ рынка
- `"КОНКУРЕНТЫ"` - Анализ конкурентов
- `"ЦА"` - Анализ целевой аудитории
- `"КАНАЛЫ"` - Анализ маркетинговых каналов
- `"SWOT"` - SWOT-анализ
---
## Полный пример запроса
### POST `/api/marketing/analysis/start`
```json
{
"businessNiche": "E-commerce платформы",
"product": "Разработка мобильных приложений для интернет-магазинов",
"targetAudience": "Малый и средний бизнес, владельцы интернет-магазинов в возрасте 30-50 лет, нуждающиеся в мобильных решениях",
"region": "Алматы",
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев и выйти на рынок соседних регионов",
"detailLevel": "СТАНДАРТНО",
"strongSide": "Опытная команда разработчиков с 10+ летним опытом, быстрая доставка проектов, использование современных технологий",
"weakSide": "Ограниченный маркетинговый бюджет, недостаточная узнаваемость бренда в новых регионах",
"analysisType": "РЫНОК"
}
```
---
## Примеры использования на фронтенде
### JavaScript/TypeScript
```typescript
interface MarketingAnalysisRequest {
businessNiche: string;
product: string;
targetAudience: string;
region: string;
goal: string;
detailLevel: 'КРАТКО' | 'СТАНДАРТНО' | 'ПОДРОБНО';
strongSide?: string;
weakSide?: string;
analysisType: 'РЫНОК' | 'КОНКУРЕНТЫ' | 'ЦА' | 'КАНАЛЫ' | 'SWOT';
}
// Список допустимых регионов
const VALID_REGIONS = [
'Алматы',
'Астана',
'Шымкент',
'Караганда',
'Актобе',
'Тараз',
'Павлодар',
'Усть-Каменогорск',
'Семей',
'Костанай',
'Кызылорда',
'Уральск',
'Петропавловск',
'Атырау',
'Актау',
'Туркестан',
'Кокшетау',
'Талдыкорган',
'Экибастуз',
'Рудный',
];
// Список уровней детализации
const DETAIL_LEVELS = ['КРАТКО', 'СТАНДАРТНО', 'ПОДРОБНО'] as const;
// Функция для отправки запроса
async function startMarketingAnalysis(
data: MarketingAnalysisRequest
): Promise<string> {
const response = await fetch(
'https://api.konturai.kz/api/marketing/analysis/start',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer YOUR_JWT_TOKEN', // Если требуется
},
body: JSON.stringify(data),
}
);
const result = await response.json();
if (!result.success) {
throw new Error(result.error?.message || 'Failed to start analysis');
}
return result.data.analysisId;
}
// Пример использования
const analysisId = await startMarketingAnalysis({
businessNiche: 'E-commerce платформы',
product: 'Разработка мобильных приложений',
targetAudience: 'Малый и средний бизнес, владельцы интернет-магазинов',
region: 'Алматы',
goal: 'Увеличить количество клиентов на 50% за следующие 6 месяцев',
detailLevel: 'СТАНДАРТНО',
strongSide: 'Опытная команда, быстрая доставка',
weakSide: 'Ограниченный маркетинговый бюджет',
analysisType: 'РЫНОК',
});
```
### React компонент с формой
```tsx
import React, { useState } from 'react';
const MarketingAnalysisForm: React.FC = () => {
const [formData, setFormData] = useState<MarketingAnalysisRequest>({
businessNiche: '',
product: '',
targetAudience: '',
region: '',
goal: '',
detailLevel: 'СТАНДАРТНО',
strongSide: '',
weakSide: '',
analysisType: 'РЫНОК',
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const analysisId = await startMarketingAnalysis(formData);
console.log('Analysis started:', analysisId);
// Перенаправление на страницу с результатами
} catch (error) {
console.error('Error:', error);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Ниша бизнеса *</label>
<input
type='text'
value={formData.businessNiche}
onChange={(e) =>
setFormData({ ...formData, businessNiche: e.target.value })
}
required
minLength={3}
maxLength={200}
/>
</div>
<div>
<label>Продукт / услуга *</label>
<input
type='text'
value={formData.product}
onChange={(e) =>
setFormData({ ...formData, product: e.target.value })
}
required
minLength={3}
maxLength={200}
/>
</div>
<div>
<label>Целевая аудитория *</label>
<textarea
value={formData.targetAudience}
onChange={(e) =>
setFormData({ ...formData, targetAudience: e.target.value })
}
required
minLength={3}
maxLength={300}
/>
</div>
<div>
<label>Регион *</label>
<select
value={formData.region}
onChange={(e) => setFormData({ ...formData, region: e.target.value })}
required
>
<option value=''>Выберите регион</option>
{VALID_REGIONS.map((region) => (
<option key={region} value={region}>
{region}
</option>
))}
</select>
</div>
<div>
<label>Цель на 6-12 месяцев *</label>
<textarea
value={formData.goal}
onChange={(e) => setFormData({ ...formData, goal: e.target.value })}
required
minLength={10}
maxLength={500}
/>
</div>
<div>
<label>Уровень детализации анализа *</label>
<div>
{DETAIL_LEVELS.map((level) => (
<label key={level}>
<input
type='radio'
name='detailLevel'
value={level}
checked={formData.detailLevel === level}
onChange={(e) =>
setFormData({
...formData,
detailLevel: e.target.value as any,
})
}
/>
{level === 'КРАТКО' && ' Кратко'}
{level === 'СТАНДАРТНО' && ' Стандартно'}
{level === 'ПОДРОБНО' && ' Подробно'}
</label>
))}
</div>
</div>
<div>
<label>Сильная сторона</label>
<textarea
value={formData.strongSide}
onChange={(e) =>
setFormData({ ...formData, strongSide: e.target.value })
}
maxLength={500}
/>
</div>
<div>
<label>Слабая сторона</label>
<textarea
value={formData.weakSide}
onChange={(e) =>
setFormData({ ...formData, weakSide: e.target.value })
}
maxLength={500}
/>
</div>
<div>
<label>Тип анализа *</label>
<select
value={formData.analysisType}
onChange={(e) =>
setFormData({ ...formData, analysisType: e.target.value as any })
}
required
>
<option value='РЫНОК'>Рынок</option>
<option value='КОНКУРЕНТЫ'>Конкуренты</option>
<option value='ЦА'>Целевая аудитория</option>
<option value='КАНАЛЫ'>Каналы</option>
<option value='SWOT'>SWOT</option>
</select>
</div>
<button type='submit'>Сгенерировать анализ бизнеса</button>
</form>
);
};
```
---
## Валидация на клиенте
Рекомендуется выполнять валидацию на клиенте перед отправкой запроса:
```typescript
function validateMarketingAnalysisRequest(data: MarketingAnalysisRequest): {
valid: boolean;
errors: Record<string, string>;
} {
const errors: Record<string, string> = {};
// Валидация businessNiche
if (!data.businessNiche || data.businessNiche.trim().length < 3) {
errors.businessNiche = 'Ниша бизнеса должна содержать минимум 3 символа';
}
if (data.businessNiche && data.businessNiche.length > 200) {
errors.businessNiche = 'Ниша бизнеса не должна превышать 200 символов';
}
// Валидация product
if (!data.product || data.product.trim().length < 3) {
errors.product = 'Продукт должен содержать минимум 3 символа';
}
if (data.product && data.product.length > 200) {
errors.product = 'Продукт не должен превышать 200 символов';
}
// Валидация targetAudience
if (!data.targetAudience || data.targetAudience.trim().length < 3) {
errors.targetAudience =
'Целевая аудитория должна содержать минимум 3 символа';
}
if (data.targetAudience && data.targetAudience.length > 300) {
errors.targetAudience =
'Целевая аудитория не должна превышать 300 символов';
}
// Валидация region
if (!data.region || !VALID_REGIONS.includes(data.region)) {
errors.region = 'Выберите допустимый регион из списка';
}
// Валидация goal
if (!data.goal || data.goal.trim().length < 10) {
errors.goal = 'Цель должна содержать минимум 10 символов';
}
if (data.goal && data.goal.length > 500) {
errors.goal = 'Цель не должна превышать 500 символов';
}
// Валидация detailLevel
if (!data.detailLevel || !DETAIL_LEVELS.includes(data.detailLevel)) {
errors.detailLevel = 'Выберите допустимый уровень детализации';
}
// Валидация strongSide (опциональное)
if (data.strongSide && data.strongSide.length > 500) {
errors.strongSide = 'Сильная сторона не должна превышать 500 символов';
}
// Валидация weakSide (опциональное)
if (data.weakSide && data.weakSide.length > 500) {
errors.weakSide = 'Слабая сторона не должна превышать 500 символов';
}
// Валидация analysisType
const validAnalysisTypes = ['РЫНОК', 'КОНКУРЕНТЫ', 'ЦА', 'КАНАЛЫ', 'SWOT'];
if (!data.analysisType || !validAnalysisTypes.includes(data.analysisType)) {
errors.analysisType = 'Выберите допустимый тип анализа';
}
return {
valid: Object.keys(errors).length === 0,
errors,
};
}
```
---
## Миграция со старого API
Если у вас есть код, использующий старые поля, необходимо обновить его следующим образом:
### Старый формат (больше не работает):
```json
{
"product": "Разработка мобильных приложений",
"location": "Алматы, Казахстан",
"client": "B2B клиенты",
"differentiator": "Быстрая разработка за 2 недели",
"analysisType": "РЫНОК"
}
```
### Новый формат:
```json
{
"businessNiche": "Разработка программного обеспечения",
"product": "Разработка мобильных приложений",
"targetAudience": "B2B клиенты, технологические компании, стартапы",
"region": "Алматы",
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев",
"detailLevel": "СТАНДАРТНО",
"strongSide": "Быстрая разработка за 2 недели, опытная команда",
"weakSide": "",
"analysisType": "РЫНОК"
}
```
### Маппинг старых полей на новые:
| Старое поле | Новое поле(я) | Примечание |
| ---------------- | ----------------------------------------- | ------------------------------------------------ |
| `location` | `region` | Только название города из списка |
| `client` | `targetAudience` | Более детальное описание (свободный текст) |
| `differentiator` | `businessNiche`, `strongSide`, `weakSide` | Разделено на несколько полей для лучшего анализа |
---
## Обработка ошибок валидации
При ошибках валидации API возвращает следующий формат:
```json
{
"success": false,
"message": "Ошибка валидации",
"error": {
"code": "VALIDATION_ERROR",
"message": "Ошибка валидации входных данных",
"details": {
"businessNiche": "Поле 'businessNiche' должно содержать от 3 до 200 символов",
"region": "Поле 'region' должно быть одним из допустимых городов Казахстана",
"detailLevel": "Поле 'detailLevel' должно быть одним из: КРАТКО, СТАНДАРТНО, ПОДРОБНО"
}
}
}
```
---
## Важные замечания
1. **Регистр важен**: Значения `region`, `detailLevel` и `analysisType` чувствительны к регистру. Используйте точные значения из списка допустимых.
2. **Все новые поля передаются в OpenAI**: Все указанные поля включаются в контекст для генерации анализа, что улучшает качество и релевантность результатов.
3. **Уровень детализации влияет на результат**: Выбор `detailLevel` напрямую влияет на объем и глубину генерируемого анализа.
4. **Опциональные поля улучшают анализ**: Хотя `strongSide` и `weakSide` опциональны, их указание помогает AI лучше понять бизнес и дать более точные рекомендации.
5. **Обратная совместимость**: Старые поля больше не поддерживаются. Необходимо обновить все клиентские приложения.
---
## Поддержка
При возникновении проблем с API обращайтесь в техническую поддержку с указанием:
- `analysisId` (если есть)
- Время запроса
- Описание проблемы
- Код ошибки (если есть)
- Пример запроса (без чувствительных данных)
+16 -9
View File
@@ -226,37 +226,44 @@ const router = createRouter({
{
path: '/marketing-analysis',
name: 'marketing-main',
component: () => import('@/views/pages/marketing/MarketingMain.vue')
component: () => import('@/views/pages/marketing/MarketingMain.vue'),
meta: { requiresAuth: true }
},
{
path: '/marketing-analysis/analysis',
name: 'marketing-analysis',
component: () => import('@/views/pages/marketing/MarketingAnalysis.vue')
component: () => import('@/views/pages/marketing/MarketingAnalysis.vue'),
meta: { requiresAuth: true }
},
{
path: '/marketing-analysis/promotion',
name: 'marketing-promotion',
component: () => import('@/views/pages/marketing/MarketingPromotion.vue')
component: () => import('@/views/pages/marketing/MarketingPromotion.vue'),
meta: { requiresAuth: true }
},
{
path: '/marketing-analysis/results',
name: 'marketing-results',
component: () => import('@/views/pages/marketing/MarketingResults.vue')
component: () => import('@/views/pages/marketing/MarketingResults.vue'),
meta: { requiresAuth: true }
},
{
path: '/marketing-analysis/analyses',
name: 'marketing-analyses-list',
component: () => import('@/views/pages/marketing/MarketingAnalysesList.vue')
component: () => import('@/views/pages/marketing/MarketingAnalysesList.vue'),
meta: { requiresAuth: true }
},
{
path: '/marketing-analysis/strategies',
name: 'marketing-strategies-list',
component: () => import('@/views/pages/marketing/MarketingStrategiesList.vue')
component: () => import('@/views/pages/marketing/MarketingStrategiesList.vue'),
meta: { requiresAuth: true }
},
{
path: '/marketing-analysis/credentials',
name: 'marketing-credentials',
component: () => import('@/views/pages/marketing/MarketingCredentials.vue')
component: () => import('@/views/pages/marketing/MarketingCredentials.vue'),
meta: { requiresAuth: true }
},
{
path: '/pages/notfound',
@@ -284,13 +291,13 @@ const router = createRouter({
router.beforeEach(async (to) => {
const isAuthenticated = !!AuthService.getAccessToken();
if (to.meta.requiresAuth && !isAuthenticated) {
if (to.meta?.requiresAuth && !isAuthenticated) {
return { name: 'login', query: { redirect: to.fullPath } };
}
if (to.name === 'login' && isAuthenticated) {
return { path: '/' };
}
if (to.meta.requiresAdmin) {
if (to.meta?.requiresAdmin) {
try {
const me = await IdentityService.getCurrentUser();
const roles = Array.isArray(me?.roles) ? me.roles : [];
+44 -26
View File
@@ -8,38 +8,54 @@ class MarketingService {
* Запуск маркетингового анализа
* POST /api/marketing/analysis/start
* @param {Object} data - Данные для анализа
* @param {string} data.businessNiche - Ниша бизнеса
* @param {string} data.product - Название продукта или услуги
* @param {string} data.location - Географическая локация работы
* @param {string} data.client - Тип целевой аудитории
* @param {string} data.differentiator - Уникальные особенности бизнеса
* @param {string} data.targetAudience - Целевая аудитория (детальное описание)
* @param {string} data.region - Регион (город Казахстана)
* @param {string} data.goal - Цель на 6-12 месяцев
* @param {string} data.detailLevel - Уровень детализации анализа (КРАТКО, СТАНДАРТНО, ПОДРОБНО)
* @param {string} data.analysisType - Тип анализа (РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT)
* @param {string} [data.strongSide] - Сильная сторона бизнеса (опционально)
* @param {string} [data.weakSide] - Слабая сторона бизнеса (опционально)
* @returns {Promise<Object>} Объект с analysisId и статусом
*/
async startAnalysis(data) {
try {
const requestBody = {
businessNiche: data.businessNiche,
product: data.product,
targetAudience: data.targetAudience,
region: data.region,
goal: data.goal,
detailLevel: data.detailLevel,
analysisType: data.analysisType
};
// Добавляем опциональные поля только если они указаны
if (data.strongSide) {
requestBody.strongSide = data.strongSide;
}
if (data.weakSide) {
requestBody.weakSide = data.weakSide;
}
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_ANALYSIS_START}`, {
method: 'POST',
...DEFAULT_REQUEST_CONFIG,
body: JSON.stringify({
product: data.product,
location: data.location,
client: data.client,
differentiator: data.differentiator,
analysisType: data.analysisType
})
body: JSON.stringify(requestBody)
});
const result = await response.json();
const result = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(result.message || result.error?.message || 'Ошибка при запуске анализа');
throw new Error(result?.message || result?.error?.message || 'Ошибка при запуске анализа');
}
if (!result.success) {
throw new Error(result.message || 'Ошибка при запуске анализа');
if (!result || !result.success) {
throw new Error(result?.message || 'Ошибка при запуске анализа');
}
return result.data;
return result.data || null;
} catch (error) {
console.error('Ошибка при запуске маркетингового анализа:', error);
throw error;
@@ -56,17 +72,17 @@ class MarketingService {
try {
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_ANALYSIS_GET}/${analysisId}`);
const result = await response.json();
const result = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(result.message || result.error?.message || 'Ошибка при получении результата анализа');
throw new Error(result?.message || result?.error?.message || 'Ошибка при получении результата анализа');
}
if (!result.success) {
throw new Error(result.message || 'Ошибка при получении результата анализа');
if (!result || !result.success) {
throw new Error(result?.message || 'Ошибка при получении результата анализа');
}
return result.data;
return result.data || null;
} catch (error) {
console.error('Ошибка при получении результата анализа:', error);
throw error;
@@ -76,23 +92,25 @@ class MarketingService {
/**
* Получение списка всех анализов текущего пользователя
* GET /api/marketing/analysis/my
* @returns {Promise<Array>} Массив анализов с полями analysisId, product, location, clientType, differentiator, status, userId, createdAt, completedAt, statusHistory
* @returns {Promise<Array>} Массив анализов. Поля могут различаться в зависимости от версии API:
* Старые анализы: analysisId, product, location, clientType, differentiator, status, userId, createdAt, completedAt, statusHistory
* Новые анализы: analysisId, product, businessNiche, targetAudience, region, goal, detailLevel, strongSide, weakSide, status, userId, createdAt, completedAt, statusHistory
*/
async getMyAnalyses() {
try {
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_ANALYSIS_MY}`);
const result = await response.json();
const result = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(result.message || result.error?.message || 'Ошибка при получении списка анализов');
throw new Error(result?.message || result?.error?.message || 'Ошибка при получении списка анализов');
}
if (!result.success) {
throw new Error(result.message || 'Ошибка при получении списка анализов');
if (!result || !result.success) {
throw new Error(result?.message || 'Ошибка при получении списка анализов');
}
return result.data;
return result.data || [];
} catch (error) {
console.error('Ошибка при получении списка анализов:', error);
throw error;
@@ -11,17 +11,17 @@
</div>
<div class="card-content">
<DataTable
:value="analyses"
:value="analyses || []"
:loading="loading"
paginator
:rows="10"
:rowsPerPageOptions="[5, 10, 20, 50]"
sortMode="multiple"
sortMode="single"
removableSort
:sortField="'createdAt'"
:sortOrder="-1"
filterDisplay="row"
:globalFilterFields="['product', 'location', 'clientType', 'status', 'analysisType']"
filterDisplay="menu"
:globalFilterFields="['product', 'location', 'region', 'clientType', 'status', 'analysisType', 'businessNiche']"
v-model:filters="filters"
:filters="filters"
dataKey="analysisId"
@@ -30,7 +30,7 @@
<div class="flex justify-content-between align-items-center flex-wrap gap-2">
<span class="p-input-icon-left">
<i class="pi pi-search" />
<InputText v-model="filters['global'].value" placeholder="Поиск анализов..." />
<InputText v-model="filters.global.value" placeholder="Поиск анализов..." />
</span>
<Button label="Обновить" icon="pi pi-refresh" severity="secondary" @click="loadAnalyses" :loading="loading" />
</div>
@@ -38,19 +38,20 @@
<Column field="product" header="Продукт" :sortable="true" style="min-width: 200px">
<template #body="slotProps">
<span class="font-semibold">{{ slotProps.data.product }}</span>
<span class="font-semibold">{{ slotProps.data.product || '—' }}</span>
</template>
</Column>
<Column field="location" header="Локация" :sortable="true" style="min-width: 150px">
<template #body="slotProps">
<span>{{ slotProps.data.location }}</span>
<span>{{ slotProps.data.location || '—' }}</span>
</template>
</Column>
<Column field="clientType" header="Тип клиента" :sortable="true" style="min-width: 150px">
<template #body="slotProps">
<Tag :value="slotProps.data.clientType" severity="info" />
<Tag v-if="slotProps.data.clientType && slotProps.data.clientType !== '—'" :value="slotProps.data.clientType" severity="info" />
<span v-else class="text-surface-400"></span>
</template>
</Column>
@@ -66,7 +67,7 @@
<Tag :value="getStatusLabel(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
</template>
<template #filter="{ filterModel, filterCallback }">
<Dropdown v-model="filterModel.value" :options="statusOptions" placeholder="Все статусы" class="p-column-filter" @change="filterCallback()">
<Dropdown v-if="filterModel" v-model="filterModel.value" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="Все статусы" class="p-column-filter" @change="filterCallback && filterCallback()">
<template #option="slotProps">
<Tag :value="slotProps.option.label" :severity="slotProps.option.severity" />
</template>
@@ -161,11 +162,21 @@ const statusHistory = ref([]);
const historyDialogVisible = ref(false);
const currentAnalysisId = ref(null);
// Filters
const filters = ref({
// Filters - initialize with all possible filter fields
const initFilters = () => {
return {
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
status: { value: null, matchMode: FilterMatchMode.EQUALS }
});
product: { value: null, matchMode: FilterMatchMode.CONTAINS },
location: { value: null, matchMode: FilterMatchMode.CONTAINS },
region: { value: null, matchMode: FilterMatchMode.CONTAINS },
clientType: { value: null, matchMode: FilterMatchMode.CONTAINS },
status: { value: null, matchMode: FilterMatchMode.EQUALS },
analysisType: { value: null, matchMode: FilterMatchMode.CONTAINS },
businessNiche: { value: null, matchMode: FilterMatchMode.CONTAINS }
};
};
const filters = ref(initFilters());
// Status options for filter
const statusOptions = [
@@ -175,12 +186,53 @@ const statusOptions = [
{ label: 'Ошибка', value: 'failed', severity: 'danger' }
];
// Normalize analysis data to ensure all required fields exist
const normalizeAnalysis = (analysis) => {
if (!analysis) {
return null;
}
// Ensure analysisId exists - generate one if missing
const analysisId = analysis.analysisId || `temp-${Date.now()}-${Math.random()}`;
// Ensure createdAt exists for sorting - use current date if missing
const createdAt = analysis.createdAt || new Date().toISOString();
return {
...analysis,
analysisId,
product: analysis.product || analysis.businessNiche || '',
location: analysis.location || analysis.region || '',
region: analysis.region || analysis.location || '',
businessNiche: analysis.businessNiche || analysis.product || '',
clientType: analysis.clientType || '',
analysisType: analysis.analysisType || null,
status: analysis.status || 'unknown',
createdAt: createdAt,
completedAt: analysis.completedAt || null
};
};
// Load analyses
const loadAnalyses = async () => {
loading.value = true;
try {
const data = await MarketingService.getMyAnalyses();
analyses.value = data || [];
console.log('Raw data from API:', data);
// Ensure data is an array
if (!Array.isArray(data)) {
console.warn('Data is not an array:', data);
analyses.value = [];
return;
}
// Normalize all analyses to ensure they have all required fields
// Filter out any null values from normalization
const normalized = data.map(normalizeAnalysis).filter((analysis) => analysis !== null && analysis !== undefined);
console.log('Normalized analyses:', normalized);
analyses.value = normalized;
} catch (error) {
console.error('Ошибка при загрузке списка анализов:', error);
toast.add({
@@ -189,6 +241,7 @@ const loadAnalyses = async () => {
detail: error.message || 'Не удалось загрузить список анализов',
life: 5000
});
analyses.value = [];
} finally {
loading.value = false;
}
+140 -40
View File
@@ -10,31 +10,63 @@
<div class="card-content">
<form @submit.prevent="handleSubmit" class="space-y-4 max-w-2xl">
<div class="field">
<label for="product" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Что вы продаете? </label>
<InputText id="product" v-model="formData.product" placeholder="Введите название продукта" class="w-full" :class="{ 'p-invalid': errors.product }" />
<label for="businessNiche" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Ниша бизнеса * </label>
<InputText id="businessNiche" v-model="formData.businessNiche" placeholder="Например: E-commerce платформы" class="w-full" :class="{ 'p-invalid': errors.businessNiche }" />
<small v-if="errors.businessNiche" class="p-error">{{ errors.businessNiche }}</small>
</div>
<div class="field">
<label for="product" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Что вы продаете? * </label>
<InputText id="product" v-model="formData.product" placeholder="Введите название продукта или услуги" class="w-full" :class="{ 'p-invalid': errors.product }" />
<small v-if="errors.product" class="p-error">{{ errors.product }}</small>
</div>
<div class="field">
<label for="location" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Где вы работаете? </label>
<InputText id="location" v-model="formData.location" placeholder="Введите локацию" class="w-full" :class="{ 'p-invalid': errors.location }" />
<small v-if="errors.location" class="p-error">{{ errors.location }}</small>
<label for="targetAudience" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Целевая аудитория * </label>
<Textarea id="targetAudience" v-model="formData.targetAudience" placeholder="Опишите вашу целевую аудиторию детально" rows="3" class="w-full" :class="{ 'p-invalid': errors.targetAudience }" />
<small v-if="errors.targetAudience" class="p-error">{{ errors.targetAudience }}</small>
</div>
<div class="field">
<label for="client" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Кто ваш клиент? </label>
<Dropdown id="client" v-model="formData.client" :options="clientOptions" placeholder="Выберите тип клиента" class="w-full" :class="{ 'p-invalid': errors.client }" />
<small v-if="errors.client" class="p-error">{{ errors.client }}</small>
<label for="region" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Регион (город Казахстана) * </label>
<Dropdown id="region" v-model="formData.region" :options="VALID_REGIONS" placeholder="Выберите город" class="w-full" :class="{ 'p-invalid': errors.region }" />
<small v-if="errors.region" class="p-error">{{ errors.region }}</small>
</div>
<div class="field">
<label for="differentiator" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Чем ваш бизнес выделяется? </label>
<InputText id="differentiator" v-model="formData.differentiator" placeholder="Опишите уникальность" class="w-full" :class="{ 'p-invalid': errors.differentiator }" />
<small v-if="errors.differentiator" class="p-error">{{ errors.differentiator }}</small>
<label for="goal" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Цель на 6-12 месяцев * </label>
<Textarea id="goal" v-model="formData.goal" placeholder="Опишите вашу цель на ближайшие 6-12 месяцев" rows="3" class="w-full" :class="{ 'p-invalid': errors.goal }" />
<small v-if="errors.goal" class="p-error">{{ errors.goal }}</small>
</div>
<div class="field">
<label for="analysisType" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Тип анализа </label>
<label class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Уровень детализации анализа * </label>
<div class="flex gap-4">
<div v-for="level in DETAIL_LEVELS" :key="level" class="flex align-items-center">
<RadioButton :inputId="`detailLevel-${level}`" v-model="formData.detailLevel" :value="level" :class="{ 'p-invalid': errors.detailLevel }" />
<label :for="`detailLevel-${level}`" class="ml-2 cursor-pointer">
{{ level === 'КРАТКО' ? 'Кратко' : level === 'СТАНДАРТНО' ? 'Стандартно' : 'Подробно' }}
</label>
</div>
</div>
<small v-if="errors.detailLevel" class="p-error">{{ errors.detailLevel }}</small>
<small class="text-surface-500 dark:text-surface-400 mt-1 block">Выберите уровень детализации анализа</small>
</div>
<div class="field">
<label for="strongSide" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Сильная сторона бизнеса </label>
<Textarea id="strongSide" v-model="formData.strongSide" placeholder="Опишите сильные стороны вашего бизнеса (опционально)" rows="2" class="w-full" :class="{ 'p-invalid': errors.strongSide }" />
<small v-if="errors.strongSide" class="p-error">{{ errors.strongSide }}</small>
</div>
<div class="field">
<label for="weakSide" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Слабая сторона бизнеса </label>
<Textarea id="weakSide" v-model="formData.weakSide" placeholder="Опишите слабые стороны вашего бизнеса (опционально)" rows="2" class="w-full" :class="{ 'p-invalid': errors.weakSide }" />
<small v-if="errors.weakSide" class="p-error">{{ errors.weakSide }}</small>
</div>
<div class="field">
<label for="analysisType" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Тип анализа * </label>
<Dropdown
id="analysisType"
v-model="formData.analysisType"
@@ -197,7 +229,9 @@ import Button from 'primevue/button';
import Dropdown from 'primevue/dropdown';
import InputText from 'primevue/inputtext';
import ProgressSpinner from 'primevue/progressspinner';
import RadioButton from 'primevue/radiobutton';
import Tag from 'primevue/tag';
import Textarea from 'primevue/textarea';
import Toast from 'primevue/toast';
import { useToast } from 'primevue/usetoast';
import { onBeforeUnmount, onMounted, ref } from 'vue';
@@ -207,17 +241,45 @@ const toast = useToast();
const router = useRouter();
const route = useRoute();
// Constants
const VALID_REGIONS = [
'Алматы',
'Астана',
'Шымкент',
'Караганда',
'Актобе',
'Тараз',
'Павлодар',
'Усть-Каменогорск',
'Семей',
'Костанай',
'Кызылорда',
'Уральск',
'Петропавловск',
'Атырау',
'Актау',
'Туркестан',
'Кокшетау',
'Талдыкорган',
'Экибастуз',
'Рудный'
];
const DETAIL_LEVELS = ['КРАТКО', 'СТАНДАРТНО', 'ПОДРОБНО'];
// Form data
const formData = ref({
businessNiche: '',
product: '',
location: '',
client: null,
differentiator: '',
targetAudience: '',
region: null,
goal: '',
detailLevel: 'СТАНДАРТНО',
strongSide: '',
weakSide: '',
analysisType: null
});
const clientOptions = ['B2B клиенты', 'B2C клиенты', 'Частные лица', 'Корпорации', 'Малый бизнес'];
const analysisTypeOptions = [
{ label: 'РЫНОК - Анализ рынка', value: 'РЫНОК' },
{ label: 'КОНКУРЕНТЫ - Анализ конкурентов', value: 'КОНКУРЕНТЫ' },
@@ -245,6 +307,13 @@ const downloadingPdf = ref(false);
const validateForm = () => {
errors.value = {};
// BusinessNiche validation
if (!formData.value.businessNiche || formData.value.businessNiche.trim().length < 3) {
errors.value.businessNiche = 'Ниша бизнеса должна содержать минимум 3 символа';
} else if (formData.value.businessNiche.trim().length > 200) {
errors.value.businessNiche = 'Ниша бизнеса не должна превышать 200 символов';
}
// Product validation
if (!formData.value.product || formData.value.product.trim().length < 3) {
errors.value.product = 'Название продукта должно содержать минимум 3 символа';
@@ -252,25 +321,40 @@ const validateForm = () => {
errors.value.product = 'Название продукта не должно превышать 200 символов';
}
// Location validation
if (!formData.value.location || formData.value.location.trim().length < 2) {
errors.value.location = 'Локация должна содержать минимум 2 символа';
} else if (formData.value.location.trim().length > 150) {
errors.value.location = 'Локация не должна превышать 150 символов';
// TargetAudience validation
if (!formData.value.targetAudience || formData.value.targetAudience.trim().length < 3) {
errors.value.targetAudience = 'Целевая аудитория должна содержать минимум 3 символа';
} else if (formData.value.targetAudience.trim().length > 300) {
errors.value.targetAudience = 'Целевая аудитория не должна превышать 300 символов';
}
// Client validation
if (!formData.value.client) {
errors.value.client = 'Пожалуйста, выберите тип клиента';
} else if (!clientOptions.includes(formData.value.client)) {
errors.value.client = 'Выбран недопустимый тип клиента';
// Region validation
if (!formData.value.region) {
errors.value.region = 'Пожалуйста, выберите регион';
} else if (!VALID_REGIONS.includes(formData.value.region)) {
errors.value.region = 'Выбран недопустимый регион';
}
// Differentiator validation
if (!formData.value.differentiator || formData.value.differentiator.trim().length < 10) {
errors.value.differentiator = 'Описание уникальности должно содержать минимум 10 символов';
} else if (formData.value.differentiator.trim().length > 500) {
errors.value.differentiator = 'Описание уникальности не должно превышать 500 символов';
// Goal validation
if (!formData.value.goal || formData.value.goal.trim().length < 10) {
errors.value.goal = 'Цель должна содержать минимум 10 символов';
} else if (formData.value.goal.trim().length > 500) {
errors.value.goal = 'Цель не должна превышать 500 символов';
}
// DetailLevel validation
if (!formData.value.detailLevel || !DETAIL_LEVELS.includes(formData.value.detailLevel)) {
errors.value.detailLevel = 'Пожалуйста, выберите уровень детализации';
}
// StrongSide validation (optional)
if (formData.value.strongSide && formData.value.strongSide.trim().length > 500) {
errors.value.strongSide = 'Сильная сторона не должна превышать 500 символов';
}
// WeakSide validation (optional)
if (formData.value.weakSide && formData.value.weakSide.trim().length > 500) {
errors.value.weakSide = 'Слабая сторона не должна превышать 500 символов';
}
// AnalysisType validation
@@ -359,13 +443,25 @@ const handleSubmit = async () => {
submitting.value = true;
try {
const result = await MarketingService.startAnalysis({
const requestData = {
businessNiche: formData.value.businessNiche.trim(),
product: formData.value.product.trim(),
location: formData.value.location.trim(),
client: formData.value.client,
differentiator: formData.value.differentiator.trim(),
targetAudience: formData.value.targetAudience.trim(),
region: formData.value.region,
goal: formData.value.goal.trim(),
detailLevel: formData.value.detailLevel,
analysisType: formData.value.analysisType
});
};
// Добавляем опциональные поля только если они заполнены
if (formData.value.strongSide && formData.value.strongSide.trim()) {
requestData.strongSide = formData.value.strongSide.trim();
}
if (formData.value.weakSide && formData.value.weakSide.trim()) {
requestData.weakSide = formData.value.weakSide.trim();
}
const result = await MarketingService.startAnalysis(requestData);
analysisId.value = result.analysisId;
status.value = result.status;
@@ -431,10 +527,14 @@ const resetAnalysis = () => {
estimatedCompletionTime.value = null;
pollingAttempts.value = 0;
formData.value = {
businessNiche: '',
product: '',
location: '',
client: null,
differentiator: '',
targetAudience: '',
region: null,
goal: '',
detailLevel: 'СТАНДАРТНО',
strongSide: '',
weakSide: '',
analysisType: null
};
errors.value = {};
@@ -0,0 +1,47 @@
// vite.config.mjs
import { fileURLToPath, URL } from "node:url";
import { PrimeVueResolver } from "file:///Users/magzhanzhumabaev/Desktop/kt%20cloud/sakai-vue/node_modules/@primevue/auto-import-resolver/index.mjs";
import vue from "file:///Users/magzhanzhumabaev/Desktop/kt%20cloud/sakai-vue/node_modules/@vitejs/plugin-vue/dist/index.mjs";
import Components from "file:///Users/magzhanzhumabaev/Desktop/kt%20cloud/sakai-vue/node_modules/unplugin-vue-components/dist/vite.js";
import { defineConfig } from "file:///Users/magzhanzhumabaev/Desktop/kt%20cloud/sakai-vue/node_modules/vite/dist/node/index.js";
var __vite_injected_original_import_meta_url = "file:///Users/magzhanzhumabaev/Desktop/kt%20cloud/sakai-vue/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,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcubWpzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL21hZ3poYW56aHVtYWJhZXYvRGVza3RvcC9rdCBjbG91ZC9zYWthaS12dWVcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9tYWd6aGFuemh1bWFiYWV2L0Rlc2t0b3Ava3QgY2xvdWQvc2FrYWktdnVlL3ZpdGUuY29uZmlnLm1qc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvbWFnemhhbnpodW1hYmFldi9EZXNrdG9wL2t0JTIwY2xvdWQvc2FrYWktdnVlL3ZpdGUuY29uZmlnLm1qc1wiO2ltcG9ydCB7IGZpbGVVUkxUb1BhdGgsIFVSTCB9IGZyb20gJ25vZGU6dXJsJztcblxuaW1wb3J0IHsgUHJpbWVWdWVSZXNvbHZlciB9IGZyb20gJ0BwcmltZXZ1ZS9hdXRvLWltcG9ydC1yZXNvbHZlcic7XG5pbXBvcnQgdnVlIGZyb20gJ0B2aXRlanMvcGx1Z2luLXZ1ZSc7XG5pbXBvcnQgQ29tcG9uZW50cyBmcm9tICd1bnBsdWdpbi12dWUtY29tcG9uZW50cy92aXRlJztcbmltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gJ3ZpdGUnO1xuXG4vLyBodHRwczovL3ZpdGVqcy5kZXYvY29uZmlnL1xuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKHtcbiAgICBvcHRpbWl6ZURlcHM6IHtcbiAgICAgICAgbm9EaXNjb3Zlcnk6IHRydWVcbiAgICB9LFxuICAgIHBsdWdpbnM6IFtcbiAgICAgICAgdnVlKCksXG4gICAgICAgIENvbXBvbmVudHMoe1xuICAgICAgICAgICAgcmVzb2x2ZXJzOiBbUHJpbWVWdWVSZXNvbHZlcigpXVxuICAgICAgICB9KVxuICAgIF0sXG4gICAgcmVzb2x2ZToge1xuICAgICAgICBhbGlhczoge1xuICAgICAgICAgICAgJ0AnOiBmaWxlVVJMVG9QYXRoKG5ldyBVUkwoJy4vc3JjJywgaW1wb3J0Lm1ldGEudXJsKSlcbiAgICAgICAgfVxuICAgIH0sXG4gICAgc2VydmVyOiB7XG4gICAgICAgIHByb3h5OiB7XG4gICAgICAgICAgICAnL2FwaSc6IHtcbiAgICAgICAgICAgICAgICB0YXJnZXQ6ICdodHRwczovL2FwaS5rb250dXJhaS5reicsXG4gICAgICAgICAgICAgICAgY2hhbmdlT3JpZ2luOiB0cnVlLFxuICAgICAgICAgICAgICAgIHNlY3VyZTogdHJ1ZSxcbiAgICAgICAgICAgICAgICBjb25maWd1cmU6IChwcm94eSwgX29wdGlvbnMpID0+IHtcbiAgICAgICAgICAgICAgICAgICAgcHJveHkub24oJ2Vycm9yJywgKGVyciwgX3JlcSwgX3JlcykgPT4ge1xuICAgICAgICAgICAgICAgICAgICAgICAgY29uc29sZS5sb2coJ3Byb3h5IGVycm9yJywgZXJyKTtcbiAgICAgICAgICAgICAgICAgICAgfSk7XG4gICAgICAgICAgICAgICAgICAgIHByb3h5Lm9uKCdwcm94eVJlcScsIChwcm94eVJlcSwgcmVxLCBfcmVzKSA9PiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBjb25zb2xlLmxvZygnU2VuZGluZyBSZXF1ZXN0IHRvIHRoZSBUYXJnZXQ6JywgcmVxLm1ldGhvZCwgcmVxLnVybCk7XG4gICAgICAgICAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgICAgICAgICBwcm94eS5vbigncHJveHlSZXMnLCAocHJveHlSZXMsIHJlcSwgX3JlcykgPT4ge1xuICAgICAgICAgICAgICAgICAgICAgICAgY29uc29sZS5sb2coJ1JlY2VpdmVkIFJlc3BvbnNlIGZyb20gdGhlIFRhcmdldDonLCBwcm94eVJlcy5zdGF0dXNDb2RlLCByZXEudXJsKTtcbiAgICAgICAgICAgICAgICAgICAgfSk7XG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgfVxufSk7XG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQTRVLFNBQVMsZUFBZSxXQUFXO0FBRS9XLFNBQVMsd0JBQXdCO0FBQ2pDLE9BQU8sU0FBUztBQUNoQixPQUFPLGdCQUFnQjtBQUN2QixTQUFTLG9CQUFvQjtBQUxnTCxJQUFNLDJDQUEyQztBQVE5UCxJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUN4QixjQUFjO0FBQUEsSUFDVixhQUFhO0FBQUEsRUFDakI7QUFBQSxFQUNBLFNBQVM7QUFBQSxJQUNMLElBQUk7QUFBQSxJQUNKLFdBQVc7QUFBQSxNQUNQLFdBQVcsQ0FBQyxpQkFBaUIsQ0FBQztBQUFBLElBQ2xDLENBQUM7QUFBQSxFQUNMO0FBQUEsRUFDQSxTQUFTO0FBQUEsSUFDTCxPQUFPO0FBQUEsTUFDSCxLQUFLLGNBQWMsSUFBSSxJQUFJLFNBQVMsd0NBQWUsQ0FBQztBQUFBLElBQ3hEO0FBQUEsRUFDSjtBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ0osT0FBTztBQUFBLE1BQ0gsUUFBUTtBQUFBLFFBQ0osUUFBUTtBQUFBLFFBQ1IsY0FBYztBQUFBLFFBQ2QsUUFBUTtBQUFBLFFBQ1IsV0FBVyxDQUFDLE9BQU8sYUFBYTtBQUM1QixnQkFBTSxHQUFHLFNBQVMsQ0FBQyxLQUFLLE1BQU0sU0FBUztBQUNuQyxvQkFBUSxJQUFJLGVBQWUsR0FBRztBQUFBLFVBQ2xDLENBQUM7QUFDRCxnQkFBTSxHQUFHLFlBQVksQ0FBQyxVQUFVLEtBQUssU0FBUztBQUMxQyxvQkFBUSxJQUFJLGtDQUFrQyxJQUFJLFFBQVEsSUFBSSxHQUFHO0FBQUEsVUFDckUsQ0FBQztBQUNELGdCQUFNLEdBQUcsWUFBWSxDQUFDLFVBQVUsS0FBSyxTQUFTO0FBQzFDLG9CQUFRLElBQUksc0NBQXNDLFNBQVMsWUFBWSxJQUFJLEdBQUc7QUFBQSxVQUNsRixDQUFDO0FBQUEsUUFDTDtBQUFBLE1BQ0o7QUFBQSxJQUNKO0FBQUEsRUFDSjtBQUNKLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg==