add knowledgebase page (#39)

* 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.
This commit is contained in:
chenghh-9609
2025-10-30 21:33:33 +08:00
committed by GitHub
parent ba6caabbc6
commit d89811f238
16 changed files with 1119 additions and 1166 deletions

View File

@@ -0,0 +1,582 @@
import {
sliceOperators,
vectorDatabases,
} from "@/mock/knowledgeBase";
import { useState } from "react";
import {
Button,
Card,
Input,
Select,
Checkbox,
Switch,
Tabs,
Divider,
Upload,
message,
Form,
} from "antd";
import {
BookOpen,
Database,
Brain,
Scissors,
Split,
Upload as UploadIcon,
Folder,
CheckCircle,
File,
ArrowLeft,
} from "lucide-react";
import { useNavigate } from "react-router";
import type { Dataset } from "@/pages/DataManagement/dataset.model";
import RadioCard from "@/components/RadioCard";
import { KBTypeMap } from "../knowledge-base.const";
import { KBType, KnowledgeBaseItem } from "../knowledge-base.model";
import { createKnowledgeBaseUsingPost } from "../knowledge-base.api";
const { TextArea } = Input;
const { Option } = Select;
const KnowledgeBaseCreatePage: React.FC = () => {
const navigate = useNavigate();
const [form] = Form.useForm();
const [datasetSearchQuery, setDatasetSearchQuery] = useState("");
const [selectedDatasetId, setSelectedDatasetId] = useState<string | null>(
null
);
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
const [datasets, setDatasets] = useState<Dataset[]>([]);
const [selectedDatasetFiles, setSelectedDatasetFiles] = useState<
{
datasetId: string;
fileId: string;
name: string;
size: string;
type: string;
}[]
>([]);
const [selectedSliceOperators, setSelectedSliceOperators] = useState<
string[]
>(["semantic-split", "paragraph-split"]);
// Form initial values
const [newKB, setNewKB] = useState<Partial<KnowledgeBaseItem>>({
name: "",
description: "",
type: KBType.STRUCTURED,
embeddingModel: "text-embedding-3-large",
llmModel: "gpt-4o",
chunkSize: 512,
overlap: 50,
sliceMethod: "semantic" as
| "paragraph"
| "length"
| "delimiter"
| "semantic",
delimiter: "",
enableQA: true,
vectorDatabase: "pinecone",
});
// Dataset file selection helpers
const handleDatasetFileToggle = (
datasetId: string,
file: MockDataset["files"][0]
) => {
setSelectedDatasetFiles((prev) => {
const isSelected = prev.some(
(f) => f.datasetId === datasetId && f.fileId === file.id
);
if (isSelected) {
return prev.filter(
(f) => !(f.datasetId === datasetId && f.fileId === file.id)
);
} else {
return [...prev, { datasetId, ...file }];
}
});
};
const handleSelectAllDatasetFiles = (
dataset: MockDataset,
checked: boolean
) => {
setSelectedDatasetFiles((prev) => {
let newSelectedFiles = [...prev];
if (checked) {
dataset.files.forEach((file) => {
if (
!newSelectedFiles.some(
(f) => f.datasetId === dataset.id && f.fileId === file.id
)
) {
newSelectedFiles.push({ datasetId: dataset.id, ...file });
}
});
} else {
newSelectedFiles = newSelectedFiles.filter(
(f) => f.datasetId !== dataset.id
);
}
return newSelectedFiles;
});
};
const isDatasetFileSelected = (datasetId: string, fileId: string) => {
return selectedDatasetFiles.some(
(f) => f.datasetId === datasetId && f.fileId === fileId
);
};
const isAllDatasetFilesSelected = (dataset: MockDataset) => {
return dataset.files.every((file) =>
isDatasetFileSelected(dataset.id, file.id)
);
};
const handleSliceOperatorToggle = (operatorId: string) => {
setSelectedSliceOperators((prev) =>
prev.includes(operatorId)
? prev.filter((id) => id !== operatorId)
: [...prev, operatorId]
);
};
// 文件上传
const handleFileChange = (info: any) => {
setUploadedFiles(info.fileList.map((f: any) => f.originFileObj));
};
// 提交表单
const handleCreateKnowledgeBase = async (values: any) => {
await createKnowledgeBaseUsingPost(values);
message.success("知识库创建成功!");
navigate("/data/knowledge-base");
};
return (
<div className="h-full flex flex-col gap-4">
{/* Header */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center">
<Button
type="text"
onClick={() => navigate("/data/knowledge-base")}
>
<ArrowLeft className="w-4 h-4 mr-1" />
</Button>
<h1 className="text-xl font-bold bg-clip-text"></h1>
</div>
</div>
<div className="border-card flex-overflow-auto">
<div className="overflow-auto p-6">
<Form
form={form}
layout="vertical"
initialValues={newKB}
onValuesChange={(_, allValues) => setNewKB(allValues)}
>
{/* 基本信息 */}
<h2 className="font-medium text-gray-900 text-lg mb-2"></h2>
<Form.Item
label="知识库名称"
name="name"
required
rules={[{ required: true, message: "请输入知识库名称" }]}
>
<Input placeholder="输入知识库名称" />
</Form.Item>
<Form.Item label="描述" name="description">
<TextArea placeholder="描述知识库的用途和内容" rows={3} />
</Form.Item>
<Form.Item label="知识库类型" name="type" required>
<RadioCard
options={Object.values(KBTypeMap)}
value={newKB.type}
onChange={(value) => setNewKB({ ...newKB, type: value })}
/>
</Form.Item>
{/* 模型配置 */}
<h2 className="font-medium text-gray-900 text-lg mb-2 flex items-center gap-2">
<Brain className="w-5 h-5" />
</h2>
<Form.Item label="嵌入模型" name="embeddingModel">
<Select>
<Option value="text-embedding-3-large">
text-embedding-3-large ()
</Option>
<Option value="text-embedding-3-small">
text-embedding-3-small
</Option>
<Option value="text-embedding-ada-002">
text-embedding-ada-002
</Option>
</Select>
</Form.Item>
<Form.Item
shouldUpdate={(prev, curr) =>
prev.type !== curr.type || prev.enableQA !== curr.enableQA
}
noStyle
>
{() =>
form.getFieldValue("type") === "unstructured" &&
form.getFieldValue("enableQA") && (
<Form.Item label="LLM模型 (用于Q&A生成)" name="llmModel">
<Select>
<Option value="gpt-4o">GPT-4o ()</Option>
<Option value="gpt-4o-mini">GPT-4o Mini</Option>
<Option value="gpt-3.5-turbo">GPT-3.5 Turbo</Option>
</Select>
</Form.Item>
)
}
</Form.Item>
<Form.Item label="向量数据库" name="vectorDatabase">
<Select>
{vectorDatabases.map((db) => (
<Option key={db.id} value={db.id}>
<div className="flex flex-col">
<span className="font-medium">{db.name}</span>
<span className="text-xs text-gray-500">
{db.description}
</span>
</div>
</Option>
))}
</Select>
</Form.Item>
{/* 切片算子配置 */}
<Form.Item
shouldUpdate={(prev, curr) => prev.type !== curr.type}
noStyle
>
{() =>
form.getFieldValue("type") === "unstructured" && (
<>
<h2 className="font-medium text-gray-900 text-lg mb-2 flex items-center gap-2">
<Scissors className="w-5 h-5" />
</h2>
<div className="grid grid-cols-2 gap-3 mb-4">
{sliceOperators.map((operator) => (
<div
key={operator.id}
className={`border rounded-lg p-3 cursor-pointer transition-all ${
selectedSliceOperators.includes(operator.id)
? "border-blue-500 bg-blue-50"
: "border-gray-200 hover:border-gray-300"
}`}
onClick={() => handleSliceOperatorToggle(operator.id)}
>
<div className="flex items-start gap-3">
<Checkbox
checked={selectedSliceOperators.includes(
operator.id
)}
onChange={() =>
handleSliceOperatorToggle(operator.id)
}
/>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-lg">{operator.icon}</span>
<span className="font-medium text-sm">
{operator.name}
</span>
<span className="ant-badge text-xs">
{operator.type}
</span>
</div>
<p className="text-xs text-gray-600">
{operator.description}
</p>
</div>
</div>
</div>
))}
</div>
<Divider />
{/* 文档分割配置 */}
<h2 className="font-medium text-gray-900 text-lg mb-2 flex items-center gap-2">
<Split className="w-5 h-5" />
</h2>
<Form.Item label="分割方式" name="sliceMethod">
<Select>
<Option value="semantic"> ()</Option>
<Option value="paragraph"></Option>
<Option value="length"></Option>
<Option value="delimiter"></Option>
</Select>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prev, curr) =>
prev.sliceMethod !== curr.sliceMethod
}
>
{() =>
form.getFieldValue("sliceMethod") === "delimiter" && (
<Form.Item
label="分隔符"
name="delimiter"
rules={[
{
required: true,
message: "请输入分隔符",
},
]}
>
<Input placeholder="输入分隔符,如 \\n\\n" />
</Form.Item>
)
}
</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>
<Form.Item
label="启用Q&A生成"
name="enableQA"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Divider />
</>
)
}
</Form.Item>
{/* 数据源选择 */}
<h2 className="font-medium text-gray-900 text-lg mb-2 flex items-center gap-2">
<UploadIcon className="w-5 h-5" />
{form.getFieldValue("type") === "structured"
? "导入模板文件"
: "选择数据源"}
</h2>
<Tabs
defaultActiveKey="upload"
items={[
{
key: "upload",
label: "上传文件",
children: (
<div className="space-y-3">
<Upload
multiple
beforeUpload={() => false}
onChange={handleFileChange}
fileList={uploadedFiles.map((file, idx) => ({
uid: String(idx),
name: file.name,
status: "done",
originFileObj: file,
}))}
showUploadList={false}
>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center relative cursor-pointer">
<UploadIcon className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p className="text-sm text-gray-600">
{form.getFieldValue("type") === "structured"
? "拖拽或点击上传Excel/CSV模板文件"
: "拖拽或点击上传文档文件"}
</p>
<Button
className="mt-2 bg-transparent pointer-events-none"
disabled
>
</Button>
</div>
</Upload>
{uploadedFiles.length > 0 && (
<div className="space-y-2">
<p className="text-sm font-medium">:</p>
<ul className="list-disc pl-5 text-sm text-gray-700">
{uploadedFiles.map((file, index) => (
<li key={index}>{file.name}</li>
))}
</ul>
</div>
)}
</div>
),
},
{
key: "dataset",
label: "从数据集选择",
children: (
<div className="space-y-3">
<div className="flex gap-2 mb-4">
<Input
placeholder="搜索数据集..."
value={datasetSearchQuery}
onChange={(e) =>
setDatasetSearchQuery(e.target.value)
}
className="flex-1"
/>
<Button onClick={() => setSelectedDatasetId(null)}>
</Button>
</div>
<div className="grid grid-cols-3 gap-4 h-80">
<div className="col-span-1 border rounded-lg overflow-y-auto p-2 space-y-2">
{datasets.length === 0 && (
<p className="text-center text-gray-500 py-4 text-sm">
</p>
)}
{datasets.map((dataset) => (
<div
key={dataset.id}
className={`flex items-center justify-between p-3 border rounded-lg cursor-pointer ${
selectedDatasetId === dataset.id
? "bg-blue-50 border-blue-500"
: "hover:bg-gray-50"
}`}
onClick={() => setSelectedDatasetId(dataset.id)}
>
<div className="flex items-center gap-3">
<Folder className="w-5 h-5 text-blue-400" />
<div>
<p className="font-medium">{dataset.name}</p>
<p className="text-xs text-gray-500">
{dataset.files.length}
</p>
</div>
</div>
{selectedDatasetId === dataset.id && (
<CheckCircle className="w-5 h-5 text-blue-600" />
)}
</div>
))}
</div>
<div className="col-span-2 border rounded-lg overflow-y-auto p-2 space-y-2">
{!selectedDatasetId ? (
<div className="text-center py-8 text-gray-500">
<Folder className="w-8 h-8 mx-auto mb-2 text-gray-300" />
<p className="text-sm"></p>
</div>
) : (
<>
<div className="flex items-center gap-2 p-2 border-b pb-2">
<Checkbox
checked={isAllDatasetFilesSelected(
datasets.find(
(d) => d.id === selectedDatasetId
)!
)}
onChange={(e) =>
handleSelectAllDatasetFiles(
datasets.find(
(d) => d.id === selectedDatasetId
)!,
e.target.checked
)
}
/>
<span className="font-medium">
(
{
datasets.find(
(d) => d.id === selectedDatasetId
)?.files.length
}{" "}
)
</span>
</div>
{datasets
.find((d) => d.id === selectedDatasetId)
?.files.map((file) => (
<div
key={file.id}
className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50"
>
<div className="flex items-center gap-3">
<Checkbox
checked={isDatasetFileSelected(
selectedDatasetId!,
file.id
)}
onChange={() =>
handleDatasetFileToggle(
selectedDatasetId!,
file
)
}
/>
<File className="w-5 h-5 text-gray-400" />
<div>
<p className="font-medium">
{file.name}
</p>
<p className="text-sm text-gray-500">
{file.size} {file.type}
</p>
</div>
</div>
</div>
))}
</>
)}
</div>
</div>
{selectedDatasetFiles.length > 0 && (
<div className="mt-4 text-sm font-medium text-gray-700">
: {selectedDatasetFiles.length}
</div>
)}
</div>
),
},
]}
/>
</Form>
</div>
<div className="flex gap-2 justify-end p-6 border-top">
<Button onClick={() => navigate("/data/knowledge-base")}>
</Button>
<Button type="primary" onClick={handleCreateKnowledgeBase}>
</Button>
</div>
</div>
</div>
);
};
export default KnowledgeBaseCreatePage;

View File

@@ -0,0 +1,630 @@
import type React from "react";
import { useEffect, useState } from "react";
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";
import DetailHeader from "@/components/DetailHeader";
import { SearchControls } from "@/components/SearchControls";
import { KnowledgeBaseItem } from "../knowledge-base.model";
const KnowledgeBaseDetailPage: React.FC = () => {
const navigate = useNavigate();
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseItem>(null);
const [files, setFiles] = useState([]);
// 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";
}
}
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 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>
),
},
{
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",
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>
),
},
{
title: "分块数",
dataIndex: "chunkCount",
key: "chunkCount",
render: (chunkCount: number) => (
<span className="font-medium text-gray-900">{chunkCount}</span>
),
},
{
title: "上传时间",
dataIndex: "uploadedAt",
key: "uploadedAt",
},
{
title: "操作",
key: "actions",
align: "right" as const,
render: (_: any, file: KBFile) => (
<Dropdown
menu={{
items: [
{
label: "重试",
key: "retry",
onClick: () => handleStartVectorization(file.id),
},
{
label: "删除",
key: "delete",
onClick: () => handleDeleteFile(file),
},
],
}}
>
<MoreHorizontal />
</Dropdown>
),
},
];
return (
<div className="">
{/* Breadcrumb */}
<div className="mb-4">
<Breadcrumb>
<Breadcrumb.Item>
<a onClick={() => navigate("/data/knowledge-base")}></a>
</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),
},
],
},
]}
/>
{/* 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>
);
};
export default KnowledgeBaseDetailPage;

View File

@@ -0,0 +1,685 @@
import React, { useState } from "react";
import {
Plus,
Eye,
Edit,
Clock,
Trash2,
Scissors,
VideoIcon as Vector,
Server,
FileText,
Download,
} from "lucide-react";
import { Card, Button, Badge, Input, Tabs, Modal, Breadcrumb, Tag } from "antd";
import { mockChunks, mockQAPairs, sliceOperators } from "@/mock/knowledgeBase";
import type {
KnowledgeBase,
KBFile,
} from "@/pages/KnowledgeBase/knowledge-base.model";
import { Link, useNavigate } from "react-router";
import DetailHeader from "@/components/DetailHeader";
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
// 状态标签
const getStatusLabel = (status: string) => {
const labels: Record<string, string> = {
ready: "就绪",
processing: "处理中",
vectorizing: "向量化中",
importing: "导入中",
error: "错误",
disabled: "已禁用",
completed: "已完成",
};
return labels[status] || status;
};
const getStatusColor = (status: string) => {
const colors: Record<string, string> = {
ready: "green",
processing: "blue",
vectorizing: "purple",
importing: "orange",
error: "blue",
disabled: "gray",
completed: "green",
};
return colors[status] || "default";
};
const KnowledgeBaseFileDetail: React.FC = () => {
return <DevelopmentInProgress showTime="2025.10.30" />;
const navigate = useNavigate();
// 假设通过 props 或路由参数获取 selectedFile/selectedKB
const [selectedFile] = useState<KBFile>(
mockChunks.length
? {
id: 1,
name: "API文档.pdf",
type: "pdf",
size: "2.5 MB",
status: "completed",
chunkCount: mockChunks.length,
progress: 100,
uploadedAt: "2024-01-22 10:30",
source: "upload",
vectorizationStatus: "completed",
}
: ({} as KBFile)
);
const [selectedKB] = useState<KnowledgeBase>({
id: 1,
name: "API知识库",
description: "",
type: "unstructured",
status: "ready",
fileCount: 1,
chunkCount: mockChunks.length,
vectorCount: mockChunks.length,
size: "2.5 MB",
progress: 100,
createdAt: "2024-01-22",
lastUpdated: "2024-01-22",
vectorDatabase: "pinecone",
config: {
embeddingModel: "text-embedding-3-large",
chunkSize: 512,
overlap: 50,
sliceMethod: "semantic",
enableQA: true,
vectorDimension: 1536,
sliceOperators: ["semantic-split", "paragraph-split"],
},
files: [],
vectorizationHistory: [],
});
const [currentChunkPage, setCurrentChunkPage] = useState(1);
const chunksPerPage = 5;
const totalPages = Math.ceil(mockChunks.length / chunksPerPage);
const startIndex = (currentChunkPage - 1) * chunksPerPage;
const currentChunks = mockChunks.slice(
startIndex,
startIndex + chunksPerPage
);
const [editingChunk, setEditingChunk] = useState<number | null>(null);
const [editChunkContent, setEditChunkContent] = useState("");
const [chunkDetailModal, setChunkDetailModal] = useState<number | null>(null);
const [showSliceTraceDialog, setShowSliceTraceDialog] = useState<
number | null
>(null);
const handleEditChunk = (chunkId: number, content: string) => {
setEditingChunk(chunkId);
setEditChunkContent(content);
};
const handleSaveChunk = (chunkId: number) => {
// 实际保存逻辑
setEditingChunk(null);
setEditChunkContent("");
};
const handleDeleteChunk = (chunkId: number) => {
// 实际删除逻辑
setEditingChunk(null);
setEditChunkContent("");
};
const handleViewChunkDetail = (chunkId: number) => {
setChunkDetailModal(chunkId);
};
const renderChunks = () => (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600">
{mockChunks.length} {startIndex + 1}-
{Math.min(startIndex + chunksPerPage, mockChunks.length)}
</div>
<div className="flex items-center gap-2">
<Button
size="small"
onClick={() =>
setCurrentChunkPage(Math.max(1, currentChunkPage - 1))
}
disabled={currentChunkPage === 1}
>
</Button>
<span className="text-sm text-gray-600">
{currentChunkPage} / {totalPages}
</span>
<Button
size="small"
onClick={() =>
setCurrentChunkPage(Math.min(totalPages, currentChunkPage + 1))
}
disabled={currentChunkPage === totalPages}
>
</Button>
</div>
</div>
<div className="space-y-4">
{currentChunks.map((chunk) => (
<Card key={chunk.id} className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<div className="flex-1 flex items-center gap-2">
<h4 className="text-sm font-semibold"> {chunk.id}</h4>
<Tag className="text-xs">
{sliceOperators.find(
(op) => op.id === chunk.sliceOperator
)?.name || chunk.sliceOperator}
</Tag>
</div>
<div className="flex items-center gap-2 ml-4">
{editingChunk === chunk.id ? (
<>
<Button
type="primary"
size="small"
onClick={() => handleSaveChunk(chunk.id)}
>
</Button>
<Button
size="small"
onClick={() => {
setEditingChunk(null);
setEditChunkContent("");
}}
>
</Button>
</>
) : (
<>
<Button
size="small"
onClick={() => handleViewChunkDetail(chunk.id)}
>
<Eye className="w-4 h-4" />
</Button>
<Button
size="small"
onClick={() =>
handleEditChunk(chunk.id, chunk.content)
}
>
<Edit className="w-4 h-4" />
</Button>
<Button
size="small"
danger
onClick={() => handleDeleteChunk(chunk.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
</>
)}
</div>
</div>
<div className="text-sm leading-relaxed text-gray-700">
{editingChunk === chunk.id ? (
<Input.TextArea
value={editChunkContent}
onChange={(e) => setEditChunkContent(e.target.value)}
rows={3}
/>
) : (
chunk.content
)}
</div>
<div className="flex items-center gap-4 mt-2 text-xs text-gray-500">
<span>: {chunk.position}</span>
<span>Token: {chunk.tokens}</span>
{chunk.metadata?.page && (
<span>: {chunk.metadata.page}</span>
)}
{chunk.metadata?.section && (
<span>: {chunk.metadata.section}</span>
)}
</div>
</div>
</div>
</Card>
))}
</div>
</div>
);
return (
<div className="flex flex-col gap-4">
<Breadcrumb
items={[
{
title: <Link to="/data/knowledge-base"></Link>,
},
{
title: (
<Link to="/data/knowledge-base/detail/1">
{selectedKB?.name}
</Link>
),
},
{
title: selectedFile.name,
},
]}
/>
<DetailHeader
data={{
id: selectedFile.id,
icon: <FileText className="w-8 h-8" />,
iconColor: "bg-blue-500 text-blue-600",
status: {
label: getStatusLabel(selectedFile.status),
color: getStatusColor(selectedFile.status),
},
name: selectedFile.name,
description: `${selectedFile.size}${
selectedFile.chunkCount
} 个分块${
selectedFile.source === "dataset"
? ` • 数据集: ${selectedFile.datasetId}`
: ""
}`,
createdAt: selectedFile.uploadedAt,
lastUpdated: selectedFile.uploadedAt,
}}
statistics={[
{
icon: <Scissors className="w-4 h-4 text-blue-500" />,
label: "分块",
value: selectedFile.chunkCount,
},
{
icon: <Vector className="w-4 h-4 text-purple-500" />,
label: "向量化状态",
value: getStatusLabel(
selectedFile.vectorizationStatus || "pending"
),
},
{
icon: <Server className="w-4 h-4 text-green-500" />,
label: "文件大小",
value: selectedFile.size,
},
{
icon: <Clock className="w-4 h-4 text-gray-500" />,
label: "上传时间",
value: selectedFile.uploadedAt,
},
]}
operations={[
{
key: "download",
label: "下载",
icon: <Download className="w-4 h-4" />,
onClick: () => {
// 下载逻辑
},
},
{
key: "delete",
label: "删除",
icon: <Trash2 className="w-4 h-4" />,
danger: true,
onClick: () => {
// 删除逻辑
},
},
]}
/>
<Card>{renderChunks()}</Card>
{/* Slice Trace Modal */}
<Modal
open={!!showSliceTraceDialog}
onCancel={() => setShowSliceTraceDialog(null)}
footer={null}
title="知识切片回溯"
width={800}
destroyOnClose
>
<div className="space-y-4">
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="font-medium mb-3"></h4>
<div className="space-y-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center text-sm font-medium">
1
</div>
<div className="flex-1">
<p className="font-medium"></p>
<p className="text-sm text-gray-600">
: {selectedFile.name}
</p>
</div>
<Badge></Badge>
</div>
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-green-500 text-white rounded-full flex items-center justify-center text-sm font-medium">
2
</div>
<div className="flex-1">
<p className="font-medium"></p>
<p className="text-sm text-gray-600">
阈值: 0.7
</p>
</div>
<Badge></Badge>
</div>
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-green-500 text-white rounded-full flex items-center justify-center text-sm font-medium">
3
</div>
<div className="flex-1">
<p className="font-medium"></p>
<p className="text-sm text-gray-600"></p>
</div>
<Badge></Badge>
</div>
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-purple-500 text-white rounded-full flex items-center justify-center text-sm font-medium">
4
</div>
<div className="flex-1">
<p className="font-medium"></p>
<p className="text-sm text-gray-600">
使 {selectedKB?.config.embeddingModel}
</p>
</div>
<Badge>
{selectedFile.vectorizationStatus === "completed"
? "完成"
: "处理中"}
</Badge>
</div>
</div>
</div>
<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 className="text-gray-600">ID:</span>
<span>{showSliceTraceDialog}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">:</span>
<span>
{mockChunks.find((c) => c.id === showSliceTraceDialog)
?.parentChunkId || "无"}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Token数:</span>
<span>
{
mockChunks.find((c) => c.id === showSliceTraceDialog)
?.tokens
}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">:</span>
<span>
{
mockChunks.find((c) => c.id === showSliceTraceDialog)
?.createdAt
}
</span>
</div>
</div>
</Card>
<Card className="p-4">
<h4 className="font-medium mb-2"></h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">ID:</span>
<span className="font-mono text-xs">
{
mockChunks.find((c) => c.id === showSliceTraceDialog)
?.vectorId
}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">:</span>
<span>{selectedKB?.config.vectorDimension}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">:</span>
<span>
{
mockChunks.find((c) => c.id === showSliceTraceDialog)
?.similarity
}
</span>
</div>
</div>
</Card>
</div>
</div>
</Modal>
{/* Chunk Detail Modal */}
<Modal
open={!!chunkDetailModal}
onCancel={() => setChunkDetailModal(null)}
footer={null}
title={`分块详细信息 - 分块 ${chunkDetailModal}`}
width={900}
destroyOnClose
>
<Tabs
defaultActiveKey="content"
items={[
{
key: "content",
label: "内容详情",
children: (
<div>
<div className="font-medium mb-1"></div>
<Input.TextArea
value={
mockChunks.find((c) => c.id === chunkDetailModal)
?.content || ""
}
rows={8}
readOnly
className="mt-2"
/>
</div>
),
},
{
key: "metadata",
label: "元数据",
children: (
<div className="grid grid-cols-2 gap-4">
<div>
<div className="font-medium mb-1"></div>
<Input
value={
mockChunks.find((c) => c.id === chunkDetailModal)
?.position || ""
}
readOnly
/>
</div>
<div>
<div className="font-medium mb-1">Token数量</div>
<Input
value={
mockChunks.find((c) => c.id === chunkDetailModal)
?.tokens || ""
}
readOnly
/>
</div>
<div>
<div className="font-medium mb-1"></div>
<Input
value={
mockChunks.find((c) => c.id === chunkDetailModal)
?.similarity || ""
}
readOnly
/>
</div>
<div>
<div className="font-medium mb-1"></div>
<Input
value={
mockChunks.find((c) => c.id === chunkDetailModal)
?.embedding?.length || ""
}
readOnly
/>
</div>
<div>
<div className="font-medium mb-1"></div>
<Input
value={
mockChunks.find((c) => c.id === chunkDetailModal)
?.createdAt || ""
}
readOnly
/>
</div>
<div>
<div className="font-medium mb-1"></div>
<Input
value={
mockChunks.find((c) => c.id === chunkDetailModal)
?.updatedAt || ""
}
readOnly
/>
</div>
<div>
<div className="font-medium mb-1">ID</div>
<Input
value={
mockChunks.find((c) => c.id === chunkDetailModal)
?.vectorId || ""
}
readOnly
/>
</div>
<div>
<div className="font-medium mb-1"></div>
<Input
value={
mockChunks.find((c) => c.id === chunkDetailModal)
?.sliceOperator || ""
}
readOnly
/>
</div>
</div>
),
},
{
key: "qa",
label: "Q&A对",
children: (
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="font-medium"></span>
<Button size="small">
<Plus className="w-4 h-4 mr-1" />
Q&A
</Button>
</div>
{mockQAPairs.map((qa) => (
<Card key={qa.id} className="p-4">
<div className="space-y-2">
<div>
<span className="text-sm font-medium text-blue-600">
{qa.id}
</span>
<p className="text-sm mt-1">{qa.question}</p>
</div>
<div>
<span className="text-sm font-medium text-green-600">
</span>
<p className="text-sm mt-1">{qa.answer}</p>
</div>
<div className="flex justify-end gap-2">
<Button type="text" size="small">
<Edit className="w-3 h-3 mr-1" />
</Button>
<Button type="text" size="small" danger>
<Trash2 className="w-3 h-3 mr-1" />
</Button>
</div>
</div>
</Card>
))}
</div>
),
},
{
key: "trace",
label: "切片回溯",
children: (
<div className="space-y-3">
<div className="flex items-center gap-3 p-3 bg-blue-50 rounded-lg">
<FileText className="w-5 h-5 text-blue-600" />
<div className="flex-1">
<p className="font-medium"></p>
<p className="text-sm text-gray-600">
{selectedFile.name}
</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-green-50 rounded-lg">
<Scissors className="w-5 h-5 text-green-600" />
<div className="flex-1">
<p className="font-medium"></p>
<p className="text-sm text-gray-600">
:{" "}
{
sliceOperators.find(
(op) =>
op.id ===
mockChunks.find((c) => c.id === chunkDetailModal)
?.sliceOperator
)?.name
}
</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-purple-50 rounded-lg">
<Vector className="w-5 h-5 text-purple-600" />
<div className="flex-1">
<p className="font-medium"></p>
<p className="text-sm text-gray-600">
{selectedKB?.config.vectorDimension}
</p>
</div>
</div>
</div>
),
},
]}
/>
</Modal>
</div>
);
};
export default KnowledgeBaseFileDetail;

View File

@@ -0,0 +1,189 @@
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 KnowledgeGenerationPage() {
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}
/>
</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

@@ -0,0 +1,66 @@
export default function AddDataDialog() {
const [isOpen, setIsOpen] = useState(false);
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const { message } = App.useApp();
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setSelectedFiles(Array.from(e.target.files));
}
};
const handleUpload = async () => {
if (selectedFiles.length === 0) {
message.error("请先选择文件");
return;
}
try {
const formData = new FormData();
selectedFiles.forEach((file) => {
formData.append("files", file);
});
await uploadDataFilesUsingPost(formData);
message.success("文件上传成功");
setIsOpen(false);
setSelectedFiles([]);
} catch (error) {
message.error("文件上传失败");
}
};
return (
<>
<Button type="primary" onClick={() => setIsOpen(true)}>
</Button>
<Modal
title="添加数据文件"
open={isOpen}
onCancel={() => setIsOpen(false)}
onOk={handleUpload}
okText="上传"
>
<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>
)}
</Modal>
</>
);
}

View File

@@ -0,0 +1,136 @@
import { Button, Form, Input, message, Modal, Select } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import { useEffect, useState } from "react";
import { queryModelListUsingGet } from "@/pages/SettingsPage/settings.apis";
import { ModelI } from "@/pages/SettingsPage/ModelAccess";
import {
createKnowledgeBaseUsingPost,
updateKnowledgeBaseByIdUsingPut,
} from "../knowledge-base.api";
import { KnowledgeBaseItem } from "../knowledge-base.model";
export default function CreateKnowledgeBase({
isEdit,
data,
onUpdate,
}: {
isEdit?: boolean;
data?: Partial<KnowledgeBaseItem> | null;
onUpdate: () => void;
}) {
const [open, setOpen] = useState(false);
const [form] = Form.useForm();
const [models, setModels] = useState<ModelI[]>([]);
const embeddingModelOptions = models
.filter((model) => model.type === "EMBEDDING")
.map((model) => ({
label: model.modelName + " (" + model.provider + ")",
value: model.id,
}));
const chatModelOptions = models
.filter((model) => model.type === "CHAT")
.map((model) => ({
label: model.modelName + " (" + model.provider + ")",
value: model.id,
}));
const fetchModels = async () => {
const { data } = await queryModelListUsingGet({ page: 0, size: 1000 });
setModels(data.content || []);
};
useEffect(() => {
if (open) fetchModels();
}, [open]);
useEffect(() => {
if (isEdit && data) {
setOpen(true);
form.setFieldsValue({
name: data.name,
description: data.description,
embeddingModel: data.embeddingModel,
chatModel: data.chatModel,
});
}
}, [isEdit, data, form]);
const handleCreateKnowledgeBase = async () => {
try {
const values = await form.validateFields();
if (isEdit && data) {
await updateKnowledgeBaseByIdUsingPut(data.id!, values);
message.success("知识库更新成功");
} else {
await createKnowledgeBaseUsingPost(values);
message.success("知识库创建成功");
}
setOpen(false);
onUpdate();
} catch (error) {
message.error("操作失败:", error.data.message);
}
};
return (
<>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
form.resetFields();
setOpen(true);
}}
>
</Button>
<Modal
title={isEdit ? "编辑知识库" : "创建知识库"}
open={open}
okText="确定"
cancelText="取消"
onCancel={() => setOpen(false)}
onOk={handleCreateKnowledgeBase}
>
<Form form={form} layout="vertical">
<Form.Item
label="知识库名称"
name="name"
rules={[{ required: true, message: "请输入知识库名称" }]}
>
<Input placeholder="请输入知识库名称" />
</Form.Item>
<Form.Item
label="描述"
name="description"
rules={[{ required: false }]}
>
<Input.TextArea placeholder="请输入知识库描述(可选)" rows={4} />
</Form.Item>
<Form.Item
label="索引模型"
name="embeddingModel"
rules={[{ required: true, message: "请选择索引模型" }]}
>
<Select
placeholder="请选择索引模型"
options={embeddingModelOptions}
/>
</Form.Item>
<Form.Item
label="文本理解模型"
name="chatModel"
rules={[{ required: true, message: "请选择文本理解模型" }]}
>
<Select
placeholder="请选择文本理解模型"
options={chatModelOptions}
/>
</Form.Item>
</Form>
</Modal>
</>
);
}

View File

@@ -0,0 +1,49 @@
import { get, post, put, del } from "@/utils/request";
// 获取知识库列表
export function queryKnowledgeBasesUsingPost(params: any) {
console.log('get tk', params);
return post("/api/knowledge-base/list", params);
}
// 创建知识库
export function createKnowledgeBaseUsingPost(data: any) {
return post("/api/knowledge-base/create", data);
}
// 获取知识库详情
export function queryKnowledgeBaseByIdUsingGet(baseId: string) {
return get(`/api/knowledge-base/${baseId}`);
}
// 更新知识库
export function updateKnowledgeBaseByIdUsingPut(baseId: string, data: any) {
return put(`/api/knowledge-base/${baseId}`, data);
}
// 删除知识库
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 addKnowledgeGenerationFilesUsingPost(baseId: string, data: any) {
return post(`/api/knowledge-base/${baseId}/files`, data);
}
// 获取知识生成文件详情
export function queryKnowledgeGenerationFilesByIdUsingGet(baseId: string, fileId: string) {
return get(`/api/knowledge-base/${baseId}/files/${fileId}`);
}
// 删除知识生成文件
export function deleteKnowledgeGenerationTaskByIdUsingDelete(baseId: string) {
return del(`/api/knowledge-base/${baseId}/files`);
}

View File

@@ -0,0 +1,61 @@
import {
BookOpen,
BookOpenText,
CheckCircle,
Clock,
Database,
XCircle,
} from "lucide-react";
import { KBStatus, KBType, KnowledgeBaseItem } from "./knowledge-base.model";
import { formatBytes, formatDateTime, formatNumber } from "@/utils/unit";
export const KBStatusMap = {
[KBStatus.READY]: {
label: KBStatus.READY,
icon: CheckCircle,
color: "#389e0d",
},
[KBStatus.VECTORIZING]: {
label: KBStatus.PROCESSING,
icon: Clock,
color: "#3b82f6",
},
[KBStatus.ERROR]: {
label: KBStatus.ERROR,
icon: XCircle,
color: "#ef4444",
},
};
export const KBTypeMap = {
[KBType.STRUCTURED]: {
value: KBType.STRUCTURED,
label: "结构化",
icon: Database,
iconColor: "blue",
description: "用于处理和分析文本数据的数据集",
},
[KBType.UNSTRUCTURED]: {
value: KBType.UNSTRUCTURED,
label: "非结构化",
icon: BookOpen,
iconColor: "green",
description: "适用于存储和管理各种格式的文件",
},
};
export function mapKnowledgeBase(kb: KnowledgeBaseItem): KnowledgeBaseItem {
return {
...kb,
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" },
],
updatedAt: formatDateTime(kb.updatedAt),
createdAt: formatDateTime(kb.createdAt),
};
}

View File

@@ -0,0 +1,85 @@
export enum KBStatus {
READY = "ready",
PROCESSING = "processing",
VECTORIZING = "vectorizing",
IMPORTING = "importing",
ERROR = "error",
DISABLED = "disabled",
}
export enum KBType {
UNSTRUCTURED = "unstructured",
STRUCTURED = "structured",
}
export interface KnowledgeBaseItem {
id: number;
name: string;
description: string;
type: KBType;
createdAt: string;
updatedAt: string;
embeddingModel: string;
chatModel: string;
}
export interface KBFile {
id: number;
name: string;
type: string;
size: string;
status: "processing" | "completed" | "error" | "disabled" | "vectorizing";
chunkCount: number;
progress: number;
uploadedAt: string;
source: "upload" | "dataset";
datasetId?: string;
chunks?: Chunk[];
vectorizationStatus?: "pending" | "processing" | "completed" | "failed";
}
interface Chunk {
id: number;
content: string;
position: number;
tokens: number;
embedding?: number[];
similarity?: string;
createdAt?: string;
updatedAt?: string;
vectorId?: string;
sliceOperator?: string;
parentChunkId?: number;
metadata?: {
source: string;
page?: number;
section?: string;
};
}
interface VectorizationRecord {
id: number;
timestamp: string;
operation: "create" | "update" | "delete" | "reprocess";
fileId: number;
fileName: string;
chunksProcessed: number;
vectorsGenerated: number;
status: "success" | "failed" | "partial";
duration: string;
config: {
embeddingModel: string;
chunkSize: number;
sliceMethod: string;
};
error?: string;
}
interface SliceOperator {
id: string;
name: string;
description: string;
type: "text" | "semantic" | "structure" | "custom";
icon: string;
params: Record<string, any>;
}