docs(asl): Complete Tool 3 extraction workbench V2.0 development plan (v1.5)

ASL Tool 3 Development Plan:
- Architecture blueprint v1.5 (6 rounds of architecture review, 13 red lines)
- M1/M2/M3 sprint checklists (Skeleton Pipeline / HITL Workbench / Dynamic Template Engine)
- Code patterns cookbook (9 chapters: Fan-out, Prompt engineering, ACL, SSE dual-track, etc.)
- Key patterns: Fan-out with Last Child Wins, Optimistic Locking, teamConcurrency throttling
- PKB ACL integration (anti-corruption layer), MinerU Cache-Aside, NOTIFY/LISTEN cross-pod SSE
- Data consistency snapshot for long-running extraction tasks

Platform capability:
- Add distributed Fan-out task pattern development guide (7 patterns + 10 anti-patterns)
- Add system-level async architecture risk analysis blueprint
- Add PDF table extraction engine design and usage guide (MinerU integration)
- Add table extraction source code (TableExtractionManager + MinerU engine)

Documentation updates:
- Update ASL module status with Tool 3 V2.0 plan readiness
- Update system status document (v6.2) with latest milestones
- Add V2.0 product requirements, prototypes, and data dictionary specs
- Add architecture review documents (4 rounds of review feedback)
- Add test PDF files for extraction validation

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-23 22:49:16 +08:00
parent 8f06d4f929
commit dc6b292308
42 changed files with 16615 additions and 41 deletions

View File

@@ -0,0 +1,127 @@
# **ASL 工具 3全文智能提取数据字典与变量规范 (EBM Expert 版)**
**文档目的:** 为 ASL 工具 3 的底层大模型 (如 DeepSeek-V3) 定义结构化的提取目标 (JSON Schema),确保提取的数据能完美喂给下游的系统综述 (Tool 4\) 和 Meta 分析引擎 (Tool 5)。
**设计原则:** 模块化、按需动态提取、强制 Quote 溯源。
## **🎯 核心逻辑:提取什么取决于下游“要画什么图”**
工具 3 的提取不是盲目的,它的所有字段都严格服务于最终的科研图表。我们将其分为四大核心模块:
### **模块一:通用基础元数据 (Basic Metadata)**
**提取来源:** PDF 首页、标题、摘要、致谢部分。
**下游用途:** 形成文献特征清单、排查同一临床试验的重复发表。
| 变量名 (JSON Key) | 字段含义 | 数据类型 | 提取位置说明 |
| :---- | :---- | :---- | :---- |
| study\_id | 研究标识 (第一作者+年份) | String | 通常在全文头部,例:*Gandhi 2018* |
| nct\_number | 临床试验注册号 | String | 摘要末尾或方法学开头,用于多篇文章去重 |
| study\_design | 研究设计类型 | Enum | 摘要或方法学 (如 RCT, Cohort Study) |
| funding\_source | 资金来源与利益冲突 | String | 文章末尾 Funding / COI 部分 |
### **模块二:基线特征数据 (Baseline Characteristics)**
**提取来源:** 核心来源于文献中的 **Table 1**
**下游用途:** 直接送入【工具 4】由后端矩阵转置后自动拼装成论文的《表 1\. 纳入研究基线特征总表》。
*注:基线数据具有一定的领域特异性,以下为最通用的核心变量。*
| 变量名 (JSON Key) | 字段含义 | 数据类型 | 提取位置说明 |
| :---- | :---- | :---- | :---- |
| treatment\_name | 实验组干预措施 | String | Table 1 表头或方法学,需包含剂量/频次 |
| control\_name | 对照组干预措施 | String | Table 1 表头或方法学 (如 Placebo) |
| n\_treatment | 实验组样本量 | Integer | Table 1 顶部列总数 (N=xxx) |
| n\_control | 对照组样本量 | Integer | Table 1 顶部列总数 (N=xxx) |
| age\_treatment | 实验组年龄 (Mean±SD) | String | Table 1 中的 Age 行 |
| age\_control | 对照组年龄 (Mean±SD) | String | Table 1 中的 Age 行 |
| male\_percent | 男性比例 (%) | String | Table 1 中的 Sex/Gender 行计算或直提 |
### **模块三:方法学与偏倚风险评估 (Risk of Bias \- RoB 2.0)**
**提取来源:** 核心来源于文献的 **Methods (方法学)** 正文段落。
**下游用途:** 送入【工具 4】生成 Cochrane 标准的“偏倚风险红绿灯图”。
大模型需要像方法学专家一样,阅读方法学正文并进行**定性评价** (Low/High/Unclear Risk)
| 变量名 (JSON Key) | 评估维度 (RoB 2.0) | AI 判断逻辑与提取目标 |
| :---- | :---- | :---- |
| rob\_randomization | 随机序列产生 | 寻找 "computer-generated", "random number table" 等词,评估是否为真随机。 |
| rob\_allocation | 分配隐藏 | 寻找 "central web-based", "opaque envelopes" 等词。 |
| rob\_blinding | 盲法实施 | 寻找 "double-blind", "open-label" 以及盲法对象 (患者、研究者、结局评估者)。 |
| rob\_attrition | 失访与数据完整性 | 从 Results 或 Consort 图中提取失访率,寻找 "Intention-to-treat (ITT)" 分析字眼。 |
### **模块四:结局指标数据 (Outcomes) —— ⚠️ 动态提取的核心**
这是最复杂的部分。**根据用户在【工具 5】中想要做的 Meta 分析类型的不同,工具 3 必须动态切换其提取的 JSON Schema。**
提取来源:正文的 **Results** 段落、**Table 2/3** (结局表)、**Kaplan-Meier 曲线下方的文字**。
#### **场景 A生存分析 / 时间-事件分析 (适用于肿瘤、心血管)**
**关注点:** 结局不仅看是否发生,还看“何时”发生。
**提取字典 (送入 Tool 5 的 HR 模板)**
* endpoint\_name: 终点名称 (如 OS, PFS, MACE)
* hr\_value: 风险比 (Hazard Ratio)
* hr\_ci\_lower: 95% 置信区间下限
* hr\_ci\_upper: 95% 置信区间上限
* p\_value: 统计学 P 值
#### **场景 B二分类数据 (适用于感染率、死亡率、有效/无效)**
**关注点:** 绝对的发生人数与总人数。
**提取字典 (送入 Tool 5 的 Dichotomous 模板)**
* event\_treatment: 实验组发生事件的**具体人数** (从正文或表格中抓取)
* total\_treatment: 实验组该指标的**分析总人数** (注意:可能与基线总人数不同,需看是否排除失访)
* event\_control: 对照组发生事件的具体人数
* total\_control: 对照组分析总人数
#### **场景 C连续型数据 (适用于评分量表、血压下降值、住院天数)**
**关注点:** 均值、标准差与样本量。
**提取字典 (送入 Tool 5 的 Continuous 模板)**
* mean\_treatment: 实验组结局指标均值
* sd\_treatment: 实验组结局指标标准差 (SD) *(注:若原文提供 SE 或 95% CI要求 LLM 尝试换算为 SD或原样摘录待人工换算)*
* n\_treatment: 实验组分析人数
* mean\_control: 对照组均值
* sd\_control: 对照组标准差
* n\_control: 对照组分析人数
### **模块五Quote 溯源系统 (Anti-Hallucination)**
这是我们系统的底层信任机制。
上述四大模块中,**每一个提取出的字段(尤其是数字),都必须在 JSON 中强制附带一个成对的 \_quote 字段。**
**示例 (LLM 输出格式)**
{
"hr\_value": 0.63,
"hr\_value\_quote": "The risk of disease progression or death was significantly lower in the intervention group (hazard ratio, 0.63; 95% CI, 0.52 to 0.76; P\<0.001)."
}
**规范要求:**
1. Quote 必须是 PDF 解析出的 Markdown 中的**原话**,不得修改任何一个单词。
2. 对于表格中提取的数据Quote 必须指出表名与行列坐标,例如:*"Table 2, Row 'Overall Survival', Column 'Hazard Ratio'."*
## **📊 最终输出报告 (Output)**
当【工具 3】完成批处理后它的输出不是一篇长篇大论的文章而是**两项高度结构化的科研资产**
1. **供人查阅的 Excel 数据宽表 (Data Extraction Matrix)**
*每一篇文献Study
* 列:上述所有提取的变量。
* 相邻列:每一个变量紧跟一列对应的 Quote 原文。
* *这张表医生可以直接带走,作为发顶刊时必备的 Supplementary Appendix。*
2. **供系统流转的 JSON Payload**
* 系统在后台将这些结构化数据自动推送至【工具 4】画图推送至【工具 5】执行 R 语言计算。

View File

@@ -0,0 +1,74 @@
# **ASL 工具 3全文智能提取“模板化”管理规范**
**文档目的:** 定义工具 3智能提取工作台的模板引擎机制明确【系统通用字段】与【用户自定义字段】的边界与交互逻辑指导底层 Prompt 的动态拼接与前端表单的渲染。
**适用场景:** 应对不同医学专科、不同研究类型RCT vs 队列研究)的碎片化、个性化数据提取需求。
## **一、 为什么要引入“模板化”机制?**
在循证医学实战中,固定的表单是反直觉的。
* **复用性需求:** 基本信息作者、年份、标准方法学评价RoB 2.0)在任何研究中都是通用的,不该让用户每次都重新配置。
* **特异性需求:** 不同的疾病模型关注的基线特征是否合并糖尿病、肿瘤分期和特定的不良反应3级以上腹泻发生率千差万别必须由研究者自己定义。
**核心解决方案:** 打造一个 **“系统级基座模板 \+ 项目级自定义插槽”** 的模板管理引擎。
## **二、 模板分类与内置字典 (The Template Library)**
系统应当在数据库中预置几套经典的“通用模板Universal Templates”。这些模板由平台的方法学专家维护**用户不可直接篡改其底层逻辑,但可以将其选为基础并“克隆”到自己的项目中。**
### **1\. 系统内置通用模板库 (Built-in Universal Templates)**
哪些东西是通用的?**凡是国际循证医学规范(如 Cochrane 手册)中明确规定了标准结构的,就是通用的。**
* **📘 模板 A标准 RCT 提取与质量评价模板 (最常用)**
* **通用基线:** 实验组/对照组名称、样本量 (N)、平均年龄、性别比例。
* **通用方法学 (RoB 2.0)** 随机序列产生、分配隐藏、盲法、结局数据完整性、选择性报告。
* **通用结局池:** 标准的 HR/CI (生存分析)、Events/Total (二分类)。
* **📙 模板 B观察性研究 (队列/病例对照) 提取模板**
* **通用基线:** 暴露组/非暴露组名称、随访人年数 (Person-years)、基线匹配/调整方法 (如 PSM 倾向性评分匹配)。
* **通用方法学 (NOS 量表)** 队列选择、组间可比性、结局评估。
* **通用结局池:** RR (相对危险度)、OR (比值比)。
* **📗 模板 C纯方法学质控模板 (快速模式)**
* **用途:** 仅提取 RoB/NOS 偏倚风险打分,不提取具体临床数据。
## **三、 用户自定义与“魔改”机制 (Customization)**
在通用的基础上,用户可以基于具体的科研问题,在自己的 Project 内部进行**自定义扩展 (Custom Fields)**。
### **1\. 哪些应该交由用户自定义?(个性化插槽)**
* **个性化基线特征 (Specific Baseline Traits)**
* *肿瘤学场景:* 增加 EGFR突变阳性率、既往接受过靶向治疗的比例。
* *心血管场景:* 增加 基线收缩压均值 (mmHg)、吸烟史比例。
* **个性化结局指标 (Specific Outcomes & Timepoints)**
* 特定的随访时间点:如 术后 30 天死亡率、1 年无进展生存率 (1-y PFS)。
* 特定的不良反应 (AEs):如 重度出血事件发生数、因不良反应停药的人数。
* **个性化的纳入排除二次校验 (Inclusion Check)**
* 增加一个自定义 AI 判断字段:该研究中包含的亚洲人比例是否大于 50%(是/否)。
### **2\. 用户交互与表单组装逻辑 (The "Clone & Edit" Workflow)**
为了平衡系统的稳定性和用户的自由度我们在前端UI和后端Prompt采用以下机制
1. **模板选择 (Select)** 医生创建一个 ASL 提取项目时,系统提示:“请选择一个基础提取模板”。医生选择了 \[标准 RCT 提取模板\]。
2. **克隆与配置 (Clone & Edit)**
* 系统将该通用模板克隆为该项目的\*\*“项目专属模板”\*\*。
* 前端展示一个类似于“表单设计器 (Form Builder)”的界面。
* 医生看到系统已经内置了“年龄”、“性别”、“分配隐藏”等只读字段。
* 医生点击 **“+ 添加自定义提取项”**。
3. **定义自定义字段 (Define Field)**
* 医生输入字段名:糖尿病史比例
* 医生选择数据类型:百分比 (%) 或 具体人数 (N)
* 医生输入给 AI 的提取说明Prompt 提示):*“请提取基线表中,患有 Type 2 Diabetes 的患者比例或人数”*。
4. **底层 Prompt 动态组装 (Dynamic Prompting)**
* 后端在调用 DeepSeek-V3 提取这篇文献时,会将【通用模板的 JSON Schema】和【用户自定义的 JSON Schema】**合并**。
* AI 引擎在阅读 PDF 时,不仅会去寻找常规的年龄性别,还会专门去寻找用户刚才定义的“糖尿病史比例”,并一并返回。
## **四、 核心价值:沉淀“专科级”模板资产**
这种“继承+魔改”的设计,不仅解决了工具 3 的灵活性问题,还能为平台带来巨大的商业/学术沉淀价值:
当一个心内科顶尖专家在您的系统上,基于通用模板,精雕细琢配置出了一套专门用于提取\*\*“SGLT2抑制剂治疗心衰”\*\*的完美模板(包含了各种特异性的心脏指标)后,**系统可以允许他将这个项目级模板“公开并发布”为【心内科专科通用模板】。**
长此以往,您的 ASL 系统将沉淀出极具价值的\*\*“各临床专科结构化提取字典库”\*\*,彻底建立学术生态护城河。

View File

@@ -0,0 +1,122 @@
# **ASL 自动化证据合成工具运行机制详解 (Tool 4 & Tool 5\)**
**文档目的:** 详细解答工具 4系统综述图表生成器与工具 5Meta 分析量化引擎)的输入、输出、操作流及底层技术原理。
**业务阶段:** 定性合成 (Qualitative Synthesis) 与 定量合成 (Quantitative Synthesis)。
**解耦声明:** 两个工具均支持“关联项目流水线自动输入”与“下载模板独立本地上传”双通道模式。
## **📊 工具 4系统综述 (SR) 图表生成器**
**一句话理解:** 帮医生把繁琐的筛选流水账和基线数据,全自动画成符合国际期刊发表规范的 PRISMA 图和横向比对表。
### **1\. 数据的【输入】是什么?**
本工具支持两种输入模式:
* **自动串联输入(主流):** 读取当前项目中,工具 1检索总数、工具 2初筛排除数及原因、工具 3全文复筛排除数、提取到的基线 JSON 数据)。
* **独立上传输入(解耦):** 医生下载系统提供的标准 Excel 模板,填入自己在其他地方做好的数据,上传生成图表。
#### **📥 核心补充:独立模式数据源模板 (Excel Template) 详解**
当用户选择“独立使用”工具 4 时,系统提供下载的文件为 SR\_Charting\_Template.xlsx。该文件包含两个工作表Sheet分别对应两种图表的数据输入
**Sheet 1: PRISMA\_Data (用于生成流程图)**
这是一个极简的键值对表格,用户只需填写各个筛选阶段的数字账本。
| 阶段节点 (Stage) | 数值 (Count) | 排除原因明细 (Exclusion\_Reasons \- 可选) |
| :---- | :---- | :---- |
| Total\_Identified (检索总数) | 1245 | |
| Duplicates\_Removed (去重排除) | 345 | |
| Title\_Abstract\_Excluded (初筛排除) | 700 | 非RCT研究:400, 人群不符:200, 综述:100 |
| FullText\_Excluded (全文排除) | 80 | 缺乏结局数据:50, 无法获取PDF:30 |
| Final\_Included (最终纳入) | 120 | |
**Sheet 2: Baseline\_Data (用于生成基线特征 Table 1\)**
这是一个典型的科研特征矩阵表。每一行代表一篇被纳入的文献。
| Study\_ID (研究标识) | Intervention\_Name (实验组名称) | Control\_Name (对照组名称) | Intervention\_N (实验组人数) | Control\_N (对照组人数) | Age\_Mean\_SD (平均年龄) | Male\_Percent (男性比例) |
| :---- | :---- | :---- | :---- | :---- | :---- | :---- |
| Gandhi 2018 | Pembrolizumab \+ Chemo | Placebo \+ Chemo | 410 | 206 | 62.5 ± 8.1 | 60.5% |
| Hellmann 2019 | Nivolumab \+ Ipilimumab | Chemotherapy | 583 | 583 | 64.0 ± 9.2 | 68.0% |
*(注:系统在读取这些 Excel 后,会在前端通过 xlsx.js 解析为底层 JSON 传递给渲染器。)*
### **2\. 用户【操作流程】**
1. **选择图表类型:** 在左侧选择要画什么图。
2. **选择数据源:** 勾选“关联当前项目”或点击“下载模板并上传本地 Excel”。
3. **一键生成:** 点击“渲染生成图表”按钮。
4. **预览与导出:** 右侧大屏渲染出矢量图,用户可一键导出 SVG/PNG 或复制表格。
### **3\. 底层【工作原理】**
* **自动汇总:** Node.js 后端去数据库做 COUNT() 统计出各个阶段的留存数量。
* **前端渲染:** 拿到统计数字或用户上传的数字后,前端利用 Echarts 或 Mermaid.js 动态填入预设的漏斗图拓扑结构中,渲染出矢量图。
* **矩阵转置:** 将提取到的纵向数据(每篇文献的各个特征),转置拼装为标准的医学横向对比 Markdown 表格。
## **📈 工具 5Meta 分析量化引擎**
**一句话理解:** 这是一个内置了“医学统计学专家”的超级计算器。它把多个独立研究的数据融合在一起,得出一个终极的“合并疗效结论”。
### **1\. 数据的【输入】是什么?**
* **统计学配置(左上角):** 配置结局指标类型和数学模型(随机/固定效应)。
* **核心矩阵数据(左下角网格):** 可以是一键从工具 3 导入的,也可以是用户独立上传的。
#### **📥 核心补充:独立模式数据源模板 (Excel Template) 详解**
Meta 分析对数据的要求极其严格。根据临床研究终点的不同,工具 5 提供了 **3 种不同分类的数据模板**(打包为 Meta\_Analysis\_Templates.zip。用户必须根据自己的结局指标类型选择对应的模板填写
**分类 1生存分析预计算型模板 (Template\_Hazard\_Ratio.xlsx)**
* **适用场景:** 肿瘤、心血管等带时间跨度的生存数据(如 OS, PFS文献直接给出了算好的 HR 值。
| Study\_ID (研究标识) | HR\_Value (风险比) | Lower\_CI (95%置信区间下限) | Upper\_CI (95%置信区间上限) |
| :---- | :---- | :---- | :---- |
| Gandhi 2018 | 0.49 | 0.38 | 0.64 |
| Hellmann 2019 | 0.79 | 0.65 | 0.96 |
**分类 2二分类原始数据型模板 (Template\_Dichotomous.xlsx)**
* **适用场景:** 计算发生率的指标(如:感染/未感染,死亡/存活。R 引擎会自动根据这些原始人数计算出 OR (比值比) 或 RR (相对危险度)。
| Study\_ID (研究标识) | Events\_Intervention (实验组事件数) | Total\_Intervention (实验组总数) | Events\_Control (对照组事件数) | Total\_Control (对照组总数) |
| :---- | :---- | :---- | :---- | :---- |
| Study A 2021 | 45 | 150 | 60 | 148 |
| Study B 2022 | 30 | 100 | 40 | 100 |
**分类 3连续型原始数据型模板 (Template\_Continuous.xlsx)**
* **适用场景:** 有均值和标准差的连续数值指标(如:血压下降了多少 mmHg体重减轻了多少 kg
| Study\_ID | Mean\_Intervention (实验组均值) | SD\_Intervention (实验组标准差) | N\_Intervention (实验组人数) | Mean\_Control (对照组均值) | SD\_Control (对照组标准差) | N\_Control (对照组人数) |
| :---- | :---- | :---- | :---- | :---- | :---- | :---- |
| Trial 1 | 12.5 | 2.1 | 100 | 8.4 | 1.9 | 100 |
### **2\. 用户【操作流程】**
1. **导入/配置数据:** 点击“继承工具3”或“下载模板并上传本地 Excel”左侧数据可视化网格瞬间填满。
2. **微调修改:** 如果发现某篇文献的数据有问题,直接在网格里手动双击单元格修改。
3. **点击“运行 R 引擎分析”:** 触发核心计算,前端将网格里的数据转为 JSON 发送给后端。
4. **等待加载:** 页面弹出暗色遮罩,调用后台 R Statistical Engine耗时 2-5 秒)。
5. **查看结果大屏:** 右侧展示计算出的合并效应量、P 值、异质性 ![][image1] 指标,并渲染出森林图。
### **3\. 底层【工作原理】(硬核技术壁垒)**
这里的原理是**跨语言微服务调用**,彻底打通前端展现与深层统计学:
* **数据打包:** Node.js 后端将左侧网格数据打包成标准 JSON。
* **呼叫 R 语言容器:** 后端将 JSON 发送给我们内网独立部署的 ssa-r-statistics Docker。
* **R 语言黑盒计算:** 在 Docker 内部R 语言调用全球最权威的医学统计包 meta::metagen()。
* **结果回传:** R 语言算出 Pooled Effect合并效应量并画出高清的**森林图 (Forest Plot)**,转为 Base64 编码图片回传给前端直接显示。
### **4\. 【输出】与【报告】交付**
* **定性结果 (工具4)** 动态可交互的 PRISMA 流程图SVG/PNG合并好的基线特征表Table 1
* **定量结果 (工具5)** 合并效应值 (如 HR: 0.63, p=0.01)、异质性检验 (![][image1])、森林图 & 漏斗图原图。
* **终极交付:** 这两者的结果最终通过大模型合成,输出一份完整的\*\*《自动化循证证据合成报告》(Word 格式)\*\*,可直接作为医生撰写 SCI 论文 Method 和 Result 部分的核心素材。
[image1]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAYCAYAAAD3Va0xAAAA9ElEQVR4XmNgGAUgIC8vvxyI/0OxIbo8UUBBQaFBRUVFFMRWUlLiBxmmrKwshq6OIABq3APEP5H4/+Tk5MqR1cABUGIHED9Ccv4XEB/kAnS1UHlLdHEUADMIXRwGgHLFQAtuoYujACkpKRGoQQfQ5UBAUVERKCV/Gl0cAwADNR1kkKysrB+6HDSQF8P4QHYRsjwKAEpeBRkkKirKgyxubGzMChS/APRSCBTnArExshoUgCt8QF6FycEwumVwQCh8iAb4wockII8jfEgGML+ji5MEkMLnMLocSQBoQA7UoGh0OaIAKKnDvISMpaWlhdHVjoJBAgCqjk8Vrk2liAAAAABJRU5ErkJggg==>

View File

@@ -0,0 +1,152 @@
# **产品需求文档 (PRD)ASL \- 智能文献全景工具箱与证据合成 MVP**
**文档版本:** v5.0 (全景工具箱与提取模板引擎增强版)
**产品归属:** AI Clinical \- ASL (智能文献系统)
**目标受众:** 研发团队(前端/后端/算法/数据、测试团队、UI/UX 设计师
**核心战略:** 构建“松耦合、可插拔的 ASL 循证医学工具箱Toolkit”。支持工具的独立使用与无缝串联。引入**动态提取模板引擎**,适应不同专科的个性化数据榨取需求。
## **一、 产品开发背景与目标 (Background & Goals)**
### **1\. 业务背景**
在过往的系统设计中,我们习惯于规划一条从“文献检索 \-\> 初筛 \-\> 复筛 \-\> 提取 \-\> 统计分析”的超长单向流水线。
然而真实的科研场景中,用户的需求往往是碎片化的。如果系统强迫用户走完漫长的前置流程,或者下游模块(如 Meta 分析)只能硬性依赖上游模块的数据传入,会极大地限制产品的受众群体。此外,不同医学专科(如肿瘤 vs 心血管)对提取变量的要求千差万别,写死提取表单将导致系统缺乏生命力。
### **2\. 产品目标 (Goals)**
打破长链路的僵化限制,将 ASL 升级为一个真正的\*\*“模块化循证工具箱 (Modular Evidence Synthesis Toolkit)”\*\*。
* **业务目标 1解耦** 提供检索、初筛、提取、SR图表、Meta分析等独立工具。每一个下游工具必须提供独立的“标准数据模板下载”和“文件上传”入口确保 100% 可单点使用。
* **业务目标 2灵活** 在核心的【工具 3提取工作台】引入“系统通用模板 \+ 用户自定义插槽”机制,满足个性化医学信息提取。
* **研发目标MVP** 明确各模块的 API 契约JSON Schema前后端解耦开发实现“分块开发、分块测试、分块上线”。
## **二、 ASL 工具箱全景版图 (The Toolkit Landscape)**
整个 ASL 模块被正式划分为以下独立且可串联的通用工具组件:
1.**工具 1智能文献检索 (Deep Research)** \- *\[已开发完成\]*
2.**工具 2标题摘要初筛 (Title/Abstract Screening)** \- *\[已开发完成\]*
3. 🚧 **工具 3全文复筛与智能提取工作台** \- *\[引入动态模板引擎,前端采用 MVP 轻量级 UI 待开发\]*
4.**工具 4系统综述 (SR) 图表生成器** \- *\[待开发,新增独立文件上传\]*
5.**工具 5Meta 分析量化引擎** \- *\[待开发,新增独立文件上传\]*
## **三、 核心用户旅程 (User Journey \- 灵活场景)**
系统不再强制单一路径,而是提供多种灵活的切入场景:
### **场景 A全生命周期串联The Pipeline**
医生从【工具 1】生成指令并获取 100 篇文献 \-\> 流入【工具 2】初筛 \-\> 流入【工具 3】配置提取模板并进行复筛提取 \-\> 数据一键内部流转至【工具 4】和【工具 5】最终同屏输出完整的 PRISMA 流程图、基线表和 Meta 森林图。
### **场景 B作为纯粹的图表生成器 (Standalone SR Charting)**
1. 医生直接打开【工具 4SR图表生成器】。
2. 医生点击\*\*“下载 PRISMA 与基线表标准模板 (Excel)”\*\*。
3. 医生在本地把自己的数字填入 Excel 后,点击\*\*“上传本地数据源”\*\*。
4. 系统瞬间渲染出漂亮、符合国际标准的矢量图供其下载。
### **场景 C作为纯粹的 Meta 分析计算器 (Standalone Meta-Analysis)**
1. 医生手里已经有一份自己几年前整理好的 Excel 结局数据。
2. 医生直接打开【工具 5Meta分析量化引擎】。
3. 医生点击\*\*“下载 Meta 数据标准模板 (Excel/CSV)”\*\*,将自己的数据整理贴入。
4. 点击\*\*“上传文件”\*\*左侧网格自动解析填满点击运行R 引擎返回森林图。
## **四、 待开发模块详细功能说明 (Pending Features & Design)**
以下重点阐述处于\*\*🚧开发中**或**⏳未开发**状态的核心工具模块,特别是**真·解耦的数据源输入设计**与**动态模板引擎\*\*。
### **🚧 工具 3全文复筛与智能提取工作台 (Extraction Workbench)**
此工具是连接原始文献与结构化数据的“转换器”。其核心不再是一个写死的表单,而是一个灵活的**模板化提取引擎**。
* **FR 3.1 轻量级列表与抽屉表单 UI (List \+ Drawer MVP)**
* 页面主体是数据表格,点击某篇文献在右侧滑出 Drawer抽屉
* 抽屉内根据用户选择的【提取模板】动态渲染表单结构。
* 顶部提供“在新标签页打开 PDF”的降级查阅按钮。
* **FR 3.2 动态提取模板引擎 (Template Engine) \- \[V5.0 新增核心\]**
* **设计意图:** 通过“系统通用基座 \+ 用户自定义插槽”解决各专科提取需求不同的问题。
* **系统内置通用模板库:** 平台方法学专家预置,用户不可篡改但可克隆使用。
1. 模板 A: 标准 RCT 提取与质量评价 (含基础基线、RoB 2.0 风险评估、标准结局)。
2. 模板 B: 观察性研究提取 (含随访人年、NOS 偏倚量表)。
3. 模板 C: 纯方法学质控快速模式 (仅提 RoB/NOS不提具体数据)。
* **用户自定义与“魔改” (Clone & Edit)**
* 交互逻辑:用户新建提取任务时,选择系统模板并将其“克隆”到本项目下。
* 自定义插槽:用户可点击“添加自定义提取项”,配置字段名(如“糖尿病史比例”)及提示 Prompt。
* 引擎融合:后端自动将“通用 Schema”与“自定义 Schema”合并交给大模型执行定向提取。
* **FR 3.3 结构化提取数据规范 (Data Extraction Dictionary) \- \[V5.0 新增核心\]** 提取目标严格服务于下游的【工具4】与【工具5】。AI 提取必须包含以下四大模块:
* **模块一:基础元数据:** Study\_ID (第一作者+年份)、NCT\_Number、Study\_Design。
* **模块二:基线特征 (供工具4拼表)** 干预/对照组名称、各组总人数 (N)、年龄 (Mean±SD)、性别比例,及用户自定义的疾病特征。
* **模块三:偏倚风险评估 (供工具4画图)** 针对随机序列、分配隐藏、盲法等进行定性评估 (Low/High/Unclear Risk)。
* **模块四:动态结局指标 (供工具5计算)**
* *生存分析 (HR)*:提取 HR\_Value, Lower\_CI, Upper\_CI。
* *二分类数据 (Events)*:提取实验组及对照组各自的 Events 和 Total N。
* *连续型数据 (Continuous)*:提取实验组及对照组各自的 Mean、SD 和 Total N。
* **FR 3.4 强约束 Quote 溯源交互 (Anti-Hallucination)**
* 每一个提取出的核心数值JSON 中必须强制附带成对的 \_quote 字段。
* **规范约束:** Quote 必须是一字不差的原文摘录(不超过 30 个词);若来源是表格,需指明表名和行列坐标。
* **交互呈现:** 在抽屉表单数值输入框下方,用灰色斜体清晰展示其对应的 \_quote 原文。
* **FR 3.5 状态流转与独立交付:**
* 底部提供“核准保存 (Approve)”按钮。只有 Approved 的行才有资格进入下游图表和引擎。
* 列表页提供“导出当前矩阵为标准 Excel 宽表”功能,结束闭环。
### **⏳ 工具 4系统综述 (SR) 图表生成器 (SR Charting Tool)**
**设计意图:** 将繁琐的文献筛选账本和基线数据,全自动画成符合国际期刊发表规范的 PRISMA 图和横向比对表。
* **FR 4.1 核心:双通道数据输入层 (Dual Input Layer)**
* **通道 A项目继承** 勾选“自动关联本项目流水线数据”,后端查表动态聚合。
* **通道 B独立文件上传**
* 提供 **“下载标准 SR 模板 (Excel)”** 按钮(内含 Sheet1: PRISMA流转数字, Sheet2: 基线数据表)。
* 提供 **“拖拽/上传本地 Excel”** 区域。上传后前端将其解析为标准的 JSON 格式送入渲染器。
* **FR 4.2 PRISMA 2020 流程图渲染:** 接收 JSON 数据,利用 Echarts 或 Mermaid.js 实时渲染标准的级联漏斗图,支持导出 SVG/PNG。
* **FR 4.3 基线特征自动拼表 (Table 1)** 将独立上传的或继承的患者特征数据,渲染为标准的学术论文 Table 1横轴干预/对照,纵轴各指标),支持导出 Word。
* **FR 4.4 偏倚风险 (RoB) 汇总图:** 接收工具 3 提取的或用户上传的风险打分渲染标准的红绿灯评价图Traffic Light Plot
### **⏳ 工具 5Meta 分析量化引擎 (Meta-Analysis Engine)**
**设计意图:** 一个内置了 R 语言统计学专家的超级计算器。合并多个独立研究的数据,得出合并疗效结论。
* **FR 5.1 核心:三通道数据输入矩阵 (Tri-Channel Input Matrix)**
* **通道 A项目继承** 一键继承【工具 3】中打上了 Approved 标签的结局指标。
* **通道 B独立文件上传**
* 提供 **“下载各种数据类型模板”** (如 HR生存分析模板、二分类事件模板、连续型均值模板)。
* 允许用户上传 Excel系统自动解析并填满左侧的可视化数据网格Data Grid
* **通道 C手动快捷录入** 左侧数据网格支持类似 Excel 的直接双击输入、修改、新增行。
* **FR 5.2 R Docker 统计引擎通信:** 后端将页面左侧网格内的数据打包为严格的 JSON发送给内网部署的 ssa-r-statistics:1.0.1 容器的 Plumber API指定相应的模型随机/固定效应)。
* **FR 5.3 结果展示大屏:**
* 接收并清晰渲染合并效应量 (Pooled Effect)、95% CI、P 值。
* 醒目展示 I² 异质性统计量。
* 渲染 R 语言返回的高清**森林图 (Forest Plot)** 和 **漏斗图 (Funnel Plot)** Base64 图像,提供一键下载原图功能。
* **FR 5.4 容错降级机制:** 若数据存在问题导致 R 引擎计算失败(如异质性无穷大、输入格式非法),拦截错误并在页面提示,允许用户在左侧网格立刻修改数据并重新运行。
## **五、 数据源模板契约 (Data Template Contracts) \- \[开发重点\]**
为了实现工具 4 和工具 5 的独立使用,必须在系统中内置以下标准 Excel 模板供用户下载:
### **1\. 工具 4 模板SR\_Charting\_Template.xlsx**
* **Sheet 1 (PRISMA\_Data)**:只需填写几个核心数字。
* 字段Total\_Identified (检索总数), Duplicates\_Removed (去重数), Title\_Excluded (初筛排除), FullText\_Excluded (全文排除), Final\_Included (最终纳入)。
* **Sheet 2 (Baseline\_Data)**
* 字段Study\_ID, Intervention\_Name, Control\_Name, Intervention\_N, Control\_N, Age\_Mean\_SD, Male\_Percent 等。
### **2\. 工具 5 模板Meta\_Analysis\_Template.xlsx**
提供多个 Sheet 应对不同数据类型:
* **Sheet 1 (Hazard\_Ratio)**:字段 Study\_ID, HR\_Value, Lower\_CI, Upper\_CI。
* **Sheet 2 (Dichotomous)**:字段 Study\_ID, Events\_Intervention, Total\_Intervention, Events\_Control, Total\_Control。
## **六、 MVP 验收标准 (Acceptance Criteria)**
1. **模板引擎验证 (工具3)**
* 用户能够在标准 RCT 模板的基础上,成功添加一个自定义字段“糖尿病史比例”,系统能通过大模型成功将其从目标文献中抽取出来并附带 Quote 溯源。
2. **真·解耦测试通过 (工具4/5)**
* 用户**不创建项目、不检索文献**,直接打开【工具 5】下载模板后填入自己伪造的 5 篇文献数据,上传文件,点击运行,系统成功画出森林图。
3. **全链路串联贯通 (The End-to-End Test)**
* 使用准备好的 10 篇“PD-1 免疫治疗”高度同质化 RCT 文献,跑通一条完整主线:上传 PDF \-\> 提取 \-\> 列表抽屉复核全点通过 \-\> 一键无缝推送数据至下游 \-\> 成功渲染出森林图与 PRISMA 流程图闭环报告。

View File

@@ -0,0 +1,612 @@
<!DOCTYPE html>
<html lang="zh-CN" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASL全景工具箱与证据合成 V5 - 真独立解耦版</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: { primary: '#1677ff', primaryHover: '#4096ff', bgBase: '#f0f2f5', panelBg: '#ffffff' },
animation: { 'pulse-fast': 'pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite', }
}
}
}
</script>
<style>
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
.drawer-slide-in { transform: translateX(100%); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.drawer-open { transform: translateX(0); }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(15px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in { animation: fadeIn 0.3s ease-out forwards; }
.tab-active { color: #1677ff; border-bottom: 2px solid #1677ff; font-weight: 500; }
.tab-inactive { color: #64748b; border-bottom: 2px solid transparent; }
.tab-inactive:hover { color: #1677ff; }
/* PRISMA 连线 */
.prisma-line { width: 2px; height: 24px; background-color: #cbd5e1; margin: 0 auto; position: relative;}
.prisma-line::after { content: ''; position: absolute; bottom: -4px; left: -4px; border-width: 5px; border-style: solid; border-color: #cbd5e1 transparent transparent transparent;}
.prisma-h-line { height: 2px; width: 30px; background-color: #cbd5e1; position: absolute; top: 50%; right: -30px;}
.prisma-h-line::after { content: ''; position: absolute; right: -8px; top: -4px; border-width: 5px; border-style: solid; border-color: transparent transparent transparent #cbd5e1;}
/* Excel 风格输入框 */
.data-grid-input { width: 100%; height: 100%; border: none; outline: none; background: transparent; padding: 6px 8px; font-family: monospace; font-size: 13px; }
.data-grid-input:focus { background: #e6f4ff; box-shadow: inset 0 0 0 1px #1677ff; }
table.excel-table td { padding: 0; border: 1px solid #e2e8f0; }
table.excel-table th { padding: 8px; border: 1px solid #cbd5e1; background-color: #f8fafc; font-weight: 600; font-size: 13px; color: #475569; }
</style>
</head>
<body class="bg-bgBase text-gray-800 font-sans h-screen flex overflow-hidden">
<!-- ================= 左侧导航栏 ================= -->
<aside class="w-64 bg-slate-900 text-white flex flex-col h-full flex-shrink-0 shadow-xl z-20">
<div class="h-16 flex items-center px-6 border-b border-slate-800">
<i class="fa-solid fa-notes-medical text-blue-400 text-xl mr-3"></i>
<span class="text-lg font-bold tracking-wide">AI Clinical ASL</span>
</div>
<div class="p-4 text-xs font-semibold text-slate-500 uppercase tracking-wider">循证医学工具箱 (Toolkit)</div>
<nav class="flex-1 px-3 space-y-1" id="nav-menu">
<button class="w-full flex items-center px-3 py-2.5 text-slate-500 opacity-60 cursor-not-allowed text-left rounded-lg">
<i class="fa-solid fa-magnifying-glass-chart w-6 text-center"></i>
<span class="ml-2 font-medium">1: 智能文献检索</span>
<i class="fa-solid fa-check ml-auto text-green-700"></i>
</button>
<button class="w-full flex items-center px-3 py-2.5 text-slate-500 opacity-60 cursor-not-allowed text-left rounded-lg">
<i class="fa-solid fa-filter w-6 text-center"></i>
<span class="ml-2 font-medium">2: 标题摘要初筛</span>
<i class="fa-solid fa-check ml-auto text-green-700"></i>
</button>
<div class="my-2 border-t border-slate-800"></div>
<button onclick="switchTool('tool3')" id="nav-tool3" class="w-full flex items-center px-3 py-2.5 bg-blue-600/20 text-blue-400 rounded-lg transition-colors text-left">
<i class="fa-solid fa-file-pdf w-6 text-center"></i>
<span class="ml-2 font-medium">3: 智能提取工作台</span>
</button>
<button onclick="switchTool('tool4')" id="nav-tool4" class="w-full flex items-center px-3 py-2.5 text-slate-300 hover:bg-slate-800 hover:text-white rounded-lg transition-colors text-left">
<i class="fa-solid fa-diagram-project w-6 text-center"></i>
<span class="ml-2 font-medium">4: SR 图表生成器</span>
</button>
<button onclick="switchTool('tool5')" id="nav-tool5" class="w-full flex items-center px-3 py-2.5 text-slate-300 hover:bg-slate-800 hover:text-white rounded-lg transition-colors text-left">
<i class="fa-solid fa-chart-line w-6 text-center"></i>
<span class="ml-2 font-medium">5: Meta 分析引擎</span>
</button>
</nav>
<div class="p-4 border-t border-slate-800">
<div class="text-xs text-slate-500">当前项目: 肺癌综述 (全局模式)</div>
</div>
</aside>
<!-- ================= 右侧工作区 ================= -->
<main class="flex-1 flex flex-col h-full relative">
<!-- 公共 Header -->
<header class="h-16 bg-panelBg shadow-sm flex items-center justify-between px-6 z-10 flex-shrink-0">
<h1 class="text-lg font-semibold text-gray-800" id="header-title">工具 3全文复筛与智能提取工作台</h1>
<div class="flex space-x-3" id="header-actions">
<button class="px-3 py-1.5 bg-white border border-gray-300 rounded text-sm text-gray-600 hover:text-primary transition-colors"><i class="fa-solid fa-file-excel mr-1 text-green-600"></i> 导出数据</button>
</div>
</header>
<!-- 全局 Toast -->
<div id="global-toast" class="fixed top-20 left-1/2 transform -translate-x-1/2 bg-green-100 border border-green-400 text-green-700 px-4 py-2 rounded shadow-lg z-50 flex items-center transition-all duration-300 opacity-0 -translate-y-4 pointer-events-none">
<i class="fa-solid fa-circle-check mr-2"></i>
<span id="toast-msg" class="text-sm font-medium">操作成功</span>
</div>
<div class="flex-1 overflow-y-auto p-6 bg-bgBase min-w-[1000px] flex flex-col">
<!-- ======================= 工具 3: 智能提取工作台 ======================= -->
<div id="tool3" class="tool-section flex-1">
<div class="max-w-6xl mx-auto animate-fade-in">
<div class="bg-blue-50 border border-blue-100 p-3 rounded-lg mb-4 text-sm text-gray-700 flex items-start">
<i class="fa-solid fa-circle-info text-blue-500 mt-0.5 mr-2"></i>
<div>请核对 AI 提取结果。只有标记为 <strong>Approved</strong> 的文献才可进入 SR 和 Meta 分析环节。</div>
</div>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<table class="w-full text-left text-sm text-gray-600">
<thead class="bg-gray-50 text-gray-700 text-xs uppercase border-b border-gray-200">
<tr>
<th class="px-4 py-3 font-semibold">第一作者 / 年份</th>
<th class="px-4 py-3 font-semibold">文献标题</th>
<th class="px-4 py-3 font-semibold w-24">PDF解析</th>
<th class="px-4 py-3 font-semibold w-32">提取状态</th>
<th class="px-4 py-3 font-semibold w-24 text-center">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr class="hover:bg-blue-50/30">
<td class="px-4 py-4 font-medium text-gray-800">Gandhi L (2018)</td>
<td class="px-4 py-4 text-primary hover:underline cursor-pointer" onclick="openDrawer()">Pembrolizumab plus Chemotherapy in Metastatic NonSmall-Cell Lung Cancer</td>
<td class="px-4 py-4"><span class="text-xs bg-green-100 text-green-700 px-2 py-1 rounded">成功</span></td>
<td class="px-4 py-4"><span class="text-xs bg-orange-50 text-orange-600 px-2 py-1 rounded border border-orange-200"><span class="w-1.5 h-1.5 inline-block rounded-full bg-orange-500 mr-1 animate-pulse"></span>待核对</span></td>
<td class="px-4 py-4 text-center"><button class="bg-primary text-white text-xs px-3 py-1.5 rounded hover:bg-primaryHover" onclick="openDrawer()">复核提单</button></td>
</tr>
<tr class="hover:bg-blue-50/30">
<td class="px-4 py-4 font-medium text-gray-800">Hellmann MD (2019)</td>
<td class="px-4 py-4 text-gray-600">Nivolumab plus Ipilimumab in Advanced NonSmall-Cell Lung Cancer</td>
<td class="px-4 py-4"><span class="text-xs bg-green-100 text-green-700 px-2 py-1 rounded">成功</span></td>
<td class="px-4 py-4"><span class="text-xs bg-green-50 text-green-600 px-2 py-1 rounded border border-green-200"><i class="fa-solid fa-check-double mr-1"></i>Approved</span></td>
<td class="px-4 py-4 text-center"><button class="border border-gray-300 text-gray-600 text-xs px-3 py-1.5 rounded">查看</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ======================= 工具 4: SR 图表生成器 (V5 解耦升级) ======================= -->
<div id="tool4" class="tool-section hidden flex-1">
<div class="max-w-6xl mx-auto flex gap-6 animate-fade-in">
<!-- 左侧:操作配置区 -->
<div class="w-1/3 shrink-0 space-y-4">
<div class="bg-white p-5 rounded-lg shadow-sm border border-gray-200">
<h3 class="font-bold text-gray-800 mb-4 border-b pb-2">图表类型</h3>
<div class="space-y-2">
<label class="flex items-center p-3 border border-primary bg-blue-50 rounded cursor-pointer">
<input type="radio" checked class="text-primary h-4 w-4">
<span class="ml-3 font-medium text-gray-800 text-sm">PRISMA 2020 流程图</span>
</label>
</div>
</div>
<!-- 💡 V5 核心升级:双通道数据输入 -->
<div class="bg-white p-5 rounded-lg shadow-sm border border-gray-200">
<h3 class="font-bold text-gray-800 mb-3 border-b pb-2">数据源输入 (Data Source)</h3>
<!-- 选项 A: 内部流转 -->
<label class="flex items-center p-2 rounded text-sm text-gray-700 hover:bg-gray-50 cursor-pointer mb-2 border border-transparent has-[:checked]:border-blue-300 has-[:checked]:bg-blue-50 transition-colors">
<input type="radio" name="sr-input" value="auto" class="mr-3 text-primary" onchange="toggleSRUpload(false)">
<div>
<div class="font-medium">关联当前项目流水线</div>
<div class="text-xs text-gray-500 mt-0.5">从初筛与工具3自动汇总数据</div>
</div>
</label>
<!-- 选项 B: 独立上传 -->
<label class="flex items-center p-2 rounded text-sm text-gray-700 hover:bg-gray-50 cursor-pointer border border-transparent has-[:checked]:border-blue-300 has-[:checked]:bg-blue-50 transition-colors">
<input type="radio" name="sr-input" value="manual" checked class="mr-3 text-primary" onchange="toggleSRUpload(true)">
<div>
<div class="font-medium">独立文件上传 (Standalone)</div>
<div class="text-xs text-gray-500 mt-0.5">无需使用上游工具上传Excel直出图</div>
</div>
</label>
<!-- 上传区域 (仅在选项B时显示) -->
<div id="sr-upload-area" class="mt-4 p-4 border-2 border-dashed border-gray-300 rounded-lg text-center bg-gray-50 transition-all">
<i class="fa-solid fa-cloud-arrow-up text-3xl text-gray-400 mb-2"></i>
<p class="text-xs font-medium text-gray-600 mb-1">将整理好的 Excel 拖拽至此处,或</p>
<button class="text-xs bg-white border border-gray-300 px-3 py-1 rounded shadow-sm hover:border-primary hover:text-primary mb-3">选择文件</button>
<div class="border-t border-gray-200 pt-2 mt-2">
<button class="text-xs text-primary hover:underline flex items-center justify-center w-full" onclick="alert('即将下载: SR_Charting_Template.xlsx')">
<i class="fa-solid fa-file-excel mr-1"></i> 下载 PRISMA 标准模板
</button>
</div>
</div>
<button onclick="generatePRISMA()" class="w-full bg-primary hover:bg-primaryHover text-white py-2.5 rounded-lg text-sm font-medium transition-colors mt-4">
<i class="fa-solid fa-wand-magic-sparkles mr-2"></i> 渲染生成图表
</button>
</div>
</div>
<!-- 右侧:渲染结果区 -->
<div class="flex-1 bg-white p-8 rounded-lg shadow-sm border border-gray-200 min-h-[600px] flex flex-col">
<div class="flex justify-between items-center mb-6 border-b pb-3">
<h3 class="text-lg font-bold text-gray-800">渲染结果 (Preview)</h3>
<button class="text-primary text-sm hover:underline"><i class="fa-solid fa-download mr-1"></i>导出 SVG</button>
</div>
<div id="sr-empty" class="flex-1 flex flex-col items-center justify-center text-gray-400">
<i class="fa-solid fa-image text-5xl mb-4 text-gray-200"></i><p>上传数据后点击左侧生成</p>
</div>
<div id="sr-loading" class="hidden flex-1 flex flex-col items-center justify-center text-primary">
<i class="fa-solid fa-circle-notch fa-spin text-4xl mb-4"></i><p class="font-medium">正在解析 Excel 数据并生成拓扑结构...</p>
</div>
<div id="sr-result-prisma" class="hidden flex-1 flex flex-col items-center pb-10 overflow-x-auto">
<h4 class="text-base font-bold text-gray-800 mb-8">PRISMA 2020 Flow Diagram</h4>
<!-- 节点 1 容器 -->
<div class="flex items-start">
<div class="w-64 border-2 border-slate-300 bg-white rounded-md p-3 text-center text-sm shadow-sm z-10">
<strong class="text-gray-700">Records identified</strong><br>
<span class="text-primary font-bold">(n = 1,245)</span>
</div>
<div class="w-12 border-b-2 border-slate-300 mt-6 relative">
<div class="absolute -right-1 -top-1 w-2 h-2 border-t-2 border-r-2 border-slate-300 transform rotate-45"></div>
</div>
<div class="w-48 border border-red-200 bg-red-50 rounded-md p-3 text-xs shadow-sm text-left">
<strong class="text-red-600">Records removed:</strong><br>Duplicate records (n = 345)
</div>
</div>
<div class="w-64 flex justify-center">
<div class="h-8 border-l-2 border-slate-300 relative">
<div class="absolute -bottom-1 -left-[5px] w-2 h-2 border-b-2 border-r-2 border-slate-300 transform rotate-45"></div>
</div>
</div>
<!-- 节点 2 容器 -->
<div class="flex items-start">
<div class="w-64 border-2 border-slate-300 bg-white rounded-md p-3 text-center text-sm shadow-sm z-10">
<strong class="text-gray-700">Records screened</strong><br>
<span class="text-primary font-bold">(n = 900)</span>
</div>
<div class="w-12 border-b-2 border-slate-300 mt-6 relative">
<div class="absolute -right-1 -top-1 w-2 h-2 border-t-2 border-r-2 border-slate-300 transform rotate-45"></div>
</div>
<div class="w-48 border border-red-200 bg-red-50 rounded-md p-3 text-xs shadow-sm text-left">
<strong class="text-red-600">Records excluded:</strong><br>Title/Abstract (n = 700)
</div>
</div>
<div class="w-64 flex justify-center">
<div class="h-8 border-l-2 border-slate-300 relative">
<div class="absolute -bottom-1 -left-[5px] w-2 h-2 border-b-2 border-r-2 border-slate-300 transform rotate-45"></div>
</div>
</div>
<!-- 节点 3 容器 -->
<div class="flex items-start">
<div class="w-64 border-2 border-slate-300 bg-white rounded-md p-3 text-center text-sm shadow-sm z-10">
<strong class="text-gray-700">Full-text articles assessed</strong><br>
<span class="text-primary font-bold">(n = 200)</span>
</div>
<div class="w-12 border-b-2 border-slate-300 mt-6 relative">
<div class="absolute -right-1 -top-1 w-2 h-2 border-t-2 border-r-2 border-slate-300 transform rotate-45"></div>
</div>
<div class="w-48 border border-red-200 bg-red-50 rounded-md p-3 text-xs shadow-sm text-left">
<strong class="text-red-600">Reports excluded:</strong><br>Wrong outcomes (n = 50)<br>No PDF (n = 30)
</div>
</div>
<div class="w-64 flex justify-center">
<div class="h-8 border-l-2 border-slate-300 relative">
<div class="absolute -bottom-1 -left-[5px] w-2 h-2 border-b-2 border-r-2 border-slate-300 transform rotate-45"></div>
</div>
</div>
<!-- 节点 4 (最终纳入) -->
<div class="w-64 border-2 border-green-500 bg-green-50 rounded-md p-3 text-center text-sm shadow-md z-10 mr-[240px]">
<strong class="text-green-700">Studies included</strong><br>
<span class="text-green-600 font-bold text-lg">(n = 120)</span>
</div>
</div>
</div>
</div>
</div>
<!-- ======================= 工具 5: Meta 分析量化引擎 (V5 解耦升级) ======================= -->
<div id="tool5" class="tool-section hidden flex-1">
<div class="max-w-7xl mx-auto flex flex-col h-full w-full space-y-4 animate-fade-in">
<!-- 顶部:模型配置 -->
<div class="bg-white p-4 rounded-lg shadow-sm border border-gray-200 flex items-end gap-6 shrink-0">
<div class="w-64">
<label class="block text-xs font-semibold text-gray-500 mb-1">结局指标数据类型</label>
<select class="w-full text-sm border-gray-300 rounded py-1.5 px-2 border focus:ring-primary"><option>Hazard Ratio (HR) - 预计算效应量</option></select>
</div>
<div class="w-64">
<label class="block text-xs font-semibold text-gray-500 mb-1">统计学模型</label>
<select class="w-full text-sm border-gray-300 rounded py-1.5 px-2 border focus:ring-primary"><option>Random Effects Model (DerSimonian-Laird)</option></select>
</div>
<button onclick="runMetaEngine()" class="bg-purple-600 text-white px-6 py-1.5 rounded hover:bg-purple-700 shadow-sm flex items-center text-sm font-medium ml-auto">
<i class="fa-solid fa-microchip mr-2"></i> 运行 R 引擎计算
</button>
</div>
<!-- 下方:左侧数据表格 + 右侧森林图 -->
<div class="flex flex-1 gap-4 min-h-[500px]">
<!-- 左侧:数据网格 (Excel Style) -->
<div class="w-[500px] bg-white rounded-lg shadow-sm border border-gray-200 flex flex-col overflow-hidden shrink-0">
<!-- 💡 V5 核心升级:数据网格头部工具栏 -->
<div class="p-3 bg-gray-50 border-b border-gray-200 flex justify-between items-center flex-wrap gap-2">
<span class="text-sm font-semibold text-gray-700">数据输入矩阵 (Matrix)</span>
<div class="flex space-x-2">
<button onclick="alert('准备下载: Meta_Analysis_Template_HR.xlsx')" class="text-xs text-gray-500 hover:text-primary transition-colors" title="下载空白模板">
<i class="fa-solid fa-download"></i> 模板
</button>
<div class="w-px h-4 bg-gray-300 my-auto"></div>
<button onclick="simulateFileUpload()" class="text-xs bg-white border border-gray-300 text-gray-700 px-2 py-1 rounded hover:text-primary hover:border-primary shadow-sm transition-colors">
<i class="fa-solid fa-file-import mr-1"></i>上传 Excel
</button>
<button onclick="importDataFromTool3()" class="text-xs bg-blue-50 border border-blue-200 text-primary px-2 py-1 rounded hover:bg-blue-100 shadow-sm transition-colors">
<i class="fa-solid fa-link mr-1"></i>继承工具3
</button>
</div>
</div>
<div class="flex-1 overflow-auto relative">
<!-- 💡 V5 核心升级:未导入时的多通道遮罩 -->
<div id="meta-data-overlay" class="absolute inset-0 bg-white/95 z-20 flex flex-col items-center justify-center p-6 text-center">
<div class="w-16 h-16 bg-gray-50 rounded-full flex items-center justify-center mb-4 border border-gray-200">
<i class="fa-solid fa-table-cells text-2xl text-gray-400"></i>
</div>
<h3 class="text-sm font-bold text-gray-800 mb-2">数据矩阵为空,请选择输入方式</h3>
<p class="text-xs text-gray-500 mb-6">您可以导入系统内已提取的数据,或者作为独立工具上传本地文件。</p>
<div class="flex gap-3 w-full max-w-[300px]">
<button onclick="importDataFromTool3()" class="flex-1 bg-blue-50 border border-blue-200 text-primary text-xs px-3 py-2.5 rounded shadow-sm hover:bg-blue-100 transition-colors">
<i class="fa-solid fa-link block text-lg mb-1"></i> 继承工具3
</button>
<button onclick="simulateFileUpload()" class="flex-1 bg-white border border-gray-300 text-gray-700 text-xs px-3 py-2.5 rounded shadow-sm hover:border-primary hover:text-primary transition-colors">
<i class="fa-solid fa-cloud-arrow-up block text-lg mb-1"></i> 上传 Excel
</button>
</div>
<button class="text-xs text-gray-400 hover:text-primary mt-6 underline" onclick="alert('即将下载标准数据录入模板')">没有模板?下载各种效应量标准 Excel 模板</button>
</div>
<table class="w-full excel-table text-left" id="meta-data-table">
<thead><tr><th>Study ID</th><th>HR</th><th>Lower CI</th><th>Upper CI</th></tr></thead>
<tbody id="meta-tbody">
<!-- 留空,通过 JS 填充 -->
</tbody>
</table>
</div>
<div class="bg-gray-50 p-2 text-[10px] text-gray-500 border-t border-gray-200 flex justify-between">
<span><i class="fa-solid fa-pen mr-1"></i>支持双击单元格直接修改数据</span>
<span><span id="row-count">0</span></span>
</div>
</div>
<!-- 右侧:森林图展示 -->
<div class="flex-1 bg-white rounded-lg shadow-sm border border-gray-200 relative flex flex-col min-w-[500px]">
<!-- R 引擎加载遮罩 -->
<div id="r-engine-overlay" class="absolute inset-0 bg-slate-900/95 z-20 hidden flex-col items-center justify-center text-white rounded-lg">
<i class="fa-brands fa-r-project text-5xl text-blue-400 mb-3 animate-pulse"></i>
<h3 class="text-lg font-bold">Calling R Statistical Engine</h3>
<p class="text-xs text-slate-400 font-mono mt-2">Packaging JSON Data...</p>
<p class="text-xs text-slate-400 font-mono mt-1">Executing meta::metagen() ...</p>
</div>
<div id="meta-empty" class="absolute inset-0 flex flex-col items-center justify-center text-gray-400 z-10 bg-white rounded-lg">
<i class="fa-solid fa-chart-column text-4xl mb-3 text-gray-200"></i><p class="text-sm">导入数据并运行引擎后,在此生成森林图</p>
</div>
<!-- 渲染结果 -->
<div id="meta-result-view" class="hidden flex-1 flex flex-col p-6">
<div class="flex justify-between items-end mb-4 border-b pb-2">
<div>
<h3 class="text-base font-bold text-gray-800">Forest Plot (Overall Survival)</h3>
<div class="text-xs font-bold text-purple-700 mt-1">Pooled HR: 0.63 [0.52, 0.76]</div>
</div>
<div class="text-right text-xs">
<span class="text-gray-500">Heterogeneity:</span>
<span class="font-mono bg-yellow-100 text-yellow-800 px-1 rounded border border-yellow-200">I²=72%, P=0.01</span>
</div>
</div>
<div class="flex-1 relative pt-6 pb-10 px-4 border border-slate-200 rounded bg-slate-50 overflow-hidden text-sm">
<div class="absolute right-2 top-2 text-[10px] text-gray-400 border px-1">R Plot Area</div>
<!-- 坐标轴 -->
<div class="absolute top-4 bottom-8 w-px bg-gray-400 z-0" style="left: 50%;"></div> <!-- Null line (1.0) -->
<div class="absolute bottom-6 left-8 right-8 h-px bg-gray-600 z-0"></div>
<div class="absolute bottom-1 text-xs text-gray-500" style="left: 25%; transform: translateX(-50%);">0.5</div>
<div class="absolute bottom-1 text-xs text-gray-500" style="left: 50%; transform: translateX(-50%);">1.0</div>
<div class="absolute bottom-1 text-xs text-gray-500" style="left: 75%; transform: translateX(-50%);">1.5</div>
<!-- 数据点行 -->
<div class="relative h-8 w-full flex items-center z-10">
<div class="w-32 text-xs font-medium shrink-0">Gandhi 2018</div>
<div class="absolute h-px bg-slate-800" style="left: 38%; right: 58%;"></div>
<div class="absolute w-3 h-3 bg-blue-600 opacity-80" style="left: 49%; transform: translateX(-50%);"></div>
</div>
<div class="relative h-8 w-full flex items-center z-10">
<div class="w-32 text-xs font-medium shrink-0">Hellmann 2019</div>
<div class="absolute h-px bg-slate-800" style="left: 65%; right: 24%;"></div>
<div class="absolute w-2 h-2 bg-blue-600 opacity-80" style="left: 79%; transform: translateX(-50%);"></div>
</div>
<div class="relative h-8 w-full flex items-center z-10">
<div class="w-32 text-xs font-medium shrink-0">Socinski 2018</div>
<div class="absolute h-px bg-slate-800" style="left: 64%; right: 24%;"></div>
<div class="absolute w-2.5 h-2.5 bg-blue-600 opacity-80" style="left: 78%; transform: translateX(-50%);"></div>
</div>
<div class="relative h-8 w-full flex items-center z-10">
<div class="w-32 text-xs font-medium shrink-0">Reck 2021</div>
<div class="absolute h-px bg-slate-800" style="left: 50%; right: 40%;"></div>
<div class="absolute w-2.5 h-2.5 bg-blue-600 opacity-80" style="left: 59%; transform: translateX(-50%);"></div>
</div>
<!-- 菱形合并结果 -->
<div class="relative h-10 w-full flex items-center z-10 mt-2 border-t border-slate-300 pt-2">
<div class="w-32 text-xs font-bold shrink-0">Random Effects</div>
<div class="absolute h-3 flex items-center justify-center" style="left: 52%; right: 44%;">
<div class="w-3 h-3 bg-purple-600 rotate-45 transform origin-center"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- ================= 侧边抽屉 (工具3使用) ================= -->
<div id="drawer-backdrop" class="fixed inset-0 bg-slate-900/40 z-40 hidden" onclick="closeDrawer()"></div>
<div id="extraction-drawer" class="fixed top-0 right-0 h-full w-[600px] bg-white shadow-2xl z-50 drawer-slide-in flex flex-col">
<!-- 保持不变 -->
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center bg-slate-50">
<div>
<span class="text-xs bg-orange-100 text-orange-600 px-2 py-0.5 rounded border border-orange-200 font-medium">Pending Review</span>
<h2 class="text-base font-bold text-gray-800 mt-1">Pembrolizumab plus Chemotherapy in NSCLC</h2>
</div>
<button class="text-gray-400 hover:text-gray-800 p-2" onclick="closeDrawer()"><i class="fa-solid fa-xmark text-lg"></i></button>
</div>
<div class="flex-1 overflow-y-auto p-6 space-y-6 bg-white">
<div class="border border-gray-200 rounded-lg shadow-sm relative overflow-hidden">
<div class="absolute left-0 top-0 bottom-0 w-1 bg-primary"></div>
<div class="p-4">
<h3 class="text-sm font-bold text-gray-800 mb-3 pl-2">实验组总人数 (Intervention N)</h3>
<input type="text" value="410" class="w-full p-2 border border-blue-300 bg-blue-50 rounded focus:ring-primary outline-none font-bold text-primary font-mono mb-3">
<div class="bg-slate-50 border border-slate-200 p-3 rounded text-sm text-slate-600 italic font-serif">
<span class="text-[10px] bg-slate-200 text-slate-500 px-1 rounded uppercase not-italic mr-2">Quote</span>
"...A total of <span class="bg-yellow-200 font-bold px-1 rounded">410</span> patients were randomly assigned to receive pembrolizumab..."
</div>
</div>
</div>
</div>
<div class="p-4 border-t border-gray-200 bg-gray-50 flex justify-end gap-3 shrink-0">
<button class="px-4 py-2 text-sm text-gray-600 border border-gray-300 rounded bg-white hover:bg-gray-100" onclick="closeDrawer()">取消</button>
<button class="px-4 py-2 text-sm text-white bg-green-600 rounded hover:bg-green-700 shadow flex items-center" onclick="approveAndClose()">
<i class="fa-solid fa-check-double mr-2"></i> 核准保存
</button>
</div>
</div>
<!-- ================= 脚本逻辑 ================= -->
<script>
function showToast(msg) {
const toast = document.getElementById('global-toast');
document.getElementById('toast-msg').innerText = msg;
toast.classList.remove('opacity-0', '-translate-y-4', 'pointer-events-none');
setTimeout(() => toast.classList.add('opacity-0', '-translate-y-4', 'pointer-events-none'), 2500);
}
function switchTool(toolId) {
['tool3', 'tool4', 'tool5'].forEach(id => {
document.getElementById(id).classList.add('hidden');
document.getElementById('nav-' + id).className = 'w-full flex items-center px-3 py-2.5 text-slate-300 hover:bg-slate-800 hover:text-white rounded-lg transition-colors text-left';
});
const activeTool = document.getElementById(toolId);
activeTool.classList.remove('hidden');
const animatedInner = activeTool.querySelector('.animate-fade-in');
if(animatedInner) {
animatedInner.classList.remove('animate-fade-in');
void animatedInner.offsetWidth;
animatedInner.classList.add('animate-fade-in');
}
const headerTitle = document.getElementById('header-title');
if (toolId === 'tool3') {
document.getElementById('nav-tool3').className = 'w-full flex items-center px-3 py-2.5 bg-blue-600/20 text-blue-400 rounded-lg transition-colors text-left';
headerTitle.innerHTML = '工具 3全文复筛与智能提取工作台';
document.getElementById('header-actions').style.display = 'flex';
} else if (toolId === 'tool4') {
document.getElementById('nav-tool4').className = 'w-full flex items-center px-3 py-2.5 bg-blue-600/20 text-blue-400 rounded-lg transition-colors text-left';
headerTitle.innerHTML = '工具 4系统综述 (SR) 图表生成器 <span class="text-xs bg-gray-100 text-gray-600 border px-2 py-1 rounded ml-2 font-normal">支持独立文件模式</span>';
document.getElementById('header-actions').style.display = 'none';
} else if (toolId === 'tool5') {
document.getElementById('nav-tool5').className = 'w-full flex items-center px-3 py-2.5 bg-blue-600/20 text-blue-400 rounded-lg transition-colors text-left';
headerTitle.innerHTML = '工具 5Meta 分析量化引擎 <span class="text-xs bg-gray-100 text-gray-600 border px-2 py-1 rounded ml-2 font-normal">支持独立文件模式</span>';
document.getElementById('header-actions').style.display = 'none';
}
}
// Drawer
const drawer = document.getElementById('extraction-drawer');
const backdrop = document.getElementById('drawer-backdrop');
function openDrawer() { backdrop.classList.remove('hidden'); void drawer.offsetWidth; drawer.classList.add('drawer-open'); }
function closeDrawer() { drawer.classList.remove('drawer-open'); setTimeout(() => backdrop.classList.add('hidden'), 300); }
function approveAndClose() { closeDrawer(); showToast('数据已核准 (Approved)'); }
// Tool 4 Logic
function toggleSRUpload(isManual) {
const uploadArea = document.getElementById('sr-upload-area');
if(isManual) {
uploadArea.classList.remove('hidden');
uploadArea.classList.add('animate-fade-in');
} else {
uploadArea.classList.add('hidden');
}
}
function generatePRISMA() {
// Check which input method is selected
const isManual = document.querySelector('input[name="sr-input"]:checked').value === 'manual';
if(isManual) {
showToast('正在解析上传的 Excel 本地数据...');
} else {
showToast('正在聚合上游工具产生的流水线数据...');
}
document.getElementById('sr-empty').classList.add('hidden');
document.getElementById('sr-result-prisma').classList.add('hidden');
document.getElementById('sr-loading').classList.remove('hidden');
setTimeout(() => {
document.getElementById('sr-loading').classList.add('hidden');
document.getElementById('sr-result-prisma').classList.remove('hidden');
document.getElementById('sr-result-prisma').classList.add('flex');
showToast('PRISMA 流程图渲染成功');
}, 1200);
}
// Tool 5 Logic
const mockTableHTML = `
<tr class="border-b border-gray-200 hover:bg-gray-50"><td class="border-r border-gray-200"><input class="data-grid-input font-medium" value="Gandhi 2018"></td><td class="border-r border-gray-200"><input class="data-grid-input" value="0.49"></td><td class="border-r border-gray-200"><input class="data-grid-input" value="0.38"></td><td><input class="data-grid-input" value="0.64"></td></tr>
<tr class="border-b border-gray-200 hover:bg-gray-50"><td class="border-r border-gray-200"><input class="data-grid-input font-medium" value="Hellmann 2019"></td><td class="border-r border-gray-200"><input class="data-grid-input" value="0.79"></td><td class="border-r border-gray-200"><input class="data-grid-input" value="0.65"></td><td><input class="data-grid-input" value="0.96"></td></tr>
<tr class="border-b border-gray-200 hover:bg-gray-50"><td class="border-r border-gray-200"><input class="data-grid-input font-medium" value="Socinski 2018"></td><td class="border-r border-gray-200"><input class="data-grid-input" value="0.78"></td><td class="border-r border-gray-200"><input class="data-grid-input" value="0.64"></td><td><input class="data-grid-input" value="0.96"></td></tr>
<tr class="border-b border-gray-200 hover:bg-gray-50"><td class="border-r border-gray-200"><input class="data-grid-input font-medium" value="Reck 2021"></td><td class="border-r border-gray-200"><input class="data-grid-input text-red-500 font-bold" value="0.59"></td><td class="border-r border-gray-200"><input class="data-grid-input" value="0.50"></td><td><input class="data-grid-input" value="0.69"></td></tr>
`;
function importDataFromTool3() {
document.getElementById('meta-data-overlay').classList.add('hidden');
document.getElementById('meta-tbody').innerHTML = mockTableHTML;
document.getElementById('row-count').innerText = "4";
showToast('已继承本项目内 4 篇 Approved 的文献数据');
}
function simulateFileUpload() {
// Simulate a file input click
const input = document.createElement('input');
input.type = 'file';
input.accept = '.xlsx, .csv';
input.onchange = e => {
showToast('读取本地 Excel 成功,正在解析矩阵...');
setTimeout(() => {
document.getElementById('meta-data-overlay').classList.add('hidden');
document.getElementById('meta-tbody').innerHTML = mockTableHTML;
document.getElementById('row-count').innerText = "4";
showToast('本地数据导入完成');
}, 800);
}
input.click();
}
function runMetaEngine() {
if (!document.getElementById('meta-data-overlay').classList.contains('hidden')) {
alert('请先通过 [继承] 或 [上传] 导入数据矩阵'); return;
}
document.getElementById('meta-empty').classList.add('hidden');
document.getElementById('meta-result-view').classList.add('hidden');
const overlay = document.getElementById('r-engine-overlay');
overlay.classList.remove('hidden');
overlay.style.display = 'flex';
setTimeout(() => {
overlay.classList.add('hidden');
overlay.style.display = 'none';
document.getElementById('meta-result-view').classList.remove('hidden');
document.getElementById('meta-result-view').classList.add('flex');
showToast('R 引擎计算完成,森林图生成成功');
}, 2000);
}
</script>
</body>
</html>

View File

@@ -0,0 +1,646 @@
<!DOCTYPE html>
<html lang="zh-CN" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>工具 3全文智能提取工作台 V1.1</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: { primary: '#1677ff', primaryHover: '#4096ff', bgBase: '#f0f2f5', panelBg: '#ffffff' },
animation: { 'pulse-fast': 'pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite', }
}
}
}
</script>
<style>
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
.drawer-slide-in { transform: translateX(100%); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.drawer-open { transform: translateX(0); }
@keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
.animate-fade-in { animation: fadeIn 0.4s ease-out forwards; }
/* 模态框动画 */
.modal-fade-in { opacity: 0; transform: scale(0.95); transition: all 0.2s ease-out; }
.modal-open { opacity: 1; transform: scale(1); }
/* 步骤条样式 */
.step-item { position: relative; flex: 1; text-align: center; }
.step-item::after { content: ''; position: absolute; top: 12px; left: 50%; width: 100%; height: 2px; background-color: #e2e8f0; z-index: 0; }
.step-item:last-child::after { display: none; }
.step-circle { width: 26px; height: 26px; border-radius: 50%; background-color: #e2e8f0; color: #64748b; font-size: 12px; font-weight: bold; display: flex; align-items: center; justify-content: center; margin: 0 auto 8px; position: relative; z-index: 10; border: 2px solid #fff; }
.step-item.active .step-circle { background-color: #1677ff; color: #fff; box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.2); }
.step-item.active ~ .step-item::after { background-color: #e2e8f0; }
.step-item.completed .step-circle { background-color: #1677ff; color: #fff; }
.step-item.completed::after { background-color: #1677ff; }
.log-container::-webkit-scrollbar-thumb { background: #475569; }
</style>
</head>
<body class="bg-bgBase text-gray-800 font-sans h-screen flex overflow-hidden">
<!-- 侧边导航 (仅作上下文展示,不可点) -->
<aside class="w-64 bg-slate-900 text-white flex flex-col h-full flex-shrink-0 shadow-xl z-20">
<div class="h-16 flex items-center px-6 border-b border-slate-800">
<i class="fa-solid fa-notes-medical text-blue-400 text-xl mr-3"></i>
<span class="text-lg font-bold tracking-wide">AI Clinical</span>
</div>
<div class="p-4 text-xs font-semibold text-slate-500 uppercase tracking-wider">循证医学工具箱</div>
<nav class="flex-1 px-3 space-y-1">
<div class="w-full flex items-center px-3 py-2.5 text-slate-500 opacity-50"><i class="fa-solid fa-magnifying-glass-chart w-6"></i><span class="ml-2">1: 智能文献检索</span></div>
<div class="w-full flex items-center px-3 py-2.5 text-slate-500 opacity-50"><i class="fa-solid fa-filter w-6"></i><span class="ml-2">2: 标题摘要初筛</span></div>
<div class="my-2 border-t border-slate-800"></div>
<div class="w-full flex items-center px-3 py-2.5 bg-blue-600/20 text-blue-400 rounded-lg"><i class="fa-solid fa-file-pdf w-6"></i><span class="ml-2 font-medium">3: 全文智能提取</span></div>
<div class="w-full flex items-center px-3 py-2.5 text-slate-500 opacity-50"><i class="fa-solid fa-diagram-project w-6"></i><span class="ml-2">4: SR 图表生成器</span></div>
<div class="w-full flex items-center px-3 py-2.5 text-slate-500 opacity-50"><i class="fa-solid fa-chart-line w-6"></i><span class="ml-2">5: Meta 分析引擎</span></div>
</nav>
</aside>
<!-- 右侧工作区 -->
<main class="flex-1 flex flex-col h-full relative">
<header class="h-16 bg-panelBg shadow-sm flex items-center justify-between px-6 z-10 flex-shrink-0">
<h1 class="text-lg font-semibold text-gray-800">工具 3全文复筛与智能提取工作台</h1>
<div id="export-action" class="hidden">
<button class="px-4 py-2 bg-green-600 text-white rounded text-sm hover:bg-green-700 transition-colors shadow flex items-center" onclick="alert('已导出标准科研 Excel 宽表!')">
<i class="fa-solid fa-file-excel mr-2"></i> 下载结构化提取结果 (Excel)
</button>
</div>
</header>
<!-- 全局 Toast -->
<div id="global-toast" class="fixed top-20 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-5 py-2.5 rounded shadow-lg z-50 flex items-center transition-all duration-300 opacity-0 -translate-y-4 pointer-events-none">
<i class="fa-solid fa-circle-info mr-2 text-blue-400"></i><span id="toast-msg" class="text-sm font-medium">提示信息</span>
</div>
<!-- 流程步骤条 -->
<div class="bg-white border-b border-gray-200 px-10 py-4 flex-shrink-0">
<div class="flex justify-between max-w-4xl mx-auto">
<div class="step-item active" id="step-indicator-1">
<div class="step-circle">1</div>
<div class="text-xs font-medium mt-1 text-gray-800">配置模板与上传</div>
</div>
<div class="step-item" id="step-indicator-2">
<div class="step-circle">2</div>
<div class="text-xs font-medium mt-1 text-gray-500">机器解析与提取</div>
</div>
<div class="step-item" id="step-indicator-3">
<div class="step-circle">3</div>
<div class="text-xs font-medium mt-1 text-gray-500">人机比对与核准</div>
</div>
</div>
</div>
<div class="flex-1 overflow-y-auto p-6 bg-bgBase flex flex-col items-center">
<!-- ================= VIEW 1: 配置与上传 ================= -->
<div id="view-setup" class="w-full max-w-6xl animate-fade-in space-y-6">
<div class="grid grid-cols-5 gap-6">
<!-- 左侧:模板配置 (占3列更宽敞以展示模板结构) -->
<div class="col-span-3 bg-white p-6 rounded-xl shadow-sm border border-gray-200 flex flex-col h-full">
<h2 class="text-base font-bold text-gray-800 mb-4 flex items-center justify-between">
<span><i class="fa-solid fa-layer-group text-primary mr-2"></i>步骤 1配置提取模板 (Schema)</span>
<span class="text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded border border-blue-200 font-normal">动态模板引擎</span>
</h2>
<!-- 1. 选择基座 -->
<div class="mb-5">
<label class="block text-xs font-semibold text-gray-500 mb-1.5 uppercase tracking-wider">选择系统通用基座</label>
<select id="base-template-select" class="w-full text-sm border-gray-300 rounded-lg py-2.5 px-3 border bg-gray-50 focus:ring-primary focus:border-primary font-medium text-gray-700 outline-none transition-colors" onchange="changeTemplate()">
<option value="RCT">模板 A: 标准 RCT 提取与质量评价 (推荐)</option>
<option value="Cohort">模板 B: 观察性研究提取与 NOS 评价</option>
</select>
<!-- 动态展示基座包含的字段 -->
<div class="mt-3 p-3 bg-slate-50 border border-slate-200 rounded-md">
<div class="text-xs text-gray-500 mb-2 flex items-center">
<i class="fa-solid fa-lock mr-1.5 text-gray-400"></i> 该基座自动包含以下标准化字段 (不可删改)
</div>
<div id="base-fields-container" class="flex flex-wrap gap-1.5">
<!-- 通过 JS 渲染 -->
</div>
</div>
</div>
<!-- 2. 自定义字段管理 -->
<div class="border-t border-gray-100 pt-5 flex-1 flex flex-col">
<div class="flex justify-between items-center mb-3">
<div>
<label class="block text-xs font-semibold text-gray-700 uppercase tracking-wider">用户自定义插槽 (Custom Fields)</label>
<p class="text-[10px] text-gray-400 mt-0.5">针对您的特定临床问题,添加专属的提取变量</p>
</div>
<button class="text-xs bg-blue-50 text-primary border border-blue-200 hover:bg-blue-100 px-3 py-1.5 rounded transition-colors flex items-center shadow-sm" onclick="openFieldModal()">
<i class="fa-solid fa-plus mr-1.5"></i>添加自定义字段
</button>
</div>
<!-- 自定义字段列表容器 -->
<div id="custom-fields-list" class="space-y-3 flex-1 overflow-y-auto pr-1">
<!-- 通过 JS 动态渲染 -->
</div>
</div>
</div>
<!-- 右侧PDF 上传 (占2列) -->
<div class="col-span-2 bg-white p-6 rounded-xl shadow-sm border border-gray-200 flex flex-col">
<h2 class="text-base font-bold text-gray-800 mb-4 flex items-center"><i class="fa-solid fa-file-pdf text-red-500 mr-2"></i>步骤 2上传文献 (PDF)</h2>
<div class="flex-1 border-2 border-dashed border-gray-300 rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors flex flex-col items-center justify-center p-6 cursor-pointer relative" id="upload-area" onclick="simulateUpload()">
<i class="fa-solid fa-cloud-arrow-up text-4xl text-gray-400 mb-3"></i>
<p class="text-sm font-medium text-gray-700">点击或将 PDF 文件拖拽至此处</p>
<p class="text-xs text-gray-500 mt-1">支持批量上传,单文件最大 50MB</p>
<div class="absolute bottom-4 text-[10px] text-gray-400 bg-white px-2 py-1 rounded shadow-sm border border-gray-200">
💡 提示:上传后将自动应用左侧配置的模板进行提取
</div>
</div>
<!-- 模拟已上传的文件列表 (初始隐藏) -->
<div id="file-list" class="hidden flex-1 flex-col space-y-2 mt-2 overflow-y-auto">
<div class="flex items-center justify-between p-2.5 bg-gray-50 border border-gray-200 rounded-md">
<div class="flex items-center overflow-hidden"><i class="fa-solid fa-file-pdf text-red-500 mr-2 text-lg"></i><span class="text-sm text-gray-700 truncate w-40">Gandhi_2018_NEJM.pdf</span></div>
<span class="text-xs text-green-600"><i class="fa-solid fa-check"></i> 1.2MB</span>
</div>
<div class="flex items-center justify-between p-2.5 bg-gray-50 border border-gray-200 rounded-md">
<div class="flex items-center overflow-hidden"><i class="fa-solid fa-file-pdf text-red-500 mr-2 text-lg"></i><span class="text-sm text-gray-700 truncate w-40">Hellmann_2019_Lancet.pdf</span></div>
<span class="text-xs text-green-600"><i class="fa-solid fa-check"></i> 3.5MB</span>
</div>
<div class="flex items-center justify-between p-2.5 bg-gray-50 border border-gray-200 rounded-md">
<div class="flex items-center overflow-hidden"><i class="fa-solid fa-file-pdf text-red-500 mr-2 text-lg"></i><span class="text-sm text-gray-700 truncate w-40">Socinski_2018_JCO.pdf</span></div>
<span class="text-xs text-green-600"><i class="fa-solid fa-check"></i> 2.1MB</span>
</div>
</div>
</div>
</div>
<div class="mt-6 flex justify-end">
<button id="btn-start" class="bg-primary hover:bg-primaryHover text-white px-8 py-3 rounded-lg font-medium shadow-md transition-all flex items-center opacity-50 cursor-not-allowed" disabled onclick="startProcessing()">
<i class="fa-solid fa-rocket mr-2"></i> 确认模板并开始批量提取
</button>
</div>
</div>
<!-- ================= VIEW 2: 机器处理流 (Processing) ================= -->
<div id="view-processing" class="w-full max-w-4xl hidden mt-10">
<div class="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
<div class="p-8 text-center border-b border-gray-100">
<div class="w-20 h-20 mx-auto relative mb-4">
<div class="absolute inset-0 border-4 border-blue-100 rounded-full"></div>
<div class="absolute inset-0 border-4 border-primary rounded-full border-t-transparent animate-spin"></div>
<div class="absolute inset-0 flex items-center justify-center"><i class="fa-solid fa-robot text-primary text-2xl"></i></div>
</div>
<h2 class="text-xl font-bold text-gray-800">机器静默提取中...</h2>
<p class="text-sm text-gray-500 mt-2">任务已进入 pg-boss 队列,利用 MinerU 与 DeepSeek-V3 联合榨取数据</p>
<div class="w-full max-w-md mx-auto bg-gray-100 rounded-full h-2 mt-6 overflow-hidden">
<div class="bg-primary h-2 rounded-full w-1/3 relative transition-all duration-1000" id="progress-bar"></div>
</div>
</div>
<!-- 模拟终端日志 -->
<div class="bg-slate-900 p-6 h-64 overflow-y-auto log-container font-mono text-sm space-y-2" id="process-logs">
<div class="text-slate-400">>> Initializing extraction pipeline...</div>
</div>
</div>
</div>
<!-- ================= VIEW 3: 提取工作台 (Workbench) ================= -->
<div id="view-workbench" class="w-full max-w-6xl hidden animate-fade-in">
<div class="bg-blue-50 border border-blue-100 p-3 rounded-lg mb-4 text-sm text-gray-700 flex justify-between items-center shadow-sm">
<div class="flex items-center">
<i class="fa-solid fa-circle-check text-blue-500 mr-2 text-lg"></i>
<span>机器提取完毕!共提取 <strong>3</strong> 篇文献。请点击“复核提单”进行人机协同验对,标记为 <strong class="text-green-600">Approved</strong> 的数据才允许导出。</span>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<table class="w-full text-left text-sm text-gray-600">
<thead class="bg-gray-50 text-gray-700 text-xs uppercase border-b border-gray-200">
<tr>
<th class="px-5 py-4 font-semibold">Study ID / 标题</th>
<th class="px-5 py-4 font-semibold w-40">机器解析流</th>
<th class="px-5 py-4 font-semibold w-32">复核状态</th>
<th class="px-5 py-4 font-semibold w-24 text-center">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100" id="workbench-tbody">
<!-- 行 1 -->
<tr class="hover:bg-blue-50/30">
<td class="px-5 py-4">
<div class="font-bold text-gray-800">Gandhi 2018</div>
<div class="text-xs text-primary hover:underline cursor-pointer mt-1 truncate w-96" onclick="openDrawer()">Pembrolizumab plus Chemotherapy in Metastatic NonSmall-Cell Lung Cancer</div>
</td>
<td class="px-5 py-4">
<div class="text-[10px] text-green-600 mb-1"><i class="fa-solid fa-check mr-1"></i>MinerU 表格还原</div>
<div class="text-[10px] text-blue-600"><i class="fa-solid fa-robot mr-1"></i>DeepSeek 榨取</div>
</td>
<td class="px-5 py-4" id="status-1"><span class="text-xs bg-orange-50 text-orange-600 px-2 py-1 rounded border border-orange-200 flex items-center w-max"><span class="w-1.5 h-1.5 rounded-full bg-orange-500 mr-1.5 animate-pulse"></span>待核对</span></td>
<td class="px-5 py-4 text-center"><button class="bg-primary text-white text-xs px-3 py-1.5 rounded hover:bg-primaryHover shadow-sm" onclick="openDrawer()">复核提单</button></td>
</tr>
<!-- 行 2 -->
<tr class="hover:bg-blue-50/30">
<td class="px-5 py-4">
<div class="font-bold text-gray-800">Hellmann 2019</div>
<div class="text-xs text-gray-500 mt-1 truncate w-96">Nivolumab plus Ipilimumab in Advanced NonSmall-Cell Lung Cancer</div>
</td>
<td class="px-5 py-4">
<div class="text-[10px] text-green-600 mb-1"><i class="fa-solid fa-check mr-1"></i>MinerU 表格还原</div>
<div class="text-[10px] text-blue-600"><i class="fa-solid fa-robot mr-1"></i>DeepSeek 榨取</div>
</td>
<td class="px-5 py-4"><span class="text-xs bg-orange-50 text-orange-600 px-2 py-1 rounded border border-orange-200 flex items-center w-max"><span class="w-1.5 h-1.5 rounded-full bg-orange-500 mr-1.5 animate-pulse"></span>待核对</span></td>
<td class="px-5 py-4 text-center"><button class="bg-primary text-white text-xs px-3 py-1.5 rounded hover:bg-primaryHover shadow-sm" onclick="showToast('原型仅演示第一篇的复核')">复核提单</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</main>
<!-- ================= 添加/编辑自定义字段 Modal ================= -->
<div id="field-modal-backdrop" class="fixed inset-0 bg-slate-900/50 z-50 hidden transition-opacity" onclick="closeFieldModal()"></div>
<div id="field-modal" class="fixed inset-0 z-50 hidden items-center justify-center pointer-events-none">
<div class="bg-white rounded-xl shadow-2xl w-[500px] flex flex-col pointer-events-auto modal-fade-in" id="field-modal-content">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center bg-slate-50 rounded-t-xl">
<h2 class="text-base font-bold text-gray-800" id="field-modal-title">添加自定义提取字段</h2>
<button class="text-gray-400 hover:text-gray-800" onclick="closeFieldModal()"><i class="fa-solid fa-xmark text-lg"></i></button>
</div>
<div class="p-6 space-y-4">
<input type="hidden" id="field-id">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">字段名称 <span class="text-red-500">*</span></label>
<input type="text" id="field-name" placeholder="例如:糖尿病史比例 (%)" class="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary focus:border-primary outline-none">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">期望数据类型 <span class="text-red-500">*</span></label>
<select id="field-type" class="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary focus:border-primary outline-none bg-white">
<option value="String">文本 (String)</option>
<option value="Number">具体数值 (Number)</option>
<option value="Percentage">百分比 (Percentage)</option>
<option value="Boolean">是/否 (Boolean)</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">AI 提取指令 (Prompt) <span class="text-red-500">*</span></label>
<p class="text-[10px] text-gray-500 mb-2">告诉大模型应该去哪里找、怎么找这个数据。</p>
<textarea id="field-prompt" rows="3" placeholder="例如:请在基线特征表 (Table 1) 中寻找合并有 Type 2 Diabetes 的患者比例或人数。" class="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary focus:border-primary outline-none resize-none text-sm"></textarea>
</div>
</div>
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50 flex justify-end gap-3 rounded-b-xl">
<button class="px-4 py-2 text-sm text-gray-600 border border-gray-300 rounded bg-white hover:bg-gray-100" onclick="closeFieldModal()">取消</button>
<button class="px-5 py-2 text-sm text-white bg-primary rounded shadow-sm hover:bg-primaryHover" onclick="saveCustomField()">保存字段</button>
</div>
</div>
</div>
<!-- ================= 核心:右侧智能提单抽屉 ================= -->
<div id="drawer-backdrop" class="fixed inset-0 bg-slate-900/50 z-40 hidden transition-opacity" onclick="closeDrawer()"></div>
<div id="extraction-drawer" class="fixed top-0 right-0 h-full w-[700px] bg-white shadow-2xl z-50 drawer-slide-in flex flex-col">
<!-- 保持之前精美的抽屉设计 -->
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center bg-slate-50 shrink-0">
<div class="pr-8">
<div class="flex items-center space-x-2 mb-1.5">
<span id="drawer-status-badge" class="text-xs bg-orange-100 text-orange-600 px-2 py-0.5 rounded border border-orange-200 font-medium"><span class="w-1.5 h-1.5 inline-block rounded-full bg-orange-500 mr-1 animate-pulse"></span>Pending Review (待复核)</span>
<span class="text-[10px] text-gray-400 bg-white border px-1.5 py-0.5 rounded"><i class="fa-solid fa-robot text-blue-500 mr-1"></i>基于所选模板提取</span>
</div>
<h2 class="text-base font-bold text-gray-800 leading-tight">Pembrolizumab plus Chemotherapy in Metastatic NonSmall-Cell Lung Cancer</h2>
</div>
<button class="text-gray-400 hover:text-gray-800 p-2 border border-gray-200 rounded bg-white shadow-sm" onclick="closeDrawer()"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="bg-slate-800 p-2.5 flex justify-between items-center shrink-0 px-6">
<span class="text-xs text-gray-300"><i class="fa-solid fa-shield-halved text-green-400 mr-1.5"></i>已强制开启 Quote 原文溯源护栏,解决 AI 幻觉。</span>
<button class="bg-gray-700 border border-gray-600 text-white hover:bg-gray-600 px-3 py-1 rounded text-xs transition-colors shadow-sm" onclick="alert('利用浏览器原生功能,将在新标签页打开 OSS 中的 PDF 进行比对')">
查看源 PDF <i class="fa-solid fa-arrow-up-right-from-square ml-1 text-[10px]"></i>
</button>
</div>
<div class="flex-1 overflow-y-auto p-6 space-y-6 bg-slate-50">
<!-- 模块 2: 基线特征 (+ 自定义字段) -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 relative overflow-hidden">
<div class="absolute left-0 top-0 bottom-0 w-1 bg-blue-500"></div>
<div class="p-4 pl-5">
<h3 class="text-sm font-bold text-gray-800 mb-4 flex items-center border-b border-gray-100 pb-2"><i class="fa-solid fa-users text-blue-500 mr-2"></i>模块 2基线特征 (Table 1 Baseline)</h3>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="text-xs text-gray-500 mb-1 block">实验组人数 (Intervention_N)</label>
<input type="text" value="410" class="w-full p-2 border border-blue-300 bg-blue-50 text-primary font-bold rounded focus:ring-1 focus:ring-primary outline-none font-mono">
</div>
<div>
<label class="text-xs text-gray-500 mb-1 block">对照组人数 (Control_N)</label>
<input type="text" value="206" class="w-full p-2 border border-gray-300 rounded focus:ring-1 focus:ring-primary outline-none font-mono">
</div>
</div>
<div class="bg-slate-50 border border-slate-200 p-2.5 rounded-md relative mt-1">
<span class="absolute -top-2 left-2 bg-slate-200 text-slate-500 text-[9px] px-1 rounded uppercase font-bold tracking-wider">AI Quote</span>
<p class="text-xs text-slate-600 italic m-0 font-serif border-l-2 border-slate-400 pl-2 mt-1">"...A total of <span class="bg-yellow-200 font-bold px-1 rounded">410</span> patients were assigned to pembrolizumab, and <span class="bg-yellow-200 font-bold px-1 rounded">206</span> to placebo..."</p>
</div>
<!-- 动态展示自定义字段提取结果 -->
<div id="drawer-custom-fields" class="pt-3 border-t border-dashed border-gray-200">
<!-- 示例 -->
<label class="text-xs text-gray-700 font-bold mb-1 flex items-center">
糖尿病史比例 (%)
<span class="ml-2 text-[9px] bg-blue-100 text-blue-600 border border-blue-200 px-1 rounded uppercase tracking-wider"><i class="fa-solid fa-bolt text-yellow-500 mr-0.5"></i> Custom Slot</span>
</label>
<input type="text" value="22.4%" class="w-1/2 p-2 border border-blue-300 bg-blue-50 text-primary font-bold rounded outline-none font-mono">
<div class="bg-slate-50 border border-slate-200 p-2.5 rounded-md relative mt-2">
<span class="absolute -top-2 left-2 bg-slate-200 text-slate-500 text-[9px] px-1 rounded uppercase font-bold tracking-wider">AI Quote</span>
<p class="text-xs text-slate-600 italic m-0 font-serif border-l-2 border-slate-400 pl-2 mt-1">"Table 1: Medical history of Type 2 Diabetes Mellitus - Pembrolizumab group: 92 (<span class="bg-yellow-200 font-bold px-1 rounded">22.4%</span>)."</p>
</div>
</div>
</div>
</div>
</div>
<!-- 模块 4: 结局指标 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 relative overflow-hidden">
<div class="absolute left-0 top-0 bottom-0 w-1 bg-purple-500"></div>
<div class="p-4 pl-5">
<div class="flex justify-between items-center border-b border-gray-100 pb-2 mb-4">
<h3 class="text-sm font-bold text-gray-800 flex items-center"><i class="fa-solid fa-chart-line text-purple-500 mr-2"></i>模块 4结局指标 (Outcomes)</h3>
<div class="bg-purple-50 text-purple-700 text-[10px] px-2 py-0.5 rounded font-medium border border-purple-200">自动检测</div>
</div>
<div class="grid grid-cols-3 gap-3 mb-3">
<div>
<label class="text-xs text-gray-500 block mb-1">HR 值 (OS)</label>
<input type="text" value="0.49" class="w-full p-2 border border-purple-300 rounded font-bold text-purple-700 bg-white outline-none text-center shadow-inner">
</div>
<div>
<label class="text-xs text-gray-500 block mb-1">95% CI 下限</label>
<input type="text" value="0.38" class="w-full p-2 border border-gray-300 rounded font-mono text-center text-sm outline-none">
</div>
<div>
<label class="text-xs text-gray-500 block mb-1">95% CI 上限</label>
<input type="text" value="0.64" class="w-full p-2 border border-gray-300 rounded font-mono text-center text-sm outline-none">
</div>
</div>
</div>
</div>
</div>
<div class="p-4 border-t border-gray-200 bg-white flex justify-between items-center shrink-0">
<span class="text-xs text-gray-400">请确保所有包含 Quote 的数值已核验</span>
<div class="space-x-3">
<button class="px-5 py-2 text-sm font-medium text-gray-600 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onclick="closeDrawer()">取消</button>
<button class="px-5 py-2 text-sm font-medium text-white bg-green-600 rounded-md hover:bg-green-700 shadow flex items-center" onclick="approveAndClose()">
<i class="fa-solid fa-check-double mr-2"></i> 核准保存
</button>
</div>
</div>
</div>
<!-- 脚本交互逻辑 -->
<script>
function showToast(msg) {
const toast = document.getElementById('global-toast');
document.getElementById('toast-msg').innerText = msg;
toast.classList.remove('opacity-0', '-translate-y-4', 'pointer-events-none');
setTimeout(() => toast.classList.add('opacity-0', '-translate-y-4', 'pointer-events-none'), 3000);
}
// --- 模板引擎数据模型 ---
const baseTemplates = {
'RCT': ['研究标识 (Study_ID)', '试验注册号 (NCT)', '研究类型 (Design)', '干预组人数 (N)', '对照组人数 (N)', '年龄 (Age)', '性别 (Gender)', 'RoB 2.0 偏倚评估', '核心结局指标 (HR/Events)'],
'Cohort': ['研究标识 (Study_ID)', '暴露组人数 (N)', '非暴露组人数 (N)', '随访人年 (Person-years)', '基线匹配方法 (PSM)', 'NOS 偏倚评分', '相对危险度 (RR/OR)']
};
let customFields = [
{ id: 1, name: '糖尿病史比例 (%)', type: 'Percentage', prompt: '请在基线表中寻找合并有 Type 2 Diabetes 的患者比例' }
];
let editingFieldId = null;
// 初始化渲染
window.onload = function() {
renderBaseFields();
renderCustomFields();
};
// 渲染基础字段标签
function renderBaseFields() {
const select = document.getElementById('base-template-select');
const container = document.getElementById('base-fields-container');
const fields = baseTemplates[select.value];
container.innerHTML = fields.map(field =>
`<span class="inline-block text-[11px] bg-white border border-slate-200 text-slate-600 px-2 py-1 rounded shadow-sm">
<i class="fa-solid fa-lock text-slate-300 mr-1"></i> ${field}
</span>`
).join('');
}
// 切换基座模板
function changeTemplate() {
renderBaseFields();
showToast('基座模板已切换,解析核心规则已更新');
}
// 渲染自定义字段列表
function renderCustomFields() {
const list = document.getElementById('custom-fields-list');
if (customFields.length === 0) {
list.innerHTML = `<div class="text-center py-4 text-xs text-gray-400 border border-dashed border-gray-200 rounded">暂无自定义字段AI 将仅提取系统基座数据</div>`;
return;
}
list.innerHTML = customFields.map(field => `
<div class="bg-white border border-blue-100 shadow-sm p-3 rounded-lg flex items-start group hover:border-blue-300 transition-colors">
<div class="flex-1">
<div class="flex items-center mb-1.5">
<span class="text-sm font-bold text-gray-800 mr-3">${field.name}</span>
<span class="text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded border border-blue-200 font-mono">Type: ${field.type}</span>
</div>
<div class="text-xs text-gray-500 bg-gray-50 border border-gray-100 p-2 rounded font-serif italic relative">
<span class="absolute -top-2 left-2 bg-gray-50 px-1 text-[9px] text-gray-400 not-italic">AI Prompt</span>
"${field.prompt}"
</div>
</div>
<div class="flex flex-col space-y-2 ml-3 opacity-0 group-hover:opacity-100 transition-opacity">
<button class="text-gray-400 hover:text-primary transition-colors" onclick="openFieldModal(${field.id})" title="编辑"><i class="fa-solid fa-pen-to-square"></i></button>
<button class="text-gray-400 hover:text-red-500 transition-colors" onclick="deleteField(${field.id})" title="删除"><i class="fa-solid fa-trash-can"></i></button>
</div>
</div>
`).join('');
}
// --- 模态框控制 ---
const modal = document.getElementById('field-modal');
const modalBackdrop = document.getElementById('field-modal-backdrop');
const modalContent = document.getElementById('field-modal-content');
function openFieldModal(id = null) {
editingFieldId = id;
if (id) {
const field = customFields.find(f => f.id === id);
document.getElementById('field-modal-title').innerText = '编辑自定义提取字段';
document.getElementById('field-name').value = field.name;
document.getElementById('field-type').value = field.type;
document.getElementById('field-prompt').value = field.prompt;
} else {
document.getElementById('field-modal-title').innerText = '添加自定义提取字段';
document.getElementById('field-name').value = '';
document.getElementById('field-type').value = 'String';
document.getElementById('field-prompt').value = '';
}
modal.classList.remove('hidden');
modal.classList.add('flex');
modalBackdrop.classList.remove('hidden');
setTimeout(() => modalContent.classList.add('modal-open'), 10);
}
function closeFieldModal() {
modalContent.classList.remove('modal-open');
setTimeout(() => {
modal.classList.add('hidden');
modal.classList.remove('flex');
modalBackdrop.classList.add('hidden');
}, 200);
}
function saveCustomField() {
const name = document.getElementById('field-name').value.trim();
const type = document.getElementById('field-type').value;
const prompt = document.getElementById('field-prompt').value.trim();
if (!name || !prompt) {
alert('请填写完整的字段名称和 AI 提取指令!');
return;
}
if (editingFieldId) {
const field = customFields.find(f => f.id === editingFieldId);
field.name = name;
field.type = type;
field.prompt = prompt;
showToast('字段修改成功');
} else {
const newId = customFields.length > 0 ? Math.max(...customFields.map(f => f.id)) + 1 : 1;
customFields.push({ id: newId, name, type, prompt });
showToast('成功添加自定义提取字段');
}
renderCustomFields();
closeFieldModal();
}
function deleteField(id) {
if (confirm('确定要删除这个自定义提取字段吗?')) {
customFields = customFields.filter(f => f.id !== id);
renderCustomFields();
showToast('字段已删除');
}
}
// --- 步骤 1: 模拟上传文件 ---
function simulateUpload() {
const uploadArea = document.getElementById('upload-area');
const fileList = document.getElementById('file-list');
const btnStart = document.getElementById('btn-start');
uploadArea.classList.add('hidden');
fileList.classList.remove('hidden');
fileList.classList.add('flex');
btnStart.classList.remove('opacity-50', 'cursor-not-allowed');
btnStart.classList.add('hover:shadow-lg');
btnStart.disabled = false;
}
// --- 步骤 2: 开始批量提取 ---
function startProcessing() {
document.getElementById('step-indicator-1').classList.replace('active', 'completed');
document.getElementById('step-indicator-2').classList.add('active');
document.getElementById('view-setup').classList.add('hidden');
document.getElementById('view-processing').classList.remove('hidden');
const logs = document.getElementById('process-logs');
const pBar = document.getElementById('progress-bar');
// 组装最终 Schema 的提示
const baseSchema = document.getElementById('base-template-select').value;
const customCount = customFields.length;
const events = [
{ delay: 500, text: `<span class="text-blue-400">[MinerU]</span> Extracting tables from Gandhi_2018_NEJM.pdf...`, progress: '20%' },
{ delay: 800, text: `<span class="text-green-400">[MinerU]</span> Table extraction success.`, progress: '30%' },
{ delay: 600, text: `<span class="text-purple-400">[DeepSeek]</span> Building Dynamic Schema: [Base: ${baseSchema}] + [Custom Fields: ${customCount}]...`, progress: '50%' },
{ delay: 1000, text: `<span class="text-yellow-400">[System]</span> 1/3 Documents processed.`, progress: '60%' },
{ delay: 500, text: `<span class="text-blue-400">[MinerU]</span> Parsing remaining documents...`, progress: '80%' },
{ delay: 1200, text: `<span class="text-green-500 font-bold">[Success] All documents successfully extracted according to custom schema!</span>`, progress: '100%' },
{ delay: 800, type: 'finish' }
];
let cumDelay = 0;
events.forEach(e => {
cumDelay += e.delay;
setTimeout(() => {
if(e.type === 'finish') {
finishProcessing();
return;
}
pBar.style.width = e.progress;
const div = document.createElement('div');
div.innerHTML = `>> ${e.text}`;
logs.appendChild(div);
logs.scrollTop = logs.scrollHeight;
}, cumDelay);
});
}
// --- 步骤 3: 进入工作台 ---
function finishProcessing() {
document.getElementById('step-indicator-2').classList.replace('active', 'completed');
document.getElementById('step-indicator-3').classList.add('active');
document.getElementById('view-processing').classList.add('hidden');
document.getElementById('view-workbench').classList.remove('hidden');
}
// --- 步骤 4: 抽屉操作 ---
const drawer = document.getElementById('extraction-drawer');
const backdrop = document.getElementById('drawer-backdrop');
function openDrawer() {
backdrop.classList.remove('hidden');
void drawer.offsetWidth;
drawer.classList.add('drawer-open');
}
function closeDrawer() {
drawer.classList.remove('drawer-open');
setTimeout(() => backdrop.classList.add('hidden'), 300);
}
function approveAndClose() {
closeDrawer();
showToast('提取数据已核准 (Approved) 存入数据库');
const statusCell = document.getElementById('status-1');
statusCell.innerHTML = `<span class="text-xs bg-green-50 text-green-600 px-2 py-1 rounded border border-green-200 flex items-center w-max"><i class="fa-solid fa-check-double mr-1"></i>Approved</span>`;
document.getElementById('export-action').classList.remove('hidden');
}
</script>
</body>
</html>