Update knowledge base layout
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import { Search, Plus, FileText, Upload, ArrowLeft, CloudUpload, File as FileIcon, X, Pencil, Trash2, Settings2 } from 'lucide-react';
|
import { Search, Plus, FileText, Upload, ArrowLeft, CloudUpload, File as FileIcon, X, Pencil, Trash2, Settings2, MoreVertical } from 'lucide-react';
|
||||||
import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Card, Dialog, Badge } from '../components/UI';
|
import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Card, Dialog, Badge } from '../components/UI';
|
||||||
import { KnowledgeBase } from '../types';
|
import { KnowledgeBase } from '../types';
|
||||||
import { createKnowledgeBase, deleteKnowledgeBase, deleteKnowledgeDocument, fetchKnowledgeBases, updateKnowledgeBase, uploadKnowledgeDocument } from '../services/backendApi';
|
import { createKnowledgeBase, deleteKnowledgeBase, deleteKnowledgeDocument, fetchKnowledgeBases, fetchLLMModels, updateKnowledgeBase, uploadKnowledgeDocument } from '../services/backendApi';
|
||||||
|
|
||||||
const EMBEDDING_OPTIONS = [
|
const EMBEDDING_OPTIONS = [
|
||||||
'text-embedding-3-small',
|
'text-embedding-3-small',
|
||||||
@@ -19,6 +19,8 @@ export const KnowledgeBasePage: React.FC = () => {
|
|||||||
const [isKbModalOpen, setIsKbModalOpen] = useState(false);
|
const [isKbModalOpen, setIsKbModalOpen] = useState(false);
|
||||||
const [editingKb, setEditingKb] = useState<KnowledgeBase | null>(null);
|
const [editingKb, setEditingKb] = useState<KnowledgeBase | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [embeddingOptions, setEmbeddingOptions] = useState<string[]>(EMBEDDING_OPTIONS);
|
||||||
|
const [openMenuKbId, setOpenMenuKbId] = useState<string | null>(null);
|
||||||
|
|
||||||
const [kbName, setKbName] = useState('');
|
const [kbName, setKbName] = useState('');
|
||||||
const [kbDescription, setKbDescription] = useState('');
|
const [kbDescription, setKbDescription] = useState('');
|
||||||
@@ -48,6 +50,28 @@ export const KnowledgeBasePage: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshKnowledgeBases();
|
refreshKnowledgeBases();
|
||||||
|
const loadEmbeddingModels = async () => {
|
||||||
|
try {
|
||||||
|
const models = await fetchLLMModels();
|
||||||
|
const fromDb = models
|
||||||
|
.filter((m) => m.type === 'embedding')
|
||||||
|
.map((m) => (m.modelName || m.name || '').trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const merged = Array.from(new Set([...fromDb, ...EMBEDDING_OPTIONS]));
|
||||||
|
if (merged.length > 0) {
|
||||||
|
setEmbeddingOptions(merged);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setEmbeddingOptions(EMBEDDING_OPTIONS);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadEmbeddingModels();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onDocClick = () => setOpenMenuKbId(null);
|
||||||
|
document.addEventListener('click', onDocClick);
|
||||||
|
return () => document.removeEventListener('click', onDocClick);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSelect = (kb: KnowledgeBase) => {
|
const handleSelect = (kb: KnowledgeBase) => {
|
||||||
@@ -207,45 +231,57 @@ export const KnowledgeBasePage: React.FC = () => {
|
|||||||
<Card
|
<Card
|
||||||
key={kb.id}
|
key={kb.id}
|
||||||
className="p-6 hover:border-primary/50 transition-colors cursor-pointer group relative"
|
className="p-6 hover:border-primary/50 transition-colors cursor-pointer group relative"
|
||||||
|
onClick={() => openEditKb(kb)}
|
||||||
>
|
>
|
||||||
<div className="absolute top-3 right-3 flex items-center gap-1 z-10">
|
<div className="absolute top-3 right-3 z-20">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
title="更多操作"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
openEditKb(kb);
|
setOpenMenuKbId((prev) => (prev === kb.id ? null : kb.id));
|
||||||
}}
|
}}
|
||||||
title="编辑知识库"
|
|
||||||
>
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<MoreVertical className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{openMenuKbId === kb.id && (
|
||||||
variant="ghost"
|
<div
|
||||||
size="icon"
|
className="absolute right-0 mt-1 w-36 rounded-md border border-white/10 bg-card/95 backdrop-blur-md shadow-lg overflow-hidden"
|
||||||
className="text-destructive hover:text-destructive"
|
onClick={(e) => e.stopPropagation()}
|
||||||
onClick={(e) => {
|
>
|
||||||
e.stopPropagation();
|
<button
|
||||||
handleDeleteKb(kb);
|
className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 text-foreground"
|
||||||
}}
|
onClick={() => {
|
||||||
title="删除知识库"
|
setOpenMenuKbId(null);
|
||||||
>
|
handleSelect(kb);
|
||||||
<Trash2 className="h-4 w-4" />
|
}}
|
||||||
</Button>
|
>
|
||||||
</div>
|
管理文档
|
||||||
<div onClick={() => handleSelect(kb)}>
|
</button>
|
||||||
<div className="flex items-start justify-between mb-4">
|
<button
|
||||||
<div className="p-2 bg-primary/10 rounded-lg text-primary">
|
className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 text-destructive"
|
||||||
<FileText className="h-6 w-6" />
|
onClick={() => {
|
||||||
|
setOpenMenuKbId(null);
|
||||||
|
handleDeleteKb(kb);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline">{kb.embeddingModel || 'embedding'}</Badge>
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold group-hover:text-primary transition-colors text-white">{kb.name}</h3>
|
<div className="flex items-start justify-between mb-4">
|
||||||
<div className="mt-4 space-y-1 text-sm text-muted-foreground">
|
<div className="p-2 bg-primary/10 rounded-lg text-primary">
|
||||||
<p>文档数量: {kb.documents.length}</p>
|
<FileText className="h-6 w-6" />
|
||||||
<p>分片数量: {kb.chunkSize ?? 500}/{kb.chunkOverlap ?? 50}</p>
|
|
||||||
<p>创建时间: {kb.createdAt}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Badge variant="outline">{kb.embeddingModel || 'embedding'}</Badge>
|
||||||
|
</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.chunkSize ?? 500}/{kb.chunkOverlap ?? 50}</p>
|
||||||
|
<p>创建时间: {kb.createdAt}</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
@@ -282,6 +318,7 @@ export const KnowledgeBasePage: React.FC = () => {
|
|||||||
setKbChunkSize={setKbChunkSize}
|
setKbChunkSize={setKbChunkSize}
|
||||||
kbChunkOverlap={kbChunkOverlap}
|
kbChunkOverlap={kbChunkOverlap}
|
||||||
setKbChunkOverlap={setKbChunkOverlap}
|
setKbChunkOverlap={setKbChunkOverlap}
|
||||||
|
embeddingOptions={embeddingOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -303,6 +340,7 @@ const KnowledgeBaseModal: React.FC<{
|
|||||||
setKbChunkSize: (v: number) => void;
|
setKbChunkSize: (v: number) => void;
|
||||||
kbChunkOverlap: number;
|
kbChunkOverlap: number;
|
||||||
setKbChunkOverlap: (v: number) => void;
|
setKbChunkOverlap: (v: number) => void;
|
||||||
|
embeddingOptions: string[];
|
||||||
}> = ({
|
}> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -319,6 +357,7 @@ const KnowledgeBaseModal: React.FC<{
|
|||||||
setKbChunkSize,
|
setKbChunkSize,
|
||||||
kbChunkOverlap,
|
kbChunkOverlap,
|
||||||
setKbChunkOverlap,
|
setKbChunkOverlap,
|
||||||
|
embeddingOptions,
|
||||||
}) => (
|
}) => (
|
||||||
<Dialog
|
<Dialog
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@@ -361,7 +400,7 @@ const KnowledgeBaseModal: React.FC<{
|
|||||||
value={kbEmbeddingModel}
|
value={kbEmbeddingModel}
|
||||||
onChange={(e) => setKbEmbeddingModel(e.target.value)}
|
onChange={(e) => setKbEmbeddingModel(e.target.value)}
|
||||||
>
|
>
|
||||||
{EMBEDDING_OPTIONS.map((m) => (
|
{embeddingOptions.map((m) => (
|
||||||
<option key={m} value={m}>{m}</option>
|
<option key={m} value={m}>{m}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
Reference in New Issue
Block a user