Files
ai-videoassistant-frontend/pages/KnowledgeBase.tsx
2026-02-04 18:36:40 +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>
);
};