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

@@ -30,62 +30,20 @@ import {
Checkbox, Checkbox,
Dropdown, Dropdown,
} from "antd"; } from "antd";
import { mockKnowledgeBases } from "@/mock/knowledgeBase";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import DetailHeader from "@/components/DetailHeader"; import DetailHeader from "@/components/DetailHeader";
import { SearchControls } from "@/components/SearchControls"; import { SearchControls } from "@/components/SearchControls";
import DevelopmentInProgress from "@/components/DevelopmentInProgress"; import { KnowledgeBaseItem } from "../knowledge-base.model";
const KnowledgeBaseDetailPage: React.FC = () => { const KnowledgeBaseDetailPage: React.FC = () => {
return <DevelopmentInProgress />;
const navigate = useNavigate(); const navigate = useNavigate();
const knowledgeBase = mockKnowledgeBases[0]; const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseItem>(null);
const [files, setFiles] = useState([]); const [files, setFiles] = useState([]);
// --- 新增的状态 ---
const [fileSearchQuery, setFileSearchQuery] = useState("");
const [fileTypeFilter, setFileTypeFilter] = useState<string | null>(null);
const [fileStatusFilter, setFileStatusFilter] = useState<string | null>(null);
const [fileSortOrder, setFileSortOrder] = useState<
"ascend" | "descend" | null
>(null);
// 获取所有类型和状态选项
const allFileTypes = Array.from(
new Set((knowledgeBase.files ?? []).map((f: KBFile) => f.type))
).filter(Boolean);
const allVectorizationStatuses = [
{ label: "全部", value: null },
{ label: "已完成", value: "completed" },
{ label: "处理中", value: "processing" },
{ label: "向量化中", value: "vectorizing" },
{ label: "导入中", value: "importing" },
{ label: "错误", value: "error" },
{ label: "已禁用", value: "disabled" },
];
useEffect(() => {
setFiles(knowledgeBase.files);
}, [knowledgeBase]);
const [showVectorizationDialog, setShowVectorizationDialog] = useState(false);
const [showEditFileDialog, setShowEditFileDialog] = useState<KBFile | null>(
null
);
// File table logic // File table logic
const handleDeleteFile = (file: KBFile) => {}; const handleDeleteFile = (file: KBFile) => {};
const handleFileSelect = (file: KBFile) => {
setShowEditFileDialog(file);
};
const handleStartVectorization = (fileId?: string) => {
message.info(fileId ? `开始向量化文件 ${fileId}` : "批量向量化所有文件");
// 实际业务逻辑可在此实现
};
const handleDeleteKB = (kb: KnowledgeBase) => {}; const handleDeleteKB = (kb: KnowledgeBase) => {};
@@ -182,7 +140,7 @@ const KnowledgeBaseDetailPage: React.FC = () => {
<Button <Button
type="link" type="link"
onClick={() => onClick={() =>
navigate("/data/knowledge-generation/file-detail/" + file.id) navigate("/data/knowledge-base/file-detail/" + file.id)
} }
> >
{file.name} {file.name}
@@ -296,7 +254,7 @@ const KnowledgeBaseDetailPage: React.FC = () => {
<div className="mb-4"> <div className="mb-4">
<Breadcrumb> <Breadcrumb>
<Breadcrumb.Item> <Breadcrumb.Item>
<a onClick={() => navigate("/data/knowledge-generation")}></a> <a onClick={() => navigate("/data/knowledge-base")}></a>
</Breadcrumb.Item> </Breadcrumb.Item>
<Breadcrumb.Item>{knowledgeBase.name}</Breadcrumb.Item> <Breadcrumb.Item>{knowledgeBase.name}</Breadcrumb.Item>
</Breadcrumb> </Breadcrumb>

View File

@@ -16,7 +16,7 @@ import { mockChunks, mockQAPairs, sliceOperators } from "@/mock/knowledgeBase";
import type { import type {
KnowledgeBase, KnowledgeBase,
KBFile, KBFile,
} from "@/pages/KnowledgeGeneration/knowledge-base.model"; } from "@/pages/KnowledgeBase/knowledge-base.model";
import { Link, useNavigate } from "react-router"; import { Link, useNavigate } from "react-router";
import DetailHeader from "@/components/DetailHeader"; import DetailHeader from "@/components/DetailHeader";
import DevelopmentInProgress from "@/components/DevelopmentInProgress"; import DevelopmentInProgress from "@/components/DevelopmentInProgress";
@@ -258,11 +258,11 @@ const KnowledgeBaseFileDetail: React.FC = () => {
<Breadcrumb <Breadcrumb
items={[ items={[
{ {
title: <Link to="/data/knowledge-generation"></Link>, title: <Link to="/data/knowledge-base"></Link>,
}, },
{ {
title: ( title: (
<Link to="/data/knowledge-generation/detail/1"> <Link to="/data/knowledge-base/detail/1">
{selectedKB?.name} {selectedKB?.name}
</Link> </Link>
), ),

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

@@ -1,33 +1,29 @@
interface KnowledgeBase { 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; id: number;
name: string; name: string;
description: string; description: string;
type: "unstructured" | "structured"; type: KBType;
status: "processing" | "ready" | "error" | "importing" | "vectorizing";
fileCount: number;
chunkCount: number;
vectorCount: number;
size: string;
progress: number;
createdAt: string; createdAt: string;
lastUpdated: string; updatedAt: string;
vectorDatabase: string; embeddingModel: string;
config: { chatModel: string;
embeddingModel: string;
llmModel?: string;
chunkSize: number;
overlap: number;
sliceMethod: "paragraph" | "length" | "delimiter" | "semantic";
delimiter?: string;
enableQA: boolean;
vectorDimension: number;
sliceOperators: string[];
};
files: KBFile[];
vectorizationHistory: VectorizationRecord[];
} }
interface KBFile { export interface KBFile {
id: number; id: number;
name: string; name: string;
type: string; type: string;

View File

@@ -1,671 +0,0 @@
import {
mockKnowledgeBases,
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 DevelopmentInProgress from "@/components/DevelopmentInProgress";
const { TextArea } = Input;
const { Option } = Select;
const KnowledgeBaseCreatePage: React.FC = () => {
return <DevelopmentInProgress showTime="2025.10.30" />;
const navigate = useNavigate();
const [form] = Form.useForm();
const [knowledgeBases, setKnowledgeBases] =
useState<KnowledgeBase[]>(mockKnowledgeBases);
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 initialValues = {
name: "",
description: "",
type: "unstructured" as "unstructured" | "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 handleFinish = (values: any) => {
const newKB: KnowledgeBase = {
id: Date.now(),
name: values.name,
description: values.description,
type: values.type,
status: "importing",
fileCount: uploadedFiles.length + selectedDatasetFiles.length,
chunkCount: 0,
vectorCount: 0,
size: "0 MB",
progress: 0,
createdAt: new Date().toISOString().split("T")[0],
lastUpdated: new Date().toISOString().split("T")[0],
vectorDatabase: values.vectorDatabase,
config: {
embeddingModel: values.embeddingModel,
llmModel: values.llmModel,
chunkSize: values.chunkSize,
overlap: values.overlap,
sliceMethod: values.sliceMethod,
delimiter: values.delimiter,
enableQA: values.enableQA,
vectorDimension: values.embeddingModel.includes("3-large")
? 3072
: 1536,
sliceOperators: selectedSliceOperators,
},
files: [
...uploadedFiles.map((file) => ({
id: Date.now() + Math.random(),
name: file.name,
type: file.type.split("/")[1] || "unknown",
size: `${(file.size / (1024 * 1024)).toFixed(2)} MB`,
status: "processing" as const,
chunkCount: 0,
progress: 0,
uploadedAt: new Date().toISOString().split("T")[0],
source: "upload" as const,
vectorizationStatus: "pending" as const,
})),
...selectedDatasetFiles.map((file) => ({
id: Date.now() + Math.random(),
name: file.name,
type: file.type,
size: file.size,
status: "processing" as const,
chunkCount: 0,
progress: 0,
uploadedAt: new Date().toISOString().split("T")[0],
source: "dataset" as const,
datasetId: file.datasetId,
vectorizationStatus: "pending" as const,
})),
],
vectorizationHistory: [],
};
setKnowledgeBases([newKB, ...knowledgeBases]);
form.resetFields();
setUploadedFiles([]);
setSelectedDatasetFiles([]);
setSelectedSliceOperators(["semantic-split", "paragraph-split"]);
setSelectedDatasetId(null);
message.success("知识库创建成功!");
navigate("/data/knowledge-generation");
};
return (
<div className="min-h-screen">
{/* Header */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center">
<Button
type="text"
onClick={() => navigate("/data/knowledge-generation")}
>
<ArrowLeft className="w-4 h-4 mr-1" />
</Button>
<h1 className="text-xl font-bold bg-clip-text"></h1>
</div>
</div>
<Card className="overflow-y-auto p-2">
<Form
form={form}
layout="vertical"
initialValues={initialValues}
onFinish={handleFinish}
>
{/* 基本信息 */}
<h2 className="font-medium text-gray-900 text-lg mb-2"></h2>
<Form.Item
label="知识库名称"
name="name"
rules={[{ required: true, message: "请输入知识库名称" }]}
>
<Input placeholder="输入知识库名称" />
</Form.Item>
<Form.Item label="描述" name="description">
<TextArea placeholder="描述知识库的用途和内容" rows={3} />
</Form.Item>
<Form.Item label="知识库类型" name="type">
<div className="grid grid-cols-2 gap-4">
<Button
onClick={() => form.setFieldValue("type", "unstructured")}
type={
form.getFieldValue("type") === "unstructured"
? "primary"
: "default"
}
className="h-auto py-4 flex flex-col items-center gap-2"
>
<BookOpen className="w-6 h-6" />
<p className="font-medium"></p>
<p className="text-xs text-center opacity-80">
PDF等文件
</p>
</Button>
<Button
onClick={() => form.setFieldValue("type", "structured")}
type={
form.getFieldValue("type") === "structured"
? "primary"
: "default"
}
className="h-auto py-4 flex flex-col items-center gap-2"
>
<Database className="w-6 h-6" />
<p className="font-medium"></p>
<p className="text-xs text-center opacity-80">
</p>
</Button>
</div>
</Form.Item>
<Divider />
{/* 模型配置 */}
<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>
<Divider />
{/* 切片算子配置 */}
<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>
),
},
]}
/>
<Divider />
<div className="flex gap-2 justify-end">
<Button onClick={() => navigate("/data/knowledge-generation")}>
</Button>
<Button type="primary" htmlType="submit">
</Button>
</div>
</Form>
</Card>
</div>
);
};
export default KnowledgeBaseCreatePage;

View File

@@ -1,409 +0,0 @@
import { useState } from "react";
import { Card, Button, Badge, Table, Dropdown } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import { SearchControls } from "@/components/SearchControls";
import {
BookOpen,
Plus,
Upload,
Database,
CheckCircle,
XCircle,
Clock,
AlertCircle,
MoreHorizontal,
VideoIcon as Vector,
} from "lucide-react";
import { mockKnowledgeBases, vectorDatabases } from "@/mock/knowledgeBase";
import { useNavigate } from "react-router";
import CardView from "@/components/CardView";
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
export default function KnowledgeGenerationPage() {
return <DevelopmentInProgress showTime="2025.10.30" />;
const navigate = useNavigate();
const [knowledgeBases, setKnowledgeBases] =
useState<KnowledgeBase[]>(mockKnowledgeBases);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: mockKnowledgeBases.length,
showSizeChanger: true,
pageSizeOptions: ["10", "20", "50", "100"],
onChange: (page: number, pageSize?: number) => {
setPagination((prev) => ({
...prev,
current: page,
pageSize: pageSize || prev.pageSize,
}));
},
onShowSizeChange: (current: number, size: number) => {
setPagination((prev) => ({
...prev,
current: current,
pageSize: size,
}));
},
});
const [typeFilter, setTypeFilter] = useState("all");
const [statusFilter, setStatusFilter] = useState("all");
const [sortBy, setSortBy] = useState<
"name" | "size" | "fileCount" | "createdAt"
>("createdAt");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
const [viewMode, setViewMode] = useState<"card" | "list">("card");
const [searchTerm, setSearchTerm] = useState("");
const filterOptions = [
{
key: "type",
label: "类型",
options: [
{ label: "非结构化", value: "unstructured" },
{ label: "结构化", value: "structured" },
],
},
{
key: "status",
label: "状态",
options: [
{ label: "就绪", value: "ready" },
{ label: "处理中", value: "processing" },
{ label: "向量化中", value: "vectorizing" },
{ label: "导入中", value: "importing" },
{ label: "错误", value: "error" },
],
},
];
const sortOptions = [
{ label: "名称", value: "name" },
{ label: "大小", value: "size" },
{ label: "文件数量", value: "fileCount" },
{ label: "创建时间", value: "createdAt" },
{ label: "修改时间", value: "lastModified" },
];
// Filter and sort logic
const filteredData = knowledgeBases.filter((item) => {
// Search filter
if (
searchTerm &&
!item.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
!item.description.toLowerCase().includes(searchTerm.toLowerCase())
) {
return false;
}
// Type filter
if (typeFilter !== "all" && item.type !== typeFilter) {
return false;
}
// Status filter
if (statusFilter !== "all" && item.status !== statusFilter) {
return false;
}
return true;
});
// Sort data
if (sortBy) {
filteredData.sort((a, b) => {
let aValue: any = a[sortBy as keyof KnowledgeBase];
let bValue: any = b[sortBy as keyof KnowledgeBase];
if (sortBy === "size") {
aValue = Number.parseFloat(aValue.replace(/[^\d.]/g, ""));
bValue = Number.parseFloat(bValue.replace(/[^\d.]/g, ""));
}
if (typeof aValue === "string") {
aValue = aValue.toLowerCase();
bValue = bValue.toLowerCase();
}
if (sortOrder === "asc") {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
}
const getStatusIcon = (status: string) => {
switch (status) {
case "ready":
case "completed":
return <CheckCircle />;
case "processing":
return <Clock />;
case "vectorizing":
return <Vector />;
case "importing":
return <Upload />;
case "error":
return <XCircle />;
case "disabled":
return <AlertCircle />;
default:
return <AlertCircle />;
}
};
const getStatusLabel = (status: string) => {
const labels = {
ready: "就绪",
processing: "处理中",
vectorizing: "向量化中",
importing: "导入中",
error: "错误",
disabled: "已禁用",
completed: "已完成",
};
return labels[status as keyof typeof labels] || status;
};
const getStatusColor = (status: string) => {
switch (status) {
case "ready":
case "completed":
return "#389e0d"; // green-500
case "processing":
case "vectorizing":
case "importing":
return "#3b82f6"; // blue-600
case "error":
return "#ef4444"; // red-600
case "disabled":
return "#6b7280"; // gray-600
default:
return "#6b7280"; // gray-600
}
};
const handleDeleteKB = (kb: KnowledgeBase) => {
if (confirm(`确定要删除知识库 "${kb.name}" 吗?此操作不可撤销。`)) {
setKnowledgeBases((prev) => prev.filter((k) => k.id !== kb.id));
}
};
const columns = [
{
title: "知识库",
dataIndex: "name",
key: "name",
fixed: "left" as const,
width: 200,
render: (_: any, kb: KnowledgeBase) => (
<div
className="flex items-center gap-3 cursor-pointer"
onClick={() => navigate(`/data/knowledge-generation/detail/${kb.id}`)}
>
{kb.name}
</div>
),
},
{
title: "类型",
dataIndex: "type",
key: "type",
render: (type: string) => (
<Badge>{type === "structured" ? "结构化" : "非结构化"}</Badge>
),
},
{
title: "状态",
dataIndex: "status",
key: "status",
render: (status: string) => (
<div
className={`inline-flex items-center text-white px-2 py-1 rounded text-xs`}
style={{ backgroundColor: getStatusColor(status) }}
>
{getStatusIcon(status)}
<span className="ml-1">{getStatusLabel(status)}</span>
</div>
),
},
{
title: "向量数据库",
dataIndex: "vectorDatabase",
key: "vectorDatabase",
render: (vectorDatabase: string) => (
<span className="text-sm">
{vectorDatabases.find((db) => db.id === vectorDatabase)?.name}
</span>
),
},
{
title: "文件数",
dataIndex: "fileCount",
key: "fileCount",
render: (fileCount: number) => (
<span className="font-medium">{fileCount}</span>
),
},
{
title: "向量数",
dataIndex: "vectorCount",
key: "vectorCount",
render: (vectorCount: number) => (
<span className="font-medium">{vectorCount?.toLocaleString()}</span>
),
},
{
title: "大小",
dataIndex: "size",
key: "size",
render: (size: string) => <span className="font-medium">{size}</span>,
},
{
title: "创建时间",
dataIndex: "createdAt",
key: "createdAt",
render: (createdAt: string) => (
<span className="text-sm text-gray-600">{createdAt}</span>
),
},
{
title: "操作",
key: "actions",
fixed: "right" as const,
render: (_: any, kb: KnowledgeBase) => (
<div className="flex items-center justify-end gap-2">
<Dropdown
trigger={["click"]}
menu={{
items: [
{
label: "编辑",
key: "edit",
},
{
label: "导出",
key: "download",
},
{ type: "divider" },
{
label: "删除",
key: "delete",
danger: true,
onClick: () => handleDeleteKB(kb),
},
],
}}
>
<Button type="text" size="small" className="h-8 w-8 p-0">
<MoreHorizontal className="h-4 w-4" />
</Button>
</Dropdown>
</div>
),
},
];
// Main list view
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h1 className="text-xl font-bold"></h1>
<Button
type="primary"
onClick={() => navigate("/data/knowledge-generation/create")}
icon={<PlusOutlined className="w-4 h-4" />}
>
</Button>
</div>
{/* Search and Controls */}
<SearchControls
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
searchPlaceholder="搜索知识库..."
filters={filterOptions}
viewMode={viewMode}
onViewModeChange={setViewMode}
/>
{viewMode === "card" ? (
<CardView
data={filteredData.map((kb) => ({
id: kb.id,
name: kb.name,
type: kb.type,
icon: kb.type === "structured" ? <Database /> : <BookOpen />,
iconColor: "bg-blue-200",
status: {
label: getStatusLabel(kb.status),
icon: getStatusIcon(kb.status),
color: getStatusColor(kb.status),
},
description: kb.description,
tags: [],
statistics: [
{ label: "文件", value: kb.fileCount },
{ label: "分块", value: kb.chunkCount },
{ label: "向量", value: kb.vectorCount },
{ label: "大小", value: kb.size },
],
lastModified: kb.lastUpdated || kb.createdAt,
}))}
operations={[
{
key: "edit",
label: "编辑",
onClick: (item) => {},
},
{
key: "download",
label: "导出",
},
{
key: "delete",
label: "删除",
onClick: (item) =>
handleDeleteKB(knowledgeBases.find((kb) => kb.id === item.id)!),
},
]}
onView={(item) =>
navigate(`/data/knowledge-generation/detail/${item.id}`)
}
pagination={pagination}
/>
) : (
<Card>
<Table
scroll={{ x: "max-content" }}
columns={columns}
dataSource={filteredData}
rowKey="id"
locale={{
emptyText: (
<div className="text-center py-16">
<div className="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<BookOpen className="w-12 h-12 text-gray-400" />
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">
</h3>
<p className="text-gray-500 mb-6">
</p>
<Button
onClick={() => navigate("/knowledge-generation/create")}
className="bg-blue-600 hover:bg-blue-700"
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
),
}}
/>
</Card>
)}
</div>
);
}

View File

@@ -71,7 +71,7 @@ export const menuItems = [
color: "bg-indigo-500", color: "bg-indigo-500",
}, },
{ {
id: "knowledge-generation", id: "knowledge-base",
title: "知识生成", title: "知识生成",
icon: BookOpen, icon: BookOpen,
description: "面向RAG的知识库构建", description: "面向RAG的知识库构建",

View File

@@ -27,12 +27,13 @@ import {
updateModelByIdUsingPut, updateModelByIdUsingPut,
} from "./settings.apis"; } from "./settings.apis";
interface ModelI { export interface ModelI {
id: string; id: string;
name: string; modelName: string;
provider: string; provider: string;
model: string; model: string;
apiKey: string; apiKey: string;
type: string;
isEnabled: boolean; isEnabled: boolean;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;

View File

@@ -31,10 +31,9 @@ import EvaluationTaskCreate from "@/pages/DataEvaluation/Create/CreateTask";
import EvaluationTaskReport from "@/pages/DataEvaluation/Report/EvaluationReport"; import EvaluationTaskReport from "@/pages/DataEvaluation/Report/EvaluationReport";
import ManualEvaluatePage from "@/pages/DataEvaluation/Evaluate/ManualEvaluate"; import ManualEvaluatePage from "@/pages/DataEvaluation/Evaluate/ManualEvaluate";
import KnowledgeGenerationPage from "@/pages/KnowledgeGeneration/Home/KnowledgeGeneration"; import KnowledgeGenerationPage from "@/pages/KnowledgeBase/Home/KnowledgeGeneration";
import KnowledgeBaseCreatePage from "@/pages/KnowledgeGeneration/Create/KnowledgeBaseCreate"; import KnowledgeBaseDetailPage from "@/pages/KnowledgeBase/Detail/KnowledgeBaseDetail";
import KnowledgeBaseDetailPage from "@/pages/KnowledgeGeneration/Detail/KnowledgeBaseDetail"; import KnowledgeBaseFileDetailPage from "@/pages/KnowledgeBase/FileDetail/KnowledgeBaseFileDetail";
import KnowledgeBaseFileDetailPage from "@/pages/KnowledgeGeneration/FileDetail/KnowledgeBaseFileDetail";
import OperatorMarketPage from "@/pages/OperatorMarket/Home/OperatorMarket"; import OperatorMarketPage from "@/pages/OperatorMarket/Home/OperatorMarket";
import OperatorPluginCreate from "@/pages/OperatorMarket/Create/OperatorPluginCreate"; import OperatorPluginCreate from "@/pages/OperatorMarket/Create/OperatorPluginCreate";
@@ -218,17 +217,13 @@ const router = createBrowserRouter([
], ],
}, },
{ {
path: "knowledge-generation", path: "knowledge-base",
children: [ children: [
{ {
path: "", path: "",
index: true, index: true,
Component: KnowledgeGenerationPage, Component: KnowledgeGenerationPage,
}, },
{
path: "create/:id?",
Component: KnowledgeBaseCreatePage,
},
{ {
path: "detail/:id", path: "detail/:id",
Component: KnowledgeBaseDetailPage, Component: KnowledgeBaseDetailPage,
@@ -248,7 +243,7 @@ const router = createBrowserRouter([
Component: OperatorMarketPage, Component: OperatorMarketPage,
}, },
{ {
path: "create", path: "create/:id?",
Component: OperatorPluginCreate, Component: OperatorPluginCreate,
}, },
{ {