Files
AIclinicalresearch/docs/03-业务模块/DC-数据清洗整理/03-UI设计/工具B_病历结构化机器人_原型设计_V4.tsx
HaHafeng d4d33528c7 feat(dc): Complete Phase 1 - Portal workbench page development
Summary:
- Implement DC module Portal page with 3 tool cards
- Create ToolCard component with decorative background and hover animations
- Implement TaskList component with table layout and progress bars
- Implement AssetLibrary component with tab switching and file cards
- Complete database verification (4 tables confirmed)
- Complete backend API verification (6 endpoints ready)
- Optimize UI to match prototype design (V2.html)

Frontend Components (~715 lines):
- components/ToolCard.tsx - Tool cards with animations
- components/TaskList.tsx - Recent tasks table view
- components/AssetLibrary.tsx - Data asset library with tabs
- hooks/useRecentTasks.ts - Task state management
- hooks/useAssets.ts - Asset state management
- pages/Portal.tsx - Main portal page
- types/portal.ts - TypeScript type definitions

Backend Verification:
- Backend API: 1495 lines code verified
- Database: dc_schema with 4 tables verified
- API endpoints: 6 endpoints tested (templates API works)

Documentation:
- Database verification report
- Backend API test report
- Phase 1 completion summary
- UI optimization report
- Development task checklist
- Development plan for Tool B

Status: Phase 1 completed (100%), ready for browser testing
Next: Phase 2 - Tool B Step 1 and 2 development
2025-12-02 21:53:24 +08:00

640 lines
35 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, {
A: string; // DeepSeek
B: string; // Qwen
chosen: string | null; // 用户采纳的值null表示冲突未解决
}>;
status: 'clean' | 'conflict'; // 行状态
}
// --- 模拟数据 ---
const TEMPLATES: Record<string, Record<string, ExtractionField[]>> = {
'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<Step>('upload');
// --- Step 1: 上传与体检 ---
const [fileName, setFileName] = useState<string>('2023_肺癌病理报告_批量.xlsx');
const [selectedColumn, setSelectedColumn] = useState<string>('');
const [columnHealth, setColumnHealth] = useState<'unknown' | 'good' | 'bad'>('unknown');
const [isChecking, setIsChecking] = useState(false);
// --- Step 2: Schema ---
const [diseaseType, setDiseaseType] = useState<string>('lung_cancer');
const [reportType, setReportType] = useState<string>('pathology');
const [fields, setFields] = useState<ExtractionField[]>([]);
// --- Step 3: 处理 ---
const [progress, setProgress] = useState(0);
const [logs, setLogs] = useState<string[]>([]);
// --- Step 4: 验证 (核心升级) ---
const [rows, setRows] = useState<VerifyRow[]>([]);
const [selectedRowId, setSelectedRowId] = useState<number | null>(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 = () => (
<div className="flex items-center justify-center mb-6 px-4">
{[
{ id: 'upload', label: '1. 选列与体检' },
{ id: 'schema', label: '2. 智能模版' },
{ id: 'processing', label: '3. 双盲提取' },
{ id: 'verify', label: '4. 交叉验证' },
{ id: 'result', label: '5. 完成' }
].map((step, idx, arr) => (
<React.Fragment key={step.id}>
<div className={`flex flex-col items-center z-10 ${currentStep === 'result' && step.id !== 'result' ? 'opacity-50' : ''}`}>
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold mb-2 transition-colors
${currentStep === step.id ? 'bg-purple-600 text-white shadow-lg shadow-purple-200' :
(arr.findIndex(s => 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 ? <CheckCircle2 className="w-5 h-5" /> : idx + 1}
</div>
<span className={`text-xs font-medium ${currentStep === step.id ? 'text-purple-700' : 'text-slate-500'}`}>{step.label}</span>
</div>
{idx < arr.length - 1 && (
<div className={`h-[2px] w-12 -mt-6 mx-2 ${arr.findIndex(s => s.id === currentStep) > idx ? 'bg-emerald-500' : 'bg-slate-200'}`}></div>
)}
</React.Fragment>
))}
</div>
);
return (
<div className="min-h-screen bg-slate-50 font-sans text-slate-800 p-6">
<div className="max-w-7xl mx-auto bg-white rounded-2xl shadow-sm border border-slate-200 min-h-[800px] flex flex-col overflow-hidden">
{/* Header */}
<div className="px-8 py-5 border-b border-slate-100 flex justify-between items-center bg-gradient-to-r from-purple-50 via-white to-white">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center text-purple-600">
<Bot className="w-6 h-6" />
</div>
<div>
<h1 className="text-xl font-bold text-slate-900"> V4</h1>
<div className="flex items-center gap-2 text-xs text-slate-500">
<span className="flex items-center gap-1"><Split className="w-3 h-3" /> </span>
<span></span>
<span>DeepSeek-V3 & Qwen-Max</span>
</div>
</div>
</div>
{/* 状态指示器 */}
{currentStep === 'verify' && (
<div className="flex gap-3">
<div className="flex items-center gap-1.5 px-3 py-1 bg-blue-50 text-blue-700 text-xs rounded-full border border-blue-100 font-medium">
<div className="w-2 h-2 rounded-full bg-blue-500"></div> DeepSeek
</div>
<div className="flex items-center gap-1.5 px-3 py-1 bg-orange-50 text-orange-700 text-xs rounded-full border border-orange-100 font-medium">
<div className="w-2 h-2 rounded-full bg-orange-500"></div> Qwen
</div>
</div>
)}
</div>
{/* Main Content */}
<div className="flex-1 flex flex-col">
<div className="pt-6 pb-2">
{renderSteps()}
</div>
<div className="flex-1 px-8 pb-8 relative overflow-hidden flex flex-col">
{/* Step 1: Upload */}
{currentStep === 'upload' && (
<div className="max-w-3xl mx-auto w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 mt-8">
<div className="flex items-center p-5 border border-slate-200 rounded-xl bg-slate-50">
<FileText className="w-10 h-10 text-slate-400 mr-4" />
<div className="flex-1">
<div className="font-medium text-slate-900 text-lg">{fileName}</div>
<div className="text-sm text-slate-500">12.5 MB 1,200 </div>
</div>
<button className="text-sm text-purple-600 hover:underline font-medium"></button>
</div>
<div className="space-y-4">
<label className="block text-sm font-bold text-slate-700"> (Input Source)</label>
<div className="flex gap-4">
<select
className="flex-1 p-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-purple-500 outline-none transition-shadow"
value={selectedColumn}
onChange={(e) => {
setSelectedColumn(e.target.value);
runHealthCheck(e.target.value);
}}
>
<option value="">-- --</option>
<option value="summary_text"> (Summary_Text)</option>
<option value="pathology_report"> (Pathology)</option>
<option value="patient_id">ID列</option>
</select>
</div>
{selectedColumn && (
<div className={`p-5 rounded-xl border transition-all duration-300 ${isChecking ? 'bg-slate-50' : columnHealth === 'good' ? 'bg-emerald-50 border-emerald-200' : 'bg-red-50 border-red-200'}`}>
<div className="flex items-start gap-3">
{isChecking ? <RefreshCw className="w-6 h-6 text-slate-400 animate-spin" /> : columnHealth === 'good' ? <CheckCircle2 className="w-6 h-6 text-emerald-600" /> : <AlertTriangle className="w-6 h-6 text-red-600" />}
<div className="flex-1">
<h4 className={`text-base font-bold mb-1 ${isChecking ? 'text-slate-600' : columnHealth === 'good' ? 'text-emerald-800' : 'text-red-800'}`}>
{isChecking ? '正在进行数据体检...' : columnHealth === 'good' ? '健康度优秀,适合提取' : '警告:该列包含大量空值或过短,不适合 AI 处理'}
</h4>
{!isChecking && columnHealth === 'good' && (
<div className="text-sm text-slate-600 mt-2 flex gap-6">
<span>: <strong>358</strong></span>
<span>: <strong>2%</strong></span>
<span> Token: <strong className="text-purple-600">450k</strong></span>
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
)}
{/* Step 2: Schema */}
{currentStep === 'schema' && (
<div className="max-w-5xl mx-auto w-full space-y-6 animate-in fade-in slide-in-from-right-4 duration-500 mt-4">
<div className="bg-purple-50 p-6 rounded-xl border border-purple-100 grid grid-cols-2 gap-6">
<div>
<label className="block text-xs font-bold text-purple-900 mb-2 flex items-center gap-1">
<Stethoscope className="w-3 h-3" />
</label>
<select className="w-full p-2.5 bg-white border border-purple-200 rounded-lg text-sm outline-none" value={diseaseType} onChange={(e) => setDiseaseType(e.target.value)}>
<option value="lung_cancer"> (Lung Cancer)</option>
<option value="hypertension"> (Hypertension)</option>
</select>
</div>
<div>
<label className="block text-xs font-bold text-purple-900 mb-2 flex items-center gap-1">
<LayoutTemplate className="w-3 h-3" />
</label>
<select className="w-full p-2.5 bg-white border border-purple-200 rounded-lg text-sm outline-none" value={reportType} onChange={(e) => setReportType(e.target.value)}>
<option value="pathology"> (Pathology)</option>
<option value="admission"> (Admission Note)</option>
</select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-3 bg-white border border-slate-200 rounded-xl p-4 h-[400px] overflow-y-auto">
<div className="flex justify-between items-center mb-2">
<h4 className="font-bold text-slate-700"></h4>
<button className="text-xs text-purple-600 flex items-center gap-1 hover:underline"><Plus className="w-3 h-3" /> </button>
</div>
{fields.map((field) => (
<div key={field.id} className="flex gap-2 items-start group p-2 hover:bg-slate-50 rounded-lg border border-transparent hover:border-slate-200 transition-all">
<div className="flex-1 grid grid-cols-5 gap-2">
<input defaultValue={field.name} className="col-span-2 bg-transparent text-sm font-medium text-slate-900 outline-none" />
<input defaultValue={field.desc} className="col-span-3 bg-transparent text-sm text-slate-500 outline-none" />
</div>
<button className="text-slate-300 hover:text-red-500"><Trash2 className="w-4 h-4" /></button>
</div>
))}
</div>
<div className="bg-slate-900 rounded-xl p-5 font-mono text-xs text-slate-300 shadow-lg flex flex-col h-[400px]">
<div className="flex items-center gap-2 text-slate-500 mb-3 border-b border-slate-700 pb-2">
<Bot className="w-3 h-3" /> System Prompt Preview
</div>
<div className="flex-1 overflow-y-auto text-slate-400 leading-relaxed pr-2">
<p className="text-purple-400 mb-2">// Role Definition</p>
<p>You are an expert in {diseaseType.replace('_', ' ')} pathology.</p>
<p className="mb-2">Extract fields in JSON format:</p>
<p className="text-yellow-500">{'{'}</p>
{fields.map(f => (
<p key={f.id} className="pl-4">
<span className="text-blue-400">"{f.name}"</span>: <span className="text-green-400">"string"</span>, <span className="text-slate-600">// {f.desc}</span>
</p>
))}
<p className="text-yellow-500">{'}'}</p>
</div>
</div>
</div>
</div>
)}
{/* Step 3: Processing */}
{currentStep === 'processing' && (
<div className="flex flex-col items-center justify-center h-full animate-in fade-in duration-500 mt-10">
<div className="relative mb-8">
<div className="w-24 h-24 rounded-full border-4 border-slate-100"></div>
<div className="absolute inset-0 w-24 h-24 rounded-full border-4 border-purple-600 border-t-transparent animate-spin"></div>
<div className="absolute inset-0 flex items-center justify-center gap-1">
<div className="w-3 h-3 rounded-full bg-blue-500 animate-bounce" style={{ animationDelay: '0s' }}></div>
<div className="w-3 h-3 rounded-full bg-orange-500 animate-bounce" style={{ animationDelay: '0.2s' }}></div>
</div>
</div>
<h3 className="text-xl font-bold text-slate-900 mb-2">...</h3>
<div className="w-96 bg-slate-100 rounded-full h-2 overflow-hidden mb-6">
<div className="bg-purple-600 h-full transition-all duration-300 ease-out" style={{ width: `${progress}%` }}></div>
</div>
<div className="w-full max-w-lg bg-slate-50 rounded-lg border border-slate-200 p-4 font-mono text-xs h-40 overflow-y-auto shadow-inner">
{logs.map((log, i) => (
<div key={i} className="mb-1 text-slate-600 flex gap-2">
<span className="text-slate-400">[{new Date().toLocaleTimeString()}]</span>
<span>{log}</span>
</div>
))}
<div className="animate-pulse text-purple-500">_</div>
</div>
</div>
)}
{/* Step 4: Verify (全景网格 + 侧边栏) */}
{currentStep === 'verify' && (
<div className="flex-1 flex flex-col relative h-full animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Toolbar */}
<div className="flex justify-between items-center mb-4">
<div className="flex items-center gap-4 text-sm">
<div className="flex items-center gap-2 bg-slate-100 px-3 py-1.5 rounded-lg">
<span className="text-slate-500">:</span>
<span className="font-bold text-slate-900">{rows.length}</span>
</div>
{conflictRowsCount > 0 ? (
<div className="flex items-center gap-2 bg-orange-50 px-3 py-1.5 rounded-lg text-orange-700 animate-pulse">
<AlertTriangle className="w-4 h-4" />
<span className="font-bold">{conflictRowsCount} </span>
</div>
) : (
<div className="flex items-center gap-2 bg-emerald-50 px-3 py-1.5 rounded-lg text-emerald-700">
<CheckCircle2 className="w-4 h-4" />
<span className="font-bold"></span>
</div>
)}
</div>
<div className="flex gap-3">
<button className="px-4 py-2 bg-white border border-slate-300 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-50 flex items-center gap-2">
<Download className="w-4 h-4" />
</button>
<button
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm font-medium hover:bg-purple-700 flex items-center gap-2 shadow-md shadow-purple-200"
onClick={() => setCurrentStep('result')}
>
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
{/* Data Grid */}
<div className="flex-1 bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden flex relative">
<div className="flex-1 overflow-auto">
<table className="w-full text-left text-sm border-collapse">
<thead className="bg-slate-50 text-slate-500 font-semibold border-b border-slate-200 sticky top-0 z-10">
<tr>
<th className="px-4 py-3 w-16 text-center">#</th>
<th className="px-4 py-3 w-64"></th>
{fields.map(f => (
<th key={f.id} className={`px-4 py-3 ${f.width || 'w-40'}`}>{f.name}</th>
))}
<th className="px-4 py-3 w-24 text-center"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{rows.map((row, idx) => (
<tr
key={row.id}
className={`hover:bg-slate-50 transition-colors cursor-pointer ${selectedRowId === row.id ? 'bg-purple-50/50' : ''}`}
onClick={() => setSelectedRowId(row.id)}
>
<td className="px-4 py-3 text-center text-slate-400">{idx + 1}</td>
<td className="px-4 py-3 group relative">
<div className="flex items-center gap-2">
<FileText className="text-slate-300 shrink-0" size={14} />
<span className="truncate w-48 block text-slate-600" title={row.text}>{row.text}</span>
</div>
</td>
{/* 动态列 */}
{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 (
<td key={f.id} className="px-2 py-2 bg-orange-50/50 border-x border-orange-100 align-top">
<div className="flex flex-col gap-1.5">
<button
className="text-left text-xs px-2 py-1.5 rounded border border-blue-200 bg-white hover:bg-blue-50 hover:border-blue-400 transition-all flex justify-between group"
onClick={(e) => { e.stopPropagation(); handleAdopt(row.id, f.name, data.A); }}
>
<span className="truncate max-w-[100px] text-slate-700" title={data.A}>{data.A}</span>
<span className="text-[10px] text-blue-400 group-hover:text-blue-600">DS</span>
</button>
<button
className="text-left text-xs px-2 py-1.5 rounded border border-orange-200 bg-white hover:bg-orange-50 hover:border-orange-400 transition-all flex justify-between group"
onClick={(e) => { e.stopPropagation(); handleAdopt(row.id, f.name, data.B); }}
>
<span className="truncate max-w-[100px] text-slate-700" title={data.B}>{data.B}</span>
<span className="text-[10px] text-orange-400 group-hover:text-orange-600">QW</span>
</button>
</div>
</td>
);
}
return (
<td key={f.id} className="px-4 py-3 align-top">
{isResolved ? (
<div className="flex items-center justify-between group">
<span className="text-blue-700 font-medium">{data.chosen}</span>
<button
className="opacity-0 group-hover:opacity-100 text-slate-400 hover:text-blue-600"
title="重置"
onClick={(e) => { e.stopPropagation(); handleAdopt(row.id, f.name, null); }}
>
<RotateCcw size={12} />
</button>
</div>
) : (
<div className="flex items-center gap-1.5 text-slate-600">
<Check size={12} className="text-emerald-400" />
{data.A}
</div>
)}
</td>
);
})}
<td className="px-4 py-3 text-center align-top">
{row.status === 'clean' ? (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700"></span>
) : (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-700 animate-pulse"></span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Drawer (侧边栏) */}
<div
className={`absolute right-0 top-0 bottom-0 w-96 bg-white border-l border-slate-200 shadow-xl transform transition-transform duration-300 z-20 flex flex-col ${selectedRowId ? 'translate-x-0' : 'translate-x-full'}`}
>
{selectedRowId && (() => {
const row = rows.find(r => r.id === selectedRowId);
if (!row) return null;
return (
<>
<div className="p-4 border-b border-slate-100 flex justify-between items-center bg-slate-50">
<div>
<h3 className="font-bold text-slate-800"></h3>
<p className="text-xs text-slate-500">Row ID: {row.id}</p>
</div>
<button onClick={() => setSelectedRowId(null)} className="text-slate-400 hover:text-slate-600"><X size={20} /></button>
</div>
<div className="flex-1 p-5 overflow-y-auto bg-white">
<p className="text-sm leading-7 text-slate-700 whitespace-pre-wrap font-medium font-serif">
{row.fullText}
</p>
</div>
<div className="p-4 bg-slate-50 border-t border-slate-200">
<h4 className="text-xs font-bold text-slate-500 uppercase mb-2"></h4>
<div className="flex flex-wrap gap-2">
{Object.entries(row.results).map(([k, v]) => (
<span key={k} className={`text-xs px-2 py-1 rounded border ${v.chosen === null ? 'bg-orange-50 text-orange-700 border-orange-200' : 'bg-white text-slate-600 border-slate-200'}`}>
{k}
</span>
))}
</div>
</div>
</>
);
})()}
</div>
</div>
</div>
)}
{/* Step 5: Result */}
{currentStep === 'result' && (
<div className="flex-1 flex flex-col items-center justify-center animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="w-20 h-20 bg-emerald-100 rounded-full flex items-center justify-center mb-6">
<CheckCircle2 className="w-10 h-10 text-emerald-600" />
</div>
<h2 className="text-3xl font-bold text-slate-900 mb-2"></h2>
<p className="text-slate-500 mb-10 text-center max-w-md">
1 <br/>
3
</p>
<div className="grid grid-cols-2 gap-6 w-full max-w-2xl mb-10">
<div className="bg-slate-50 p-6 rounded-xl border border-slate-200 text-center">
<div className="text-sm text-slate-500 mb-1"></div>
<div className="font-bold text-slate-800 flex items-center justify-center gap-2">
<ShieldCheck className="w-4 h-4 text-emerald-500" /> PII
</div>
</div>
<div className="bg-slate-50 p-6 rounded-xl border border-slate-200 text-center">
<div className="text-sm text-slate-500 mb-1">Token </div>
<div className="font-bold text-slate-800 flex items-center justify-center gap-2">
<Zap className="w-4 h-4 text-yellow-500" /> ~45k Tokens
</div>
</div>
</div>
<div className="flex gap-4">
<button className="flex items-center gap-2 px-8 py-3 bg-white border border-slate-300 rounded-xl text-slate-700 hover:bg-slate-50 font-medium shadow-sm">
<Download className="w-5 h-5" /> Excel
</button>
<button className="flex items-center gap-2 px-8 py-3 bg-emerald-600 text-white rounded-xl hover:bg-emerald-700 font-medium shadow-md shadow-emerald-200">
<Table2 className="w-5 h-5" />
</button>
</div>
</div>
)}
</div>
{/* Footer Navigation (Step 4 隐藏,因为有自己的工具栏) */}
{currentStep !== 'processing' && currentStep !== 'result' && currentStep !== 'verify' && (
<div className="px-8 py-4 border-t border-slate-100 flex justify-between bg-slate-50">
<button
className={`px-4 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 ${currentStep === 'upload' ? 'invisible' : ''}`}
onClick={() => {
if(currentStep === 'schema') setCurrentStep('upload');
}}
>
</button>
<button
className={`flex items-center gap-2 px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 font-medium shadow-sm transition-transform active:scale-95 ${columnHealth === 'bad' ? 'opacity-50 cursor-not-allowed' : ''}`}
disabled={columnHealth === 'bad'}
onClick={() => {
if(currentStep === 'upload') setCurrentStep('schema');
else if(currentStep === 'schema') setCurrentStep('processing');
}}
>
<ArrowRight className="w-4 h-4" />
</button>
</div>
)}
</div>
</div>
</div>
);
};
export default ToolB_AIStructurerV4;