Add final report download button (#329)
* fix(app): 改进应用健康检查机制并更新默认配置 添加专用的健康检查路径和代理配置,重构健康检查URL构建逻辑 增加健康检查失败时的日志记录 延长应用启动等待时间至90秒 * style(templates): 统一CSS选择器缩进格式并修复空格问题 * feat(报告下载): 实现报告文件下载功能并增强任务状态管理 - 在ReportAgent中修改generate_report返回包含文件路径的字典 - 在ReportTask中添加文件路径相关字段 - 新增/download接口用于下载报告文件 - 在前端添加下载按钮及相关控制逻辑 - 完善任务状态显示,增加文件路径信息 * feat(report): 添加报告下载功能并优化状态管理 - 在ReportAgent中返回报告文件保存路径信息 - 新增Flask接口/download/<task_id>用于下载报告文件 - 在前端添加下载按钮及相关控制逻辑 - 修复报告生成状态重置问题 - 优化健康检查URL构建和代理设置 - 统一CSS样式中的空格和缩进 --------- Co-authored-by: HKLHaoBin <we3q@qq.com> Co-authored-by: Zhang Yuxiang <51037789+NTFago@users.noreply.github.com>
This commit is contained in:
+229
-47
@@ -699,6 +699,13 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@@ -801,51 +808,51 @@
|
||||
}
|
||||
|
||||
/* 不同Engine的颜色区分 */
|
||||
.forum-message.agent:has(.forum-message-header:contains("Query Engine")) {
|
||||
background-color: #eaf1f8;
|
||||
border-color: #608ab1;
|
||||
}
|
||||
.forum-message.agent:has(.forum-message-header:contains("Query Engine")) {
|
||||
background-color: #eaf1f8;
|
||||
border-color: #608ab1;
|
||||
}
|
||||
|
||||
.forum-message.agent:has(.forum-message-header:contains("QUERY Engine")) {
|
||||
background-color: #eaf1f8;
|
||||
border-color: #608ab1;
|
||||
}
|
||||
background-color: #eaf1f8;
|
||||
border-color: #608ab1;
|
||||
}
|
||||
|
||||
.forum-message.agent:has(.forum-message-header:contains("Insight Engine")) {
|
||||
background-color: #f2ebf3;
|
||||
border-color: #8e6a9f;
|
||||
}
|
||||
.forum-message.agent:has(.forum-message-header:contains("Insight Engine")) {
|
||||
background-color: #f2ebf3;
|
||||
border-color: #8e6a9f;
|
||||
}
|
||||
|
||||
.forum-message.agent:has(.forum-message-header:contains("INSIGHT Engine")) {
|
||||
background-color: #f2ebf3;
|
||||
border-color: #8e6a9f;
|
||||
}
|
||||
background-color: #f2ebf3;
|
||||
border-color: #8e6a9f;
|
||||
}
|
||||
|
||||
.forum-message.agent:has(.forum-message-header:contains("Media Engine")) {
|
||||
background-color: #ebf2ea;
|
||||
border-color: #6a9a6e;
|
||||
}
|
||||
.forum-message.agent:has(.forum-message-header:contains("Media Engine")) {
|
||||
background-color: #ebf2ea;
|
||||
border-color: #6a9a6e;
|
||||
}
|
||||
|
||||
.forum-message.agent:has(.forum-message-header:contains("MEDIA Engine")) {
|
||||
background-color: #ebf2ea;
|
||||
border-color: #6a9a6e;
|
||||
}
|
||||
background-color: #ebf2ea;
|
||||
border-color: #6a9a6e;
|
||||
}
|
||||
|
||||
/* 备用方案:通过JavaScript添加的类 */
|
||||
.forum-message.query-engine {
|
||||
background-color: #eaf1f8;
|
||||
border-color: #608ab1;
|
||||
}
|
||||
/* 备用方案:通过JavaScript添加的类 */
|
||||
.forum-message.query-engine {
|
||||
background-color: #eaf1f8;
|
||||
border-color: #608ab1;
|
||||
}
|
||||
|
||||
.forum-message.insight-engine {
|
||||
background-color: #f2ebf3;
|
||||
border-color: #8e6a9f;
|
||||
}
|
||||
.forum-message.insight-engine {
|
||||
background-color: #f2ebf3;
|
||||
border-color: #8e6a9f;
|
||||
}
|
||||
|
||||
.forum-message.media-engine {
|
||||
background-color: #ebf2ea;
|
||||
border-color: #6a9a6e;
|
||||
}
|
||||
.forum-message.media-engine {
|
||||
background-color: #ebf2ea;
|
||||
border-color: #6a9a6e;
|
||||
}
|
||||
|
||||
.forum-message.agent.QUERY {
|
||||
background-color: #eaf1f8;
|
||||
@@ -857,10 +864,10 @@
|
||||
border-color: #608ab1;
|
||||
}
|
||||
|
||||
.forum-message.agent.MEDIA {
|
||||
background-color: #ebf2ea;
|
||||
border-color: #6a9a6e;
|
||||
}
|
||||
.forum-message.agent.MEDIA {
|
||||
background-color: #ebf2ea;
|
||||
border-color: #6a9a6e;
|
||||
}
|
||||
|
||||
.forum-message.agent.media-engine {
|
||||
background-color: #ebf2ea;
|
||||
@@ -2378,6 +2385,7 @@
|
||||
// Report Engine 相关函数
|
||||
let reportLogLineCount = 0;
|
||||
let reportLockCheckInterval = null;
|
||||
let lastCompletedReportTask = null;
|
||||
|
||||
// 实时刷新论坛消息(适用于所有页面)
|
||||
function refreshForumMessages() {
|
||||
@@ -2783,6 +2791,12 @@
|
||||
<div>正在初始化...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制按钮区域 -->
|
||||
<div class="report-controls">
|
||||
<button class="report-button primary" id="generateReportButton">生成最终报告</button>
|
||||
<button class="report-button" id="downloadReportButton" disabled>下载HTML</button>
|
||||
</div>
|
||||
|
||||
<!-- 任务进度区域 -->
|
||||
<div id="taskProgressArea"></div>
|
||||
@@ -2796,19 +2810,146 @@
|
||||
`;
|
||||
|
||||
reportContent.innerHTML = interfaceHTML;
|
||||
initializeReportControls();
|
||||
|
||||
// 立即更新状态信息
|
||||
updateEngineStatusDisplay(statusData);
|
||||
|
||||
// 如果有当前任务,显示任务状态
|
||||
if (statusData.current_task) {
|
||||
const taskArea = document.getElementById('taskProgressArea');
|
||||
if (taskArea) {
|
||||
taskArea.innerHTML = renderTaskStatus(statusData.current_task);
|
||||
updateTaskProgressStatus(statusData.current_task);
|
||||
} else {
|
||||
updateDownloadButtonState(null);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeReportControls() {
|
||||
const generateButton = document.getElementById('generateReportButton');
|
||||
if (generateButton && !generateButton.dataset.bound) {
|
||||
generateButton.dataset.bound = 'true';
|
||||
generateButton.addEventListener('click', () => {
|
||||
if (reportTaskId) {
|
||||
showMessage('已有报告生成任务在运行', 'info');
|
||||
return;
|
||||
}
|
||||
const reportButton = document.querySelector('[data-app="report"]');
|
||||
if (reportButton && reportButton.classList.contains('locked')) {
|
||||
showMessage('需等待三个Agent完成最新分析后才能生成最终报告', 'error');
|
||||
return;
|
||||
}
|
||||
generateReport();
|
||||
});
|
||||
}
|
||||
|
||||
const downloadButton = document.getElementById('downloadReportButton');
|
||||
if (downloadButton && !downloadButton.dataset.bound) {
|
||||
downloadButton.dataset.bound = 'true';
|
||||
downloadButton.addEventListener('click', () => downloadReport());
|
||||
}
|
||||
|
||||
if (reportTaskId) {
|
||||
setGenerateButtonState(true);
|
||||
} else {
|
||||
setGenerateButtonState(false);
|
||||
}
|
||||
|
||||
if (lastCompletedReportTask) {
|
||||
updateDownloadButtonState(lastCompletedReportTask);
|
||||
}
|
||||
}
|
||||
|
||||
function setGenerateButtonState(forceLoading = false) {
|
||||
const generateButton = document.getElementById('generateReportButton');
|
||||
if (!generateButton) return;
|
||||
|
||||
if (forceLoading || reportTaskId) {
|
||||
if (!generateButton.dataset.originalText) {
|
||||
generateButton.dataset.originalText = generateButton.textContent || '生成最终报告';
|
||||
}
|
||||
generateButton.disabled = true;
|
||||
generateButton.textContent = '生成中...';
|
||||
} else {
|
||||
const originalText = generateButton.dataset.originalText || '生成最终报告';
|
||||
generateButton.disabled = false;
|
||||
generateButton.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
function updateDownloadButtonState(task) {
|
||||
const downloadButton = document.getElementById('downloadReportButton');
|
||||
if (!downloadButton) return;
|
||||
|
||||
if (task && task.status === 'completed' && (task.report_file_ready || task.report_file_path)) {
|
||||
downloadButton.disabled = false;
|
||||
downloadButton.dataset.taskId = task.task_id;
|
||||
downloadButton.dataset.filename = task.report_file_name || '';
|
||||
const label = task.report_file_name ? `下载HTML (${task.report_file_name})` : '下载HTML';
|
||||
downloadButton.textContent = label;
|
||||
lastCompletedReportTask = task;
|
||||
} else if (!lastCompletedReportTask || (task && task.status !== 'completed')) {
|
||||
downloadButton.disabled = true;
|
||||
downloadButton.dataset.taskId = '';
|
||||
downloadButton.dataset.filename = '';
|
||||
downloadButton.textContent = '下载HTML';
|
||||
if (!reportTaskId) {
|
||||
lastCompletedReportTask = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function downloadReport(taskId = null) {
|
||||
const downloadButton = document.getElementById('downloadReportButton');
|
||||
const targetTaskId = taskId || (downloadButton ? downloadButton.dataset.taskId : '');
|
||||
|
||||
if (!targetTaskId) {
|
||||
showMessage('暂无可下载的报告,请先生成最终报告', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
let preferredFileName = '';
|
||||
if (downloadButton && downloadButton.dataset.filename) {
|
||||
preferredFileName = downloadButton.dataset.filename;
|
||||
} else if (lastCompletedReportTask && lastCompletedReportTask.task_id === targetTaskId) {
|
||||
preferredFileName = lastCompletedReportTask.report_file_name || '';
|
||||
}
|
||||
|
||||
fetch(`/api/report/download/${targetTaskId}`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
const contentType = response.headers.get('Content-Type') || '';
|
||||
if (contentType.includes('application/json')) {
|
||||
return response.json().then(err => {
|
||||
throw new Error(err.error || '下载失败');
|
||||
});
|
||||
}
|
||||
throw new Error('下载失败');
|
||||
}
|
||||
const disposition = response.headers.get('Content-Disposition') || '';
|
||||
return response.blob().then(blob => ({ blob, disposition }));
|
||||
})
|
||||
.then(({ blob, disposition }) => {
|
||||
let filename = preferredFileName;
|
||||
if (!filename) {
|
||||
const match = disposition.match(/filename="?([^";]+)"?/i);
|
||||
filename = match ? match[1] : `final_report_${targetTaskId}.html`;
|
||||
}
|
||||
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename || 'final_report.html';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
showMessage('报告文件已开始下载', 'success');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('下载报告失败:', error);
|
||||
showMessage('下载报告失败: ' + error.message, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染任务状态(使用新的进度条样式)
|
||||
function renderTaskStatus(task) {
|
||||
// 状态文本的中文映射
|
||||
@@ -2863,7 +3004,18 @@
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
if (task.report_file_path) {
|
||||
statusHTML += `
|
||||
<div class="task-info-line">
|
||||
<div class="task-info-item">
|
||||
<span class="task-info-label">保存路径:</span>
|
||||
<span class="task-info-value">${task.report_file_path}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (task.error_message) {
|
||||
statusHTML += `
|
||||
<div class="task-error-message">
|
||||
@@ -2871,13 +3023,33 @@
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
if (task.status === 'completed') {
|
||||
statusHTML += `
|
||||
<div class="task-actions">
|
||||
<button class="report-button primary" onclick="viewReport('${task.task_id}')">重新加载</button>
|
||||
${task.report_file_ready ? `<button class="report-button" onclick="downloadReport('${task.task_id}')">下载HTML</button>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
statusHTML += '</div>';
|
||||
return statusHTML;
|
||||
}
|
||||
|
||||
// 生成报告
|
||||
function generateReport() {
|
||||
if (reportTaskId) {
|
||||
showMessage('已有报告生成任务在运行', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const reportButton = document.querySelector('[data-app="report"]');
|
||||
if (reportButton && reportButton.classList.contains('locked')) {
|
||||
showMessage('需等待三个Agent完成最新分析后才能生成最终报告', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const query = document.getElementById('searchInput').value.trim() || '智能舆情分析报告';
|
||||
|
||||
// 重置日志计数器,因为后台会清空日志文件
|
||||
@@ -2887,8 +3059,8 @@
|
||||
const consoleOutput = document.getElementById('consoleOutput');
|
||||
consoleOutput.innerHTML = '<div class="console-line">[系统] 开始生成报告,日志已重置</div>';
|
||||
|
||||
// 按钮已移除,无需操作按钮状态
|
||||
|
||||
setGenerateButtonState(true);
|
||||
|
||||
// 在现有状态信息后添加任务进度状态,而不是替换
|
||||
addTaskProgressStatus('正在启动报告生成任务...', 'loading');
|
||||
|
||||
@@ -2934,6 +3106,7 @@
|
||||
// 重置标志允许重新尝试
|
||||
autoGenerateTriggered = false;
|
||||
reportTaskId = null;
|
||||
setGenerateButtonState(false);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -2942,6 +3115,7 @@
|
||||
// 重置标志允许重新尝试
|
||||
autoGenerateTriggered = false;
|
||||
reportTaskId = null;
|
||||
setGenerateButtonState(false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2977,6 +3151,7 @@
|
||||
// 重置自动生成标志,允许下次有新内容时自动生成
|
||||
autoGenerateTriggered = false;
|
||||
reportTaskId = null;
|
||||
setGenerateButtonState(false);
|
||||
} else if (data.task.status === 'error') {
|
||||
clearInterval(reportPollingInterval);
|
||||
showMessage('报告生成失败: ' + data.task.error_message, 'error');
|
||||
@@ -2984,6 +3159,7 @@
|
||||
// 重置自动生成标志,允许重新尝试
|
||||
autoGenerateTriggered = false;
|
||||
reportTaskId = null;
|
||||
setGenerateButtonState(false);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -3020,11 +3196,17 @@
|
||||
|
||||
if (task) {
|
||||
taskArea.innerHTML = renderTaskStatus(task);
|
||||
if (task.status === 'completed') {
|
||||
lastCompletedReportTask = task;
|
||||
} else if (task.status === 'running') {
|
||||
lastCompletedReportTask = null;
|
||||
}
|
||||
updateDownloadButtonState(task);
|
||||
} else if (status && errorMessage) {
|
||||
const loadingIndicator = status === 'loading' ? '<span class="report-loading-spinner"></span>' : '';
|
||||
const statusBadgeClass = status === 'error' ? 'task-status-error' : 'task-status-running';
|
||||
const statusText = status === 'error' ? '错误' : '处理中';
|
||||
|
||||
|
||||
taskArea.innerHTML = `
|
||||
<div class="task-progress-container">
|
||||
<div class="task-progress-header">
|
||||
@@ -3253,4 +3435,4 @@
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
Reference in New Issue
Block a user