Files
AIclinicalresearch/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx
HaHafeng e59676342a docs(pkb): Add development records and update system status
Summary:
- Add PKB module development record for 2026-01-07
- Create PKB module status document (00-模块当前状态与开发指南.md)
- Update system status document to v2.7

Documents added:
- docs/03-业务模块/PKB-个人知识库/06-开发记录/2026-01-07_PKB模块前端V3设计实现.md
- docs/03-业务模块/PKB-个人知识库/00-模块当前状态与开发指南.md

Documents updated:
- docs/00-系统总体设计/00-系统当前状态与开发指南.md

PKB module progress: 75% complete
- Frontend Dashboard: 90%
- Frontend Workspace: 85%
- 3 work modes implemented
- Batch processing API pending debug
2026-01-07 10:35:03 +08:00

559 lines
15 KiB
TypeScript
Raw 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.
/**
* 全文复筛 - 详情与复核抽屉组件
*
* 功能:
* 1. Tab1: 双模型AI判断对比12字段
* 2. Tab2: PDF全文预览MVP占位符
* 3. Tab3: 12字段详细证据
* 4. 底部: 人工决策表单
*/
import { useState } from 'react';
import {
Drawer,
Tabs,
Card,
Row,
Col,
Tag,
Form,
Radio,
Input,
Button,
Space,
Divider,
Alert,
Table,
Collapse,
} from 'antd';
import type { TableColumnsType } from 'antd';
import {
CheckCircleOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
FilePdfOutlined,
} from '@ant-design/icons';
import ConclusionTag from './ConclusionTag';
const { TextArea } = Input;
// 12字段结果类型
interface FieldResult {
completeness: 'complete' | 'partial' | 'missing';
extractability: 'extractable' | 'difficult' | 'impossible';
evidence: string;
pageNumber?: number;
}
// 全文复筛结果类型
interface FulltextResult {
resultId: string;
literatureId: string;
literature: {
id: string;
pmid?: string;
title: string;
authors?: string;
journal?: string;
year?: number;
};
modelAResult: {
modelName: string;
fields: Record<string, FieldResult>;
overall: {
conclusion: 'include' | 'exclude';
confidence: number;
reason: string;
};
};
modelBResult: {
modelName: string;
fields: Record<string, FieldResult>;
overall: {
conclusion: 'include' | 'exclude';
confidence: number;
reason: string;
};
};
conflict: {
isConflict: boolean;
severity: 'high' | 'medium' | 'low';
conflictFields: string[];
};
review: {
finalDecision: 'include' | 'exclude' | null;
};
}
interface Props {
visible: boolean;
result: FulltextResult | null;
onClose: () => void;
onSubmitReview: (resultId: string, decision: 'include' | 'exclude', note?: string) => void;
isReviewing: boolean;
}
// 12字段名称映射
const FIELD_NAMES: Record<string, string> = {
field_1: '研究设计',
field_2: '随机化方法',
field_3: '分配隐藏',
field_4: '盲法',
field_5: '研究对象',
field_6: '样本量',
field_7: '干预措施',
field_8: '对照措施',
field_9: '结局指标',
field_10: '结果数据',
field_11: '统计方法',
field_12: '结果完整性',
};
const FulltextDetailDrawer: React.FC<Props> = ({
visible,
result,
onClose,
onSubmitReview,
isReviewing,
}) => {
const [form] = Form.useForm();
const [activeTab, setActiveTab] = useState('comparison');
if (!result) return null;
// 处理提交
const handleSubmit = async () => {
try {
const values = await form.validateFields();
onSubmitReview(result.resultId, values.decision, values.note);
form.resetFields();
} catch (error) {
// 验证失败
}
};
// Tab1: 双模型对比表格
const renderComparisonTab = () => {
const fieldsData = Object.entries(FIELD_NAMES).map(([key, name]) => ({
key,
fieldName: name,
modelA: result.modelAResult.fields[key],
modelB: result.modelBResult.fields[key],
}));
const columns: TableColumnsType<any> = [
{
title: '字段',
dataIndex: 'fieldName',
key: 'fieldName',
width: 120,
},
{
title: `${result.modelAResult.modelName} - 完整性`,
key: 'modelA_completeness',
width: 120,
align: 'center',
render: (_, record) => {
const completeness = record.modelA?.completeness;
const colorMap = {
complete: 'success',
partial: 'warning',
missing: 'error',
};
return completeness ? (
<Tag color={colorMap[completeness as keyof typeof colorMap]}>{completeness}</Tag>
) : (
'-'
);
},
},
{
title: `${result.modelBResult.modelName} - 完整性`,
key: 'modelB_completeness',
width: 120,
align: 'center',
render: (_, record) => {
const completeness = record.modelB?.completeness;
const colorMap = {
complete: 'success',
partial: 'warning',
missing: 'error',
};
return completeness ? (
<Tag color={colorMap[completeness as keyof typeof colorMap]}>{completeness}</Tag>
) : (
'-'
);
},
},
{
title: '冲突',
key: 'conflict',
width: 80,
align: 'center',
render: (_, record) => {
const isConflict =
record.modelA?.completeness !== record.modelB?.completeness;
return isConflict ? (
<ExclamationCircleOutlined style={{ color: '#faad14', fontSize: 18 }} />
) : (
<CheckCircleOutlined style={{ color: '#52c41a', fontSize: 18 }} />
);
},
},
];
return (
<div>
{/* 整体结论对比 */}
<Row gutter={16} className="mb-6">
<Col span={12}>
<Card
size="small"
title={
<div className="flex items-center justify-between">
<Tag color="blue">{result.modelAResult.modelName}</Tag>
<ConclusionTag conclusion={result.modelAResult.overall.conclusion} />
</div>
}
>
<div className="text-sm">
<div className="mb-2">
: {(result.modelAResult.overall.confidence * 100).toFixed(0)}%
</div>
<div className="text-gray-600">{result.modelAResult.overall.reason}</div>
</div>
</Card>
</Col>
<Col span={12}>
<Card
size="small"
title={
<div className="flex items-center justify-between">
<Tag color="purple">{result.modelBResult.modelName}</Tag>
<ConclusionTag conclusion={result.modelBResult.overall.conclusion} />
</div>
}
>
<div className="text-sm">
<div className="mb-2">
: {(result.modelBResult.overall.confidence * 100).toFixed(0)}%
</div>
<div className="text-gray-600">{result.modelBResult.overall.reason}</div>
</div>
</Card>
</Col>
</Row>
{/* 12字段对比表格 */}
<Table
columns={columns}
dataSource={fieldsData}
rowKey="key"
pagination={false}
size="small"
bordered
/>
</div>
);
};
// Tab2: PDF预览MVP占位符
const renderPdfTab = () => {
return (
<div className="text-center py-12">
<FilePdfOutlined style={{ fontSize: 64, color: '#d9d9d9' }} />
<div className="mt-4 text-gray-500">
<p className="text-lg font-semibold">PDF预览功能</p>
<p className="text-sm mt-2"></p>
</div>
<Alert
message="技术债务 #15"
description="PDF预览与高亮功能已列入技术债务清单将在MVP之后的迭代中实现"
type="info"
showIcon
className="mt-6 max-w-md mx-auto"
/>
</div>
);
};
// Tab3: 12字段详细证据
const renderFieldsDetailTab = () => {
const fieldsData = Object.entries(FIELD_NAMES).map(([key, name]) => ({
key,
label: name,
children: (
<Row gutter={16}>
<Col span={12}>
<Card size="small" title={<Tag color="blue">{result.modelAResult.modelName}</Tag>}>
{result.modelAResult.fields[key] ? (
<div className="text-sm space-y-2">
<div>
<span className="font-semibold">: </span>
<Tag
color={
result.modelAResult.fields[key].completeness === 'complete'
? 'success'
: result.modelAResult.fields[key].completeness === 'partial'
? 'warning'
: 'error'
}
>
{result.modelAResult.fields[key].completeness}
</Tag>
</div>
<div>
<span className="font-semibold">: </span>
<Tag>{result.modelAResult.fields[key].extractability}</Tag>
</div>
<div>
<span className="font-semibold">: </span>
<div className="mt-1 p-2 bg-gray-50 rounded text-gray-700">
{result.modelAResult.fields[key].evidence || '未提供证据'}
</div>
</div>
{result.modelAResult.fields[key].pageNumber && (
<div className="text-xs text-gray-500">
: {result.modelAResult.fields[key].pageNumber}
</div>
)}
</div>
) : (
<div className="text-gray-400"></div>
)}
</Card>
</Col>
<Col span={12}>
<Card size="small" title={<Tag color="purple">{result.modelBResult.modelName}</Tag>}>
{result.modelBResult.fields[key] ? (
<div className="text-sm space-y-2">
<div>
<span className="font-semibold">: </span>
<Tag
color={
result.modelBResult.fields[key].completeness === 'complete'
? 'success'
: result.modelBResult.fields[key].completeness === 'partial'
? 'warning'
: 'error'
}
>
{result.modelBResult.fields[key].completeness}
</Tag>
</div>
<div>
<span className="font-semibold">: </span>
<Tag>{result.modelBResult.fields[key].extractability}</Tag>
</div>
<div>
<span className="font-semibold">: </span>
<div className="mt-1 p-2 bg-gray-50 rounded text-gray-700">
{result.modelBResult.fields[key].evidence || '未提供证据'}
</div>
</div>
{result.modelBResult.fields[key].pageNumber && (
<div className="text-xs text-gray-500">
: {result.modelBResult.fields[key].pageNumber}
</div>
)}
</div>
) : (
<div className="text-gray-400"></div>
)}
</Card>
</Col>
</Row>
),
}));
return <Collapse items={fieldsData} />;
};
return (
<Drawer
title={
<div>
<div className="text-lg font-bold"> - </div>
<div className="text-sm text-gray-500 font-normal mt-1">
{result.literature.title}
</div>
</div>
}
placement="right"
width="80%"
onClose={onClose}
open={visible}
footer={
<div className="flex justify-between items-center">
<Button onClick={onClose}></Button>
<Button type="primary" onClick={handleSubmit} loading={isReviewing}>
</Button>
</div>
}
>
{/* 文献元信息 */}
<Card size="small" className="mb-4">
<div className="text-sm space-y-1">
<div>
<span className="font-semibold">PMID:</span> {result.literature.pmid || '-'}
</div>
<div>
<span className="font-semibold">:</span> {result.literature.authors || '-'}
</div>
<div>
<span className="font-semibold">:</span> {result.literature.journal || '-'} (
{result.literature.year || '-'})
</div>
</div>
</Card>
{/* 冲突提示 */}
{result.conflict.isConflict && (
<Alert
message="存在模型判断冲突"
description={`冲突严重程度: ${result.conflict.severity.toUpperCase()} | 冲突字段: ${result.conflict.conflictFields.join(', ')}`}
type="warning"
showIcon
className="mb-4"
/>
)}
{/* Tab内容 */}
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={[
{
key: 'comparison',
label: '双模型对比',
children: renderComparisonTab(),
},
{
key: 'pdf',
label: 'PDF全文预览',
children: renderPdfTab(),
},
{
key: 'fields',
label: '12字段详情',
children: renderFieldsDetailTab(),
},
]}
/>
<Divider />
{/* 人工决策表单 */}
<Card title="人工复核决策" size="small">
<Form form={form} layout="vertical">
<Form.Item
name="decision"
label="最终决策"
rules={[{ required: true, message: '请选择决策' }]}
>
<Radio.Group>
<Space direction="vertical">
<Radio value="include">
<CheckCircleOutlined style={{ color: '#52c41a' }} />
</Radio>
<Radio value="exclude">
<CloseCircleOutlined style={{ color: '#ff4d4f' }} />
</Radio>
</Space>
</Radio.Group>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.decision !== currentValues.decision
}
>
{({ getFieldValue }) =>
getFieldValue('decision') === 'exclude' ? (
<Form.Item
name="exclusionReason"
label="排除原因"
rules={[{ required: true, message: '请输入排除原因' }]}
>
<Input placeholder="例如P不匹配、S不是RCT、12字段不完整等" />
</Form.Item>
) : null
}
</Form.Item>
<Form.Item name="note" label="复核备注(可选)">
<TextArea
rows={3}
placeholder="记录复核理由、特殊情况说明等..."
maxLength={500}
showCount
/>
</Form.Item>
</Form>
</Card>
</Drawer>
);
};
export default FulltextDetailDrawer;