feat(pkb): Complete PKB module frontend migration with V3 design
Summary: - Implement PKB Dashboard and Workspace pages based on V3 prototype - Add single-layer header with integrated Tab navigation - Implement 3 work modes: Full Text, Deep Read, Batch Processing - Integrate Ant Design X Chat component for AI conversations - Create BatchModeComplete with template selection and document processing - Add compact work mode selector with dropdown design Backend: - Migrate PKB controllers and services to /modules/pkb structure - Register v2 API routes at /api/v2/pkb/knowledge - Maintain dual API routes for backward compatibility Technical details: - Use Zustand for state management - Handle SSE streaming responses for AI chat - Support document selection for Deep Read mode - Implement batch processing with progress tracking Known issues: - Batch processing API integration pending - Knowledge assets page navigation needs optimization Status: Frontend functional, pending refinement
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
# **AI 临床医生与医院知识库 \- MVP 阶段产品需求文档 (PRD) V5.0**
|
||||
|
||||
|
|
||||
|
||||
| 版本号 | 日期 | 修改人 | 修改内容 |
|
||||
| V4.0 | 2024-06-XX | Product Lead | 降维策略 MVP |
|
||||
| V5.0 | 2024-06-XX | Product Lead | 完整落地版:基于“全量阅读”策略,补全 UI/UX 规范与详细功能定义 |
|
||||
|
||||
## **1\. 核心战略与范围 (Strategy & Scope)**
|
||||
|
||||
### **1.1 核心价值主张**
|
||||
|
||||
放弃传统的“切片式 RAG”,利用 DeepSeek-V3 / Qwen-Max 的长窗口能力,通过 **Document-Level RAG (文档级阅读)** 技术,为医生提供**逻辑完整、引用精准**的知识库问答体验。
|
||||
|
||||
### **1.2 MVP 阶段边界 (Scope Freeze)**
|
||||
|
||||
* **适用终端:** Web 端 (PC/Mac 浏览器),兼容 iPad。
|
||||
* **支持格式:** PDF, Word (.docx), PPT (.pptx)。
|
||||
* **硬性限制:** 单库文件数 ≤ 30 个;单文件大小 ≤ 20MB。
|
||||
* **核心场景:** 循证检索 (指南)、深度研读 (文献)、用药助手 (药品)、考典刷题 (考试)。
|
||||
|
||||
## **2\. 功能模块详情 (Detailed Requirements)**
|
||||
|
||||
### **2.1 模块一:仪表盘 (Dashboard)**
|
||||
|
||||
**用户故事:** 作为医生,我希望一眼看到我有多少个知识库,并能快速创建一个新的专科知识库。
|
||||
|
||||
#### **2.1.1 页面布局与 UI 规范**
|
||||
|
||||
* **布局结构:**
|
||||
* **顶部:** 全局导航栏 (Logo \+$$智能统计$$$$智能清洗$$$$\*\*AI 知识库\*\*$$$$AI 问答$$
|
||||
\+ 头像)。
|
||||
* **主体:** "1+3" 卡片阵列布局 (Grid System)。
|
||||
* **卡片设计:**
|
||||
* **Slot 1 (新建入口):**
|
||||
* **样式:** 浅蓝色渐变背景 (bg-blue-50),深蓝色虚线边框。
|
||||
* **内容:** 大号 "+" 图标,文案 "创建知识库",下方并列展示 5 个场景图标 (指南/文献/病例/药品/考试) 以提示能力。
|
||||
* **Slot 2-N (现有知识库):**
|
||||
* **样式:** 白色卡片,微阴影 (shadow-sm \-\> hover shadow-md)。
|
||||
* **内容:** 图标(左上) \+ 标题(加粗) \+ 类型标签(胶囊样式) \+ "进入工作台"按钮(底部通栏)。
|
||||
|
||||
#### **2.1.2 交互流程:创建知识库**
|
||||
|
||||
* **触发:** 点击“创建知识库”卡片 \-\> 弹出模态框 (Modal)。
|
||||
* **Step 1: 类型选择 (Type Selection)**
|
||||
* **UI:** 5 个大卡片网格。
|
||||
* **选项:**
|
||||
1. **循证检索 (指南):** 图标 BookOpen (Blue)。文案:"查诊疗标准、用药剂量"。
|
||||
2. **深度研读 (文献):** 图标 Microscope (Purple)。文案:"文献综述、横向对比"。
|
||||
3. **临床决策 (病例):** 图标 Stethoscope (Green)。文案:"疑难病例参考"。
|
||||
4. **用药助手 (药品):** 图标 Pill (Rose)。文案:"查配伍禁忌"。
|
||||
5. **考典刷题 (考试):** 图标 GraduationCap (Orange)。文案:"主治/副高备考"。
|
||||
* **Step 2: 基础信息 & 角色注入**
|
||||
* **字段:** 知识库名称 (必填)、所属科室 (下拉选:心内/呼吸/消化...)。
|
||||
* **逻辑:** 选中“心内科”后,后端自动在 System Prompt 中注入 *"你是一名心内科专家..."*。
|
||||
* **Step 3: 文件上传 (Upload)**
|
||||
* **UI:** 大面积拖拽上传区 (Dropzone)。
|
||||
* **逻辑:**
|
||||
* 前端校验大小 (\>20MB 飘红报错)。
|
||||
* 前端校验数量 (\>30 个 飘红报错)。
|
||||
* 上传成功后显示列表,状态流转:上传中... \-\> 就绪。
|
||||
|
||||
### **2.2 模块二:沉浸式工作台 (Workspace)**
|
||||
|
||||
**用户故事:** 作为医生,我希望在一个无干扰的环境中与我的资料对话,并能随时核对原文。
|
||||
|
||||
#### **2.2.1 全局框架 (Immersive Layout)**
|
||||
|
||||
* **顶部导航 (Header):**
|
||||
* **样式:** 深色背景 (bg-slate-900),高度 56px。
|
||||
* **左侧:** \< 返回 (白色文字按钮,点击返回 Dashboard)。
|
||||
* **中间:** 知识库名称 \+ 图标。
|
||||
* **右侧:** 简单的设置齿轮。
|
||||
* **模式切换 (Tabs):**
|
||||
* 位于 Header 下方,白色背景,高度 48px。
|
||||
* **Tab A:** \[💬 智能问答\] (默认选中,底部蓝条)。
|
||||
* **Tab B:** \[📂 知识资产\] (显示文件计数 Badge)。
|
||||
|
||||
#### **2.2.2 视图 A:智能问答 (Smart Chat)**
|
||||
|
||||
* **布局:**
|
||||
* **默认:** 单栏宽屏聊天窗口 (最大宽度 900px,居中)。
|
||||
* **扩展:** 当点击引用或手动展开时,右侧滑出 PDF 阅读器 (占比 45%),聊天窗自动收缩至左侧。
|
||||
* **对话交互 (Chat Interaction):**
|
||||
* **输入框:** 底部固定。支持 Shift+Enter 换行。
|
||||
* **AI 回答:** 流式输出 (Typewriter Effect)。
|
||||
* **引用标注:** 必须以 \[文档名\] 或 \[1\] 形式高亮显示,颜色为品牌蓝。
|
||||
* **后端策略路由 (核心逻辑):**
|
||||
* **场景一 (小库直读):** 知识库总 Token \< 32k。
|
||||
* *UI:* 顶部 Toast 提示 "已加载全量上下文,AI 具备全知视角"。
|
||||
* **场景二 (大库路由):** 知识库总 Token \> 32k。
|
||||
* *UI:* 用户提问后,输入框上方出现微型 Loading 状态 —— "正在分析摘要..." \-\> "已定位至《2024指南》等 3 篇文档" \-\> 开始回答。
|
||||
|
||||
#### **2.2.3 视图 B:知识资产 (Assets Management)**
|
||||
|
||||
* **UI 形式:** 全屏数据表格 (Table)。
|
||||
* **列定义:**
|
||||
1. **文件名:** 图标 (PDF/Word) \+ 名称。
|
||||
2. **AI 摘要:** 展示 2-3 行核心摘要文本 (由 DeepSeek 生成),支持 hover 查看全部。
|
||||
3. **状态:** 准备中 (灰色) / 就绪 (绿色) / 失败 (红色)。
|
||||
4. **操作:** 预览、删除。
|
||||
* **筛选器:** 右上角“筛选”按钮,点击弹出下拉面板 (按时间/状态/类型)。
|
||||
|
||||
## **3\. 详细 UI/UX 设计规范 (Design Specs)**
|
||||
|
||||
为确保还原度,请 UI 设计师和前端开发遵循以下规范:
|
||||
|
||||
### **3.1 色彩体系 (Color Palette)**
|
||||
|
||||
使用 Tailwind CSS 默认色板:
|
||||
|
||||
* **主色 (Primary):** Blue-600 (\#2563EB) \- 用于按钮、链接、高亮。
|
||||
* **场景色 (Category Colors):**
|
||||
* 指南: Blue-500
|
||||
* 文献: Purple-600
|
||||
* 病例: Emerald-600
|
||||
* 药品: Rose-600
|
||||
* 考试: Orange-500
|
||||
* **中性色:** Slate-50 (背景), Slate-900 (Header), Slate-500 (次要文字)。
|
||||
|
||||
### **3.2 图标系统 (Iconography)**
|
||||
|
||||
* **风格:** 线性图标 (Stroke 2px)。
|
||||
* **技术:** 使用 **Inline SVG** (参考 TDD V3.0 的 Icons 常量),严禁引入外部 Icon Font。
|
||||
|
||||
### **3.3 字体与排版**
|
||||
|
||||
* **字体:** 系统默认无衬线字体 (Inter, Roboto, PingFang SC)。
|
||||
* **字号:**
|
||||
* H1 (页面标题): 20px Bold
|
||||
* H2 (模块标题): 16px Bold
|
||||
* Body (正文): 14px Regular (行高 1.6)
|
||||
* Caption (说明): 12px Text-Slate-400
|
||||
|
||||
## **4\. 异常流程与边界处理 (Exception Handling)**
|
||||
|
||||
### **4.1 上传失败**
|
||||
|
||||
* **场景:** 用户上传了加密 PDF 或损坏文件。
|
||||
* **后端:** 解析器捕获异常,返回 status: FAILED,error\_msg: "File encrypted"。
|
||||
* **前端:** 资产列表中该行变红,显示“解析失败:文件已加密”,并提供“删除”按钮。
|
||||
|
||||
### **4.2 AI 回答超时/失败**
|
||||
|
||||
* **场景:** DeepSeek API 响应超时 (\>60s)。
|
||||
* **UI:**
|
||||
* 消息气泡显示红色感叹号。
|
||||
* 提示文案: "AI 思考超时,请尝试精简问题或重试。"
|
||||
* 提供$$重试$$
|
||||
按钮。
|
||||
|
||||
### **4.3 引用源定位失败**
|
||||
|
||||
* **场景:** 用户点击 \[1\],但该文件已被删除。
|
||||
* **UI:** 弹出 Toast 提示 "源文件已被删除,无法查看原文"。
|
||||
|
||||
## **5\. 验收测试标准 (QA Acceptance)**
|
||||
|
||||
| ID | 测试点 | 预期结果 | 优先级 |
|
||||
| TC01 | 格式兼容性 | 上传 .pdf, .docx, .pptx 文件,均能在资产列表显示“就绪”,且能被检索到。 | P0 |
|
||||
| TC02 | 边界限制 | 尝试上传第 31 个文件,前端应阻止上传并弹窗提示限制。 | P0 |
|
||||
| TC03 | 引用跳转 | 提问后,点击回答中的 \[文档名\],右侧面板应滑出并正确加载该文档。 | P0 |
|
||||
| TC04 | Word表格解析 | 上传包含表格的 Word 指南,询问表格内数据,AI 应能准确回答数值。 | P1 |
|
||||
| TC05 | 路由逻辑 | 在 \>32k Token 的库中提问,观察 Network 请求,应看到系统先请求了摘要接口,再请求了全文接口。 | P1 |
|
||||
425
docs/03-业务模块/PKB-个人知识库/01-需求分析/工作台V3.html
Normal file
425
docs/03-业务模块/PKB-个人知识库/01-需求分析/工作台V3.html
Normal file
@@ -0,0 +1,425 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI 知识库工作台 V3 (交互优化版)</title>
|
||||
|
||||
<!-- 1. 引入 Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- 2. 配置 Import Map -->
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react": "https://esm.sh/react@18.2.0",
|
||||
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
|
||||
"lucide-react": "https://esm.sh/lucide-react@0.263.1"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- 3. 引入 Babel -->
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
|
||||
<style>
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.3s ease-out forwards;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(5px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.animate-slide-in-right {
|
||||
animation: slideInRight 0.3s ease-out forwards;
|
||||
}
|
||||
@keyframes slideInRight {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
/* PDF 模拟背景 */
|
||||
.pdf-pattern {
|
||||
background-color: #f1f5f9;
|
||||
background-image: linear-gradient(45deg, #e2e8f0 25%, transparent 25%, transparent 75%, #e2e8f0 75%, #e2e8f0),
|
||||
linear-gradient(45deg, #e2e8f0 25%, transparent 25%, transparent 75%, #e2e8f0 75%, #e2e8f0);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 10px 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 text-slate-800 font-sans antialiased overflow-hidden">
|
||||
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="text/babel" data-type="module">
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import {
|
||||
ArrowLeft, MessageSquare, Database, Search, Plus,
|
||||
FileText, Trash2, RotateCw, CheckCircle2, Loader2,
|
||||
Bot, User, Send, ChevronRight, MoreHorizontal,
|
||||
Maximize2, Download, AlertCircle, FileSearch, SplitSquareHorizontal,
|
||||
X, Settings, ChevronDown, Filter, LayoutDashboard, PanelRightOpen, Calendar
|
||||
} from 'lucide-react';
|
||||
|
||||
// --- MOCK DATA ---
|
||||
const KB_INFO = {
|
||||
id: 'KB001',
|
||||
name: '心内科 2024 临床指南合集',
|
||||
type: '临床指南',
|
||||
docCount: 12,
|
||||
engine: 'Dify RAG'
|
||||
};
|
||||
|
||||
const MOCK_MESSAGES = [
|
||||
{ id: 1, role: 'ai', content: '您好,李医生。我是您的循证医学助手。已加载 12 份心内科相关指南。请问有什么可以帮您?', citations: [] },
|
||||
{ id: 2, role: 'user', content: '请总结一下2024年高血压指南中关于老年患者降压目标值的变化。', citations: [] },
|
||||
{ id: 3, role: 'ai', content: '根据《中国高血压防治指南(2024修订版)》[1],针对老年高血压患者的降压目标值主要更新如下:\n\n1. **65~79岁老年人**:如能耐受,推荐将收缩压降至 <140 mmHg,若耐受性良好,可进一步降至 <130 mmHg。\n2. **≥80岁高龄老年人**:一般建议将收缩压降至 <150 mmHg;如耐受性良好,可尝试降至 <140 mmHg。\n\n相比2018版,新版指南更加强调在耐受前提下进行强化降压。', citations: [{id: 'doc1', page: 45, text: '老年高血压...'}] }
|
||||
];
|
||||
|
||||
const MOCK_FILES = [
|
||||
{ id: 1, name: '中国高血压防治指南(2024修订版).pdf', size: '4.2 MB', uploadTime: '2024-05-20 10:30', status: 'ready', progress: 100, tokens: '45k' },
|
||||
{ id: 2, name: '慢性心力衰竭诊断治疗指南2023.pdf', size: '3.8 MB', uploadTime: '2024-05-19 14:20', status: 'ready', progress: 100, tokens: '32k' },
|
||||
{ id: 3, name: '房颤抗凝治疗专家共识.pdf', size: '1.5 MB', uploadTime: '2024-05-19 14:22', status: 'ready', progress: 100, tokens: '12k' },
|
||||
{ id: 4, name: '冠心病心脏康复基层指南.pdf', size: '2.8 MB', uploadTime: '2024-05-18 09:15', status: 'ready', progress: 100, tokens: '28k' },
|
||||
{ id: 5, name: '最新_心肌梗死急诊流程_草稿.pdf', size: '2.1 MB', uploadTime: '刚刚', status: 'analyzing_layout', progress: 35, tokens: '-' },
|
||||
];
|
||||
|
||||
function WorkspaceV3() {
|
||||
// Navigation State
|
||||
const [activeTab, setActiveTab] = useState('CHAT');
|
||||
|
||||
// Chat State
|
||||
const [messages, setMessages] = useState(MOCK_MESSAGES);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
// V3 UPDATE: Default isPdfOpen is now FALSE
|
||||
const [isPdfOpen, setIsPdfOpen] = useState(false);
|
||||
|
||||
// Asset State
|
||||
const [files, setFiles] = useState(MOCK_FILES);
|
||||
const [isFilterOpen, setIsFilterOpen] = useState(false); // Filter Dropdown State
|
||||
|
||||
// Simulation of MinerU Progress
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setFiles(prev => prev.map(f => {
|
||||
if (f.status === 'ready') return f;
|
||||
let newProgress = f.progress + 5;
|
||||
let newStatus = f.status;
|
||||
if (newProgress > 30) newStatus = 'extracting_table';
|
||||
if (newProgress > 70) newStatus = 'indexing';
|
||||
if (newProgress >= 100) { newProgress = 100; newStatus = 'ready'; }
|
||||
return { ...f, progress: newProgress, status: newStatus };
|
||||
}));
|
||||
}, 800);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Helper for Status Badge
|
||||
const getStatusBadge = (status) => {
|
||||
switch(status) {
|
||||
case 'ready': return <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-50 text-green-700 border border-green-100"><CheckCircle2 className="w-3 h-3 mr-1"/> 解析完成</span>;
|
||||
case 'analyzing_layout': return <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-50 text-blue-700 border border-blue-100"><Loader2 className="w-3 h-3 mr-1 animate-spin"/> MinerU 版面分析</span>;
|
||||
case 'extracting_table': return <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-50 text-purple-700 border border-purple-100"><Loader2 className="w-3 h-3 mr-1 animate-spin"/> 结构化提取</span>;
|
||||
case 'indexing': return <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-orange-50 text-orange-700 border border-orange-100"><Loader2 className="w-3 h-3 mr-1 animate-spin"/> 向量索引</span>;
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
|
||||
// V3 UPDATE: Toggle PDF function
|
||||
const handleCitationClick = () => {
|
||||
if (!isPdfOpen) setIsPdfOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-gray-50">
|
||||
|
||||
{/* 1. IMMERSIVE HEADER */}
|
||||
<header className="h-14 bg-slate-900 text-white flex items-center justify-between px-4 flex-shrink-0 z-30 shadow-md">
|
||||
<div className="flex items-center">
|
||||
<button className="flex items-center text-slate-300 hover:text-white hover:bg-slate-800 px-3 py-1.5 rounded-lg transition-all group mr-4">
|
||||
<ChevronDown className="w-4 h-4 mr-1 transform rotate-90 group-hover:-translate-x-1 transition-transform" />
|
||||
<span className="font-bold text-sm">返回知识库列表</span>
|
||||
</button>
|
||||
<div className="h-6 w-px bg-slate-700 mx-2"></div>
|
||||
<div className="ml-2">
|
||||
<h1 className="text-base font-bold flex items-center text-white">
|
||||
<Database className="w-4 h-4 mr-2 text-blue-400" />
|
||||
{KB_INFO.name}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-xs text-slate-400 border border-slate-700 px-2 py-0.5 rounded bg-slate-800">{KB_INFO.type}</span>
|
||||
<button className="text-slate-400 hover:text-white"><Settings className="w-5 h-5" /></button>
|
||||
<div className="w-8 h-8 bg-indigo-500 rounded-full flex items-center justify-center text-white font-bold text-xs shadow-sm ring-2 ring-slate-800">DL</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 2. PROMINENT TAB NAVIGATION */}
|
||||
<div className="bg-white border-b border-gray-200 px-6 flex items-center shadow-sm z-20 h-14">
|
||||
<div className="flex space-x-8 h-full">
|
||||
<button
|
||||
onClick={() => setActiveTab('CHAT')}
|
||||
className={`flex items-center px-2 border-b-2 transition-all h-full ${activeTab === 'CHAT' ? 'border-blue-600 text-blue-600 font-bold' : 'border-transparent text-slate-500 hover:text-slate-700 font-medium'}`}
|
||||
>
|
||||
<MessageSquare className={`w-5 h-5 mr-2 ${activeTab === 'CHAT' ? 'fill-blue-100' : ''}`} />
|
||||
<span className="text-base">智能问答</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('ASSETS')}
|
||||
className={`flex items-center px-2 border-b-2 transition-all h-full ${activeTab === 'ASSETS' ? 'border-blue-600 text-blue-600 font-bold' : 'border-transparent text-slate-500 hover:text-slate-700 font-medium'}`}
|
||||
>
|
||||
<FileText className={`w-5 h-5 mr-2 ${activeTab === 'ASSETS' ? 'fill-blue-100' : ''}`} />
|
||||
<span className="text-base">知识资产</span>
|
||||
<span className="ml-2 bg-gray-100 text-slate-500 text-xs px-2 py-0.5 rounded-full">12</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3. MAIN CONTENT */}
|
||||
<main className="flex-1 overflow-hidden relative">
|
||||
|
||||
{/* === VIEW A: CHAT MODE === */}
|
||||
{activeTab === 'CHAT' && (
|
||||
<div className="flex h-full animate-fade-in">
|
||||
{/* Left: Chat Messages (Width dynamic based on PDF state) */}
|
||||
<div className={`flex-1 flex flex-col bg-white border-r border-gray-200 transition-all duration-300`}>
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
{messages.map(msg => (
|
||||
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`flex items-start max-w-3xl ${msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'}`}>
|
||||
<div className={`w-9 h-9 rounded-full flex items-center justify-center flex-shrink-0 mt-1 shadow-sm ${msg.role === 'user' ? 'bg-indigo-100 text-indigo-700 ml-3' : 'bg-white border border-gray-200 text-blue-600 mr-3'}`}>
|
||||
{msg.role === 'user' ? <User className="w-5 h-5"/> : <Bot className="w-5 h-5"/>}
|
||||
</div>
|
||||
<div className={`p-4 rounded-2xl text-sm leading-relaxed shadow-sm ${msg.role === 'user' ? 'bg-blue-600 text-white rounded-tr-none' : 'bg-slate-50 text-slate-800 rounded-tl-none border border-gray-100'}`}>
|
||||
<div className="whitespace-pre-wrap">{msg.content}</div>
|
||||
{msg.citations && msg.citations.length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-200/50 flex flex-wrap gap-2">
|
||||
{msg.citations.map((cite, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={handleCitationClick}
|
||||
className="bg-white border border-blue-200 text-blue-600 px-2 py-1 rounded text-xs hover:bg-blue-50 transition-colors flex items-center shadow-sm"
|
||||
>
|
||||
<FileText className="w-3 h-3 mr-1" />
|
||||
来源 [{idx + 1}]
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="p-5 border-t border-gray-200 bg-white">
|
||||
<div className="max-w-4xl mx-auto relative">
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
placeholder="请输入临床问题,AI 将基于知识库回答..."
|
||||
className="w-full pl-5 pr-12 py-3.5 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none shadow-sm text-sm"
|
||||
/>
|
||||
<button className="absolute right-2 top-2 p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||
<Send className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: PDF Viewer (Conditional) */}
|
||||
{isPdfOpen && (
|
||||
<div className="w-[45%] flex flex-col bg-slate-100 border-l border-gray-200 shadow-xl z-10 animate-slide-in-right">
|
||||
<div className="h-10 border-b border-gray-200 bg-white flex items-center justify-between px-3">
|
||||
<span className="text-xs font-bold text-slate-700 flex items-center truncate">
|
||||
<FileText className="w-3 h-3 mr-2 text-red-500" />
|
||||
中国高血压防治指南(2024修订版).pdf
|
||||
</span>
|
||||
<button onClick={() => setIsPdfOpen(false)} className="p-1 hover:bg-gray-100 rounded text-slate-500">
|
||||
<X className="w-4 h-4"/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 pdf-pattern p-8 overflow-y-auto flex justify-center">
|
||||
<div className="bg-white shadow-lg w-full max-w-xl h-[1200px] p-10 opacity-95 relative">
|
||||
<div className="w-1/3 h-6 bg-slate-800 mb-8"></div>
|
||||
<div className="space-y-4">
|
||||
<div className="w-full h-3 bg-slate-200"></div>
|
||||
<div className="w-full h-3 bg-slate-200"></div>
|
||||
<div className="w-5/6 h-3 bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="mt-12 border-l-4 border-yellow-400 bg-yellow-50/50 p-4 -mx-4">
|
||||
<div className="w-full h-3 bg-slate-300 mb-2"></div>
|
||||
<div className="w-3/4 h-3 bg-slate-300"></div>
|
||||
</div>
|
||||
<div className="mt-12 space-y-4">
|
||||
<div className="w-full h-3 bg-slate-200"></div>
|
||||
<div className="w-full h-3 bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show Toggle Button only when closed */}
|
||||
{!isPdfOpen && (
|
||||
<button
|
||||
onClick={() => setIsPdfOpen(true)}
|
||||
className="absolute right-0 top-1/2 transform -translate-y-1/2 bg-white border border-gray-200 border-r-0 shadow-md p-2 rounded-l-lg text-slate-500 hover:text-blue-600 z-10"
|
||||
title="展开 PDF 预览"
|
||||
>
|
||||
<PanelRightOpen className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* === VIEW B: ASSETS MODE === */}
|
||||
{activeTab === 'ASSETS' && (
|
||||
<div className="flex flex-col h-full bg-slate-50 p-8 overflow-hidden animate-fade-in">
|
||||
<div className="max-w-7xl mx-auto w-full flex-1 flex flex-col">
|
||||
<div className="flex justify-between items-end mb-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-800">文档资产管理</h2>
|
||||
<p className="text-sm text-slate-500 mt-1">管理该知识库下的所有文件,查看 MinerU 解析状态。</p>
|
||||
</div>
|
||||
<div className="flex space-x-3 relative">
|
||||
{/* Filter Button & Dropdown */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setIsFilterOpen(!isFilterOpen)}
|
||||
className={`flex items-center px-4 py-2 border rounded-lg text-sm font-medium shadow-sm transition-colors ${isFilterOpen ? 'bg-blue-50 border-blue-200 text-blue-700' : 'bg-white border-gray-300 text-slate-700 hover:bg-gray-50'}`}
|
||||
>
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
筛选
|
||||
</button>
|
||||
|
||||
{/* V3 UPDATE: Filter Dropdown Implementation */}
|
||||
{isFilterOpen && (
|
||||
<div className="absolute top-12 right-0 w-72 bg-white rounded-xl shadow-xl border border-gray-200 p-4 z-50 animate-fade-in">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-slate-500 uppercase mb-2">解析状态</h4>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center space-x-2 text-sm text-slate-700 cursor-pointer hover:bg-gray-50 p-1 rounded">
|
||||
<input type="checkbox" className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" defaultChecked />
|
||||
<span>全部</span>
|
||||
</label>
|
||||
<label className="flex items-center space-x-2 text-sm text-slate-700 cursor-pointer hover:bg-gray-50 p-1 rounded">
|
||||
<input type="checkbox" className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
|
||||
<span className="flex items-center"><Loader2 className="w-3 h-3 mr-1 text-blue-500"/> 进行中 (MinerU)</span>
|
||||
</label>
|
||||
<label className="flex items-center space-x-2 text-sm text-slate-700 cursor-pointer hover:bg-gray-50 p-1 rounded">
|
||||
<input type="checkbox" className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
|
||||
<span className="flex items-center"><AlertCircle className="w-3 h-3 mr-1 text-red-500"/> 解析失败</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-px bg-gray-100"></div>
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-slate-500 uppercase mb-2">上传时间</h4>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center space-x-2 text-sm text-slate-700 cursor-pointer hover:bg-gray-50 p-1 rounded">
|
||||
<input type="radio" name="time" className="border-gray-300 text-blue-600 focus:ring-blue-500" />
|
||||
<span>最近 7 天</span>
|
||||
</label>
|
||||
<label className="flex items-center space-x-2 text-sm text-slate-700 cursor-pointer hover:bg-gray-50 p-1 rounded">
|
||||
<input type="radio" name="time" className="border-gray-300 text-blue-600 focus:ring-blue-500" />
|
||||
<span>最近 30 天</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 pt-3 border-t border-gray-100 flex justify-end">
|
||||
<button onClick={() => setIsFilterOpen(false)} className="text-xs text-blue-600 font-bold hover:underline">应用筛选</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button className="flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-bold shadow-md transition-colors">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
上传新文件
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table Container */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl shadow-sm flex-1 overflow-hidden flex flex-col">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="bg-gray-50/50 text-slate-500 font-medium border-b border-gray-200">
|
||||
<tr>
|
||||
<th className="px-6 py-4 w-12"><input type="checkbox" className="rounded border-gray-300"/></th>
|
||||
<th className="px-6 py-4">文件名</th>
|
||||
<th className="px-6 py-4">解析状态 (MinerU Pipeline)</th>
|
||||
<th className="px-6 py-4">文件大小</th>
|
||||
<th className="px-6 py-4">Tokens</th>
|
||||
<th className="px-6 py-4">上传时间</th>
|
||||
<th className="px-6 py-4 text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 bg-white">
|
||||
{files.map(file => (
|
||||
<tr key={file.id} className="hover:bg-blue-50/30 transition-colors group">
|
||||
<td className="px-6 py-4"><input type="checkbox" className="rounded border-gray-300"/></td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center">
|
||||
<div className="w-9 h-9 bg-red-50 text-red-500 rounded-lg flex items-center justify-center mr-3">
|
||||
<FileText className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="font-bold text-slate-700">{file.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex flex-col space-y-1.5 max-w-[180px]">
|
||||
<div>{getStatusBadge(file.status)}</div>
|
||||
{file.status !== 'ready' && (
|
||||
<div className="w-full bg-gray-100 h-1.5 rounded-full overflow-hidden">
|
||||
<div className="bg-blue-500 h-full transition-all duration-300" style={{width: `${file.progress}%`}}></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-slate-500 font-mono">{file.size}</td>
|
||||
<td className="px-6 py-4 text-slate-500 font-mono">{file.tokens}</td>
|
||||
<td className="px-6 py-4 text-slate-400">{file.uploadTime}</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button className="text-slate-400 hover:text-red-500 p-2 rounded-lg hover:bg-red-50 transition-colors opacity-0 group-hover:opacity-100">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const root = createRoot(document.getElementById('root'));
|
||||
root.render(<WorkspaceV3 />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
577
docs/03-业务模块/PKB-个人知识库/01-需求分析/知识库仪表盘V5.html
Normal file
577
docs/03-业务模块/PKB-个人知识库/01-需求分析/知识库仪表盘V5.html
Normal file
@@ -0,0 +1,577 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI 临床医生与医院知识库 Dashboard V5</title>
|
||||
|
||||
<!-- 1. 引入 Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- 2. 配置 Import Map -->
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react": "https://esm.sh/react@18.2.0",
|
||||
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
|
||||
"lucide-react": "https://esm.sh/lucide-react@0.344.0"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- 3. 引入 Babel -->
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
|
||||
<style>
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
.animate-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: scale(0.98); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
/* 增加一个微弱的呼吸动画给新建按钮 */
|
||||
@keyframes pulse-soft {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.2); }
|
||||
50% { box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); }
|
||||
}
|
||||
.hover-pulse:hover {
|
||||
animation: pulse-soft 2s infinite;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 text-slate-800 font-sans antialiased overflow-hidden">
|
||||
|
||||
<div id="root"></div>
|
||||
|
||||
<!-- 4. React 代码逻辑 -->
|
||||
<script type="text/babel" data-type="module">
|
||||
import React, { useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import {
|
||||
Plus, BookOpen, Microscope, FileText, User, X, CheckCircle2,
|
||||
Loader2, ChevronRight, Upload, Database, BrainCircuit, MessageSquare,
|
||||
MoreHorizontal, GraduationCap, Pill, Stethoscope, Wrench, BarChart3,
|
||||
Eraser, ArrowRight, Sparkles, Trash2
|
||||
} from 'lucide-react';
|
||||
|
||||
// --- MOCK DATA: Platform Context ---
|
||||
const PLATFORM_MODULES = [
|
||||
{ name: '智能统计分析', icon: BarChart3, active: false },
|
||||
{ name: '智能数据清洗', icon: Eraser, active: false },
|
||||
{ name: '智能文献', icon: BookOpen, active: false },
|
||||
{ name: 'AI 问答', icon: MessageSquare, active: false },
|
||||
{ name: 'AI 知识库', icon: Database, active: true },
|
||||
];
|
||||
|
||||
// --- MOCK DATA: Knowledge Base Types (Reverted to V3 Clinical Terms) ---
|
||||
const KB_TYPES = [
|
||||
{
|
||||
id: 'GUIDELINE',
|
||||
name: '临床指南',
|
||||
icon: BookOpen,
|
||||
color: 'text-blue-600',
|
||||
bg: 'bg-blue-100',
|
||||
desc: '存储诊疗规范、专家共识。支持精确检索与引用跳转。',
|
||||
tags: ['RAG', '精准溯源']
|
||||
},
|
||||
{
|
||||
id: 'RESEARCH',
|
||||
name: '科研文献',
|
||||
icon: Microscope,
|
||||
color: 'text-purple-600',
|
||||
bg: 'bg-purple-100',
|
||||
desc: '存储同主题文献。支持全文深度阅读、横向对比总结。',
|
||||
tags: ['Long Context', '深度分析']
|
||||
},
|
||||
{
|
||||
id: 'CASE_REPORT',
|
||||
name: '典型病例',
|
||||
icon: Stethoscope,
|
||||
color: 'text-emerald-600',
|
||||
bg: 'bg-emerald-100',
|
||||
desc: '存储疑难病历。支持相似病例检索、临床决策辅助。',
|
||||
tags: ['Multimodal', '时序分析']
|
||||
},
|
||||
{
|
||||
id: 'DRUG_SAFETY',
|
||||
name: '药品安全',
|
||||
icon: Pill,
|
||||
color: 'text-rose-600',
|
||||
bg: 'bg-rose-100',
|
||||
desc: '存储药品说明书。支持配伍禁忌、不良反应查询。',
|
||||
tags: ['RAG', '结构化提取']
|
||||
},
|
||||
{
|
||||
id: 'EXAM',
|
||||
name: '职称考试',
|
||||
icon: GraduationCap,
|
||||
color: 'text-orange-600',
|
||||
bg: 'bg-orange-100',
|
||||
desc: '存储题库与解析。支持考点生成、模拟练习。',
|
||||
tags: ['Hybrid', '题库模式']
|
||||
},
|
||||
{
|
||||
id: 'CUSTOM',
|
||||
name: '自定义',
|
||||
icon: Wrench,
|
||||
color: 'text-slate-600',
|
||||
bg: 'bg-slate-200',
|
||||
desc: '自由配置 AI 引擎参数,混合管理多种文档。',
|
||||
tags: ['Advanced', '自由配置']
|
||||
},
|
||||
];
|
||||
|
||||
// --- MOCK DATA: Existing Assets (Default 3 items) ---
|
||||
const MOCK_KBS = [
|
||||
{
|
||||
id: 1,
|
||||
name: "心内科 2024 临床指南合集",
|
||||
type: "GUIDELINE",
|
||||
docCount: 12,
|
||||
lastActive: "10分钟前",
|
||||
snippet: "包含高血压、心衰、房颤等核心指南",
|
||||
status: "ready"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "GLP-1 药物心脏获益研究",
|
||||
type: "RESEARCH",
|
||||
docCount: 5,
|
||||
lastActive: "昨天",
|
||||
snippet: "NEJM, Lancet 等顶级期刊文献综述",
|
||||
status: "ready"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "2023 年度科室疑难病例归档",
|
||||
type: "CASE_REPORT",
|
||||
docCount: 28,
|
||||
lastActive: "3天前",
|
||||
snippet: "包含影像学报告与病程记录",
|
||||
status: "processing"
|
||||
}
|
||||
];
|
||||
|
||||
function DashboardV5() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [createStep, setCreateStep] = useState(1);
|
||||
const [selectedTypeId, setSelectedTypeId] = useState(null);
|
||||
|
||||
// Create Form State
|
||||
const [formData, setFormData] = useState({ name: '', department: 'Cardiology' });
|
||||
const [files, setFiles] = useState([]);
|
||||
|
||||
// Simulation: File Upload Pipeline
|
||||
const simulateUpload = () => {
|
||||
const newFile = {
|
||||
id: Math.random().toString(),
|
||||
name: `New_Clinical_Protocol_v${files.length + 1}.pdf`,
|
||||
size: "3.5 MB",
|
||||
status: 'uploading',
|
||||
progress: 0
|
||||
};
|
||||
setFiles(prev => [...prev, newFile]);
|
||||
|
||||
let progress = 0;
|
||||
const interval = setInterval(() => {
|
||||
progress += 2;
|
||||
setFiles(prev => prev.map(f => {
|
||||
if (f.id !== newFile.id) return f;
|
||||
let status = 'uploading';
|
||||
if (progress > 15) status = 'analyzing_layout';
|
||||
if (progress > 50) status = 'extracting_table';
|
||||
if (progress > 85) status = 'indexing';
|
||||
if (progress >= 100) status = 'ready';
|
||||
return { ...f, progress: Math.min(progress, 100), status };
|
||||
}));
|
||||
if (progress >= 100) clearInterval(interval);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const getKbTypeConfig = (id) => KB_TYPES.find(t => t.id === id) || KB_TYPES[0];
|
||||
|
||||
const handleCreateOpen = () => {
|
||||
setCreateStep(1);
|
||||
setSelectedTypeId(null);
|
||||
setFormData({ name: '', department: 'Cardiology' });
|
||||
setFiles([]);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const getStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'uploading': return '上传中...';
|
||||
case 'analyzing_layout': return 'MinerU 版面分析...';
|
||||
case 'extracting_table': return '结构化表格提取...';
|
||||
case 'indexing': return '构建向量索引...';
|
||||
case 'ready': return '就绪';
|
||||
default: return '';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-gray-50 font-sans text-slate-800">
|
||||
|
||||
{/* ---------------- 1. PLATFORM GLOBAL NAVIGATION ---------------- */}
|
||||
<header className="bg-white border-b border-gray-200 h-16 flex items-center px-6 justify-between flex-shrink-0 z-30 shadow-sm relative">
|
||||
<div className="flex items-center space-x-8">
|
||||
{/* Logo Area */}
|
||||
<div className="flex items-center space-x-2 mr-2 cursor-pointer hover:opacity-80 transition-opacity">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-indigo-600 to-blue-700 rounded-lg flex items-center justify-center text-white shadow-md">
|
||||
<BrainCircuit className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="text-lg font-bold text-slate-800 tracking-tight">AI 科研平台</span>
|
||||
</div>
|
||||
|
||||
{/* Module Nav */}
|
||||
<nav className="hidden lg:flex space-x-1">
|
||||
{PLATFORM_MODULES.map((module) => {
|
||||
const Icon = module.icon;
|
||||
return (
|
||||
<button
|
||||
key={module.name}
|
||||
className={`flex items-center space-x-2 px-3 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||
module.active
|
||||
? 'bg-blue-50 text-blue-700 ring-1 ring-blue-100'
|
||||
: 'text-slate-500 hover:text-slate-800 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<Icon className={`w-4 h-4 ${module.active ? 'text-blue-600' : 'text-slate-400'}`} />
|
||||
<span>{module.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* User Profile */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="hidden md:flex flex-col items-end mr-1">
|
||||
<span className="text-sm font-semibold text-slate-700">Dr. Li</span>
|
||||
<span className="text-xs text-slate-500">心内科 | 主治医师</span>
|
||||
</div>
|
||||
<div className="w-9 h-9 bg-indigo-100 rounded-full flex items-center justify-center text-indigo-700 font-bold border-2 border-white shadow-sm cursor-pointer hover:ring-2 hover:ring-indigo-200 transition-all">
|
||||
DL
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* ---------------- 2. MAIN CONTENT (1+3 GRID) ---------------- */}
|
||||
<main className="flex-1 overflow-y-auto p-6 md:p-10 w-full max-w-[1600px] mx-auto">
|
||||
|
||||
{/* Layout Strategy: 4 Columns
|
||||
1st Column: Create New (Highlighted)
|
||||
2nd-4th Columns: Existing KBs
|
||||
*/}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
|
||||
{/* --- CARD 1: THE "HERO" CREATE CARD --- */}
|
||||
<button
|
||||
onClick={handleCreateOpen}
|
||||
className="group relative bg-gradient-to-br from-blue-50 to-indigo-50 border-2 border-dashed border-blue-300 rounded-xl p-4 flex flex-col items-center justify-center hover:shadow-lg hover:border-blue-400 hover:from-blue-100 hover:to-indigo-100 transition-all h-[240px] overflow-hidden hover-pulse"
|
||||
>
|
||||
{/* Main Action */}
|
||||
<div className="z-10 flex flex-col items-center mb-5 mt-2">
|
||||
<div className="w-14 h-14 bg-blue-600 rounded-full flex items-center justify-center mb-3 shadow-md group-hover:scale-110 transition-transform duration-300">
|
||||
<Plus className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<span className="font-bold text-lg text-blue-800 group-hover:text-blue-900 tracking-tight">创建知识库</span>
|
||||
</div>
|
||||
|
||||
{/* Micro-Showcase: Capabilities (Added Text Labels) */}
|
||||
<div className="z-10 w-full px-1">
|
||||
<div className="flex justify-between items-center px-1 mb-2 opacity-70">
|
||||
<span className="text-[10px] text-blue-800 font-bold uppercase tracking-wider mx-auto">支持 5 种专业类型</span>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4">
|
||||
{KB_TYPES.slice(0, 5).map(type => {
|
||||
const TypeIcon = type.icon;
|
||||
return (
|
||||
<div key={type.id} className="flex flex-col items-center group/icon" title={type.name}>
|
||||
<div className={`w-8 h-8 rounded-lg bg-white flex items-center justify-center mb-1 shadow-sm border border-blue-100 group-hover/icon:border-blue-300 transition-all`}>
|
||||
<TypeIcon className={`w-4 h-4 ${type.color}`} />
|
||||
</div>
|
||||
{/* Added Text Label Here */}
|
||||
<span className="text-[10px] text-slate-500 font-medium scale-90 whitespace-nowrap group-hover/icon:text-blue-600 transition-colors">
|
||||
{type.name.substring(0, 4)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* --- CARDS 2-4: EXISTING ASSETS --- */}
|
||||
{MOCK_KBS.map(kb => {
|
||||
const style = getKbTypeConfig(kb.type);
|
||||
const TypeIcon = style.icon;
|
||||
|
||||
return (
|
||||
<div key={kb.id} className="bg-white rounded-xl border border-gray-200 p-5 hover:shadow-lg hover:border-blue-200/50 transition-all flex flex-col h-[240px] group relative">
|
||||
{/* Card Header & Body */}
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className={`p-2.5 rounded-lg ${style.bg} ${style.color}`}>
|
||||
<TypeIcon className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
{kb.status === 'processing' && <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />}
|
||||
<button className="text-gray-300 hover:text-gray-600 p-1 rounded hover:bg-gray-100">
|
||||
<MoreHorizontal className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="font-bold text-lg text-slate-800 mb-2 line-clamp-1 group-hover:text-blue-700 transition-colors">{kb.name}</h3>
|
||||
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className={`text-xs font-bold px-2 py-0.5 rounded border border-transparent ${style.bg} ${style.color.replace('text-', 'text-opacity-90 ')}`}>
|
||||
{style.name}库
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-400 mb-3 line-clamp-2 h-8 leading-relaxed">{kb.snippet}</p>
|
||||
</div>
|
||||
|
||||
{/* Card Footer */}
|
||||
<div className="pt-4 mt-2 border-t border-gray-100">
|
||||
<div className="flex items-center justify-between text-xs text-slate-400 mb-3">
|
||||
<span className="flex items-center"><FileText className="w-3 h-3 mr-1"/> {kb.docCount} 份文档</span>
|
||||
<span>{kb.lastActive}</span>
|
||||
</div>
|
||||
<button className="w-full bg-slate-800 hover:bg-blue-600 text-white text-sm font-medium py-2.5 rounded-lg flex items-center justify-center transition-all shadow-sm transform active:scale-[0.98]">
|
||||
<MessageSquare className="w-4 h-4 mr-2" />
|
||||
进入工作台
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* ---------------- 3. THE "WIZARD" MODAL (Updated Terminology) ---------------- */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 animate-in">
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-6xl flex flex-col max-h-[90vh] overflow-hidden">
|
||||
|
||||
{/* Modal Header */}
|
||||
<div className="px-8 py-6 border-b border-gray-100 flex justify-between items-center bg-white z-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-slate-800 flex items-center">
|
||||
{createStep === 1 && <Sparkles className="w-6 h-6 mr-3 text-blue-600" />}
|
||||
{createStep === 1 ? '选择知识库类型' : createStep === 2 ? '基础信息配置' : '上传知识资产'}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500 mt-1.5 ml-0.5">
|
||||
{createStep === 1 ? '不同的业务场景将配置不同的 AI 策略,请根据您的实际需求选择。' :
|
||||
createStep === 2 ? '完善信息以便 AI 更好地扮演专家角色。' : '支持 PDF 批量上传,系统将自动进行 MinerU 深度解析。'}
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-slate-700 bg-gray-50 hover:bg-gray-100 p-2 rounded-full transition-colors">
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Modal Content */}
|
||||
<div className="flex-1 overflow-y-auto bg-slate-50/50 p-8">
|
||||
{/* STEP 1: Type Selection (Reverted to V3 Names) */}
|
||||
{createStep === 1 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{KB_TYPES.map((type) => {
|
||||
const TypeIcon = type.icon;
|
||||
return (
|
||||
<button
|
||||
key={type.id}
|
||||
onClick={() => {
|
||||
setSelectedTypeId(type.id);
|
||||
setCreateStep(2);
|
||||
}}
|
||||
className={`relative flex flex-col items-start p-6 rounded-xl border-2 transition-all duration-200 hover:shadow-xl bg-white text-left group min-h-[180px]
|
||||
${type.id === 'CUSTOM' ? 'border-dashed border-slate-300 hover:border-slate-500' : 'border-transparent hover:border-blue-500 ring-1 ring-slate-100'}
|
||||
`}
|
||||
>
|
||||
<div className="flex w-full justify-between items-start mb-4">
|
||||
<div className={`p-3.5 rounded-xl ${type.bg} ${type.color} group-hover:scale-110 transition-transform duration-300`}>
|
||||
<TypeIcon className="w-8 h-8" />
|
||||
</div>
|
||||
<ArrowRight className="w-5 h-5 text-gray-300 group-hover:text-blue-500 transition-colors" />
|
||||
</div>
|
||||
|
||||
<h3 className="font-bold text-xl text-slate-800 mb-2 group-hover:text-blue-700">{type.name}</h3>
|
||||
<p className="text-sm text-slate-500 leading-relaxed mb-4">{type.desc}</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mt-auto">
|
||||
{type.tags.map(tag => (
|
||||
<span key={tag} className="text-[10px] font-bold px-2 py-1 rounded bg-slate-100 text-slate-600 uppercase tracking-wide border border-slate-200">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* STEP 2: Basic Info */}
|
||||
{createStep === 2 && selectedTypeId && (
|
||||
<div className="max-w-xl mx-auto mt-6">
|
||||
<div className="bg-white p-8 rounded-xl border border-gray-200 shadow-sm space-y-6">
|
||||
<div className={`inline-flex items-center space-x-2 px-4 py-1.5 rounded-full text-sm font-bold ${getKbTypeConfig(selectedTypeId).bg} ${getKbTypeConfig(selectedTypeId).color}`}>
|
||||
{React.createElement(getKbTypeConfig(selectedTypeId).icon, { className: "w-4 h-4" })}
|
||||
<span>正在创建:{getKbTypeConfig(selectedTypeId).name}知识库</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-slate-700 mb-2">知识库名称 <span className="text-red-500">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
||||
placeholder="例如:2024年心衰诊疗指南合集"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all text-base"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-slate-700 mb-2">所属科室 (用于 AI 角色设定)</label>
|
||||
<select
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg bg-white outline-none focus:ring-2 focus:ring-blue-500 text-base"
|
||||
value={formData.department}
|
||||
onChange={(e) => setFormData({...formData, department: e.target.value})}
|
||||
>
|
||||
<option value="Cardiology">心内科</option>
|
||||
<option value="Neurology">神经内科</option>
|
||||
<option value="Oncology">肿瘤科</option>
|
||||
<option value="General">全科</option>
|
||||
</select>
|
||||
<p className="text-xs text-slate-400 mt-2 flex items-center">
|
||||
<User className="w-3 h-3 mr-1" />
|
||||
系统将自动为您设定该科室专家的 Prompt 角色。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* STEP 3: Upload */}
|
||||
{createStep === 3 && (
|
||||
<div className="max-w-3xl mx-auto space-y-6 mt-4">
|
||||
<div
|
||||
onClick={simulateUpload}
|
||||
className="border-2 border-dashed border-gray-300 rounded-xl p-12 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-50 hover:border-blue-500 transition-all group bg-white"
|
||||
>
|
||||
<div className="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
|
||||
<Upload className="w-10 h-10 text-blue-600" />
|
||||
</div>
|
||||
<p className="text-xl font-bold text-slate-700">点击上传 PDF 文件</p>
|
||||
<p className="text-sm text-slate-400 mt-2">支持高清 PDF 及扫描件 (MinerU 深度解析引擎已就绪)</p>
|
||||
</div>
|
||||
|
||||
{files.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
||||
<div className="px-4 py-3 bg-gray-50 border-b border-gray-100 flex justify-between items-center">
|
||||
<span className="text-xs font-bold text-slate-500 uppercase">上传队列 ({files.length})</span>
|
||||
</div>
|
||||
<div className="max-h-[280px] overflow-y-auto p-2 space-y-2">
|
||||
{files.map(f => (
|
||||
<div key={f.id} className="flex items-center p-3 rounded-lg border border-gray-100 hover:bg-gray-50 transition-colors">
|
||||
<div className="w-10 h-10 bg-red-50 rounded-lg flex items-center justify-center text-red-500 mr-4 flex-shrink-0">
|
||||
<FileText className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 mr-4">
|
||||
<div className="flex justify-between mb-1">
|
||||
<span className="font-medium text-slate-800 text-sm truncate">{f.name}</span>
|
||||
<span className="text-xs text-slate-500">{f.size}</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-100 h-1.5 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all duration-300 ${f.status === 'ready' ? 'bg-green-500' : 'bg-blue-600'}`}
|
||||
style={{ width: `${f.progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex justify-between mt-1">
|
||||
<span className={`text-[10px] font-bold ${f.status === 'ready' ? 'text-green-600' : 'text-blue-600'} flex items-center`}>
|
||||
{f.status !== 'ready' && <Loader2 className="w-3 h-3 mr-1 animate-spin" />}
|
||||
{getStatusText(f.status)}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-400">{f.progress}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="text-gray-300 hover:text-red-500 transition-colors">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className="px-8 py-5 border-t border-gray-100 bg-white flex justify-between items-center z-10">
|
||||
<button
|
||||
onClick={() => setCreateStep(Math.max(1, createStep - 1))}
|
||||
disabled={createStep === 1}
|
||||
className={`px-6 py-2.5 rounded-lg font-medium transition-colors ${createStep === 1 ? 'text-gray-300 cursor-not-allowed' : 'text-slate-600 hover:bg-gray-100'}`}
|
||||
>
|
||||
上一步
|
||||
</button>
|
||||
|
||||
{createStep < 3 ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (createStep === 1 && !selectedTypeId) return;
|
||||
if (createStep === 2 && !formData.name) return;
|
||||
setCreateStep(createStep + 1);
|
||||
}}
|
||||
disabled={(createStep === 1 && !selectedTypeId) || (createStep === 2 && !formData.name)}
|
||||
className={`px-10 py-3 rounded-lg text-white font-bold shadow-md transition-all flex items-center ${
|
||||
(createStep === 1 && !selectedTypeId) || (createStep === 2 && !formData.name)
|
||||
? 'bg-gray-300 cursor-not-allowed'
|
||||
: 'bg-blue-600 hover:bg-blue-700 active:scale-95'
|
||||
}`}
|
||||
>
|
||||
下一步 <ChevronRight className="w-4 h-4 ml-1" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="px-10 py-3 rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white font-bold shadow-lg hover:shadow-xl transition-all active:scale-95 flex items-center"
|
||||
>
|
||||
<CheckCircle2 className="w-5 h-5 mr-2" />
|
||||
完成并进入工作台
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const root = createRoot(document.getElementById('root'));
|
||||
root.render(<DashboardV5 />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user