Update knowledge frontend

This commit is contained in:
Xin Wang
2026-02-09 07:36:47 +08:00
parent 7206c313d2
commit bdd5a7a274
3 changed files with 445 additions and 199 deletions

View File

@@ -1,9 +1,14 @@
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 } from 'lucide-react'; import { Search, Plus, FileText, Upload, ArrowLeft, CloudUpload, File as FileIcon, X, Pencil, Trash2, Settings2 } from 'lucide-react';
import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Card, Dialog } 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, deleteKnowledgeDocument, fetchKnowledgeBases, uploadKnowledgeDocument } from '../services/backendApi'; import { createKnowledgeBase, deleteKnowledgeBase, deleteKnowledgeDocument, fetchKnowledgeBases, updateKnowledgeBase, uploadKnowledgeDocument } from '../services/backendApi';
const EMBEDDING_OPTIONS = [
'text-embedding-3-small',
'text-embedding-3-large',
'bge-small-zh',
];
export const KnowledgeBasePage: React.FC = () => { export const KnowledgeBasePage: React.FC = () => {
const [view, setView] = useState<'list' | 'detail'>('list'); const [view, setView] = useState<'list' | 'detail'>('list');
@@ -11,11 +16,18 @@ export const KnowledgeBasePage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [kbs, setKbs] = useState<KnowledgeBase[]>([]); const [kbs, setKbs] = useState<KnowledgeBase[]>([]);
const [isUploadOpen, setIsUploadOpen] = useState(false); const [isUploadOpen, setIsUploadOpen] = useState(false);
const [isCreateKbOpen, setIsCreateKbOpen] = useState(false); const [isKbModalOpen, setIsKbModalOpen] = useState(false);
const [newKbName, setNewKbName] = useState(''); const [editingKb, setEditingKb] = useState<KnowledgeBase | null>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const filteredKbs = kbs.filter(kb => kb.name.toLowerCase().includes(searchTerm.toLowerCase())); const [kbName, setKbName] = useState('');
const [kbDescription, setKbDescription] = useState('');
const [kbEmbeddingModel, setKbEmbeddingModel] = useState('text-embedding-3-small');
const [kbChunkSize, setKbChunkSize] = useState(500);
const [kbChunkOverlap, setKbChunkOverlap] = useState(50);
const [isSavingKb, setIsSavingKb] = useState(false);
const filteredKbs = kbs.filter((kb) => kb.name.toLowerCase().includes(searchTerm.toLowerCase()));
const refreshKnowledgeBases = async () => { const refreshKnowledgeBases = async () => {
setIsLoading(true); setIsLoading(true);
@@ -26,9 +38,9 @@ export const KnowledgeBasePage: React.FC = () => {
const nextSelected = list.find((item) => item.id === selectedKb.id) || null; const nextSelected = list.find((item) => item.id === selectedKb.id) || null;
setSelectedKb(nextSelected); setSelectedKb(nextSelected);
} }
} catch (error) { } catch (error: any) {
console.error(error); console.error(error);
alert('加载知识库失败,请检查后端服务。'); alert(error?.message || '加载知识库失败,请检查后端服务。');
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -43,20 +55,85 @@ export const KnowledgeBasePage: React.FC = () => {
setView('detail'); setView('detail');
}; };
const handleImportClick = () => { const openCreateKb = () => {
setIsUploadOpen(true); setEditingKb(null);
setKbName('');
setKbDescription('');
setKbEmbeddingModel('text-embedding-3-small');
setKbChunkSize(500);
setKbChunkOverlap(50);
setIsKbModalOpen(true);
}; };
const handleCreateKb = async () => { const openEditKb = (kb: KnowledgeBase) => {
if (!newKbName.trim()) return; setEditingKb(kb);
setKbName(kb.name || '');
setKbDescription(kb.description || '');
setKbEmbeddingModel(kb.embeddingModel || 'text-embedding-3-small');
setKbChunkSize(kb.chunkSize ?? 500);
setKbChunkOverlap(kb.chunkOverlap ?? 50);
setIsKbModalOpen(true);
};
const handleSaveKb = async () => {
if (!kbName.trim()) {
alert('请输入知识库名称');
return;
}
if (kbChunkSize <= 0) {
alert('Chunk Size 必须大于 0');
return;
}
if (kbChunkOverlap < 0) {
alert('Chunk Overlap 不能小于 0');
return;
}
if (kbChunkOverlap >= kbChunkSize) {
alert('Chunk Overlap 必须小于 Chunk Size');
return;
}
try { try {
await createKnowledgeBase(newKbName.trim()); setIsSavingKb(true);
if (editingKb) {
await updateKnowledgeBase(editingKb.id, {
name: kbName.trim(),
description: kbDescription,
embeddingModel: kbEmbeddingModel,
chunkSize: kbChunkSize,
chunkOverlap: kbChunkOverlap,
});
} else {
await createKnowledgeBase({
name: kbName.trim(),
description: kbDescription,
embeddingModel: kbEmbeddingModel,
chunkSize: kbChunkSize,
chunkOverlap: kbChunkOverlap,
});
}
setIsKbModalOpen(false);
await refreshKnowledgeBases(); await refreshKnowledgeBases();
setIsCreateKbOpen(false); } catch (error: any) {
setNewKbName('');
} catch (error) {
console.error(error); console.error(error);
alert('新建知识库失败。'); alert(error?.message || (editingKb ? '更新知识库失败。' : '新建知识库失败。'));
} finally {
setIsSavingKb(false);
}
};
const handleDeleteKb = async (kb: KnowledgeBase) => {
if (!confirm(`确认删除知识库「${kb.name}」吗?此操作会删除其所有文档和向量数据。`)) return;
try {
await deleteKnowledgeBase(kb.id);
if (selectedKb?.id === kb.id) {
setSelectedKb(null);
setView('list');
}
await refreshKnowledgeBases();
} catch (error: any) {
console.error(error);
alert(error?.message || '删除知识库失败。');
} }
}; };
@@ -66,14 +143,16 @@ export const KnowledgeBasePage: React.FC = () => {
<KnowledgeBaseDetail <KnowledgeBaseDetail
kb={selectedKb} kb={selectedKb}
onBack={() => setView('list')} onBack={() => setView('list')}
onImport={handleImportClick} onImport={() => setIsUploadOpen(true)}
onEdit={() => openEditKb(selectedKb)}
onDelete={() => handleDeleteKb(selectedKb)}
onDeleteDocument={async (docId) => { onDeleteDocument={async (docId) => {
try { try {
await deleteKnowledgeDocument(selectedKb.id, docId); await deleteKnowledgeDocument(selectedKb.id, docId);
await refreshKnowledgeBases(); await refreshKnowledgeBases();
} catch (error) { } catch (error: any) {
console.error(error); console.error(error);
alert('删除文档失败。'); alert(error?.message || '删除文档失败。');
} }
}} }}
/> />
@@ -83,6 +162,24 @@ export const KnowledgeBasePage: React.FC = () => {
onClose={() => setIsUploadOpen(false)} onClose={() => setIsUploadOpen(false)}
onUploaded={refreshKnowledgeBases} onUploaded={refreshKnowledgeBases}
/> />
<KnowledgeBaseModal
isOpen={isKbModalOpen}
onClose={() => setIsKbModalOpen(false)}
onSave={handleSaveKb}
saving={isSavingKb}
editingKb={editingKb}
kbName={kbName}
setKbName={setKbName}
kbDescription={kbDescription}
setKbDescription={setKbDescription}
kbEmbeddingModel={kbEmbeddingModel}
setKbEmbeddingModel={setKbEmbeddingModel}
kbChunkSize={kbChunkSize}
setKbChunkSize={setKbChunkSize}
kbChunkOverlap={kbChunkOverlap}
setKbChunkOverlap={setKbChunkOverlap}
/>
</div> </div>
); );
} }
@@ -93,7 +190,6 @@ export const KnowledgeBasePage: React.FC = () => {
<h1 className="text-2xl font-bold tracking-tight text-white"></h1> <h1 className="text-2xl font-bold tracking-tight text-white"></h1>
</div> </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="bg-card/50 p-4 rounded-lg border border-white/5 shadow-sm">
<div className="relative w-full"> <div className="relative w-full">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
@@ -107,35 +203,61 @@ export const KnowledgeBasePage: React.FC = () => {
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredKbs.map(kb => ( {filteredKbs.map((kb) => (
<Card <Card
key={kb.id} key={kb.id}
className="p-6 hover:border-primary/50 transition-colors cursor-pointer group" className="p-6 hover:border-primary/50 transition-colors cursor-pointer group relative"
> >
<div className="absolute top-3 right-3 flex items-center gap-1 z-10">
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
openEditKb(kb);
}}
title="编辑知识库"
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="text-destructive hover:text-destructive"
onClick={(e) => {
e.stopPropagation();
handleDeleteKb(kb);
}}
title="删除知识库"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<div onClick={() => handleSelect(kb)}> <div onClick={() => handleSelect(kb)}>
<div className="flex items-start justify-between mb-4"> <div className="flex items-start justify-between mb-4">
<div className="p-2 bg-primary/10 rounded-lg text-primary"> <div className="p-2 bg-primary/10 rounded-lg text-primary">
<FileText className="h-6 w-6" /> <FileText className="h-6 w-6" />
</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> <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"> <div className="mt-4 space-y-1 text-sm text-muted-foreground">
<p>: {kb.documents.length}</p> <p>: {kb.documents.length}</p>
<p>: {kb.creator}</p> <p>: {kb.chunkSize ?? 500}/{kb.chunkOverlap ?? 50}</p>
<p>: {kb.createdAt}</p> <p>: {kb.createdAt}</p>
</div> </div>
</div> </div>
</Card> </Card>
))} ))}
{/* Add New Placeholder */}
<div <div
onClick={() => setIsCreateKbOpen(true)} onClick={openCreateKb}
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]" 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" /> <Plus className="h-8 w-8 mb-2 opacity-50" />
<span></span> <span></span>
</div> </div>
{!isLoading && filteredKbs.length === 0 && ( {!isLoading && filteredKbs.length === 0 && (
<div className="col-span-full text-center text-muted-foreground py-8"></div> <div className="col-span-full text-center text-muted-foreground py-8"></div>
)} )}
@@ -144,15 +266,68 @@ export const KnowledgeBasePage: React.FC = () => {
)} )}
</div> </div>
{/* New Knowledge Base Dialog */} <KnowledgeBaseModal
isOpen={isKbModalOpen}
onClose={() => setIsKbModalOpen(false)}
onSave={handleSaveKb}
saving={isSavingKb}
editingKb={editingKb}
kbName={kbName}
setKbName={setKbName}
kbDescription={kbDescription}
setKbDescription={setKbDescription}
kbEmbeddingModel={kbEmbeddingModel}
setKbEmbeddingModel={setKbEmbeddingModel}
kbChunkSize={kbChunkSize}
setKbChunkSize={setKbChunkSize}
kbChunkOverlap={kbChunkOverlap}
setKbChunkOverlap={setKbChunkOverlap}
/>
</div>
);
};
const KnowledgeBaseModal: React.FC<{
isOpen: boolean;
onClose: () => void;
onSave: () => Promise<void>;
saving: boolean;
editingKb: KnowledgeBase | null;
kbName: string;
setKbName: (v: string) => void;
kbDescription: string;
setKbDescription: (v: string) => void;
kbEmbeddingModel: string;
setKbEmbeddingModel: (v: string) => void;
kbChunkSize: number;
setKbChunkSize: (v: number) => void;
kbChunkOverlap: number;
setKbChunkOverlap: (v: number) => void;
}> = ({
isOpen,
onClose,
onSave,
saving,
editingKb,
kbName,
setKbName,
kbDescription,
setKbDescription,
kbEmbeddingModel,
setKbEmbeddingModel,
kbChunkSize,
setKbChunkSize,
kbChunkOverlap,
setKbChunkOverlap,
}) => (
<Dialog <Dialog
isOpen={isCreateKbOpen} isOpen={isOpen}
onClose={() => setIsCreateKbOpen(false)} onClose={onClose}
title="新建知识库" title={editingKb ? '编辑知识库' : '新建知识库'}
footer={ footer={
<> <>
<Button variant="ghost" onClick={() => setIsCreateKbOpen(false)}></Button> <Button variant="ghost" onClick={onClose}></Button>
<Button onClick={handleCreateKb} disabled={!newKbName.trim()}></Button> <Button onClick={onSave} disabled={saving || !kbName.trim()}>{saving ? '保存中...' : (editingKb ? '保存修改' : '确认创建')}</Button>
</> </>
} }
> >
@@ -160,30 +335,64 @@ export const KnowledgeBasePage: React.FC = () => {
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block"></label> <label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block"></label>
<Input <Input
value={newKbName} value={kbName}
onChange={(e) => setNewKbName(e.target.value)} onChange={(e) => setKbName(e.target.value)}
placeholder="请输入知识库名称..." placeholder="请输入知识库名称..."
autoFocus autoFocus
onKeyDown={(e) => e.key === 'Enter' && handleCreateKb()} onKeyDown={(e) => e.key === 'Enter' && onSave()}
/> />
</div> </div>
<div className="space-y-1.5">
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block"></label>
<textarea
className="flex min-h-[80px] w-full rounded-md border border-white/10 bg-white/5 px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-white"
value={kbDescription}
onChange={(e) => setKbDescription(e.target.value)}
placeholder="描述知识库用途与内容范围..."
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="space-y-1.5">
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Embedding Model</label>
<select
className="flex h-9 w-full rounded-md border-0 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground"
value={kbEmbeddingModel}
onChange={(e) => setKbEmbeddingModel(e.target.value)}
>
{EMBEDDING_OPTIONS.map((m) => (
<option key={m} value={m}>{m}</option>
))}
</select>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Chunk Size</label>
<Input type="number" min={1} value={kbChunkSize} onChange={(e) => setKbChunkSize(parseInt(e.target.value || '0', 10))} />
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Chunk Overlap</label>
<Input type="number" min={0} value={kbChunkOverlap} onChange={(e) => setKbChunkOverlap(parseInt(e.target.value || '0', 10))} />
</div>
</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
AI 使 ChromaDB Embedding Model
</p> </p>
</div> </div>
</Dialog> </Dialog>
</div>
); );
};
const KnowledgeBaseDetail: React.FC<{ const KnowledgeBaseDetail: React.FC<{
kb: KnowledgeBase; kb: KnowledgeBase;
onBack: () => void; onBack: () => void;
onImport: () => void; onImport: () => void;
onEdit: () => void;
onDelete: () => void;
onDeleteDocument: (docId: string) => void; onDeleteDocument: (docId: string) => void;
}> = ({ kb, onBack, onImport, onDeleteDocument }) => { }> = ({ kb, onBack, onImport, onEdit, onDelete, onDeleteDocument }) => {
const [docSearch, setDocSearch] = useState(''); const [docSearch, setDocSearch] = useState('');
const filteredDocs = kb.documents.filter(d => d.name.toLowerCase().includes(docSearch.toLowerCase())); const filteredDocs = kb.documents.filter((d) => d.name.toLowerCase().includes(docSearch.toLowerCase()));
return ( return (
<div className="space-y-6 animate-in slide-in-from-right-4"> <div className="space-y-6 animate-in slide-in-from-right-4">
@@ -194,12 +403,14 @@ const KnowledgeBaseDetail: React.FC<{
</Button> </Button>
<div> <div>
<h1 className="text-2xl font-bold text-white">{kb.name}</h1> <h1 className="text-2xl font-bold text-white">{kb.name}</h1>
<p className="text-sm text-muted-foreground"> {kb.createdAt} · by {kb.creator}</p> <p className="text-sm text-muted-foreground"> {kb.createdAt} · {kb.embeddingModel} · by {kb.creator}</p>
</div> </div>
</div> </div>
<Button onClick={onImport}> <div className="flex items-center gap-2">
<Upload className="mr-2 h-4 w-4" /> () <Button variant="outline" onClick={onEdit}><Settings2 className="mr-2 h-4 w-4" /> </Button>
</Button> <Button onClick={onImport}><Upload className="mr-2 h-4 w-4" /> ()</Button>
<Button variant="ghost" className="text-destructive" onClick={onDelete}><Trash2 className="mr-2 h-4 w-4" /> </Button>
</div>
</div> </div>
<Card className="overflow-hidden border-white/5"> <Card className="overflow-hidden border-white/5">
@@ -224,7 +435,7 @@ const KnowledgeBaseDetail: React.FC<{
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<tbody> <tbody>
{filteredDocs.length > 0 ? filteredDocs.map(doc => ( {filteredDocs.length > 0 ? filteredDocs.map((doc) => (
<TableRow key={doc.id}> <TableRow key={doc.id}>
<TableCell className="font-medium flex items-center text-white"> <TableCell className="font-medium flex items-center text-white">
<FileText className="h-4 w-4 mr-2 text-primary" /> {doc.name} <FileText className="h-4 w-4 mr-2 text-primary" /> {doc.name}
@@ -262,9 +473,9 @@ const UploadModal: React.FC<{ kbId: string; isOpen: boolean; onClose: () => void
const handleDrag = (e: React.DragEvent) => { const handleDrag = (e: React.DragEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") { if (e.type === 'dragenter' || e.type === 'dragover') {
setDragActive(true); setDragActive(true);
} else if (e.type === "dragleave") { } else if (e.type === 'dragleave') {
setDragActive(false); setDragActive(false);
} }
}; };
@@ -274,19 +485,19 @@ const UploadModal: React.FC<{ kbId: string; isOpen: boolean; onClose: () => void
e.stopPropagation(); e.stopPropagation();
setDragActive(false); setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) { if (e.dataTransfer.files && e.dataTransfer.files[0]) {
setFiles(prev => [...prev, ...Array.from(e.dataTransfer.files)]); setFiles((prev) => [...prev, ...Array.from(e.dataTransfer.files)]);
} }
}; };
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault(); e.preventDefault();
if (e.target.files && e.target.files[0]) { if (e.target.files && e.target.files[0]) {
setFiles(prev => [...prev, ...Array.from(e.target.files || [])]); setFiles((prev) => [...prev, ...Array.from(e.target.files || [])]);
} }
}; };
const removeFile = (idx: number) => { const removeFile = (idx: number) => {
setFiles(prev => prev.filter((_, i) => i !== idx)); setFiles((prev) => prev.filter((_, i) => i !== idx));
}; };
const handleUpload = async () => { const handleUpload = async () => {
@@ -296,9 +507,9 @@ const UploadModal: React.FC<{ kbId: string; isOpen: boolean; onClose: () => void
await onUploaded(); await onUploaded();
onClose(); onClose();
setFiles([]); setFiles([]);
} catch (error) { } catch (error: any) {
console.error(error); console.error(error);
alert('上传失败,请稍后重试。'); alert(error?.message || '上传失败,请稍后重试。');
} }
}; };
@@ -315,7 +526,7 @@ const UploadModal: React.FC<{ kbId: string; isOpen: boolean; onClose: () => void
} }
> >
<div <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"}`} 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} onDragEnter={handleDrag}
onDragLeave={handleDrag} onDragLeave={handleDrag}
onDragOver={handleDrag} onDragOver={handleDrag}

View File

@@ -143,6 +143,11 @@ const mapKnowledgeBase = (raw: AnyRecord): KnowledgeBase => ({
name: readField(raw, ['name'], ''), name: readField(raw, ['name'], ''),
creator: 'Admin', creator: 'Admin',
createdAt: normalizeDateLabel(readField(raw, ['createdAt', 'created_at'], '')), createdAt: normalizeDateLabel(readField(raw, ['createdAt', 'created_at'], '')),
description: readField(raw, ['description'], ''),
embeddingModel: readField(raw, ['embeddingModel', 'embedding_model'], ''),
chunkSize: Number(readField(raw, ['chunkSize', 'chunk_size'], 500)),
chunkOverlap: Number(readField(raw, ['chunkOverlap', 'chunk_overlap'], 50)),
status: readField(raw, ['status'], 'active'),
documents: readField(raw, ['documents'], []).map((doc: AnyRecord) => mapKnowledgeDocument(doc)), documents: readField(raw, ['documents'], []).map((doc: AnyRecord) => mapKnowledgeDocument(doc)),
}); });
@@ -522,12 +527,37 @@ export const fetchKnowledgeBases = async (): Promise<KnowledgeBase[]> => {
return list.map((item) => mapKnowledgeBase(item)); return list.map((item) => mapKnowledgeBase(item));
}; };
export const createKnowledgeBase = async (name: string): Promise<KnowledgeBase> => { export const createKnowledgeBase = async (data: {
const payload = { name, description: '', embeddingModel: 'text-embedding-3-small', chunkSize: 500, chunkOverlap: 50 }; name: string;
description?: string;
embeddingModel?: string;
chunkSize?: number;
chunkOverlap?: number;
}): Promise<KnowledgeBase> => {
const payload = {
name: data.name,
description: data.description || '',
embeddingModel: data.embeddingModel || 'text-embedding-3-small',
chunkSize: data.chunkSize ?? 500,
chunkOverlap: data.chunkOverlap ?? 50,
};
const response = await apiRequest<AnyRecord>('/knowledge/bases', { method: 'POST', body: payload }); const response = await apiRequest<AnyRecord>('/knowledge/bases', { method: 'POST', body: payload });
return mapKnowledgeBase(response); return mapKnowledgeBase(response);
}; };
export const updateKnowledgeBase = async (kbId: string, data: Partial<KnowledgeBase>): Promise<KnowledgeBase> => {
const payload = {
name: data.name,
description: data.description,
embeddingModel: data.embeddingModel,
chunkSize: data.chunkSize,
chunkOverlap: data.chunkOverlap,
status: data.status,
};
const response = await apiRequest<AnyRecord>(`/knowledge/bases/${kbId}`, { method: 'PUT', body: payload });
return mapKnowledgeBase(response);
};
export const deleteKnowledgeBase = async (kbId: string): Promise<void> => { export const deleteKnowledgeBase = async (kbId: string): Promise<void> => {
await apiRequest(`/knowledge/bases/${kbId}`, { method: 'DELETE' }); await apiRequest(`/knowledge/bases/${kbId}`, { method: 'DELETE' });
}; };

View File

@@ -44,6 +44,11 @@ export interface KnowledgeBase {
name: string; name: string;
creator: string; creator: string;
createdAt: string; createdAt: string;
description?: string;
embeddingModel?: string;
chunkSize?: number;
chunkOverlap?: number;
status?: string;
documents: KnowledgeDocument[]; documents: KnowledgeDocument[];
} }