.
This commit is contained in:
@@ -1283,6 +1283,77 @@ public class MarketingAnalysisService {
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет «пустые» фразы-заглушки про графики, если рядом нет плейсхолдера
|
||||
* [[CHART_...]] / [[TABLE_...]] или ```json:<key> блока.
|
||||
*
|
||||
* Мотивация: модель может написать «Анализ данных на графике ниже
|
||||
* показывает...», но забыть
|
||||
* вставить плейсхолдер. В результате в тексте остаётся пустая ссылка на «график
|
||||
* ниже».
|
||||
*/
|
||||
private String removeDanglingChartNarration(String text) {
|
||||
if (text == null || text.trim().isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
|
||||
String[] needles = {
|
||||
"Анализ данных на графике ниже показывает",
|
||||
"Из этих показателей следует, что"
|
||||
};
|
||||
|
||||
String[] lines = text.split("\\r?\\n", -1);
|
||||
List<String> out = new ArrayList<>(lines.length);
|
||||
|
||||
java.util.regex.Pattern placeholderPattern = java.util.regex.Pattern
|
||||
.compile("\\[\\[\\s*(CHART|TABLE)_[A-Za-z0-9_]+\\s*\\]\\]", java.util.regex.Pattern.CASE_INSENSITIVE);
|
||||
java.util.regex.Pattern jsonBlockStartPattern = java.util.regex.Pattern
|
||||
.compile("```\\s*json\\s*:", java.util.regex.Pattern.CASE_INSENSITIVE);
|
||||
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
String line = lines[i];
|
||||
String trimmed = line != null ? line.trim() : "";
|
||||
|
||||
boolean isNarration = false;
|
||||
for (String n : needles) {
|
||||
if (trimmed.toLowerCase().contains(n.toLowerCase())) {
|
||||
isNarration = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNarration) {
|
||||
out.add(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keep narration only if there is a placeholder or json block nearby (next 3
|
||||
// lines or same line).
|
||||
boolean hasVisualNear = false;
|
||||
int from = Math.max(0, i);
|
||||
int to = Math.min(lines.length - 1, i + 3);
|
||||
for (int j = from; j <= to; j++) {
|
||||
String ctx = lines[j] != null ? lines[j] : "";
|
||||
if (placeholderPattern.matcher(ctx).find() || jsonBlockStartPattern.matcher(ctx).find()) {
|
||||
hasVisualNear = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasVisualNear) {
|
||||
out.add(line);
|
||||
} else {
|
||||
// Drop this line (and if it's alone in a paragraph, later normalization will
|
||||
// collapse empty lines)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
String cleaned = String.join("\n", out);
|
||||
cleaned = cleaned.replaceAll("\\n{3,}", "\n\n").trim();
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Подготавливает контекстный JSON для конкретного раздела отчета.
|
||||
* Извлекает только релевантные данные для экономии токенов.
|
||||
@@ -1651,6 +1722,8 @@ public class MarketingAnalysisService {
|
||||
promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
|
||||
promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
|
||||
promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n\n");
|
||||
promptBuilder.append("ОБЯЗАТЕЛЬНАЯ СТРОКА:\n");
|
||||
promptBuilder.append("- Обязательно напиши строку [[CHART_SEASONALITY]] (на отдельной строке).\n\n");
|
||||
promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
|
||||
promptBuilder.append("III. Анализ рынка и сезонности\n\n");
|
||||
promptBuilder.append("Детально проанализируй:\n");
|
||||
@@ -1671,9 +1744,11 @@ public class MarketingAnalysisService {
|
||||
promptBuilder.append(
|
||||
"1. В логичном месте раздела (после описания сезонности) вставь плейсхолдер [[CHART_SEASONALITY]] на отдельной строке\n");
|
||||
promptBuilder.append(
|
||||
"2. ПЕРЕД графиком напиши вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике ниже показывает...\"\n");
|
||||
"2. ПЕРЕД плейсхолдером напиши вводный абзац (2-3 предложения), но ТОЛЬКО если следующей строкой действительно идет [[CHART_SEASONALITY]].\n");
|
||||
promptBuilder.append(
|
||||
"3. ПОСЛЕ графика напиши интерпретацию данных (2-3 предложения), начинающуюся с: \"Из этих показателей следует, что...\"\n\n");
|
||||
"3. ПОСЛЕ плейсхолдера напиши интерпретацию данных (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_SEASONALITY]].\n\n");
|
||||
promptBuilder.append(
|
||||
"ЗАПРЕТ НА ЗАГЛУШКИ:\n- Запрещено писать фразы типа \"Анализ данных на графике ниже показывает...\" и \"Из этих показателей следует, что...\", если ты НЕ вставил строку [[CHART_SEASONALITY]].\n\n");
|
||||
promptBuilder.append("ВАЖНО:\n");
|
||||
promptBuilder.append("- Начинай сразу с заголовка \"III. Анализ рынка и сезонности\"\n");
|
||||
promptBuilder.append("- Не добавляй вступления или заключения\n");
|
||||
@@ -1724,6 +1799,9 @@ public class MarketingAnalysisService {
|
||||
promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n");
|
||||
promptBuilder.append(
|
||||
"5. ЦИФРЫ ОБЯЗАТЕЛЬНЫ: в каждом подпункте делай выводы на основе процентов/чисел из audienceAnalysis.ageGroups и audienceAnalysis.genderDistribution.\n\n");
|
||||
promptBuilder.append("ОБЯЗАТЕЛЬНЫЕ СТРОКИ:\n");
|
||||
promptBuilder.append(
|
||||
"- Обязательно напиши строки [[CHART_AGE_DISTRIBUTION]] и [[CHART_GENDER_DISTRIBUTION]] (каждая на отдельной строке).\n\n");
|
||||
|
||||
promptBuilder.append("ЕСЛИ В JSON НЕТ ТОЧНЫХ ЦИФР (РАЗРЕШЕННАЯ АППРОКСИМАЦИЯ):\n");
|
||||
promptBuilder.append(
|
||||
@@ -1756,15 +1834,17 @@ public class MarketingAnalysisService {
|
||||
promptBuilder.append(
|
||||
"1. После разбора возрастных групп вставь плейсхолдер [[CHART_AGE_DISTRIBUTION]] на отдельной строке\n");
|
||||
promptBuilder.append(
|
||||
" - ПЕРЕД графиком: вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике ниже показывает...\"\n");
|
||||
" - ПЕРЕД плейсхолдером: вводный абзац (2-3 предложения), но ТОЛЬКО если следующей строкой идет [[CHART_AGE_DISTRIBUTION]].\n");
|
||||
promptBuilder.append(
|
||||
" - ПОСЛЕ графика: интерпретация (2-3 предложения), начинающаяся с: \"Из этих показателей следует, что...\"\n");
|
||||
" - ПОСЛЕ плейсхолдера: интерпретация (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_AGE_DISTRIBUTION]].\n");
|
||||
promptBuilder.append(
|
||||
"2. После разбора гендерного распределения вставь плейсхолдер [[CHART_GENDER_DISTRIBUTION]] на отдельной строке\n");
|
||||
promptBuilder.append(
|
||||
" - ПЕРЕД графиком: вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике ниже показывает...\"\n");
|
||||
" - ПЕРЕД плейсхолдером: вводный абзац (2-3 предложения), но ТОЛЬКО если следующей строкой идет [[CHART_GENDER_DISTRIBUTION]].\n");
|
||||
promptBuilder.append(
|
||||
" - ПОСЛЕ графика: интерпретация (2-3 предложения), начинающаяся с: \"Из этих показателей следует, что...\"\n\n");
|
||||
" - ПОСЛЕ плейсхолдера: интерпретация (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_GENDER_DISTRIBUTION]].\n\n");
|
||||
promptBuilder.append(
|
||||
"ЗАПРЕТ НА ЗАГЛУШКИ:\n- Запрещено писать фразы типа \"Анализ данных на графике ниже показывает...\" и \"Из этих показателей следует, что...\", если ты НЕ вставил соответствующий плейсхолдер графика.\n\n");
|
||||
promptBuilder.append(
|
||||
"ОБЯЗАТЕЛЬНО ПРО ГЕНДЕР:\n- Явно укажи долю женщин по данным genderDistribution (в типовом кейсе около 60%) и объясни, почему аудитория смещена в сторону женщин.\n");
|
||||
promptBuilder.append(
|
||||
@@ -1821,6 +1901,9 @@ public class MarketingAnalysisService {
|
||||
promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n");
|
||||
promptBuilder.append(
|
||||
"5. Это должен быть самый мощный раздел отчёта: каждое утверждение подтверждай цифрами из competitorAnalysis.marketShareChart / competitorAnalysis.comparisonTable.\n\n");
|
||||
promptBuilder.append("ОБЯЗАТЕЛЬНЫЕ СТРОКИ:\n");
|
||||
promptBuilder.append(
|
||||
"- Обязательно напиши строки [[CHART_MARKET_SHARE]] и [[TABLE_COMPARISON]] (каждая на отдельной строке).\n\n");
|
||||
promptBuilder.append(
|
||||
"КРИТИЧНО (ОБЯЗАТЕЛЬНЫЕ ТЕГИ): Ты ОБЯЗАН вставить тег [[CHART_MARKET_SHARE]] в начале раздела и [[TABLE_COMPARISON]] в середине. Без этих тегов отчет будет неполным.\n");
|
||||
promptBuilder.append(
|
||||
@@ -1847,15 +1930,17 @@ public class MarketingAnalysisService {
|
||||
promptBuilder.append(
|
||||
"1. В НАЧАЛЕ раздела (сразу после заголовка V. Анализ конкурентов) вставь плейсхолдер [[CHART_MARKET_SHARE]] на отдельной строке\n");
|
||||
promptBuilder.append(
|
||||
" - ПЕРЕД графиком: вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике ниже показывает...\"\n");
|
||||
" - ПЕРЕД плейсхолдером: вводный абзац (2-3 предложения), но ТОЛЬКО если следующей строкой идет [[CHART_MARKET_SHARE]].\n");
|
||||
promptBuilder.append(
|
||||
" - ПОСЛЕ графика: интерпретация (2-3 предложения), начинающаяся с: \"Из этих показателей следует, что...\"\n");
|
||||
" - ПОСЛЕ плейсхолдера: интерпретация (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_MARKET_SHARE]].\n");
|
||||
promptBuilder.append(
|
||||
"2. В СЕРЕДИНЕ раздела вставь плейсхолдер [[TABLE_COMPARISON]] на отдельной строке (это сравнительная таблица)\n");
|
||||
promptBuilder.append(
|
||||
" - ПЕРЕД таблицей: короткий ввод (1-2 предложения), что именно сравниваем\n");
|
||||
promptBuilder.append(
|
||||
" - ПОСЛЕ таблицы: интерпретация (3-5 предложений) с выводом по позиционированию\n\n");
|
||||
promptBuilder.append(
|
||||
"ЗАПРЕТ НА ЗАГЛУШКИ:\n- Запрещено писать фразы типа \"Анализ данных на графике ниже показывает...\" и \"Из этих показателей следует, что...\", если ты НЕ вставил строку [[CHART_MARKET_SHARE]].\n\n");
|
||||
promptBuilder.append(
|
||||
"ОБЯЗАТЕЛЬНОЕ СРАВНЕНИЕ С 'КОНКУРЕНТОМ A' И 'КОНКУРЕНТОМ B':\n");
|
||||
promptBuilder.append(
|
||||
@@ -2038,6 +2123,8 @@ public class MarketingAnalysisService {
|
||||
promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
|
||||
promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
|
||||
promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n");
|
||||
promptBuilder.append("ОБЯЗАТЕЛЬНАЯ СТРОКА:\n");
|
||||
promptBuilder.append("- Обязательно напиши строку [[CHART_FUNNEL]] (на отдельной строке).\n\n");
|
||||
promptBuilder.append(
|
||||
"ВНИМАНИЕ: Проверяй математическую логику. Количество покупок НЕ МОЖЕТ быть больше количества заявок. "
|
||||
+ "Конверсия не может превышать 100%. Если данные в JSON противоречивы, используй логические значения "
|
||||
@@ -2063,9 +2150,11 @@ public class MarketingAnalysisService {
|
||||
promptBuilder.append(
|
||||
"1. В логичном месте раздела (после описания этапов) вставь плейсхолдер [[CHART_FUNNEL]] на отдельной строке\n");
|
||||
promptBuilder.append(
|
||||
"2. ПЕРЕД графиком напиши вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике ниже показывает...\"\n");
|
||||
"2. ПЕРЕД плейсхолдером напиши вводный абзац (2-3 предложения), но ТОЛЬКО если следующей строкой идет [[CHART_FUNNEL]].\n");
|
||||
promptBuilder.append(
|
||||
"3. ПОСЛЕ графика напиши интерпретацию данных (2-3 предложения), начинающуюся с: \"Из этих показателей следует, что...\"\n\n");
|
||||
"3. ПОСЛЕ плейсхолдера напиши интерпретацию данных (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_FUNNEL]].\n\n");
|
||||
promptBuilder.append(
|
||||
"ЗАПРЕТ НА ЗАГЛУШКИ:\n- Запрещено писать фразы типа \"Анализ данных на графике ниже показывает...\" и \"Из этих показателей следует, что...\", если ты НЕ вставил строку [[CHART_FUNNEL]].\n\n");
|
||||
promptBuilder.append("ВАЖНО:\n");
|
||||
promptBuilder.append("- Начинай сразу с заголовка \"VIII. Воронка спроса\"\n");
|
||||
promptBuilder.append("- Не добавляй вступления или заключения\n");
|
||||
@@ -3347,8 +3436,9 @@ public class MarketingAnalysisService {
|
||||
report.put("analysisTypes", analysisTypeNames);
|
||||
report.put("detailLevel", detailLevelName);
|
||||
|
||||
// Очищаем текст от сообщений об ошибках AI перед сохранением
|
||||
// Очищаем текст: ошибки/отказы + "пустые" фразы про графики без реального блока
|
||||
String cleanedTextReport = processedTextReport != null ? removeErrorMessages(processedTextReport) : null;
|
||||
cleanedTextReport = cleanedTextReport != null ? removeDanglingChartNarration(cleanedTextReport) : null;
|
||||
if (cleanedTextReport != null && cleanedTextReport.trim().isEmpty()) {
|
||||
cleanedTextReport = "Анализ не удалось сгенерировать.";
|
||||
}
|
||||
@@ -3647,55 +3737,74 @@ public class MarketingAnalysisService {
|
||||
private String addMissingChartsToSections(String text, Map<String, Object> chartsData,
|
||||
Map<String, ChartMapping> placeholderMap, Set<String> processedPlaceholders) {
|
||||
String result = text;
|
||||
if (result == null || result.isBlank()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
Map<String, List<String>> sectionToPlaceholders = new HashMap<>();
|
||||
// NOTE: Используем regex-паттерны, чтобы совпасть с вариациями заголовков,
|
||||
// которые может генерировать модель
|
||||
sectionToPlaceholders.put("III\\.\\s*Анализ рынка и сезонности", List.of("[[CHART_SEASONALITY]]"));
|
||||
sectionToPlaceholders.put("IV\\.\\s*Анализ целевой аудитории",
|
||||
List.of("[[CHART_AGE_DISTRIBUTION]]", "[[CHART_GENDER_DISTRIBUTION]]"));
|
||||
sectionToPlaceholders.put("V\\.\\s*Анализ конкурентов",
|
||||
List.of("[[CHART_MARKET_SHARE]]", "[[TABLE_COMPARISON]]"));
|
||||
sectionToPlaceholders.put("VII\\.\\s*Каналы продвижения", List.of("[[CHART_CHANNELS]]"));
|
||||
sectionToPlaceholders.put("VIII\\.\\s*Воронка спроса", List.of("[[CHART_FUNNEL]]"));
|
||||
// SWOT может быть написан как SWOT-анализ / SWOT анализ / SWОT-анализ, поэтому
|
||||
// делаем мягче
|
||||
sectionToPlaceholders.put("VI\\.\\s*SWOT[-\\s]?анализ", List.of("[[CHART_SWOT]]"));
|
||||
// Section -> required placeholders by Roman numeral.
|
||||
// Must work for headings like:
|
||||
// - "III. Анализ рынка..."
|
||||
// - "## III. Анализ рынка..."
|
||||
// - "## V. Анализ конкурентов"
|
||||
Map<String, List<String>> romanToPlaceholders = new HashMap<>();
|
||||
romanToPlaceholders.put("III", List.of("[[CHART_SEASONALITY]]"));
|
||||
romanToPlaceholders.put("IV", List.of("[[CHART_AGE_DISTRIBUTION]]", "[[CHART_GENDER_DISTRIBUTION]]"));
|
||||
romanToPlaceholders.put("V", List.of("[[CHART_MARKET_SHARE]]", "[[TABLE_COMPARISON]]"));
|
||||
romanToPlaceholders.put("VI", List.of("[[CHART_SWOT]]"));
|
||||
romanToPlaceholders.put("VII", List.of("[[CHART_CHANNELS]]"));
|
||||
romanToPlaceholders.put("VIII", List.of("[[CHART_FUNNEL]]"));
|
||||
|
||||
// Умный fallback: если модель НЕ ВСТАВИЛА плейсхолдер в текст секции,
|
||||
// а данные для него есть — добавляем сам плейсхолдер в КОНЕЦ секции.
|
||||
// Далее replaceChartPlaceholders() (final pass) заменит плейсхолдер на
|
||||
// ```json:<key> блок.
|
||||
for (Map.Entry<String, List<String>> sectionEntry : sectionToPlaceholders.entrySet()) {
|
||||
String sectionPattern = sectionEntry.getKey();
|
||||
List<String> placeholders = sectionEntry.getValue();
|
||||
// Find all section headings by Roman numerals. Allow optional markdown heading
|
||||
// prefix.
|
||||
// Example matches:
|
||||
// "## III. Анализ рынка и сезонности"
|
||||
// "V. Анализ конкурентов"
|
||||
java.util.regex.Pattern sectionHeader = java.util.regex.Pattern.compile(
|
||||
"(?im)^(?:#{2,6}\\s*)?\\s*([IVX]{1,6})\\s*\\.\\s*.*$");
|
||||
java.util.regex.Matcher m = sectionHeader.matcher(result);
|
||||
|
||||
class SectionRange {
|
||||
final int start;
|
||||
final String roman;
|
||||
|
||||
SectionRange(int start, String roman) {
|
||||
this.start = start;
|
||||
this.roman = roman;
|
||||
}
|
||||
}
|
||||
|
||||
List<SectionRange> headers = new ArrayList<>();
|
||||
while (m.find()) {
|
||||
String roman = m.group(1) != null ? m.group(1).toUpperCase() : null;
|
||||
if (roman == null || roman.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (!romanToPlaceholders.containsKey(roman)) {
|
||||
continue;
|
||||
}
|
||||
headers.add(new SectionRange(m.start(), roman));
|
||||
}
|
||||
|
||||
if (headers.isEmpty()) {
|
||||
logger.debug("addMissingChartsToSections: No target sections found by roman headers");
|
||||
return result;
|
||||
}
|
||||
|
||||
// Insert from bottom to top so indices stay valid.
|
||||
for (int i = headers.size() - 1; i >= 0; i--) {
|
||||
SectionRange h = headers.get(i);
|
||||
int sectionStart = h.start;
|
||||
int sectionEnd = (i < headers.size() - 1) ? headers.get(i + 1).start : result.length();
|
||||
if (sectionStart < 0 || sectionEnd <= sectionStart || sectionEnd > result.length()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String sectionText = result.substring(sectionStart, sectionEnd);
|
||||
List<String> placeholders = romanToPlaceholders.get(h.roman);
|
||||
if (placeholders == null || placeholders.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1) Найти старт секции по заголовку (с ## или без)
|
||||
java.util.regex.Pattern sectionStartPattern = java.util.regex.Pattern.compile(
|
||||
"(?im)^(?:##+\\s*)?" + sectionPattern + ".*$");
|
||||
java.util.regex.Matcher sectionStartMatcher = sectionStartPattern.matcher(result);
|
||||
if (!sectionStartMatcher.find()) {
|
||||
logger.debug("addMissingChartsToSections: Section not found for pattern: {}", sectionPattern);
|
||||
continue;
|
||||
}
|
||||
|
||||
int sectionStart = sectionStartMatcher.start();
|
||||
int scanFrom = sectionStartMatcher.end();
|
||||
|
||||
// 2) Найти конец секции (следующий заголовок вида IV./V./VI... или Markdown
|
||||
// heading)
|
||||
java.util.regex.Pattern nextSectionPattern = java.util.regex.Pattern.compile(
|
||||
"(?im)^(?:##+\\s*)?[IVX]+\\.\\s+.*$");
|
||||
java.util.regex.Matcher nextSectionMatcher = nextSectionPattern.matcher(result);
|
||||
int sectionEnd = result.length();
|
||||
if (nextSectionMatcher.find(scanFrom)) {
|
||||
sectionEnd = nextSectionMatcher.start();
|
||||
}
|
||||
|
||||
String sectionText = result.substring(sectionStart, sectionEnd);
|
||||
StringBuilder tagsToAppend = new StringBuilder();
|
||||
boolean willAppendAnything = false;
|
||||
|
||||
@@ -3708,22 +3817,18 @@ public class MarketingAnalysisService {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Если график уже был обработан (заменен на ```json блок в другой части
|
||||
// текста),
|
||||
// то НИ В КОЕМ СЛУЧАЕ не добавляем его повторно в конец секции.
|
||||
String normalizedPlaceholder = normalizePlaceholderToken(placeholder);
|
||||
if (processedPlaceholders != null && processedPlaceholders.contains(normalizedPlaceholder)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Если плейсхолдер уже есть в тексте секции (даже с пробелами/регистром) — не
|
||||
// добавляем
|
||||
// If placeholder already exists in section (even with whitespace/case
|
||||
// variants), skip.
|
||||
if (sectionContainsPlaceholder(sectionText, placeholder)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Если JSON-блок для этого графика уже присутствует в секции — значит он уже
|
||||
// вставлен/обработан; не добавляем повторно в конец секции
|
||||
// If json block already exists in this section, mark as processed and skip.
|
||||
if (mapping.chartKey != null && sectionContainsJsonBlock(sectionText, mapping.chartKey)) {
|
||||
if (processedPlaceholders != null) {
|
||||
processedPlaceholders.add(normalizedPlaceholder);
|
||||
@@ -3731,30 +3836,27 @@ public class MarketingAnalysisService {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Если данные отсутствуют — не добавляем тег (он будет удален final pass-ом)
|
||||
// Only append if data exists and is not empty.
|
||||
if (chartsData == null || !chartsData.containsKey(mapping.dataKey)) {
|
||||
logger.warn(
|
||||
"addMissingChartsToSections: chartsData missing key '{}' for placeholder {} in section {}",
|
||||
mapping.dataKey, placeholder, sectionPattern);
|
||||
"addMissingChartsToSections: chartsData missing key '{}' for placeholder {} in roman section {}",
|
||||
mapping.dataKey, placeholder, h.roman);
|
||||
continue;
|
||||
}
|
||||
|
||||
Object chartData = chartsData.get(mapping.dataKey);
|
||||
if (isEmptyChartData(chartData)) {
|
||||
logger.warn(
|
||||
"addMissingChartsToSections: chartData empty for key '{}' (placeholder {}) in section {}",
|
||||
mapping.dataKey, placeholder, sectionPattern);
|
||||
"addMissingChartsToSections: chartData empty for key '{}' (placeholder {}) in roman section {}",
|
||||
mapping.dataKey, placeholder, h.roman);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Добавляем тег в конец секции. Это решает кейс: «модель забыла вставить
|
||||
// [[TABLE_COMPARISON]]»
|
||||
// Force append to the very end of this section.
|
||||
tagsToAppend.append("\n\n").append(placeholder).append("\n");
|
||||
willAppendAnything = true;
|
||||
|
||||
// Не считаем как processed (пусть final pass заменит его на JSON-блок)
|
||||
// но помечаем, чтобы ниже не пытаться добавлять повторно в рамках этой итерации
|
||||
processedPlaceholders.add(normalizedPlaceholder);
|
||||
if (processedPlaceholders != null) {
|
||||
processedPlaceholders.add(normalizedPlaceholder);
|
||||
}
|
||||
}
|
||||
|
||||
if (!willAppendAnything) {
|
||||
@@ -3762,8 +3864,7 @@ public class MarketingAnalysisService {
|
||||
}
|
||||
|
||||
result = result.substring(0, sectionEnd) + tagsToAppend + result.substring(sectionEnd);
|
||||
logger.debug("addMissingChartsToSections: Added missing placeholder tags to section pattern: {}",
|
||||
sectionPattern);
|
||||
logger.debug("addMissingChartsToSections: Added missing placeholder tags to roman section {}", h.roman);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user