feat(ssa): Complete V11 UI development and frontend-backend integration - Pixel-perfect V11 UI, multi-task support, Word export, input overlay fix, code cleanup. MVP Phase 1 core 95% complete.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-20 14:46:45 +08:00
parent 49b5c37cb1
commit 8d496d1515
38 changed files with 7255 additions and 1074 deletions

View File

@@ -0,0 +1,102 @@
# **SSA-Pro 智能工作台 V7 vs V8 深度对比与技术选型报告**
**分析对象:** V7 (单流沉浸式 \- 方案A) vs V8 (双屏工作台 \- 方案C)
**编写目的:** 为前端研发团队提供 UI/UX 选型依据与技术实现路径指导。
**核心结论:** **强烈推荐采用 V8双屏版** 作为 SSA-Pro 的最终形态,并**深度复用 AIA 模块已有的双屏组件资产**。
## **1\. 核心观点与倾向性建议**
**我个人的强烈倾向是:选择 V8双屏版**
### **为什么 V8 更符合用户需求?**
1. **科研场景的天然属性**:医生做统计分析,不是为了“聊天”,而是为了“要结果(图/表/代码”。V7 会把复杂的统计图表挤在狭窄的聊天气泡里,阅读体验极差;而 V8 给了结果最大的展示空间(右侧 60% 屏幕)。
2. **多线程工作能力**:在 V8 中,用户可以看着右侧的“分析计划书 (SAP)”,在左侧继续对 AI 提出修改意见(“把正态性检验去掉”)。而在 V7 中,一旦继续聊天,计划书就被顶到上面看不见了。
3. **专业信任感**V8 的布局极具“现代桌面软件”的既视感(类似于 RStudio、VS Code 或 Notion这能极大地提升医生对该系统“严谨性”的信任。
## **2\. 体验与需求契合度对比 (UX Comparison)**
| 维度 | V7 (单流沉浸式 \- 类似 ChatGPT) | V8 (双屏工作台 \- 类似 Claude Artifacts) | 胜出者 |
| :---- | :---- | :---- | :---- |
| **空间利用率** | 低。大量屏幕两侧留白,宽表必须横向滚动。 | **极高**。完美适配 1080p 以上宽屏,图表展示极其舒展。 | 🏆 V8 |
| **上下文记忆** | 差。旧卡片会被新消息顶出屏幕。 | **极佳**。右侧工作区状态独立,左侧聊天随便滚,右侧不动。 | 🏆 V8 |
| **代码/文档阅读** | 差。需弹窗或在狭窄气泡内阅读。 | **极佳**。右侧面板就是完美的文档/代码阅读器。 | 🏆 V8 |
| **移动端适配** | **极佳**。天然的移动端流式布局。 | 差。双屏在手机上无法并排显示。 | 🏆 V7 |
| **学习成本** | 极低(所有人都会用微信)。 | 较低(需理解左右协同逻辑)。 | 🏆 V7 |
*结论:除非我们首发主打手机端(这在医学统计场景几乎不可能),否则 PC 端的 V8 完胜。*
## **3\. 实现难度与技术路径差异 (Implementation Complexity)**
最初评估时V8 的代价是前端开发难度直线上升。**但鉴于本平台 AIA 模块Protocol Agent已成功交付类似架构V8 的实际落地成本已大幅降低。**
### **3.1 状态管理 (State Management) 的差异**
* **V7 (简单)**:状态是局部的、线性的。整个页面就是一个 MessageList\[\] 数组。每个 AI 回复的卡片组件,只需要接收自己那一条的 props 即可渲染,组件之间不需要通信。
* **V8 (复杂但已攻克)**:状态是全局的、分离的。左侧聊天框里的点击动作必须触发右侧面板的状态切换(跨面板状态同步)。目前可直接复用平台已有的状态管理最佳实践。
### **3.2 渲染机制 (Rendering Logic) 的差异**
* **V7 (流式)**:新消息永远 push 到列表最下方,浏览器自动处理滚动。
* **V8 (插槽式)**右侧是一个动态插槽Dynamic Slot / View。需要维护一个 ActiveArtifact 状态,根据左侧的焦点,不断替换右侧挂载的 React 组件(是渲染 SAPViewer 还是 ResultViewer
## **4\. V8 架构所需的核心技术栈与路线 (Tech Stack Support)**
要完美实现 V8前端团队必须引入以下技术路线
### **4.1 全局状态管理库 (必须)**
* **推荐**Zustand 或 Redux Toolkit。
* **用途**:定义一个全局 Store包含
interface WorkspaceStore {
chatHistory: Message\[\]; // 控制左侧
activePane: 'empty' | 'sap' | 'execution' | 'result'; // 控制右侧视图
currentArtifactData: any; // 右侧正在展示的数据 (JSON)
setPane: (pane, data) \=\> void; // 左侧按钮调用的方法
}
### **4.2 布局伸缩库 (推荐与复用)**
* **推荐**:直接复用 AIA 模块已实现的 ResizableSplitPane 组件。
* **用途**允许用户拖拽中间的分割线自由调整左侧Chat和右侧Workspace的宽度比例这是高级桌面工具的标配
### **4.3 渲染优化技术 (关键)**
* **推荐**React.memo / useMemo。
* **用途**:隔离左右两侧的渲染。当左侧用户正在打字时,绝不能导致右侧包含几千个 DOM 节点的复杂数据表格发生不必要的重渲染Re-render否则输入会严重卡顿。
## **5\. 跨模块资产复用分析AIA Protocol Agent 带来的架构红利**
通过审查 00-模块当前状态与开发指南.md我们发现 **Protocol Agent 的 V3.1 MVP 已经完美实现了“聊天+文档”的双屏形态**。这为 SSA-Pro 的 V8 方案提供了极其宝贵的基础设施。
### **5.1 Protocol Agent 布局与 SSA V8 布局的对比度**
| 核心特性 | Protocol Agent (AIA) 现有实现 | SSA-Pro (V8) 需求期望 | 一致性评估 |
| :---- | :---- | :---- | :---- |
| **整体架构** | 左侧 Chat \+ 右侧 DocumentPanel (A4纸张预览) | 左侧 Chat \+ 右侧 Artifacts (图表/表格/代码) | 🟢 **高度一致**。同属 "Left Brain, Right Hand" 模式。 |
| **面板控制** | ResizableSplitPane 动态双面板布局,可拖拽调整 | 需要中间可拖拽的分割线调整视窗大小 | 🟢 **完全吻合**。可 100% 照搬该组件。 |
| **状态联动** | 5阶段流程状态面板折叠/展开 | 计划/执行/结果等阶段状态栏的切换 | 🟡 **逻辑相似**。基于 Zustand 的联动机制可复用。 |
| **结果导出** | Word 导出服务集成 \+ 一键下载 | 代码下载 \+ 报告导出 | 🟢 **直接复用**。Python 的 pypandoc 导出服务可直接为 SSA 的 SAP 文档导出服务。 |
### **5.2 复用策略与哪个“更好”?**
**结论:它们不是互斥的选项,而是“前辈”与“后继者”的关系。复用 Protocol Agent 的架构来实现 V8 是最优解。**
1. **底层 Layout 无缝复用**:前端团队**无需从零开发**左右分屏和拖拽逻辑,直接将 Protocol Agent 中经过 V3.1 验证的 ResizableSplitPane 提取到 frontend-v2/src/shared/components/Layout 供全局使用。
2. **右侧面板 (Right Pane) 的多态化扩展**
* Protocol Agent 的右侧是 DocumentPanel渲染 Markdown 和 A4 纸效果)。
* SSA-Pro V8 只需要在此基础上扩展右侧面板的内容类型:除了渲染 SAP 文档,新增 ChartPanel渲染 Base64 图表)和 TablePanel渲染三线表即可。
3. **消除技术债务与风险**:原本 V8 最大的风险在于“复杂的交互状态管理和窗口重绘性能”,既然 AIA 团队已经在 \~8500 行代码的迭代中解决了滚动条显示、状态联动等 BugSSA 团队直接站在巨人的肩膀上,**将原本需要 2 周的基建时间压缩至 2 天**。
## **6\. 最终开发实施建议**
1. **确立 V8 为唯一方向**:全面转向左右双屏架构,放弃气泡内展示复杂图表的念头。
2. **组件下沉与复用 (Day 1 任务)**:前端负责同学第一步应与 AIA 团队对齐,将 ResizableSplitPane 等双屏布局基建下沉为系统级 Common 组件。
3. **组件解耦开发**
* 同学 A 专门负责开发右侧的 ResultTable、PlotViewer、ExecutionTree 组件(只需接收 JSON props 即可,纯展示组件)。
* 同学 B 复用 AIStreamChat 组件负责左侧的指令解析。
* 最后通过全局 Store 将两人拼装在一起。
**总结V8 难在起步的架构搭建,但由于 Protocol Agent 已经替我们蹚平了道路,我们获得了巨大的“后发优势”。实施 V8 架构不仅风险极低,而且能保证整个 AI Clinical Research 平台(从写方案到做统计)的 UI 体验高度统一、专业感拉满。**

View File

@@ -0,0 +1,600 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSA-Pro 智能统计工作台 V11.0 (V9 Visuals + V10 Features)</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; background-color: #f8fafc; color: #334155; overflow: hidden; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
.fade-in { animation: fadeIn 0.4s ease-out forwards; opacity: 0; }
.slide-up { animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; opacity: 0; transform: translateY(15px); }
.pop-in { animation: popIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards; opacity: 0; transform: scale(0.95); }
@keyframes fadeIn { to { opacity: 1; } }
@keyframes slideUp { to { transform: translateY(0); opacity: 1; } }
@keyframes popIn { to { transform: scale(1); opacity: 1; } }
/* 科学表格样式 (V9 风格) */
.sci-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.sci-table th { border-top: 2px solid #1e293b; border-bottom: 1px solid #1e293b; padding: 10px 12px; text-align: left; font-weight: 600; color: #1e293b; }
.sci-table td { border-bottom: 1px solid #e2e8f0; padding: 10px 12px; color: #475569; }
.sci-table tr:last-child td { border-bottom: 2px solid #1e293b; }
.status-dot { height: 8px; width: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
.status-success { background-color: #22c55e; box-shadow: 0 0 0 2px #dcfce7; }
pre code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.85rem; line-height: 1.5; }
/* Artifact 阴影 (V9 风格) */
.artifact-shadow { box-shadow: 0 4px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); }
/* 模态框背景模糊 */
.modal-backdrop { backdrop-filter: blur(4px); }
</style>
</head>
<body class="h-screen flex text-slate-800">
<!-- 全局 Toast 容器 -->
<div id="toast-container" class="fixed top-6 left-1/2 transform -translate-x-1/2 z-50 flex flex-col gap-2 pointer-events-none"></div>
<!-- ========================================== -->
<!-- 左侧Gemini 交互 + V9 视觉的抽屉侧边栏 -->
<!-- ========================================== -->
<aside id="sidebar" class="w-16 bg-slate-50 border-r border-slate-200 flex flex-col z-30 flex-shrink-0 transition-all duration-300 ease-in-out relative">
<!-- 顶部:汉堡菜单与 Logo -->
<div class="h-16 flex items-center px-3 flex-shrink-0 overflow-hidden whitespace-nowrap">
<button onclick="toggleSidebar()" class="w-10 h-10 rounded-lg hover:bg-slate-200 flex items-center justify-center text-slate-600 transition flex-shrink-0">
<i class="fa-solid fa-bars text-lg"></i>
</button>
<div class="sidebar-text hidden ml-3 flex items-center gap-2 fade-in">
<div class="w-7 h-7 bg-slate-800 rounded flex items-center justify-center text-white shadow-sm">
<i class="fa-solid fa-chart-simple text-xs"></i>
</div>
<span class="font-bold text-slate-800 tracking-tight text-lg">SSA-Pro</span>
</div>
</div>
<!-- 核心操作区 -->
<div class="px-3 mt-4 flex flex-col gap-3">
<!-- 新建按钮 -->
<button id="new-chat-btn" class="w-10 h-10 bg-white border border-slate-200 hover:border-blue-300 shadow-sm rounded-lg flex items-center justify-center text-slate-700 transition-all duration-300 overflow-hidden whitespace-nowrap group">
<i class="fa-solid fa-plus text-blue-600"></i>
<span class="sidebar-text hidden ml-3 font-medium text-sm text-slate-700 group-hover:text-blue-600 transition">新建分析</span>
</button>
<!-- 收起时显示的历史图标 -->
<button id="history-icon-only" onclick="toggleSidebar()" class="w-10 h-10 rounded-lg hover:bg-slate-200 flex items-center justify-center text-slate-500 transition-all duration-300" title="查看历史记录">
<i class="fa-solid fa-clock-rotate-left"></i>
</button>
</div>
<!-- 历史记录列表 (展开时显示) -->
<div id="history-list" class="flex-1 overflow-y-auto px-3 mt-6 hidden opacity-0 transition-opacity duration-300 pb-4">
<div class="px-2 py-1 text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2">近期分析</div>
<div class="space-y-1">
<button class="w-full text-left px-3 py-2.5 rounded-lg bg-white border border-slate-200 text-blue-700 font-medium text-sm truncate flex items-center gap-3 shadow-sm">
<i class="fa-regular fa-message text-blue-500"></i> 心血管药物研究
</button>
<button class="w-full text-left px-3 py-2.5 rounded-lg hover:bg-slate-200 text-slate-600 text-sm truncate flex items-center gap-3 transition">
<i class="fa-regular fa-message text-slate-400"></i> 生存分析方案咨询
</button>
<button class="w-full text-left px-3 py-2.5 rounded-lg hover:bg-slate-200 text-slate-600 text-sm truncate flex items-center gap-3 transition">
<i class="fa-regular fa-message text-slate-400"></i> 基线特征 Table 1
</button>
</div>
</div>
<!-- 底部设置图标 -->
<div class="mt-auto p-3 flex-shrink-0 border-t border-slate-200">
<button id="settings-btn" class="w-10 h-10 rounded-lg hover:bg-slate-200 flex items-center justify-center text-slate-500 transition-all duration-300 overflow-hidden whitespace-nowrap">
<i class="fa-solid fa-gear"></i>
<span class="sidebar-text hidden ml-3 font-medium text-sm">配置中心</span>
</button>
</div>
</aside>
<!-- ========================================== -->
<!-- 动态主容器 (Chat + Workspace) -->
<!-- ========================================== -->
<div class="flex-1 flex overflow-hidden relative bg-white" id="main-container">
<!-- ========================================== -->
<!-- 左半部分 (或全屏):对话区 (Chat Pane) -->
<!-- 采用了 V10 的居中布局V9 的视觉卡片 -->
<!-- ========================================== -->
<section id="chat-pane" class="flex-1 flex flex-col h-full transition-all duration-500 ease-in-out relative bg-[#f8fafc]">
<!-- Chat Header (V9 风格) -->
<header class="h-16 flex items-center justify-between px-6 z-10 border-b border-slate-100 bg-white/90 backdrop-blur flex-shrink-0 absolute top-0 left-0 right-0">
<div class="font-semibold text-slate-800 text-lg">心血管药物疗效研究</div>
<div class="flex items-center gap-2 text-xs font-medium text-slate-500 bg-slate-50 px-3 py-1.5 rounded-full border border-slate-100 shadow-sm">
<span class="status-dot status-success animate-pulse"></span> R Engine Ready
</div>
</header>
<!-- Chat Messages (居中对齐) -->
<div id="chat-container" class="flex-1 overflow-y-auto px-4 md:px-8 pt-24 flex flex-col items-center pb-6">
<!-- 内部定宽容器,保证阅读体验 -->
<div class="w-full max-w-3xl space-y-8 min-h-full pb-32">
<!-- 欢迎语 (V9 风格) -->
<div class="flex gap-4 slide-up">
<div class="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center flex-shrink-0 mt-0.5"><i class="fa-solid fa-robot text-white text-xs"></i></div>
<div class="bg-white text-slate-700 p-4 rounded-2xl rounded-tl-none text-sm leading-relaxed border border-slate-200 shadow-sm">
你好!我是 SSA-Pro 智能统计助手。<br>你可以直接描述研究目标,我将为你生成分析方案;或者点击下方 📎 <b>上传数据文件</b>,我们将直接开始分析。
</div>
</div>
<!-- User Msg (V9 风格) -->
<div class="flex gap-4 flex-row-reverse slide-up" style="animation-delay: 0.1s">
<div class="w-8 h-8 rounded-full bg-slate-200 flex items-center justify-center flex-shrink-0 mt-0.5"><i class="fa-solid fa-user text-slate-500 text-xs"></i></div>
<div class="bg-blue-600 text-white p-4 rounded-2xl rounded-tr-none text-sm leading-relaxed shadow-md">
我在研究一种新药,收集了实验组和对照组患者的血压下降值数据。我想知道新药是否有效。
</div>
</div>
<!-- AI Msg (无数据纯咨询) -->
<div class="flex gap-4 slide-up" style="animation-delay: 0.2s">
<div class="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center flex-shrink-0 mt-0.5"><i class="fa-solid fa-robot text-white text-xs"></i></div>
<div class="bg-white border border-slate-200 text-slate-700 p-4 rounded-2xl rounded-tl-none text-sm leading-relaxed shadow-sm">
理解了。针对“两组独立样本疗效对比”,如果是连续数值型变量,通常推荐使用 <b>独立样本 T 检验 (T-Test)</b><br><br>
💡 <b>请上传数据文件</b>,我将根据实际列名生成可执行的方案并展示在右侧工作区。
</div>
</div>
<div id="chat-anchor"></div>
</div>
</div>
<!-- Chat Input (V9 质感的悬浮输入框) -->
<div class="flex-shrink-0 w-full flex justify-center pb-6 px-4 absolute bottom-0 bg-gradient-to-t from-[#f8fafc] via-[#f8fafc] to-transparent pt-12 z-20">
<div class="w-full max-w-3xl relative">
<!-- V9 风格:包裹挂载区和输入框的容器 -->
<div class="shadow-lg rounded-2xl bg-white border border-slate-200 focus-within:border-blue-400 focus-within:ring-4 focus-within:ring-blue-50 transition-all flex flex-col p-2">
<!-- 数据挂载区 (嵌入式) -->
<div id="data-mount-zone" class="hidden items-center mb-1 pl-1 pop-in w-full">
<div class="bg-slate-50 border border-slate-200 rounded-lg px-2 py-1.5 flex items-center gap-3 w-max transition-all hover:border-blue-300">
<div class="w-8 h-8 rounded bg-green-50 flex items-center justify-center border border-green-100">
<i class="fa-solid fa-file-csv text-green-600 text-sm"></i>
</div>
<div class="flex flex-col pr-2">
<span class="text-xs font-bold text-slate-700">BP_Trial_Data.csv</span>
<span class="text-[10px] text-slate-400 mt-0.5 leading-none">120 rows • 45KB</span>
</div>
<div class="h-6 w-px bg-slate-200 mx-1"></div>
<button onclick="removeData(event)" class="text-slate-400 hover:text-red-500 transition px-1" title="移除数据">
<i class="fa-solid fa-xmark text-sm"></i>
</button>
</div>
</div>
<!-- 文本输入行 -->
<div class="relative flex items-end w-full">
<button onclick="simulateUpload()" id="btn-upload" class="absolute left-1 bottom-1 text-slate-400 hover:text-blue-600 transition w-9 h-9 flex items-center justify-center rounded-lg hover:bg-slate-100 z-10" title="上传数据">
<i class="fa-solid fa-paperclip text-lg"></i>
</button>
<textarea class="w-full bg-transparent py-2.5 pl-11 pr-12 focus:outline-none resize-none text-sm text-slate-700 placeholder-slate-400" rows="1" placeholder="发送消息,或点击回形针 📎 上传数据触发分析..." style="min-height: 44px;"></textarea>
<button class="absolute right-1 bottom-1 w-9 h-9 bg-slate-800 text-white rounded-lg hover:bg-slate-700 transition flex items-center justify-center shadow-sm z-10">
<i class="fa-solid fa-arrow-up text-sm"></i>
</button>
</div>
</div>
<div class="text-center mt-2">
<span class="text-[10px] text-slate-400">AI 可能会犯错,请核实生成的统计结论。</span>
</div>
</div>
</div>
</section>
<!-- ========================================== -->
<!-- 右半部分:动态工作区 (Workspace Pane) - 保持 V9 完美体验 -->
<!-- ========================================== -->
<section id="workspace-pane" class="w-0 overflow-hidden bg-slate-50 transition-all duration-500 ease-in-out flex flex-col flex-shrink-0 border-l border-transparent relative">
<div class="w-full min-w-[600px] h-full flex flex-col absolute inset-0">
<!-- 顶部工具栏 -->
<header class="h-14 flex items-center justify-between px-4 border-b border-slate-200 bg-slate-100/50 flex-shrink-0">
<div class="flex items-center gap-2">
<span id="workspace-badge" class="px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-blue-100 text-blue-700 border border-blue-200">
Planning
</span>
<span id="workspace-title" class="text-sm font-semibold text-slate-700">统计分析计划 (SAP)</span>
</div>
<div class="flex gap-2">
<button id="btn-code" class="hidden w-8 h-8 bg-white border border-slate-200 text-slate-500 rounded-lg hover:text-blue-600 transition shadow-sm flex items-center justify-center" title="查看源代码" onclick="openModal('code-modal')">
<i class="fa-brands fa-r-project"></i>
</button>
<button onclick="closeWorkspace()" class="w-8 h-8 bg-white border border-slate-200 text-slate-500 rounded-lg hover:bg-slate-100 hover:text-slate-800 transition shadow-sm flex items-center justify-center" title="收起工作区">
<i class="fa-solid fa-right-to-bracket"></i>
</button>
</div>
</header>
<!-- 工作区主画布 -->
<div class="flex-1 p-4 md:p-6 overflow-y-auto">
<div class="bg-white rounded-2xl artifact-shadow border border-slate-200/60 min-h-full flex flex-col relative overflow-hidden">
<!-- 视图 1SAP 文档 -->
<div id="view-sap" class="p-8 w-full fade-in">
<h1 class="text-xl font-bold text-slate-900 border-b border-slate-200 pb-4 mb-6">研究课题:新药治疗高血压疗效对比</h1>
<div class="space-y-6">
<section>
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">1. 推荐统计方法</h3>
<div class="border border-blue-100 rounded-xl overflow-hidden">
<div class="bg-blue-50 px-4 py-3 border-b border-blue-100 font-medium text-blue-800 text-sm flex items-center gap-2">
<i class="fa-solid fa-star text-amber-500"></i> 首选:独立样本 T 检验 (Independent T-Test)
</div>
<div class="p-4 text-sm text-slate-700 bg-white grid grid-cols-2 gap-4">
<div class="bg-slate-50 p-3 rounded-lg border border-slate-100">
<div class="text-xs text-slate-400 mb-1">自变量 (X)</div>
<div class="font-mono bg-slate-200/50 px-1 rounded inline-block">Group</div> <span class="text-xs text-slate-500 ml-1">(分类)</span>
</div>
<div class="bg-slate-50 p-3 rounded-lg border border-slate-100">
<div class="text-xs text-slate-400 mb-1">因变量 (Y)</div>
<div class="font-mono bg-slate-200/50 px-1 rounded inline-block">BP_Change</div> <span class="text-xs text-slate-500 ml-1">(数值)</span>
</div>
</div>
</div>
</section>
<section>
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">2. 统计护栏与执行策略</h3>
<ul class="text-sm text-slate-700 space-y-3 pl-1">
<li class="flex items-start gap-2 bg-slate-50 p-3 rounded-lg border border-slate-100">
<i class="fa-solid fa-shield text-blue-500 mt-1"></i>
<div>
<b>正态性假设检验 (Shapiro-Wilk)</b><br>
<span class="text-xs text-slate-500 mt-1 block">系统将在核心计算前执行检查。若 P < 0.05将触发降级策略自动改用 <b>Wilcoxon 秩和检验</b> 以保证结果严谨性。</span>
</div>
</li>
</ul>
</section>
<!-- 执行按钮区 -->
<div class="mt-8 pt-6 border-t border-slate-100 flex justify-end">
<button onclick="runAnalysis(this)" id="btn-run" class="bg-blue-600 text-white px-6 py-2.5 rounded-lg font-medium hover:bg-blue-700 transition shadow-md flex items-center gap-2">
<i class="fa-solid fa-play text-xs"></i> 开始执行分析
</button>
</div>
</div>
</div>
<!-- 视图 2动态执行日志 -->
<div id="view-execution" class="absolute inset-0 bg-white p-8 hidden flex-col items-center justify-center">
<div class="w-full max-w-lg">
<div class="flex items-center gap-3 mb-6 justify-center">
<i class="fa-solid fa-circle-notch fa-spin text-blue-600 text-2xl"></i>
<h3 class="font-bold text-slate-800 text-lg">正在调用云端 R 引擎...</h3>
</div>
<!-- 酷炫的终端样式 -->
<div class="bg-[#0f172a] rounded-xl p-5 font-mono text-xs text-slate-300 shadow-2xl relative overflow-hidden h-64 border border-slate-700">
<div class="absolute left-[31px] top-6 bottom-6 w-px bg-slate-700"></div>
<div id="trace-logs" class="space-y-4 relative z-10">
<!-- 动态插入日志 -->
</div>
</div>
</div>
</div>
<!-- 视图 3结果报告 -->
<div id="view-result" class="p-8 w-full hidden">
<!-- 核心结论 -->
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 p-5 rounded-xl mb-8 flex gap-4 items-start shadow-sm">
<i class="fa-solid fa-lightbulb text-blue-500 text-xl mt-0.5"></i>
<div>
<h4 class="font-bold text-slate-800 text-sm mb-1">AI 统计解读</h4>
<p class="text-sm text-slate-700 leading-relaxed">
结果表明新药组Drug的血压下降幅度显著大于对照组Placebo。差异具有统计学意义 (<span class="text-red-600 font-bold">P < 0.001</span>)。
<br><span class="text-xs text-slate-500 mt-1 inline-block">* 注:因数据未通过正态性检验,本结果由自动降级后的 Wilcoxon 秩和检验得出。</span>
</p>
</div>
</div>
<!-- 表格 -->
<div class="mb-8">
<h4 class="text-xs font-bold text-slate-500 uppercase tracking-wider mb-3">Table 1. 血压下降值比较</h4>
<div class="border border-slate-200 rounded-lg overflow-hidden">
<table class="sci-table bg-white">
<thead>
<tr class="bg-slate-50">
<th>组别</th>
<th>N</th>
<th>下降值 (Median [IQR])</th>
<th>W 统计量</th>
<th>P 值</th>
</tr>
</thead>
<tbody>
<tr class="hover:bg-slate-50">
<td>Drug</td>
<td>60</td>
<td>14.5 [12.1 - 16.8]</td>
<td rowspan="2" class="align-middle border-l border-slate-100">2845.5</td>
<td rowspan="2" class="align-middle border-l border-slate-100 text-red-600 font-bold">< 0.001 **</td>
</tr>
<tr class="hover:bg-slate-50">
<td>Placebo</td>
<td>60</td>
<td>8.2 [6.5 - 10.4]</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 图表 -->
<div>
<h4 class="text-xs font-bold text-slate-500 uppercase tracking-wider mb-3">Figure 1. 分布可视化</h4>
<div class="border border-slate-200 rounded-xl p-6 bg-slate-50 flex justify-center shadow-sm">
<div class="relative w-full max-w-sm h-56 flex items-end justify-center gap-16 pb-8 border-b-2 border-l-2 border-slate-400">
<div class="w-16 h-40 bg-blue-500/80 border-2 border-blue-600 rounded-sm shadow-sm relative"><span class="absolute -bottom-6 w-full text-center text-xs font-bold text-slate-600">Drug</span></div>
<div class="w-16 h-20 bg-slate-300/80 border-2 border-slate-400 rounded-sm shadow-sm relative"><span class="absolute -bottom-6 w-full text-center text-xs font-bold text-slate-600">Placebo</span></div>
<div class="absolute top-6 left-[30%] right-[30%] h-3 border-t-2 border-l-2 border-r-2 border-slate-800"></div>
<div class="absolute top-0 w-full text-center text-sm font-bold text-slate-800">P < 0.001</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
<!-- R代码模态框 (保留 V9 风格) -->
<div id="code-modal" class="fixed inset-0 z-50 hidden flex items-center justify-center">
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" onclick="closeModal('code-modal')"></div>
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-3xl max-h-[80vh] flex flex-col relative z-10 pop-in m-4 border border-slate-200">
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-100 bg-slate-50 rounded-t-2xl">
<h3 class="font-bold text-slate-800 flex items-center gap-2">
<i class="fa-brands fa-r-project text-blue-600 text-xl"></i> R 源代码交付
</h3>
<button onclick="closeModal('code-modal')" class="text-slate-400 hover:text-slate-700 w-8 h-8 rounded-full hover:bg-slate-200 transition flex items-center justify-center">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="p-6 overflow-y-auto flex-1 bg-white">
<pre class="bg-slate-900 text-slate-50 p-5 rounded-xl overflow-x-auto shadow-inner text-xs font-mono leading-relaxed"><code><span class="text-slate-500"># ------------------------------------------------</span>
<span class="text-slate-500"># SSA-Pro 生成代码: 独立样本差异分析</span>
<span class="text-slate-500"># ------------------------------------------------</span>
<span class="text-pink-400">library</span>(ggplot2)
<span class="text-slate-500"># 1. 加载数据</span>
df <- <span class="text-blue-300">read.csv</span>(<span class="text-green-300">"BP_Trial_Data.csv"</span>)
<span class="text-slate-500"># 2. 核心计算 (自动降级至 Wilcoxon)</span>
res <- <span class="text-blue-300">wilcox.test</span>(BP_Change ~ Group, data = df)
<span class="text-blue-300">print</span>(res)</code></pre>
</div>
<div class="px-6 py-4 border-t border-slate-100 flex justify-end gap-3 bg-slate-50 rounded-b-2xl">
<button class="px-4 py-2 text-sm font-medium text-white bg-slate-800 rounded-lg hover:bg-slate-700 transition shadow-sm" onclick="showToast('R 脚本已下载', 'success'); closeModal('code-modal');">
<i class="fa-solid fa-download mr-1"></i> 下载 .R 文件
</button>
</div>
</div>
</div>
<script>
const sidebar = document.getElementById('sidebar');
const chatContainer = document.getElementById('chat-container');
const anchor = document.getElementById('chat-anchor');
const workspacePane = document.getElementById('workspace-pane');
let isSidebarExpanded = false;
// 1. 抽屉菜单切换
function toggleSidebar() {
const texts = document.querySelectorAll('.sidebar-text');
const historyList = document.getElementById('history-list');
const newChatBtn = document.getElementById('new-chat-btn');
const historyIconBtn = document.getElementById('history-icon-only');
const settingsBtn = document.getElementById('settings-btn');
isSidebarExpanded = !isSidebarExpanded;
if(isSidebarExpanded) {
// 展开 w-64
sidebar.classList.replace('w-16', 'w-64');
texts.forEach(el => el.classList.remove('hidden'));
newChatBtn.classList.replace('w-10', 'w-full');
newChatBtn.classList.replace('justify-center', 'justify-start');
newChatBtn.classList.add('px-3');
settingsBtn.classList.replace('w-10', 'w-full');
settingsBtn.classList.replace('justify-center', 'justify-start');
settingsBtn.classList.add('px-3');
historyIconBtn.classList.add('hidden');
historyList.classList.remove('hidden');
setTimeout(() => historyList.classList.replace('opacity-0', 'opacity-100'), 150);
} else {
// 收缩 w-16
sidebar.classList.replace('w-64', 'w-16');
texts.forEach(el => el.classList.add('hidden'));
newChatBtn.classList.replace('w-full', 'w-10');
newChatBtn.classList.replace('justify-start', 'justify-center');
newChatBtn.classList.remove('px-3');
settingsBtn.classList.replace('w-full', 'w-10');
settingsBtn.classList.replace('justify-start', 'justify-center');
settingsBtn.classList.remove('px-3');
historyList.classList.add('hidden');
historyList.classList.replace('opacity-100', 'opacity-0');
historyIconBtn.classList.remove('hidden');
}
}
// 2. 工作区拉出与收起
function openWorkspace() {
workspacePane.classList.remove('w-0', 'border-transparent');
workspacePane.classList.add('w-[60%]', 'border-slate-200');
setTimeout(scrollToBottom, 300);
}
function closeWorkspace() {
workspacePane.classList.remove('w-[60%]', 'border-slate-200');
workspacePane.classList.add('w-0', 'border-transparent');
}
// 3. 模拟上传与执行交互
function simulateUpload() {
const mountZone = document.getElementById('data-mount-zone');
mountZone.classList.remove('hidden');
mountZone.classList.add('flex');
document.getElementById('btn-upload').classList.add('opacity-50', 'pointer-events-none');
showToast('数据读取成功,正在分析结构...', 'success');
setTimeout(() => {
const aiResponseHTML = `
<div class="flex gap-4 slide-up w-full">
<div class="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fa-solid fa-robot text-white text-xs"></i>
</div>
<div class="bg-white border border-slate-200 text-slate-700 p-4 rounded-2xl rounded-tl-none shadow-sm flex-1 max-w-2xl">
<div class="text-sm leading-relaxed mb-3">
<i class="fa-solid fa-bolt text-amber-500 mr-1"></i> <b>数据已挂载</b>。我已经为您规划好了统计分析计划书 (SAP)。
</div>
<button onclick="openWorkspace()" class="bg-blue-50 text-blue-700 border border-blue-200 px-4 py-2.5 rounded-xl w-full flex items-center justify-between hover:bg-blue-100 transition">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-white rounded-lg flex items-center justify-center text-blue-600 shadow-sm"><i class="fa-solid fa-file-signature"></i></div>
<div class="text-left">
<div class="text-sm font-bold">查看分析计划 (SAP)</div>
<div class="text-[10px] text-blue-500">参数映射完成,等待执行</div>
</div>
</div>
<i class="fa-solid fa-arrow-right-long text-blue-400"></i>
</button>
</div>
</div>`;
anchor.insertAdjacentHTML('beforebegin', aiResponseHTML);
scrollToBottom();
openWorkspace();
}, 1000);
}
function removeData(e) {
e.stopPropagation();
document.getElementById('data-mount-zone').classList.add('hidden');
document.getElementById('data-mount-zone').classList.remove('flex');
document.getElementById('btn-upload').classList.remove('opacity-50', 'pointer-events-none');
closeWorkspace();
}
function runAnalysis(btn) {
btn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> 执行中...';
btn.classList.replace('bg-blue-600', 'bg-slate-400');
btn.classList.replace('hover:bg-blue-700', 'hover:bg-slate-400');
btn.disabled = true;
document.getElementById('view-sap').classList.add('hidden');
document.getElementById('view-execution').classList.remove('hidden');
document.getElementById('view-execution').classList.add('flex');
document.getElementById('workspace-badge').innerText = 'Running';
document.getElementById('workspace-badge').className = 'px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-amber-100 text-amber-700 border border-amber-200';
document.getElementById('workspace-title').innerText = '执行日志 Trace';
const execMsg = `
<div class="flex gap-4 slide-up w-full">
<div class="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center flex-shrink-0 mt-0.5"><i class="fa-solid fa-robot text-white text-xs"></i></div>
<div class="bg-amber-50 border border-amber-200 text-amber-800 p-3 rounded-2xl rounded-tl-none shadow-sm text-sm">
<i class="fa-solid fa-circle-notch fa-spin mr-1"></i> 正在调用 R 引擎执行计算...
</div>
</div>`;
anchor.insertAdjacentHTML('beforebegin', execMsg);
scrollToBottom();
const logBox = document.getElementById('trace-logs');
logBox.innerHTML = '';
setTimeout(() => { addLog(logBox, 'text-green-400', 'fa-check-circle', '[System] 读取数据 BP_Trial_Data.csv (N=120) 成功'); }, 500);
setTimeout(() => { addLog(logBox, 'text-blue-400', 'fa-spinner fa-spin', '[Guardrail] 正在进行 Shapiro-Wilk 正态性检验...'); }, 1200);
setTimeout(() => {
logBox.lastElementChild.innerHTML = '<i class="fa-solid fa-times-circle text-red-400 bg-[#0f172a] z-10 relative"></i> <span class="ml-2">[Guardrail] 正态性检验未通过 (P=0.002 < 0.05)</span>';
addLog(logBox, 'text-amber-400', 'fa-arrow-turn-down', '[Action] 触发护栏:自动降级为 Wilcoxon 秩和检验');
}, 2500);
setTimeout(() => { addLog(logBox, 'text-blue-400', 'fa-spinner fa-spin', '[Compute] 执行 wilcox.test()...'); }, 3200);
setTimeout(() => {
document.getElementById('view-execution').classList.remove('flex');
document.getElementById('view-execution').classList.add('hidden');
document.getElementById('view-result').classList.remove('hidden');
document.getElementById('view-result').classList.add('fade-in');
document.getElementById('workspace-badge').innerText = 'Completed';
document.getElementById('workspace-badge').className = 'px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-green-100 text-green-700 border border-green-200';
document.getElementById('workspace-title').innerText = '分析结果报告';
document.getElementById('btn-code').classList.remove('hidden');
const finalMsg = `
<div class="flex gap-4 slide-up w-full">
<div class="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center flex-shrink-0 mt-0.5"><i class="fa-solid fa-robot text-white text-xs"></i></div>
<div class="bg-green-50 border border-green-200 text-green-800 p-3 rounded-2xl rounded-tl-none shadow-sm text-sm">
<i class="fa-solid fa-check-circle mr-1"></i> 分析完成!👉 <b>请在右侧面板查看详细的结果报告和图表。</b>
</div>
</div>`;
anchor.insertAdjacentHTML('beforebegin', finalMsg);
scrollToBottom();
}, 4500);
}
function addLog(container, color, icon, text) {
const div = document.createElement('div');
div.className = 'flex items-center fade-in text-slate-300';
div.innerHTML = `<i class="fa-solid ${icon} ${color} bg-[#0f172a] z-10 relative"></i> <span class="ml-3">${text}</span>`;
container.appendChild(div);
}
function scrollToBottom() {
setTimeout(() => {
const c = document.getElementById('chat-container');
c.scrollTo({ top: c.scrollHeight, behavior: 'smooth' });
}, 50);
}
function openModal(id) { document.getElementById(id).classList.remove('hidden'); }
function closeModal(id) { document.getElementById(id).classList.add('hidden'); }
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
let icon = '<i class="fa-solid fa-info-circle text-blue-500"></i>';
if(type === 'success') icon = '<i class="fa-solid fa-check-circle text-green-500"></i>';
toast.className = `bg-white border border-slate-200 shadow-lg rounded-full px-5 py-3 text-sm font-medium text-slate-700 flex items-center gap-3 slide-up`;
toast.innerHTML = `${icon} <span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateY(-10px)';
toast.style.transition = 'all 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 2500);
}
</script>
</body>
</html>

View File

@@ -0,0 +1,470 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSA-Pro 智能统计工作台 V8.0 (Dual-Pane Workspace)</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; background-color: #f1f5f9; color: #334155; overflow: hidden; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
.fade-in { animation: fadeIn 0.4s ease-out forwards; opacity: 0; }
.slide-up { animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; opacity: 0; transform: translateY(15px); }
.pop-in { animation: popIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards; opacity: 0; transform: scale(0.95); }
@keyframes fadeIn { to { opacity: 1; } }
@keyframes slideUp { to { transform: translateY(0); opacity: 1; } }
@keyframes popIn { to { transform: scale(1); opacity: 1; } }
/* 科学表格样式 */
.sci-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.sci-table th { border-top: 2px solid #1e293b; border-bottom: 1px solid #1e293b; padding: 10px 12px; text-align: left; font-weight: 600; color: #1e293b; }
.sci-table td { border-bottom: 1px solid #e2e8f0; padding: 10px 12px; color: #475569; }
.sci-table tr:last-child td { border-bottom: 2px solid #1e293b; }
/* 状态点 */
.status-dot { height: 8px; width: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
.status-success { background-color: #22c55e; box-shadow: 0 0 0 2px #dcfce7; }
pre code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.85rem; line-height: 1.5; }
.artifact-shadow { box-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.05), 0 0 3px rgba(0,0,0,0.02); }
</style>
</head>
<body class="h-screen flex text-slate-800">
<!-- 最左侧:极简全局导航 (Icon only) -->
<nav class="w-16 bg-slate-900 flex flex-col items-center py-4 z-30 flex-shrink-0">
<div class="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center text-white mb-8 shadow-md">
<i class="fa-solid fa-chart-simple"></i>
</div>
<div class="flex flex-col gap-4 w-full px-2">
<button class="w-full aspect-square rounded-xl bg-slate-800 text-white flex items-center justify-center hover:bg-slate-700 transition" title="新建分析">
<i class="fa-solid fa-plus"></i>
</button>
<button class="w-full aspect-square rounded-xl text-slate-400 flex items-center justify-center hover:bg-slate-800 hover:text-white transition" title="历史记录">
<i class="fa-solid fa-clock-rotate-left"></i>
</button>
<button class="w-full aspect-square rounded-xl text-slate-400 flex items-center justify-center hover:bg-slate-800 hover:text-white transition" title="配置中心">
<i class="fa-solid fa-gear"></i>
</button>
</div>
</nav>
<!-- 左半屏:对话控制区 (Chat Pane) -->
<section class="w-[400px] lg:w-[450px] flex flex-col bg-white border-r border-slate-200 z-20 flex-shrink-0 shadow-[4px_0_24px_rgba(0,0,0,0.02)]">
<!-- Chat Header -->
<header class="h-16 flex items-center justify-between px-5 border-b border-slate-100 flex-shrink-0">
<div class="font-semibold text-slate-800 text-sm">心血管药物疗效研究</div>
<div class="flex items-center gap-2 text-[10px] font-medium text-slate-500 bg-slate-50 px-2.5 py-1 rounded-full border border-slate-200">
<span class="status-dot status-success"></span> Engine Ready
</div>
</header>
<!-- Chat Messages -->
<div id="chat-container" class="flex-1 overflow-y-auto p-5 space-y-6">
<!-- 欢迎语 -->
<div class="flex gap-3">
<div class="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center flex-shrink-0 mt-0.5"><i class="fa-solid fa-robot text-white text-xs"></i></div>
<div class="bg-slate-50 text-slate-700 p-3.5 rounded-2xl rounded-tl-none text-sm leading-relaxed border border-slate-100">
你好!你可以直接描述研究目标,我将为你生成分析方案;或者上传数据,我们将直接开始智能分析。
</div>
</div>
<!-- User Msg -->
<div class="flex gap-3 flex-row-reverse slide-up" style="animation-delay: 0.1s">
<div class="w-8 h-8 rounded-full bg-slate-200 flex items-center justify-center flex-shrink-0 mt-0.5"><i class="fa-solid fa-user text-slate-500 text-xs"></i></div>
<div class="bg-blue-600 text-white p-3.5 rounded-2xl rounded-tr-none text-sm leading-relaxed shadow-sm">
我在研究一种新药,收集了实验组和对照组患者的血压下降值数据。我想知道新药是否有效。
</div>
</div>
<!-- AI Msg (生成 SAP 触发器) -->
<div class="flex gap-3 slide-up" style="animation-delay: 0.2s">
<div class="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center flex-shrink-0 mt-0.5"><i class="fa-solid fa-robot text-white text-xs"></i></div>
<div class="bg-white border border-slate-200 text-slate-700 p-3.5 rounded-2xl rounded-tl-none text-sm leading-relaxed shadow-sm flex flex-col gap-3">
<span>理解了针对“两组独立样本疗效对比”我为您制定了统计分析计划SAP</span>
<!-- 交互触发卡片 -->
<button class="bg-blue-50 text-blue-700 border border-blue-100 p-2.5 rounded-xl flex items-center gap-3 hover:bg-blue-100 transition text-left">
<div class="w-8 h-8 bg-white rounded flex items-center justify-center text-blue-600 shadow-sm"><i class="fa-solid fa-file-lines"></i></div>
<div class="flex-1">
<div class="font-semibold text-xs">初步分析方案.sap</div>
<div class="text-[10px] text-blue-500/80">点击在右侧工作区查看</div>
</div>
<i class="fa-solid fa-angle-right text-blue-400"></i>
</button>
<span class="text-xs text-slate-500">确认方案无误后,请上传数据文件。</span>
</div>
</div>
<div id="chat-anchor"></div>
</div>
<!-- Chat Input -->
<div class="p-4 bg-white border-t border-slate-100 flex-shrink-0">
<!-- 嵌入式挂载区 -->
<div id="data-mount-zone" class="hidden items-center mb-2 pop-in">
<div class="bg-slate-50 border border-slate-200 rounded-lg px-2 py-1.5 flex items-center gap-2 w-max">
<i class="fa-solid fa-file-csv text-green-600 text-sm ml-1"></i>
<div class="flex flex-col pr-2">
<span class="text-xs font-bold text-slate-700">BP_Trial_Data.csv</span>
<span class="text-[10px] text-slate-400 leading-none mt-0.5">120 rows • 45KB</span>
</div>
<button onclick="removeData(event)" class="text-slate-400 hover:text-red-500 px-1"><i class="fa-solid fa-xmark text-xs"></i></button>
</div>
</div>
<div class="relative flex items-end shadow-sm border border-slate-200 rounded-2xl bg-slate-50 focus-within:bg-white focus-within:border-blue-400 focus-within:ring-4 focus-within:ring-blue-50 transition-all p-1">
<button onclick="simulateUpload()" id="btn-upload" class="flex-shrink-0 w-9 h-9 flex items-center justify-center rounded-full text-slate-400 hover:text-blue-600 hover:bg-blue-50 transition" title="上传数据">
<i class="fa-solid fa-paperclip text-lg"></i>
</button>
<textarea class="flex-1 bg-transparent py-2.5 px-2 focus:outline-none resize-none text-sm text-slate-700" rows="1" placeholder="发送消息或上传数据..." style="min-height: 40px;"></textarea>
<button class="flex-shrink-0 w-9 h-9 bg-slate-800 text-white rounded-full hover:bg-slate-700 transition flex items-center justify-center shadow-sm">
<i class="fa-solid fa-arrow-up text-sm"></i>
</button>
</div>
</div>
</section>
<!-- 右半屏:工作区 (Artifacts Workspace) -->
<section class="flex-1 flex flex-col relative bg-[#f1f5f9] overflow-hidden" id="workspace">
<!-- 空状态背景 -->
<div id="workspace-empty" class="absolute inset-0 flex flex-col items-center justify-center text-slate-400 z-0 fade-in">
<i class="fa-solid fa-layer-group text-6xl mb-4 opacity-20"></i>
<p class="text-sm font-medium">分析方案、执行过程与图表将在这里展示</p>
</div>
<!-- 动态 Artifact 容器 -->
<div id="artifact-container" class="absolute inset-0 p-6 flex flex-col z-10 hidden">
<!-- 头部栏 (通用) -->
<header class="flex items-center justify-between mb-4 flex-shrink-0">
<div class="flex items-center gap-3">
<div id="artifact-icon" class="w-8 h-8 rounded-lg bg-blue-100 text-blue-600 flex items-center justify-center"><i class="fa-solid fa-file-lines"></i></div>
<h2 id="artifact-title" class="text-lg font-bold text-slate-800">统计分析计划书 (SAP)</h2>
</div>
<div class="flex gap-2">
<button id="btn-code" class="hidden px-3 py-1.5 bg-white border border-slate-200 text-slate-600 text-xs font-medium rounded-lg hover:bg-slate-50 transition shadow-sm"><i class="fa-brands fa-r-project mr-1 text-blue-600"></i> 查看 R 源码</button>
<button class="px-3 py-1.5 bg-white border border-slate-200 text-slate-600 text-xs font-medium rounded-lg hover:bg-slate-50 transition shadow-sm"><i class="fa-solid fa-download mr-1"></i> 下载</button>
</div>
</header>
<!-- 渲染区域 (白板) -->
<div class="flex-1 bg-white rounded-2xl artifact-shadow border border-slate-200/60 overflow-y-auto flex flex-col relative">
<!-- 视图 1SAP 文档 (默认显示) -->
<div id="view-sap" class="p-8 md:p-12 max-w-4xl mx-auto w-full fade-in">
<h1 class="text-2xl font-bold text-slate-900 border-b-2 border-slate-900 pb-3 mb-6">研究课题:新药治疗高血压疗效对比</h1>
<div class="space-y-8">
<section>
<h3 class="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-3">1. 研究目的与假设</h3>
<p class="text-sm text-slate-700 leading-relaxed bg-slate-50 p-4 rounded-lg border border-slate-100">探究新药在降低患者血压方面是否优于传统安慰剂/对照药。假设:两组的血压下降值存在显著差异。</p>
</section>
<section>
<h3 class="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-3">2. 推荐统计方法</h3>
<div class="border border-blue-100 rounded-lg overflow-hidden">
<div class="bg-blue-50 px-4 py-2 border-b border-blue-100 font-medium text-blue-800 text-sm"><i class="fa-solid fa-star text-amber-500 mr-2"></i>首选:独立样本 T 检验 (Independent T-Test)</div>
<div class="p-4 text-sm text-slate-600 space-y-2">
<div class="flex"><span class="w-24 font-medium text-slate-500">自变量 (X):</span> <span>分组 (分类变量2个水平)</span></div>
<div class="flex"><span class="w-24 font-medium text-slate-500">因变量 (Y):</span> <span>血压下降值 (连续数值)</span></div>
</div>
</div>
</section>
<section>
<h3 class="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-3">3. 统计护栏 (执行前置条件)</h3>
<ul class="text-sm text-slate-700 space-y-3 pl-1">
<li class="flex items-start gap-2">
<i class="fa-solid fa-check text-green-500 mt-1"></i>
<div><b>独立性假设:</b> 样本需相互独立(由研究设计保证)。</div>
</li>
<li class="flex items-start gap-2">
<i class="fa-solid fa-scale-balanced text-amber-500 mt-1"></i>
<div><b>正态性假设检验:</b> 将执行 Shapiro-Wilk 检验。若 P < 0.05 (拒绝正态)将自动降级使用 <b>Wilcoxon 秩和检验</b></div>
</li>
</ul>
</section>
</div>
</div>
<!-- 视图 2执行追踪 (隐藏) -->
<div id="view-execution" class="absolute inset-0 bg-white p-8 hidden flex-col items-center justify-center">
<div class="w-full max-w-xl">
<div class="flex items-center justify-between mb-6">
<h3 class="font-bold text-slate-700">正在执行云端计算...</h3>
<i class="fa-solid fa-circle-notch fa-spin text-blue-500 text-xl"></i>
</div>
<div class="bg-slate-900 rounded-xl p-5 font-mono text-xs text-slate-300 shadow-inner relative overflow-hidden h-64">
<div class="absolute left-[31px] top-6 bottom-6 w-px bg-slate-700"></div>
<div id="trace-logs" class="space-y-4 relative z-10">
<!-- 动态插入日志 -->
</div>
</div>
</div>
</div>
<!-- 视图 3最终结果报告 (隐藏) -->
<div id="view-result" class="p-8 md:p-12 max-w-4xl mx-auto w-full hidden">
<!-- 核心结论提示 -->
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 p-5 rounded-xl mb-8 flex gap-4 items-start shadow-sm">
<i class="fa-solid fa-lightbulb text-blue-500 text-xl mt-0.5"></i>
<div>
<h4 class="font-bold text-slate-800 text-sm mb-1">AI 统计解读</h4>
<p class="text-sm text-slate-700 leading-relaxed">
结果表明新药组Drug的血压下降幅度显著大于对照组Placebo。差异具有统计学意义 (<span class="text-red-600 font-bold">P < 0.001</span>)。
<i>注:因数据未通过正态性检验,本结果由自动降级后的 Wilcoxon 秩和检验得出。</i>
</p>
</div>
</div>
<!-- 结果表格 -->
<div class="mb-10">
<h4 class="text-xs font-bold text-slate-500 uppercase tracking-wider mb-3">Table 1. 血压下降值组间比较</h4>
<div class="border border-slate-200 rounded-lg overflow-hidden">
<table class="sci-table bg-white">
<thead>
<tr class="bg-slate-50">
<th>组别 (Group)</th>
<th>样本量 (N)</th>
<th>下降值 (Median [IQR])</th>
<th>W 统计量</th>
<th>P 值</th>
</tr>
</thead>
<tbody>
<tr class="hover:bg-slate-50">
<td>Drug (新药组)</td>
<td>60</td>
<td>14.5 [12.1 - 16.8]</td>
<td rowspan="2" class="align-middle">2845.5</td>
<td rowspan="2" class="align-middle text-red-600 font-bold">< 0.001 **</td>
</tr>
<tr class="hover:bg-slate-50">
<td>Placebo (对照组)</td>
<td>60</td>
<td>8.2 [6.5 - 10.4]</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 图表区域 -->
<div>
<h4 class="text-xs font-bold text-slate-500 uppercase tracking-wider mb-3">Figure 1. 分布可视化</h4>
<div class="border border-slate-200 rounded-xl p-6 bg-white flex justify-center shadow-sm">
<!-- 纯 CSS 模拟的精美图表 -->
<div class="relative w-full max-w-md h-64 flex items-end justify-center gap-16 pb-8 border-b-2 border-l-2 border-slate-800 ml-8">
<!-- Y轴刻度 -->
<div class="absolute -left-8 top-0 text-[10px] text-slate-500">20</div>
<div class="absolute -left-8 top-1/2 -translate-y-1/2 text-[10px] text-slate-500">10</div>
<div class="absolute -left-6 bottom-6 text-[10px] text-slate-500">0</div>
<!-- 箱线图 Drug -->
<div class="relative w-16 h-48 bg-blue-500/20 border-2 border-blue-600 rounded-sm flex justify-center items-center group cursor-pointer hover:bg-blue-500/30 transition">
<div class="absolute w-full h-[2px] bg-blue-800"></div> <!-- 中位数 -->
<div class="absolute -top-4 w-px h-4 bg-blue-600"></div> <!-- 上触须 -->
<div class="absolute -bottom-6 w-px h-6 bg-blue-600"></div> <!-- 下触须 -->
<div class="absolute -top-4 w-6 h-[2px] bg-blue-600"></div>
<div class="absolute -bottom-6 w-6 h-[2px] bg-blue-600"></div>
<div class="absolute -bottom-14 text-xs font-bold text-slate-700">Drug</div>
</div>
<!-- 箱线图 Placebo -->
<div class="relative w-16 h-24 bg-red-500/20 border-2 border-red-600 rounded-sm flex justify-center items-center group cursor-pointer hover:bg-red-500/30 transition">
<div class="absolute w-full h-[2px] bg-red-800"></div> <!-- 中位数 -->
<div class="absolute -top-6 w-px h-6 bg-red-600"></div> <!-- 上触须 -->
<div class="absolute -bottom-4 w-px h-4 bg-red-600"></div> <!-- 下触须 -->
<div class="absolute -top-6 w-6 h-[2px] bg-red-600"></div>
<div class="absolute -bottom-4 w-6 h-[2px] bg-red-600"></div>
<div class="absolute -bottom-14 text-xs font-bold text-slate-700">Placebo</div>
</div>
<!-- 显著性标记 -->
<div class="absolute top-2 left-[30%] right-[30%] h-4 border-t-2 border-l-2 border-r-2 border-slate-700"></div>
<div class="absolute -top-3 w-full text-center text-sm font-bold text-slate-800">P < 0.001 ***</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 模态框R 代码预览 (在方案C中代码也可以直接在右侧面板展示但为了保持报告整洁保留模态框) -->
<div id="code-modal" class="fixed inset-0 z-50 hidden flex items-center justify-center">
<div class="absolute inset-0 bg-slate-900/40 modal-backdrop" onclick="closeModal('code-modal')"></div>
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-3xl max-h-[80vh] flex flex-col relative z-10 pop-in m-4">
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-100">
<h3 class="font-bold text-slate-800 flex items-center gap-2">
<i class="fa-brands fa-r-project text-blue-600 text-xl"></i> 可复现 R 源码
</h3>
<button onclick="closeModal('code-modal')" class="text-slate-400 hover:text-slate-700 w-8 h-8 rounded-full hover:bg-slate-100 flex items-center justify-center">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="p-6 overflow-y-auto flex-1 bg-slate-50">
<pre class="bg-slate-900 text-slate-50 p-4 rounded-xl overflow-x-auto shadow-inner text-xs"><code><span class="text-slate-500"># SSA-Pro 生成代码: 独立样本差异分析</span>
<span class="text-pink-400">library</span>(ggplot2)
df <- <span class="text-blue-300">read.csv</span>(<span class="text-green-300">"BP_Trial_Data.csv"</span>)
df$Group <- <span class="text-blue-300">as.factor</span>(df$Group)
df$BP_Change <- <span class="text-blue-300">as.numeric</span>(df$BP_Change)
<span class="text-slate-500"># 核心计算 (自动降级至非参数检验)</span>
res <- <span class="text-blue-300">wilcox.test</span>(BP_Change ~ Group, data = df)
<span class="text-blue-300">print</span>(res)</code></pre>
</div>
</div>
</div>
<script>
const chatAnchor = document.getElementById('chat-anchor');
const workspaceEmpty = document.getElementById('workspace-empty');
const artifactContainer = document.getElementById('artifact-container');
const vSap = document.getElementById('view-sap');
const vExec = document.getElementById('view-execution');
const vRes = document.getElementById('view-result');
const artTitle = document.getElementById('artifact-title');
const artIcon = document.getElementById('artifact-icon');
const btnCode = document.getElementById('btn-code');
// 初始化显示 SAP
setTimeout(() => {
workspaceEmpty.classList.add('hidden');
artifactContainer.classList.remove('hidden');
}, 500);
// 模拟上传数据
function simulateUpload() {
const mountZone = document.getElementById('data-mount-zone');
mountZone.classList.remove('hidden');
mountZone.classList.add('flex');
document.getElementById('btn-upload').classList.add('opacity-50', 'pointer-events-none');
// 聊天区增加对话
setTimeout(() => {
const aiHTML = `
<div class="flex gap-3 slide-up">
<div class="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center flex-shrink-0 mt-0.5"><i class="fa-solid fa-robot text-white text-xs"></i></div>
<div class="bg-white border border-slate-200 text-slate-700 p-4 rounded-2xl rounded-tl-none shadow-sm w-full">
<div class="text-sm font-medium mb-3 border-b border-slate-100 pb-2">
<i class="fa-solid fa-link text-blue-500 mr-1"></i> 数据已挂载,参数映射完成:
</div>
<div class="text-xs space-y-2 mb-4">
<div class="flex justify-between"><span class="text-slate-500">分组变量:</span> <span class="font-mono bg-slate-100 px-1 rounded">Group</span></div>
<div class="flex justify-between"><span class="text-slate-500">检验变量:</span> <span class="font-mono bg-slate-100 px-1 rounded">BP_Change</span></div>
</div>
<button onclick="runAnalysis(this)" class="w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition shadow-sm text-sm font-medium flex items-center justify-center gap-2">
<i class="fa-solid fa-play text-xs"></i> 确认执行分析
</button>
</div>
</div>`;
chatAnchor.insertAdjacentHTML('beforebegin', aiHTML);
scrollToBottom();
}, 600);
}
// 移除数据
function removeData(e) {
e.stopPropagation();
document.getElementById('data-mount-zone').classList.add('hidden');
document.getElementById('data-mount-zone').classList.remove('flex');
document.getElementById('btn-upload').classList.remove('opacity-50', 'pointer-events-none');
}
// 执行分析 (重头戏:右侧工作区切换状态)
function runAnalysis(btn) {
// 1. 左侧按钮状态变更
btn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> 执行中...';
btn.classList.replace('bg-blue-600', 'bg-slate-400');
btn.classList.replace('hover:bg-blue-700', 'hover:bg-slate-400');
btn.disabled = true;
// 2. 右侧切到执行日志视图
vSap.classList.add('hidden');
vExec.classList.remove('hidden');
vExec.classList.add('flex');
artTitle.innerText = "执行日志 (Engine Trace)";
artIcon.innerHTML = '<i class="fa-solid fa-terminal"></i>';
artIcon.className = "w-8 h-8 rounded-lg bg-slate-800 text-white flex items-center justify-center";
// 3. 模拟打印日志
const logBox = document.getElementById('trace-logs');
logBox.innerHTML = '';
setTimeout(() => { addLog(logBox, 'text-green-400', 'fa-check-circle', '[System] 读取数据 BP_Trial_Data.csv (N=120) 成功'); }, 500);
setTimeout(() => { addLog(logBox, 'text-blue-400', 'fa-spinner fa-spin', '[Guardrail] 正在进行 Shapiro-Wilk 正态性检验...'); }, 1200);
setTimeout(() => {
logBox.lastElementChild.innerHTML = '<i class="fa-solid fa-times-circle text-red-400 bg-slate-900 z-10 relative"></i> <span class="ml-2">[Guardrail] 正态性检验未通过 (P=0.002 < 0.05)</span>';
addLog(logBox, 'text-amber-400', 'fa-arrow-turn-down', '[Action] 触发护栏:自动降级为 Wilcoxon 秩和检验');
}, 2500);
setTimeout(() => { addLog(logBox, 'text-blue-400', 'fa-spinner fa-spin', '[Compute] 执行 wilcox.test()...'); }, 3200);
// 4. 完成,展示结果
setTimeout(() => {
// 左侧按钮更新
btn.innerHTML = '<i class="fa-solid fa-check"></i> 分析完成';
btn.classList.replace('bg-slate-400', 'bg-green-500');
btn.classList.replace('hover:bg-slate-400', 'hover:bg-green-600');
// 右侧切到结果视图
vExec.classList.remove('flex');
vExec.classList.add('hidden');
vRes.classList.remove('hidden');
vRes.classList.add('fade-in');
// 更新右侧头部
artTitle.innerText = "分析结果:血压变化值差异检验";
artIcon.innerHTML = '<i class="fa-solid fa-chart-pie"></i>';
artIcon.className = "w-8 h-8 rounded-lg bg-indigo-100 text-indigo-600 flex items-center justify-center";
// 显示代码按钮
btnCode.classList.remove('hidden');
btnCode.onclick = () => { document.getElementById('code-modal').classList.remove('hidden') };
// 左侧追加提示
setTimeout(() => {
const aiFinal = `
<div class="flex gap-3 slide-up">
<div class="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center flex-shrink-0 mt-0.5"><i class="fa-solid fa-robot text-white text-xs"></i></div>
<div class="bg-slate-50 text-slate-700 p-3.5 rounded-2xl rounded-tl-none text-sm leading-relaxed border border-slate-200">
分析已完成!👉 <b>请在右侧面板查看详细的结果报告和图表。</b>
</div>
</div>`;
chatAnchor.insertAdjacentHTML('beforebegin', aiFinal);
scrollToBottom();
}, 500);
}, 4500);
}
function addLog(container, color, icon, text) {
const div = document.createElement('div');
div.className = 'flex items-center fade-in text-slate-300';
div.innerHTML = `<i class="fa-solid ${icon} ${color} bg-slate-900 z-10 relative"></i> <span class="ml-3">${text}</span>`;
container.appendChild(div);
}
function scrollToBottom() {
const c = document.getElementById('chat-container');
c.scrollTo({ top: c.scrollHeight, behavior: 'smooth' });
}
function closeModal(id) {
document.getElementById(id).classList.add('hidden');
}
</script>
</body>
</html>