Files
HaHafeng 87655ea7e6 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
2026-03-07 19:24:21 +08:00

541 lines
37 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>