fix(rvw): speed up review flow and complete forensics export
Run RVW skills in controlled parallel mode and persist per-skill progress so users can view completed tabs during execution. Also include data-forensics tables in Word export and refresh the RVW module status documentation. Made-with: Cursor
This commit is contained in:
@@ -39,6 +39,7 @@ const getProgressSteps = (selectedAgents: string[]) => {
|
||||
const steps = [
|
||||
{ key: 'upload', label: '上传文档' },
|
||||
{ key: 'extract', label: '文本提取' },
|
||||
{ key: 'forensics', label: '数据验证' },
|
||||
];
|
||||
|
||||
if (selectedAgents.includes('editorial')) {
|
||||
@@ -67,6 +68,17 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
|
||||
const isCompleted = task.status === 'completed' || task.status === 'partial_completed';
|
||||
const isPartial = task.status === 'partial_completed';
|
||||
const isFailed = task.status === 'failed';
|
||||
const hasIncrementalResult =
|
||||
!!task.editorialReview || !!task.methodologyReview || !!task.forensicsResult || !!task.clinicalReview;
|
||||
const displayReport: ReviewReport | null = report || (hasIncrementalResult
|
||||
? {
|
||||
...task,
|
||||
editorialReview: task.editorialReview,
|
||||
methodologyReview: task.methodologyReview,
|
||||
forensicsResult: task.forensicsResult,
|
||||
clinicalReview: task.clinicalReview,
|
||||
}
|
||||
: null);
|
||||
|
||||
// 轮询任务状态
|
||||
useEffect(() => {
|
||||
@@ -110,20 +122,20 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
|
||||
}
|
||||
}, [isCompleted, task.id, report]);
|
||||
|
||||
// 报告加载后自动设置正确的 Tab
|
||||
// 报告(含增量结果)加载后自动设置可用 Tab
|
||||
useEffect(() => {
|
||||
if (report) {
|
||||
if (report.editorialReview) {
|
||||
if (displayReport) {
|
||||
if (displayReport.editorialReview) {
|
||||
setActiveTab('editorial');
|
||||
} else if (report.methodologyReview) {
|
||||
} else if (displayReport.methodologyReview) {
|
||||
setActiveTab('methodology');
|
||||
} else if (report.forensicsResult) {
|
||||
} else if (displayReport.forensicsResult) {
|
||||
setActiveTab('forensics');
|
||||
} else if (report.clinicalReview) {
|
||||
} else if (displayReport.clinicalReview) {
|
||||
setActiveTab('clinical');
|
||||
}
|
||||
}
|
||||
}, [report]);
|
||||
}, [displayReport]);
|
||||
|
||||
// 动态获取进度步骤
|
||||
const progressSteps = getProgressSteps(task.selectedAgents || ['editorial', 'methodology']);
|
||||
@@ -132,6 +144,21 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
|
||||
const getStepStatus = (stepKey: string): 'completed' | 'active' | 'pending' => {
|
||||
const hasEditorial = task.selectedAgents?.includes('editorial');
|
||||
const hasMethodology = task.selectedAgents?.includes('methodology');
|
||||
const hasClinical = task.selectedAgents?.includes('clinical');
|
||||
const skillMap: Record<string, string> = {
|
||||
forensics: 'DataForensicsSkill',
|
||||
editorial: 'EditorialSkill',
|
||||
methodology: 'MethodologySkill',
|
||||
clinical: 'ClinicalAssessmentSkill',
|
||||
};
|
||||
const currentSkill = stepKey in skillMap ? task.reviewProgress?.[skillMap[stepKey]] : undefined;
|
||||
|
||||
if (currentSkill?.status && ['success', 'warning', 'skipped'].includes(currentSkill.status)) {
|
||||
return 'completed';
|
||||
}
|
||||
if (currentSkill?.status && ['error', 'timeout'].includes(currentSkill.status)) {
|
||||
return 'completed';
|
||||
}
|
||||
|
||||
if (task.status === 'pending') {
|
||||
return stepKey === 'upload' ? 'completed' : 'pending';
|
||||
@@ -143,7 +170,10 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
|
||||
}
|
||||
if (task.status === 'reviewing' || task.status === 'reviewing_editorial') {
|
||||
if (['upload', 'extract'].includes(stepKey)) return 'completed';
|
||||
if (stepKey === 'forensics') return 'active';
|
||||
if (stepKey === 'editorial' && hasEditorial) return 'active';
|
||||
if (stepKey === 'methodology' && hasMethodology) return 'active';
|
||||
if (stepKey === 'clinical' && hasClinical) return 'active';
|
||||
return 'pending';
|
||||
}
|
||||
if (task.status === 'reviewing_methodology') {
|
||||
@@ -245,12 +275,20 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
|
||||
);
|
||||
|
||||
children.push(new Paragraph({ spacing: { after: 300 } }));
|
||||
|
||||
let sectionIndex = 1;
|
||||
const sectionNumbers = ['一', '二', '三', '四', '五', '六'];
|
||||
const nextSectionTitle = (title: string) => {
|
||||
const current = sectionNumbers[sectionIndex - 1] || `${sectionIndex}`;
|
||||
sectionIndex += 1;
|
||||
return `${current}、${title}`;
|
||||
};
|
||||
|
||||
// 稿约规范性评估
|
||||
if (report.editorialReview) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: '一、稿约规范性评估',
|
||||
text: nextSectionTitle('稿约规范性评估'),
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
spacing: { before: 400, after: 200 },
|
||||
})
|
||||
@@ -313,7 +351,7 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
|
||||
if (report.methodologyReview) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: '二、方法学评估',
|
||||
text: nextSectionTitle('方法学评估'),
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
spacing: { before: 400, after: 200 },
|
||||
})
|
||||
@@ -374,13 +412,152 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 数据验证
|
||||
if (report.forensicsResult) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: nextSectionTitle('数据验证'),
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
spacing: { before: 400, after: 200 },
|
||||
})
|
||||
);
|
||||
|
||||
const summary = report.forensicsResult.summary;
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: `共识别表格 ${summary.totalTables} 个;发现问题 ${summary.totalIssues} 个(错误 ${summary.errorCount},警告 ${summary.warningCount})。`,
|
||||
spacing: { after: 160 },
|
||||
})
|
||||
);
|
||||
|
||||
// 导出表格明细(按 forensicsResult.tables 渲染)
|
||||
if (report.forensicsResult.tables.length > 0) {
|
||||
report.forensicsResult.tables.slice(0, 20).forEach((tableItem, tableIndex) => {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: `表${tableIndex + 1}:${tableItem.caption || tableItem.id || '未命名表格'}`,
|
||||
heading: HeadingLevel.HEADING_2,
|
||||
spacing: { before: 180, after: 100 },
|
||||
})
|
||||
);
|
||||
|
||||
const rows: TableRow[] = [];
|
||||
const headers = tableItem.headers || [];
|
||||
const tableData = tableItem.data || [];
|
||||
|
||||
if (headers.length > 0) {
|
||||
rows.push(
|
||||
new TableRow({
|
||||
children: headers.map((header) => new TableCell({
|
||||
children: [new Paragraph({ children: [new TextRun({ text: String(header ?? ''), bold: true })] })],
|
||||
})),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 限制导出行数,避免超大表格导致文档膨胀
|
||||
tableData.slice(0, 200).forEach((row) => {
|
||||
rows.push(
|
||||
new TableRow({
|
||||
children: row.map((cell) => new TableCell({
|
||||
children: [new Paragraph(String(cell ?? ''))],
|
||||
})),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (rows.length > 0) {
|
||||
children.push(
|
||||
new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: '(该表未提取到可导出的结构化数据)',
|
||||
spacing: { after: 80 },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const tableIssues = (tableItem.issues || []).slice(0, 50);
|
||||
if (tableIssues.length > 0) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: '该表问题:', bold: true })],
|
||||
spacing: { before: 80, after: 40 },
|
||||
})
|
||||
);
|
||||
tableIssues.forEach((issue) => {
|
||||
const level = issue.severity === 'ERROR' ? '错误' : issue.severity === 'WARNING' ? '警告' : '提示';
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: `• [${level}] ${issue.message}`,
|
||||
indent: { left: 720 },
|
||||
spacing: { after: 40 },
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (report.forensicsResult.tables.length > 20) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: `(其余 ${report.forensicsResult.tables.length - 20} 张表格已省略)`,
|
||||
spacing: { before: 80, after: 80 },
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (report.forensicsResult.issues.length > 0) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: '问题清单:', bold: true })],
|
||||
spacing: { after: 80 },
|
||||
})
|
||||
);
|
||||
|
||||
report.forensicsResult.issues.slice(0, 120).forEach((issue) => {
|
||||
const level = issue.severity === 'ERROR' ? '错误' : issue.severity === 'WARNING' ? '警告' : '提示';
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: `• [${level}] ${issue.message}`,
|
||||
indent: { left: 720 },
|
||||
spacing: { after: 50 },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (report.forensicsResult.issues.length > 120) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: `(其余 ${report.forensicsResult.issues.length - 120} 条问题已省略)`,
|
||||
indent: { left: 720 },
|
||||
spacing: { after: 100 },
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: '✓ 未发现数据一致性问题', color: '006600' })],
|
||||
indent: { left: 720 },
|
||||
spacing: { after: 100 },
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 临床专业评估
|
||||
if (report.clinicalReview) {
|
||||
const sectionNum = report.methodologyReview ? '三' : '二';
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: `${sectionNum}、临床专业评估`,
|
||||
text: nextSectionTitle('临床专业评估'),
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
spacing: { before: 400, after: 200 },
|
||||
})
|
||||
@@ -552,7 +729,7 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
|
||||
{/* 提示信息 */}
|
||||
<div className="text-center text-slate-500 text-sm bg-slate-50 rounded-lg py-4">
|
||||
<p>AI 正在分析您的稿件,这可能需要 1-3 分钟</p>
|
||||
<p className="text-slate-400 mt-1">请耐心等待,完成后将自动显示结果</p>
|
||||
<p className="text-slate-400 mt-1">模块完成后会实时显示,无需等待全部结束</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -594,8 +771,8 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 完成状态 - 显示报告 */}
|
||||
{isCompleted && report && (
|
||||
{/* 增量报告:处理中也可查看已完成模块 */}
|
||||
{displayReport && (isProcessing || isCompleted || isPartial) && (
|
||||
<>
|
||||
{/* 审查完成信息 */}
|
||||
<div className="bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl shadow-lg p-5 mb-8 text-white">
|
||||
@@ -603,10 +780,10 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle className="w-6 h-6 text-white/90" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">审查完成</h2>
|
||||
<h2 className="text-lg font-semibold">{isCompleted ? '审查完成' : '已完成模块结果(实时更新)'}</h2>
|
||||
<p className="text-indigo-100 text-sm">
|
||||
审查用时 {report.durationSeconds ? formatTime(report.durationSeconds) : '-'}
|
||||
{report.completedAt && ` · ${new Date(report.completedAt).toLocaleString('zh-CN')}`}
|
||||
审查用时 {displayReport.durationSeconds ? formatTime(displayReport.durationSeconds) : '-'}
|
||||
{displayReport.completedAt && ` · ${new Date(displayReport.completedAt).toLocaleString('zh-CN')}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -615,7 +792,7 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
|
||||
|
||||
{/* Tab切换 */}
|
||||
<div className="flex gap-1 bg-slate-200/50 p-1 rounded-lg mb-6 w-fit mx-auto">
|
||||
{report.editorialReview && (
|
||||
{displayReport.editorialReview && (
|
||||
<button
|
||||
onClick={() => setActiveTab('editorial')}
|
||||
className={`px-6 py-2 rounded-md text-sm transition-all ${
|
||||
@@ -627,7 +804,7 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
|
||||
稿约规范性
|
||||
</button>
|
||||
)}
|
||||
{report.methodologyReview && (
|
||||
{displayReport.methodologyReview && (
|
||||
<button
|
||||
onClick={() => setActiveTab('methodology')}
|
||||
className={`px-6 py-2 rounded-md text-sm transition-all ${
|
||||
@@ -639,7 +816,7 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
|
||||
方法学评估
|
||||
</button>
|
||||
)}
|
||||
{report.forensicsResult && (
|
||||
{displayReport.forensicsResult && (
|
||||
<button
|
||||
onClick={() => setActiveTab('forensics')}
|
||||
className={`px-6 py-2 rounded-md text-sm transition-all ${
|
||||
@@ -648,10 +825,10 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
|
||||
: 'font-medium text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
数据验证 ({report.forensicsResult.summary.totalIssues || 0}个问题)
|
||||
数据验证 ({displayReport.forensicsResult.summary.totalIssues || 0}个问题)
|
||||
</button>
|
||||
)}
|
||||
{report.clinicalReview && (
|
||||
{displayReport.clinicalReview && (
|
||||
<button
|
||||
onClick={() => setActiveTab('clinical')}
|
||||
className={`px-6 py-2 rounded-md text-sm transition-all ${
|
||||
@@ -666,7 +843,7 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
|
||||
</div>
|
||||
|
||||
{/* 非 docx 文件无数据验证提示 */}
|
||||
{!report.forensicsResult && (report.editorialReview || report.methodologyReview) && (() => {
|
||||
{!displayReport.forensicsResult && (displayReport.editorialReview || displayReport.methodologyReview) && (() => {
|
||||
const fileName = task.fileName || '';
|
||||
const isPdf = fileName.toLowerCase().endsWith('.pdf');
|
||||
const isDoc = fileName.toLowerCase().endsWith('.doc');
|
||||
@@ -686,17 +863,17 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
|
||||
})()}
|
||||
|
||||
{/* 报告内容 */}
|
||||
{activeTab === 'editorial' && report.editorialReview && (
|
||||
<EditorialReport data={report.editorialReview} />
|
||||
{activeTab === 'editorial' && displayReport.editorialReview && (
|
||||
<EditorialReport data={displayReport.editorialReview} />
|
||||
)}
|
||||
{activeTab === 'methodology' && report.methodologyReview && (
|
||||
<MethodologyReport data={report.methodologyReview} />
|
||||
{activeTab === 'methodology' && displayReport.methodologyReview && (
|
||||
<MethodologyReport data={displayReport.methodologyReview} />
|
||||
)}
|
||||
{activeTab === 'forensics' && report.forensicsResult && (
|
||||
<ForensicsReport data={report.forensicsResult} />
|
||||
{activeTab === 'forensics' && displayReport.forensicsResult && (
|
||||
<ForensicsReport data={displayReport.forensicsResult} />
|
||||
)}
|
||||
{activeTab === 'clinical' && report.clinicalReview && (
|
||||
<ClinicalReport data={report.clinicalReview} />
|
||||
{activeTab === 'clinical' && displayReport.clinicalReview && (
|
||||
<ClinicalReport data={displayReport.clinicalReview} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -42,6 +42,16 @@ export interface ReviewTask {
|
||||
timeoutCount: number;
|
||||
totalSkills: number;
|
||||
};
|
||||
editorialReview?: EditorialReviewResult;
|
||||
methodologyReview?: MethodologyReviewResult;
|
||||
forensicsResult?: ForensicsResult;
|
||||
clinicalReview?: ClinicalReviewResult;
|
||||
reviewProgress?: Record<string, {
|
||||
status: 'success' | 'warning' | 'error' | 'timeout' | 'skipped';
|
||||
executionTime?: number;
|
||||
completedAt?: string;
|
||||
error?: string | null;
|
||||
}>;
|
||||
createdAt: string;
|
||||
completedAt?: string;
|
||||
durationSeconds?: number;
|
||||
|
||||
Reference in New Issue
Block a user