From e908f7840a1c7c33b5b3d66d2cef239c7c9f6a59 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 5 Oct 2025 16:18:33 +0500 Subject: [PATCH] . --- .../parser/dto/DeepResearchResponse.java | 45 +++-- .../parser/service/ResearchPdfService.java | 168 ++++++++++-------- 2 files changed, 128 insertions(+), 85 deletions(-) diff --git a/src/main/java/kz/konturai/parser/dto/DeepResearchResponse.java b/src/main/java/kz/konturai/parser/dto/DeepResearchResponse.java index b871986..fb803a0 100644 --- a/src/main/java/kz/konturai/parser/dto/DeepResearchResponse.java +++ b/src/main/java/kz/konturai/parser/dto/DeepResearchResponse.java @@ -64,28 +64,47 @@ public class DeepResearchResponse { /** * Получить основной текст отчёта (report или answer + learnings) */ + // public String getMainContent() { + // StringBuilder contentBuilder = new StringBuilder(); + + // // Сначала добавляем answer, если есть + // if (answer != null && !answer.trim().isEmpty()) { + // contentBuilder.append(answer).append("\n\n"); + // } + + // // Затем добавляем learnings, если они есть + // if (learnings != null && !learnings.isEmpty()) { + // contentBuilder.append("### Подробные выводы:\n"); + // for (String learning : learnings) { + // contentBuilder.append("- ").append(learning).append("\n"); + // } + // contentBuilder.append("\n"); + // } + + // // Также можно добавить report, если вдруг deep-research начнет его + // возвращать + // // if (report != null && !report.trim().isEmpty()) { + // // contentBuilder.append(report).append("\n\n"); + // // } + + // return contentBuilder.toString().trim(); + // } + public String getMainContent() { StringBuilder contentBuilder = new StringBuilder(); - // Сначала добавляем answer, если есть + // Используем краткий 'answer' как вступительное предложение. if (answer != null && !answer.trim().isEmpty()) { - contentBuilder.append(answer).append("\n\n"); + contentBuilder.append(answer).append(" "); // Добавляем пробел для соединения } - // Затем добавляем learnings, если они есть + // Объединяем все learnings в одну строку, разделяя их пробелами. if (learnings != null && !learnings.isEmpty()) { - contentBuilder.append("### Подробные выводы:\n"); - for (String learning : learnings) { - contentBuilder.append("- ").append(learning).append("\n"); - } - contentBuilder.append("\n"); + String singleArticleText = String.join(" ", learnings); + contentBuilder.append(singleArticleText); } - // Также можно добавить report, если вдруг deep-research начнет его возвращать - // if (report != null && !report.trim().isEmpty()) { - // contentBuilder.append(report).append("\n\n"); - // } - return contentBuilder.toString().trim(); } + } \ No newline at end of file diff --git a/src/main/java/kz/konturai/parser/service/ResearchPdfService.java b/src/main/java/kz/konturai/parser/service/ResearchPdfService.java index 27adb0b..cfc77db 100644 --- a/src/main/java/kz/konturai/parser/service/ResearchPdfService.java +++ b/src/main/java/kz/konturai/parser/service/ResearchPdfService.java @@ -2,6 +2,7 @@ package kz.konturai.parser.service; import com.lowagie.text.*; import com.lowagie.text.pdf.BaseFont; +import com.lowagie.text.pdf.ColumnText; import com.lowagie.text.pdf.PdfWriter; import kz.konturai.parser.dto.DeepResearchResponse; import org.springframework.stereotype.Service; @@ -11,6 +12,12 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; +import com.lowagie.text.pdf.PdfPageEventHelper; + +import java.awt.Color; // УБЕДИТЕСЬ, ЧТО ИМПОРТИРОВАН java.awt.Color + +import java.io.InputStream; + @Service public class ResearchPdfService { @@ -22,41 +29,28 @@ public class ResearchPdfService { private final Font normalFont; private final Font smallFont; private final Font linkFont; + private final Font footerFont; - /** - * Конструктор для инициализации шрифтов. - * Убедитесь, что файл шрифта DejaVuSans.ttf находится в /resources/fonts/ - */ public ResearchPdfService() { BaseFont resolvedBaseFont; try { - // Пытаемся загрузить шрифт из classpath (работает внутри fat JAR) - java.io.InputStream is = Thread.currentThread().getContextClassLoader() - .getResourceAsStream("fonts/DejaVuSans.ttf"); - if (is != null) { - try (is) { - byte[] fontBytes = is.readAllBytes(); - resolvedBaseFont = BaseFont.createFont( - "DejaVuSans.ttf", - BaseFont.IDENTITY_H, - BaseFont.EMBEDDED, - false, - fontBytes, - null); - } - } else { - // Фолбэк: пробуем загрузить по файловому пути (на случай, если ресурс не - // упакован) - resolvedBaseFont = BaseFont.createFont("fonts/DejaVuSans.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED); + InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("fonts/DejaVuSans.ttf"); + if (is == null) { + throw new RuntimeException("Font file fonts/DejaVuSans.ttf not found in classpath."); + } + try (is) { + byte[] fontBytes = is.readAllBytes(); + resolvedBaseFont = BaseFont.createFont("DejaVuSans.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED, false, + fontBytes, null); } } catch (Exception e) { - // Фолбэк на стандартный шрифт, чтобы не падал бином (кириллица может - // отображаться некорректно) + // Фолбэк на стандартный шрифт, если DejaVuSans не загрузился try { resolvedBaseFont = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.WINANSI, BaseFont.NOT_EMBEDDED); - System.err.println("ResearchPdfService: DejaVuSans.ttf not found. Falling back to Helvetica."); + System.err.println( + "ResearchPdfService: DejaVuSans.ttf not found. Falling back to Helvetica. Cyrillic may not display correctly."); } catch (Exception inner) { - throw new RuntimeException("Failed to load font", e); + throw new RuntimeException("Failed to load any font", inner); } } @@ -66,43 +60,58 @@ public class ResearchPdfService { this.subheadingFont = new Font(baseFont, 12, Font.BOLD); this.normalFont = new Font(baseFont, 10, Font.NORMAL); this.smallFont = new Font(baseFont, 8, Font.ITALIC); - this.linkFont = new Font(baseFont, 8, Font.UNDERLINE); + this.footerFont = new Font(baseFont, 8, Font.NORMAL, Color.GRAY); + // ИСПРАВЛЕНО: Возвращаем синий цвет ссылкам + this.linkFont = new Font(baseFont, 8, Font.UNDERLINE, Color.BLUE); + } + + // УЛУЧШЕНО: Внутренний класс для добавления нумерации страниц + private static class PageFooter extends PdfPageEventHelper { + private final Font font; + + public PageFooter(Font font) { + this.font = font; + } + + @Override + public void onEndPage(PdfWriter writer, Document document) { + if (document.getPageNumber() > 1) { // Не добавляем номер на титульную страницу + ColumnText.showTextAligned(writer.getDirectContent(), + Element.ALIGN_CENTER, + new Phrase(String.format("Страница %d", writer.getPageNumber()), font), + (document.right() - document.left()) / 2 + document.leftMargin(), + document.bottom() - 10, 0); + } + } } - /** - * Генерирует PDF отчёт на основе ответа от deep-research API - */ public byte[] generatePdfReport(String query, DeepResearchResponse researchResponse) { try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - Document document = new Document(PageSize.A4); - PdfWriter.getInstance(document, outputStream); + Document document = new Document(PageSize.A4, 36, 36, 36, 54); // Добавлены отступы + PdfWriter writer = PdfWriter.getInstance(document, outputStream); + + // УЛУЧШЕНО: Устанавливаем обработчик событий для нумерации страниц + writer.setPageEvent(new PageFooter(footerFont)); - // --- Добавляем метаданные в документ --- document.addTitle(query); document.addAuthor("AI Research Agent"); document.addSubject("Research Report"); document.open(); - // 1. Титульная страница addTitlePage(document, query); - - // 2. Основной отчёт с динамическим содержанием addMainReport(document, researchResponse); - - // 3. Список источников addSources(document, researchResponse.getVisitedUrls()); document.close(); return outputStream.toByteArray(); } catch (Exception e) { - // Логирование ошибки было бы здесь полезно throw new RuntimeException("Failed to generate PDF report", e); } } private void addTitlePage(Document document, String query) throws DocumentException { - document.newPage(); // Начинаем с чистого листа + // Этот метод был в порядке, оставляем без изменений Paragraph title = new Paragraph(query, titleFont); title.setAlignment(Element.ALIGN_CENTER); title.setSpacingBefore(50); @@ -121,63 +130,78 @@ public class ResearchPdfService { document.add(date); } + // ИСПРАВЛЕНО: Корректное регулярное выражение для очистки + private String cleanContent(String rawContent) { + if (rawContent == null) { + return ""; + } + // Удаляем теги типа и другие похожие артефакты + return rawContent.replaceAll("\\", "").trim(); + } + private void addMainReport(Document document, DeepResearchResponse researchResponse) throws DocumentException { - String content = researchResponse.getMainContent(); - if (content == null || content.trim().isEmpty()) { - content = "## Отчёт не был сгенерирован.\n\nДанные для анализа отсутствуют."; + String cleanedContent = cleanContent(researchResponse.getMainContent()); + if (cleanedContent.isEmpty()) { + cleanedContent = "## Отчёт не был сгенерирован.\n\nДанные для анализа отсутствуют."; } - document.newPage(); // Новая страница для основного контента + document.newPage(); - // --- Улучшенный парсер контента с поддержкой Markdown --- - String[] lines = content.split("\n"); - Chapter chapter = null; // Используем главы и секции для автоматического содержания + String[] lines = cleanedContent.split("\n"); + Chapter chapter = new Chapter(new Paragraph("Основной отчёт", headingFont), 1); + chapter.setNumberDepth(0); // Скрываем номер главы "1." + + // ИСПРАВЛЕНО: Логика обработки списков + com.lowagie.text.List currentList = null; for (String line : lines) { if (line.trim().isEmpty()) continue; - if (line.startsWith("# ")) { // Главный заголовок (H1) -> Глава - if (chapter != null) - document.add(chapter); - chapter = new Chapter(new Paragraph(line.substring(2), headingFont), 1); - } else if (line.startsWith("## ")) { // Подзаголовок (H2) -> Секция - if (chapter == null) - chapter = new Chapter(new Paragraph("Основная часть", headingFont), 1); - chapter.addSection(new Paragraph(line.substring(3), subheadingFont)); - } else if (line.startsWith("- ") || line.startsWith("* ")) { // Элемент списка - com.lowagie.text.List list = new com.lowagie.text.List(com.lowagie.text.List.UNORDERED); - list.setListSymbol(new Chunk("- ", normalFont)); - list.add(new ListItem(line.substring(2), normalFont)); - if (chapter != null) - chapter.add(list); - else - document.add(list); - } else { // Обычный параграф + // Если текущая строка не является элементом списка, а мы были в списке, + // то завершаем старый список + if (!(line.startsWith("- ") || line.startsWith("* ")) && currentList != null) { + chapter.add(currentList); + currentList = null; + } + + if (line.startsWith("# ")) { + chapter.add(new Paragraph(line.substring(2), headingFont)); + } else if (line.startsWith("## ")) { + chapter.add(new Paragraph(line.substring(3), subheadingFont)); + } else if (line.startsWith("- ") || line.startsWith("* ")) { + // Если мы еще не в списке, создаем новый + if (currentList == null) { + currentList = new com.lowagie.text.List(com.lowagie.text.List.UNORDERED); + currentList.setListSymbol(new Chunk("- ", normalFont)); + } + currentList.add(new ListItem(line.substring(2), normalFont)); + } else { Paragraph para = new Paragraph(line, normalFont); para.setSpacingAfter(10); - if (chapter != null) - chapter.add(para); - else - document.add(para); + chapter.add(para); } } - if (chapter != null) { - document.add(chapter); + // Добавляем последний список, если он остался + if (currentList != null) { + chapter.add(currentList); } + + document.add(chapter); } private void addSources(Document document, List visitedUrls) throws DocumentException { document.newPage(); - Chapter sourcesChapter = new Chapter(new Paragraph("Источники", headingFont), 2); + Paragraph sourcesTitle = new Paragraph("Источники", headingFont); + Chapter sourcesChapter = new Chapter(sourcesTitle, 2); + sourcesChapter.setNumberDepth(0); // Скрываем номер if (visitedUrls == null || visitedUrls.isEmpty()) { sourcesChapter.add(new Paragraph("Источники не были предоставлены.", normalFont)); } else { com.lowagie.text.List list = new com.lowagie.text.List(com.lowagie.text.List.ORDERED); for (String url : visitedUrls) { - // Создаем кликабельные ссылки Anchor anchor = new Anchor(url, linkFont); anchor.setReference(url); list.add(new ListItem(anchor));