You've already forked DataMate
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:
582
frontend/src/pages/KnowledgeBase/Create/KnowledgeBaseCreate.tsx
Normal file
582
frontend/src/pages/KnowledgeBase/Create/KnowledgeBaseCreate.tsx
Normal 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;
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
),
|
),
|
||||||
189
frontend/src/pages/KnowledgeBase/Home/KnowledgeGeneration.tsx
Normal file
189
frontend/src/pages/KnowledgeBase/Home/KnowledgeGeneration.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
frontend/src/pages/KnowledgeBase/knowledge-base.api.ts
Normal file
49
frontend/src/pages/KnowledgeBase/knowledge-base.api.ts
Normal 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`);
|
||||||
|
}
|
||||||
61
frontend/src/pages/KnowledgeBase/knowledge-base.const.tsx
Normal file
61
frontend/src/pages/KnowledgeBase/knowledge-base.const.tsx
Normal 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),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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的知识库构建",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user