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

425 lines
31 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 知识库工作台 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>