Files
HaHafeng 5a17d096a7 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
2026-01-06 22:15:42 +08:00

577 lines
36 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 临床医生与医院知识库 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>