feat: add knowledge base retrieve test tab with Card-based UI (#1583)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin <Chin>, u79E6u9A8Fu8A00 in Chinese, you can call me my english name Rock Chin. <rockchinq@gmail.com>
This commit is contained in:
devin-ai-integration[bot]
2025-07-20 17:56:46 +08:00
committed by GitHub
parent ea51cec57e
commit dd1ec15a39
6 changed files with 159 additions and 1 deletions
+18 -1
View File
@@ -24,6 +24,7 @@ import { httpClient } from '@/app/infra/http/HttpClient';
// import { KnowledgeBase } from '@/app/infra/entities/api'; // import { KnowledgeBase } from '@/app/infra/entities/api';
import KBForm from '@/app/home/knowledge/components/kb-form/KBForm'; import KBForm from '@/app/home/knowledge/components/kb-form/KBForm';
import KBDoc from '@/app/home/knowledge/components/kb-docs/KBDoc'; import KBDoc from '@/app/home/knowledge/components/kb-docs/KBDoc';
import KBRetrieve from '@/app/home/knowledge/components/kb-retrieve/KBRetrieve';
interface KBDetailDialogProps { interface KBDetailDialogProps {
open: boolean; open: boolean;
@@ -81,6 +82,19 @@ export default function KBDetailDialog({
</svg> </svg>
), ),
}, },
{
key: 'retrieve',
label: t('knowledge.retrieve'),
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M18.031 16.617l4.283 4.282-1.415 1.415-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9 9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617zm-2.006-.742A6.977 6.977 0 0 0 18 11c0-3.868-3.133-7-7-7-3.868 0-7 3.132-7 7 0 3.867 3.132 7 7 7a6.977 6.977 0 0 0 4.875-1.975l.15-.15z"></path>
</svg>
),
},
]; ];
const confirmDelete = () => { const confirmDelete = () => {
@@ -168,7 +182,9 @@ export default function KBDetailDialog({
<DialogTitle> <DialogTitle>
{activeMenu === 'metadata' {activeMenu === 'metadata'
? t('knowledge.editKnowledgeBase') ? t('knowledge.editKnowledgeBase')
: t('knowledge.editDocument')} : activeMenu === 'documents'
? t('knowledge.editDocument')
: t('knowledge.retrieveTest')}
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="flex-1 overflow-y-auto px-6 pb-6"> <div className="flex-1 overflow-y-auto px-6 pb-6">
@@ -180,6 +196,7 @@ export default function KBDetailDialog({
/> />
)} )}
{activeMenu === 'documents' && <KBDoc kbId={kbId} />} {activeMenu === 'documents' && <KBDoc kbId={kbId} />}
{activeMenu === 'retrieve' && <KBRetrieve kbId={kbId} />}
</div> </div>
{activeMenu === 'metadata' && ( {activeMenu === 'metadata' && (
<DialogFooter className="px-6 py-4 border-t shrink-0"> <DialogFooter className="px-6 py-4 border-t shrink-0">
@@ -0,0 +1,98 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useTranslation } from 'react-i18next';
import { httpClient } from '@/app/infra/http/HttpClient';
import { RetrieveResult, KnowledgeBaseFile } from '@/app/infra/entities/api';
import { toast } from 'sonner';
interface KBRetrieveProps {
kbId: string;
}
export default function KBRetrieve({ kbId }: KBRetrieveProps) {
const { t } = useTranslation();
const [query, setQuery] = useState('');
const [results, setResults] = useState<RetrieveResult[]>([]);
const [files, setFiles] = useState<KnowledgeBaseFile[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const loadFiles = async () => {
try {
const response = await httpClient.getKnowledgeBaseFiles(kbId);
setFiles(response.files);
} catch (error) {
console.error('Failed to load files:', error);
}
};
loadFiles();
}, [kbId]);
const handleRetrieve = async () => {
if (!query.trim()) return;
setLoading(true);
try {
const response = await httpClient.retrieveKnowledgeBase(kbId, query);
setResults(response.results);
} catch (error) {
console.error('Retrieve failed:', error);
toast.error(t('knowledge.retrieveError'));
} finally {
setLoading(false);
}
};
const getFileName = (fileId: string) => {
const file = files.find((f) => f.uuid === fileId);
return file?.file_name || fileId;
};
return (
<div className="space-y-4">
<div className="flex gap-2">
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t('knowledge.queryPlaceholder')}
onKeyPress={(e) => e.key === 'Enter' && handleRetrieve()}
/>
<Button onClick={handleRetrieve} disabled={loading || !query.trim()}>
{loading ? t('common.loading') : t('knowledge.query')}
</Button>
</div>
<div className="space-y-3">
<h3 className="text-lg font-semibold">
{t('knowledge.retrieveResults')} ({results.length})
</h3>
{results.length === 0 && !loading && (
<p className="text-muted-foreground">{t('knowledge.noResults')}</p>
)}
{results.map((result) => (
<Card key={result.id} className="w-full">
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex justify-between items-center">
<span>{getFileName(result.metadata.file_id)}</span>
<span className="text-xs text-muted-foreground">
{t('knowledge.distance')}: {result.distance.toFixed(4)}
</span>
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm whitespace-pre-wrap">
{result.metadata.text}
</p>
</CardContent>
</Card>
))}
</div>
</div>
);
}
+15
View File
@@ -296,3 +296,18 @@ export interface ApiRespWebChatMessage {
export interface ApiRespWebChatMessages { export interface ApiRespWebChatMessages {
messages: Message[]; messages: Message[];
} }
export interface RetrieveResult {
id: string;
metadata: {
file_id: string;
text: string;
uuid: string;
[key: string]: unknown;
};
distance: number;
}
export interface ApiRespKnowledgeBaseRetrieve {
results: RetrieveResult[];
}
+8
View File
@@ -38,6 +38,7 @@ import {
ApiRespKnowledgeBase, ApiRespKnowledgeBase,
KnowledgeBase, KnowledgeBase,
ApiRespKnowledgeBaseFiles, ApiRespKnowledgeBaseFiles,
ApiRespKnowledgeBaseRetrieve,
} from '@/app/infra/entities/api'; } from '@/app/infra/entities/api';
import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest'; import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest';
import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse'; import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
@@ -499,6 +500,13 @@ class HttpClient {
return this.delete(`/api/v1/knowledge/bases/${uuid}`); return this.delete(`/api/v1/knowledge/bases/${uuid}`);
} }
public retrieveKnowledgeBase(
uuid: string,
query: string,
): Promise<ApiRespKnowledgeBaseRetrieve> {
return this.post(`/api/v1/knowledge/bases/${uuid}/retrieve`, { query });
}
// ============ Plugins API ============ // ============ Plugins API ============
public getPlugins(): Promise<ApiRespPlugins> { public getPlugins(): Promise<ApiRespPlugins> {
return this.get('/api/v1/plugins'); return this.get('/api/v1/plugins');
+10
View File
@@ -282,6 +282,16 @@ const enUS = {
}, },
deleteKnowledgeBaseConfirmation: deleteKnowledgeBaseConfirmation:
'Are you sure you want to delete this knowledge base? All documents in this knowledge base will be deleted.', 'Are you sure you want to delete this knowledge base? All documents in this knowledge base will be deleted.',
retrieve: 'Retrieve Test',
retrieveTest: 'Retrieve Test',
query: 'Query',
queryPlaceholder: 'Enter query text...',
retrieveResults: 'Retrieve Results',
distance: 'Distance',
content: 'Content',
fileName: 'File Name',
noResults: 'No results',
retrieveError: 'Retrieve failed',
}, },
register: { register: {
title: 'Initialize LangBot 👋', title: 'Initialize LangBot 👋',
+10
View File
@@ -274,6 +274,16 @@ const zhHans = {
}, },
deleteKnowledgeBaseConfirmation: deleteKnowledgeBaseConfirmation:
'你确定要删除这个知识库吗?此知识库下的所有文档将被删除。', '你确定要删除这个知识库吗?此知识库下的所有文档将被删除。',
retrieve: '检索测试',
retrieveTest: '检索测试',
query: '查询',
queryPlaceholder: '输入查询内容...',
retrieveResults: '检索结果',
distance: '距离',
content: '内容',
fileName: '文件名',
noResults: '暂无结果',
retrieveError: '检索失败',
}, },
register: { register: {
title: '初始化 LangBot 👋', title: '初始化 LangBot 👋',