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>
646 lines
42 KiB
HTML
646 lines
42 KiB
HTML
<!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 Non–Small-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 Non–Small-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 Non–Small-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> |