/** * 完整的批处理模式组件 * 精细化设计,支持模板选择、文档选择、执行和结果展示 */ import React, { useState, useEffect } from 'react'; import { Button, Table, Progress, message, Card, Steps, Checkbox, Radio, Tooltip } from 'antd'; import { Play, Download, RotateCw, FileText, CheckCircle2, Zap } from 'lucide-react'; import type { KnowledgeBase, Document } from '../../api/knowledgeBaseApi'; interface BatchModeCompleteProps { kbId: string; kbInfo: KnowledgeBase; documents: Document[]; template: string; } interface BatchTemplate { id: string; name: string; description: string; fields: { key: string; label: string }[]; } interface BatchResult { documentId: string; documentName: string; status: 'pending' | 'processing' | 'completed' | 'error'; progress: number; result?: Record; error?: string; } // 模板配置(ID必须与后端 PRESET_TEMPLATES 匹配) const TEMPLATES: BatchTemplate[] = [ { id: 'clinical_research', // ✅ 必须与后端模板ID匹配 name: '临床研究信息提取', description: '提取研究目的、设计、对象、样本量、干预、对照、结果、证据等级', fields: [ { key: 'research_purpose', label: '研究目的' }, { key: 'research_design', label: '研究设计' }, { key: 'research_subjects', label: '研究对象' }, { key: 'sample_size', label: '样本量' }, { key: 'intervention_group', label: '干预组' }, { key: 'control_group', label: '对照组' }, { key: 'results_data', label: '结果及数据' }, { key: 'oxford_level', label: '牛津评级' }, ], }, ]; export const BatchModeComplete: React.FC = ({ kbId, documents, template: initialTemplate, }) => { const [step, setStep] = useState(0); // 0: 配置, 1: 执行中, 2: 结果 const [selectedTemplate, setSelectedTemplate] = useState(TEMPLATES[0]); const [selectedDocs, setSelectedDocs] = useState([]); const [results, setResults] = useState([]); const [isExecuting, setIsExecuting] = useState(false); // 过滤出已完成解析的文档,并去重(确保唯一性) const completedDocs = React.useMemo(() => { const seen = new Set(); return documents.filter(doc => { if (doc.status === 'completed' && !seen.has(doc.id)) { seen.add(doc.id); return true; } return false; }); }, [documents]); useEffect(() => { // 初始化模板(如果有传入则使用,否则默认第一个) if (initialTemplate) { const template = TEMPLATES.find(t => t.id === initialTemplate); if (template) setSelectedTemplate(template); } }, [initialTemplate]); // 切换文档选择状态(toggle模式,参考旧版实现) const handleToggleDocument = (docId: string) => { if (selectedDocs.includes(docId)) { // 已选中 -> 取消选择 setSelectedDocs(prev => prev.filter(id => id !== docId)); } else { // 未选中 -> 添加选择 if (selectedDocs.length >= 50) { message.warning('最多选择50篇文档'); return; } setSelectedDocs(prev => [...prev, docId]); } }; // 全选/取消全选 const handleSelectAll = (checked: boolean) => { if (checked) { const allIds = completedDocs.slice(0, 50).map(d => d.id); setSelectedDocs(allIds); } else { setSelectedDocs([]); } }; // 执行批处理 const handleExecute = async () => { if (selectedDocs.length < 3) { message.warning('请至少选择3篇文档'); return; } if (!selectedTemplate) { message.error('请选择批处理模板'); return; } setStep(1); setIsExecuting(true); // 初始化结果 - 使用唯一索引确保key唯一 const initialResults: BatchResult[] = selectedDocs.map((docId, index) => { const doc = completedDocs.find(d => d.id === docId); return { documentId: `${docId}-${index}`, // 确保唯一性 documentName: doc?.filename || '未知文档', status: 'pending', progress: 0, }; }); setResults(initialResults); try { // 调用批处理API(使用v2新版API) // 完整路径: /api/v2/pkb/batch-tasks/batch/execute // 请求体格式必须匹配后端 ExecuteBatchBody 接口 const response = await fetch('/api/v2/pkb/batch-tasks/batch/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kb_id: kbId, // 后端期望 kb_id document_ids: selectedDocs, // 后端期望 document_ids template_type: 'preset', // 使用预设模板 template_id: selectedTemplate.id, // 后端期望 template_id model_type: 'qwen-long', // 使用qwen-long模型 task_name: `${selectedTemplate.name}_${new Date().toLocaleString('zh-CN')}`, }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.message || '批处理执行失败'); } const responseData = await response.json(); console.log('[BatchMode] API响应:', responseData); // 后端返回格式: { success: true, data: { task_id: xxx } } const taskId = responseData.data?.task_id || responseData.taskId || responseData.task_id; if (!taskId) { console.error('[BatchMode] 未获取到taskId:', responseData); message.error('批处理任务创建失败:未获取到任务ID'); setIsExecuting(false); setStep(0); return; } console.log('[BatchMode] 任务已创建,开始轮询:', taskId); // 轮询任务状态 const pollInterval = setInterval(async () => { try { const statusRes = await fetch(`/api/v2/pkb/batch-tasks/batch/tasks/${taskId}`); if (!statusRes.ok) { console.error('[BatchMode] 获取任务状态失败:', statusRes.status); return; } const statusJson = await statusRes.json(); const taskData = statusJson.data || statusJson; console.log('[BatchMode] 任务状态:', taskData); // 计算进度 - 基于后端返回的completed_count const completedCount = taskData.completed_count || 0; const failedCount = taskData.failed_count || 0; const processedCount = completedCount + failedCount; // 更新每个文档的状态(根据处理进度模拟) setResults(prev => prev.map((r, idx) => { if (idx < completedCount) { // 已完成 return { ...r, status: 'completed' as const, progress: 100 }; } else if (idx < processedCount) { // 失败 return { ...r, status: 'error' as const, progress: 100 }; } else if (idx === processedCount && taskData.status === 'processing') { // 正在处理 return { ...r, status: 'processing' as const, progress: 50 }; } return r; })); // 检查是否全部完成 if (taskData.status === 'completed' || taskData.status === 'failed') { clearInterval(pollInterval); setIsExecuting(false); // 获取最终结果 try { const resultsRes = await fetch(`/api/v2/pkb/batch-tasks/batch/tasks/${taskId}/results`); console.log('[BatchMode] 获取结果响应状态:', resultsRes.status); if (resultsRes.ok) { const resultsJson = await resultsRes.json(); console.log('[BatchMode] 结果数据:', JSON.stringify(resultsJson, null, 2)); const resultsData = resultsJson.data?.results || []; console.log('[BatchMode] 解析到的结果数量:', resultsData.length); if (resultsData.length > 0) { // 构建新的结果数组 - 后端返回的提取数据在 data 字段中 const newResults: BatchResult[] = resultsData.map((docResult: any, idx: number) => { console.log(`[BatchMode] 文档 ${idx}:`, { id: docResult.document_id, name: docResult.document_name, status: docResult.status, hasData: !!docResult.data, dataKeys: docResult.data ? Object.keys(docResult.data) : [], }); // 🔑 后端返回的状态是 "success" 或 "failed",需要映射为前端的 "completed" 或 "error" const isSuccess = docResult.status === 'success' || docResult.status === 'completed'; return { documentId: docResult.document_id || `doc-${idx}`, documentName: docResult.document_name || `文档${idx + 1}`, status: isSuccess ? 'completed' as const : 'error' as const, progress: 100, result: docResult.data, // 后端返回的提取数据 error: docResult.error_message, }; }); console.log('[BatchMode] 更新结果:', newResults); setResults(newResults); } else { console.warn('[BatchMode] 没有结果数据'); } } else { console.error('[BatchMode] 获取结果失败,状态码:', resultsRes.status); } } catch (e) { console.error('[BatchMode] 获取结果异常:', e); } // 延迟设置step,确保状态更新完成 setTimeout(() => { setStep(2); }, 100); if (taskData.status === 'completed') { message.success(`批处理完成!成功 ${completedCount} 篇,失败 ${failedCount} 篇`); } else { message.error('批处理失败: ' + (taskData.error || '未知错误')); } } } catch (error) { console.error('[BatchMode] 轮询任务状态失败:', error); } }, 2000); // 设置超时保护(5分钟) setTimeout(() => { clearInterval(pollInterval); if (isExecuting) { setIsExecuting(false); setStep(2); message.warning('任务执行超时,请稍后查看结果'); } }, 5 * 60 * 1000); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : '执行失败'; message.error(errorMessage); setIsExecuting(false); setStep(0); // 返回配置步骤 } }; // 导出Excel (CSV格式) const handleExport = () => { if (!selectedTemplate) return; const completedResults = results.filter(r => r.status === 'completed' && r.result); if (completedResults.length === 0) { message.warning('没有可导出的结果'); return; } // 构建CSV数据 - 使用模板定义的字段 const headers = ['文档名称', ...selectedTemplate.fields.map(f => f.label)]; const rows = completedResults.map(r => [ r.documentName, ...selectedTemplate.fields.map(f => { const value = r.result?.[f.key]; if (!value) return '-'; // 处理换行符和引号 return String(value).replace(/"/g, '""').replace(/\n/g, ' '); }), ]); const csvContent = [ headers.join(','), ...rows.map(row => row.map(cell => `"${cell}"`).join(',')), ].join('\n'); // 下载 const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); const timestamp = new Date().toISOString().split('T')[0]; link.download = `批处理结果_${selectedTemplate.name}_${timestamp}.csv`; link.click(); message.success(`已导出 ${completedResults.length} 条结果`); }; // 重置 const handleReset = () => { setStep(0); setSelectedDocs([]); setResults([]); }; return (
{/* 步骤指示器 */}
{/* 步骤1: 配置 */} {step === 0 && (
{/* 模板选择 */} 选择批处理模板
} size="small" className="shadow-sm" > { const template = TEMPLATES.find(t => t.id === e.target.value); setSelectedTemplate(template || null); }} className="w-full" >
{TEMPLATES.map(t => (
setSelectedTemplate(t)} >
{t.name}
{t.description}
{t.fields.slice(0, 5).map(f => ( {f.label} ))} {t.fields.length > 5 && ( +{t.fields.length - 5}项 )}
))}
{/* 文档选择 */}
选择文档 ({completedDocs.length} 篇可用)
0} indeterminate={selectedDocs.length > 0 && selectedDocs.length < completedDocs.length} onChange={(e) => handleSelectAll(e.target.checked)} > 全选
} size="small" className="shadow-sm" >
{completedDocs.length === 0 ? (

暂无已完成解析的文档

) : ( completedDocs.map(doc => (
handleToggleDocument(doc.id)} > handleToggleDocument(doc.id)} onClick={(e) => e.stopPropagation()} />
{doc.filename}
{doc.tokensCount ? `${(doc.tokensCount / 1000).toFixed(0)}k tokens` : ''}
{selectedDocs.includes(doc.id) && ( )}
)) )}
= 3 ? 'bg-green-50 border border-green-200' : 'bg-yellow-50 border border-yellow-200' }`}> = 3 ? 'text-green-500' : 'text-yellow-500' }`} /> 已选择 {selectedDocs.length} 篇文档 {selectedDocs.length < 3 && (至少需要3篇)}
{/* 开始按钮 - 显示实际选择的文档数量 */}
)} {/* 步骤2: 执行中 */} {step === 1 && (
批处理执行中...
} size="small" className="shadow-sm" >
{results.map((result) => (
{result.documentName}
{result.status === 'completed' && ✅ 完成} {result.status === 'processing' && ⏳ 处理中} {result.status === 'error' && ❌ 失败} {result.status === 'pending' && ⏸️ 等待}
))}
)} {/* 步骤3: 结果 */} {step === 2 && selectedTemplate && (
已完成 {results.filter(r => r.status === 'completed').length} / {results.length} 篇文档处理
{results.filter(r => r.status === 'completed' && r.result).length === 0 ? (

暂无提取结果

请等待处理完成或检查是否有失败的文档

) : ( r.status === 'completed' && r.result)} rowKey="documentId" pagination={false} scroll={{ x: 1800, y: 'calc(100vh - 400px)' }} size="small" className="batch-results-table" columns={[ { title: '文档名称', dataIndex: 'documentName', key: 'documentName', fixed: 'left', width: 220, render: (text: string) => (
{text}
), }, ...selectedTemplate.fields.map(field => ({ title: field.label, key: field.key, width: field.key === 'results_data' ? 280 : 180, // 结果数据列更宽 render: (_: unknown, record: BatchResult) => { const value = record.result?.[field.key]; if (!value) return -; const textValue = String(value); return ( {textValue}} placement="topLeft" overlayStyle={{ maxWidth: 450 }} >
{textValue}
); }, })), ]} /> )} )} ); };