.
This commit is contained in:
+115
-33
@@ -1,42 +1,124 @@
|
|||||||
## Frontend notes: rendering chart JSON blocks
|
## Документация для фронтенда: как отображать графики из маркетингового анализа
|
||||||
|
|
||||||
- Markdown now emits chart data as fenced code blocks with language tags like `json:seasonality`, `json:competitors`, `json:audienceSegmentation`, `json:channelsPotential`, and `json:funnel`.
|
Сервис возвращает данные для графиков в двух местах:
|
||||||
- Each block is pretty-printed JSON; parse the string inside the fence to feed the relevant chart component.
|
|
||||||
- Headings stay the same; only tables were replaced by these code fences.
|
|
||||||
|
|
||||||
### Expected shapes
|
- **`report.chartsData`** (объект-словарь ключ → данные графика/таблицы)
|
||||||
|
- **`report.fullAnalysis`** (markdown-текст), где данные вставлены **инлайн** как fenced-блоки:
|
||||||
|
- формат: ```json:<key> … ```
|
||||||
|
|
||||||
- `seasonality` – object map: `{ "Январь": 70, "Февраль": 60, ... }`.
|
Фронтенд может рендерить графики либо **по `chartsData` (проще)**, либо **инлайн** — парся `fullAnalysis` и заменяя ` ```json:<key>` на React-компоненты.
|
||||||
- `competitors` – array of objects: `{ name, reach, activity, reviews, strengths, weaknesses }`.
|
|
||||||
- `audienceSegmentation` – object with optional arrays: `ageGroups[]`, `genders[]`, `segments[]`, each item `{ label, value }`.
|
|
||||||
- `channelsPotential` – array of objects: `{ name, potential, justification }`.
|
|
||||||
- `funnel` – array of objects: `{ stage, value, conversion }`.
|
|
||||||
|
|
||||||
### Rendering hook (React + react-markdown)
|
---
|
||||||
|
|
||||||
```javascript
|
### Ключи `json:<key>`, которые нужно поддержать
|
||||||
components={{
|
|
||||||
code({ node, inline, className, children, ...props }) {
|
#### Chart.js config (рендерить через Chart.js / react-chartjs-2)
|
||||||
const match = /language-(\w+):([\w-]+)/.exec(className || "");
|
- **`seasonality`**: сезонность (Chart.js config)
|
||||||
if (!inline && match && match[1] === "json") {
|
- **`audienceAge`**: возрастное распределение (Chart.js config)
|
||||||
const chartType = match[2];
|
- **`audienceGender`**: гендерное распределение (Chart.js config)
|
||||||
const data = JSON.parse(String(children).replace(/\n$/, ""));
|
- **`marketShareChart`**: доли рынка (Chart.js config)
|
||||||
switch (chartType) {
|
- **`channelsPotential`**: потенциал каналов (Chart.js config)
|
||||||
case "seasonality": return <SeasonalityChart data={data} />;
|
- **`funnel`**: воронка (Chart.js config)
|
||||||
case "competitors": return <CompetitorsChart data={data} />;
|
Примечание: в `chartsData` ключ может быть `conversionFunnel`, но в тексте `fullAnalysis` блок идёт как `json:funnel`.
|
||||||
case "audienceSegmentation": return <AudienceSegmentationChart data={data} />;
|
|
||||||
case "channelsPotential": return <ChannelsPotentialChart data={data} />;
|
#### Таблица (рендерить табличным компонентом)
|
||||||
case "funnel": return <FunnelChart data={data} />;
|
- **`comparisonTable`**: сравнительная таблица конкурентов (обычно `Array<Object>`)
|
||||||
default: return <pre>{children}</pre>;
|
|
||||||
}
|
---
|
||||||
|
|
||||||
|
### Формат данных для графиков (Chart.js config)
|
||||||
|
|
||||||
|
Ваш пример — это **валидный Chart.js config** (минимально нужные поля: `labels`, `datasets[]`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"datasets": [
|
||||||
|
{
|
||||||
|
"data": [25, 30, 20, 25],
|
||||||
|
"label": "Возрастные группы"
|
||||||
}
|
}
|
||||||
return <code className={className} {...props}>{children}</code>;
|
],
|
||||||
}
|
"labels": ["18-24", "25-34", "35-44", "45+"]
|
||||||
}}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tips
|
Рендеринг (пример для Bar):
|
||||||
|
```ts
|
||||||
|
<Bar data={chartJson} />
|
||||||
|
```
|
||||||
|
|
||||||
- Trim trailing newline before `JSON.parse` to avoid parse errors.
|
---
|
||||||
- Keep a safe fallback (`<pre>`) for unknown chart types.
|
|
||||||
- If using another markdown renderer, ensure it preserves the code fence language string in a similar `json:<chart>` format.
|
### Инлайн-рендеринг из `fullAnalysis` (React + react-markdown)
|
||||||
|
|
||||||
|
Идея: перехватить `code`-блоки, найти `json:<key>`, распарсить JSON и заменить на компонент.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import React from "react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
|
||||||
|
function parseInfoString(className?: string) {
|
||||||
|
// react-markdown обычно кладёт info string как className вида:
|
||||||
|
// "language-json:audienceAge" или "language-json:marketShareChart"
|
||||||
|
const m = /language-([^ ]+)/.exec(className || "");
|
||||||
|
if (!m) return null;
|
||||||
|
const raw = m[1]; // "json:audienceAge"
|
||||||
|
const idx = raw.indexOf(":");
|
||||||
|
if (idx === -1) return { lang: raw, key: null };
|
||||||
|
return { lang: raw.slice(0, idx), key: raw.slice(idx + 1) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReportMarkdown({ markdown }: { markdown: string }) {
|
||||||
|
return (
|
||||||
|
<ReactMarkdown
|
||||||
|
components={{
|
||||||
|
code({ inline, className, children, ...props }) {
|
||||||
|
if (inline) return <code className={className} {...props}>{children}</code>;
|
||||||
|
|
||||||
|
const info = parseInfoString(className);
|
||||||
|
if (!info || info.lang !== "json" || !info.key) {
|
||||||
|
return <pre><code className={className} {...props}>{children}</code></pre>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = String(children).replace(/\n$/, "");
|
||||||
|
let data: any;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return <pre><code className={className} {...props}>{children}</code></pre>;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (info.key) {
|
||||||
|
case "seasonality":
|
||||||
|
return <SeasonalityChart data={data} />;
|
||||||
|
case "audienceAge":
|
||||||
|
return <AudienceAgeChart data={data} />;
|
||||||
|
case "audienceGender":
|
||||||
|
return <AudienceGenderChart data={data} />;
|
||||||
|
case "marketShareChart":
|
||||||
|
return <MarketShareChart data={data} />;
|
||||||
|
case "channelsPotential":
|
||||||
|
return <ChannelsPotentialChart data={data} />;
|
||||||
|
case "funnel":
|
||||||
|
return <FunnelChart data={data} />;
|
||||||
|
case "comparisonTable":
|
||||||
|
return <ComparisonTable data={data} />;
|
||||||
|
default:
|
||||||
|
return <pre><code className={className} {...props}>{children}</code></pre>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{markdown}
|
||||||
|
</ReactMarkdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Рекомендации по устойчивости
|
||||||
|
|
||||||
|
- **JSON.parse**: всегда `trim`/убирайте trailing newline (`replace(/\n$/, "")`).
|
||||||
|
- **Fallback**: для неизвестных ключей оставляйте `<pre>` (чтобы ничего не “ломалось”).
|
||||||
|
- **Таблицы**: `comparisonTable` лучше рендерить как таблицу, а не Chart.js.
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ public class MarketingAnalysisService {
|
|||||||
private static final String AI_PREMIUM_CONSULTING_STYLE_GUIDE = ""
|
private static final String AI_PREMIUM_CONSULTING_STYLE_GUIDE = ""
|
||||||
+ "СТИЛЬ (ОБЯЗАТЕЛЬНО): пиши как консалтинг «Большой четверки»: структурно, уверенно, с причинно-следственной логикой.\n"
|
+ "СТИЛЬ (ОБЯЗАТЕЛЬНО): пиши как консалтинг «Большой четверки»: структурно, уверенно, с причинно-следственной логикой.\n"
|
||||||
+ "Используй профессиональную лексику и леммы: «аналитическая экстраполяция», «дифференциация предложения», «конверсионный слой», «кастомизация оффера».\n"
|
+ "Используй профессиональную лексику и леммы: «аналитическая экстраполяция», «дифференциация предложения», «конверсионный слой», «кастомизация оффера».\n"
|
||||||
+ "Текст должен звучать дорого: меньше общих слов, больше управленческих выводов, четких аргументов и практических шагов.\n\n";
|
+ "Текст должен звучать дорого: меньше общих слов, больше управленческих выводов, четких аргументов и практических шагов.\n"
|
||||||
|
+ "ФОРМАТ (ОБЯЗАТЕЛЬНО): пиши аналитический текст короткими емкими абзацами (2–4 предложения), без «воды».\n"
|
||||||
|
+ "ГРАФИКИ (ОБЯЗАТЕЛЬНО): располагай теги графиков (например, [[CHART_...]]) непосредственно внутри или сразу после описания конкретных данных/цифр, к которым относится график.\n\n";
|
||||||
|
|
||||||
private void appendMandatoryJsonUsageWarning(StringBuilder promptBuilder) {
|
private void appendMandatoryJsonUsageWarning(StringBuilder promptBuilder) {
|
||||||
if (promptBuilder != null) {
|
if (promptBuilder != null) {
|
||||||
@@ -82,6 +84,58 @@ public class MarketingAnalysisService {
|
|||||||
return text.replaceAll(placeholderWithNewlines, "\n");
|
return text.replaceAll(placeholderWithNewlines, "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String buildChartJsonBlock(String chartKey, String jsonStr) {
|
||||||
|
// Keep block compact: do not add extra blank lines here (front-end correlates
|
||||||
|
// adjacent text with the chart).
|
||||||
|
return "```json:" + chartKey + "\n" + jsonStr + "\n```";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace a specific placeholder variant with a JSON fenced block while keeping
|
||||||
|
* spacing compact (avoid accidental \\n\\n\\n around inserted blocks).
|
||||||
|
*/
|
||||||
|
private String replacePlaceholderVariantWithJsonBlock(String text, String placeholderVariant, String jsonBlock) {
|
||||||
|
if (text == null || text.isBlank() || placeholderVariant == null || placeholderVariant.isBlank()
|
||||||
|
|| jsonBlock == null || jsonBlock.isBlank()) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
String result = text;
|
||||||
|
|
||||||
|
// 1) Standalone line occurrences (allow indentation). Replace the whole line
|
||||||
|
// to keep fences at the start of a line (no leading spaces before ```).
|
||||||
|
java.util.regex.Pattern standaloneLine = java.util.regex.Pattern.compile(
|
||||||
|
"(?m)^[ \\t]*" + java.util.regex.Pattern.quote(placeholderVariant) + "[ \\t]*$");
|
||||||
|
result = standaloneLine.matcher(result)
|
||||||
|
.replaceAll(java.util.regex.Matcher.quoteReplacement(jsonBlock));
|
||||||
|
|
||||||
|
// 2) Inline occurrences: force the block onto its own line with minimal
|
||||||
|
// spacing.
|
||||||
|
java.util.regex.Pattern inline = java.util.regex.Pattern
|
||||||
|
.compile(java.util.regex.Pattern.quote(placeholderVariant));
|
||||||
|
java.util.regex.Matcher m = inline.matcher(result);
|
||||||
|
StringBuilder sb = new StringBuilder(result.length() + jsonBlock.length());
|
||||||
|
int last = 0;
|
||||||
|
while (m.find()) {
|
||||||
|
int start = m.start();
|
||||||
|
int end = m.end();
|
||||||
|
sb.append(result, last, start);
|
||||||
|
|
||||||
|
boolean hasNewlineBefore = start > 0
|
||||||
|
&& (result.charAt(start - 1) == '\n' || result.charAt(start - 1) == '\r');
|
||||||
|
boolean hasNewlineAfter = end < result.length()
|
||||||
|
&& (result.charAt(end) == '\n' || result.charAt(end) == '\r');
|
||||||
|
|
||||||
|
String prefix = hasNewlineBefore ? "" : "\n";
|
||||||
|
String suffix = hasNewlineAfter ? "" : "\n";
|
||||||
|
|
||||||
|
sb.append(prefix).append(jsonBlock).append(suffix);
|
||||||
|
last = end;
|
||||||
|
}
|
||||||
|
sb.append(result, last, result.length());
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private static final String[] SECTION_ROMANS = new String[] { "I", "II", "III", "IV", "V", "VI", "VII", "VIII",
|
private static final String[] SECTION_ROMANS = new String[] { "I", "II", "III", "IV", "V", "VI", "VII", "VIII",
|
||||||
"IX", "X", "XI" };
|
"IX", "X", "XI" };
|
||||||
private static final Map<String, String> ROMAN_TO_ANCHOR_ID = Map.ofEntries(
|
private static final Map<String, String> ROMAN_TO_ANCHOR_ID = Map.ofEntries(
|
||||||
@@ -1723,7 +1777,8 @@ public class MarketingAnalysisService {
|
|||||||
promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
|
promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
|
||||||
promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n\n");
|
promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n\n");
|
||||||
promptBuilder.append("ОБЯЗАТЕЛЬНАЯ СТРОКА:\n");
|
promptBuilder.append("ОБЯЗАТЕЛЬНАЯ СТРОКА:\n");
|
||||||
promptBuilder.append("- Обязательно напиши строку [[CHART_SEASONALITY]] (на отдельной строке).\n\n");
|
promptBuilder.append(
|
||||||
|
"- Обязательно вставь тег [[CHART_SEASONALITY]] непосредственно рядом с описанием сезонности (внутри абзаца или сразу после него).\n\n");
|
||||||
promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
|
promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
|
||||||
promptBuilder.append("III. Анализ рынка и сезонности\n\n");
|
promptBuilder.append("III. Анализ рынка и сезонности\n\n");
|
||||||
promptBuilder.append("Детально проанализируй:\n");
|
promptBuilder.append("Детально проанализируй:\n");
|
||||||
@@ -1742,13 +1797,13 @@ public class MarketingAnalysisService {
|
|||||||
|
|
||||||
promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n");
|
promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"1. В логичном месте раздела (после описания сезонности) вставь плейсхолдер [[CHART_SEASONALITY]] на отдельной строке\n");
|
"1. В логичном месте раздела (сразу после конкретного описания сезонных колебаний/пиковых месяцев) вставь тег [[CHART_SEASONALITY]] рядом с этим описанием (внутри абзаца или сразу после него)\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"2. ПЕРЕД плейсхолдером напиши вводный абзац (2-3 предложения), но ТОЛЬКО если следующей строкой действительно идет [[CHART_SEASONALITY]].\n");
|
"2. ПЕРЕД тегом напиши вводный абзац (2-3 предложения), но ТОЛЬКО если ты действительно вставляешь тег [[CHART_SEASONALITY]].\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"3. ПОСЛЕ плейсхолдера напиши интерпретацию данных (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_SEASONALITY]].\n\n");
|
"3. ПОСЛЕ плейсхолдера напиши интерпретацию данных (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_SEASONALITY]].\n\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"ЗАПРЕТ НА ЗАГЛУШКИ:\n- Запрещено писать фразы типа \"Анализ данных на графике ниже показывает...\" и \"Из этих показателей следует, что...\", если ты НЕ вставил строку [[CHART_SEASONALITY]].\n\n");
|
"ЗАПРЕТ НА ЗАГЛУШКИ:\n- Запрещено писать фразы типа \"Анализ данных на графике показывает...\" и \"Из этих показателей следует, что...\", если ты НЕ вставил тег [[CHART_SEASONALITY]].\n\n");
|
||||||
promptBuilder.append("ВАЖНО:\n");
|
promptBuilder.append("ВАЖНО:\n");
|
||||||
promptBuilder.append("- Начинай сразу с заголовка \"III. Анализ рынка и сезонности\"\n");
|
promptBuilder.append("- Начинай сразу с заголовка \"III. Анализ рынка и сезонности\"\n");
|
||||||
promptBuilder.append("- Не добавляй вступления или заключения\n");
|
promptBuilder.append("- Не добавляй вступления или заключения\n");
|
||||||
@@ -1801,7 +1856,7 @@ public class MarketingAnalysisService {
|
|||||||
"5. ЦИФРЫ ОБЯЗАТЕЛЬНЫ: в каждом подпункте делай выводы на основе процентов/чисел из audienceAnalysis.ageGroups и audienceAnalysis.genderDistribution.\n\n");
|
"5. ЦИФРЫ ОБЯЗАТЕЛЬНЫ: в каждом подпункте делай выводы на основе процентов/чисел из audienceAnalysis.ageGroups и audienceAnalysis.genderDistribution.\n\n");
|
||||||
promptBuilder.append("ОБЯЗАТЕЛЬНЫЕ СТРОКИ:\n");
|
promptBuilder.append("ОБЯЗАТЕЛЬНЫЕ СТРОКИ:\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"- Обязательно напиши строки [[CHART_AGE_DISTRIBUTION]] и [[CHART_GENDER_DISTRIBUTION]] (каждая на отдельной строке).\n\n");
|
"- Обязательно вставь теги [[CHART_AGE_DISTRIBUTION]] и [[CHART_GENDER_DISTRIBUTION]] рядом с описанием конкретных процентов/выводов (внутри абзаца или сразу после него).\n\n");
|
||||||
|
|
||||||
promptBuilder.append("ЕСЛИ В JSON НЕТ ТОЧНЫХ ЦИФР (РАЗРЕШЕННАЯ АППРОКСИМАЦИЯ):\n");
|
promptBuilder.append("ЕСЛИ В JSON НЕТ ТОЧНЫХ ЦИФР (РАЗРЕШЕННАЯ АППРОКСИМАЦИЯ):\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
@@ -1832,19 +1887,19 @@ public class MarketingAnalysisService {
|
|||||||
promptBuilder.append("- Рекомендации по коммуникации с каждым сегментом\n\n");
|
promptBuilder.append("- Рекомендации по коммуникации с каждым сегментом\n\n");
|
||||||
promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКОВ (2 шт):\n");
|
promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКОВ (2 шт):\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"1. После разбора возрастных групп вставь плейсхолдер [[CHART_AGE_DISTRIBUTION]] на отдельной строке\n");
|
"1. Сразу после конкретного описания возрастных долей (проценты по возрастным группам) вставь тег [[CHART_AGE_DISTRIBUTION]] рядом с этим описанием (внутри абзаца или сразу после него)\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
" - ПЕРЕД плейсхолдером: вводный абзац (2-3 предложения), но ТОЛЬКО если следующей строкой идет [[CHART_AGE_DISTRIBUTION]].\n");
|
" - ПЕРЕД тегом: вводный абзац (2-3 предложения), но ТОЛЬКО если ты действительно вставляешь тег [[CHART_AGE_DISTRIBUTION]].\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
" - ПОСЛЕ плейсхолдера: интерпретация (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_AGE_DISTRIBUTION]].\n");
|
" - ПОСЛЕ плейсхолдера: интерпретация (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_AGE_DISTRIBUTION]].\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"2. После разбора гендерного распределения вставь плейсхолдер [[CHART_GENDER_DISTRIBUTION]] на отдельной строке\n");
|
"2. Сразу после конкретного описания гендерных долей (проценты) вставь тег [[CHART_GENDER_DISTRIBUTION]] рядом с этим описанием (внутри абзаца или сразу после него)\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
" - ПЕРЕД плейсхолдером: вводный абзац (2-3 предложения), но ТОЛЬКО если следующей строкой идет [[CHART_GENDER_DISTRIBUTION]].\n");
|
" - ПЕРЕД тегом: вводный абзац (2-3 предложения), но ТОЛЬКО если ты действительно вставляешь тег [[CHART_GENDER_DISTRIBUTION]].\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
" - ПОСЛЕ плейсхолдера: интерпретация (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_GENDER_DISTRIBUTION]].\n\n");
|
" - ПОСЛЕ плейсхолдера: интерпретация (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_GENDER_DISTRIBUTION]].\n\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"ЗАПРЕТ НА ЗАГЛУШКИ:\n- Запрещено писать фразы типа \"Анализ данных на графике ниже показывает...\" и \"Из этих показателей следует, что...\", если ты НЕ вставил соответствующий плейсхолдер графика.\n\n");
|
"ЗАПРЕТ НА ЗАГЛУШКИ:\n- Запрещено писать фразы типа \"Анализ данных на графике показывает...\" и \"Из этих показателей следует, что...\", если ты НЕ вставил соответствующий тег графика.\n\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"ОБЯЗАТЕЛЬНО ПРО ГЕНДЕР:\n- Явно укажи долю женщин по данным genderDistribution (в типовом кейсе около 60%) и объясни, почему аудитория смещена в сторону женщин.\n");
|
"ОБЯЗАТЕЛЬНО ПРО ГЕНДЕР:\n- Явно укажи долю женщин по данным genderDistribution (в типовом кейсе около 60%) и объясни, почему аудитория смещена в сторону женщин.\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
@@ -1903,7 +1958,7 @@ public class MarketingAnalysisService {
|
|||||||
"5. Это должен быть самый мощный раздел отчёта: каждое утверждение подтверждай цифрами из competitorAnalysis.marketShareChart / competitorAnalysis.comparisonTable.\n\n");
|
"5. Это должен быть самый мощный раздел отчёта: каждое утверждение подтверждай цифрами из competitorAnalysis.marketShareChart / competitorAnalysis.comparisonTable.\n\n");
|
||||||
promptBuilder.append("ОБЯЗАТЕЛЬНЫЕ СТРОКИ:\n");
|
promptBuilder.append("ОБЯЗАТЕЛЬНЫЕ СТРОКИ:\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"- Обязательно напиши строки [[CHART_MARKET_SHARE]] и [[TABLE_COMPARISON]] (каждая на отдельной строке).\n\n");
|
"- Обязательно вставь теги [[CHART_MARKET_SHARE]] и [[TABLE_COMPARISON]] рядом с теми утверждениями, где ты обсуждаешь конкретные цифры/сравнения (внутри абзаца или сразу после него).\n\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"КРИТИЧНО (ОБЯЗАТЕЛЬНЫЕ ТЕГИ): Ты ОБЯЗАН вставить тег [[CHART_MARKET_SHARE]] в начале раздела и [[TABLE_COMPARISON]] в середине. Без этих тегов отчет будет неполным.\n");
|
"КРИТИЧНО (ОБЯЗАТЕЛЬНЫЕ ТЕГИ): Ты ОБЯЗАН вставить тег [[CHART_MARKET_SHARE]] в начале раздела и [[TABLE_COMPARISON]] в середине. Без этих тегов отчет будет неполным.\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
@@ -1928,19 +1983,19 @@ public class MarketingAnalysisService {
|
|||||||
promptBuilder.append("- Рекомендации по дифференциации и конкурентным преимуществам\n\n");
|
promptBuilder.append("- Рекомендации по дифференциации и конкурентным преимуществам\n\n");
|
||||||
promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА И ТАБЛИЦЫ:\n");
|
promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА И ТАБЛИЦЫ:\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"1. В НАЧАЛЕ раздела (сразу после заголовка V. Анализ конкурентов) вставь плейсхолдер [[CHART_MARKET_SHARE]] на отдельной строке\n");
|
"1. В НАЧАЛЕ раздела (сразу после первого абзаца с выводом по долям рынка) вставь тег [[CHART_MARKET_SHARE]] рядом с этим выводом (внутри абзаца или сразу после него)\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
" - ПЕРЕД плейсхолдером: вводный абзац (2-3 предложения), но ТОЛЬКО если следующей строкой идет [[CHART_MARKET_SHARE]].\n");
|
" - ПЕРЕД тегом: вводный абзац (2-3 предложения), но ТОЛЬКО если ты действительно вставляешь тег [[CHART_MARKET_SHARE]].\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
" - ПОСЛЕ плейсхолдера: интерпретация (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_MARKET_SHARE]].\n");
|
" - ПОСЛЕ плейсхолдера: интерпретация (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_MARKET_SHARE]].\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"2. В СЕРЕДИНЕ раздела вставь плейсхолдер [[TABLE_COMPARISON]] на отдельной строке (это сравнительная таблица)\n");
|
"2. В СЕРЕДИНЕ раздела, сразу после абзаца где ты перечисляешь ключевые различия/feature-ы, вставь тег [[TABLE_COMPARISON]] рядом с этим описанием (внутри абзаца или сразу после него)\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
" - ПЕРЕД таблицей: короткий ввод (1-2 предложения), что именно сравниваем\n");
|
" - ПЕРЕД таблицей: короткий ввод (1-2 предложения), что именно сравниваем\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
" - ПОСЛЕ таблицы: интерпретация (3-5 предложений) с выводом по позиционированию\n\n");
|
" - ПОСЛЕ таблицы: интерпретация (3-5 предложений) с выводом по позиционированию\n\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"ЗАПРЕТ НА ЗАГЛУШКИ:\n- Запрещено писать фразы типа \"Анализ данных на графике ниже показывает...\" и \"Из этих показателей следует, что...\", если ты НЕ вставил строку [[CHART_MARKET_SHARE]].\n\n");
|
"ЗАПРЕТ НА ЗАГЛУШКИ:\n- Запрещено писать фразы типа \"Анализ данных на графике показывает...\" и \"Из этих показателей следует, что...\", если ты НЕ вставил тег [[CHART_MARKET_SHARE]].\n\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"ОБЯЗАТЕЛЬНОЕ СРАВНЕНИЕ С 'КОНКУРЕНТОМ A' И 'КОНКУРЕНТОМ B':\n");
|
"ОБЯЗАТЕЛЬНОЕ СРАВНЕНИЕ С 'КОНКУРЕНТОМ A' И 'КОНКУРЕНТОМ B':\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
@@ -2012,9 +2067,9 @@ public class MarketingAnalysisService {
|
|||||||
promptBuilder.append("- Практические рекомендации на основе SWOT-матрицы\n\n");
|
promptBuilder.append("- Практические рекомендации на основе SWOT-матрицы\n\n");
|
||||||
promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n");
|
promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"1. В логичном месте раздела (после SWOT-анализа) вставь плейсхолдер [[CHART_SWOT]] на отдельной строке\n");
|
"1. Сразу после конкретного описания пунктов SWOT (S/W/O/T) вставь тег [[CHART_SWOT]] рядом с этим описанием (внутри абзаца или сразу после него)\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"2. ПЕРЕД графиком напиши вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике ниже показывает...\"\n");
|
"2. ПЕРЕД графиком напиши вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике показывает...\"\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"3. ПОСЛЕ графика напиши интерпретацию данных (2-3 предложения), начинающуюся с: \"Из этих показателей следует, что...\"\n\n");
|
"3. ПОСЛЕ графика напиши интерпретацию данных (2-3 предложения), начинающуюся с: \"Из этих показателей следует, что...\"\n\n");
|
||||||
promptBuilder.append("ВАЖНО:\n");
|
promptBuilder.append("ВАЖНО:\n");
|
||||||
@@ -2077,9 +2132,9 @@ public class MarketingAnalysisService {
|
|||||||
promptBuilder.append("- Интеграция каналов в единую маркетинговую стратегию\n\n");
|
promptBuilder.append("- Интеграция каналов в единую маркетинговую стратегию\n\n");
|
||||||
promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n");
|
promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"1. В логичном месте раздела (после анализа каналов) вставь плейсхолдер [[CHART_CHANNELS]] на отдельной строке\n");
|
"1. Сразу после абзаца, где ты сравниваешь/ранжируешь каналы с конкретными аргументами, вставь тег [[CHART_CHANNELS]] рядом с этим описанием (внутри абзаца или сразу после него)\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"2. ПЕРЕД графиком напиши вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике ниже показывает...\"\n");
|
"2. ПЕРЕД графиком напиши вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике показывает...\"\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"3. ПОСЛЕ графика напиши интерпретацию данных (2-3 предложения), начинающуюся с: \"Из этих показателей следует, что...\"\n\n");
|
"3. ПОСЛЕ графика напиши интерпретацию данных (2-3 предложения), начинающуюся с: \"Из этих показателей следует, что...\"\n\n");
|
||||||
promptBuilder.append("ВАЖНО:\n");
|
promptBuilder.append("ВАЖНО:\n");
|
||||||
@@ -2124,7 +2179,8 @@ public class MarketingAnalysisService {
|
|||||||
promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
|
promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
|
||||||
promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n");
|
promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n");
|
||||||
promptBuilder.append("ОБЯЗАТЕЛЬНАЯ СТРОКА:\n");
|
promptBuilder.append("ОБЯЗАТЕЛЬНАЯ СТРОКА:\n");
|
||||||
promptBuilder.append("- Обязательно напиши строку [[CHART_FUNNEL]] (на отдельной строке).\n\n");
|
promptBuilder.append(
|
||||||
|
"- Обязательно вставь тег [[CHART_FUNNEL]] непосредственно рядом с описанием этапов/конверсий (внутри абзаца или сразу после него).\n\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"ВНИМАНИЕ: Проверяй математическую логику. Количество покупок НЕ МОЖЕТ быть больше количества заявок. "
|
"ВНИМАНИЕ: Проверяй математическую логику. Количество покупок НЕ МОЖЕТ быть больше количества заявок. "
|
||||||
+ "Конверсия не может превышать 100%. Если данные в JSON противоречивы, используй логические значения "
|
+ "Конверсия не может превышать 100%. Если данные в JSON противоречивы, используй логические значения "
|
||||||
@@ -2148,13 +2204,13 @@ public class MarketingAnalysisService {
|
|||||||
promptBuilder.append("- Интеграция воронки с каналами продвижения\n\n");
|
promptBuilder.append("- Интеграция воронки с каналами продвижения\n\n");
|
||||||
promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n");
|
promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"1. В логичном месте раздела (после описания этапов) вставь плейсхолдер [[CHART_FUNNEL]] на отдельной строке\n");
|
"1. Сразу после описания этапов и ключевых выводов по узким местам вставь тег [[CHART_FUNNEL]] рядом с этим описанием (внутри абзаца или сразу после него)\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"2. ПЕРЕД плейсхолдером напиши вводный абзац (2-3 предложения), но ТОЛЬКО если следующей строкой идет [[CHART_FUNNEL]].\n");
|
"2. ПЕРЕД тегом напиши вводный абзац (2-3 предложения), но ТОЛЬКО если ты действительно вставляешь тег [[CHART_FUNNEL]].\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"3. ПОСЛЕ плейсхолдера напиши интерпретацию данных (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_FUNNEL]].\n\n");
|
"3. ПОСЛЕ плейсхолдера напиши интерпретацию данных (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_FUNNEL]].\n\n");
|
||||||
promptBuilder.append(
|
promptBuilder.append(
|
||||||
"ЗАПРЕТ НА ЗАГЛУШКИ:\n- Запрещено писать фразы типа \"Анализ данных на графике ниже показывает...\" и \"Из этих показателей следует, что...\", если ты НЕ вставил строку [[CHART_FUNNEL]].\n\n");
|
"ЗАПРЕТ НА ЗАГЛУШКИ:\n- Запрещено писать фразы типа \"Анализ данных на графике показывает...\" и \"Из этих показателей следует, что...\", если ты НЕ вставил тег [[CHART_FUNNEL]].\n\n");
|
||||||
promptBuilder.append("ВАЖНО:\n");
|
promptBuilder.append("ВАЖНО:\n");
|
||||||
promptBuilder.append("- Начинай сразу с заголовка \"VIII. Воронка спроса\"\n");
|
promptBuilder.append("- Начинай сразу с заголовка \"VIII. Воронка спроса\"\n");
|
||||||
promptBuilder.append("- Не добавляй вступления или заключения\n");
|
promptBuilder.append("- Не добавляй вступления или заключения\n");
|
||||||
@@ -3660,10 +3716,8 @@ public class MarketingAnalysisService {
|
|||||||
if (!isEmptyChartData(chartData)) {
|
if (!isEmptyChartData(chartData)) {
|
||||||
try {
|
try {
|
||||||
String jsonStr = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(chartData);
|
String jsonStr = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(chartData);
|
||||||
String jsonBlock = "\n```json:" + mapping.chartKey + "\n" + jsonStr + "\n```\n";
|
String jsonBlock = buildChartJsonBlock(mapping.chartKey, jsonStr);
|
||||||
// Replace all occurrences of this exact variant
|
result = replacePlaceholderVariantWithJsonBlock(result, placeholderVariant, jsonBlock);
|
||||||
result = result.replaceAll(java.util.regex.Pattern.quote(placeholderVariant),
|
|
||||||
java.util.regex.Matcher.quoteReplacement(jsonBlock));
|
|
||||||
processedPlaceholders.add(normalized);
|
processedPlaceholders.add(normalized);
|
||||||
logger.info("Successfully replaced placeholder: {} (normalized {}) with dataKey: {}",
|
logger.info("Successfully replaced placeholder: {} (normalized {}) with dataKey: {}",
|
||||||
placeholderVariant, normalized, mapping.dataKey);
|
placeholderVariant, normalized, mapping.dataKey);
|
||||||
@@ -3698,9 +3752,8 @@ public class MarketingAnalysisService {
|
|||||||
try {
|
try {
|
||||||
String jsonStr = objectMapper.writerWithDefaultPrettyPrinter()
|
String jsonStr = objectMapper.writerWithDefaultPrettyPrinter()
|
||||||
.writeValueAsString(chartsData.get(mapping.dataKey));
|
.writeValueAsString(chartsData.get(mapping.dataKey));
|
||||||
String jsonBlock = "\n```json:" + mapping.chartKey + "\n" + jsonStr + "\n```\n";
|
String jsonBlock = buildChartJsonBlock(mapping.chartKey, jsonStr);
|
||||||
result = result.replaceAll(java.util.regex.Pattern.quote(placeholderVariant),
|
result = replacePlaceholderVariantWithJsonBlock(result, placeholderVariant, jsonBlock);
|
||||||
java.util.regex.Matcher.quoteReplacement(jsonBlock));
|
|
||||||
processedPlaceholders.add(normalized);
|
processedPlaceholders.add(normalized);
|
||||||
continue;
|
continue;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user