import React, { useState, useEffect, useRef } from 'react'; import { Bot, UploadCloud, FileText, ArrowRight, CheckCircle2, AlertTriangle, Settings2, Download, Table2, Plus, Trash2, RefreshCw, ShieldCheck, Zap, LayoutTemplate, Stethoscope, Split, AlertCircle, Check, X, RotateCcw, MoreHorizontal, Search } from 'lucide-react'; // --- 类型定义 --- type Step = 'upload' | 'schema' | 'processing' | 'verify' | 'result'; interface ExtractionField { id: string; name: string; desc: string; width?: string; // 用于表格宽度 } interface VerifyRow { id: number; text: string; // 原文摘要 fullText: string; // 原文全文 results: Record; status: 'clean' | 'conflict'; // 行状态 } // --- 模拟数据 --- const TEMPLATES: Record> = { 'lung_cancer': { 'pathology': [ { id: 'p1', name: '病理类型', desc: '如:浸润性腺癌', width: 'w-40' }, { id: 'p2', name: '分化程度', desc: '高/中/低分化', width: 'w-32' }, { id: 'p3', name: '肿瘤大小', desc: '最大径,单位cm', width: 'w-32' }, { id: 'p4', name: '淋巴结转移', desc: '有/无及具体组别', width: 'w-48' }, { id: 'p5', name: '免疫组化', desc: '关键指标', width: 'w-56' } ], 'admission': [ { id: 'a1', name: '主诉', desc: '患者入院的主要症状', width: 'w-48' }, { id: 'a2', name: '现病史', desc: '发病过程', width: 'w-64' }, { id: 'a3', name: '吸烟史', desc: '吸烟年支数', width: 'w-32' } ] } }; const ToolB_AIStructurerV4 = () => { const [currentStep, setCurrentStep] = useState('upload'); // --- Step 1: 上传与体检 --- const [fileName, setFileName] = useState('2023_肺癌病理报告_批量.xlsx'); const [selectedColumn, setSelectedColumn] = useState(''); const [columnHealth, setColumnHealth] = useState<'unknown' | 'good' | 'bad'>('unknown'); const [isChecking, setIsChecking] = useState(false); // --- Step 2: Schema --- const [diseaseType, setDiseaseType] = useState('lung_cancer'); const [reportType, setReportType] = useState('pathology'); const [fields, setFields] = useState([]); // --- Step 3: 处理 --- const [progress, setProgress] = useState(0); const [logs, setLogs] = useState([]); // --- Step 4: 验证 (核心升级) --- const [rows, setRows] = useState([]); const [selectedRowId, setSelectedRowId] = useState(null); // 侧边栏控制 // 初始化模版 useEffect(() => { if (diseaseType && reportType && TEMPLATES[diseaseType]?.[reportType]) { setFields(TEMPLATES[diseaseType][reportType]); } else { setFields([]); } }, [diseaseType, reportType]); // 模拟健康检查 const runHealthCheck = (col: string) => { if (!col) return; setIsChecking(true); setColumnHealth('unknown'); setTimeout(() => { setIsChecking(false); setColumnHealth(col.includes('ID') || col.includes('时间') ? 'bad' : 'good'); }, 1000); }; // 模拟处理过程 + 生成验证数据 useEffect(() => { if (currentStep === 'processing') { const timer = setInterval(() => { setProgress(prev => { if (prev >= 100) { clearInterval(timer); // 生成模拟验证数据 setRows([ { id: 1, text: "病理诊断:(右肺上叶)浸润性腺癌,腺泡为主型(70%)...", fullText: "病理诊断:(右肺上叶)浸润性腺癌,腺泡为主型(70%),伴乳头状成分(30%)。肿瘤大小 3.2*2.5*2.0cm。支气管断端未见癌。第7组淋巴结(1/3)见转移。免疫组化:TTF-1(+), NapsinA(+)。", results: { '病理类型': { A: '浸润性腺癌', B: '浸润性腺癌', chosen: '浸润性腺癌' }, '分化程度': { A: '未提及', B: '中分化', chosen: null }, // 冲突 '肿瘤大小': { A: '3.2cm', B: '3.2*2.5*2.0cm', chosen: null }, // 冲突 '淋巴结转移': { A: '第7组(1/3)转移', B: '有', chosen: null }, // 冲突 '免疫组化': { A: 'TTF-1(+)', B: 'TTF-1(+), NapsinA(+)', chosen: null } }, status: 'conflict' }, { id: 2, text: "送检(左肺下叶)组织,镜下见异型细胞巢状排列...", fullText: "送检(左肺下叶)组织,镜下见异型细胞巢状排列,角化珠形成,符合鳞状细胞癌。免疫组化:CK5/6(+), P40(+), TTF-1(-)。", results: { '病理类型': { A: '鳞状细胞癌', B: '鳞状细胞癌', chosen: '鳞状细胞癌' }, '分化程度': { A: '未提及', B: '未提及', chosen: '未提及' }, '肿瘤大小': { A: '未提及', B: '未提及', chosen: '未提及' }, '淋巴结转移': { A: '无', B: '无', chosen: '无' }, '免疫组化': { A: 'CK5/6(+), P40(+)', B: 'CK5/6(+), P40(+)', chosen: 'CK5/6(+), P40(+)' } }, status: 'clean' }, { id: 3, text: "右肺中叶穿刺活检:腺癌。EGFR 19-del(+)...", fullText: "右肺中叶穿刺活检:腺癌。基因检测结果显示:EGFR 19-del(+), ALK(-), ROS1(-)。建议靶向治疗。", results: { '病理类型': { A: '腺癌', B: '腺癌', chosen: '腺癌' }, '分化程度': { A: '未提及', B: '未提及', chosen: '未提及' }, '肿瘤大小': { A: '未提及', B: '未提及', chosen: '未提及' }, '淋巴结转移': { A: '未提及', B: '未提及', chosen: '未提及' }, '免疫组化': { A: 'EGFR(+)', B: 'EGFR 19-del(+)', chosen: null } // 冲突 }, status: 'conflict' } ]); setTimeout(() => setCurrentStep('verify'), 800); return 100; } // 模拟日志 if (prev === 5) setLogs(l => [...l, '初始化双模型引擎 (DeepSeek-V3 & Qwen-Max)...']); if (prev === 20) setLogs(l => [...l, 'PII 脱敏完成...']); if (prev === 40) setLogs(l => [...l, 'DeepSeek: 提取进度 45%']); if (prev === 45) setLogs(l => [...l, 'Qwen: 提取进度 50%']); if (prev === 80) setLogs(l => [...l, '正在进行交叉验证 (Cross-Validation)...']); return prev + 1; }); }, 40); return () => clearInterval(timer); } }, [currentStep]); // Step 4 逻辑: 采纳值 const handleAdopt = (rowId: number, fieldName: string, value: string | null) => { setRows(prev => prev.map(row => { if (row.id !== rowId) return row; const newResults = { ...row.results }; newResults[fieldName].chosen = value; // 检查该行是否还有未解决的冲突 const hasConflict = Object.values(newResults).some(f => f.chosen === null); return { ...row, results: newResults, status: hasConflict ? 'conflict' : 'clean' }; })); }; // 统计数据 const conflictRowsCount = rows.filter(r => r.status === 'conflict').length; // --- 渲染辅助 --- const renderSteps = () => (
{[ { id: 'upload', label: '1. 选列与体检' }, { id: 'schema', label: '2. 智能模版' }, { id: 'processing', label: '3. 双盲提取' }, { id: 'verify', label: '4. 交叉验证' }, { id: 'result', label: '5. 完成' } ].map((step, idx, arr) => (
s.id === currentStep) > idx || currentStep === 'result') ? 'bg-emerald-500 text-white' : 'bg-slate-200 text-slate-500'}`}> {(arr.findIndex(s => s.id === currentStep) > idx || currentStep === 'result') && step.id !== currentStep ? : idx + 1}
{step.label}
{idx < arr.length - 1 && (
s.id === currentStep) > idx ? 'bg-emerald-500' : 'bg-slate-200'}`}>
)}
))}
); return (
{/* Header */}

病历结构化机器人 V4

双模型交叉验证 DeepSeek-V3 & Qwen-Max
{/* 状态指示器 */} {currentStep === 'verify' && (
DeepSeek
Qwen
)}
{/* Main Content */}
{renderSteps()}
{/* Step 1: Upload */} {currentStep === 'upload' && (
{fileName}
12.5 MB • 1,200 行
{selectedColumn && (
{isChecking ? : columnHealth === 'good' ? : }

{isChecking ? '正在进行数据体检...' : columnHealth === 'good' ? '健康度优秀,适合提取' : '警告:该列包含大量空值或过短,不适合 AI 处理'}

{!isChecking && columnHealth === 'good' && (
平均字符: 358 空值率: 2% 预计 Token: 450k
)}
)}
)} {/* Step 2: Schema */} {currentStep === 'schema' && (

提取字段列表

{fields.map((field) => (
))}
System Prompt Preview

// Role Definition

You are an expert in {diseaseType.replace('_', ' ')} pathology.

Extract fields in JSON format:

{'{'}

{fields.map(f => (

"{f.name}": "string", // {f.desc}

))}

{'}'}

)} {/* Step 3: Processing */} {currentStep === 'processing' && (

双盲提取交叉验证中...

{logs.map((log, i) => (
[{new Date().toLocaleTimeString()}] {log}
))}
_
)} {/* Step 4: Verify (全景网格 + 侧边栏) */} {currentStep === 'verify' && (
{/* Toolbar */}
总数据: {rows.length}
{conflictRowsCount > 0 ? (
{conflictRowsCount} 条冲突待裁决
) : (
所有冲突已解决
)}
{/* Data Grid */}
{fields.map(f => ( ))} {rows.map((row, idx) => ( setSelectedRowId(row.id)} > {/* 动态列 */} {fields.map(f => { const cell = row.results[f.name]; // 简单的 Mock 数据映射逻辑 (真实场景中每个字段都有A/B) const data = cell || { A: '-', B: '-', chosen: '-' }; const isConflict = data.A !== data.B && data.chosen === null; const isResolved = data.chosen !== null; if (isConflict) { return ( ); } return ( ); })} ))}
# 原文摘要{f.name}状态
{idx + 1}
{row.text}
{isResolved ? (
{data.chosen}
) : (
{data.A}
)}
{row.status === 'clean' ? ( 通过 ) : ( 待裁决 )}
{/* Drawer (侧边栏) */}
{selectedRowId && (() => { const row = rows.find(r => r.id === selectedRowId); if (!row) return null; return ( <>

病历原文详情

Row ID: {row.id}

{row.fullText}

快速导航

{Object.entries(row.results).map(([k, v]) => ( {k} ))}
); })()}
)} {/* Step 5: Result */} {currentStep === 'result' && (

结构化处理完成

双模型交叉验证已完成。人工裁决修正了 1 条冲突数据。
最终数据集包含 3 条高质量记录。

隐私安全
PII 已脱敏
Token 消耗
~45k Tokens
)}
{/* Footer Navigation (Step 4 隐藏,因为有自己的工具栏) */} {currentStep !== 'processing' && currentStep !== 'result' && currentStep !== 'verify' && (
)}
); }; export default ToolB_AIStructurerV4;