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;
|
||||
630
frontend/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.tsx
Normal file
630
frontend/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.tsx
Normal file
@@ -0,0 +1,630 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
File,
|
||||
Trash2,
|
||||
Save,
|
||||
Layers,
|
||||
RefreshCw,
|
||||
BookOpen,
|
||||
Database,
|
||||
MoreHorizontal,
|
||||
Upload,
|
||||
Zap,
|
||||
StarOff,
|
||||
CheckCircle,
|
||||
VectorSquareIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Table,
|
||||
Badge,
|
||||
Button,
|
||||
Progress,
|
||||
Input,
|
||||
Modal,
|
||||
message,
|
||||
Card,
|
||||
Breadcrumb,
|
||||
Checkbox,
|
||||
Dropdown,
|
||||
} from "antd";
|
||||
import { useNavigate } from "react-router";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import { KnowledgeBaseItem } from "../knowledge-base.model";
|
||||
|
||||
const KnowledgeBaseDetailPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseItem>(null);
|
||||
const [files, setFiles] = useState([]);
|
||||
|
||||
|
||||
// File table logic
|
||||
const handleDeleteFile = (file: KBFile) => {};
|
||||
|
||||
|
||||
const handleDeleteKB = (kb: KnowledgeBase) => {};
|
||||
|
||||
// 状态 Badge 映射
|
||||
function getStatusBadgeVariant(status: string) {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
case "ready":
|
||||
return "success";
|
||||
case "processing":
|
||||
case "vectorizing":
|
||||
return "processing";
|
||||
case "importing":
|
||||
return "warning";
|
||||
case "error":
|
||||
return "error";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
}
|
||||
function getStatusLabel(status: string) {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
case "ready":
|
||||
return "已完成";
|
||||
case "processing":
|
||||
return "处理中";
|
||||
case "vectorizing":
|
||||
return "向量化中";
|
||||
case "importing":
|
||||
return "导入中";
|
||||
case "error":
|
||||
return "错误";
|
||||
case "disabled":
|
||||
return "已禁用";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
}
|
||||
function getStatusIcon(status: string) {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
case "ready":
|
||||
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
||||
case "processing":
|
||||
case "vectorizing":
|
||||
return <RefreshCw className="w-4 h-4 text-blue-500 animate-spin" />;
|
||||
case "importing":
|
||||
return <Upload className="w-4 h-4 text-orange-500" />;
|
||||
case "error":
|
||||
return <Trash2 className="w-4 h-4 text-red-500" />;
|
||||
default:
|
||||
return <File className="w-4 h-4 text-gray-400" />;
|
||||
}
|
||||
}
|
||||
|
||||
const fileColumns = [
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div style={{ padding: 8 }}>
|
||||
<Input
|
||||
placeholder="搜索文件名"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={confirm}
|
||||
style={{ width: 188, marginBottom: 8, display: "block" }}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={confirm}
|
||||
size="small"
|
||||
style={{ width: 90, marginRight: 8 }}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
<Button onClick={clearFilters} size="small" style={{ width: 90 }}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: KBFile) =>
|
||||
record.name.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string, file: KBFile) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() =>
|
||||
navigate("/data/knowledge-base/file-detail/" + file.id)
|
||||
}
|
||||
>
|
||||
{file.name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
filters: allFileTypes.map((type) => ({
|
||||
text: type,
|
||||
value: type,
|
||||
})),
|
||||
onFilter: (value: string, record: KBFile) => record.type === value,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "size",
|
||||
key: "size",
|
||||
sorter: (a: KBFile, b: KBFile) => parseFloat(a.size) - parseFloat(b.size),
|
||||
sortOrder: fileSortOrder,
|
||||
},
|
||||
{
|
||||
title: "向量化状态",
|
||||
dataIndex: "vectorizationStatus",
|
||||
key: "vectorizationStatus",
|
||||
filters: allVectorizationStatuses
|
||||
.filter((opt) => opt.value !== null)
|
||||
.map((opt) => ({
|
||||
text: opt.label,
|
||||
value: opt.value,
|
||||
})),
|
||||
onFilter: (value: string, record: KBFile) =>
|
||||
record.vectorizationStatus === value,
|
||||
render: (_: any, file: KBFile) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
status={getStatusBadgeVariant(
|
||||
file.vectorizationStatus || "pending"
|
||||
)}
|
||||
text={getStatusLabel(file.vectorizationStatus || "pending")}
|
||||
/>
|
||||
{file.vectorizationStatus === "processing" && (
|
||||
<div className="w-16">
|
||||
<Progress percent={file.progress} size="small" showInfo={false} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "来源",
|
||||
dataIndex: "source",
|
||||
key: "source",
|
||||
render: (_: any, file: KBFile) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
status={file.source === "upload" ? "processing" : "default"}
|
||||
text={file.source === "upload" ? "上传" : "数据集"}
|
||||
/>
|
||||
{file.datasetId && (
|
||||
<span className="text-xs text-gray-500">({file.datasetId})</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "分块数",
|
||||
dataIndex: "chunkCount",
|
||||
key: "chunkCount",
|
||||
render: (chunkCount: number) => (
|
||||
<span className="font-medium text-gray-900">{chunkCount}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "上传时间",
|
||||
dataIndex: "uploadedAt",
|
||||
key: "uploadedAt",
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
align: "right" as const,
|
||||
render: (_: any, file: KBFile) => (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: "重试",
|
||||
key: "retry",
|
||||
onClick: () => handleStartVectorization(file.id),
|
||||
},
|
||||
{
|
||||
label: "删除",
|
||||
key: "delete",
|
||||
onClick: () => handleDeleteFile(file),
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<MoreHorizontal />
|
||||
</Dropdown>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
{/* Breadcrumb */}
|
||||
<div className="mb-4">
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<a onClick={() => navigate("/data/knowledge-base")}>知识库</a>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>{knowledgeBase.name}</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Knowledge Base Header */}
|
||||
<DetailHeader
|
||||
data={{
|
||||
icon:
|
||||
knowledgeBase.type === "structured" ? (
|
||||
<Database className="w-8 h-8" />
|
||||
) : (
|
||||
<BookOpen className="w-8 h-8" />
|
||||
),
|
||||
status: {
|
||||
label: getStatusLabel(knowledgeBase.status),
|
||||
icon: getStatusIcon(knowledgeBase.status),
|
||||
color: getStatusBadgeVariant(knowledgeBase.status),
|
||||
},
|
||||
name: knowledgeBase.name,
|
||||
description: knowledgeBase.description,
|
||||
createdAt: knowledgeBase.createdAt,
|
||||
lastUpdated: knowledgeBase.lastUpdated,
|
||||
}}
|
||||
statistics={[
|
||||
{
|
||||
icon: <File className="w-4 h-4 text-gray-400" />,
|
||||
label: "文件",
|
||||
value: knowledgeBase.fileCount,
|
||||
},
|
||||
{
|
||||
icon: <Layers className="w-4 h-4 text-gray-400" />,
|
||||
label: "分块",
|
||||
value: knowledgeBase.chunkCount?.toLocaleString?.() ?? 0,
|
||||
},
|
||||
{
|
||||
icon: <StarOff className="w-4 h-4 text-gray-400" />,
|
||||
label: "向量",
|
||||
value: knowledgeBase.vectorCount?.toLocaleString?.() ?? 0,
|
||||
},
|
||||
{
|
||||
icon: <Database className="w-4 h-4 text-gray-400" />,
|
||||
label: "大小",
|
||||
value: knowledgeBase.size,
|
||||
},
|
||||
]}
|
||||
operations={[
|
||||
{
|
||||
key: "edit",
|
||||
label: "修改参数配置",
|
||||
icon: <Edit className="w-4 h-4" />,
|
||||
onClick: () => {
|
||||
setEditForm(knowledgeBase);
|
||||
setCurrentView("config");
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "vector",
|
||||
label: "向量化管理",
|
||||
icon: <VectorSquareIcon className="w-4 h-4" />,
|
||||
onClick: () => setShowVectorizationDialog(true),
|
||||
},
|
||||
...(knowledgeBase.status === "error"
|
||||
? [
|
||||
{
|
||||
key: "retry",
|
||||
label: "重试",
|
||||
onClick: () => {}, // 填写重试逻辑
|
||||
danger: false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "more",
|
||||
label: "更多操作",
|
||||
icon: <MoreHorizontal className="w-4 h-4" />,
|
||||
isDropdown: true,
|
||||
items: [
|
||||
{
|
||||
key: "download",
|
||||
label: "导出",
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
label: "配置",
|
||||
},
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
onClick: () => handleDeleteKB(knowledgeBase),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{/* Tab Navigation */}
|
||||
<Card>
|
||||
{/* Files Section */}
|
||||
<div className="flex items-center justify-between mb-4 gap-4">
|
||||
<div className="flex-1">
|
||||
<SearchControls
|
||||
searchTerm={fileSearchQuery}
|
||||
onSearchChange={setFileSearchQuery}
|
||||
searchPlaceholder="搜索文件名..."
|
||||
filters={[
|
||||
{
|
||||
key: "status",
|
||||
label: "状态筛选",
|
||||
options: [
|
||||
{ label: "全部状态", value: "all" },
|
||||
{ label: "已完成", value: "completed" },
|
||||
{ label: "处理中", value: "processing" },
|
||||
{ label: "向量化中", value: "vectorizing" },
|
||||
{ label: "错误", value: "error" },
|
||||
{ label: "已禁用", value: "disabled" },
|
||||
],
|
||||
},
|
||||
]}
|
||||
onFiltersChange={(filters) => {
|
||||
setFileStatusFilter(filters.status?.[0] || "all");
|
||||
}}
|
||||
showViewToggle={false}
|
||||
/>
|
||||
</div>
|
||||
<Button type="primary">
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
添加文件
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Files Table */}
|
||||
<Table
|
||||
columns={fileColumns}
|
||||
dataSource={files}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<div className="text-center py-12">
|
||||
<File className="w-12 h-12 mx-auto mb-4 text-gray-300" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
没有找到文件
|
||||
</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
尝试调整搜索条件或添加新文件
|
||||
</p>
|
||||
<Button type="dashed">
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
添加文件
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
{/* Vectorization Dialog */}
|
||||
<Modal
|
||||
open={showVectorizationDialog}
|
||||
onCancel={() => setShowVectorizationDialog(false)}
|
||||
footer={null}
|
||||
title="向量化管理"
|
||||
width={700}
|
||||
destroyOnClose
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Card className="p-4">
|
||||
<h4 className="font-medium mb-2">当前状态</h4>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span>已向量化文件:</span>
|
||||
<span>
|
||||
{
|
||||
knowledgeBase.files.filter(
|
||||
(f) => f.vectorizationStatus === "completed"
|
||||
).length
|
||||
}
|
||||
/{knowledgeBase.files.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>向量总数:</span>
|
||||
<span>
|
||||
{knowledgeBase.vectorCount?.toLocaleString?.() ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>存储大小:</span>
|
||||
<span>{knowledgeBase.size}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<h4 className="font-medium mb-2">操作选项</h4>
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
className="w-full"
|
||||
type="primary"
|
||||
onClick={() => handleStartVectorization()}
|
||||
>
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
批量向量化
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => message.info("TODO: 重新向量化全部")}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
重新向量化全部
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
danger
|
||||
onClick={() => message.info("TODO: 清空向量数据")}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
清空向量数据
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium mb-3">文件向量化状态</h4>
|
||||
<div className="space-y-2 max-h-60 overflow-y-auto">
|
||||
{knowledgeBase.files.map((file: KBFile) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex items-center justify-between p-3 border rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<File className="w-4 h-4 text-gray-400" />
|
||||
<div>
|
||||
<p className="font-medium text-sm">{file.name}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{file.chunkCount} 个分块
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
status={getStatusBadgeVariant(
|
||||
file.vectorizationStatus || "pending"
|
||||
)}
|
||||
text={getStatusLabel(
|
||||
file.vectorizationStatus || "pending"
|
||||
)}
|
||||
/>
|
||||
{file.vectorizationStatus === "processing" && (
|
||||
<div className="w-16">
|
||||
<Progress
|
||||
percent={file.progress}
|
||||
size="small"
|
||||
showInfo={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{file.vectorizationStatus !== "completed" && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => handleStartVectorization(file.id)}
|
||||
>
|
||||
<StarOff className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end pt-2">
|
||||
<Button onClick={() => setShowVectorizationDialog(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
{/* Edit File Dialog */}
|
||||
<Modal
|
||||
open={!!showEditFileDialog}
|
||||
onCancel={() => setShowEditFileDialog(null)}
|
||||
title="编辑文件"
|
||||
width={600}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setShowEditFileDialog(null)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="save"
|
||||
type="primary"
|
||||
onClick={() => setShowEditFileDialog(null)}
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
保存更改
|
||||
</Button>,
|
||||
]}
|
||||
destroyOnClose
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block mb-1">文件名</label>
|
||||
<Input value={showEditFileDialog?.name} readOnly />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1">文件来源</label>
|
||||
<Input
|
||||
value={
|
||||
showEditFileDialog?.source === "upload"
|
||||
? "上传文件"
|
||||
: "数据集文件"
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showEditFileDialog?.source === "upload" ? (
|
||||
<div className="space-y-3">
|
||||
<label className="block mb-1">更新文件</label>
|
||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
|
||||
<Upload className="w-8 h-8 mx-auto mb-2 text-gray-400" />
|
||||
<p className="text-sm text-gray-600">
|
||||
拖拽或点击上传新版本文件
|
||||
</p>
|
||||
<Button className="mt-2 bg-transparent" disabled>
|
||||
选择文件
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<label className="block mb-1">数据集文件管理</label>
|
||||
<div className="p-4 border rounded-lg bg-gray-50">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium">
|
||||
当前数据集: {showEditFileDialog?.datasetId}
|
||||
</span>
|
||||
<Button size="small">
|
||||
<RefreshCw className="w-4 h-4 mr-1" />
|
||||
更新数据集文件
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600">
|
||||
此文件来自数据集,可以选择更新数据集中的对应文件或切换到其他数据集文件
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="block mb-1">处理选项</label>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="reprocess" />
|
||||
<span className="text-sm">更新后重新处理分块</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="revectorize" />
|
||||
<span className="text-sm">重新生成向量</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseDetailPage;
|
||||
@@ -0,0 +1,685 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Plus,
|
||||
Eye,
|
||||
Edit,
|
||||
Clock,
|
||||
Trash2,
|
||||
Scissors,
|
||||
VideoIcon as Vector,
|
||||
Server,
|
||||
FileText,
|
||||
Download,
|
||||
} from "lucide-react";
|
||||
import { Card, Button, Badge, Input, Tabs, Modal, Breadcrumb, Tag } from "antd";
|
||||
import { mockChunks, mockQAPairs, sliceOperators } from "@/mock/knowledgeBase";
|
||||
import type {
|
||||
KnowledgeBase,
|
||||
KBFile,
|
||||
} from "@/pages/KnowledgeBase/knowledge-base.model";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
|
||||
|
||||
// 状态标签
|
||||
const getStatusLabel = (status: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
ready: "就绪",
|
||||
processing: "处理中",
|
||||
vectorizing: "向量化中",
|
||||
importing: "导入中",
|
||||
error: "错误",
|
||||
disabled: "已禁用",
|
||||
completed: "已完成",
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
ready: "green",
|
||||
processing: "blue",
|
||||
vectorizing: "purple",
|
||||
importing: "orange",
|
||||
error: "blue",
|
||||
disabled: "gray",
|
||||
completed: "green",
|
||||
};
|
||||
return colors[status] || "default";
|
||||
};
|
||||
|
||||
const KnowledgeBaseFileDetail: React.FC = () => {
|
||||
return <DevelopmentInProgress showTime="2025.10.30" />;
|
||||
const navigate = useNavigate();
|
||||
// 假设通过 props 或路由参数获取 selectedFile/selectedKB
|
||||
const [selectedFile] = useState<KBFile>(
|
||||
mockChunks.length
|
||||
? {
|
||||
id: 1,
|
||||
name: "API文档.pdf",
|
||||
type: "pdf",
|
||||
size: "2.5 MB",
|
||||
status: "completed",
|
||||
chunkCount: mockChunks.length,
|
||||
progress: 100,
|
||||
uploadedAt: "2024-01-22 10:30",
|
||||
source: "upload",
|
||||
vectorizationStatus: "completed",
|
||||
}
|
||||
: ({} as KBFile)
|
||||
);
|
||||
const [selectedKB] = useState<KnowledgeBase>({
|
||||
id: 1,
|
||||
name: "API知识库",
|
||||
description: "",
|
||||
type: "unstructured",
|
||||
status: "ready",
|
||||
fileCount: 1,
|
||||
chunkCount: mockChunks.length,
|
||||
vectorCount: mockChunks.length,
|
||||
size: "2.5 MB",
|
||||
progress: 100,
|
||||
createdAt: "2024-01-22",
|
||||
lastUpdated: "2024-01-22",
|
||||
vectorDatabase: "pinecone",
|
||||
config: {
|
||||
embeddingModel: "text-embedding-3-large",
|
||||
chunkSize: 512,
|
||||
overlap: 50,
|
||||
sliceMethod: "semantic",
|
||||
enableQA: true,
|
||||
vectorDimension: 1536,
|
||||
sliceOperators: ["semantic-split", "paragraph-split"],
|
||||
},
|
||||
files: [],
|
||||
vectorizationHistory: [],
|
||||
});
|
||||
|
||||
const [currentChunkPage, setCurrentChunkPage] = useState(1);
|
||||
const chunksPerPage = 5;
|
||||
const totalPages = Math.ceil(mockChunks.length / chunksPerPage);
|
||||
const startIndex = (currentChunkPage - 1) * chunksPerPage;
|
||||
const currentChunks = mockChunks.slice(
|
||||
startIndex,
|
||||
startIndex + chunksPerPage
|
||||
);
|
||||
|
||||
const [editingChunk, setEditingChunk] = useState<number | null>(null);
|
||||
const [editChunkContent, setEditChunkContent] = useState("");
|
||||
const [chunkDetailModal, setChunkDetailModal] = useState<number | null>(null);
|
||||
const [showSliceTraceDialog, setShowSliceTraceDialog] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const handleEditChunk = (chunkId: number, content: string) => {
|
||||
setEditingChunk(chunkId);
|
||||
setEditChunkContent(content);
|
||||
};
|
||||
|
||||
const handleSaveChunk = (chunkId: number) => {
|
||||
// 实际保存逻辑
|
||||
setEditingChunk(null);
|
||||
setEditChunkContent("");
|
||||
};
|
||||
|
||||
const handleDeleteChunk = (chunkId: number) => {
|
||||
// 实际删除逻辑
|
||||
setEditingChunk(null);
|
||||
setEditChunkContent("");
|
||||
};
|
||||
|
||||
const handleViewChunkDetail = (chunkId: number) => {
|
||||
setChunkDetailModal(chunkId);
|
||||
};
|
||||
|
||||
const renderChunks = () => (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600">
|
||||
共 {mockChunks.length} 个分块,第 {startIndex + 1}-
|
||||
{Math.min(startIndex + chunksPerPage, mockChunks.length)} 个
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setCurrentChunkPage(Math.max(1, currentChunkPage - 1))
|
||||
}
|
||||
disabled={currentChunkPage === 1}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<span className="text-sm text-gray-600">
|
||||
{currentChunkPage} / {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setCurrentChunkPage(Math.min(totalPages, currentChunkPage + 1))
|
||||
}
|
||||
disabled={currentChunkPage === totalPages}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{currentChunks.map((chunk) => (
|
||||
<Card key={chunk.id} className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<h4 className="text-sm font-semibold">分块 {chunk.id}</h4>
|
||||
<Tag className="text-xs">
|
||||
{sliceOperators.find(
|
||||
(op) => op.id === chunk.sliceOperator
|
||||
)?.name || chunk.sliceOperator}
|
||||
</Tag>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
{editingChunk === chunk.id ? (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => handleSaveChunk(chunk.id)}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setEditingChunk(null);
|
||||
setEditChunkContent("");
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => handleViewChunkDetail(chunk.id)}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() =>
|
||||
handleEditChunk(chunk.id, chunk.content)
|
||||
}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
onClick={() => handleDeleteChunk(chunk.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm leading-relaxed text-gray-700">
|
||||
{editingChunk === chunk.id ? (
|
||||
<Input.TextArea
|
||||
value={editChunkContent}
|
||||
onChange={(e) => setEditChunkContent(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
) : (
|
||||
chunk.content
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-2 text-xs text-gray-500">
|
||||
<span>位置: {chunk.position}</span>
|
||||
<span>Token: {chunk.tokens}</span>
|
||||
{chunk.metadata?.page && (
|
||||
<span>页码: {chunk.metadata.page}</span>
|
||||
)}
|
||||
{chunk.metadata?.section && (
|
||||
<span>章节: {chunk.metadata.section}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
title: <Link to="/data/knowledge-base">知识库</Link>,
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Link to="/data/knowledge-base/detail/1">
|
||||
{selectedKB?.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: selectedFile.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<DetailHeader
|
||||
data={{
|
||||
id: selectedFile.id,
|
||||
icon: <FileText className="w-8 h-8" />,
|
||||
iconColor: "bg-blue-500 text-blue-600",
|
||||
status: {
|
||||
label: getStatusLabel(selectedFile.status),
|
||||
color: getStatusColor(selectedFile.status),
|
||||
},
|
||||
name: selectedFile.name,
|
||||
description: `${selectedFile.size} • ${
|
||||
selectedFile.chunkCount
|
||||
} 个分块${
|
||||
selectedFile.source === "dataset"
|
||||
? ` • 数据集: ${selectedFile.datasetId}`
|
||||
: ""
|
||||
}`,
|
||||
createdAt: selectedFile.uploadedAt,
|
||||
lastUpdated: selectedFile.uploadedAt,
|
||||
}}
|
||||
statistics={[
|
||||
{
|
||||
icon: <Scissors className="w-4 h-4 text-blue-500" />,
|
||||
label: "分块",
|
||||
value: selectedFile.chunkCount,
|
||||
},
|
||||
{
|
||||
icon: <Vector className="w-4 h-4 text-purple-500" />,
|
||||
label: "向量化状态",
|
||||
value: getStatusLabel(
|
||||
selectedFile.vectorizationStatus || "pending"
|
||||
),
|
||||
},
|
||||
{
|
||||
icon: <Server className="w-4 h-4 text-green-500" />,
|
||||
label: "文件大小",
|
||||
value: selectedFile.size,
|
||||
},
|
||||
{
|
||||
icon: <Clock className="w-4 h-4 text-gray-500" />,
|
||||
label: "上传时间",
|
||||
value: selectedFile.uploadedAt,
|
||||
},
|
||||
]}
|
||||
operations={[
|
||||
{
|
||||
key: "download",
|
||||
label: "下载",
|
||||
icon: <Download className="w-4 h-4" />,
|
||||
onClick: () => {
|
||||
// 下载逻辑
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
// 删除逻辑
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Card>{renderChunks()}</Card>
|
||||
|
||||
{/* Slice Trace Modal */}
|
||||
<Modal
|
||||
open={!!showSliceTraceDialog}
|
||||
onCancel={() => setShowSliceTraceDialog(null)}
|
||||
footer={null}
|
||||
title="知识切片回溯"
|
||||
width={800}
|
||||
destroyOnClose
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<h4 className="font-medium mb-3">切片处理流程</h4>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center text-sm font-medium">
|
||||
1
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">原始文档导入</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
文档: {selectedFile.name}
|
||||
</p>
|
||||
</div>
|
||||
<Badge>完成</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-green-500 text-white rounded-full flex items-center justify-center text-sm font-medium">
|
||||
2
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">语义分割算子</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
基于语义相似度智能分割,阈值: 0.7
|
||||
</p>
|
||||
</div>
|
||||
<Badge>完成</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-green-500 text-white rounded-full flex items-center justify-center text-sm font-medium">
|
||||
3
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">段落分割算子</p>
|
||||
<p className="text-sm text-gray-600">按段落边界进一步细分</p>
|
||||
</div>
|
||||
<Badge>完成</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-purple-500 text-white rounded-full flex items-center justify-center text-sm font-medium">
|
||||
4
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">向量化处理</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
使用 {selectedKB?.config.embeddingModel} 生成向量
|
||||
</p>
|
||||
</div>
|
||||
<Badge>
|
||||
{selectedFile.vectorizationStatus === "completed"
|
||||
? "完成"
|
||||
: "处理中"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Card className="p-4">
|
||||
<h4 className="font-medium mb-2">分块信息</h4>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">分块ID:</span>
|
||||
<span>{showSliceTraceDialog}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">父分块:</span>
|
||||
<span>
|
||||
{mockChunks.find((c) => c.id === showSliceTraceDialog)
|
||||
?.parentChunkId || "无"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Token数:</span>
|
||||
<span>
|
||||
{
|
||||
mockChunks.find((c) => c.id === showSliceTraceDialog)
|
||||
?.tokens
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">创建时间:</span>
|
||||
<span>
|
||||
{
|
||||
mockChunks.find((c) => c.id === showSliceTraceDialog)
|
||||
?.createdAt
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<h4 className="font-medium mb-2">向量信息</h4>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">向量ID:</span>
|
||||
<span className="font-mono text-xs">
|
||||
{
|
||||
mockChunks.find((c) => c.id === showSliceTraceDialog)
|
||||
?.vectorId
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">向量维度:</span>
|
||||
<span>{selectedKB?.config.vectorDimension}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">相似度:</span>
|
||||
<span>
|
||||
{
|
||||
mockChunks.find((c) => c.id === showSliceTraceDialog)
|
||||
?.similarity
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Chunk Detail Modal */}
|
||||
<Modal
|
||||
open={!!chunkDetailModal}
|
||||
onCancel={() => setChunkDetailModal(null)}
|
||||
footer={null}
|
||||
title={`分块详细信息 - 分块 ${chunkDetailModal}`}
|
||||
width={900}
|
||||
destroyOnClose
|
||||
>
|
||||
<Tabs
|
||||
defaultActiveKey="content"
|
||||
items={[
|
||||
{
|
||||
key: "content",
|
||||
label: "内容详情",
|
||||
children: (
|
||||
<div>
|
||||
<div className="font-medium mb-1">分块内容</div>
|
||||
<Input.TextArea
|
||||
value={
|
||||
mockChunks.find((c) => c.id === chunkDetailModal)
|
||||
?.content || ""
|
||||
}
|
||||
rows={8}
|
||||
readOnly
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "metadata",
|
||||
label: "元数据",
|
||||
children: (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="font-medium mb-1">位置</div>
|
||||
<Input
|
||||
value={
|
||||
mockChunks.find((c) => c.id === chunkDetailModal)
|
||||
?.position || ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">Token数量</div>
|
||||
<Input
|
||||
value={
|
||||
mockChunks.find((c) => c.id === chunkDetailModal)
|
||||
?.tokens || ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">相似度</div>
|
||||
<Input
|
||||
value={
|
||||
mockChunks.find((c) => c.id === chunkDetailModal)
|
||||
?.similarity || ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">向量维度</div>
|
||||
<Input
|
||||
value={
|
||||
mockChunks.find((c) => c.id === chunkDetailModal)
|
||||
?.embedding?.length || ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">创建时间</div>
|
||||
<Input
|
||||
value={
|
||||
mockChunks.find((c) => c.id === chunkDetailModal)
|
||||
?.createdAt || ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">更新时间</div>
|
||||
<Input
|
||||
value={
|
||||
mockChunks.find((c) => c.id === chunkDetailModal)
|
||||
?.updatedAt || ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">向量ID</div>
|
||||
<Input
|
||||
value={
|
||||
mockChunks.find((c) => c.id === chunkDetailModal)
|
||||
?.vectorId || ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">切片算子</div>
|
||||
<Input
|
||||
value={
|
||||
mockChunks.find((c) => c.id === chunkDetailModal)
|
||||
?.sliceOperator || ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "qa",
|
||||
label: "Q&A对",
|
||||
children: (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">关联的问答对</span>
|
||||
<Button size="small">
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
添加Q&A
|
||||
</Button>
|
||||
</div>
|
||||
{mockQAPairs.map((qa) => (
|
||||
<Card key={qa.id} className="p-4">
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-blue-600">
|
||||
问题 {qa.id}
|
||||
</span>
|
||||
<p className="text-sm mt-1">{qa.question}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-green-600">
|
||||
答案
|
||||
</span>
|
||||
<p className="text-sm mt-1">{qa.answer}</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="text" size="small">
|
||||
<Edit className="w-3 h-3 mr-1" />
|
||||
编辑
|
||||
</Button>
|
||||
<Button type="text" size="small" danger>
|
||||
<Trash2 className="w-3 h-3 mr-1" />
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "trace",
|
||||
label: "切片回溯",
|
||||
children: (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3 p-3 bg-blue-50 rounded-lg">
|
||||
<FileText className="w-5 h-5 text-blue-600" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">原始文档</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{selectedFile.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-green-50 rounded-lg">
|
||||
<Scissors className="w-5 h-5 text-green-600" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">切片算子处理</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
应用算子:{" "}
|
||||
{
|
||||
sliceOperators.find(
|
||||
(op) =>
|
||||
op.id ===
|
||||
mockChunks.find((c) => c.id === chunkDetailModal)
|
||||
?.sliceOperator
|
||||
)?.name
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-purple-50 rounded-lg">
|
||||
<Vector className="w-5 h-5 text-purple-600" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">向量化处理</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
生成 {selectedKB?.config.vectorDimension} 维向量
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseFileDetail;
|
||||
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),
|
||||
};
|
||||
}
|
||||
85
frontend/src/pages/KnowledgeBase/knowledge-base.model.ts
Normal file
85
frontend/src/pages/KnowledgeBase/knowledge-base.model.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
export enum KBStatus {
|
||||
READY = "ready",
|
||||
PROCESSING = "processing",
|
||||
VECTORIZING = "vectorizing",
|
||||
IMPORTING = "importing",
|
||||
ERROR = "error",
|
||||
DISABLED = "disabled",
|
||||
}
|
||||
|
||||
export enum KBType {
|
||||
UNSTRUCTURED = "unstructured",
|
||||
STRUCTURED = "structured",
|
||||
}
|
||||
|
||||
export interface KnowledgeBaseItem {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
type: KBType;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
embeddingModel: string;
|
||||
chatModel: string;
|
||||
}
|
||||
|
||||
export interface KBFile {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
size: string;
|
||||
status: "processing" | "completed" | "error" | "disabled" | "vectorizing";
|
||||
chunkCount: number;
|
||||
progress: number;
|
||||
uploadedAt: string;
|
||||
source: "upload" | "dataset";
|
||||
datasetId?: string;
|
||||
chunks?: Chunk[];
|
||||
vectorizationStatus?: "pending" | "processing" | "completed" | "failed";
|
||||
}
|
||||
|
||||
interface Chunk {
|
||||
id: number;
|
||||
content: string;
|
||||
position: number;
|
||||
tokens: number;
|
||||
embedding?: number[];
|
||||
similarity?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
vectorId?: string;
|
||||
sliceOperator?: string;
|
||||
parentChunkId?: number;
|
||||
metadata?: {
|
||||
source: string;
|
||||
page?: number;
|
||||
section?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface VectorizationRecord {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
operation: "create" | "update" | "delete" | "reprocess";
|
||||
fileId: number;
|
||||
fileName: string;
|
||||
chunksProcessed: number;
|
||||
vectorsGenerated: number;
|
||||
status: "success" | "failed" | "partial";
|
||||
duration: string;
|
||||
config: {
|
||||
embeddingModel: string;
|
||||
chunkSize: number;
|
||||
sliceMethod: string;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface SliceOperator {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: "text" | "semantic" | "structure" | "custom";
|
||||
icon: string;
|
||||
params: Record<string, any>;
|
||||
}
|
||||
Reference in New Issue
Block a user