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
361 lines
29 KiB
HTML
361 lines
29 KiB
HTML
<!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>
|