.
This commit is contained in:
+1
-1
@@ -25,7 +25,7 @@ export const API_CONFIG = {
|
|||||||
PARSERS_HEALTH: '/api/parser/health/parsers',
|
PARSERS_HEALTH: '/api/parser/health/parsers',
|
||||||
|
|
||||||
// ReportController - Генерация и управление отчётами
|
// ReportController - Генерация и управление отчётами
|
||||||
REPORT_GENERATE: '/api/parser/report/generate',
|
REPORT_GENERATE: '/api/parser/report',
|
||||||
REPORT_HISTORY: '/api/parser/report/history',
|
REPORT_HISTORY: '/api/parser/report/history',
|
||||||
REPORT_DOWNLOAD: '/api/parser/report/history',
|
REPORT_DOWNLOAD: '/api/parser/report/history',
|
||||||
|
|
||||||
|
|||||||
@@ -4,22 +4,21 @@ class ReportService {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.baseURL = API_CONFIG.BASE_URL;
|
this.baseURL = API_CONFIG.BASE_URL;
|
||||||
this.endpoints = {
|
this.endpoints = {
|
||||||
GENERATE: '/api/parser/report/generate',
|
GENERATE: API_CONFIG.ENDPOINTS.REPORT_GENERATE,
|
||||||
HISTORY: '/api/parser/report/history',
|
HISTORY: API_CONFIG.ENDPOINTS.REPORT_HISTORY,
|
||||||
DOWNLOAD: '/api/parser/report/history'
|
DOWNLOAD: API_CONFIG.ENDPOINTS.REPORT_DOWNLOAD
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Генерирует новый отчёт
|
* Генерирует новый отчёт
|
||||||
* @param {Object} reportData - Данные для генерации отчёта
|
* @param {Object} reportData - Данные для генерации отчёта
|
||||||
* @param {string} reportData.reportTitle - Заголовок отчёта
|
* @param {string} reportData.query - Тема для исследования
|
||||||
* @param {string} reportData.authorName - Имя автора
|
* @param {string} reportData.lang - Язык отчёта ("ru", "en")
|
||||||
* @param {string} reportData.companyName - Название компании
|
* @param {number} reportData.depth - Глубина исследования (1-5)
|
||||||
* @param {string} reportData.startDate - Дата начала периода (ISO 8601)
|
* @param {number} reportData.breadth - Широта исследования (2-10)
|
||||||
* @param {string} reportData.endDate - Дата окончания периода (ISO 8601)
|
* @param {string} reportData.report_type - Тип отчёта ("report", "answer")
|
||||||
* @param {string} reportData.format - Формат файла (PDF или DOCX)
|
* @returns {Promise<Blob>} - PDF-файл с отчётом
|
||||||
* @returns {Promise<Blob>} - Файл отчёта
|
|
||||||
*/
|
*/
|
||||||
async generateReport(reportData) {
|
async generateReport(reportData) {
|
||||||
try {
|
try {
|
||||||
@@ -37,7 +36,24 @@ class ReportService {
|
|||||||
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
|
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.blob();
|
const contentType = response.headers.get('Content-Type') || '';
|
||||||
|
|
||||||
|
// Если пришёл PDF/бинарный файл — вернуть как blob
|
||||||
|
if (contentType.includes('application/pdf') || contentType.includes('application/octet-stream')) {
|
||||||
|
const blob = await response.blob();
|
||||||
|
const filename = this.getFilenameFromResponse(response) || 'report.pdf';
|
||||||
|
return { type: 'blob', blob, filename };
|
||||||
|
}
|
||||||
|
|
||||||
|
// В противном случае — попробовать прочитать текст/JSON сообщение
|
||||||
|
const text = await response.text();
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
const message = json.message || text || 'Операция выполнена';
|
||||||
|
return { type: 'message', message };
|
||||||
|
} catch (_) {
|
||||||
|
return { type: 'message', message: text || 'Операция выполнена' };
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при генерации отчёта:', error);
|
console.error('Ошибка при генерации отчёта:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -79,6 +95,40 @@ class ReportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерирует и скачивает отчёт
|
||||||
|
* @param {Object} reportData - Данные для генерации отчёта
|
||||||
|
* @param {string} reportData.query - Тема для исследования
|
||||||
|
* @param {string} reportData.lang - Язык отчёта ("ru", "en")
|
||||||
|
* @param {number} reportData.depth - Глубина исследования (1-5)
|
||||||
|
* @param {number} reportData.breadth - Широта исследования (2-10)
|
||||||
|
* @param {string} reportData.report_type - Тип отчёта ("report", "answer")
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async generateAndDownloadReport(reportData) {
|
||||||
|
try {
|
||||||
|
const result = await this.generateReport(reportData);
|
||||||
|
|
||||||
|
if (result && result.type === 'blob') {
|
||||||
|
// Имя из ответа или сгенерированное
|
||||||
|
const filename = result.filename || `research_report_${new Date().toISOString().replace(/[:.]/g, '-')}.pdf`;
|
||||||
|
this.downloadFile(result.blob, filename);
|
||||||
|
return { downloaded: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если пришло сообщение — вернуть его вызывающей стороне
|
||||||
|
if (result && result.type === 'message') {
|
||||||
|
return { downloaded: false, message: result.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Неожиданный формат
|
||||||
|
return { downloaded: false, message: 'Неизвестный ответ сервера' };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при генерации и скачивании отчёта:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Скачивает отчёт по ID
|
* Скачивает отчёт по ID
|
||||||
* @param {string} reportId - ID отчёта
|
* @param {string} reportId - ID отчёта
|
||||||
@@ -169,13 +219,15 @@ class ReportService {
|
|||||||
if (error.message.includes('404')) {
|
if (error.message.includes('404')) {
|
||||||
return 'Отчёт не найден';
|
return 'Отчёт не найден';
|
||||||
} else if (error.message.includes('400')) {
|
} else if (error.message.includes('400')) {
|
||||||
return 'Неверные данные запроса';
|
return 'Некорректные параметры запроса';
|
||||||
} else if (error.message.includes('401')) {
|
} else if (error.message.includes('401')) {
|
||||||
return 'Необходима авторизация';
|
return 'Необходима авторизация';
|
||||||
} else if (error.message.includes('403')) {
|
} else if (error.message.includes('403')) {
|
||||||
return 'Доступ запрещён';
|
return 'Доступ запрещён';
|
||||||
} else if (error.message.includes('500')) {
|
} else if (error.message.includes('500')) {
|
||||||
return 'Внутренняя ошибка сервера';
|
return 'Внутренняя ошибка сервера';
|
||||||
|
} else if (error.message.includes('504')) {
|
||||||
|
return 'Таймаут при обращении к deep-research API. Попробуйте позже';
|
||||||
} else {
|
} else {
|
||||||
return 'Произошла ошибка: ' + error.message;
|
return 'Произошла ошибка: ' + error.message;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,62 +13,39 @@
|
|||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="reportTitle">Заголовок отчёта *</label>
|
<label for="query">Тема исследования *</label>
|
||||||
<InputText id="reportTitle" v-model="formData.reportTitle" :class="{ 'p-invalid': errors.reportTitle }" placeholder="Введите заголовок отчёта" size="large" />
|
<InputText id="query" v-model="formData.query" :class="{ 'p-invalid': errors.query }" placeholder="Введите тему для исследования" size="large" />
|
||||||
<small v-if="errors.reportTitle" class="p-error">{{ errors.reportTitle }}</small>
|
<small v-if="errors.query" class="p-error">{{ errors.query }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 md:col-6">
|
<div class="col-12 md:col-6">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="authorName">Автор *</label>
|
<label for="lang">Язык отчёта</label>
|
||||||
<InputText id="authorName" v-model="formData.authorName" :class="{ 'p-invalid': errors.authorName }" placeholder="Введите имя автора" />
|
<Dropdown id="lang" v-model="formData.lang" :options="langOptions" optionLabel="label" optionValue="value" placeholder="Выберите язык" />
|
||||||
<small v-if="errors.authorName" class="p-error">{{ errors.authorName }}</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 md:col-6">
|
<div class="col-12 md:col-6">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="companyName">Компания *</label>
|
<label for="report_type">Тип отчёта</label>
|
||||||
<InputText id="companyName" v-model="formData.companyName" :class="{ 'p-invalid': errors.companyName }" placeholder="Введите название компании" />
|
<Dropdown id="report_type" v-model="formData.report_type" :options="reportTypeOptions" optionLabel="label" optionValue="value" placeholder="Выберите тип" />
|
||||||
<small v-if="errors.companyName" class="p-error">{{ errors.companyName }}</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 md:col-6">
|
<div class="col-12 md:col-6">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="startDate">Дата начала *</label>
|
<label for="depth">Глубина исследования</label>
|
||||||
<Calendar id="startDate" v-model="formData.startDate" :class="{ 'p-invalid': errors.startDate }" dateFormat="dd.mm.yy" showTime hourFormat="24" placeholder="Выберите дату начала" :maxDate="formData.endDate" />
|
<Slider id="depth" v-model.number="formData.depth" :min="1" :max="5" :step="1" :disabled="false" />
|
||||||
<small v-if="errors.startDate" class="p-error">{{ errors.startDate }}</small>
|
<small class="text-600">Текущее значение: {{ formData.depth }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 md:col-6">
|
<div class="col-12 md:col-6">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="endDate">Дата окончания *</label>
|
<label for="breadth">Широта исследования</label>
|
||||||
<Calendar id="endDate" v-model="formData.endDate" :class="{ 'p-invalid': errors.endDate }" dateFormat="dd.mm.yy" showTime hourFormat="24" placeholder="Выберите дату окончания" :minDate="formData.startDate" />
|
<Slider id="breadth" v-model.number="formData.breadth" :min="2" :max="10" :step="1" :disabled="false" />
|
||||||
<small v-if="errors.endDate" class="p-error">{{ errors.endDate }}</small>
|
<small class="text-600">Текущее значение: {{ formData.breadth }}</small>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 md:col-6">
|
|
||||||
<div class="field">
|
|
||||||
<label for="format">Формат файла *</label>
|
|
||||||
<Dropdown id="format" v-model="formData.format" :options="formatOptions" optionLabel="label" optionValue="value" placeholder="Выберите формат" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 md:col-6">
|
|
||||||
<div class="field">
|
|
||||||
<label for="template">Шаблон отчёта</label>
|
|
||||||
<Dropdown id="template" v-model="formData.template" :options="templateOptions" optionLabel="label" optionValue="value" placeholder="Выберите шаблон" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="field">
|
|
||||||
<label for="description">Описание отчёта</label>
|
|
||||||
<Textarea id="description" v-model="formData.description" rows="3" placeholder="Введите краткое описание отчёта (необязательно)" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -89,29 +66,43 @@
|
|||||||
<div class="flex flex-column gap-3">
|
<div class="flex flex-column gap-3">
|
||||||
<div class="flex align-items-center gap-2">
|
<div class="flex align-items-center gap-2">
|
||||||
<i class="pi pi-info-circle text-blue-500"></i>
|
<i class="pi pi-info-circle text-blue-500"></i>
|
||||||
<span class="text-sm">Отчёт будет создан на основе данных за выбранный период</span>
|
<span class="text-sm">Отчёт будет создан с помощью AI-исследования</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex align-items-center gap-2">
|
<div class="flex align-items-center gap-2">
|
||||||
<i class="pi pi-clock text-orange-500"></i>
|
<i class="pi pi-clock text-orange-500"></i>
|
||||||
<span class="text-sm">Время генерации: 1-3 минуты</span>
|
<span class="text-sm">Время генерации: до 5 минут</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex align-items-center gap-2">
|
<div class="flex align-items-center gap-2">
|
||||||
<i class="pi pi-download text-green-500"></i>
|
<i class="pi pi-download text-green-500"></i>
|
||||||
<span class="text-sm">Файл будет автоматически скачан</span>
|
<span class="text-sm">PDF-файл будет автоматически скачан</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<h6>Поддерживаемые форматы</h6>
|
<h6>Параметры исследования</h6>
|
||||||
|
<div class="flex flex-column gap-2">
|
||||||
|
<div class="flex align-items-center gap-2">
|
||||||
|
<i class="pi pi-search text-purple-500"></i>
|
||||||
|
<span class="text-sm">Глубина: детальность анализа</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex align-items-center gap-2">
|
||||||
|
<i class="pi pi-globe text-cyan-500"></i>
|
||||||
|
<span class="text-sm">Широта: количество источников</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<h6>Типы отчётов</h6>
|
||||||
<div class="flex flex-column gap-2">
|
<div class="flex flex-column gap-2">
|
||||||
<div class="flex align-items-center gap-2">
|
<div class="flex align-items-center gap-2">
|
||||||
<i class="pi pi-file-pdf text-red-500"></i>
|
<i class="pi pi-file-pdf text-red-500"></i>
|
||||||
<span class="text-sm">PDF - для печати и просмотра</span>
|
<span class="text-sm">Отчёт - полное исследование</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex align-items-center gap-2">
|
<div class="flex align-items-center gap-2">
|
||||||
<i class="pi pi-file-word text-blue-500"></i>
|
<i class="pi pi-question-circle text-blue-500"></i>
|
||||||
<span class="text-sm">DOCX - для редактирования</span>
|
<span class="text-sm">Ответ - краткий ответ на вопрос</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,35 +124,27 @@ const loading = ref(false);
|
|||||||
|
|
||||||
// Данные формы
|
// Данные формы
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
reportTitle: '',
|
query: '',
|
||||||
authorName: '',
|
lang: 'ru',
|
||||||
companyName: '',
|
depth: 1,
|
||||||
startDate: null,
|
breadth: 2,
|
||||||
endDate: null,
|
report_type: 'report'
|
||||||
format: 'PDF',
|
|
||||||
template: 'standard',
|
|
||||||
description: ''
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ошибки валидации
|
// Ошибки валидации
|
||||||
const errors = reactive({
|
const errors = reactive({
|
||||||
reportTitle: null,
|
query: null
|
||||||
authorName: null,
|
|
||||||
companyName: null,
|
|
||||||
startDate: null,
|
|
||||||
endDate: null
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Опции
|
// Опции
|
||||||
const formatOptions = [
|
const langOptions = [
|
||||||
{ label: 'PDF', value: 'PDF' },
|
{ label: 'Русский', value: 'ru' },
|
||||||
{ label: 'DOCX', value: 'DOCX' }
|
{ label: 'English', value: 'en' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const templateOptions = [
|
const reportTypeOptions = [
|
||||||
{ label: 'Стандартный', value: 'standard' },
|
{ label: 'Отчёт', value: 'report' },
|
||||||
{ label: 'Краткий', value: 'brief' },
|
{ label: 'Ответ', value: 'answer' }
|
||||||
{ label: 'Подробный', value: 'detailed' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Методы
|
// Методы
|
||||||
@@ -171,33 +154,8 @@ const validateForm = () => {
|
|||||||
|
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
|
|
||||||
if (!formData.reportTitle.trim()) {
|
if (!formData.query.trim()) {
|
||||||
errors.reportTitle = 'Заголовок отчёта обязателен';
|
errors.query = 'Тема исследования обязательна';
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.authorName.trim()) {
|
|
||||||
errors.authorName = 'Имя автора обязательно';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.companyName.trim()) {
|
|
||||||
errors.companyName = 'Название компании обязательно';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.startDate) {
|
|
||||||
errors.startDate = 'Дата начала обязательна';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.endDate) {
|
|
||||||
errors.endDate = 'Дата окончания обязательна';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formData.startDate && formData.endDate && formData.startDate >= formData.endDate) {
|
|
||||||
errors.endDate = 'Дата окончания должна быть позже даты начала';
|
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,34 +178,25 @@ const handleGenerate = async () => {
|
|||||||
try {
|
try {
|
||||||
// Подготовка данных для API
|
// Подготовка данных для API
|
||||||
const reportData = {
|
const reportData = {
|
||||||
reportTitle: formData.reportTitle.trim(),
|
query: formData.query.trim(),
|
||||||
authorName: formData.authorName.trim(),
|
lang: formData.lang,
|
||||||
companyName: formData.companyName.trim(),
|
depth: formData.depth,
|
||||||
startDate: formData.startDate.toISOString(),
|
breadth: formData.breadth,
|
||||||
endDate: formData.endDate.toISOString(),
|
report_type: formData.report_type
|
||||||
format: formData.format
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const blob = await ReportService.generateReport(reportData);
|
// Генерируем и скачиваем отчёт
|
||||||
|
await ReportService.generateAndDownloadReport(reportData);
|
||||||
// Создание имени файла
|
|
||||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
|
||||||
const filename = `report_${timestamp}.${formData.format.toLowerCase()}`;
|
|
||||||
|
|
||||||
// Скачивание файла
|
|
||||||
ReportService.downloadFile(blob, filename);
|
|
||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
summary: 'Успешно',
|
summary: 'Отчёт сгенерирован',
|
||||||
detail: 'Отчёт сгенерирован и скачан',
|
detail: 'PDF-файл с отчётом скачан',
|
||||||
life: 3000
|
life: 5000
|
||||||
});
|
});
|
||||||
|
|
||||||
// Переход к истории отчётов
|
// Очищаем форму после успешной генерации
|
||||||
setTimeout(() => {
|
resetForm();
|
||||||
router.push('/reports/history');
|
|
||||||
}, 2000);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка генерации отчёта:', error);
|
console.error('Ошибка генерации отчёта:', error);
|
||||||
toast.add({
|
toast.add({
|
||||||
@@ -263,14 +212,11 @@ const handleGenerate = async () => {
|
|||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
Object.assign(formData, {
|
Object.assign(formData, {
|
||||||
reportTitle: '',
|
query: '',
|
||||||
authorName: '',
|
lang: 'ru',
|
||||||
companyName: '',
|
depth: 3,
|
||||||
startDate: null,
|
breadth: 5,
|
||||||
endDate: null,
|
report_type: 'report'
|
||||||
format: 'PDF',
|
|
||||||
template: 'standard',
|
|
||||||
description: ''
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(errors).forEach((key) => (errors[key] = null));
|
Object.keys(errors).forEach((key) => (errors[key] = null));
|
||||||
@@ -278,12 +224,7 @@ const resetForm = () => {
|
|||||||
|
|
||||||
// Инициализация
|
// Инициализация
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Установка дат по умолчанию (последние 7 дней)
|
// Форма уже инициализирована значениями по умолчанию
|
||||||
const today = new Date();
|
|
||||||
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
formData.endDate = today;
|
|
||||||
formData.startDate = weekAgo;
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5>Генерация отчётов</h5>
|
<h5>Генерация отчётов</h5>
|
||||||
<p>Создайте новый отчёт на основе собранных данных за выбранный период.</p>
|
<p>Создайте новый отчёт с помощью AI-исследования по выбранной теме.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -13,38 +13,29 @@
|
|||||||
<h5>Создать новый отчёт</h5>
|
<h5>Создать новый отчёт</h5>
|
||||||
<form @submit.prevent="handleGenerate" class="p-fluid">
|
<form @submit.prevent="handleGenerate" class="p-fluid">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="reportTitle">Заголовок отчёта *</label>
|
<label for="query">Тема исследования *</label>
|
||||||
<InputText id="reportTitle" v-model="formData.reportTitle" :class="{ 'p-invalid': errors.reportTitle }" placeholder="Введите заголовок отчёта" />
|
<InputText id="query" v-model="formData.query" :class="{ 'p-invalid': errors.query }" placeholder="Введите тему для исследования" />
|
||||||
<small v-if="errors.reportTitle" class="p-error">{{ errors.reportTitle }}</small>
|
<small v-if="errors.query" class="p-error">{{ errors.query }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="authorName">Автор *</label>
|
<label for="lang">Язык отчёта</label>
|
||||||
<InputText id="authorName" v-model="formData.authorName" :class="{ 'p-invalid': errors.authorName }" placeholder="Введите имя автора" />
|
<Dropdown id="lang" v-model="formData.lang" :options="langOptions" optionLabel="label" optionValue="value" placeholder="Выберите язык" />
|
||||||
<small v-if="errors.authorName" class="p-error">{{ errors.authorName }}</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="companyName">Компания *</label>
|
<label for="report_type">Тип отчёта</label>
|
||||||
<InputText id="companyName" v-model="formData.companyName" :class="{ 'p-invalid': errors.companyName }" placeholder="Введите название компании" />
|
<Dropdown id="report_type" v-model="formData.report_type" :options="reportTypeOptions" optionLabel="label" optionValue="value" placeholder="Выберите тип" />
|
||||||
<small v-if="errors.companyName" class="p-error">{{ errors.companyName }}</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="startDate">Дата начала *</label>
|
<label for="depth">Глубина исследования: {{ formData.depth }}</label>
|
||||||
<Calendar id="startDate" v-model="formData.startDate" :class="{ 'p-invalid': errors.startDate }" dateFormat="dd.mm.yy" showTime hourFormat="24" placeholder="Выберите дату начала" />
|
<Slider id="depth" v-model.number="formData.depth" :min="1" :max="5" :step="1" :disabled="false" />
|
||||||
<small v-if="errors.startDate" class="p-error">{{ errors.startDate }}</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="endDate">Дата окончания *</label>
|
<label for="breadth">Широта исследования: {{ formData.breadth }}</label>
|
||||||
<Calendar id="endDate" v-model="formData.endDate" :class="{ 'p-invalid': errors.endDate }" dateFormat="dd.mm.yy" showTime hourFormat="24" placeholder="Выберите дату окончания" />
|
<Slider id="breadth" v-model.number="formData.breadth" :min="2" :max="10" :step="1" :disabled="false" />
|
||||||
<small v-if="errors.endDate" class="p-error">{{ errors.endDate }}</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label for="format">Формат файла *</label>
|
|
||||||
<Dropdown id="format" v-model="formData.format" :options="formatOptions" optionLabel="label" optionValue="value" placeholder="Выберите формат" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -159,27 +150,27 @@ const downloadingReports = ref([]);
|
|||||||
|
|
||||||
// Данные формы
|
// Данные формы
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
reportTitle: '',
|
query: '',
|
||||||
authorName: '',
|
lang: 'ru',
|
||||||
companyName: '',
|
depth: 1,
|
||||||
startDate: null,
|
breadth: 2,
|
||||||
endDate: null,
|
report_type: 'report'
|
||||||
format: 'PDF'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ошибки валидации
|
// Ошибки валидации
|
||||||
const errors = reactive({
|
const errors = reactive({
|
||||||
reportTitle: null,
|
query: null
|
||||||
authorName: null,
|
|
||||||
companyName: null,
|
|
||||||
startDate: null,
|
|
||||||
endDate: null
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Опции форматов
|
// Опции
|
||||||
const formatOptions = [
|
const langOptions = [
|
||||||
{ label: 'PDF', value: 'PDF' },
|
{ label: 'Русский', value: 'ru' },
|
||||||
{ label: 'DOCX', value: 'DOCX' }
|
{ label: 'English', value: 'en' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const reportTypeOptions = [
|
||||||
|
{ label: 'Отчёт', value: 'report' },
|
||||||
|
{ label: 'Ответ', value: 'answer' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Вычисляемые свойства
|
// Вычисляемые свойства
|
||||||
@@ -202,33 +193,8 @@ const validateForm = () => {
|
|||||||
|
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
|
|
||||||
if (!formData.reportTitle.trim()) {
|
if (!formData.query.trim()) {
|
||||||
errors.reportTitle = 'Заголовок отчёта обязателен';
|
errors.query = 'Тема исследования обязательна';
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.authorName.trim()) {
|
|
||||||
errors.authorName = 'Имя автора обязательно';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.companyName.trim()) {
|
|
||||||
errors.companyName = 'Название компании обязательно';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.startDate) {
|
|
||||||
errors.startDate = 'Дата начала обязательна';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.endDate) {
|
|
||||||
errors.endDate = 'Дата окончания обязательна';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formData.startDate && formData.endDate && formData.startDate >= formData.endDate) {
|
|
||||||
errors.endDate = 'Дата окончания должна быть позже даты начала';
|
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,32 +217,34 @@ const handleGenerate = async () => {
|
|||||||
try {
|
try {
|
||||||
// Подготовка данных для API
|
// Подготовка данных для API
|
||||||
const reportData = {
|
const reportData = {
|
||||||
reportTitle: formData.reportTitle.trim(),
|
query: formData.query.trim(),
|
||||||
authorName: formData.authorName.trim(),
|
lang: formData.lang,
|
||||||
companyName: formData.companyName.trim(),
|
depth: formData.depth,
|
||||||
startDate: formData.startDate.toISOString(),
|
breadth: formData.breadth,
|
||||||
endDate: formData.endDate.toISOString(),
|
report_type: formData.report_type
|
||||||
format: formData.format
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const blob = await ReportService.generateReport(reportData);
|
// Генерируем и скачиваем отчёт (или показываем сообщение)
|
||||||
|
const result = await ReportService.generateAndDownloadReport(reportData);
|
||||||
|
|
||||||
// Создание имени файла
|
if (result && result.downloaded) {
|
||||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
toast.add({
|
||||||
const filename = `report_${timestamp}.${formData.format.toLowerCase()}`;
|
severity: 'success',
|
||||||
|
summary: 'Отчёт сгенерирован',
|
||||||
|
detail: 'PDF-файл с отчётом скачан',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.add({
|
||||||
|
severity: 'info',
|
||||||
|
summary: 'Информация',
|
||||||
|
detail: result && result.message ? result.message : 'Запрос выполнен',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Скачивание файла
|
// Очищаем форму после успешной генерации
|
||||||
ReportService.downloadFile(blob, filename);
|
resetForm();
|
||||||
|
|
||||||
toast.add({
|
|
||||||
severity: 'success',
|
|
||||||
summary: 'Успешно',
|
|
||||||
detail: 'Отчёт сгенерирован и скачан',
|
|
||||||
life: 3000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Перезагрузка списка отчётов
|
|
||||||
await loadReports();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка генерации отчёта:', error);
|
console.error('Ошибка генерации отчёта:', error);
|
||||||
toast.add({
|
toast.add({
|
||||||
@@ -358,6 +326,18 @@ const formatFileSize = (bytes) => {
|
|||||||
return ReportService.formatFileSize(bytes);
|
return ReportService.formatFileSize(bytes);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
Object.assign(formData, {
|
||||||
|
query: '',
|
||||||
|
lang: 'ru',
|
||||||
|
depth: 3,
|
||||||
|
breadth: 5,
|
||||||
|
report_type: 'report'
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(errors).forEach((key) => (errors[key] = null));
|
||||||
|
};
|
||||||
|
|
||||||
// Инициализация
|
// Инициализация
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadReports();
|
loadReports();
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
# Руководство по API генерации исследовательских отчётов
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
Новый эндпоинт `/api/parser/report` интегрирован с сервисом `deep-research` для генерации PDF-отчётов на основе пользовательских запросов.
|
||||||
|
|
||||||
|
## Эндпоинт
|
||||||
|
|
||||||
|
**POST** `/api/parser/report`
|
||||||
|
|
||||||
|
### Параметры запроса
|
||||||
|
|
||||||
|
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|
||||||
|
| ------------- | ------- | ------------ | ------------ | ------------------------------- |
|
||||||
|
| `query` | string | ✅ | - | Тема для исследования |
|
||||||
|
| `lang` | string | ❌ | "ru" | Язык отчёта ("ru", "en") |
|
||||||
|
| `depth` | integer | ❌ | 3 | Глубина исследования (1-5) |
|
||||||
|
| `breadth` | integer | ❌ | 5 | Широта исследования (2-10) |
|
||||||
|
| `report_type` | string | ❌ | "report" | Тип отчёта ("report", "answer") |
|
||||||
|
|
||||||
|
### Пример запроса
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": "Искусственный интеллект в здравоохранении",
|
||||||
|
"lang": "ru",
|
||||||
|
"depth": 3,
|
||||||
|
"breadth": 5,
|
||||||
|
"report_type": "report"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ответы
|
||||||
|
|
||||||
|
#### Успешный ответ (200 OK)
|
||||||
|
|
||||||
|
- **Content-Type**: `application/pdf`
|
||||||
|
- **Content-Disposition**: `attachment; filename="research_report_[timestamp].pdf"`
|
||||||
|
- **Тело**: PDF-файл с отчётом
|
||||||
|
|
||||||
|
#### Ошибки
|
||||||
|
|
||||||
|
| Код | Описание |
|
||||||
|
| --- | ----------------------------------------- |
|
||||||
|
| 400 | Некорректные параметры запроса |
|
||||||
|
| 500 | Внутренняя ошибка сервера |
|
||||||
|
| 504 | Таймаут при обращении к deep-research API |
|
||||||
|
|
||||||
|
## Структура PDF-отчёта
|
||||||
|
|
||||||
|
Сгенерированный PDF содержит:
|
||||||
|
|
||||||
|
1. **Титульная страница**
|
||||||
|
|
||||||
|
- Название исследования (из поля `query`)
|
||||||
|
- Дата создания
|
||||||
|
- Подзаголовок "Исследовательский отчёт"
|
||||||
|
|
||||||
|
2. **Содержание**
|
||||||
|
|
||||||
|
- Автоматически сгенерированное оглавление
|
||||||
|
|
||||||
|
3. **Основная часть**
|
||||||
|
|
||||||
|
- Введение
|
||||||
|
- Основной текст отчёта от deep-research API
|
||||||
|
- Заключение
|
||||||
|
|
||||||
|
4. **Источники**
|
||||||
|
- Список URL-адресов из поля `visitedUrls`
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
|
||||||
|
Настройки в `application.properties`:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# Deep Research API Configuration
|
||||||
|
deep-research.api.url=http://185.35.223.45:3051
|
||||||
|
deep-research.api.timeout=300000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Примеры использования
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://api.konturai/api/parser/report" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"query": "Блокчейн технологии в финансах",
|
||||||
|
"lang": "ru",
|
||||||
|
"depth": 4,
|
||||||
|
"breadth": 6
|
||||||
|
}' \
|
||||||
|
--output "blockchain_report.pdf"
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript (fetch)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch('/api/parser/report', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: 'Квантовые вычисления',
|
||||||
|
lang: 'ru',
|
||||||
|
depth: 3,
|
||||||
|
breadth: 5,
|
||||||
|
report_type: 'report'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'quantum_computing_report.pdf';
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python (requests)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
'https://api.konturai/api/parser/report',
|
||||||
|
json={
|
||||||
|
'query': 'Машинное обучение в медицине',
|
||||||
|
'lang': 'ru',
|
||||||
|
'depth': 3,
|
||||||
|
'breadth': 5,
|
||||||
|
'report_type': 'report'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
with open('ml_medicine_report.pdf', 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
print("Отчёт сохранён как ml_medicine_report.pdf")
|
||||||
|
else:
|
||||||
|
print(f"Ошибка: {response.status_code}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
Для тестирования API используйте скрипт `test_research_api.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test_research_api.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Логирование
|
||||||
|
|
||||||
|
Все операции логируются. Для отладки проверьте логи приложения:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tail -f logs/application.log | grep "DeepResearchService\|ResearchPdfService"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ограничения
|
||||||
|
|
||||||
|
- Максимальное время ожидания: 5 минут (300 секунд)
|
||||||
|
- Размер генерируемого PDF ограничен только ресурсами сервера
|
||||||
|
- Deep-research API должен быть доступен по указанному URL
|
||||||
|
|
||||||
|
## Устранение неполадок
|
||||||
|
|
||||||
|
### Ошибка 504 (Gateway Timeout)
|
||||||
|
|
||||||
|
- Проверьте доступность deep-research API
|
||||||
|
- Увеличьте timeout в конфигурации
|
||||||
|
- Проверьте сетевые настройки
|
||||||
|
|
||||||
|
### Ошибка 500 (Internal Server Error)
|
||||||
|
|
||||||
|
- Проверьте логи приложения
|
||||||
|
- Убедитесь, что все зависимости установлены
|
||||||
|
- Проверьте конфигурацию MinIO для сохранения файлов
|
||||||
|
|
||||||
|
### Пустой PDF
|
||||||
|
|
||||||
|
- Проверьте, что deep-research API возвращает корректные данные
|
||||||
|
- Убедитесь, что поле `report` или `answer` в ответе не пустое
|
||||||
Reference in New Issue
Block a user