This commit is contained in:
root
2025-10-05 16:18:33 +05:00
parent 31f8c8212f
commit e908f7840a
2 changed files with 128 additions and 85 deletions
@@ -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();
}
}
@@ -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<String> 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));