diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index 545f740..49a89cb 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -380,8 +380,16 @@ public class MarketingAnalysisService { promptBuilder.append(" - без академического жаргона,\n"); promptBuilder.append(" - без лишней теории маркетинга,\n"); promptBuilder.append(" - с акцентом на практическую пользу.\n\n"); - promptBuilder.append("4. Не повторяй сам JSON.\n"); - promptBuilder.append(" Отчёт — это человеческое объяснение на основе уже рассчитанных данных.\n\n"); + promptBuilder.append("4. Вставляй специальные метки плейсхолдеров там, где должны быть графики:\n\n"); + promptBuilder.append(" - [[CHART_SEASONALITY]] — внутри раздела \"III. Анализ рынка и сезонности\"\n"); + promptBuilder.append(" - [[CHART_AUDIENCE]] — внутри раздела \"IV. Анализ целевой аудитории\"\n"); + promptBuilder.append(" - [[CHART_COMPETITORS]] — внутри раздела \"V. Анализ конкурентов\"\n"); + promptBuilder + .append(" - [[CHART_CHANNELS]] — внутри раздела \"VII. Каналы продвижения и их потенциал\"\n"); + promptBuilder.append(" - [[CHART_FUNNEL]] — внутри раздела \"VIII. Воронка спроса\"\n"); + promptBuilder.append(" - [[CHART_SWOT]] — внутри раздела \"VI. SWOT-анализ\"\n\n"); + promptBuilder.append( + " Вставляй метки в логичных местах текста, где график будет уместен для иллюстрации данных.\n\n"); promptBuilder.append("5. Не выдумывай новые цифры — используй только те, что есть в JSON.\n"); String prompt = promptBuilder.toString(); @@ -1081,79 +1089,6 @@ public class MarketingAnalysisService { } markdown.append("\n"); - // Add chart data tables - @SuppressWarnings("unchecked") - Map chartsData = (Map) reportData.get("chartsData"); - if (chartsData != null && !chartsData.isEmpty()) { - markdown.append("## 📊 Числовые данные для графиков\n\n"); - - // Seasonality JSON block - @SuppressWarnings("unchecked") - Map seasonality = (Map) chartsData.get("seasonality"); - appendJsonChart(markdown, "### 1️⃣ Сезонность спроса (12 месяцев)", "seasonality", seasonality); - - // Competitors JSON block - @SuppressWarnings("unchecked") - List> competitors = (List>) chartsData.get("competitors"); - appendJsonChart(markdown, "### 2️⃣ Сравнительный анализ конкурентов", "competitors", competitors); - - // Audience segmentation JSON block - @SuppressWarnings("unchecked") - Map audienceSegmentation = (Map) chartsData.get("audienceSegmentation"); - appendJsonChart(markdown, "### 3️⃣ Сегментация целевой аудитории", "audienceSegmentation", - audienceSegmentation); - - // Channels potential JSON block - @SuppressWarnings("unchecked") - List> channelsPotential = (List>) chartsData - .get("channelsPotential"); - appendJsonChart(markdown, "### 4️⃣ Потенциал каналов продвижения", "channelsPotential", channelsPotential); - - // Conversion funnel JSON block - @SuppressWarnings("unchecked") - List> conversionFunnel = (List>) chartsData.get("conversionFunnel"); - appendJsonChart(markdown, "### 5️⃣ Конверсионная воронка", "funnel", conversionFunnel); - - // SWOT table (if available from JSON) - @SuppressWarnings("unchecked") - Map swot = (Map) chartsData.get("swot"); - if (swot != null) { - markdown.append("### 6️⃣ SWOT-анализ\n\n"); - @SuppressWarnings("unchecked") - List strengths = (List) swot.get("strengths"); - @SuppressWarnings("unchecked") - List weaknesses = (List) swot.get("weaknesses"); - @SuppressWarnings("unchecked") - List opportunities = (List) swot.get("opportunities"); - @SuppressWarnings("unchecked") - List threats = (List) swot.get("threats"); - - markdown.append("| Сильные стороны | Слабые стороны |\n"); - markdown.append("|----------------|----------------|\n"); - markdown.append("| "); - if (strengths != null && !strengths.isEmpty()) { - markdown.append(String.join("; ", strengths)); - } - markdown.append(" | "); - if (weaknesses != null && !weaknesses.isEmpty()) { - markdown.append(String.join("; ", weaknesses)); - } - markdown.append(" |\n\n"); - - markdown.append("| Возможности | Угрозы |\n"); - markdown.append("|-------------|-------|\n"); - markdown.append("| "); - if (opportunities != null && !opportunities.isEmpty()) { - markdown.append(String.join("; ", opportunities)); - } - markdown.append(" | "); - if (threats != null && !threats.isEmpty()) { - markdown.append(String.join("; ", threats)); - } - markdown.append(" |\n\n"); - } - } - // Add additional sections from JSON analysis if available @SuppressWarnings("unchecked") Map analysisJson = (Map) reportData.get("analysisJson"); @@ -1256,8 +1191,13 @@ public class MarketingAnalysisService { // Display full analysis with 10 sections String fullAnalysis = (String) reportData.get("fullAnalysis"); if (fullAnalysis != null && !fullAnalysis.trim().isEmpty()) { + // Получаем данные для графиков и заменяем плейсхолдеры + @SuppressWarnings("unchecked") + Map chartsData = (Map) reportData.get("chartsData"); + String processedAnalysis = replaceChartPlaceholders(fullAnalysis, chartsData); + markdown.append("## 📘 Полный маркетинговый анализ\n\n"); - markdown.append(fullAnalysis).append("\n\n"); + markdown.append(processedAnalysis).append("\n\n"); } else { // Fallback to summary if fullAnalysis is not available String summary = (String) reportData.get("summary"); @@ -1352,6 +1292,157 @@ public class MarketingAnalysisService { return false; } + /** + * Заменяет плейсхолдеры в тексте отчета на JSON-блоки с данными графиков + * + * @param fullAnalysis текст отчета с плейсхолдерами + * @param chartsData данные для графиков + * @return текст отчета с замененными плейсхолдерами на JSON-блоки + */ + private String replaceChartPlaceholders(String fullAnalysis, Map chartsData) { + if (fullAnalysis == null || fullAnalysis.trim().isEmpty()) { + return fullAnalysis; + } + + if (chartsData == null || chartsData.isEmpty()) { + // Если данных нет, просто удаляем все плейсхолдеры + return fullAnalysis + .replace("[[CHART_SEASONALITY]]", "") + .replace("[[CHART_AUDIENCE]]", "") + .replace("[[CHART_COMPETITORS]]", "") + .replace("[[CHART_CHANNELS]]", "") + .replace("[[CHART_FUNNEL]]", "") + .replace("[[CHART_SWOT]]", ""); + } + + String result = fullAnalysis; + Set processedPlaceholders = new HashSet<>(); + + // Маппинг плейсхолдеров на ключи данных и chartKey + Map placeholderMap = new HashMap<>(); + placeholderMap.put("[[CHART_SEASONALITY]]", new ChartMapping("seasonality", "seasonality")); + placeholderMap.put("[[CHART_AUDIENCE]]", new ChartMapping("audienceSegmentation", "audienceSegmentation")); + placeholderMap.put("[[CHART_COMPETITORS]]", new ChartMapping("competitors", "competitors")); + placeholderMap.put("[[CHART_CHANNELS]]", new ChartMapping("channelsPotential", "channelsPotential")); + placeholderMap.put("[[CHART_FUNNEL]]", new ChartMapping("conversionFunnel", "funnel")); + placeholderMap.put("[[CHART_SWOT]]", new ChartMapping("swot", "swot")); + + // Заменяем найденные плейсхолдеры + for (Map.Entry entry : placeholderMap.entrySet()) { + String placeholder = entry.getKey(); + ChartMapping mapping = entry.getValue(); + + if (result.contains(placeholder)) { + processedPlaceholders.add(placeholder); + Object chartData = chartsData.get(mapping.dataKey); + + if (!isEmptyChartData(chartData)) { + try { + String jsonStr = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(chartData); + String jsonBlock = "\n```json:" + mapping.chartKey + "\n" + jsonStr + "\n```\n"; + result = result.replace(placeholder, jsonBlock); + } catch (JsonProcessingException e) { + logger.warn("Failed to serialize chart data for {}: {}", mapping.dataKey, e.getMessage()); + // Удаляем плейсхолдер при ошибке сериализации + result = result.replace(placeholder, ""); + } + } else { + // Данных нет - удаляем плейсхолдер + result = result.replace(placeholder, ""); + } + } + } + + // Fallback: добавляем графики в конец соответствующих разделов, если + // плейсхолдер отсутствовал + result = addMissingChartsToSections(result, chartsData, placeholderMap, processedPlaceholders); + + return result; + } + + /** + * Вспомогательный класс для маппинга плейсхолдеров + */ + private static class ChartMapping { + final String dataKey; + final String chartKey; + + ChartMapping(String dataKey, String chartKey) { + this.dataKey = dataKey; + this.chartKey = chartKey; + } + } + + /** + * Добавляет графики в конец соответствующих разделов, если плейсхолдер + * отсутствовал, но данные есть + */ + private String addMissingChartsToSections(String text, Map chartsData, + Map placeholderMap, Set processedPlaceholders) { + String result = text; + + // Маппинг разделов на плейсхолдеры + Map sectionToPlaceholder = new HashMap<>(); + sectionToPlaceholder.put("III\\.\\s*Анализ рынка и сезонности", "[[CHART_SEASONALITY]]"); + sectionToPlaceholder.put("IV\\.\\s*Анализ целевой аудитории", "[[CHART_AUDIENCE]]"); + sectionToPlaceholder.put("V\\.\\s*Анализ конкурентов", "[[CHART_COMPETITORS]]"); + sectionToPlaceholder.put("VII\\.\\s*Каналы продвижения", "[[CHART_CHANNELS]]"); + sectionToPlaceholder.put("VIII\\.\\s*Воронка спроса", "[[CHART_FUNNEL]]"); + sectionToPlaceholder.put("VI\\.\\s*SWOT-анализ", "[[CHART_SWOT]]"); + + for (Map.Entry sectionEntry : sectionToPlaceholder.entrySet()) { + String sectionPattern = sectionEntry.getKey(); + String placeholder = sectionEntry.getValue(); + + // Если плейсхолдер уже был обработан, пропускаем + if (processedPlaceholders.contains(placeholder)) { + continue; + } + + ChartMapping mapping = placeholderMap.get(placeholder); + if (mapping == null) { + continue; + } + + Object chartData = chartsData.get(mapping.dataKey); + if (isEmptyChartData(chartData)) { + continue; + } + + // Ищем раздел в тексте + java.util.regex.Pattern pattern = java.util.regex.Pattern + .compile("(##+\\s*" + sectionPattern + "[^#]*)", java.util.regex.Pattern.CASE_INSENSITIVE); + java.util.regex.Matcher matcher = pattern.matcher(result); + + if (matcher.find()) { + try { + String jsonStr = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(chartData); + String jsonBlock = "\n\n```json:" + mapping.chartKey + "\n" + jsonStr + "\n```\n"; + // Вставляем после найденного раздела, перед следующим разделом или концом + // текста + int insertPos = matcher.end(); + // Ищем конец текущего раздела (начало следующего раздела или конец текста) + String remainingText = result.substring(insertPos); + java.util.regex.Pattern nextSectionPattern = java.util.regex.Pattern.compile("^##+\\s*[IVX]+\\."); + java.util.regex.Matcher nextSectionMatcher = nextSectionPattern.matcher(remainingText); + + if (nextSectionMatcher.find()) { + // Вставляем перед следующим разделом + int nextSectionPos = insertPos + nextSectionMatcher.start(); + result = result.substring(0, nextSectionPos) + jsonBlock + result.substring(nextSectionPos); + } else { + // Вставляем в конец текста + result = result + jsonBlock; + } + } catch (JsonProcessingException e) { + logger.warn("Failed to serialize chart data for fallback {}: {}", mapping.dataKey, e.getMessage()); + } + } + } + + return result; + } + public MarketingAnalysisResult getAnalysisResult(String analysisId) { Optional optAnalysis = repository.findById(analysisId); if (optAnalysis.isEmpty()) {