.
This commit is contained in:
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user