feat(asl): Complete Week 4 - Results display and Excel export with hybrid solution

Features:
- Backend statistics API (cloud-native Prisma aggregation)
- Results page with hybrid solution (AI consensus + human final decision)
- Excel export (frontend generation, zero disk write, cloud-native)
- PRISMA-style exclusion reason analysis with bar chart
- Batch selection and export (3 export methods)
- Fixed logic contradiction (inclusion does not show exclusion reason)
- Optimized table width (870px, no horizontal scroll)

Components:
- Backend: screeningController.ts - add getProjectStatistics API
- Frontend: ScreeningResults.tsx - complete results page (hybrid solution)
- Frontend: excelExport.ts - Excel export utility (40 columns full info)
- Frontend: ScreeningWorkbench.tsx - add navigation button
- Utils: get-test-projects.mjs - quick test tool

Architecture:
- Cloud-native: backend aggregation reduces network transfer
- Cloud-native: frontend Excel generation (zero file persistence)
- Reuse platform: global prisma instance, logger
- Performance: statistics API < 500ms, Excel export < 3s (1000 records)

Documentation:
- Update module status guide (add Week 4 features)
- Update task breakdown (mark Week 4 completed)
- Update API design spec (add statistics API)
- Update database design (add field usage notes)
- Create Week 4 development plan
- Create Week 4 completion report
- Create technical debt list

Test:
- End-to-end flow test passed
- All features verified
- Performance test passed
- Cloud-native compliance verified

Ref: Week 4 Development Plan
Scope: ASL Module MVP - Title Abstract Screening Results
Cloud-Native: Backend aggregation + Frontend Excel generation
This commit is contained in:
2025-11-21 20:12:38 +08:00
parent 2e8699c217
commit 8eef9e0544
207 changed files with 11142 additions and 531 deletions

View File

@@ -0,0 +1,360 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>全文解析与数据提取模块原型 V4</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/heroicons/2.1.3/24/outline/css/heroicons.min.css" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
.sidebar { background-color: #f8fafc; transition: width 0.3s ease; }
.sidebar-link.active { background-color: #e0f2fe; color: #0c4a6e; font-weight: 600; }
.sidebar.collapsed { width: 5rem; }
.sidebar:not(.collapsed) .sidebar-text { display: inline; }
.sidebar.collapsed .sidebar-text, .sidebar.collapsed .logo-text, .sidebar.collapsed .nav-submenu { display: none; }
.modal-backdrop { background-color: rgba(0, 0, 0, 0.5); }
.progress-bar div { transition: width 0.5s ease-in-out; }
.main-content { transition: margin-left 0.3s ease; }
.sidebar.collapsed ~ .main-content { margin-left: 5rem; }
.sidebar:not(.collapsed) ~ .main-content { margin-left: 16rem; }
/* V4 Styles */
.workbench-queue { transition: width 0.3s ease; }
.workbench-queue.collapsed { width: 0rem; padding: 0; overflow: hidden; }
.workbench-queue.collapsed ~ .workbench-main { width: 100%; }
.workbench-queue-item.active { background-color: #e0f2fe; }
.pdf-highlight { background-color: #fef08a; transition: background-color 0.3s; }
.data-field:hover { background-color: #f0f9ff; }
.data-conflict { border: 2px solid #ef4444; }
.tooltip { visibility: hidden; opacity: 0; transition: opacity 0.2s; }
.has-tooltip:hover .tooltip { visibility: visible; opacity: 1; }
</style>
</head>
<body class="bg-gray-100 text-gray-800">
<!-- 主容器 -->
<div class="h-screen flex">
<!-- 模拟的左侧主导航 -->
<aside id="sidebar" class="sidebar w-64 border-r border-gray-200 p-4 flex-shrink-0 flex flex-col fixed h-full">
<div class="text-xl font-bold text-gray-800 mb-8 flex items-center space-x-2">
<svg class="h-8 w-8 text-sky-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25" /></svg>
<span class="logo-text">AI文献平台</span>
</div>
<nav class="flex-grow space-y-2">
<a href="#" class="sidebar-link group flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-700 space-x-3"><i class="h-5 w-5 hi-outline hi-home"></i><span class="sidebar-text">项目概览</span></a>
<a href="#" class="sidebar-link group flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-700 space-x-3"><i class="h-5 w-5 hi-outline hi-magnifying-glass"></i><span class="sidebar-text">1. 智能文献检索</span></a>
<a href="#" class="sidebar-link group flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-700 space-x-3"><i class="h-5 w-5 hi-outline hi-beaker"></i><span class="sidebar-text">2. AI辅助初筛</span></a>
<div id="nav-section-extraction">
<a href="#" id="nav-extraction-main" class="sidebar-link active group flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-700 space-x-3">
<i class="h-5 w-5 hi-outline hi-document-text"></i>
<span class="sidebar-text">3. 全文解析与数据提取</span>
</a>
<div class="nav-submenu pl-6 mt-1 space-y-1">
<a href="#" id="nav-library" class="sidebar-link active group flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-700"><span class="sidebar-text">文献库与模板</span></a>
<a href="#" id="nav-workbench" class="sidebar-link group flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-700"><span class="sidebar-text">审查台</span></a>
<a href="#" id="nav-summary" class="sidebar-link group flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-700"><span class="sidebar-text">数据汇总</span></a>
</div>
</div>
<a href="#" class="sidebar-link group flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-700 space-x-3"><i class="h-5 w-5 hi-outline hi-chart-pie"></i><span class="sidebar-text">4. 数据分析与报告</span></a>
</nav>
<div class="flex-shrink-0 mt-auto"><button id="sidebar-toggle" class="group flex items-center w-full px-3 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-200 space-x-3"><i class="h-5 w-5 hi-outline hi-arrows-right-left"></i><span class="sidebar-text">收起</span></button></div>
</aside>
<!-- 主内容区 -->
<div class="main-content flex-grow flex flex-col w-full" style="margin-left: 16rem;">
<header class="bg-white shadow-sm flex-shrink-0 z-10 p-4 border-b"><h1 id="header-title" class="text-xl font-bold">全文解析与数据提取 / 文献库与模板</h1></header>
<div id="view-container" class="flex-grow p-6 overflow-auto">
<!-- 视图1: 文献库与模板 -->
<div id="library-view">
<!-- content from v3, unchanged -->
<div class="space-y-8"><div><h2 class="text-2xl font-bold mb-4">1. 数据提取与评价模板</h2><div class="bg-white p-6 rounded-lg shadow flex items-center justify-between"><p class="text-gray-600">为保证提取质量,请为本项目选择或创建一个模板。</p><div class="flex items-center space-x-4"><select id="template-selector" class="rounded-md border-gray-300 shadow-sm"><option value="">选择一个模板...</option><option value="meta_analysis">Meta分析标准模板</option><option value="drug_eval">药物评价模板</option></select><button class="text-sky-600 hover:text-sky-800 font-semibold">创建新模板</button></div></div></div><div><div class="flex justify-between items-center mb-4"><h2 class="text-2xl font-bold">2. 待提取文献库 (50篇)</h2><button id="launch-extraction-btn" disabled class="bg-sky-600 text-white font-bold py-2 px-6 rounded-lg shadow-md transition-all disabled:bg-gray-400 disabled:cursor-not-allowed">启动AI提取</button></div><div class="bg-white rounded-lg shadow overflow-hidden"><table class="min-w-full divide-y divide-gray-200"><thead class="bg-gray-50"><tr><th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">文献标题</th><th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">作者</th><th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">状态</th><th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">操作</th></tr></thead><tbody id="literature-library-body" class="bg-white divide-y divide-gray-200"></tbody></table></div></div></div>
</div>
<!-- 视图2: 三栏式审查台 (默认隐藏) -->
<div id="workbench-view" class="hidden h-full flex flex-col">
<div id="workbench-placeholder" class="hidden text-center p-10 bg-white rounded-lg shadow">
<h3 class="text-lg font-semibold text-gray-700">请选择一篇文献</h3>
<p class="text-gray-500 mt-2">请先从“文献库与模板”页面中选择一篇已完成的文献进入审查台。</p>
<button onclick="showView('library')" class="mt-4 bg-sky-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-sky-700">返回文献库</button>
</div>
<div id="workbench-content" class="flex-grow flex space-x-4 overflow-hidden">
<!-- V4: New Workbench Queue -->
<aside id="workbench-queue" class="workbench-queue w-80 bg-white rounded-lg shadow flex-shrink-0 flex flex-col">
<div class="p-4 border-b flex justify-between items-center">
<h3 class="font-bold text-lg">工作队列</h3>
<button id="queue-toggle-btn" class="p-1 text-gray-500 hover:bg-gray-200 rounded">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" /></svg>
</button>
</div>
<div class="p-2"><input type="search" placeholder="搜索文献..." class="w-full p-2 border rounded-md text-sm"></div>
<ul id="queue-list" class="overflow-y-auto flex-grow p-2 space-y-1"></ul>
</aside>
<!-- V4: Main Workbench Area (PDF + Form) -->
<div class="workbench-main flex-grow flex space-x-4">
<div class="w-1/2 bg-white rounded-lg shadow flex flex-col overflow-hidden">
<div class="p-4 border-b font-bold bg-gray-50">PDF原文阅读器</div>
<div id="pdf-viewer" class="overflow-y-auto p-6 text-gray-700 leading-relaxed"></div>
</div>
<div class="w-1/2 bg-white rounded-lg shadow flex flex-col overflow-hidden">
<div class="p-4 border-b font-bold bg-gray-50">结构化数据与评价模板</div>
<div id="data-template-form" class="overflow-y-auto p-6 space-y-6"></div>
</div>
</div>
</div>
</div>
<!-- 视图3: 数据汇总页 (默认隐藏) -->
<div id="summary-view" class="hidden">
<!-- content from v3, unchanged -->
<div class="bg-white rounded-lg shadow p-6"><div class="flex justify-between items-center mb-6"><h2 class="text-2xl font-bold">数据汇总</h2><div class="space-x-4"><button class="bg-gray-700 hover:bg-gray-800 text-white font-bold py-2 px-4 rounded-lg">↓ 导出为Excel</button><button class="bg-sky-600 hover:bg-sky-700 text-white font-bold py-2 px-4 rounded-lg">进入数据分析 →</button></div></div><div id="summary-stats" class="grid grid-cols-3 gap-6 mb-8 text-center"></div><div id="model-performance-summary" class="mb-8 bg-gray-50 p-4 rounded-lg"></div><input type="text" id="summary-search" placeholder="在数据中搜索..." class="w-full p-2 border rounded mb-4"><div class="overflow-x-auto"><table id="summary-table" class="min-w-full divide-y divide-gray-200"></table></div></div>
</div>
</div>
</div>
</div>
<!-- 任务状态面板 (模态框) -->
<div id="task-status-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center">
<!-- content from v3, unchanged -->
<div class="modal-backdrop absolute inset-0"></div><div class="bg-white rounded-lg shadow-xl p-8 w-full max-w-2xl z-10"><h2 class="text-2xl font-bold text-center mb-4">AI提取进行中...</h2><div class="flex items-center space-x-4 mb-6"><div class="w-full bg-gray-200 rounded-full h-4 progress-bar"><div id="progress-bar-inner" class="bg-sky-500 h-4 rounded-full" style="width: 0%"></div></div><span id="progress-text" class="font-semibold">0 / 50</span></div><div id="live-counts" class="grid grid-cols-2 gap-x-8 gap-y-4 bg-gray-50 p-4 rounded-lg mb-4 text-sm"></div><p id="eta-text" class="text-center text-gray-500 mb-6">预计剩余时间: 计算中...</p><div class="flex justify-center"><button id="close-modal-btn" class="hidden bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-6 rounded-lg">提取完成!返回文献库</button></div></div>
</div>
<script>
// --- STATE MANAGEMENT ---
let currentView = 'library';
let currentWorkbenchDocId = null;
let templateSelected = false;
let literatureData = [];
const TOTAL_DOCS = 50;
// --- DATA SIMULATION ---
function generateLiteratureData() {
for (let i = 1; i <= TOTAL_DOCS; i++) {
let status;
const rand = Math.random();
if (rand < 0.1) status = 'failed';
else if (rand < 0.3) status = 'fetching';
else status = 'ready';
literatureData.push({
id: i,
title: `${i}篇RCT研究: Drug-X对糖尿病的疗效`,
authors: 'Smith J, et al.',
status: status,
is_reviewed: false, // V4: Track review status
pdf_content: {
introduction: `研究背景II型糖尿病是全球性的健康问题。本研究旨在评估新型药物Drug-X的有效性。`,
methods: `研究设计:我们进行了一项多中心、双盲、随机对照试验(RCT)共纳入152名患者随机分为试验组(n=75)和安慰剂组(n=77)。主要终点为糖化血红蛋白变化。`,
results: `研究结果试验组的糖化血红蛋白平均降低了1.5%而安慰剂组为0.2% (p < 0.01)。`,
},
extracted_data: null
});
}
}
// --- INITIALIZATION ---
document.addEventListener('DOMContentLoaded', () => {
generateLiteratureData();
renderLibraryTable();
});
// --- VIEW & NAVIGATION MANAGEMENT ---
const views = { library: document.getElementById('library-view'), workbench: document.getElementById('workbench-view'), summary: document.getElementById('summary-view') };
const navLinks = { library: document.getElementById('nav-library'), workbench: document.getElementById('nav-workbench'), summary: document.getElementById('nav-summary') };
const headerTitle = document.getElementById('header-title');
function showView(view, docId = null) {
currentView = view;
Object.values(views).forEach(v => v.classList.add('hidden'));
Object.values(navLinks).forEach(l => l.classList.remove('active'));
views[view].classList.remove('hidden');
if(navLinks[view]) navLinks[view].classList.add('active');
const titles = { library: '文献库与模板', workbench: '审查台', summary: '数据汇总' };
headerTitle.textContent = '全文解析与数据提取 / ' + (titles[view] || '未知页面');
if (view === 'summary') renderSummaryPage();
if (view === 'workbench') {
const workbenchContent = document.getElementById('workbench-content');
const workbenchPlaceholder = document.getElementById('workbench-placeholder');
if (docId) {
currentWorkbenchDocId = docId;
workbenchContent.classList.remove('hidden');
workbenchPlaceholder.classList.add('hidden');
renderWorkbench(docId);
} else {
currentWorkbenchDocId = null;
workbenchContent.classList.add('hidden');
workbenchPlaceholder.classList.remove('hidden');
}
}
}
document.getElementById('nav-library').addEventListener('click', (e) => { e.preventDefault(); showView('library'); });
document.getElementById('nav-summary').addEventListener('click', (e) => { e.preventDefault(); showView('summary'); });
document.getElementById('nav-workbench').addEventListener('click', (e) => {
e.preventDefault();
showView('workbench', currentWorkbenchDocId);
});
// --- VIEW 1: LIBRARY & TEMPLATES (unchanged from V3) ---
const templateSelector = document.getElementById('template-selector');
const launchBtn = document.getElementById('launch-extraction-btn');
templateSelector.addEventListener('change', () => { templateSelected = templateSelector.value !== ''; updateLaunchButtonState(); });
function updateLaunchButtonState() { const readyDocs = literatureData.some(doc => doc.status === 'ready'); launchBtn.disabled = !(templateSelected && readyDocs); }
function renderLibraryTable() { const tbody = document.getElementById('literature-library-body'); tbody.innerHTML = literatureData.map(doc => { const statusMap = { fetching: `<span class="text-gray-500">正在获取全文...</span>`, ready: `<span class="text-green-600 font-semibold">准备就绪</span>`, failed: `<span class="text-red-600 font-semibold">获取失败</span>`, extracting: `<span class="text-blue-600">AI提取中...</span>`, completed: `<span class="text-purple-600 font-semibold">已完成</span>` }; let actionButton; switch(doc.status) { case 'failed': actionButton = `<button class="text-sky-600 hover:underline" onclick="handleUpload(${doc.id})">上传PDF</button>`; break; case 'completed': actionButton = `<button class="text-sky-600 hover:underline font-semibold" onclick="showView('workbench', ${doc.id})">进入审查台</button>`; break; case 'extracting': actionButton = `<span class="text-gray-400">提取中...</span>`; break; case 'ready': actionButton = `<span class="text-gray-400">等待提取</span>`; break; default: actionButton = `<span class="text-gray-400">获取中...</span>`; } return `<tr><td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${doc.title}</td><td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${doc.authors}</td><td class="px-6 py-4 whitespace-nowrap text-sm">${statusMap[doc.status]}</td><td class="px-6 py-4 whitespace-nowrap text-sm">${actionButton}</td></tr>`; }).join(''); }
function handleUpload(docId) { alert(`模拟为文献ID ${docId} 上传PDF...`); const doc = literatureData.find(d => d.id === docId); doc.status = 'ready'; renderLibraryTable(); updateLaunchButtonState(); }
// --- TASK STATUS MODAL (unchanged from V3) ---
const taskModal = document.getElementById('task-status-modal');
launchBtn.addEventListener('click', () => { taskModal.classList.remove('hidden'); let processed = 0; const docsToProcess = literatureData.filter(d => d.status === 'ready'); const totalToProcess = docsToProcess.length; const progressBarInner = document.getElementById('progress-bar-inner'); const progressText = document.getElementById('progress-text'); const liveCounts = document.getElementById('live-counts'); const etaText = document.getElementById('eta-text'); const closeModalBtn = document.getElementById('close-modal-btn'); docsToProcess.forEach(d => d.status = 'extracting'); renderLibraryTable(); let counts = { pdf_ok: 0, pdf_fail: 0, deepseek_done: 0, qwen3_done: 0 }; const interval = setInterval(() => { if (processed < totalToProcess) { processed++; if (Math.random() > 0.05) counts.pdf_ok++; else counts.pdf_fail++; if (processed > 2 && Math.random() > 0.1) counts.deepseek_done++; if (processed > 4 && Math.random() > 0.2) counts.qwen3_done++; const percentage = (processed / totalToProcess) * 100; progressBarInner.style.width = `${percentage}%`; progressText.textContent = `${processed} / ${totalToProcess}`; liveCounts.innerHTML = `<div>PDF解析成功: <span class="font-bold">${counts.pdf_ok}</span></div><div>PDF解析失败: <span class="font-bold text-red-500">${counts.pdf_fail}</span></div><div>DeepSeek 提取完成: <span class="font-bold">${counts.deepseek_done}</span></div><div>Qwen3 提取完成: <span class="font-bold">${counts.qwen3_done}</span></div>`; etaText.textContent = `预计剩余时间: 约 ${Math.round((totalToProcess - processed) * 0.5)}`; } else { clearInterval(interval); etaText.textContent = '所有任务已加入后台队列处理完成!'; closeModalBtn.classList.remove('hidden'); docsToProcess.forEach(d => { d.status = 'completed'; d.extracted_data = { study_design: { deepseek: 'RCT', qwen3: 'RCT', source: '我们进行了一项多中心、双盲、随机对照试验(RCT)' }, sample_size: { deepseek: '150', qwen3: '152', source: '共纳入152名患者' }, intervention_group_n: { deepseek: '75', qwen3: '75', source: '随机分为试验组(n=75)和安慰剂组(n=77)' }, rob_randomization: { user_judgement: null, evidence: '' }, rob_blinding: { user_judgement: null, evidence: '' }, }; }); renderLibraryTable(); } }, 500); });
document.getElementById('close-modal-btn').addEventListener('click', () => { taskModal.classList.add('hidden'); });
// --- VIEW 2: WORKBENCH (V4 Refactor) ---
function renderWorkbench(docId) {
const doc = literatureData.find(d => d.id === docId);
if (!doc) return;
renderWorkbenchQueue();
renderPdfViewer(doc);
renderDataForm(doc);
addWorkbenchInteractivity();
}
function renderWorkbenchQueue() {
const queueList = document.getElementById('queue-list');
const completedDocs = literatureData.filter(d => d.status === 'completed');
queueList.innerHTML = completedDocs.map(doc => {
const isActive = doc.id === currentWorkbenchDocId;
const statusIcon = doc.is_reviewed
? `<span class="text-green-500" title="已审查">✓</span>`
: `<span class="text-gray-400" title="待审查">○</span>`;
return `<li class="workbench-queue-item p-3 rounded-md cursor-pointer hover:bg-gray-100 ${isActive ? 'active' : ''}" onclick="showView('workbench', ${doc.id})">
<div class="flex justify-between items-start">
<p class="text-sm font-medium text-gray-800 truncate pr-2">${doc.title}</p>
${statusIcon}
</div>
<p class="text-xs text-gray-500">${doc.authors}</p>
</li>`;
}).join('');
}
function renderPdfViewer(doc) {
const pdfViewer = document.getElementById('pdf-viewer');
pdfViewer.innerHTML = Object.entries(doc.pdf_content).map(([key, text]) =>
`<p class="mb-4"><strong class="capitalize">${key}:</strong> <span data-section="${key}">${text}</span></p>`
).join('');
}
function renderDataForm(doc) {
const formContainer = document.getElementById('data-template-form');
const data = doc.extracted_data;
if (!data) {
formContainer.innerHTML = `<div class="text-center p-8"><h3 class="font-bold text-lg">数据加载失败</h3><p class="text-gray-600 mt-2">该文献的数据不存在或尚未提取。</p></div>`;
return;
}
formContainer.innerHTML = `
<div class="space-y-4">
<h3 class="text-lg font-bold border-b pb-2">数据提取</h3>
${renderDataField('study_design', '研究设计', data.study_design)}
${renderDataField('sample_size', '总样本量', data.sample_size)}
${renderDataField('intervention_group_n', '干预组样本量', data.intervention_group_n)}
</div>
<div class="space-y-4 mt-8">
<h3 class="text-lg font-bold border-b pb-2">批判性评价 (Cochrane RoB 2)</h3>
${renderAppraisalField('rob_randomization', '随机过程产生的偏倚')}
${renderAppraisalField('rob_blinding', '致盲产生的偏倚')}
</div>
<div class="mt-8 pt-4 border-t">
<button onclick="markAsReviewed(${doc.id})" class="w-full bg-green-600 text-white font-bold py-3 rounded-lg hover:bg-green-700">标记为已审查</button>
</div>
`;
}
function renderDataField(id, label, data) {
const isConflict = data.deepseek !== data.qwen3;
return `
<div class="data-field rounded-lg border p-4 ${isConflict ? 'data-conflict' : ''}" data-source="${data.source}">
<label class="font-semibold text-gray-700 block mb-3">${label} ${isConflict ? '<span class="text-red-500 font-bold text-xs ml-2">待仲裁</span>' : ''}</label>
<div class="grid grid-cols-2 gap-4 mb-3 text-sm">
<div class="bg-gray-50 p-2 rounded"><p class="font-bold text-gray-500">DeepSeek:</p><p class="text-gray-800">${data.deepseek}</p></div>
<div class="bg-gray-50 p-2 rounded"><p class="font-bold text-gray-500">Qwen3:</p><p class="text-gray-800">${data.qwen3}</p></div>
</div>
<div>
<label class="text-sm font-medium">最终值:</label>
<input type="text" value="${isConflict ? '' : data.deepseek}" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2">
</div>
</div>
`;
}
function renderAppraisalField(id, label) { return `<div class="rounded-lg border p-4"><label class="font-semibold text-gray-700 block mb-3">${label}</label><div class="flex space-x-4"><label class="flex items-center"><input type="radio" name="${id}" class="mr-1"> 低风险</label><label class="flex items-center"><input type="radio" name="${id}" class="mr-1"> 有些顾虑</label><label class="flex items-center"><input type="radio" name="${id}" class="mr-1"> 高风险</label></div><button class="mt-3 text-xs text-sky-600 hover:underline">🔗 链接证据</button></div>`; }
function addWorkbenchInteractivity() {
document.querySelectorAll('.data-field').forEach(field => {
field.addEventListener('mouseover', () => {
const sourceText = field.dataset.source;
highlightPdfText(sourceText, true);
});
field.addEventListener('mouseout', () => {
highlightPdfText(null, false);
});
});
}
function highlightPdfText(textToHighlight, shouldHighlight) {
const pdfViewer = document.getElementById('pdf-viewer');
let content = pdfViewer.innerHTML.replace(/<mark class="pdf-highlight">/g, '').replace(/<\/mark>/g, '');
if (shouldHighlight && textToHighlight) {
content = content.replace(textToHighlight, `<mark class="pdf-highlight">${textToHighlight}</mark>`);
}
pdfViewer.innerHTML = content;
if (shouldHighlight) {
const mark = pdfViewer.querySelector('mark');
if (mark) mark.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
function markAsReviewed(docId) {
const doc = literatureData.find(d => d.id === docId);
if(doc) doc.is_reviewed = true;
renderWorkbenchQueue();
}
document.getElementById('queue-toggle-btn').addEventListener('click', () => {
document.getElementById('workbench-queue').classList.toggle('collapsed');
});
// --- VIEW 3: SUMMARY (unchanged from V3) ---
function renderSummaryPage() {
const completedDocs = literatureData.filter(d => d.status === 'completed');
document.getElementById('summary-stats').innerHTML = `<div class="bg-gray-50 p-4 rounded-lg"><div class="text-3xl font-bold">${TOTAL_DOCS}</div><div class="text-gray-500">总计文献</div></div><div class="bg-green-50 p-4 rounded-lg"><div class="text-3xl font-bold text-green-600">${completedDocs.length}</div><div class="text-gray-500">已提取文献</div></div><div class="bg-purple-50 p-4 rounded-lg"><div class="text-3xl font-bold text-purple-600">${completedDocs.length * 5}</div><div class="text-gray-500">已提取数据点</div></div>`;
const deepseekAccuracy = "92.5%"; const qwen3Accuracy = "88.0%";
document.getElementById('model-performance-summary').innerHTML = `<h3 class="font-bold text-lg mb-2">模型表现评估</h3><div class="flex space-x-8 text-center"><div><div class="text-2xl font-bold text-sky-700">${deepseekAccuracy}</div><div class="text-sm text-gray-500">DeepSeek 正确率</div></div><div><div class="text-2xl font-bold text-teal-700">${qwen3Accuracy}</div><div class="text-sm text-gray-500">Qwen3 正确率</div></div></div>`;
const summaryTable = document.getElementById('summary-table');
const tableHead = `<thead><tr class="bg-gray-50"><th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">文献</th><th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">研究设计</th><th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">总样本量</th><th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">干预组样本量</th><th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">随机化偏倚</th></tr></thead>`;
const tableBody = `<tbody>${completedDocs.map(doc => `
<tr class="hover:bg-gray-50">
<td class="px-4 py-2 text-sm font-medium">${doc.title}</td>
<td class="px-4 py-2 text-sm has-tooltip relative"><span>${doc.extracted_data.study_design.deepseek}</span><div class="tooltip absolute z-10 -mt-16 w-64 bg-gray-800 text-white text-xs rounded py-1 px-2">${doc.extracted_data.study_design.source}</div></td>
<td class="px-4 py-2 text-sm has-tooltip relative"><span>${doc.extracted_data.sample_size.qwen3}</span><div class="tooltip absolute z-10 -mt-16 w-64 bg-gray-800 text-white text-xs rounded py-1 px-2">${doc.extracted_data.sample_size.source}</div></td>
<td class="px-4 py-2 text-sm">...</td>
<td class="px-4 py-2 text-sm">...</td>
</tr>`).join('')}</tbody>`;
summaryTable.innerHTML = tableHead + tableBody;
}
// --- GLOBAL INTERACTIVITY ---
document.getElementById('sidebar-toggle').addEventListener('click', () => {
document.getElementById('sidebar').classList.toggle('collapsed');
});
</script>
</body>
</html>

View File

@@ -0,0 +1,303 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据综合分析模块原型 V1</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/heroicons/2.1.3/24/outline/css/heroicons.min.css" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
.sidebar { background-color: #f8fafc; transition: width 0.3s ease; }
.sidebar-link.active { background-color: #e0f2fe; color: #0c4a6e; font-weight: 600; }
.main-content { transition: margin-left 0.3s ease; margin-left: 16rem; }
.modal-backdrop { background-color: rgba(0, 0, 0, 0.5); }
.wizard-step { display: none; }
.wizard-step.active { display: block; }
.droppable { background-color: #f9fafb; border: 2px dashed #d1d5db; }
.draggable { cursor: grab; }
.bubble { transition: all 0.3s ease; }
.bubble-chart-cell:hover .bubble { transform: scale(1.2); }
.tooltip { visibility: hidden; opacity: 0; transition: opacity 0.2s; }
.has-tooltip:hover .tooltip { visibility: visible; opacity: 1; }
</style>
</head>
<body class="bg-gray-100 text-gray-800">
<div class="h-screen flex">
<!-- 模拟的左侧主导航 -->
<aside id="sidebar" class="sidebar w-64 border-r border-gray-200 p-4 flex-shrink-0 flex flex-col fixed h-full">
<div class="text-xl font-bold text-gray-800 mb-8 flex items-center space-x-2">
<svg class="h-8 w-8 text-sky-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25" /></svg>
<span class="logo-text">AI文献平台</span>
</div>
<nav class="flex-grow space-y-2">
<a href="#" class="sidebar-link group flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-700 space-x-3"><i class="h-5 w-5 hi-outline hi-home"></i><span class="sidebar-text">项目概览</span></a>
<a href="#" class="sidebar-link group flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-700 space-x-3"><i class="h-5 w-5 hi-outline hi-magnifying-glass"></i><span class="sidebar-text">1. 智能文献检索</span></a>
<a href="#" class="sidebar-link group flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-700 space-x-3"><i class="h-5 w-5 hi-outline hi-beaker"></i><span class="sidebar-text">2. AI辅助初筛</span></a>
<a href="#" class="sidebar-link group flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-700 space-x-3"><i class="h-5 w-5 hi-outline hi-document-text"></i><span class="sidebar-text">3. 全文解析与数据提取</span></a>
<a href="#" id="nav-analysis-main" class="sidebar-link active group flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-700 space-x-3"><i class="h-5 w-5 hi-outline hi-chart-pie"></i><span class="sidebar-text">4. 数据综合分析与报告</span></a>
</nav>
</aside>
<!-- 主内容区 -->
<div class="main-content flex-grow flex flex-col w-full">
<header class="bg-white shadow-sm flex-shrink-0 z-10 p-4 border-b"><h1 id="header-title" class="text-xl font-bold">数据综合分析与报告生成</h1></header>
<div id="view-container" class="flex-grow p-6 overflow-auto">
<!-- 视图1: 应用选择中心 -->
<div id="hub-view">
<h2 class="text-3xl font-bold mb-6">应用选择中心</h2>
<p class="text-gray-600 mb-8">请选择您希望进行的分析应用。数据将自动从“全文解析与数据提取”模块导入。</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="bg-white p-8 rounded-lg shadow hover:shadow-xl transition-shadow cursor-pointer" onclick="startWizard()">
<h3 class="font-bold text-xl mb-3 text-sky-700">证据图谱生成</h3>
<p class="text-sm text-gray-600 mb-4">通过可视化矩阵,直观展示研究领域的证据分布,快速识别研究热点与证据空白。</p>
<span class="font-semibold text-sky-600">开始分析 →</span>
</div>
<div class="bg-white p-8 rounded-lg shadow hover:shadow-xl transition-shadow cursor-pointer opacity-50">
<h3 class="font-bold text-xl mb-3">Meta分析数据准备</h3>
<p class="text-sm text-gray-600 mb-4">为RevMan, Stata等专业统计软件准备和导出格式化、可直接使用的数据文件。</p>
<span class="font-semibold text-gray-500">即将推出</span>
</div>
<div class="bg-white p-8 rounded-lg shadow hover:shadow-xl transition-shadow cursor-pointer opacity-50">
<h3 class="font-bold text-xl mb-3">药物综合评价报告</h3>
<p class="text-sm text-gray-600 mb-4">基于模板,一键生成包含有效性、安全性等多维度的综合评价报告初稿。</p>
<span class="font-semibold text-gray-500">即将推出</span>
</div>
</div>
</div>
<!-- 视图2: 配置向导 -->
<div id="wizard-view" class="hidden">
<div class="max-w-4xl mx-auto bg-white p-8 rounded-lg shadow">
<h2 class="text-2xl font-bold mb-2">证据图谱框架配置向导</h2>
<p class="text-gray-500 mb-6">请按照步骤定义图谱的框架,以便系统为您生成可视化结果。</p>
<!-- Steps -->
<div id="wizard-step-1" class="wizard-step active">
<h3 class="text-lg font-semibold mb-4">步骤 1/3: 定义Y轴 (干预措施)</h3>
<p class="text-sm text-gray-600 mb-4">请将左侧提取的干预措施拖拽或分配到右侧的分组中。</p>
<div class="grid grid-cols-2 gap-4 h-96">
<div class="border rounded p-4">
<h4 class="font-semibold mb-2">可用的干预措施</h4>
<div id="available-interventions" class="space-y-2 text-sm">
<div class="draggable bg-gray-100 p-2 rounded">Drug-X</div><div class="draggable bg-gray-100 p-2 rounded">Drug-Y</div><div class="draggable bg-gray-100 p-2 rounded">安慰剂</div>
</div>
</div>
<div class="droppable rounded p-4">
<h4 class="font-semibold mb-2">干预措施分组</h4>
<div class="p-2 bg-white border rounded mb-2"><strong>分组1:</strong> Drug-X, Drug-Y <button class="text-xs text-sky-600 ml-2">重命名</button></div>
<div class="p-2 bg-white border rounded"><strong>分组2:</strong> 安慰剂 <button class="text-xs text-sky-600 ml-2">重命名</button></div>
</div>
</div>
</div>
<div id="wizard-step-2" class="wizard-step">
<h3 class="text-lg font-semibold mb-4">步骤 2/3: 定义X轴 (结局指标)</h3>
<p class="text-sm text-gray-600 mb-4">请将左侧提取的结局指标拖拽或分配到右侧的分组中。</p>
<!-- Simplified for demo -->
<p class="p-4 bg-gray-50 rounded">此步骤与上一步类似,用户可对“糖化血红蛋白”、“不良事件”等结局指标进行分组。</p>
</div>
<div id="wizard-step-3" class="wizard-step">
<h3 class="text-lg font-semibold mb-4">步骤 3/3: 配置气泡含义</h3>
<p class="text-sm text-gray-600 mb-4">请选择气泡颜色所代表的维度。气泡大小固定代表研究数量。</p>
<div class="space-y-3">
<label class="flex items-center p-4 border rounded-lg cursor-pointer"><input type="radio" name="bubble-color" class="mr-3" checked>研究质量/偏倚风险</label>
<label class="flex items-center p-4 border rounded-lg cursor-pointer"><input type="radio" name="bubble-color" class="mr-3">研究设计</label>
</div>
</div>
<!-- Navigation -->
<div class="mt-8 flex justify-between">
<button id="wizard-prev" onclick="navigateWizard(-1)" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded">上一步</button>
<button id="wizard-next" onclick="navigateWizard(1)" class="bg-sky-600 hover:bg-sky-700 text-white font-bold py-2 px-4 rounded">下一步</button>
<button id="wizard-finish" onclick="finishWizard()" class="hidden bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">生成证据图谱</button>
</div>
</div>
</div>
<!-- 视图3: 分析仪表盘 -->
<div id="dashboard-view" class="hidden h-full flex flex-col">
<div class="flex-shrink-0 flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold">交互式分析仪表盘</h2>
<button onclick="showView('report')" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded-lg shadow">生成完整报告</button>
</div>
<div class="flex-grow flex space-x-6 overflow-hidden">
<!-- Left Panel -->
<aside class="w-1/4 bg-white rounded-lg shadow p-6 overflow-y-auto">
<h3 class="text-lg font-bold mb-4">动态筛选器</h3>
<!-- Filters -->
<div class="space-y-4 text-sm">
<div><label class="font-semibold">发表年份:</label><input type="range" class="w-full mt-1"></div>
<div><label class="font-semibold">研究设计:</label><div class="mt-1 space-y-1"><label class="flex items-center"><input type="checkbox" checked class="mr-2">RCT</label><label class="flex items-center"><input type="checkbox" checked class="mr-2">队列研究</label></div></div>
</div>
<hr class="my-6">
<h3 class="text-lg font-bold mb-4">AI洞察摘要</h3>
<div class="text-sm text-gray-700 space-y-3 bg-gray-50 p-4 rounded-lg">
<p><strong>证据热点:</strong> 研究证据主要集中在Drug-X糖化血红蛋白的影响上。</p>
<p><strong>证据空白:</strong>Drug-Y不良事件交叉领域缺少高质量研究。</p>
</div>
</aside>
<!-- Main Panel -->
<main class="w-3/4 flex flex-col space-y-6 overflow-y-auto">
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-xl font-bold mb-4">证据图谱</h3>
<div id="bubble-chart-container" class="border rounded-lg p-4"></div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-xl font-bold mb-4">描述性统计</h3>
<p class="text-sm text-gray-600">本次分析共纳入50项研究其中60% (30项) 为随机对照试验 (RCT)...</p>
</div>
</main>
</div>
</div>
<!-- 视图4: 报告编辑器 -->
<div id="report-view" class="hidden">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold">智能报告编辑器</h2>
<div>
<button onclick="showView('dashboard')" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded mr-2">返回仪表盘</button>
<button class="bg-sky-600 hover:bg-sky-700 text-white font-bold py-2 px-4 rounded">导出为 Word</button>
</div>
</div>
<div class="bg-white p-12 rounded-lg shadow prose max-w-none">
<h1>XX药物治疗XX疾病的证据图谱分析报告</h1>
<p class="text-gray-500">生成日期: 2025-10-22</p>
<h2>摘要</h2>
<p>本报告旨在通过证据图谱方法系统性梳理XX药物治疗XX疾病的现有研究证据。分析共纳入50项研究结果表明研究热点主要集中于... 同时,我们发现...领域存在显著的证据空白,提示这是未来需要重点投入的研究方向。</p>
<h2>方法</h2>
<p>我们遵循系统评价流程... 最终纳入50篇文献进行分析。证据图谱的干预措施维度包括... 结局指标维度包括...</p>
<h2>结果</h2>
<h3>纳入研究的描述性统计</h3>
<p>如图1所示本次分析共纳入50项研究。其中随机对照试验RCT是主要的研究类型占总数的60%30项其次是队列研究25%12项...</p>
<h3>证据图谱可视化与解读</h3>
<p>证据图谱图2显示研究证据主要集中在抗血小板药物主要心血管不良事件(MACE)’的影响上...</p>
<figure>
<div class="border rounded-lg p-4 bg-gray-50 text-center text-gray-500">[此处为证据图谱图片]</div>
<figcaption>图2: 证据图谱可视化</figcaption>
</figure>
<h4>证据空白分析</h4>
<p>图谱也揭示了显著的证据空白。特别是在‘新一代抗凝药’与‘出血风险’这一关键领域的直接对比研究非常稀少...</p>
<h2>结论</h2>
<p>...</p>
<h2>附录</h2>
<p>...</p>
</div>
</div>
</div>
</div>
</div>
<script>
// --- STATE MANAGEMENT ---
let currentView = 'hub';
let wizardStep = 1;
// --- DOM ELEMENTS ---
const views = {
hub: document.getElementById('hub-view'),
wizard: document.getElementById('wizard-view'),
dashboard: document.getElementById('dashboard-view'),
report: document.getElementById('report-view')
};
const headerTitle = document.getElementById('header-title');
// --- NAVIGATION ---
function showView(view) {
currentView = view;
Object.values(views).forEach(v => v.classList.add('hidden'));
views[view].classList.remove('hidden');
const titles = {
hub: '数据综合分析与报告生成',
wizard: '证据图谱配置向导',
dashboard: '交互式分析仪表盘',
report: '智能报告编辑器'
};
headerTitle.textContent = titles[view];
}
function startWizard() {
wizardStep = 1;
updateWizardView();
showView('wizard');
}
function navigateWizard(direction) {
wizardStep += direction;
updateWizardView();
}
function updateWizardView() {
document.querySelectorAll('.wizard-step').forEach(step => step.classList.remove('active'));
document.getElementById(`wizard-step-${wizardStep}`).classList.add('active');
document.getElementById('wizard-prev').style.visibility = wizardStep === 1 ? 'hidden' : 'visible';
document.getElementById('wizard-next').style.display = wizardStep === 3 ? 'none' : 'block';
document.getElementById('wizard-finish').style.display = wizardStep === 3 ? 'block' : 'none';
}
function finishWizard() {
renderBubbleChart();
showView('dashboard');
}
// --- DASHBOARD: BUBBLE CHART ---
function renderBubbleChart() {
const container = document.getElementById('bubble-chart-container');
const interventions = ['Drug-X', 'Drug-Y', '安慰剂'];
const outcomes = ['糖化血红蛋白', '不良事件', 'MACE'];
// Simulated data
const data = [
[15, 2, 8],
[5, 1, 3],
[12, 10, 11]
];
const quality = [
['green', 'red', 'yellow'],
['yellow', 'red', 'green'],
['green', 'green', 'yellow']
];
let tableHTML = `<table class="w-full border-collapse"><thead><tr><th class="border p-2"></th>`;
outcomes.forEach(o => tableHTML += `<th class="border p-2 text-sm font-semibold">${o}</th>`);
tableHTML += `</tr></thead><tbody>`;
interventions.forEach((inter, i) => {
tableHTML += `<tr><td class="border p-2 text-sm font-semibold text-right">${inter}</td>`;
outcomes.forEach((outc, j) => {
const count = data[i][j];
const size = count * 3 + 10; // Simple size mapping
const color = quality[i][j];
const colorClass = {green: 'bg-green-500', yellow: 'bg-yellow-400', red: 'bg-red-500'}[color];
tableHTML += `
<td class="border p-2 h-24 w-24 text-center bubble-chart-cell has-tooltip relative">
${count > 0 ? `
<div class="bubble w-12 h-12 rounded-full mx-auto flex items-center justify-center text-white font-bold ${colorClass}" style="width:${size}px; height:${size}px;">
${count}
</div>
<div class="tooltip absolute z-10 -mt-24 w-48 bg-gray-800 text-white text-xs rounded py-1 px-2 text-left">
<strong>${inter} vs ${outc}</strong><br>
研究数量: ${count}<br>
质量: ${color === 'green' ? '高' : '中'}
</div>` : ''}
</td>`;
});
tableHTML += `</tr>`;
});
tableHTML += `</tbody></table>`;
container.innerHTML = tableHTML;
}
// Initial view
showView('hub');
</script>
</body>
</html>