Files
AI-VideoAssistant/web/pages/KnowledgeBase.tsx
Xin Wang d96ffdeda4 Add web
2026-02-06 20:43:35 +08:00

299 lines
12 KiB
TypeScript
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.
import React, { useState, useRef } from 'react';
import { Search, Plus, FileText, Upload, ArrowLeft, CloudUpload, File as FileIcon, X } from 'lucide-react';
import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Card, Dialog } from '../components/UI';
import { mockKnowledgeBases } from '../services/mockData';
import { KnowledgeBase } from '../types';
export const KnowledgeBasePage: React.FC = () => {
const [view, setView] = useState<'list' | 'detail'>('list');
const [selectedKb, setSelectedKb] = useState<KnowledgeBase | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [kbs, setKbs] = useState(mockKnowledgeBases);
const [isUploadOpen, setIsUploadOpen] = useState(false);
const [isCreateKbOpen, setIsCreateKbOpen] = useState(false);
const [newKbName, setNewKbName] = useState('');
const filteredKbs = kbs.filter(kb => kb.name.toLowerCase().includes(searchTerm.toLowerCase()));
const handleSelect = (kb: KnowledgeBase) => {
setSelectedKb(kb);
setView('detail');
};
const handleImportClick = () => {
setIsUploadOpen(true);
};
const handleCreateKb = () => {
if (!newKbName.trim()) return;
const newKb: KnowledgeBase = {
id: `kb_${Date.now()}`,
name: newKbName.trim(),
creator: 'Admin User',
createdAt: new Date().toISOString().split('T')[0],
documents: []
};
setKbs([newKb, ...kbs]);
setIsCreateKbOpen(false);
setNewKbName('');
};
if (view === 'detail' && selectedKb) {
return (
<div className="py-4 pb-10">
<KnowledgeBaseDetail
kb={selectedKb}
onBack={() => setView('list')}
onImport={handleImportClick}
/>
<UploadModal isOpen={isUploadOpen} onClose={() => setIsUploadOpen(false)} />
</div>
);
}
return (
<div className="space-y-6 animate-in fade-in py-4 pb-10">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold tracking-tight text-white">知识库</h1>
</div>
{/* Search Bar - Layout aligned with History Page and width filled */}
<div className="bg-card/50 p-4 rounded-lg border border-white/5 shadow-sm">
<div className="relative w-full">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜索知识库名称..."
className="pl-9 border-0 bg-white/5 w-full"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredKbs.map(kb => (
<Card
key={kb.id}
className="p-6 hover:border-primary/50 transition-colors cursor-pointer group"
>
<div onClick={() => handleSelect(kb)}>
<div className="flex items-start justify-between mb-4">
<div className="p-2 bg-primary/10 rounded-lg text-primary">
<FileText className="h-6 w-6" />
</div>
</div>
<h3 className="text-lg font-semibold group-hover:text-primary transition-colors text-white">{kb.name}</h3>
<div className="mt-4 space-y-1 text-sm text-muted-foreground">
<p>文档数量: {kb.documents.length}</p>
<p>创建人: {kb.creator}</p>
<p>创建时间: {kb.createdAt}</p>
</div>
</div>
</Card>
))}
{/* Add New Placeholder */}
<div
onClick={() => setIsCreateKbOpen(true)}
className="border border-dashed border-white/10 rounded-xl p-6 flex flex-col items-center justify-center text-muted-foreground hover:bg-white/5 hover:border-primary/30 transition-all cursor-pointer min-h-[200px]"
>
<Plus className="h-8 w-8 mb-2 opacity-50" />
<span>新建知识库</span>
</div>
</div>
{/* New Knowledge Base Dialog */}
<Dialog
isOpen={isCreateKbOpen}
onClose={() => setIsCreateKbOpen(false)}
title="新建知识库"
footer={
<>
<Button variant="ghost" onClick={() => setIsCreateKbOpen(false)}>取消</Button>
<Button onClick={handleCreateKb} disabled={!newKbName.trim()}>确认创建</Button>
</>
}
>
<div className="space-y-4">
<div className="space-y-1.5">
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">知识库名称</label>
<Input
value={newKbName}
onChange={(e) => setNewKbName(e.target.value)}
placeholder="请输入知识库名称..."
autoFocus
onKeyDown={(e) => e.key === 'Enter' && handleCreateKb()}
/>
</div>
<p className="text-xs text-muted-foreground">
知识库用于存储私域文档AI 小助手在回答问题时会优先检索绑定的知识库内容。
</p>
</div>
</Dialog>
</div>
);
};
const KnowledgeBaseDetail: React.FC<{
kb: KnowledgeBase;
onBack: () => void;
onImport: () => void;
}> = ({ kb, onBack, onImport }) => {
const [docSearch, setDocSearch] = useState('');
const filteredDocs = kb.documents.filter(d => d.name.toLowerCase().includes(docSearch.toLowerCase()));
return (
<div className="space-y-6 animate-in slide-in-from-right-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Button variant="ghost" size="icon" onClick={onBack}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-2xl font-bold text-white">{kb.name}</h1>
<p className="text-sm text-muted-foreground">创建于 {kb.createdAt} · by {kb.creator}</p>
</div>
</div>
<Button onClick={onImport}>
<Upload className="mr-2 h-4 w-4" /> 新增知识 (导入)
</Button>
</div>
<Card className="overflow-hidden border-white/5">
<div className="p-4 border-b border-white/5 flex justify-between items-center bg-white/5">
<h3 className="font-medium text-white">文档列表</h3>
<div className="w-64">
<Input
placeholder="搜索文档..."
value={docSearch}
onChange={(e) => setDocSearch(e.target.value)}
className="bg-black/20 border-transparent focus:bg-black/40"
/>
</div>
</div>
<table className="w-full text-sm">
<TableHeader>
<TableRow>
<TableHead>文档名称</TableHead>
<TableHead>大小</TableHead>
<TableHead>上传时间</TableHead>
<TableHead className="text-right">操作</TableHead>
</TableRow>
</TableHeader>
<tbody>
{filteredDocs.length > 0 ? filteredDocs.map(doc => (
<TableRow key={doc.id}>
<TableCell className="font-medium flex items-center text-white">
<FileText className="h-4 w-4 mr-2 text-primary"/> {doc.name}
</TableCell>
<TableCell className="text-muted-foreground">{doc.size}</TableCell>
<TableCell className="text-muted-foreground">{doc.uploadDate}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" className="text-destructive hover:text-destructive/80">删除</Button>
</TableCell>
</TableRow>
)) : (
<TableRow>
<TableCell colSpan={4} className="text-center py-8 text-muted-foreground">暂无文档</TableCell>
</TableRow>
)}
</tbody>
</table>
</Card>
</div>
);
};
const UploadModal: React.FC<{ isOpen: boolean; onClose: () => void }> = ({ isOpen, onClose }) => {
const [dragActive, setDragActive] = useState(false);
const [files, setFiles] = useState<File[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const handleDrag = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
setFiles(prev => [...prev, ...Array.from(e.dataTransfer.files)]);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
if (e.target.files && e.target.files[0]) {
setFiles(prev => [...prev, ...Array.from(e.target.files || [])]);
}
};
const removeFile = (idx: number) => {
setFiles(prev => prev.filter((_, i) => i !== idx));
};
return (
<Dialog
isOpen={isOpen}
onClose={onClose}
title="上传知识文档"
footer={
<>
<Button variant="ghost" onClick={onClose}>取消</Button>
<Button onClick={() => { alert('Upload Started!'); onClose(); setFiles([]); }}>确认上传</Button>
</>
}
>
<div
className={`relative flex flex-col items-center justify-center w-full h-48 rounded-lg border-2 border-dashed transition-colors ${dragActive ? "border-primary bg-primary/10" : "border-white/10 bg-white/5 hover:bg-white/10"}`}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
onClick={() => inputRef.current?.click()}
>
<input
ref={inputRef}
type="file"
multiple
className="hidden"
onChange={handleChange}
accept=".pdf,.doc,.docx,.txt,.md"
/>
<CloudUpload className={`h-10 w-10 mb-3 ${dragActive ? 'text-primary' : 'text-muted-foreground'}`} />
<p className="text-sm text-muted-foreground text-center">
<span className="font-semibold text-primary">点击上传</span> 或将文件拖拽到此处
</p>
<p className="text-xs text-muted-foreground mt-1 text-white/50">支持 PDF, DOCX, TXT (Max 10MB)</p>
</div>
{files.length > 0 && (
<div className="mt-4 space-y-2 max-h-40 overflow-y-auto pr-1 custom-scrollbar">
{files.map((file, idx) => (
<div key={idx} className="flex items-center justify-between p-2 rounded-md bg-white/5 border border-white/5">
<div className="flex items-center space-x-2 overflow-hidden">
<FileIcon className="h-4 w-4 text-primary shrink-0" />
<span className="text-sm truncate max-w-[200px] text-white">{file.name}</span>
<span className="text-xs text-muted-foreground">({(file.size / 1024).toFixed(1)} KB)</span>
</div>
<button onClick={() => removeFile(idx)} className="text-muted-foreground hover:text-destructive transition-colors">
<X className="h-4 w-4" />
</button>
</div>
))}
</div>
)}
</Dialog>
);
};