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
425 lines
31 KiB
HTML
425 lines
31 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>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> |