- Add one-click research protocol generation with streaming output - Implement Word document export via Pandoc integration - Add dynamic dual-panel layout with resizable split pane - Implement collapsible content for StatePanel stages - Add conversation history management with title auto-update - Fix scroll behavior, markdown rendering, and UI layout issues - Simplify conversation creation logic for reliability
277 lines
16 KiB
HTML
277 lines
16 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>Protocol Agent V3.0 - 生成全流程</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script src="https://unpkg.com/lucide@latest"></script>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Noto+Sans+SC:wght@300;400;500;700&family=Noto+Serif+SC:wght@400;700&display=swap');
|
||
|
||
body { font-family: 'Inter', 'Noto Sans SC', sans-serif; }
|
||
|
||
/* 聊天气泡 */
|
||
.chat-bubble-ai { background-color: #F3F4F6; border-radius: 12px 12px 12px 2px; }
|
||
.chat-bubble-user { background-color: #4F46E5; color: white; border-radius: 12px 12px 2px 12px; }
|
||
|
||
/* A4 纸张效果 (预览模式) */
|
||
.a4-paper {
|
||
width: 100%;
|
||
max-width: 210mm;
|
||
min-height: 297mm;
|
||
padding: 20mm;
|
||
background: white;
|
||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||
margin: 0 auto;
|
||
font-family: 'Noto Serif SC', serif; /* 宋体风格 */
|
||
color: #1F2937;
|
||
line-height: 1.8;
|
||
font-size: 15px;
|
||
}
|
||
|
||
/* 打字机光标 */
|
||
.typing-cursor::after { content: '❘'; animation: blink 1s infinite; color: #4F46E5; }
|
||
@keyframes blink { 50% { opacity: 0; } }
|
||
|
||
/* 右侧面板切换动画 */
|
||
.panel-transition { transition: opacity 0.3s ease, transform 0.3s ease; }
|
||
.panel-hidden { opacity: 0; transform: translateX(20px); pointer-events: none; position: absolute; top:0; left:0; right:0; }
|
||
.panel-visible { opacity: 1; transform: translateX(0); position: relative; }
|
||
</style>
|
||
</head>
|
||
<body class="bg-gray-100 h-screen flex flex-col overflow-hidden">
|
||
|
||
<!-- Header -->
|
||
<header class="bg-white border-b border-gray-200 h-14 flex items-center justify-between px-4 shadow-sm z-20 shrink-0">
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center text-white font-bold">
|
||
<i data-lucide="bot" class="w-5 h-5"></i>
|
||
</div>
|
||
<div>
|
||
<h1 class="font-bold text-gray-800 text-sm">Protocol Agent</h1>
|
||
<div class="flex items-center gap-1.5">
|
||
<span class="w-2 h-2 bg-green-500 rounded-full"></span>
|
||
<span class="text-[10px] text-gray-500">PICO Ready</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab Switcher (用于演示) -->
|
||
<div class="flex bg-gray-100 p-1 rounded-lg">
|
||
<button id="btn-view-context" class="px-3 py-1 text-xs font-medium bg-white shadow-sm rounded text-gray-700">视图: 关键要素</button>
|
||
<button id="btn-view-doc" class="px-3 py-1 text-xs font-medium text-gray-500 hover:text-gray-700">视图: 完整方案</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="flex-1 flex overflow-hidden">
|
||
|
||
<!-- Left: Chat (35%) -->
|
||
<section class="w-[400px] flex flex-col bg-white border-r border-gray-200 z-10 shadow-lg">
|
||
<div id="chat-box" class="flex-1 overflow-y-auto p-4 space-y-6 bg-slate-50">
|
||
|
||
<!-- History -->
|
||
<div class="flex gap-3">
|
||
<div class="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center shrink-0"><i data-lucide="bot" class="w-4 h-4 text-indigo-600"></i></div>
|
||
<div class="chat-bubble-ai p-3 text-sm text-gray-700 shadow-sm">
|
||
样本量计算已完成 (N=386)。至此,您的研究方案 <strong>5 个关键要素</strong> 已全部收集完毕。
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 核心:生成按钮 Action Card -->
|
||
<div class="flex justify-center" id="action-area">
|
||
<div class="bg-white border-2 border-indigo-100 rounded-xl p-4 shadow-md w-full max-w-sm hover:border-indigo-300 transition-all cursor-default">
|
||
<div class="flex items-center gap-3 mb-3">
|
||
<div class="bg-indigo-600 text-white p-2 rounded-lg"><i data-lucide="sparkles" class="w-5 h-5"></i></div>
|
||
<div>
|
||
<h3 class="font-bold text-gray-800 text-sm">要素已就绪</h3>
|
||
<p class="text-xs text-gray-500">基于已确认的 PICO 与样本量</p>
|
||
</div>
|
||
</div>
|
||
<button onclick="startGeneration()" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-bold py-2.5 rounded-lg shadow-sm flex items-center justify-center gap-2 transition-transform active:scale-95">
|
||
开始撰写完整方案
|
||
<i data-lucide="arrow-right" class="w-4 h-4"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 生成中的状态 (初始隐藏) -->
|
||
<div id="generating-msg" class="hidden flex gap-3 animate-fade-in">
|
||
<div class="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center shrink-0"><i data-lucide="pen-tool" class="w-4 h-4 text-indigo-600 animate-pulse"></i></div>
|
||
<div class="chat-bubble-ai p-3 text-sm text-gray-700 shadow-sm w-full">
|
||
<p class="font-bold text-indigo-600 mb-2 text-xs">正在撰写...</p>
|
||
<div class="space-y-2">
|
||
<div class="flex items-center justify-between text-xs">
|
||
<span class="text-gray-600">1. 研究背景</span>
|
||
<i data-lucide="check-circle" class="w-3 h-3 text-green-500"></i>
|
||
</div>
|
||
<div class="flex items-center justify-between text-xs">
|
||
<span class="text-gray-600">2. 研究目的</span>
|
||
<i data-lucide="check-circle" class="w-3 h-3 text-green-500"></i>
|
||
</div>
|
||
<div class="flex items-center justify-between text-xs">
|
||
<span class="text-indigo-600 font-medium">3. 研究设计</span>
|
||
<i data-lucide="loader-2" class="w-3 h-3 text-indigo-500 animate-spin"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Input -->
|
||
<div class="p-3 border-t border-gray-200 bg-white">
|
||
<div class="relative">
|
||
<input type="text" placeholder="对生成结果不满意?输入修改指令..." class="w-full pl-4 pr-10 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||
<button class="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-indigo-600"><i data-lucide="send" class="w-4 h-4"></i></button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Right: Dual Panel (Context vs Document) -->
|
||
<section class="flex-1 bg-slate-100 relative overflow-hidden">
|
||
|
||
<!-- Panel 1: Context View (结构化数据) -->
|
||
<div id="panel-context" class="absolute inset-0 p-8 overflow-y-auto panel-visible panel-transition">
|
||
<div class="max-w-3xl mx-auto">
|
||
<div class="flex items-center justify-between mb-6">
|
||
<h2 class="text-lg font-bold text-gray-800 flex items-center gap-2">
|
||
<i data-lucide="database" class="w-5 h-5 text-gray-500"></i>
|
||
关键要素 (Context Data)
|
||
</h2>
|
||
<span class="text-xs bg-white border border-gray-200 px-3 py-1 rounded-full text-gray-500">数据源: Postgres JSONB</span>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 gap-4">
|
||
<!-- PICO Card -->
|
||
<div class="bg-white p-5 rounded-xl border border-gray-200 shadow-sm">
|
||
<div class="text-xs font-bold text-gray-400 uppercase mb-3">PICO 模型</div>
|
||
<div class="space-y-3">
|
||
<div class="flex"><span class="w-8 font-bold text-blue-600">P</span><span class="text-sm text-gray-700">≥65岁原发性高血压患者</span></div>
|
||
<div class="flex"><span class="w-8 font-bold text-indigo-600">I</span><span class="text-sm text-gray-700">阿司匹林肠溶片 100mg/d</span></div>
|
||
<div class="flex"><span class="w-8 font-bold text-orange-600">C</span><span class="text-sm text-gray-700">安慰剂</span></div>
|
||
<div class="flex"><span class="w-8 font-bold text-green-600">O</span><span class="text-sm text-gray-700">5年内缺血性脑卒中发生率</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sample Size Card -->
|
||
<div class="bg-white p-5 rounded-xl border border-gray-200 shadow-sm flex items-center justify-between">
|
||
<div>
|
||
<div class="text-xs font-bold text-gray-400 uppercase mb-1">样本量估算</div>
|
||
<div class="text-sm text-gray-600">Alpha=0.05, Power=0.8</div>
|
||
</div>
|
||
<div class="text-2xl font-bold text-indigo-600 font-mono">N=386</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-8 text-center text-sm text-gray-400">
|
||
<i data-lucide="arrow-left" class="w-4 h-4 inline mr-1"></i>
|
||
点击左侧 "开始撰写" 将基于以上数据生成文档
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Panel 2: Document View (A4 Preview) -->
|
||
<div id="panel-doc" class="absolute inset-0 p-8 overflow-y-auto panel-hidden panel-transition bg-gray-200/50">
|
||
<!-- Toolbar -->
|
||
<div class="absolute top-4 right-8 flex gap-2 z-10">
|
||
<button class="bg-white hover:bg-gray-50 text-gray-700 px-3 py-1.5 rounded border border-gray-300 text-xs font-medium shadow-sm flex items-center gap-1">
|
||
<i data-lucide="copy" class="w-3 h-3"></i> 复制
|
||
</button>
|
||
<button class="bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1.5 rounded border border-transparent text-xs font-medium shadow-sm flex items-center gap-1">
|
||
<i data-lucide="download" class="w-3 h-3"></i> 导出 Word
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Paper -->
|
||
<div class="a4-paper">
|
||
<h1 class="text-2xl font-bold text-center mb-10 pt-4">阿司匹林预防老年高血压患者缺血性脑卒中的<br>前瞻性、随机、双盲对照研究方案</h1>
|
||
|
||
<div class="space-y-6">
|
||
<div>
|
||
<h2 class="text-lg font-bold mb-2 border-b border-gray-800 pb-1">1. 研究背景 (Background)</h2>
|
||
<p class="text-justify indent-8">
|
||
脑卒中是全球范围内导致死亡和长期残疾的主要原因之一。流行病学数据显示,在65岁以上的老年人群中,高血压是缺血性脑卒中最重要的独立危险因素...
|
||
</p>
|
||
</div>
|
||
|
||
<div>
|
||
<h2 class="text-lg font-bold mb-2 border-b border-gray-800 pb-1">2. 研究目的 (Objectives)</h2>
|
||
<p class="text-justify indent-8">
|
||
<strong>主要目的:</strong> 评价每日口服 100mg 阿司匹林肠溶片对比安慰剂,在降低 65 岁及以上原发性高血压患者 5 年内缺血性脑卒中发生率方面的有效性。
|
||
</p>
|
||
<p class="text-justify indent-8 mt-2">
|
||
<strong>次要目的:</strong> 评估阿司匹林组与安慰剂组在全因死亡率、出血性脑卒中发生率及主要出血事件(如消化道出血)方面的差异。
|
||
</p>
|
||
</div>
|
||
|
||
<div>
|
||
<h2 class="text-lg font-bold mb-2 border-b border-gray-800 pb-1">3. 研究设计 (Study Design)</h2>
|
||
<p class="text-justify indent-8 typing-cursor">
|
||
本研究采用多中心、随机、双盲、安慰剂对照设计 (RCT)。入组后的受试者将通过中央随机系统以 1:1 的比例分配至试验组(阿司匹林)或对照组(安慰剂)。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h-20"></div> <!-- Bottom Spacer -->
|
||
</div>
|
||
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<script>
|
||
lucide.createIcons();
|
||
|
||
const panelContext = document.getElementById('panel-context');
|
||
const panelDoc = document.getElementById('panel-doc');
|
||
const btnViewContext = document.getElementById('btn-view-context');
|
||
const btnViewDoc = document.getElementById('btn-view-doc');
|
||
const actionArea = document.getElementById('action-area');
|
||
const generatingMsg = document.getElementById('generating-msg');
|
||
const chatBox = document.getElementById('chat-box');
|
||
|
||
function switchTab(view) {
|
||
if (view === 'context') {
|
||
panelContext.classList.remove('panel-hidden');
|
||
panelContext.classList.add('panel-visible');
|
||
panelDoc.classList.remove('panel-visible');
|
||
panelDoc.classList.add('panel-hidden');
|
||
|
||
btnViewContext.classList.add('bg-white', 'shadow-sm', 'text-gray-700');
|
||
btnViewContext.classList.remove('text-gray-500');
|
||
btnViewDoc.classList.remove('bg-white', 'shadow-sm', 'text-gray-700');
|
||
btnViewDoc.classList.add('text-gray-500');
|
||
} else {
|
||
panelContext.classList.remove('panel-visible');
|
||
panelContext.classList.add('panel-hidden');
|
||
panelDoc.classList.remove('panel-hidden');
|
||
panelDoc.classList.add('panel-visible');
|
||
|
||
btnViewDoc.classList.add('bg-white', 'shadow-sm', 'text-gray-700');
|
||
btnViewDoc.classList.remove('text-gray-500');
|
||
btnViewContext.classList.remove('bg-white', 'shadow-sm', 'text-gray-700');
|
||
btnViewContext.classList.add('text-gray-500');
|
||
}
|
||
}
|
||
|
||
function startGeneration() {
|
||
// 1. 切换到文档视图
|
||
switchTab('doc');
|
||
|
||
// 2. Chat UI 更新
|
||
actionArea.style.display = 'none'; // 隐藏按钮
|
||
generatingMsg.classList.remove('hidden'); // 显示进度条
|
||
|
||
// 滚动到底部
|
||
chatBox.scrollTo({ top: chatBox.scrollHeight, behavior: 'smooth' });
|
||
}
|
||
|
||
// 绑定 Tab 点击事件
|
||
btnViewContext.onclick = () => switchTab('context');
|
||
btnViewDoc.onclick = () => switchTab('doc');
|
||
|
||
</script>
|
||
</body>
|
||
</html> |