This commit is contained in:
Archer
2023-11-15 11:36:25 +08:00
committed by GitHub
parent 592e1a93a2
commit bfd8be5df0
181 changed files with 2499 additions and 1552 deletions

View File

@@ -1,10 +1,11 @@
import React, { useMemo } from 'react';
import { Box, Flex, Button, Textarea, IconButton, BoxProps, Image, Link } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import React, { useMemo, useState } from 'react';
import { Box, Flex, Button, Textarea, BoxProps, Image, useTheme, Grid } from '@chakra-ui/react';
import { useFieldArray, useForm } from 'react-hook-form';
import {
postData2Dataset,
postInsertData2Dataset,
putDatasetDataById,
delOneDatasetDataById
delOneDatasetDataById,
getDatasetCollectionById
} from '@/web/core/dataset/api';
import { useToast } from '@/web/common/hooks/useToast';
import { getErrText } from '@fastgpt/global/common/error/utils';
@@ -13,62 +14,106 @@ import MyModal from '@/components/MyModal';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { useTranslation } from 'next-i18next';
import { getFileAndOpen } from '@/web/core/dataset/utils';
import { strIsLink } from '@fastgpt/global/common/string/tools';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import type { SetOneDatasetDataProps } from '@/global/core/api/datasetReq';
import { useRequest } from '@/web/common/hooks/useRequest';
import { countPromptTokens } from '@/global/common/tiktoken';
import { countPromptTokens } from '@fastgpt/global/common/string/tiktoken';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import { feConfigs } from '@/web/common/system/staticData';
import { getDefaultIndex, getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import { feConfigs, vectorModelList } from '@/web/common/system/staticData';
import { DatasetDataIndexTypeEnum } from '@fastgpt/global/core/dataset/constant';
import { DatasetDataIndexItemType } from '@fastgpt/global/core/dataset/type';
import SideTabs from '@/components/SideTabs';
import { useLoading } from '@/web/common/hooks/useLoading';
import DeleteIcon from '@/components/Icon/delete';
import { defaultCollectionDetail } from '@/constants/dataset';
export type RawSourceType = {
export type RawSourceTextProps = BoxProps & {
sourceName?: string;
sourceId?: string;
addr?: boolean;
canView?: boolean;
};
export type RawSourceTextProps = BoxProps & RawSourceType;
export type InputDataType = SetOneDatasetDataProps & RawSourceType;
export type InputDataType = {
id?: string;
q: string;
a?: string;
indexes: (Omit<DatasetDataIndexItemType, 'dataId'> & {
dataId?: string; // pg data id
})[];
};
enum TabEnum {
content = 'content',
index = 'index',
delete = 'delete',
doc = 'doc'
}
const InputDataModal = ({
collectionId,
defaultValue,
onClose,
onSuccess,
onDelete,
datasetId,
defaultValues = {
collectionId: '',
sourceId: '',
sourceName: ''
},
canWrite
onDelete
}: {
collectionId: string;
defaultValue: InputDataType;
onClose: () => void;
onSuccess: (data: SetOneDatasetDataProps) => void;
onSuccess: (data: InputDataType) => void;
onDelete?: () => void;
datasetId: string;
defaultValues: InputDataType;
canWrite: boolean;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const { toast } = useToast();
const { datasetDetail, loadDatasetDetail } = useDatasetStore();
const { Loading } = useLoading();
const [currentTab, setCurrentTab] = useState(TabEnum.content);
const { register, handleSubmit, reset } = useForm<InputDataType>({
defaultValues
const { register, handleSubmit, reset, control } = useForm<InputDataType>({
defaultValues: defaultValue
});
const {
fields: indexes,
append: appendIndexes,
remove: removeIndexes
} = useFieldArray({
control,
name: 'indexes'
});
const tabList = [
{ label: t('dataset.data.edit.Content'), id: TabEnum.content, icon: 'overviewLight' },
{
label: t('dataset.data.edit.Index', { amount: indexes.length }),
id: TabEnum.index,
icon: 'kbTest'
},
...(defaultValue.id
? [{ label: t('dataset.data.edit.Delete'), id: TabEnum.delete, icon: 'delete' }]
: []),
{ label: t('dataset.data.edit.Course'), id: TabEnum.doc, icon: 'courseLight' }
];
const { ConfirmModal, openConfirm } = useConfirm({
content: t('dataset.data.Delete Tip')
});
const maxToken = datasetDetail.vectorModel?.maxToken || 2000;
const { data: collection = defaultCollectionDetail } = useQuery(
['loadCollectionId', collectionId],
() => {
return getDatasetCollectionById(collectionId);
}
);
/**
* 确认导入新数据
*/
const maxToken = useMemo(() => {
const vectorModel =
vectorModelList.find((item) => item.model === collection.datasetId.vectorModel) ||
vectorModelList[0];
return vectorModel?.maxToken || 3000;
}, [collection.datasetId.vectorModel]);
// import new data
const { mutate: sureImportData, isLoading: isImporting } = useRequest({
mutationFn: async (e: InputDataType) => {
if (!e.q) {
@@ -85,10 +130,16 @@ const InputDataModal = ({
}
const data = { ...e };
delete data.sourceName;
delete data.sourceId;
data.id = await postData2Dataset(data);
data.id = await postInsertData2Dataset({
collectionId: collection._id,
q: e.q,
a: e.a,
// remove dataId
indexes: e.indexes.map((index) =>
index.defaultIndex ? getDefaultIndex({ q: e.q, a: e.a }) : index
)
});
return data;
},
@@ -97,26 +148,26 @@ const InputDataModal = ({
reset({
...e,
q: '',
a: ''
a: '',
indexes: [getDefaultIndex({ q: e.q, a: e.a, dataId: `${Date.now()}` })]
});
onSuccess(e);
},
errorToast: t('common.error.unKnow')
});
// update
const { mutate: onUpdateData, isLoading: isUpdating } = useRequest({
mutationFn: async (e: SetOneDatasetDataProps) => {
mutationFn: async (e: InputDataType) => {
if (!e.id) return e;
// not exactly same
if (e.q !== defaultValues.q || e.a !== defaultValues.a) {
await putDatasetDataById({
...e,
q: e.q === defaultValues.q ? '' : e.q
});
return e;
}
await putDatasetDataById({
id: e.id,
q: e.q,
a: e.a,
indexes: e.indexes
});
return e;
},
@@ -127,153 +178,207 @@ const InputDataModal = ({
onClose();
}
});
// delete
const { mutate: onDeleteData, isLoading: isDeleting } = useRequest({
mutationFn: () => {
if (!onDelete || !defaultValue.id) return Promise.resolve(null);
return delOneDatasetDataById(defaultValue.id);
},
onSuccess() {
if (!onDelete) return;
onDelete();
onClose();
},
successToast: t('common.Delete Success'),
errorToast: t('common.error.unKnow')
});
const loading = useMemo(() => isImporting || isUpdating, [isImporting, isUpdating]);
useQuery(['loadDatasetDetail'], () => {
if (datasetDetail._id === datasetId) return null;
return loadDatasetDetail(datasetId);
});
return (
<MyModal
isOpen={true}
isCentered
title={
<Flex alignItems={'flex-end'}>
<Box>
{defaultValues.id ? t('dataset.data.Update Data') : t('dataset.data.Input Data')}
</Box>
<Link
href={`${feConfigs.docUrl}/docs/use-cases/datasetengine`}
target={'_blank'}
fontSize={'sm'}
color={'myGray.600'}
textDecor={'underline'}
ml={2}
>
</Link>
</Flex>
}
w={'90vw'}
maxW={'90vw'}
h={'90vh'}
>
<Flex flexDirection={'column'} h={'100%'}>
<Box
display={'flex'}
flexDirection={['column', 'row']}
flex={'1 0 0'}
h={['100%', 0]}
overflow={'overlay'}
px={6}
pb={2}
>
<Box flex={1} mr={[0, 4]} mb={[4, 0]} h={['50%', '100%']}>
<Flex>
<Box h={'25px'}>{'被搜索的内容'}</Box>
<MyTooltip
label={
'被向量化的部分,该部分的质量决定了对话时,能否高效的查找到合适的知识点。\n该内容通常是问题或是一段陈述描述介绍'
}
>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Textarea
placeholder={`被向量化的部分,该部分的质量决定了对话时,能否高效的查找到合适的知识点。\n该内容通常是问题或是一段陈述描述介绍最多 ${maxToken} 字。`}
maxLength={maxToken}
resize={'none'}
h={'calc(100% - 30px)'}
{...register(`q`, {
required: true
})}
/>
</Box>
<Box flex={1} h={['50%', '100%']}>
<Flex>
<Box h={'25px'}>{'补充内容(可选)'}</Box>
<MyTooltip
label={
'该部分内容不影响搜索质量。当“被搜索的内容”被搜索到后,“补充内容”可以选择性被填入提示词,从而实现更加丰富的提示词组合。'
}
>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Textarea
placeholder={
'该部分内容不影响搜索质量。当“被搜索的内容”被搜索到后,“补充内容”可以选择性被填入提示词,从而实现更加丰富的提示词组合。可以是问题的答案、代码、图片、表格等。'
}
resize={'none'}
h={'calc(100% - 30px)'}
{...register('a')}
/>
</Box>
</Box>
<Flex px={6} pt={['34px', 2]} pb={4} alignItems={'center'} position={'relative'}>
<MyModal isOpen={true} isCentered w={'90vw'} maxW={'90vw'} h={'90vh'}>
<Flex h={'100%'}>
<Box p={5} borderRight={theme.borders.base}>
<RawSourceText
sourceName={defaultValues.sourceName}
sourceId={defaultValues.sourceId}
position={'absolute'}
left={'50%'}
top={['16px', '50%']}
transform={'translate(-50%,-50%)'}
w={'200px'}
className=""
whiteSpace={'pre-wrap'}
sourceName={collection.sourceName}
sourceId={collection.sourceId}
mb={6}
fontSize={['14px', '16px']}
/>
<Box flex={1}>
{defaultValues.id && onDelete && canWrite && (
<IconButton
variant={'outline'}
icon={<MyIcon name={'delete'} w={'16px'} h={'16px'} />}
aria-label={''}
isLoading={loading}
size={'sm'}
_hover={{
color: 'red.600',
borderColor: 'red.600'
}}
onClick={openConfirm(async () => {
if (!onDelete || !defaultValues.id) return;
try {
await delOneDatasetDataById(defaultValues.id);
onDelete();
onClose();
toast({
status: 'success',
title: '记录已删除'
});
} catch (error) {
toast({
status: 'warning',
title: getErrText(error)
});
console.log(error);
<SideTabs
list={tabList}
activeId={currentTab}
onChange={async (e: any) => {
if (e === TabEnum.delete) {
return openConfirm(onDeleteData)();
}
if (e === TabEnum.doc) {
return window.open(`${feConfigs.docUrl}/docs/use-cases/datasetengine`, '_blank');
}
setCurrentTab(e);
}}
/>
</Box>
<Flex flexDirection={'column'} px={5} py={3} flex={1} h={'100%'}>
<Box fontSize={'lg'} fontWeight={'bold'} mb={4}>
{currentTab === TabEnum.content && (
<>{defaultValue.id ? t('dataset.data.Update Data') : t('dataset.data.Input Data')}</>
)}
{currentTab === TabEnum.index && <> {t('dataset.data.Index Edit')}</>}
</Box>
<Box flex={1} overflow={'auto'}>
{currentTab === TabEnum.content && (
<>
<Box>
<Flex alignItems={'center'}>
<Box>
<Box as="span" color={'red.600'}>
*
</Box>
{'相关数据内容'}
</Box>
<MyTooltip
label={'该输入框是必填项\n该内容通常是对于知识点的描述也可以是用户的问题。'}
>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Textarea
mt={1}
placeholder={`该输入框是必填项,该内容通常是对于知识点的描述,也可以是用户的问题,最多 ${maxToken} 字。`}
maxLength={maxToken}
rows={10}
bg={'myWhite.400'}
{...register(`q`, {
required: true
})}
/>
</Box>
<Box mt={5}>
<Flex>
<Box>{'辅助数据'}</Box>
<MyTooltip
label={
'该部分为可选填项\n该内容通常是为了与前面的数据内容配合构建结构化提示词用于特殊场景'
}
>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Textarea
mt={1}
placeholder={`该部分为可选填项, 通常是为了与前面的【数据内容】配合,构建结构化提示词,用于特殊场景,最多 ${
maxToken * 1.5
} 字。`}
bg={'myWhite.400'}
rows={10}
maxLength={maxToken * 1.5}
{...register('a')}
/>
</Box>
</>
)}
{currentTab === TabEnum.index && (
<Grid gridTemplateColumns={['1fr', '1fr 1fr']} gridGap={4}>
{indexes.map((index, i) => (
<Box
key={index.dataId || i}
p={3}
borderRadius={'md'}
border={theme.borders.base}
bg={i % 2 !== 0 ? 'myWhite.400' : ''}
_hover={{
'& .delete': {
display: index.defaultIndex && indexes.length === 1 ? 'none' : 'block'
}
}}
>
<Flex mb={1}>
<Box flex={1}>
{index.defaultIndex
? t('dataset.data.Default Index')
: t('dataset.data.Custom Index Number', { number: i })}
</Box>
<DeleteIcon
onClick={() => {
if (indexes.length <= 1) {
appendIndexes(getDefaultIndex({ dataId: `${Date.now()}` }));
}
removeIndexes(i);
}}
/>
</Flex>
{index.defaultIndex ? (
<Box>
使
</Box>
) : (
<Textarea
maxLength={maxToken}
rows={10}
borderColor={'transparent'}
px={0}
_focus={{
borderColor: 'myBlue.400',
px: 3
}}
placeholder={t('dataset.data.Index Placeholder')}
{...register(`indexes.${i}.text`, {
required: true
})}
/>
)}
</Box>
))}
<Flex
flexDirection={'column'}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
border={theme.borders.base}
cursor={'pointer'}
_hover={{
bg: 'myBlue.100'
}}
minH={'100px'}
onClick={() =>
appendIndexes({
defaultIndex: false,
type: DatasetDataIndexTypeEnum.chunk,
text: '',
dataId: `${Date.now()}`
})
}
})}
/>
>
<MyIcon name={'addCircle'} w={'16px'} />
<Box>{t('dataset.data.Add Index')}</Box>
</Flex>
</Grid>
)}
</Box>
<Box>
<Flex justifyContent={'flex-end'} mt={4}>
<Button variant={'base'} mr={3} isLoading={loading} onClick={onClose}>
{t('common.Close')}
</Button>
<MyTooltip label={canWrite ? '' : t('dataset.data.Can not edit')}>
<MyTooltip label={collection.canWrite ? '' : t('dataset.data.Can not edit')}>
<Button
isDisabled={!canWrite}
isDisabled={!collection.canWrite}
isLoading={loading}
// @ts-ignore
onClick={handleSubmit(defaultValues.id ? onUpdateData : sureImportData)}
onClick={handleSubmit(defaultValue.id ? onUpdateData : sureImportData)}
>
{defaultValues.id ? '确认变更' : '确认导入'}
{defaultValue.id ? '确认变更' : '确认导入'}
</Button>
</MyTooltip>
</Box>
</Flex>
</Flex>
</Flex>
<ConfirmModal />
<Loading fixed={false} loading={isDeleting} />
</MyModal>
);
};
@@ -283,14 +388,14 @@ export default InputDataModal;
export function RawSourceText({
sourceId,
sourceName = '',
addr = true,
canView = true,
...props
}: RawSourceTextProps) {
const { t } = useTranslation();
const { toast } = useToast();
const { setLoading } = useSystemStore();
const canPreview = useMemo(() => !!sourceId && addr, [addr, sourceId]);
const canPreview = useMemo(() => !!sourceId && canView, [canView, sourceId]);
const icon = useMemo(() => getSourceNameIcon({ sourceId, sourceName }), [sourceId, sourceName]);
@@ -302,16 +407,12 @@ export function RawSourceText({
<Box
color={'myGray.600'}
display={'inline-flex'}
alignItems={'center'}
whiteSpace={'nowrap'}
{...(canPreview
? {
cursor: 'pointer',
textDecoration: 'underline',
onClick: async () => {
if (strIsLink(sourceId)) {
return window.open(sourceId, '_blank');
}
setLoading(true);
try {
await getFileAndOpen(sourceId as string);
@@ -327,8 +428,8 @@ export function RawSourceText({
: {})}
{...props}
>
<Image src={icon} alt="" w={'14px'} mr={2} />
<Box maxW={['200px', '300px']} className={'textEllipsis'}>
<Image src={icon} alt="" w={['14px', '16px']} mr={2} />
<Box maxW={['200px', '300px']} className={props.className ?? 'textEllipsis'}>
{sourceName || t('common.UnKnow Source')}
</Box>
</Box>