feat: fileCard and dataCard
This commit is contained in:
parent
5b9332c673
commit
ba6c2d27d5
1
client/public/imgs/files/file.svg
Normal file
1
client/public/imgs/files/file.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694227361688" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4974" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M862 902c0 16.569-13.431 30-30 30H192c-16.569 0-30-13.431-30-30V122c0-16.569 13.431-30 30-30h476l194 194v616z" fill="#4895FF" p-id="4975"></path><path d="M862 286H698c-16.569 0-30-13.431-30-30V92" fill="#FFFFFF" fill-opacity=".296" p-id="4976"></path><path d="M290 349m37 0l224.931 0q37 0 37 37l0 0q0 37-37 37l-224.931 0q-37 0-37-37l0 0q0-37 37-37Z" fill="#FFFFFF" p-id="4977"></path><path d="M290 511m37 0l370 0q37 0 37 37l0 0q0 37-37 37l-370 0q-37 0-37-37l0 0q0-37 37-37Z" fill="#FFFFFF" p-id="4978"></path><path d="M290 673m37 0l370 0q37 0 37 37l0 0q0 37-37 37l-370 0q-37 0-37-37l0 0q0-37 37-37Z" fill="#FFFFFF" p-id="4979"></path></svg>
|
||||
|
After Width: | Height: | Size: 971 B |
@ -78,6 +78,7 @@
|
||||
"Copy Successful": "Copy Successful",
|
||||
"Course": "",
|
||||
"Delete": "Delete",
|
||||
"Delete Failed": "Delete Failed",
|
||||
"Delete Success": "Delete Successful",
|
||||
"Delete Warning": "Warning",
|
||||
"Filed is repeat": "Filed is repeated",
|
||||
@ -86,10 +87,13 @@
|
||||
"Output": "Output",
|
||||
"Password inconsistency": "Password inconsistency",
|
||||
"Rename": "Rename",
|
||||
"Search": "Search",
|
||||
"Status": "Status",
|
||||
"export": ""
|
||||
},
|
||||
"dataset": {
|
||||
"Confirm to delete the data": "Confirm to delete the data?",
|
||||
"Export": "Export",
|
||||
"Queue Desc": "This data refers to the current amount of training for the entire system. FastGPT uses queued training, and if you have too much data to train, you may need to wait for a while",
|
||||
"System Data Queue": "Data Queue"
|
||||
},
|
||||
@ -99,9 +103,11 @@
|
||||
"Create File": "Create File",
|
||||
"Create file": "Create file",
|
||||
"Drag and drop": "Drag and drop files here",
|
||||
"Embedding": "Embedding",
|
||||
"Fetch Url": "Fetch Url",
|
||||
"If the imported file is garbled, please convert CSV to UTF-8 encoding format": "If the imported file is garbled, please convert CSV to UTF-8 encoding format",
|
||||
"Parse": "{{name}} Parsing...",
|
||||
"Ready": "Ready",
|
||||
"Release the mouse to upload the file": "Release the mouse to upload the file",
|
||||
"Select a maximum of 10 files": "Select a maximum of 10 files",
|
||||
"Uploading": "Uploading: {{name}}, Progress: {{percent}}%",
|
||||
@ -156,11 +162,18 @@
|
||||
"slogan": "Let the AI know more about you"
|
||||
},
|
||||
"kb": {
|
||||
"Chunk Length": "Chunk Length",
|
||||
"Confirm to delete the file": "Are you sure to delete the file and all its data?",
|
||||
"Create Folder": "Create Folder",
|
||||
"Delete Dataset Error": "Delete dataset failed",
|
||||
"Edit Folder": "Edit Folder",
|
||||
"File Size": "File Size",
|
||||
"Filename": "Filename",
|
||||
"Files": "{{total}} Files",
|
||||
"Folder Name": "Input folder name",
|
||||
"My Dataset": "My Dataset",
|
||||
"Other Data": "Other Data",
|
||||
"Upload Time": "Upload Time",
|
||||
"deleteDatasetTips": "Are you sure to delete the knowledge base? Data cannot be recovered after deletion, please confirm!",
|
||||
"deleteFolderTips": "Are you sure to delete this folder and all the knowledge bases it contains? Data cannot be recovered after deletion, please confirm!"
|
||||
},
|
||||
|
||||
@ -78,6 +78,7 @@
|
||||
"Copy Successful": "复制成功",
|
||||
"Course": "",
|
||||
"Delete": "删除",
|
||||
"Delete Failed": "删除失败",
|
||||
"Delete Success": "删除成功",
|
||||
"Delete Warning": "删除警告",
|
||||
"Filed is repeat": "",
|
||||
@ -86,10 +87,13 @@
|
||||
"Output": "输出",
|
||||
"Password inconsistency": "两次密码不一致",
|
||||
"Rename": "重命名",
|
||||
"Search": "搜索",
|
||||
"Status": "状态",
|
||||
"export": ""
|
||||
},
|
||||
"dataset": {
|
||||
"Confirm to delete the data": "确认删除该数据?",
|
||||
"Export": "导出",
|
||||
"Queue Desc": "该数据是指整个系统当前待训练的数量。{{title}} 采用排队训练的方式,如果待训练的数据过多,可能需要等待一段时间",
|
||||
"System Data Queue": "排队长度"
|
||||
},
|
||||
@ -99,9 +103,11 @@
|
||||
"Create File": "创建新文件",
|
||||
"Create file": "创建文件",
|
||||
"Drag and drop": "拖拽文件至此",
|
||||
"Embedding": "索引中",
|
||||
"Fetch Url": "链接读取",
|
||||
"If the imported file is garbled, please convert CSV to UTF-8 encoding format": "如果导入文件乱码,请将 CSV 转成 UTF-8 编码格式",
|
||||
"Parse": "{{name}} 解析中...",
|
||||
"Ready": "可用",
|
||||
"Release the mouse to upload the file": "松开鼠标上传文件",
|
||||
"Select a maximum of 10 files": "最多选择10个文件",
|
||||
"Uploading": "正在上传 {{name}},进度: {{percent}}%",
|
||||
@ -156,11 +162,18 @@
|
||||
"slogan": "让 AI 更懂你的知识"
|
||||
},
|
||||
"kb": {
|
||||
"Chunk Length": "数据总量",
|
||||
"Confirm to delete the file": "确认删除该文件及其所有数据?",
|
||||
"Create Folder": "创建文件夹",
|
||||
"Delete Dataset Error": "删除知识库异常",
|
||||
"Edit Folder": "编辑文件夹",
|
||||
"File Size": "文件大小",
|
||||
"Filename": "文件名",
|
||||
"Files": "文件: {{total}}个",
|
||||
"Folder Name": "输入文件夹名称",
|
||||
"My Dataset": "我的知识库",
|
||||
"Other Data": "其他数据",
|
||||
"Upload Time": "上传时间",
|
||||
"deleteDatasetTips": "确认删除该知识库?删除后数据无法恢复,请确认!",
|
||||
"deleteFolderTips": "确认删除该文件夹及其包含的所有知识库?删除后数据无法恢复,请确认!"
|
||||
},
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import { GET, POST, PUT, DELETE } from '../request';
|
||||
import type { DatasetItemType, KbItemType, KbListItemType, KbPathItemType } from '@/types/plugin';
|
||||
import { RequestPaging } from '@/types/index';
|
||||
import type {
|
||||
DatasetItemType,
|
||||
FileInfo,
|
||||
KbFileItemType,
|
||||
KbItemType,
|
||||
KbListItemType,
|
||||
KbPathItemType
|
||||
} from '@/types/plugin';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import {
|
||||
Props as PushDataProps,
|
||||
@ -11,7 +17,7 @@ import {
|
||||
Response as SearchTestResponse
|
||||
} from '@/pages/api/openapi/kb/searchTest';
|
||||
import { Props as UpdateDataProps } from '@/pages/api/openapi/kb/updateData';
|
||||
import type { KbUpdateParams, CreateKbParams } from '../request/kb';
|
||||
import type { KbUpdateParams, CreateKbParams, GetKbDataListProps } from '../request/kb';
|
||||
import { QuoteItemType } from '@/types/chat';
|
||||
|
||||
/* knowledge base */
|
||||
@ -29,11 +35,17 @@ export const putKbById = (data: KbUpdateParams) => PUT(`/plugins/kb/update`, dat
|
||||
|
||||
export const delKbById = (id: string) => DELETE(`/plugins/kb/delete?id=${id}`);
|
||||
|
||||
/* kb file */
|
||||
export const getKbFiles = (kbId: string) =>
|
||||
GET<KbFileItemType[]>(`/plugins/kb/file/list`, { kbId });
|
||||
export const deleteKbFileById = (params: { fileId: string; kbId: string }) =>
|
||||
DELETE(`/plugins/kb/file/delFileByFileId`, params);
|
||||
export const getFileInfoById = (fileId: string) =>
|
||||
GET<FileInfo>(`/plugins/kb/file/getFileInfo`, { fileId });
|
||||
export const delEmptyFiles = (kbId: string) =>
|
||||
DELETE(`/plugins/kb/file/deleteEmptyFiles`, { kbId });
|
||||
|
||||
/* kb data */
|
||||
type GetKbDataListProps = RequestPaging & {
|
||||
kbId: string;
|
||||
searchText: string;
|
||||
};
|
||||
export const getKbDataList = (data: GetKbDataListProps) =>
|
||||
POST(`/plugins/kb/data/getDataList`, data);
|
||||
|
||||
|
||||
8
client/src/api/request/kb.d.ts
vendored
8
client/src/api/request/kb.d.ts
vendored
@ -1,4 +1,6 @@
|
||||
import { KbTypeEnum } from '@/constants/kb';
|
||||
import type { RequestPaging } from '@/types';
|
||||
|
||||
export type KbUpdateParams = {
|
||||
id: string;
|
||||
tags?: string;
|
||||
@ -13,3 +15,9 @@ export type CreateKbParams = {
|
||||
vectorModel?: string;
|
||||
type: `${KbTypeEnum}`;
|
||||
};
|
||||
|
||||
export type GetKbDataListProps = RequestPaging & {
|
||||
kbId: string;
|
||||
searchText: string;
|
||||
fileId: string;
|
||||
};
|
||||
|
||||
8
client/src/components/Icon/icons/light/search.svg
Normal file
8
client/src/components/Icon/icons/light/search.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694224177076"
|
||||
class="icon" viewBox="0 0 1026 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3984"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" width="64.125" height="64">
|
||||
<path
|
||||
d="M989.365124 873.455175c21.85764 24.973422 33.760499 46.831061 35.714294 65.567202 1.948078 18.730424-5.271103 37.076377-21.661831 55.026425-18.736141 21.075836-39.416072 31.030616-62.055515 29.858625-22.633727-1.171991-44.491366-10.344968-65.567202-27.513213L679.093265 806.715982c-35.128298 22.633727-72.786383 40.197876-112.989976 52.68673-40.197876 12.488855-82.545355 18.730424-127.036721 18.730424-60.882095 0-117.863745-11.512672-170.940663-34.536586-53.078347-23.029631-99.523509-54.446147-139.331197-94.252406-39.811976-39.811976-71.228492-86.25285-94.252406-139.329768C11.512672 556.93603 0 499.950092 0 439.066568c0-60.883524 11.512672-117.863745 34.542303-170.940663 23.023914-53.078347 54.44043-99.523509 94.252406-139.331197 39.807688-39.811976 86.25285-71.228492 139.331197-94.252406 53.076918-23.029631 110.058568-34.542303 170.940663-34.542303 60.883524 0 117.869462 11.512672 170.94638 34.542303 53.078347 23.023914 99.517792 54.44043 139.329768 94.252406 39.807688 39.807688 71.222775 86.25285 94.252406 139.331197 23.023914 53.076918 34.536586 110.057139 34.536586 170.940663 0 46.054974-6.633185 89.764536-19.903844 131.134403s-32.002511 79.619664-56.198417 114.742246l38.639985 38.639985c18.730424 18.730424 38.439889 38.249797 59.124108 58.543829s39.61188 39.416072 56.784413 57.371837C973.754771 857.448917 984.680017 868.771497 989.365124 873.455175L989.365124 873.455175zM443.751675 731.779995c40.588063 0 78.83786-7.609369 114.742246-22.829535 35.904385-15.224454 67.13081-36.105911 93.66641-62.641511 26.541317-26.541317 47.422774-57.762025 62.641511-93.66641 15.218737-35.910102 22.835252-74.154183 22.835252-114.747963 0-40.589492-7.615086-78.832143-22.835252-114.742246-15.218737-35.905815-36.100194-67.125093-62.641511-93.667839-26.5356-26.5356-57.762025-47.415628-93.66641-62.641511-35.904385-15.218737-74.154183-22.828106-114.742246-22.828106-40.589492 0-78.83929 7.609369-114.743675 22.828106-35.904385 15.225883-67.129381 36.105911-93.66641 62.641511-26.541317 26.542747-47.422774 57.762025-62.641511 93.667839-15.218737 35.910102-22.829535 74.152753-22.829535 114.742246 0 40.59378 7.610798 78.83786 22.829535 114.747963 15.218737 35.904385 36.100194 67.125093 62.641511 93.66641 26.53703 26.5356 57.762025 47.417057 93.66641 62.641511C364.912385 724.170627 403.162183 731.779995 443.751675 731.779995L443.751675 731.779995zM443.751675 731.779995"
|
||||
p-id="3985"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@ -81,7 +81,8 @@ const map = {
|
||||
badLight: require('./icons/light/bad.svg').default,
|
||||
markLight: require('./icons/light/mark.svg').default,
|
||||
retryLight: require('./icons/light/retry.svg').default,
|
||||
rightArrowLight: require('./icons/light/rightArrow.svg').default
|
||||
rightArrowLight: require('./icons/light/rightArrow.svg').default,
|
||||
searchLight: require('./icons/light/search.svg').default
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof map;
|
||||
|
||||
28
client/src/components/MyInput/index.tsx
Normal file
28
client/src/components/MyInput/index.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { Flex, Input, InputProps } from '@chakra-ui/react';
|
||||
|
||||
interface Props extends InputProps {
|
||||
leftIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const MyInput = ({ leftIcon, ...props }: Props) => {
|
||||
return (
|
||||
<Flex position={'relative'} alignItems={'center'}>
|
||||
<Input w={'100%'} pl={leftIcon ? '30px' : 3} {...props} />
|
||||
{leftIcon && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
position={'absolute'}
|
||||
left={3}
|
||||
w={'20px'}
|
||||
zIndex={10}
|
||||
transform={'translateY(1.5px)'}
|
||||
>
|
||||
{leftIcon}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyInput;
|
||||
@ -10,7 +10,8 @@ export const fileImgs = [
|
||||
{ suffix: 'csv', src: '/imgs/files/csv.svg' },
|
||||
{ suffix: '(doc|docs)', src: '/imgs/files/doc.svg' },
|
||||
{ suffix: 'txt', src: '/imgs/files/txt.svg' },
|
||||
{ suffix: 'md', src: '/imgs/files/markdown.svg' }
|
||||
{ suffix: 'md', src: '/imgs/files/markdown.svg' },
|
||||
{ suffix: '.', src: '/imgs/files/file.svg' }
|
||||
];
|
||||
|
||||
export enum TrackEventName {
|
||||
|
||||
@ -19,6 +19,10 @@ export enum KbTypeEnum {
|
||||
folder = 'folder',
|
||||
dataset = 'dataset'
|
||||
}
|
||||
export enum FileStatusEnum {
|
||||
embedding = 'embedding',
|
||||
ready = 'ready'
|
||||
}
|
||||
|
||||
export const KbTypeMap = {
|
||||
[KbTypeEnum.folder]: {
|
||||
@ -30,3 +34,4 @@ export const KbTypeMap = {
|
||||
};
|
||||
|
||||
export const FolderAvatarSrc = '/imgs/files/folder.svg';
|
||||
export const OtherFileId = 'other';
|
||||
|
||||
@ -5,6 +5,7 @@ import { authUser } from '@/service/utils/auth';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import type { KbDataItemType } from '@/types/plugin';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { OtherFileId } from '@/constants/kb';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
@ -12,12 +13,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
kbId,
|
||||
pageNum = 1,
|
||||
pageSize = 10,
|
||||
searchText = ''
|
||||
searchText = '',
|
||||
fileId = ''
|
||||
} = req.body as {
|
||||
kbId: string;
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
searchText: string;
|
||||
fileId: string;
|
||||
};
|
||||
if (!kbId) {
|
||||
throw new Error('缺少参数');
|
||||
@ -33,6 +36,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
['user_id', userId],
|
||||
'AND',
|
||||
['kb_id', kbId],
|
||||
...(fileId
|
||||
? fileId === OtherFileId
|
||||
? ["AND (file_id IS NULL OR file_id = '')"]
|
||||
: ['AND', ['file_id', fileId]]
|
||||
: []),
|
||||
...(searchText
|
||||
? [
|
||||
'AND',
|
||||
@ -50,7 +58,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
offset: pageSize * (pageNum - 1)
|
||||
}),
|
||||
PgClient.count(PgTrainingTableName, {
|
||||
fields: ['id'],
|
||||
fields: ['kb_id'],
|
||||
where
|
||||
})
|
||||
]);
|
||||
|
||||
55
client/src/pages/api/plugins/kb/file/delFileByFileId.ts
Normal file
55
client/src/pages/api/plugins/kb/file/delFileByFileId.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { GridFSStorage } from '@/service/lib/gridfs';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { Types } from 'mongoose';
|
||||
import { OtherFileId } from '@/constants/kb';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const { fileId, kbId } = req.query as { fileId: string; kbId: string };
|
||||
|
||||
if (!fileId || !kbId) {
|
||||
throw new Error('fileId and kbId is required');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
if (fileId === OtherFileId) {
|
||||
await PgClient.delete(PgTrainingTableName, {
|
||||
where: [
|
||||
['user_id', userId],
|
||||
'AND',
|
||||
['kb_id', kbId],
|
||||
"AND (file_id IS NULL OR file_id = '')"
|
||||
]
|
||||
});
|
||||
} else {
|
||||
const gridFs = new GridFSStorage('dataset', userId);
|
||||
const bucket = gridFs.GridFSBucket();
|
||||
|
||||
await gridFs.findAndAuthFile(fileId);
|
||||
|
||||
// delete all pg data
|
||||
await PgClient.delete(PgTrainingTableName, {
|
||||
where: [['user_id', userId], 'AND', ['kb_id', kbId], 'AND', ['file_id', fileId]]
|
||||
});
|
||||
|
||||
// delete file
|
||||
await bucket.delete(new Types.ObjectId(fileId));
|
||||
}
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
59
client/src/pages/api/plugins/kb/file/deleteEmptyFiles.ts
Normal file
59
client/src/pages/api/plugins/kb/file/deleteEmptyFiles.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { GridFSStorage } from '@/service/lib/gridfs';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { Types } from 'mongoose';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const { kbId } = req.query as { kbId: string };
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
const gridFs = new GridFSStorage('dataset', userId);
|
||||
const bucket = gridFs.GridFSBucket();
|
||||
|
||||
const files = await bucket
|
||||
// 1 hours expired
|
||||
.find({
|
||||
uploadDate: { $lte: new Date(Date.now() - 60 * 1000) },
|
||||
['metadata.kbId']: kbId,
|
||||
['metadata.userId']: userId
|
||||
})
|
||||
.sort({ _id: -1 })
|
||||
.toArray();
|
||||
|
||||
const data = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
return {
|
||||
id: file._id,
|
||||
chunkLength: await PgClient.count(PgTrainingTableName, {
|
||||
fields: ['kb_id'],
|
||||
where: [
|
||||
['user_id', userId],
|
||||
'AND',
|
||||
['kb_id', kbId],
|
||||
'AND',
|
||||
['file_id', String(file._id)]
|
||||
]
|
||||
})
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
data
|
||||
.filter((item) => item.chunkLength === 0)
|
||||
.map((file) => bucket.delete(new Types.ObjectId(file.id)))
|
||||
);
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res);
|
||||
}
|
||||
}
|
||||
43
client/src/pages/api/plugins/kb/file/getFileInfo.ts
Normal file
43
client/src/pages/api/plugins/kb/file/getFileInfo.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { GridFSStorage } from '@/service/lib/gridfs';
|
||||
import { OtherFileId } from '@/constants/kb';
|
||||
import type { FileInfo } from '@/types/plugin';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const { fileId } = req.query as { kbId: string; fileId: string };
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
if (fileId === OtherFileId) {
|
||||
return jsonRes<FileInfo>(res, {
|
||||
data: {
|
||||
id: OtherFileId,
|
||||
size: 0,
|
||||
filename: 'kb.Other Data',
|
||||
uploadDate: new Date(),
|
||||
encoding: '',
|
||||
contentType: ''
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const gridFs = new GridFSStorage('dataset', userId);
|
||||
|
||||
const file = await gridFs.findAndAuthFile(fileId);
|
||||
|
||||
jsonRes<FileInfo>(res, {
|
||||
data: file
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
82
client/src/pages/api/plugins/kb/file/list.ts
Normal file
82
client/src/pages/api/plugins/kb/file/list.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, TrainingData } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { GridFSStorage } from '@/service/lib/gridfs';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { KbFileItemType } from '@/types/plugin';
|
||||
import { FileStatusEnum, OtherFileId } from '@/constants/kb';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const { kbId } = req.query as { kbId: string };
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
const gridFs = new GridFSStorage('dataset', userId);
|
||||
const bucket = gridFs.GridFSBucket();
|
||||
|
||||
const files = await bucket
|
||||
.find({ ['metadata.kbId']: kbId })
|
||||
.sort({ _id: -1 })
|
||||
.toArray();
|
||||
|
||||
async function GetOtherData() {
|
||||
return {
|
||||
id: OtherFileId,
|
||||
size: 0,
|
||||
filename: 'kb.Other Data',
|
||||
uploadTime: new Date(),
|
||||
status: (await TrainingData.findOne({ userId, kbId, file_id: '' }))
|
||||
? FileStatusEnum.embedding
|
||||
: FileStatusEnum.ready,
|
||||
chunkLength: await PgClient.count(PgTrainingTableName, {
|
||||
fields: ['kb_id'],
|
||||
where: [
|
||||
['user_id', userId],
|
||||
'AND',
|
||||
['kb_id', kbId],
|
||||
"AND (file_id IS NULL OR file_id = '')"
|
||||
]
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const data = await Promise.all([
|
||||
GetOtherData(),
|
||||
...files.map(async (file) => {
|
||||
return {
|
||||
id: String(file._id),
|
||||
size: file.length,
|
||||
filename: file.filename,
|
||||
uploadTime: file.uploadDate,
|
||||
status: (await TrainingData.findOne({ userId, kbId, file_id: file._id }))
|
||||
? FileStatusEnum.embedding
|
||||
: FileStatusEnum.ready,
|
||||
chunkLength: await PgClient.count(PgTrainingTableName, {
|
||||
fields: ['kb_id'],
|
||||
where: [
|
||||
['user_id', userId],
|
||||
'AND',
|
||||
['kb_id', kbId],
|
||||
'AND',
|
||||
['file_id', String(file._id)]
|
||||
]
|
||||
})
|
||||
};
|
||||
})
|
||||
]);
|
||||
|
||||
jsonRes<KbFileItemType[]>(res, {
|
||||
data: data.flat().filter((item) => item.chunkLength > 0)
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,13 @@
|
||||
import React, { useCallback, useState, useRef } from 'react';
|
||||
import { Box, Card, IconButton, Flex, Button, Input, Grid } from '@chakra-ui/react';
|
||||
import React, { useCallback, useState, useRef, useMemo } from 'react';
|
||||
import { Box, Card, IconButton, Flex, Button, Grid, Image } from '@chakra-ui/react';
|
||||
import type { KbDataItemType } from '@/types/plugin';
|
||||
import { usePagination } from '@/hooks/usePagination';
|
||||
import {
|
||||
getKbDataList,
|
||||
getExportDataList,
|
||||
delOneKbDataByDataId,
|
||||
getTrainingData
|
||||
getTrainingData,
|
||||
getFileInfoById
|
||||
} from '@/api/plugins/kb';
|
||||
import { DeleteIcon, RepeatIcon } from '@chakra-ui/icons';
|
||||
import { fileDownload } from '@/utils/file';
|
||||
@ -18,12 +19,18 @@ import { debounce } from 'lodash';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import MyInput from '@/components/MyInput';
|
||||
import { fileImgs } from '@/constants/common';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
|
||||
const DataCard = ({ kbId }: { kbId: string }) => {
|
||||
const BoxRef = useRef<HTMLDivElement>(null);
|
||||
const lastSearch = useRef('');
|
||||
const router = useRouter();
|
||||
const { fileId = '' } = router.query as { fileId: string };
|
||||
const { t } = useTranslation();
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const { toast } = useToast();
|
||||
@ -45,7 +52,8 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
||||
pageSize: 24,
|
||||
params: {
|
||||
kbId,
|
||||
searchText
|
||||
searchText,
|
||||
fileId
|
||||
},
|
||||
onChange() {
|
||||
if (BoxRef.current) {
|
||||
@ -73,7 +81,7 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
||||
);
|
||||
|
||||
// get al data and export csv
|
||||
const { mutate: onclickExport, isLoading: isLoadingExport = false } = useMutation({
|
||||
const { mutate: onclickExport, isLoading: isLoadingExport = false } = useRequest({
|
||||
mutationFn: () => getExportDataList(kbId),
|
||||
onSuccess(res) {
|
||||
const text = Papa.unparse({
|
||||
@ -85,20 +93,12 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
||||
type: 'text/csv',
|
||||
filename: 'data.csv'
|
||||
});
|
||||
toast({
|
||||
title: '导出成功,下次导出需要半小时后',
|
||||
status: 'success'
|
||||
});
|
||||
},
|
||||
onError(err: any) {
|
||||
toast({
|
||||
title: getErrText(err, '导出异常'),
|
||||
status: 'error'
|
||||
});
|
||||
console.log(err);
|
||||
}
|
||||
successToast: '导出成功,下次导出需要半小时后',
|
||||
errorToast: '导出异常'
|
||||
});
|
||||
|
||||
// get first page data
|
||||
const getFirstData = useCallback(
|
||||
debounce(() => {
|
||||
getData(1);
|
||||
@ -113,57 +113,78 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
||||
enabled: qaListLen > 0 || vectorListLen > 0
|
||||
});
|
||||
|
||||
// get file info
|
||||
const { data: fileInfo } = useQuery(['getFileInfo', fileId], () => getFileInfoById(fileId));
|
||||
const fileIcon = useMemo(
|
||||
() =>
|
||||
fileImgs.find((item) => new RegExp(item.suffix, 'gi').test(fileInfo?.filename || ''))?.src,
|
||||
[fileInfo?.filename]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box ref={BoxRef} position={'relative'} px={5} py={[1, 5]} h={'100%'} overflow={'overlay'}>
|
||||
<Flex justifyContent={'space-between'}>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'} mr={2}>
|
||||
知识库数据: {total}组
|
||||
</Box>
|
||||
<Flex alignItems={'center'}>
|
||||
<Flex
|
||||
className="textEllipsis"
|
||||
flex={'1 0 0'}
|
||||
mr={[3, 5]}
|
||||
fontSize={['sm', 'md']}
|
||||
alignItems={'center'}
|
||||
>
|
||||
<Image src={fileIcon} w={'16px'} mr={2} alt={''} />
|
||||
{t(fileInfo?.filename || 'Filename')}
|
||||
</Flex>
|
||||
<Button
|
||||
mr={2}
|
||||
size={['sm', 'md']}
|
||||
variant={'base'}
|
||||
borderColor={'myBlue.600'}
|
||||
color={'myBlue.600'}
|
||||
isLoading={isLoadingExport || isLoading}
|
||||
title={'半小时仅能导出1次'}
|
||||
onClick={onclickExport}
|
||||
>
|
||||
{t('dataset.Export')}
|
||||
</Button>
|
||||
<Box>
|
||||
<MyTooltip label={'刷新'}>
|
||||
<IconButton
|
||||
icon={<RepeatIcon />}
|
||||
size={['sm', 'md']}
|
||||
aria-label={'refresh'}
|
||||
variant={'base'}
|
||||
isLoading={isLoading}
|
||||
mr={[2, 4]}
|
||||
size={'sm'}
|
||||
onClick={() => {
|
||||
getData(pageNum);
|
||||
getTrainingData({ kbId, init: true });
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Button
|
||||
mr={2}
|
||||
size={'sm'}
|
||||
variant={'base'}
|
||||
borderColor={'myBlue.600'}
|
||||
color={'myBlue.600'}
|
||||
isLoading={isLoadingExport || isLoading}
|
||||
title={'半小时仅能导出1次'}
|
||||
onClick={() => onclickExport()}
|
||||
>
|
||||
导出数据
|
||||
</Button>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex my={4}>
|
||||
{qaListLen > 0 || vectorListLen > 0 ? (
|
||||
<Box fontSize={'xs'}>
|
||||
{qaListLen > 0 ? `${qaListLen}条数据正在拆分,` : ''}
|
||||
{vectorListLen > 0 ? `${vectorListLen}条数据正在生成索引,` : ''}
|
||||
请耐心等待...
|
||||
<Flex my={3} alignItems={'center'}>
|
||||
<Box>
|
||||
<Box as={'span'} fontSize={['md', 'lg']}>
|
||||
{total}组
|
||||
</Box>
|
||||
) : (
|
||||
<Box fontSize={'xs'}>所有数据已就绪~</Box>
|
||||
)}
|
||||
<Box as={'span'}>
|
||||
{(qaListLen > 0 || vectorListLen > 0) && (
|
||||
<>
|
||||
({qaListLen > 0 ? `${qaListLen}条数据正在拆分,` : ''}
|
||||
{vectorListLen > 0 ? `${vectorListLen}条数据正在生成索引,` : ''}
|
||||
请耐心等待... )
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flex={1} mr={1} />
|
||||
<Input
|
||||
maxW={['60%', '300px']}
|
||||
size={'sm'}
|
||||
value={searchText}
|
||||
<MyInput
|
||||
leftIcon={
|
||||
<MyIcon name="searchLight" position={'absolute'} w={'14px'} color={'myGray.500'} />
|
||||
}
|
||||
w={['200px', '300px']}
|
||||
placeholder="根据匹配知识,预期答案和来源进行搜索"
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
setSearchText(e.target.value);
|
||||
getFirstData();
|
||||
|
||||
196
client/src/pages/kb/detail/components/FileCard.tsx
Normal file
196
client/src/pages/kb/detail/components/FileCard.tsx
Normal file
@ -0,0 +1,196 @@
|
||||
import React, { useCallback, useState, useRef, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
TableContainer,
|
||||
Table,
|
||||
Thead,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
Tbody,
|
||||
Image
|
||||
} from '@chakra-ui/react';
|
||||
import { getKbFiles, deleteKbFileById } from '@/api/plugins/kb';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { debounce } from 'lodash';
|
||||
import { formatFileSize } from '@/utils/tools';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import MyInput from '@/components/MyInput';
|
||||
import dayjs from 'dayjs';
|
||||
import { fileImgs } from '@/constants/common';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { FileStatusEnum } from '@/constants/kb';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const FileCard = ({ kbId }: { kbId: string }) => {
|
||||
const BoxRef = useRef<HTMLDivElement>(null);
|
||||
const lastSearch = useRef('');
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const { Loading } = useLoading();
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
content: t('kb.Confirm to delete the file')
|
||||
});
|
||||
|
||||
const {
|
||||
data: files = [],
|
||||
refetch,
|
||||
isInitialLoading
|
||||
} = useQuery(['getFiles', kbId], () => getKbFiles(kbId), {
|
||||
refetchInterval: 6000,
|
||||
refetchOnWindowFocus: true
|
||||
});
|
||||
const formatFiles = useMemo(
|
||||
() =>
|
||||
files.map((file) => ({
|
||||
...file,
|
||||
icon: fileImgs.find((item) => new RegExp(item.suffix, 'gi').test(file.filename))?.src
|
||||
})),
|
||||
[files]
|
||||
);
|
||||
const totalDataLength = useMemo(
|
||||
() => files.reduce((sum, item) => sum + item.chunkLength, 0),
|
||||
[files]
|
||||
);
|
||||
|
||||
const { mutate: onDeleteFile, isLoading } = useRequest({
|
||||
mutationFn: (fileId: string) =>
|
||||
deleteKbFileById({
|
||||
fileId,
|
||||
kbId
|
||||
}),
|
||||
onSuccess() {
|
||||
refetch();
|
||||
},
|
||||
successToast: t('common.Delete Success'),
|
||||
errorToast: t('common.Delete Failed')
|
||||
});
|
||||
|
||||
const statusMap = {
|
||||
[FileStatusEnum.embedding]: {
|
||||
color: 'myGray.500',
|
||||
text: t('file.Embedding')
|
||||
},
|
||||
[FileStatusEnum.ready]: {
|
||||
color: 'green.500',
|
||||
text: t('file.Ready')
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box ref={BoxRef} position={'relative'} py={[1, 5]} h={'100%'} overflow={'overlay'}>
|
||||
<Flex justifyContent={'space-between'} px={5}>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'} mr={2}>
|
||||
{t('kb.Files', { total: files.length })}
|
||||
</Box>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyInput
|
||||
leftIcon={
|
||||
<MyIcon name="searchLight" position={'absolute'} w={'14px'} color={'myGray.500'} />
|
||||
}
|
||||
w={['100%', '200px']}
|
||||
placeholder={t('common.Search') || ''}
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
setSearchText(e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (searchText === lastSearch.current) return;
|
||||
refetch();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (searchText === lastSearch.current) return;
|
||||
if (e.key === 'Enter') {
|
||||
refetch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<TableContainer mt={[0, 3]}>
|
||||
<Table variant={'simple'} fontSize={'sm'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('kb.Filename')}</Th>
|
||||
<Th>
|
||||
{t('kb.Chunk Length')}({totalDataLength})
|
||||
</Th>
|
||||
<Th>{t('kb.Upload Time')}</Th>
|
||||
<Th>{t('kb.File Size')}</Th>
|
||||
<Th>{t('common.Status')}</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{formatFiles.map((file) => (
|
||||
<Tr
|
||||
key={file.id}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
cursor={'pointer'}
|
||||
title={'点击查看数据详情'}
|
||||
onClick={() =>
|
||||
router.push({
|
||||
query: {
|
||||
kbId,
|
||||
fileId: file.id,
|
||||
currentTab: 'dataCard'
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Td display={'flex'} alignItems={'center'}>
|
||||
<Image src={file.icon} w={'16px'} mr={2} alt={''} />
|
||||
<Box maxW={['300px', '400px']} className="textEllipsis">
|
||||
{t(file.filename)}
|
||||
</Box>
|
||||
</Td>
|
||||
<Td fontSize={'md'} fontWeight={'bold'}>
|
||||
{file.chunkLength}
|
||||
</Td>
|
||||
<Td>{dayjs(file.uploadTime).format('YYYY/MM/DD HH:mm')}</Td>
|
||||
<Td>{formatFileSize(file.size)}</Td>
|
||||
<Td
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
_before={{
|
||||
content: '""',
|
||||
w: '10px',
|
||||
h: '10px',
|
||||
mr: 2,
|
||||
borderRadius: 'lg',
|
||||
bg: statusMap[file.status].color
|
||||
}}
|
||||
>
|
||||
{statusMap[file.status].text}
|
||||
</Td>
|
||||
<Td onClick={(e) => e.stopPropagation()}>
|
||||
<MyIcon
|
||||
name={'delete'}
|
||||
w={'14px'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() =>
|
||||
openConfirm(() => {
|
||||
onDeleteFile(file.id);
|
||||
})()
|
||||
}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<ConfirmModal />
|
||||
<Loading loading={isInitialLoading || isLoading} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(FileCard);
|
||||
@ -86,7 +86,7 @@ const ChunkImport = ({ kbId }: { kbId: string }) => {
|
||||
router.replace({
|
||||
query: {
|
||||
kbId,
|
||||
currentTab: 'data'
|
||||
currentTab: 'dataset'
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@ -73,7 +73,7 @@ const CsvImport = ({ kbId }: { kbId: string }) => {
|
||||
router.replace({
|
||||
query: {
|
||||
kbId,
|
||||
currentTab: 'data'
|
||||
currentTab: 'dataset'
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@ -74,7 +74,7 @@ const QAImport = ({ kbId }: { kbId: string }) => {
|
||||
router.replace({
|
||||
query: {
|
||||
kbId,
|
||||
currentTab: 'data'
|
||||
currentTab: 'dataset'
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@ -261,6 +261,7 @@ export function RawFileText({ fileId, filename = '', ...props }: RawFileTextProp
|
||||
<Box
|
||||
color={'myGray.600'}
|
||||
display={'inline-block'}
|
||||
whiteSpace={'nowrap'}
|
||||
{...(!!fileId
|
||||
? {
|
||||
cursor: 'pointer',
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Box, Flex, IconButton, useTheme } from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
@ -11,7 +11,6 @@ import { useGlobalStore } from '@/store/global';
|
||||
import { type ComponentRef } from './components/Info';
|
||||
import Tabs from '@/components/Tabs';
|
||||
import dynamic from 'next/dynamic';
|
||||
import DataCard from './components/DataCard';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import SideTabs from '@/components/SideTabs';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
@ -19,12 +18,16 @@ import Avatar from '@/components/Avatar';
|
||||
import Info from './components/Info';
|
||||
import { serviceSideProps } from '@/utils/i18n';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getTrainingQueueLen } from '@/api/plugins/kb';
|
||||
import { delEmptyFiles, getTrainingQueueLen } from '@/api/plugins/kb';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { feConfigs } from '@/store/static';
|
||||
import Script from 'next/script';
|
||||
import FileCard from './components/FileCard';
|
||||
|
||||
const DataCard = dynamic(() => import('./components/DataCard'), {
|
||||
ssr: false
|
||||
});
|
||||
const ImportData = dynamic(() => import('./components/Import'), {
|
||||
ssr: false
|
||||
});
|
||||
@ -33,7 +36,8 @@ const Test = dynamic(() => import('./components/Test'), {
|
||||
});
|
||||
|
||||
enum TabEnum {
|
||||
data = 'data',
|
||||
dataCard = 'dataCard',
|
||||
dataset = 'dataset',
|
||||
import = 'import',
|
||||
test = 'test',
|
||||
info = 'info'
|
||||
@ -49,7 +53,7 @@ const Detail = ({ kbId, currentTab }: { kbId: string; currentTab: `${TabEnum}` }
|
||||
const { kbDetail, getKbDetail } = useUserStore();
|
||||
|
||||
const tabList = useRef([
|
||||
{ label: '数据集', id: TabEnum.data, icon: 'overviewLight' },
|
||||
{ label: '数据集', id: TabEnum.dataset, icon: 'overviewLight' },
|
||||
{ label: '导入数据', id: TabEnum.import, icon: 'importLight' },
|
||||
{ label: '搜索测试', id: TabEnum.test, icon: 'kbTest' },
|
||||
{ label: '配置', id: TabEnum.info, icon: 'settingLight' }
|
||||
@ -86,9 +90,17 @@ const Detail = ({ kbId, currentTab }: { kbId: string; currentTab: `${TabEnum}` }
|
||||
});
|
||||
|
||||
const { data: trainingQueueLen = 0 } = useQuery(['getTrainingQueueLen'], getTrainingQueueLen, {
|
||||
refetchInterval: 5000
|
||||
refetchInterval: 10000
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
try {
|
||||
delEmptyFiles(kbId);
|
||||
} catch (error) {}
|
||||
};
|
||||
}, [kbId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script src="/js/pdf.js" strategy="lazyOnload"></Script>
|
||||
@ -141,7 +153,7 @@ const Detail = ({ kbId, currentTab }: { kbId: string; currentTab: `${TabEnum}` }
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
_hover={{ bg: 'myGray.100' }}
|
||||
onClick={() => router.back()}
|
||||
onClick={() => router.replace('/kb/list')}
|
||||
>
|
||||
<IconButton
|
||||
mr={3}
|
||||
@ -174,7 +186,8 @@ const Detail = ({ kbId, currentTab }: { kbId: string; currentTab: `${TabEnum}` }
|
||||
|
||||
{!!kbDetail._id && (
|
||||
<Box flex={'1 0 0'} h={'100%'} pb={[4, 0]}>
|
||||
{currentTab === TabEnum.data && <DataCard kbId={kbId} />}
|
||||
{currentTab === TabEnum.dataset && <FileCard kbId={kbId} />}
|
||||
{currentTab === TabEnum.dataCard && <DataCard kbId={kbId} />}
|
||||
{currentTab === TabEnum.import && <ImportData kbId={kbId} />}
|
||||
{currentTab === TabEnum.test && <Test kbId={kbId} />}
|
||||
{currentTab === TabEnum.info && <Info ref={InfoRef} kbId={kbId} form={form} />}
|
||||
@ -187,7 +200,7 @@ const Detail = ({ kbId, currentTab }: { kbId: string; currentTab: `${TabEnum}` }
|
||||
};
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
const currentTab = context?.query?.currentTab || TabEnum.data;
|
||||
const currentTab = context?.query?.currentTab || TabEnum.dataset;
|
||||
const kbId = context?.query?.kbId;
|
||||
|
||||
return {
|
||||
|
||||
@ -158,7 +158,7 @@ A2:
|
||||
console.log('openai error: 生成QA错误');
|
||||
console.log(err.response?.status, err.response?.statusText, err.response?.data);
|
||||
} else {
|
||||
console.log('生成QA错误:', err);
|
||||
addLog.error('生成 QA 错误', err);
|
||||
}
|
||||
|
||||
// message error or openai account error
|
||||
|
||||
@ -96,9 +96,7 @@ export async function generateVector(): Promise<any> {
|
||||
data: err.response?.data
|
||||
});
|
||||
} else {
|
||||
addLog.info('openai error: 生成向量错误', {
|
||||
err
|
||||
});
|
||||
addLog.error('openai error: 生成向量错误', err);
|
||||
}
|
||||
|
||||
// message error or openai account error
|
||||
|
||||
@ -2,19 +2,12 @@ import mongoose, { Types } from 'mongoose';
|
||||
import fs from 'fs';
|
||||
import fsp from 'fs/promises';
|
||||
import { ERROR_ENUM } from '../errorCode';
|
||||
import type { FileInfo } from '@/types/plugin';
|
||||
|
||||
enum BucketNameEnum {
|
||||
dataset = 'dataset'
|
||||
}
|
||||
|
||||
type FileInfo = {
|
||||
id: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
contentType: string;
|
||||
encoding: string;
|
||||
};
|
||||
|
||||
export class GridFSStorage {
|
||||
readonly type = 'gridfs';
|
||||
readonly bucket: `${BucketNameEnum}`;
|
||||
@ -88,6 +81,7 @@ export class GridFSStorage {
|
||||
filename: file.filename,
|
||||
contentType: file.metadata?.contentType,
|
||||
encoding: file.metadata?.encoding,
|
||||
uploadDate: file.uploadDate,
|
||||
size: file.length
|
||||
};
|
||||
}
|
||||
|
||||
@ -155,7 +155,7 @@ export const authUser = async ({
|
||||
})();
|
||||
|
||||
return {
|
||||
userId: uid,
|
||||
userId: String(uid),
|
||||
appId,
|
||||
authType,
|
||||
user
|
||||
|
||||
19
client/src/types/plugin.d.ts
vendored
19
client/src/types/plugin.d.ts
vendored
@ -1,3 +1,4 @@
|
||||
import { FileStatusEnum } from '@/constants/kb';
|
||||
import { VectorModelItemType } from './model';
|
||||
import type { kbSchema } from './mongoSchema';
|
||||
|
||||
@ -22,6 +23,15 @@ export interface KbItemType {
|
||||
tags: string;
|
||||
}
|
||||
|
||||
export type KbFileItemType = {
|
||||
id: string;
|
||||
size: number;
|
||||
filename: string;
|
||||
uploadTime: Date;
|
||||
chunkLength: number;
|
||||
status: `${FileStatusEnum}`;
|
||||
};
|
||||
|
||||
export type DatasetItemType = {
|
||||
q: string; // 提问词
|
||||
a: string; // 原文
|
||||
@ -44,3 +54,12 @@ export type FetchResultItem = {
|
||||
url: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type FileInfo = {
|
||||
id: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
contentType: string;
|
||||
encoding: string;
|
||||
uploadDate: Date;
|
||||
};
|
||||
|
||||
@ -59,6 +59,9 @@ export const Obj2Query = (obj: Record<string, string | number>) => {
|
||||
return queryParams.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* parse string to query object
|
||||
*/
|
||||
export const parseQueryString = (str: string) => {
|
||||
const queryObject: Record<string, any> = {};
|
||||
|
||||
@ -125,6 +128,16 @@ export const formatTimeToChatTime = (time: Date) => {
|
||||
return target.format('YYYY/M/D');
|
||||
};
|
||||
|
||||
export const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
export const hasVoiceApi = typeof window !== 'undefined' && 'speechSynthesis' in window;
|
||||
/**
|
||||
* voice broadcast
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user