knowledge base pages (#43)

* feat: Update site name to DataMate and refine text for AI data processing

* feat: Refactor settings page and implement model access functionality

- Created a new ModelAccess component for managing model configurations.
- Removed the old Settings component and replaced it with a new SettingsPage component that integrates ModelAccess, SystemConfig, and WebhookConfig.
- Added SystemConfig component for managing system settings.
- Implemented WebhookConfig component for managing webhook configurations.
- Updated API functions for model management in settings.apis.ts.
- Adjusted routing to point to the new SettingsPage component.

* feat: Implement Data Collection Page with Task Management and Execution Log

- Created DataCollectionPage component to manage data collection tasks.
- Added TaskManagement and ExecutionLog components for task handling and logging.
- Integrated task operations including start, stop, edit, and delete functionalities.
- Implemented filtering and searching capabilities in task management.
- Introduced SimpleCronScheduler for scheduling tasks with cron expressions.
- Updated CreateTask component to utilize new scheduling and template features.
- Enhanced BasicInformation component to conditionally render fields based on visibility settings.
- Refactored ImportConfiguration component to remove NAS import section.

* feat: Update task creation API endpoint and enhance task creation form with new fields and validation

* Refactor file upload and operator management components

- Removed unnecessary console logs from file download and export functions.
- Added size property to TaskItem interface for better task management.
- Simplified TaskUpload component by utilizing useFileSliceUpload hook for file upload logic.
- Enhanced OperatorPluginCreate component to handle file uploads and parsing more efficiently.
- Updated ConfigureStep component to use Ant Design Form for better data handling and validation.
- Improved PreviewStep component to navigate back to the operator market.
- Added support for additional file types in UploadStep component.
- Implemented delete operator functionality in OperatorMarketPage with confirmation prompts.
- Cleaned up unused API functions in operator.api.ts to streamline the codebase.
- Fixed number formatting utility to handle zero values correctly.

* Refactor Knowledge Generation to Knowledge Base

- Created new API service for Knowledge Base operations including querying, creating, updating, and deleting knowledge bases and files.
- Added constants for Knowledge Base status and type mappings.
- Defined models for Knowledge Base and related files.
- Removed obsolete Knowledge Base creation and home components, replacing them with new implementations under the Knowledge Base structure.
- Updated routing to reflect the new Knowledge Base paths.
- Adjusted menu items to align with the new Knowledge Base terminology.
- Modified ModelAccess interface to include modelName and type properties.

* feat: Implement Knowledge Base Page with CRUD operations and data management

- Added KnowledgeBasePage component for displaying and managing knowledge bases.
- Integrated search and filter functionalities with SearchControls component.
- Implemented CreateKnowledgeBase component for creating and editing knowledge bases.
- Enhanced AddDataDialog for file uploads and dataset selections.
- Introduced TableTransfer component for managing data transfers between tables.
- Updated API functions for knowledge base operations, including file management.
- Refactored knowledge base model to include file status and metadata.
- Adjusted routing to point to the new KnowledgeBasePage.
This commit is contained in:
chenghh-9609
2025-10-31 10:03:42 +08:00
committed by GitHub
parent d89811f238
commit c6958d1511
16 changed files with 974 additions and 769 deletions

View File

@@ -200,6 +200,7 @@ export function mapDataset(dataset: AnyObject): Dataset {
datasetTypeMap[dataset?.datasetType] || {};
return {
...dataset,
key: dataset.id,
type: datasetTypeMap[dataset.datasetType]?.label || "未知",
size: formatBytes(dataset.totalSize || 0),
createdAt: formatDateTime(dataset.createdAt) || "--",

View File

@@ -1,628 +1,236 @@
import type React from "react";
import { useEffect, useState } from "react";
import { Table, Badge, Button, Breadcrumb, Tooltip, App } from "antd";
import {
Plus,
Edit,
File,
Trash2,
Save,
Layers,
RefreshCw,
BookOpen,
Database,
MoreHorizontal,
Upload,
Zap,
StarOff,
CheckCircle,
VectorSquareIcon,
} from "lucide-react";
import {
Table,
Badge,
Button,
Progress,
Input,
Modal,
message,
Card,
Breadcrumb,
Checkbox,
Dropdown,
} from "antd";
import { useNavigate } from "react-router";
DeleteOutlined,
EditOutlined,
ReloadOutlined,
} from "@ant-design/icons";
import { useNavigate, useParams } from "react-router";
import DetailHeader from "@/components/DetailHeader";
import { SearchControls } from "@/components/SearchControls";
import { KnowledgeBaseItem } from "../knowledge-base.model";
import { KBFile, KnowledgeBaseItem } from "../knowledge-base.model";
import { mapFileData, mapKnowledgeBase } from "../knowledge-base.const";
import {
deleteKnowledgeBaseByIdUsingDelete,
deleteKnowledgeBaseFileByIdUsingDelete,
queryKnowledgeBaseByIdUsingGet,
queryKnowledgeBaseFilesUsingGet,
} from "../knowledge-base.api";
import useFetchData from "@/hooks/useFetchData";
import AddDataDialog from "../components/AddDataDialog";
import CreateKnowledgeBase from "../components/CreateKnowledgeBase";
const KnowledgeBaseDetailPage: React.FC = () => {
const navigate = useNavigate();
const { message } = App.useApp();
const { id } = useParams<{ id: string }>();
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseItem>(null);
const [files, setFiles] = useState([]);
const [showEdit, setShowEdit] = useState(false);
const fetchKnowledgeBaseDetails = async (id: string) => {
const { data } = await queryKnowledgeBaseByIdUsingGet(id);
setKnowledgeBase(mapKnowledgeBase(data));
};
useEffect(() => {
if (id) {
fetchKnowledgeBaseDetails(id);
}
}, [id]);
const {
loading,
tableData: files,
searchParams,
pagination,
fetchData: fetchFiles,
setSearchParams,
handleFiltersChange,
} = useFetchData<KBFile>(
(params) => queryKnowledgeBaseFilesUsingGet(knowledgeBase?.id, params),
mapFileData
);
// File table logic
const handleDeleteFile = (file: KBFile) => {};
const handleDeleteKB = (kb: KnowledgeBase) => {};
// 状态 Badge 映射
function getStatusBadgeVariant(status: string) {
switch (status) {
case "completed":
case "ready":
return "success";
case "processing":
case "vectorizing":
return "processing";
case "importing":
return "warning";
case "error":
return "error";
default:
return "default";
const handleDeleteFile = async (file: KBFile) => {
try {
await deleteKnowledgeBaseFileByIdUsingDelete(knowledgeBase.id, file.id);
message.success("文件已删除");
fetchFiles();
} catch (error) {
message.error("文件删除失败");
}
}
function getStatusLabel(status: string) {
switch (status) {
case "completed":
case "ready":
return "已完成";
case "processing":
return "处理中";
case "vectorizing":
return "向量化中";
case "importing":
return "导入中";
case "error":
return "错误";
case "disabled":
return "已禁用";
default:
return "未知";
}
}
function getStatusIcon(status: string) {
switch (status) {
case "completed":
case "ready":
return <CheckCircle className="w-4 h-4 text-green-500" />;
case "processing":
case "vectorizing":
return <RefreshCw className="w-4 h-4 text-blue-500 animate-spin" />;
case "importing":
return <Upload className="w-4 h-4 text-orange-500" />;
case "error":
return <Trash2 className="w-4 h-4 text-red-500" />;
default:
return <File className="w-4 h-4 text-gray-400" />;
}
}
};
const handleDeleteKB = async (kb: KnowledgeBaseItem) => {
await deleteKnowledgeBaseByIdUsingDelete(kb.id);
message.success("知识库已删除");
navigate("/data/knowledge-base");
};
const handleRefreshPage = () => {
fetchKnowledgeBaseDetails(knowledgeBase.id);
fetchFiles();
setShowEdit(false);
};
const operations = [
{
key: "edit",
label: "编辑知识库",
icon: <EditOutlined className="w-4 h-4" />,
onClick: () => {
setShowEdit(true);
},
},
{
key: "refresh",
label: "刷新知识库",
icon: <ReloadOutlined className="w-4 h-4" />,
onClick: () => {
handleRefreshPage();
},
},
{
key: "delete",
label: "删除知识库",
danger: true,
confirm: {
title: "确认删除该知识库吗?",
description: "删除后将无法恢复,请谨慎操作。",
cancelText: "取消",
okText: "删除",
okType: "danger",
onConfirm: () => handleDeleteKB(knowledgeBase),
},
icon: <DeleteOutlined className="w-4 h-4" />,
},
];
const fileOps = [
{
key: "delete",
label: "删除文件",
icon: <DeleteOutlined className="w-4 h-4" />,
danger: true,
onClick: handleDeleteFile,
},
];
const fileColumns = [
{
title: "文件名",
dataIndex: "name",
key: "name",
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
}: any) => (
<div style={{ padding: 8 }}>
<Input
placeholder="搜索文件名"
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={confirm}
style={{ width: 188, marginBottom: 8, display: "block" }}
/>
<Button
type="primary"
onClick={confirm}
size="small"
style={{ width: 90, marginRight: 8 }}
>
</Button>
<Button onClick={clearFilters} size="small" style={{ width: 90 }}>
</Button>
</div>
),
onFilter: (value: string, record: KBFile) =>
record.name.toLowerCase().includes(value.toLowerCase()),
render: (text: string, file: KBFile) => (
<Button
type="link"
onClick={() =>
navigate("/data/knowledge-base/file-detail/" + file.id)
}
>
{file.name}
</Button>
),
width: 200,
ellipsis: true,
fixed: "left" as const,
},
{
title: "类型",
dataIndex: "type",
key: "type",
filters: allFileTypes.map((type) => ({
text: type,
value: type,
})),
onFilter: (value: string, record: KBFile) => record.type === value,
},
{
title: "大小",
dataIndex: "size",
key: "size",
sorter: (a: KBFile, b: KBFile) => parseFloat(a.size) - parseFloat(b.size),
sortOrder: fileSortOrder,
},
{
title: "向量化状态",
dataIndex: "vectorizationStatus",
title: "状态",
dataIndex: "status",
key: "vectorizationStatus",
filters: allVectorizationStatuses
.filter((opt) => opt.value !== null)
.map((opt) => ({
text: opt.label,
value: opt.value,
})),
onFilter: (value: string, record: KBFile) =>
record.vectorizationStatus === value,
render: (_: any, file: KBFile) => (
<div className="flex items-center gap-2">
<Badge
status={getStatusBadgeVariant(
file.vectorizationStatus || "pending"
)}
text={getStatusLabel(file.vectorizationStatus || "pending")}
/>
{file.vectorizationStatus === "processing" && (
<div className="w-16">
<Progress percent={file.progress} size="small" showInfo={false} />
</div>
)}
</div>
),
},
{
title: "来源",
dataIndex: "source",
key: "source",
render: (_: any, file: KBFile) => (
<div className="flex items-center gap-2">
<Badge
status={file.source === "upload" ? "processing" : "default"}
text={file.source === "upload" ? "上传" : "数据集"}
/>
{file.datasetId && (
<span className="text-xs text-gray-500">({file.datasetId})</span>
)}
</div>
width: 120,
render: (status: any) => (
<Badge color={status?.color} text={status?.label} />
),
},
{
title: "分块数",
dataIndex: "chunkCount",
key: "chunkCount",
render: (chunkCount: number) => (
<span className="font-medium text-gray-900">{chunkCount}</span>
),
width: 100,
ellipsis: true,
},
{
title: "上传时间",
dataIndex: "uploadedAt",
key: "uploadedAt",
title: "创建时间",
dataIndex: "createdAt",
key: "createdAt",
ellipsis: true,
width: 180,
},
{
title: "更新时间",
dataIndex: "updatedAt",
key: "updatedAt",
ellipsis: true,
width: 180,
},
{
title: "操作",
key: "actions",
align: "right" as const,
width: 100,
render: (_: any, file: KBFile) => (
<Dropdown
menu={{
items: [
{
label: "重试",
key: "retry",
onClick: () => handleStartVectorization(file.id),
},
{
label: "删除",
key: "delete",
onClick: () => handleDeleteFile(file),
},
],
}}
>
<MoreHorizontal />
</Dropdown>
<div>
{fileOps.map((op) => (
<Tooltip key={op.key} title={op.label}>
<Button
type="text"
icon={op.icon}
danger={op?.danger}
onClick={() => op.onClick(file)}
/>
</Tooltip>
))}
</div>
),
},
];
return (
<div className="">
{/* Breadcrumb */}
<div className="h-full flex flex-col">
<div className="mb-4">
<Breadcrumb>
<Breadcrumb.Item>
<a onClick={() => navigate("/data/knowledge-base")}></a>
</Breadcrumb.Item>
<Breadcrumb.Item>{knowledgeBase.name}</Breadcrumb.Item>
<Breadcrumb.Item>{knowledgeBase?.name}</Breadcrumb.Item>
</Breadcrumb>
</div>
<div className="flex flex-col gap-4">
{/* Knowledge Base Header */}
<DetailHeader
data={{
icon:
knowledgeBase.type === "structured" ? (
<Database className="w-8 h-8" />
) : (
<BookOpen className="w-8 h-8" />
),
status: {
label: getStatusLabel(knowledgeBase.status),
icon: getStatusIcon(knowledgeBase.status),
color: getStatusBadgeVariant(knowledgeBase.status),
},
name: knowledgeBase.name,
description: knowledgeBase.description,
createdAt: knowledgeBase.createdAt,
lastUpdated: knowledgeBase.lastUpdated,
}}
statistics={[
{
icon: <File className="w-4 h-4 text-gray-400" />,
label: "文件",
value: knowledgeBase.fileCount,
},
{
icon: <Layers className="w-4 h-4 text-gray-400" />,
label: "分块",
value: knowledgeBase.chunkCount?.toLocaleString?.() ?? 0,
},
{
icon: <StarOff className="w-4 h-4 text-gray-400" />,
label: "向量",
value: knowledgeBase.vectorCount?.toLocaleString?.() ?? 0,
},
{
icon: <Database className="w-4 h-4 text-gray-400" />,
label: "大小",
value: knowledgeBase.size,
},
]}
operations={[
{
key: "edit",
label: "修改参数配置",
icon: <Edit className="w-4 h-4" />,
onClick: () => {
setEditForm(knowledgeBase);
setCurrentView("config");
},
},
{
key: "vector",
label: "向量化管理",
icon: <VectorSquareIcon className="w-4 h-4" />,
onClick: () => setShowVectorizationDialog(true),
},
...(knowledgeBase.status === "error"
? [
{
key: "retry",
label: "重试",
onClick: () => {}, // 填写重试逻辑
danger: false,
},
]
: []),
{
key: "more",
label: "更多操作",
icon: <MoreHorizontal className="w-4 h-4" />,
isDropdown: true,
items: [
{
key: "download",
label: "导出",
},
{
key: "settings",
label: "配置",
},
{ type: "divider" },
{
key: "delete",
label: "删除",
danger: true,
onClick: () => handleDeleteKB(knowledgeBase),
},
],
},
]}
<DetailHeader
data={knowledgeBase}
statistics={knowledgeBase?.statistics || []}
operations={operations}
/>
<CreateKnowledgeBase
showBtn={false}
isEdit={showEdit}
data={knowledgeBase}
onUpdate={handleRefreshPage}
onClose={() => setShowEdit(false)}
/>
<div className="flex-1 border-card p-6 mt-4">
<div className="flex items-center justify-between mb-4 gap-3">
<div className="flex-1">
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={(keyword) =>
setSearchParams({ ...searchParams, keyword })
}
searchPlaceholder="搜索文件名..."
filters={[]}
onFiltersChange={handleFiltersChange}
onClearFilters={() =>
setSearchParams({ ...searchParams, filter: {} })
}
showViewToggle={false}
showReload={false}
/>
</div>
<AddDataDialog knowledgeBase={knowledgeBase} />
</div>
<Table
loading={loading}
columns={fileColumns}
dataSource={files}
rowKey="id"
pagination={pagination}
scroll={{ y: "calc(100vh - 30rem)" }}
/>
{/* Tab Navigation */}
<Card>
{/* Files Section */}
<div className="flex items-center justify-between mb-4 gap-4">
<div className="flex-1">
<SearchControls
searchTerm={fileSearchQuery}
onSearchChange={setFileSearchQuery}
searchPlaceholder="搜索文件名..."
filters={[
{
key: "status",
label: "状态筛选",
options: [
{ label: "全部状态", value: "all" },
{ label: "已完成", value: "completed" },
{ label: "处理中", value: "processing" },
{ label: "向量化中", value: "vectorizing" },
{ label: "错误", value: "error" },
{ label: "已禁用", value: "disabled" },
],
},
]}
onFiltersChange={(filters) => {
setFileStatusFilter(filters.status?.[0] || "all");
}}
showViewToggle={false}
/>
</div>
<Button type="primary">
<Plus className="w-4 h-4 mr-1" />
</Button>
</div>
{/* Files Table */}
<Table
columns={fileColumns}
dataSource={files}
rowKey="id"
pagination={false}
locale={{
emptyText: (
<div className="text-center py-12">
<File className="w-12 h-12 mx-auto mb-4 text-gray-300" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
</h3>
<p className="text-gray-500 mb-4">
</p>
<Button type="dashed">
<Upload className="w-4 h-4 mr-2" />
</Button>
</div>
),
}}
/>
</Card>
</div>
{/* Vectorization Dialog */}
<Modal
open={showVectorizationDialog}
onCancel={() => setShowVectorizationDialog(false)}
footer={null}
title="向量化管理"
width={700}
destroyOnClose
>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<Card className="p-4">
<h4 className="font-medium mb-2"></h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span>:</span>
<span>
{
knowledgeBase.files.filter(
(f) => f.vectorizationStatus === "completed"
).length
}
/{knowledgeBase.files.length}
</span>
</div>
<div className="flex justify-between">
<span>:</span>
<span>
{knowledgeBase.vectorCount?.toLocaleString?.() ?? 0}
</span>
</div>
<div className="flex justify-between">
<span>:</span>
<span>{knowledgeBase.size}</span>
</div>
</div>
</Card>
<Card className="p-4">
<h4 className="font-medium mb-2"></h4>
<div className="space-y-2">
<Button
className="w-full"
type="primary"
onClick={() => handleStartVectorization()}
>
<Zap className="w-4 h-4 mr-2" />
</Button>
<Button
className="w-full"
onClick={() => message.info("TODO: 重新向量化全部")}
>
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
<Button
className="w-full"
danger
onClick={() => message.info("TODO: 清空向量数据")}
>
<Trash2 className="w-4 h-4 mr-2" />
</Button>
</div>
</Card>
</div>
<div>
<h4 className="font-medium mb-3"></h4>
<div className="space-y-2 max-h-60 overflow-y-auto">
{knowledgeBase.files.map((file: KBFile) => (
<div
key={file.id}
className="flex items-center justify-between p-3 border rounded-lg"
>
<div className="flex items-center gap-3">
<File className="w-4 h-4 text-gray-400" />
<div>
<p className="font-medium text-sm">{file.name}</p>
<p className="text-xs text-gray-500">
{file.chunkCount}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Badge
status={getStatusBadgeVariant(
file.vectorizationStatus || "pending"
)}
text={getStatusLabel(
file.vectorizationStatus || "pending"
)}
/>
{file.vectorizationStatus === "processing" && (
<div className="w-16">
<Progress
percent={file.progress}
size="small"
showInfo={false}
/>
</div>
)}
{file.vectorizationStatus !== "completed" && (
<Button
size="small"
onClick={() => handleStartVectorization(file.id)}
>
<StarOff className="w-3 h-3" />
</Button>
)}
</div>
</div>
))}
</div>
</div>
<div className="flex justify-end pt-2">
<Button onClick={() => setShowVectorizationDialog(false)}>
</Button>
</div>
</div>
</Modal>
{/* Edit File Dialog */}
<Modal
open={!!showEditFileDialog}
onCancel={() => setShowEditFileDialog(null)}
title="编辑文件"
width={600}
footer={[
<Button key="cancel" onClick={() => setShowEditFileDialog(null)}>
</Button>,
<Button
key="save"
type="primary"
onClick={() => setShowEditFileDialog(null)}
>
<Save className="w-4 h-4 mr-2" />
</Button>,
]}
destroyOnClose
>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block mb-1"></label>
<Input value={showEditFileDialog?.name} readOnly />
</div>
<div>
<label className="block mb-1"></label>
<Input
value={
showEditFileDialog?.source === "upload"
? "上传文件"
: "数据集文件"
}
readOnly
/>
</div>
</div>
{showEditFileDialog?.source === "upload" ? (
<div className="space-y-3">
<label className="block mb-1"></label>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
<Upload className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p className="text-sm text-gray-600">
</p>
<Button className="mt-2 bg-transparent" disabled>
</Button>
</div>
</div>
) : (
<div className="space-y-3">
<label className="block mb-1"></label>
<div className="p-4 border rounded-lg bg-gray-50">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">
: {showEditFileDialog?.datasetId}
</span>
<Button size="small">
<RefreshCw className="w-4 h-4 mr-1" />
</Button>
</div>
<p className="text-xs text-gray-600">
</p>
</div>
</div>
)}
<div className="space-y-3">
<label className="block mb-1"></label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox id="reprocess" />
<span className="text-sm"></span>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="revectorize" />
<span className="text-sm"></span>
</div>
</div>
</div>
</div>
</Modal>
</div>
);
};

View File

@@ -0,0 +1,195 @@
import { useState } from "react";
import { Card, Button, Table, Tooltip, message } from "antd";
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
import { SearchControls } from "@/components/SearchControls";
import { useNavigate } from "react-router";
import CardView from "@/components/CardView";
import {
deleteKnowledgeBaseByIdUsingDelete,
queryKnowledgeBasesUsingPost,
} from "../knowledge-base.api";
import useFetchData from "@/hooks/useFetchData";
import { KnowledgeBaseItem } from "../knowledge-base.model";
import CreateKnowledgeBase from "../components/CreateKnowledgeBase";
import { mapKnowledgeBase } from "../knowledge-base.const";
export default function KnowledgeBasePage() {
const navigate = useNavigate();
const [viewMode, setViewMode] = useState<"card" | "list">("card");
const [isEdit, setIsEdit] = useState(false);
const [currentKB, setCurrentKB] = useState<KnowledgeBaseItem | null>(null);
const {
loading,
tableData,
searchParams,
pagination,
fetchData,
setSearchParams,
handleFiltersChange,
} = useFetchData<KnowledgeBaseItem>(
queryKnowledgeBasesUsingPost,
mapKnowledgeBase
);
const handleDeleteKB = async (kb: KnowledgeBaseItem) => {
try {
await deleteKnowledgeBaseByIdUsingDelete(kb.id);
message.success("知识库删除成功");
fetchData();
} catch (error) {
message.error("知识库删除失败");
}
};
const operations = [
{
key: "edit",
label: "编辑",
icon: <EditOutlined />,
onClick: (item) => {
setIsEdit(true);
setCurrentKB(item);
},
},
{
key: "delete",
label: "删除",
danger: true,
icon: <DeleteOutlined />,
confirm: {
title: "确认删除",
description: "此操作不可撤销,是否继续?",
okText: "删除",
okType: "danger",
cancelText: "取消",
},
onClick: (item) => handleDeleteKB(item),
},
];
const columns = [
{
title: "知识库",
dataIndex: "name",
key: "name",
fixed: "left" as const,
width: 200,
ellipsis: true,
render: (_: any, kb: KnowledgeBaseItem) => (
<Button
type="link"
onClick={() => navigate(`/data/knowledge-base/detail/${kb.id}`)}
>
{kb.name}
</Button>
),
},
{
title: "向量数据库",
dataIndex: "embeddingModel",
key: "embeddingModel",
width: 150,
ellipsis: true,
},
{
title: "大语言模型",
dataIndex: "chatModel",
key: "chatModel",
width: 150,
ellipsis: true,
},
{
title: "创建时间",
dataIndex: "createdAt",
key: "createdAt",
ellipsis: true,
width: 150,
},
{
title: "更新时间",
dataIndex: "updatedAt",
key: "updatedAt",
ellipsis: true,
width: 150,
},
{
title: "描述",
dataIndex: "description",
key: "description",
width: 120,
ellipsis: true,
},
{
title: "操作",
key: "actions",
fixed: "right" as const,
width: 150,
render: (_: any, kb: KnowledgeBaseItem) => (
<div className="flex items-center gap-2">
{operations.map((op) => (
<Tooltip key={op.key} title={op.label}>
<Button
type="text"
icon={op.icon}
danger={op.danger}
onClick={() => op.onClick(kb)}
/>
</Tooltip>
))}
</div>
),
},
];
// Main list view
return (
<div className="h-full flex flex-col gap-4">
<div className="flex items-center justify-between">
<h1 className="text-xl font-bold"></h1>
<CreateKnowledgeBase
isEdit={isEdit}
data={currentKB}
onUpdate={() => {
fetchData();
}}
onClose={() => {
setIsEdit(false);
setCurrentKB(null);
}}
/>
</div>
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={(keyword) =>
setSearchParams({ ...searchParams, keyword })
}
searchPlaceholder="搜索知识库..."
filters={[]}
onFiltersChange={handleFiltersChange}
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
viewMode={viewMode}
onViewModeChange={setViewMode}
showViewToggle
onReload={fetchData}
/>
{viewMode === "card" ? (
<CardView
data={tableData}
operations={operations}
onView={(item) => navigate(`/data/knowledge-base/detail/${item.id}`)}
pagination={pagination}
/>
) : (
<Card>
<Table
loading={loading}
scroll={{ x: "max-content", y: "calc(100vh - 20rem)" }}
columns={columns}
dataSource={tableData}
rowKey="id"
/>
</Card>
)}
</div>
);
}

View File

@@ -1,65 +1,320 @@
export default function AddDataDialog() {
const [isOpen, setIsOpen] = useState(false);
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const { message } = App.useApp();
import { useEffect, useState } from "react";
import {
Button,
App,
Input,
Select,
Form,
Modal,
UploadFile,
Radio,
Tree,
} from "antd";
import { InboxOutlined, PlusOutlined } from "@ant-design/icons";
import { KnowledgeBaseItem } from "../knowledge-base.model";
import Dragger from "antd/es/upload/Dragger";
import {
queryDatasetFilesUsingGet,
queryDatasetsUsingGet,
} from "@/pages/DataManagement/dataset.api";
import { datasetTypeMap } from "@/pages/DataManagement/dataset.const";
import { addKnowledgeBaseFilesUsingPost } from "../knowledge-base.api";
import { DatasetType } from "@/pages/DataManagement/dataset.model";
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setSelectedFiles(Array.from(e.target.files));
}
const dataSourceOptions = [
{ label: "本地上传", value: "local" },
{ label: "数据集", value: "dataset" },
];
const sliceOptions = [
{ label: "章节分块", value: "CHAPTER_CHUNK" },
{ label: "段落分块", value: "PARAGRAPH_CHUNK" },
{ label: "长度分块", value: "LENGTH_CHUNK" },
{ label: "自定义分割符分块", value: "CUSTOM_SEPARATOR_CHUNK" },
{ label: "默认分块", value: "DEFAULT_CHUNK" },
];
const columns = [
{
dataIndex: "name",
title: "名称",
ellipsis: true,
},
{
dataIndex: "datasetType",
title: "类型",
ellipsis: true,
render: (type) => datasetTypeMap[type].label,
},
{
dataIndex: "size",
title: "大小",
ellipsis: true,
},
{
dataIndex: "fileCount",
title: "文件数",
ellipsis: true,
},
];
export default function AddDataDialog({ knowledgeBase }) {
const [isOpen, setIsOpen] = useState(false);
const { message } = App.useApp();
const [form] = Form.useForm();
const [fileList, setFileList] = useState<UploadFile[]>([]);
// Form initial values
const [newKB, setNewKB] = useState<Partial<KnowledgeBaseItem>>({
dataSource: "dataset",
processType: "DEFAULT_CHUNK",
chunkSize: 500,
overlap: 50,
datasetIds: [],
});
const [filesTree, setFilesTree] = useState<any[]>([]);
const fetchDatasets = async () => {
const { data } = await queryDatasetsUsingGet({
page: 0,
size: 1000,
type: DatasetType.TEXT,
});
const datasets =
data.content.map((item) => ({
...item,
key: item.id,
title: item.name,
isLeaf: item.fileCount === 0,
disabled: item.fileCount === 0,
})) || [];
setFilesTree(datasets);
};
const handleUpload = async () => {
if (selectedFiles.length === 0) {
message.error("请先选择文件");
return;
}
useEffect(() => {
if (isOpen) fetchDatasets();
}, [isOpen]);
try {
const formData = new FormData();
selectedFiles.forEach((file) => {
formData.append("files", file);
const updateTreeData = (list, key: React.Key, children) =>
list.map((node) => {
if (node.key === key) {
return {
...node,
children,
};
}
if (node.children) {
return {
...node,
children: updateTreeData(node.children, key, children),
};
}
return node;
});
const onLoadFiles = async ({ key, children }) =>
new Promise<void>((resolve) => {
if (children) {
resolve();
return;
}
queryDatasetFilesUsingGet(key, {
page: 0,
size: 1000,
}).then(({ data }) => {
const children = data.content.map((file) => ({
title: file.fileName,
key: file.id,
isLeaf: true,
}));
setFilesTree((origin) => updateTreeData(origin, key, children));
resolve();
});
});
await uploadDataFilesUsingPost(formData);
message.success("文件上传成功");
setIsOpen(false);
setSelectedFiles([]);
} catch (error) {
message.error("文件上传失败");
}
const handleBeforeUpload = (_, files: UploadFile[]) => {
setFileList([...fileList, ...files]);
return false;
};
const handleRemoveFile = (file: UploadFile) => {
setFileList((prev) => prev.filter((f) => f.uid !== file.uid));
};
const handleAddData = async () => {
await addKnowledgeBaseFilesUsingPost(knowledgeBase.id, {
knowledgeBaseId: knowledgeBase.id,
files: newKB.dataSource === "local" ? fileList : newKB.files,
processType: newKB.processType,
chunkSize: newKB.chunkSize,
overlap: newKB.overlap,
delimiter: newKB.delimiter,
});
message.success("数据添加成功");
form.resetFields();
setIsOpen(false);
};
return (
<>
<Button type="primary" onClick={() => setIsOpen(true)}>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setIsOpen(true)}
>
</Button>
<Modal
title="添加数据文件"
title="添加数据"
open={isOpen}
onCancel={() => setIsOpen(false)}
onOk={handleUpload}
okText="上传"
onOk={handleAddData}
okText="确定"
cancelText="取消"
width={1000}
>
<input
type="file"
multiple
onChange={handleFileChange}
accept=".txt,.pdf,.docx,.csv,.json"
/>
{selectedFiles.length > 0 && (
<div className="mt-4">
<h4></h4>
<ul>
{selectedFiles.map((file, index) => (
<li key={index}>
{file.name} - {(file.size / 1024).toFixed(2)} KB
</li>
))}
</ul>
</div>
)}
<div className="overflow-auto p-6">
<Form
form={form}
layout="vertical"
initialValues={newKB}
onValuesChange={(_, allValues) => setNewKB(allValues)}
>
<Form.Item
label="分块方式"
name="processType"
required
rules={[{ required: true }]}
>
<Select options={sliceOptions}></Select>
</Form.Item>
<div className="grid grid-cols-2 gap-3">
<Form.Item
label="分块大小"
name="chunkSize"
rules={[
{
required: true,
message: "请输入分块大小",
},
]}
>
<Input type="number" />
</Form.Item>
<Form.Item
label="重叠长度"
name="overlap"
rules={[
{
required: true,
message: "请输入重叠长度",
},
]}
>
<Input type="number" />
</Form.Item>
</div>
{newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && (
<Form.Item
label="分隔符"
name="delimiter"
rules={[
{
required: true,
message: "请输入分隔符",
},
]}
>
<Input placeholder="输入分隔符,如 \\n\\n" />
</Form.Item>
)}
<Form.Item
label="数据来源"
name="dataSource"
rules={[
{
required: true,
message: "请选择数据来源",
},
]}
>
<Radio.Group options={dataSourceOptions} />
</Form.Item>
{newKB.dataSource === "local" && (
<Form.Item
label="上传文件"
name="files"
rules={[
{
required: true,
message: "请上传文件",
},
]}
>
<Dragger
className="w-full"
onRemove={handleRemoveFile}
beforeUpload={handleBeforeUpload}
multiple
>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text"></p>
<p className="ant-upload-hint">
</p>
</Dragger>
</Form.Item>
)}
{newKB.dataSource === "dataset" && (
<Form.Item
label="选择数据集文件"
name="datasetId"
rules={[
{
required: true,
message: "请选择数据集",
},
]}
>
<div className="border-card p-4 overflow-auto h-[300px]">
<Tree
blockNode
multiple
loadData={onLoadFiles}
treeData={filesTree}
onSelect={(_, { selectedNodes }) => {
console.log({
...newKB,
files: selectedNodes
.filter((node) => node.isLeaf)
.map((node) => ({
...node,
id: node.key,
name: node.title,
})),
});
setNewKB({
...newKB,
files: selectedNodes
.filter((node) => node.isLeaf)
.map((node) => ({
...node,
id: node.key,
name: node.title,
})),
});
}}
/>
</div>
</Form.Item>
)}
</Form>
</div>
</Modal>
</>
);

View File

@@ -12,11 +12,15 @@ import { KnowledgeBaseItem } from "../knowledge-base.model";
export default function CreateKnowledgeBase({
isEdit,
data,
showBtn = true,
onUpdate,
onClose,
}: {
isEdit?: boolean;
showBtn?: boolean;
data?: Partial<KnowledgeBaseItem> | null;
onUpdate: () => void;
onClose: () => void;
}) {
const [open, setOpen] = useState(false);
const [form] = Form.useForm();
@@ -74,24 +78,32 @@ export default function CreateKnowledgeBase({
}
};
const handleCloseModal = () => {
setOpen(false);
onClose?.();
};
return (
<>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
form.resetFields();
setOpen(true);
}}
>
</Button>
{showBtn && (
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
form.resetFields();
setOpen(true);
}}
>
</Button>
)}
<Modal
title={isEdit ? "编辑知识库" : "创建知识库"}
open={open}
okText="确定"
cancelText="取消"
onCancel={() => setOpen(false)}
maskClosable={false}
onCancel={handleCloseModal}
onOk={handleCreateKnowledgeBase}
>
<Form form={form} layout="vertical">

View File

@@ -0,0 +1,75 @@
import React from "react";
import { Table, Transfer } from "antd";
import type {
GetProp,
TableColumnsType,
TableProps,
TransferProps,
} from "antd";
type TransferItem = GetProp<TransferProps, "dataSource">[number];
type TableRowSelection<T extends object> = TableProps<T>["rowSelection"];
interface DataType {
key: string;
title: string;
description: string;
}
interface TableTransferProps extends TransferProps<TransferItem> {
dataSource: DataType[];
leftColumns: TableColumnsType<DataType>;
rightColumns: TableColumnsType<DataType>;
}
// Customize Table Transfer
const TableTransfer: React.FC<TableTransferProps> = (props) => {
const { leftColumns, rightColumns, ...restProps } = props;
return (
<Transfer style={{ width: "100%" }} {...restProps}>
{({
direction,
filteredItems,
onItemSelect,
onItemSelectAll,
selectedKeys: listSelectedKeys,
disabled: listDisabled,
}) => {
const columns = direction === "left" ? leftColumns : rightColumns;
const rowSelection: TableRowSelection<TransferItem> = {
getCheckboxProps: () => ({ disabled: listDisabled }),
onChange(selectedRowKeys) {
onItemSelectAll(selectedRowKeys, "replace");
},
selectedRowKeys: listSelectedKeys,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE,
],
};
return (
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={filteredItems}
size="small"
scroll={{ y: 300 }}
style={{ pointerEvents: listDisabled ? "none" : undefined }}
onRow={({ key, disabled: itemDisabled }) => ({
onClick: () => {
if (itemDisabled || listDisabled) {
return;
}
onItemSelect(key, !listSelectedKeys.includes(key));
},
})}
/>
);
}}
</Transfer>
);
};
export default TableTransfer;

View File

@@ -1,10 +1,9 @@
import { get, post, put, del } from "@/utils/request";
// 获取知识库列表
export function queryKnowledgeBasesUsingPost(params: any) {
console.log('get tk', params);
console.log("get tk", params);
return post("/api/knowledge-base/list", params);
}
@@ -28,22 +27,28 @@ export function deleteKnowledgeBaseByIdUsingDelete(baseId: string) {
return del(`/api/knowledge-base/${baseId}`);
}
// 获取知识生成任务列表
export function queryKnowledgeGenerationTasksUsingPost(params: any) {
return post("/api/knowledge-base/tasks", params);
// 获取知识生成文件列表
export function queryKnowledgeBaseFilesUsingGet(baseId: string, data) {
return get(`/api/knowledge-base/${baseId}/files`, data);
}
// 添加文件到知识库
export function addKnowledgeGenerationFilesUsingPost(baseId: string, data: any) {
export function addKnowledgeBaseFilesUsingPost(baseId: string, data: any) {
return post(`/api/knowledge-base/${baseId}/files`, data);
}
// 获取知识生成文件详情
export function queryKnowledgeGenerationFilesByIdUsingGet(baseId: string, fileId: string) {
export function queryKnowledgeBaseFilesByIdUsingGet(
baseId: string,
fileId: string
) {
return get(`/api/knowledge-base/${baseId}/files/${fileId}`);
}
// 删除知识生成文件
export function deleteKnowledgeGenerationTaskByIdUsingDelete(baseId: string) {
return del(`/api/knowledge-base/${baseId}/files`);
export function deleteKnowledgeBaseFileByIdUsingDelete(
baseId: string,
fileId: string
) {
return del(`/api/knowledge-base/${baseId}/files/${fileId}`);
}

View File

@@ -1,29 +1,48 @@
import {
BookOpen,
BookOpenText,
BookType,
ChartNoAxesColumn,
CheckCircle,
CircleEllipsis,
Clock,
Database,
File,
VectorSquare,
XCircle,
} from "lucide-react";
import { KBStatus, KBType, KnowledgeBaseItem } from "./knowledge-base.model";
import {
KBFile,
KBFileStatus,
KBType,
KnowledgeBaseItem,
} from "./knowledge-base.model";
import { formatBytes, formatDateTime, formatNumber } from "@/utils/unit";
export const KBStatusMap = {
[KBStatus.READY]: {
label: KBStatus.READY,
export const KBFileStatusMap = {
[KBFileStatus.PROCESSED]: {
value: KBFileStatus.PROCESSED,
label: "已处理",
icon: CheckCircle,
color: "#389e0d",
},
[KBStatus.VECTORIZING]: {
label: KBStatus.PROCESSING,
[KBFileStatus.PROCESSING]: {
value: KBFileStatus.PROCESSING,
label: "处理中",
icon: Clock,
color: "#3b82f6",
color: "#faad14",
},
[KBStatus.ERROR]: {
label: KBStatus.ERROR,
[KBFileStatus.PROCESS_FAILED]: {
value: KBFileStatus.PROCESS_FAILED,
label: "处理失败",
icon: XCircle,
color: "#ef4444",
color: "#ff4d4f",
},
[KBFileStatus.UNPROCESSED]: {
value: KBFileStatus.UNPROCESSED,
label: "未处理",
icon: CircleEllipsis,
color: "#d9d9d9",
},
};
@@ -50,12 +69,47 @@ export function mapKnowledgeBase(kb: KnowledgeBaseItem): KnowledgeBaseItem {
icon: <BookOpenText className="text-gray-400" />,
description: kb.description,
statistics: [
{ label: "索引模型", value: kb.embeddingModel },
{ label: "文本理解模型", value: kb.chatModel },
{ label: "文件数", value: formatNumber(kb?.fileCount) || 0 },
{ label: "大小", value: formatBytes(kb?.size) || "0 MB" },
{
label: "索引模型",
key: "embeddingModel",
icon: <VectorSquare className="w-4 h-4 text-blue-500" />,
value: kb.embeddingModel,
},
{
label: "文本理解模型",
key: "chatModel",
icon: <BookType className="w-4 h-4 text-green-500" />,
value: kb.chatModel,
},
{
label: "文件数",
key: "fileCount",
icon: <File className="w-4 h-4 text-yellow-500" />,
value: formatNumber(kb?.fileCount) || 0,
},
{
label: "大小",
key: "size",
icon: <ChartNoAxesColumn className="w-4 h-4 text-red-500" />,
value: formatBytes(kb?.size) || "0 MB",
},
],
updatedAt: formatDateTime(kb.updatedAt),
createdAt: formatDateTime(kb.createdAt),
};
}
export function mapFileData(file: Partial<KBFile>): KBFile {
return {
...file,
name: file.fileName,
createdAt: formatDateTime(file.createdAt),
updatedAt: formatDateTime(file.updatedAt),
status: KBFileStatusMap[file.status] || {
value: file.status,
label: "未知状态",
icon: CircleEllipsis,
color: "#d9d9d9",
},
};
}

View File

@@ -1,10 +1,8 @@
export enum KBStatus {
READY = "ready",
PROCESSING = "processing",
VECTORIZING = "vectorizing",
IMPORTING = "importing",
ERROR = "error",
DISABLED = "disabled",
export enum KBFileStatus {
UNPROCESSED = "UNPROCESSED",
PROCESSING = "PROCESSING",
PROCESSED = "PROCESSED",
PROCESS_FAILED = "PROCESS_FAILED",
}
export enum KBType {
@@ -25,17 +23,17 @@ export interface KnowledgeBaseItem {
export interface KBFile {
id: number;
name: string;
type: string;
size: string;
status: "processing" | "completed" | "error" | "disabled" | "vectorizing";
fileName: string;
name?: string;
createdAt: string;
updatedAt: string;
status: KBFileStatus;
chunkCount: number;
progress: number;
uploadedAt: string;
source: "upload" | "dataset";
datasetId?: string;
chunks?: Chunk[];
vectorizationStatus?: "pending" | "processing" | "completed" | "failed";
metadata: Record<string, any>;
knowledgeBaseId: string;
fileId: string;
updatedBy: string;
createdBy: string;
}
interface Chunk {
@@ -74,12 +72,3 @@ interface VectorizationRecord {
};
error?: string;
}
interface SliceOperator {
id: string;
name: string;
description: string;
type: "text" | "semantic" | "structure" | "custom";
icon: string;
params: Record<string, any>;
}

View File

@@ -53,7 +53,7 @@ export default function TaskUpload() {
></Button>
</div>
<Progress size="small" percent={Number(task.percent.toFixed(2))} />
<Progress size="small" percent={task.percent} />
</div>
))}
{taskList.length === 0 && (