feat(rvw,asl): RVW V3.0 smart review + ASL deep research history + stability

RVW module (V3.0 Smart Review Enhancement):
- Add LLM data validation via PromptService (RVW_DATA_VALIDATION)
- Add ClinicalAssessmentSkill with FINER-based evaluation (RVW_CLINICAL)
- Remove all numeric scores from UI (editorial, methodology, overall)
- Implement partial_completed status with Promise.allSettled
- Add error_details JSON field to ReviewTask for granular failure info
- Fix overallStatus logic: warning status now counts as success
- Restructure ForensicsReport: per-table LLM results, remove top-level block
- Refactor ClinicalReport: structured collapsible sections
- Increase all skill timeouts to 300s for long manuscripts (20+ pages)
- Increase DataForensics LLM timeout to 180s, pg-boss to 15min
- Executor default fallback timeout 30s -> 60s

ASL module:
- Add deep research history with sidebar accordion UI
- Implement waterfall flow for historical task display
- Upgrade Unifuncs DeepSearch API from S2 to S3 with fallback
- Add ASL_SR module seed for admin configurability
- Fix new search button inconsistency

Docs:
- Update RVW module status to V3.0
- Update deployment changelist
- Add 0305 deployment summary

DB Migration:
- Add error_details JSONB column to rvw_schema.review_tasks

Tested: All 4 review modules verified, partial completion working
Made-with: Cursor
This commit is contained in:
2026-03-07 19:24:21 +08:00
parent 91ae80888e
commit 87655ea7e6
46 changed files with 2929 additions and 511 deletions

View File

@@ -0,0 +1,541 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI智能文献平台 v6.0 (互斥手风琴版)</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
.fade-in { animation: fadeIn 0.3s ease-out forwards; }
.slide-in-from-bottom { animation: slideInFromBottom 0.3s ease-out forwards; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideInFromBottom { from { transform: translateY(1rem); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
/* 隐藏滚动条但保留滚动功能 */
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* * 核心黑科技:利用 CSS Grid 实现完美的柔性折叠动画
* grid-rows-[0fr] 会将高度压缩到 0
* grid-rows-[1fr] 会让高度填满 flex 分配的空间
*/
.accordion-grid {
transition: grid-template-rows 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.accordion-content {
overflow: hidden;
}
</style>
</head>
<body class="bg-slate-50 font-sans text-slate-900 overflow-hidden h-screen flex flex-col">
<!-- 全局顶部导航 -->
<header class="h-14 bg-slate-900 text-slate-300 flex items-center justify-between px-6 shadow-md z-30 shrink-0">
<div class="flex items-center space-x-8">
<div class="flex items-center space-x-2 text-white font-bold tracking-wide">
<div class="bg-blue-600 p-1 rounded"><i data-lucide="activity" class="w-4 h-4 text-white"></i></div>
<span>医学科研 AI 平台</span>
</div>
<nav class="flex space-x-1">
<a href="#" class="px-4 py-2 text-sm font-medium hover:text-white transition rounded-md">AI 问答</a>
<a href="#" class="px-4 py-2 text-sm font-medium text-white bg-slate-800 rounded-md shadow-inner flex items-center">
<i data-lucide="book-open" class="w-4 h-4 mr-2 text-blue-400"></i> AI 文献
</a>
<a href="#" class="px-4 py-2 text-sm font-medium hover:text-white transition rounded-md">数据清洗</a>
<a href="#" class="px-4 py-2 text-sm font-medium hover:text-white transition rounded-md">统计分析</a>
</nav>
</div>
<div class="flex items-center space-x-4">
<button class="text-slate-400 hover:text-white"><i data-lucide="bell" class="w-5 h-5"></i></button>
<div class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-white font-bold text-sm">Dr</div>
</div>
</header>
<div class="flex-1 flex overflow-hidden">
<!-- ============================================== -->
<!-- 核心重构 V6: 互斥手风琴侧边栏 (Mutually Exclusive Accordion) -->
<!-- ============================================== -->
<div class="w-72 bg-white border-r border-slate-200 flex flex-col z-20 shrink-0 shadow-sm relative">
<!-- 【面板 A】智能文献检索 -->
<div id="panel-search-wrapper" class="flex flex-col transition-all duration-400">
<!-- Header A -->
<button onclick="togglePanel('SEARCH')" class="flex items-center justify-between px-5 py-4 hover:bg-slate-50 transition-colors w-full group border-b border-transparent" id="header-search">
<div class="flex items-center text-slate-800 font-bold text-[14px] transition-colors" id="title-search">
<i data-lucide="sparkles" class="w-4 h-4 mr-2.5 text-indigo-500" id="icon-search"></i>
智能文献检索
</div>
<i data-lucide="chevron-down" id="chevron-search" class="w-4 h-4 text-slate-400 transition-transform duration-400"></i>
</button>
<!-- Content A (利用 Grid 实现流畅折叠) -->
<div id="grid-search" class="grid accordion-grid grid-rows-[1fr]">
<div class="accordion-content flex flex-col min-h-0 bg-white">
<div class="p-4 pb-2 pt-0 shrink-0">
<button onclick="createNewSearch()" class="w-full bg-indigo-600 text-white hover:bg-indigo-700 py-2.5 rounded-lg flex items-center justify-center text-sm font-semibold transition-colors shadow-sm shadow-indigo-200">
<i data-lucide="plus" class="w-4 h-4 mr-1.5"></i> 新建智能检索
</button>
</div>
<div class="px-4 py-2 flex items-center justify-between text-xs font-bold text-slate-400 uppercase tracking-wider shrink-0">
<span>最近检索历史</span>
<i data-lucide="history" class="w-3.5 h-3.5"></i>
</div>
<!-- List A -->
<div class="flex-1 overflow-y-auto px-3 pb-3 space-y-1 no-scrollbar" id="search-list-container">
<!-- 由 JS 渲染 -->
</div>
</div>
</div>
</div>
<!-- 始终存在的分割线 -->
<div class="h-px bg-slate-200 shrink-0 z-10 shadow-sm"></div>
<!-- 【面板 B】系统综述项目 -->
<div id="panel-project-wrapper" class="flex flex-col transition-all duration-400">
<!-- Header B -->
<button onclick="togglePanel('PROJECT')" class="flex items-center justify-between px-5 py-4 hover:bg-slate-50 transition-colors w-full group bg-slate-50/50" id="header-project">
<div class="flex items-center text-slate-700 font-bold text-[14px] transition-colors" id="title-project">
<i data-lucide="folder-kanban" class="w-4 h-4 mr-2.5 text-slate-500" id="icon-project"></i>
系统综述项目 (SR)
</div>
<i data-lucide="chevron-up" id="chevron-project" class="w-4 h-4 text-slate-400 transition-transform duration-400"></i>
</button>
<!-- Content B -->
<div id="grid-project" class="grid accordion-grid grid-rows-[0fr]">
<div class="accordion-content flex flex-col min-h-0 bg-slate-50">
<div class="px-4 py-2 flex items-center justify-between text-xs font-bold text-slate-400 uppercase tracking-wider shrink-0 mt-1">
<span>我的工作区</span>
<i data-lucide="layout-grid" class="w-3.5 h-3.5"></i>
</div>
<!-- List B -->
<div class="flex-1 overflow-y-auto px-3 pb-2 space-y-1 no-scrollbar" id="project-list-container">
<!-- 由 JS 渲染项目列表 -->
</div>
<!-- 弱化设计的新建项目按钮 -->
<div class="px-4 pb-4 shrink-0">
<button onclick="createNewProject()" class="w-full border border-dashed border-slate-300 text-slate-500 hover:text-blue-600 hover:border-blue-300 hover:bg-blue-50 py-2 rounded-lg flex items-center justify-center text-[13px] font-medium transition-colors bg-white">
<i data-lucide="plus" class="w-3.5 h-3.5 mr-1.5"></i> 创建新 SR 项目
</button>
</div>
</div>
</div>
</div>
<!-- 侧边栏底部弹簧垫片,把剩余空间挤上去 -->
<div class="flex-1 bg-slate-50"></div>
<!-- 底部配额信息 (固定在最底) -->
<div class="p-4 border-t border-slate-200 bg-white shrink-0">
<div class="text-[10px] text-slate-500 mb-1.5 font-medium flex justify-between uppercase tracking-wider">
<span>DeepSearch 配额</span>
<span>45k/100k</span>
</div>
<div class="w-full bg-slate-100 rounded-full h-1.5 overflow-hidden">
<div class="bg-indigo-400 h-full rounded-full" style="width: 45%"></div>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="flex-1 flex flex-col overflow-hidden bg-slate-50 relative" id="main-content-container">
<!-- 由 JS 渲染核心界面 -->
</div>
</div>
<script>
// ==========================================
// 1. 数据模型与状态 (Data Models & State)
// ==========================================
let searchHistories = [
{ id: 1, title: 'SGLT2 抑制剂对糖尿病的心血管影响', date: '今天', hasResult: true },
{ id: 2, title: 'PD-1 联合化疗在非小细胞肺癌中的疗效', date: '昨天', hasResult: true },
{ id: 3, title: 'Aspirin 一级预防最新 RCT', date: '前天', hasResult: true },
{ id: 4, title: '阿兹海默症的最新靶向治疗方案综述', date: '本周', hasResult: true },
{ id: 5, title: '肠道微生态与抑郁症发病机制的相关性研究', date: '本周', hasResult: true },
{ id: 6, title: 'COVID-19 长新冠症状的系统性回顾', date: '更早', hasResult: true },
{ id: 7, title: '间歇性禁食对代谢综合征的影响', date: '更早', hasResult: true },
{ id: 8, title: 'CAR-T 细胞疗法在实体瘤中的进展', date: '更早', hasResult: true }
];
let srProjects = [
{ id: 1, title: 'SGLT2 抑制剂 Meta 分析', phase: 4, status: '进行中', count: 42, tiabConflictsResolved: true, metaRun: false },
{ id: 2, title: '二甲双胍对认知功能的系统综述', phase: 2, status: '初筛中', count: 856, tiabConflictsResolved: false, metaRun: false },
{ id: 3, title: 'CAR-T 治疗严重不良事件汇总', phase: 6, status: '已闭环', count: 18, tiabConflictsResolved: true, metaRun: true }
];
const PHASES = [
{ id: 1, name: '检索', icon: 'search' },
{ id: 2, name: '初筛', icon: 'file-text' },
{ id: 3, name: '复筛', icon: 'file-text' },
{ id: 4, name: '提取/RoB', icon: 'database' },
{ id: 5, name: 'Meta', icon: 'activity' },
{ id: 6, name: '报告', icon: 'file-output' }
];
// 核心全局状态 V6.0
const state = {
expandedPanel: 'SEARCH', // 核心变量:控制当前哪个抽屉打开 ('SEARCH' 或 'PROJECT')
activeContext: 'SEARCH', // 核心变量:控制右侧主屏显示什么内容
currentSearchId: 1,
currentProjectId: null,
};
// ==========================================
// 2. 状态变更函数 (Actions)
// ==========================================
// ⭐ 手风琴核心逻辑
function togglePanel(panelName) {
if (state.expandedPanel !== panelName) {
state.expandedPanel = panelName;
// 联动:当用户点开某个抽屉时,如果右侧内容不属于该抽屉,自动切换右侧内容
if (panelName === 'SEARCH' && state.activeContext !== 'SEARCH') {
state.activeContext = 'SEARCH';
if (!state.currentSearchId && searchHistories.length > 0) state.currentSearchId = searchHistories[0].id;
} else if (panelName === 'PROJECT' && state.activeContext !== 'PROJECT') {
state.activeContext = 'PROJECT';
if (!state.currentProjectId && srProjects.length > 0) state.currentProjectId = srProjects[0].id;
}
renderApp();
}
}
function selectSearch(id) {
state.activeContext = 'SEARCH';
state.currentSearchId = id;
if (state.expandedPanel !== 'SEARCH') state.expandedPanel = 'SEARCH';
renderApp();
}
function selectProject(id) {
state.activeContext = 'PROJECT';
state.currentProjectId = id;
if (state.expandedPanel !== 'PROJECT') state.expandedPanel = 'PROJECT';
renderApp();
}
function createNewSearch() {
state.activeContext = 'SEARCH';
state.currentSearchId = 'new';
renderApp();
}
function createNewProject() {
state.activeContext = 'PROJECT';
state.currentProjectId = 'new';
renderApp();
}
// ⭐ 转化逻辑:从左上角直接飞入左下角
function convertToProject() {
const sourceSearch = searchHistories.find(s => s.id === state.currentSearchId);
const newProject = {
id: Date.now(),
title: `${sourceSearch ? sourceSearch.title : '未命名'} (AI 转化)`,
phase: 2,
status: '初筛中',
count: 1250,
tiabConflictsResolved: false,
metaRun: false
};
srProjects.unshift(newProject);
// 切换上下文并强制展开下方抽屉,收起上方抽屉
state.activeContext = 'PROJECT';
state.currentProjectId = newProject.id;
state.expandedPanel = 'PROJECT';
renderApp();
}
// 项目流程控制函数 (同前)
function resolveProjectConflicts(projectId) {
const p = srProjects.find(p => p.id === projectId);
if(p) p.tiabConflictsResolved = true;
renderApp();
}
function advanceProjectPhase(projectId, targetPhase) {
const p = srProjects.find(p => p.id === projectId);
if(p) { p.phase = targetPhase; p.status = targetPhase === 6 ? '已闭环' : '进行中'; }
renderApp();
}
function runProjectMeta(projectId) {
const p = srProjects.find(p => p.id === projectId);
if(p) p.metaRun = true;
renderApp();
}
// ==========================================
// 3. 视图渲染逻辑 (Views)
// ==========================================
function renderSidebar() {
const isSearchExpanded = state.expandedPanel === 'SEARCH';
// 1. 处理 CSS Grid 动画折叠
const gridSearch = document.getElementById('grid-search');
const gridProject = document.getElementById('grid-project');
const panelSearchWrap = document.getElementById('panel-search-wrapper');
const panelProjectWrap = document.getElementById('panel-project-wrapper');
if (isSearchExpanded) {
// 展开上方,收起下方
gridSearch.classList.replace('grid-rows-[0fr]', 'grid-rows-[1fr]');
gridProject.classList.replace('grid-rows-[1fr]', 'grid-rows-[0fr]');
panelSearchWrap.classList.replace('shrink-0', 'flex-1');
panelSearchWrap.classList.replace('min-h-0', 'min-h-0'); // keep
panelProjectWrap.classList.replace('flex-1', 'shrink-0');
} else {
// 收起上方,展开下方
gridSearch.classList.replace('grid-rows-[1fr]', 'grid-rows-[0fr]');
gridProject.classList.replace('grid-rows-[0fr]', 'grid-rows-[1fr]');
panelSearchWrap.classList.replace('flex-1', 'shrink-0');
panelProjectWrap.classList.replace('shrink-0', 'flex-1');
panelProjectWrap.classList.replace('min-h-0', 'min-h-0'); // keep
}
// 2. 处理 Header 的图标旋转与高亮
const chevSearch = document.getElementById('chevron-search');
const chevProject = document.getElementById('chevron-project');
const titleSearch = document.getElementById('title-search');
const titleProject = document.getElementById('title-project');
const iconSearch = document.getElementById('icon-search');
const iconProject = document.getElementById('icon-project');
if (isSearchExpanded) {
chevSearch.classList.add('rotate-180');
chevProject.classList.remove('rotate-180');
titleSearch.classList.add('text-indigo-700'); titleSearch.classList.remove('text-slate-800');
iconSearch.classList.replace('text-slate-400', 'text-indigo-500');
titleProject.classList.replace('text-blue-700', 'text-slate-700');
iconProject.classList.replace('text-blue-500', 'text-slate-500');
} else {
chevSearch.classList.remove('rotate-180');
chevProject.classList.add('rotate-180');
titleSearch.classList.remove('text-indigo-700'); titleSearch.classList.add('text-slate-800');
iconSearch.classList.replace('text-indigo-500', 'text-slate-400');
titleProject.classList.replace('text-slate-700', 'text-blue-700');
iconProject.classList.replace('text-slate-500', 'text-blue-500');
}
// 3. 渲染列表内部项目
const searchContainer = document.getElementById('search-list-container');
searchContainer.innerHTML = searchHistories.map(h => {
const isActive = (state.activeContext === 'SEARCH' && state.currentSearchId === h.id);
return `
<div onclick="selectSearch(${h.id})" class="cursor-pointer group flex items-start p-2.5 rounded-lg transition-colors ${isActive ? 'bg-indigo-50 border border-indigo-100 shadow-sm' : 'hover:bg-slate-100 border border-transparent'}">
<i data-lucide="message-square" class="w-4 h-4 mt-0.5 mr-2.5 shrink-0 ${isActive ? 'text-indigo-600' : 'text-slate-400 group-hover:text-slate-600'}"></i>
<div class="flex-1 overflow-hidden">
<h4 class="text-sm font-medium truncate ${isActive ? 'text-indigo-900' : 'text-slate-700'}">${h.title}</h4>
<span class="text-[10px] text-slate-400 mt-0.5 block">${h.date}</span>
</div>
</div>
`;
}).join('');
const projectContainer = document.getElementById('project-list-container');
projectContainer.innerHTML = srProjects.map(p => {
const isActive = (state.activeContext === 'PROJECT' && state.currentProjectId === p.id);
const statusColor = p.status === '已闭环' ? 'emerald' : p.status === '初筛中' ? 'amber' : 'blue';
return `
<div onclick="selectProject(${p.id})" class="cursor-pointer group flex items-start p-2.5 mb-1.5 rounded-lg transition-colors ${isActive ? 'bg-white border border-blue-200 shadow-sm ring-1 ring-blue-50' : 'hover:bg-white border border-transparent'}">
<i data-lucide="folder" class="w-4 h-4 mt-0.5 mr-2.5 shrink-0 ${isActive ? 'text-blue-600' : 'text-slate-400 group-hover:text-blue-500'}"></i>
<div class="flex-1 overflow-hidden">
<h4 class="text-[13px] font-bold truncate ${isActive ? 'text-blue-900' : 'text-slate-600'}">${p.title}</h4>
<div class="flex items-center mt-1.5 space-x-2">
<span class="text-[10px] font-bold text-${statusColor}-700 bg-${statusColor}-100 px-1.5 rounded">${p.status}</span>
<span class="text-[10px] font-mono text-slate-500">P${p.phase}</span>
</div>
</div>
</div>
`;
}).join('');
}
// 主内容区渲染 (与 V5 逻辑一致)
function renderMainContent() {
const container = document.getElementById('main-content-container');
if (state.activeContext === 'SEARCH') {
if (state.currentSearchId === 'new') {
container.innerHTML = `
<div class="flex-1 flex flex-col items-center justify-center p-8 fade-in h-full bg-white">
<div class="w-16 h-16 bg-indigo-50 border border-indigo-100 rounded-2xl flex items-center justify-center mb-6 shadow-sm">
<i data-lucide="sparkles" class="w-8 h-8 text-indigo-500"></i>
</div>
<h2 class="text-2xl font-bold text-slate-800 mb-2">探索医学前沿</h2>
<p class="text-slate-500 mb-8 max-w-md text-center text-sm">输入您的研究问题或 PICOS 关键词DeepSearch 将为您生成高质量综述报告与文献库。</p>
<div class="w-full max-w-2xl bg-white p-2 rounded-xl border border-slate-300 shadow-sm flex items-center focus-within:ring-2 ring-indigo-500/50 transition-all focus-within:border-indigo-500">
<input type="text" class="flex-1 outline-none px-4 py-3 text-slate-700 placeholder-slate-400 bg-transparent" placeholder="例如SGLT2 抑制剂对 2 型糖尿病的心血管结局影响...">
<button class="bg-indigo-600 hover:bg-indigo-700 text-white p-3 rounded-lg shadow-sm transition">
<i data-lucide="arrow-up" class="w-5 h-5"></i>
</button>
</div>
</div>
`;
} else {
const searchInfo = searchHistories.find(s => s.id === state.currentSearchId) || searchHistories[0];
container.innerHTML = `
<div class="flex flex-col h-full fade-in bg-white">
<header class="h-14 bg-white border-b border-slate-100 flex items-center px-6 shrink-0">
<h1 class="text-sm font-bold text-slate-700 truncate">${searchInfo.title}</h1>
<span class="ml-3 text-[10px] font-medium text-slate-500 bg-slate-100 px-2 py-1 rounded-md border border-slate-200 flex items-center"><i data-lucide="layers" class="w-3 h-3 mr-1"></i>1250 篇</span>
</header>
<div class="flex-1 overflow-y-auto p-6 md:p-8 space-y-8 pb-32">
<div class="max-w-4xl mx-auto flex space-x-4">
<div class="w-8 h-8 rounded-full bg-slate-200 flex items-center justify-center shrink-0 mt-1"><span class="text-slate-600 text-xs font-bold">Dr</span></div>
<div class="flex-1"><div class="inline-block bg-slate-100 px-5 py-3.5 rounded-2xl rounded-tl-none text-slate-800 text-sm shadow-sm">帮我查一下相关文献要求是近5年的英文随机对照试验(RCT)。</div></div>
</div>
<div class="max-w-4xl mx-auto flex space-x-4">
<div class="w-8 h-8 rounded-full bg-indigo-600 flex items-center justify-center shrink-0 mt-1 shadow-md shadow-indigo-200"><i data-lucide="bot" class="w-4 h-4 text-white"></i></div>
<div class="flex-1">
<div class="bg-white border border-slate-200 rounded-2xl p-6 shadow-sm">
<h4 class="font-bold text-slate-800 flex items-center mb-3">
<i data-lucide="check-circle-2" class="w-5 h-5 text-emerald-500 mr-2"></i> 智能检索完成:获取有效文献 1,250 篇
</h4>
<p class="text-sm text-slate-600 leading-relaxed mb-5">
基于您的查询,我从 PubMed 和 Cochrane Library 汇总了最新高质量研究。主要发现包括:目标干预不仅能有效改善主要结局指标,还在多个大型试验中显著降低了不良事件风险。
</p>
<div class="mt-4 p-5 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 rounded-xl flex items-center justify-between">
<div>
<h5 class="font-bold text-blue-900 text-sm flex items-center"><i data-lucide="workflow" class="w-4 h-4 mr-1.5 text-blue-600"></i> 需要进行严格的系统综述吗?</h5>
<p class="text-xs text-blue-700 mt-1.5 opacity-80">将这 1,250 篇文献转入流水线,即可启动自动化双盲初筛。</p>
</div>
<button onclick="convertToProject()" class="bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium py-2 px-5 rounded-lg shadow-sm shadow-blue-200 transition flex items-center shrink-0">
转入 SR 项目流水线 <i data-lucide="arrow-right" class="w-4 h-4 ml-1.5"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-white via-white to-transparent p-6 pb-8">
<div class="max-w-4xl mx-auto bg-white p-1.5 rounded-xl border border-slate-300 shadow-lg shadow-slate-200/50 flex items-center">
<input type="text" class="flex-1 outline-none px-4 py-2 text-sm text-slate-700 bg-transparent" placeholder="追问检索结果,或修改条件...">
<button class="bg-indigo-600 text-white p-2.5 rounded-lg shadow-sm"><i data-lucide="arrow-up" class="w-4 h-4"></i></button>
</div>
</div>
</div>
`;
}
} else if (state.activeContext === 'PROJECT') {
if (state.currentProjectId === 'new') {
container.innerHTML = `
<div class="flex-1 flex flex-col items-center justify-center p-8 fade-in h-full bg-slate-50">
<div class="w-16 h-16 bg-blue-100 rounded-2xl flex items-center justify-center mb-6 shadow-sm border border-blue-200">
<i data-lucide="folder-plus" class="w-8 h-8 text-blue-600"></i>
</div>
<h2 class="text-2xl font-bold text-slate-800 mb-2">创建系统综述项目 (SR)</h2>
<p class="text-slate-500 mb-8 max-w-md text-center text-sm">建立严谨的瀑布流科研管线。支持通过 DeepSearch 抓取或本地导入文献库。</p>
<div class="w-full max-w-lg bg-white p-7 rounded-2xl border border-slate-200 shadow-sm space-y-5">
<div>
<label class="block text-sm font-bold text-slate-700 mb-2">项目名称</label>
<input type="text" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 outline-none transition-all bg-slate-50 hover:bg-white" placeholder="例如:某某药物的疗效与安全性 Meta 分析">
</div>
<div class="pt-4 flex justify-end space-x-3 border-t border-slate-100">
<button class="px-5 py-2.5 text-sm font-medium text-slate-600 hover:bg-slate-100 rounded-lg transition-colors">取消</button>
<button class="px-5 py-2.5 text-sm font-bold text-white bg-blue-600 hover:bg-blue-700 rounded-lg shadow-sm shadow-blue-200 transition-colors">创建并配置方案</button>
</div>
</div>
</div>
`;
} else {
const project = srProjects.find(p => p.id === state.currentProjectId) || srProjects[0];
container.innerHTML = `
<div class="flex flex-col h-full fade-in bg-slate-50/50">
<header class="h-14 bg-white border-b border-slate-200 flex items-center justify-between px-6 shadow-sm shrink-0">
<div class="flex items-center">
<div class="bg-blue-100 p-1 rounded mr-3"><i data-lucide="folder-kanban" class="w-4 h-4 text-blue-600"></i></div>
<h1 class="text-sm font-bold text-slate-800">${project.title}</h1>
</div>
<span class="text-[11px] font-bold text-slate-500 bg-slate-100 px-2.5 py-1 rounded-md border border-slate-200 tracking-wide uppercase">Workspace</span>
</header>
<!-- 瀑布流步进器 Stepper -->
<div class="bg-white border-b border-slate-200 px-8 py-3 shadow-sm shrink-0 overflow-x-auto">
<div class="flex items-center justify-between min-w-max">
${PHASES.map((phase, index) => {
const isActive = project.phase === phase.id;
const isPast = project.phase > phase.id;
const circleClasses = isActive ? 'border-blue-600 bg-blue-50 text-blue-600 shadow-sm ring-2 ring-blue-100'
: isPast ? 'border-emerald-500 bg-emerald-500 text-white'
: 'border-slate-300 bg-slate-50 text-slate-400';
const textClasses = isActive ? 'text-blue-800 font-bold' : isPast ? 'text-slate-700' : 'text-slate-400 font-medium';
const lineClasses = isPast ? 'bg-emerald-500' : 'bg-slate-200';
return `
<div class="flex items-center cursor-pointer group" onclick="advanceProjectPhase(${project.id}, ${phase.id})">
<div class="flex items-center justify-center w-7 h-7 rounded-full border-2 transition-all ${circleClasses}">
${isPast ? `<i data-lucide="check" class="w-3.5 h-3.5"></i>` : `<i data-lucide="${phase.icon}" class="w-3 h-3"></i>`}
</div>
<span class="ml-2.5 text-[13px] transition-colors ${textClasses} group-hover:text-blue-600">${phase.name}</span>
${index < PHASES.length - 1 ? `<div class="w-8 h-[2px] mx-4 transition-colors ${lineClasses}"></div>` : ''}
</div>
`;
}).join('')}
</div>
</div>
<main class="flex-1 overflow-y-auto p-8" id="sr-step-content">
${renderProjectPhaseContent(project)}
</main>
</div>
`;
}
}
}
function renderProjectPhaseContent(project) {
const SectionHeader = (title, desc) => `
<div class="mb-6">
<h2 class="text-xl font-bold text-slate-800">${title}</h2>
<p class="text-sm text-slate-500 mt-1.5">${desc}</p>
</div>
`;
switch(project.phase) {
case 1: return `<div class="max-w-4xl mx-auto fade-in">${SectionHeader("Step 1: 选题与检索数据源", "准备阶段...")}<div class="bg-white p-10 rounded-2xl border border-slate-200 text-center shadow-sm"><button onclick="advanceProjectPhase(${project.id}, 2)" class="bg-blue-600 text-white px-5 py-2.5 rounded-lg text-sm font-medium hover:bg-blue-700 shadow-sm">模拟导入完成,进入初筛</button></div></div>`;
case 2:
let conflictUI = !project.tiabConflictsResolved
? `<div class="bg-amber-50 border border-amber-200 rounded-xl p-6 mb-6"><h4 class="font-bold text-amber-800 text-sm flex items-center mb-2"><i data-lucide="alert-triangle" class="w-4 h-4 mr-2"></i> 必须解决冲突</h4><div class="bg-white rounded-lg border border-amber-100 p-4 flex justify-between items-center shadow-sm"><span class="text-sm font-medium text-slate-700">冲突记录: Example Paper 2024</span><button onclick="resolveProjectConflicts(${project.id})" class="text-xs font-bold bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 shadow-sm">一键解决</button></div></div>`
: `<div class="bg-emerald-50 border border-emerald-200 rounded-xl p-6 mb-6 flex items-center"><i data-lucide="check-circle" class="w-6 h-6 text-emerald-500 mr-3"></i><div><h4 class="font-bold text-emerald-800 text-sm">初筛已完成</h4></div></div>`;
let nxtBtn = !project.tiabConflictsResolved
? `<button disabled class="px-6 py-2.5 rounded-lg bg-slate-200 text-slate-400 text-sm font-medium cursor-not-allowed">进入复筛</button>`
: `<button onclick="advanceProjectPhase(${project.id}, 3)" class="bg-blue-600 text-white px-6 py-2.5 rounded-lg text-sm font-bold shadow-sm hover:bg-blue-700">进入复筛</button>`;
return `<div class="max-w-4xl mx-auto fade-in">${SectionHeader("Step 2: 标题摘要初筛", "双盲初筛流水线。")}${conflictUI}<div class="flex justify-end">${nxtBtn}</div></div>`;
case 3:
case 4:
case 5:
let btnText = project.phase === 3 ? '进入数据提取' : project.phase === 4 ? '进入Meta分析' : '生成最终报告';
return `<div class="max-w-4xl mx-auto fade-in">${SectionHeader(`Step ${project.phase}: ${PHASES[project.phase-1].name}`, "专业操作区。")}<div class="bg-white p-16 text-center rounded-2xl border border-slate-200 mb-6 shadow-sm"><i data-lucide="${PHASES[project.phase-1].icon}" class="w-12 h-12 text-slate-300 mx-auto mb-4"></i><p class="text-slate-500 text-sm font-medium">这里是 ${PHASES[project.phase-1].name} 的详细工作台区域。</p></div><div class="flex justify-end"><button onclick="advanceProjectPhase(${project.id}, ${project.phase + 1})" class="bg-slate-800 text-white px-6 py-2.5 rounded-lg text-sm font-bold flex items-center shadow-sm hover:bg-slate-700">${btnText} <i data-lucide="arrow-right" class="w-4 h-4 ml-2"></i></button></div></div>`;
case 6:
return `<div class="max-w-4xl mx-auto fade-in">${SectionHeader("Step 6: 报告生成", "项目成果。")}<div class="bg-white border border-slate-200 rounded-2xl p-12 flex flex-col items-center text-center shadow-sm"><div class="bg-emerald-50 p-4 rounded-full border border-emerald-100 mb-4"><i data-lucide="check-circle" class="w-12 h-12 text-emerald-500"></i></div><h2 class="text-2xl font-black text-slate-800">流程闭环完成</h2><button class="mt-8 bg-blue-600 text-white px-6 py-3 rounded-xl shadow-md text-sm font-bold flex items-center hover:bg-blue-700 transition"><i data-lucide="download" class="w-4 h-4 mr-2"></i> 下载 PRISMA & Word 报告</button></div></div>`;
}
}
// ==========================================
// 4. 初始化引擎
// ==========================================
function renderApp() {
renderSidebar();
renderMainContent();
lucide.createIcons();
}
document.addEventListener('DOMContentLoaded', () => {
renderApp();
});
</script>
</body>
</html>