Files
DataMate/frontend/src/pages/KnowledgeBase/Search/KnowledgeBaseSearch.tsx
Jerry Yan 76f70a6847 feat(knowledge-base): 添加知识库文件全库检索功能
- 新增相对路径字段替代原有的metadata存储方式
- 实现跨知识库文件检索接口searchFiles
- 添加前端全库检索页面和相关API调用
- 优化文件路径处理和数据库索引配置
- 统一请求参数类型定义为RequestPayload和RequestParams
- 简化RagFile模型中的元数据结构设计
2026-01-30 22:24:12 +08:00

218 lines
6.6 KiB
TypeScript

import { useCallback, useMemo, useState } from "react";
import { App, Badge, Breadcrumb, Button, Input, Table } from "antd";
import { useNavigate } from "react-router";
import {
KBFileStatus,
KnowledgeBaseFileSearchResult,
} from "../knowledge-base.model";
import { KBFileStatusMap } from "../knowledge-base.const";
import { queryKnowledgeBaseFilesSearchUsingGet } from "../knowledge-base.api";
import { formatDateTime } from "@/utils/unit";
const PATH_SEPARATOR = "/";
const normalizePath = (value?: string) =>
(value ?? "").replace(/\\/g, PATH_SEPARATOR);
const resolvePrefix = (relativePath?: string) => {
const normalized = normalizePath(relativePath);
const parts = normalized.split(PATH_SEPARATOR).filter(Boolean);
if (parts.length <= 1) {
return "";
}
parts.pop();
return `${parts.join(PATH_SEPARATOR)}${PATH_SEPARATOR}`;
};
export default function KnowledgeBaseSearch() {
const navigate = useNavigate();
const { message } = App.useApp();
const [searchTerm, setSearchTerm] = useState("");
const [activeKeyword, setActiveKeyword] = useState("");
const [loading, setLoading] = useState(false);
const [searched, setSearched] = useState(false);
const [results, setResults] = useState<KnowledgeBaseFileSearchResult[]>([]);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
const fetchResults = useCallback(
async (keyword: string, page?: number, pageSize?: number) => {
const resolvedPage = page ?? pagination.current;
const resolvedPageSize = pageSize ?? pagination.pageSize;
if (!keyword) {
setResults([]);
setPagination((prev) => ({ ...prev, total: 0, current: resolvedPage }));
setSearched(false);
return;
}
setLoading(true);
try {
const { data } = await queryKnowledgeBaseFilesSearchUsingGet({
fileName: keyword,
page: Math.max(resolvedPage - 1, 0),
size: resolvedPageSize,
});
const content = Array.isArray(data?.content) ? data.content : [];
setResults(content);
setPagination({
current: resolvedPage,
pageSize: resolvedPageSize,
total: data?.totalElements ?? 0,
});
setSearched(true);
} catch (error) {
console.error("Failed to search knowledge base files:", error);
message.error("检索失败,请稍后重试");
} finally {
setLoading(false);
}
},
[message, pagination]
);
const handleSearch = (value?: string) => {
const keyword = (value ?? searchTerm).trim();
if (!keyword) {
message.warning("请输入文件名");
return;
}
setActiveKeyword(keyword);
fetchResults(keyword, 1, pagination.pageSize);
};
const columns = useMemo(
() => [
{
title: "知识库",
dataIndex: "knowledgeBaseName",
key: "knowledgeBaseName",
width: 220,
ellipsis: true,
render: (text: string) => text || "-",
},
{
title: "文件名",
dataIndex: "fileName",
key: "fileName",
width: 220,
ellipsis: true,
},
{
title: "相对路径",
dataIndex: "relativePath",
key: "relativePath",
ellipsis: true,
render: (value: string) => value || "-",
},
{
title: "状态",
dataIndex: "status",
key: "status",
width: 120,
render: (status?: KBFileStatus) => {
const config = status ? KBFileStatusMap[status] : undefined;
if (!config) {
return <Badge color="default" text={status || "-"} />;
}
return <Badge color={config.color} text={config.label} />;
},
},
{
title: "更新时间",
dataIndex: "updatedAt",
key: "updatedAt",
width: 180,
ellipsis: true,
render: (value: string) => formatDateTime(value) || "-",
},
{
title: "操作",
key: "action",
width: 120,
align: "right" as const,
render: (_: unknown, record: KnowledgeBaseFileSearchResult) => (
<Button
type="link"
onClick={() => {
const prefix = resolvePrefix(record.relativePath);
const searchParams = new URLSearchParams();
if (prefix) {
searchParams.set("prefix", prefix);
}
navigate(
`/data/knowledge-base/detail/${record.knowledgeBaseId}?${searchParams.toString()}`
);
}}
>
</Button>
),
},
],
[navigate]
);
return (
<div className="h-full flex flex-col gap-4">
<Breadcrumb>
<Breadcrumb.Item>
<a onClick={() => navigate("/data/knowledge-base")}></a>
</Breadcrumb.Item>
<Breadcrumb.Item></Breadcrumb.Item>
</Breadcrumb>
<div className="flex items-center justify-between">
<h1 className="text-xl font-bold"></h1>
</div>
<div className="flex items-center gap-3">
<Input.Search
allowClear
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)}
onSearch={handleSearch}
placeholder="输入文件名,回车或点击搜索"
enterButton="搜索"
loading={loading}
/>
</div>
<Table
rowKey="id"
loading={loading}
columns={columns}
dataSource={results}
pagination={{
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
showTotal: (total) => `${total}`,
onChange: (page, pageSize) => {
const nextKeyword = activeKeyword.trim();
if (!nextKeyword) {
message.warning("请输入文件名");
return;
}
fetchResults(nextKeyword, page, pageSize || pagination.pageSize);
},
}}
locale={{
emptyText: searched ? "暂无匹配文件" : "请输入文件名开始检索",
}}
onRow={(record) => ({
onClick: () => {
const prefix = resolvePrefix(record.relativePath);
const searchParams = new URLSearchParams();
if (prefix) {
searchParams.set("prefix", prefix);
}
navigate(
`/data/knowledge-base/detail/${record.knowledgeBaseId}?${searchParams.toString()}`
);
},
})}
/>
</div>
);
}