* fix http plugin edge (#95) * fix http plugin edge * use getHandleId * perf: i18n file * feat: histories list * perf: request lock * fix: ts * move box components * fix: edit form refresh --------- Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
438 lines
14 KiB
TypeScript
438 lines
14 KiB
TypeScript
import React, { useMemo, useState } from 'react';
|
|
import { Box, Flex, Button, Textarea, useTheme, Grid, HStack } from '@chakra-ui/react';
|
|
import { UseFormRegister, useFieldArray, useForm } from 'react-hook-form';
|
|
import {
|
|
postInsertData2Dataset,
|
|
putDatasetDataById,
|
|
delOneDatasetDataById,
|
|
getDatasetCollectionById,
|
|
getDatasetDataItemById
|
|
} from '@/web/core/dataset/api';
|
|
import { useToast } from '@fastgpt/web/hooks/useToast';
|
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
|
import MyModal from '@fastgpt/web/components/common/MyModal';
|
|
import MyTooltip from '@/components/MyTooltip';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useTranslation } from 'next-i18next';
|
|
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
|
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
|
import { getDefaultIndex } from '@fastgpt/global/core/dataset/utils';
|
|
import { DatasetDataIndexItemType } from '@fastgpt/global/core/dataset/type';
|
|
import SideTabs from '@/components/SideTabs';
|
|
import DeleteIcon from '@fastgpt/web/components/common/Icon/delete';
|
|
import { defaultCollectionDetail } from '@/constants/dataset';
|
|
import { getDocPath } from '@/web/common/system/doc';
|
|
import RawSourceBox from '@/components/core/dataset/RawSourceBox';
|
|
import MyBox from '@fastgpt/web/components/common/MyBox';
|
|
import { getErrText } from '@fastgpt/global/common/error/utils';
|
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
|
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
|
|
|
export type InputDataType = {
|
|
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,
|
|
dataId,
|
|
defaultValue,
|
|
onClose,
|
|
onSuccess,
|
|
onDelete
|
|
}: {
|
|
collectionId: string;
|
|
dataId?: string;
|
|
defaultValue?: { q: string; a?: string };
|
|
onClose: () => void;
|
|
onSuccess: (data: InputDataType & { dataId: string }) => void;
|
|
onDelete?: () => void;
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const theme = useTheme();
|
|
const { toast } = useToast();
|
|
const [currentTab, setCurrentTab] = useState(TabEnum.content);
|
|
const { vectorModelList } = useSystemStore();
|
|
|
|
const { register, handleSubmit, reset, control } = useForm<InputDataType>();
|
|
const {
|
|
fields: indexes,
|
|
append: appendIndexes,
|
|
remove: removeIndexes
|
|
} = useFieldArray({
|
|
control,
|
|
name: 'indexes'
|
|
});
|
|
|
|
const tabList = [
|
|
{ label: t('dataset.data.edit.Content'), id: TabEnum.content, icon: 'common/overviewLight' },
|
|
{
|
|
label: t('dataset.data.edit.Index', { amount: indexes.length }),
|
|
id: TabEnum.index,
|
|
icon: 'kbTest'
|
|
},
|
|
...(dataId
|
|
? [{ label: t('dataset.data.edit.Delete'), id: TabEnum.delete, icon: 'delete' }]
|
|
: []),
|
|
{ label: t('dataset.data.edit.Course'), id: TabEnum.doc, icon: 'common/courseLight' }
|
|
];
|
|
|
|
const { ConfirmModal, openConfirm } = useConfirm({
|
|
content: t('dataset.data.Delete Tip'),
|
|
type: 'delete'
|
|
});
|
|
|
|
const { data: collection = defaultCollectionDetail } = useQuery(
|
|
['loadCollectionId', collectionId],
|
|
() => {
|
|
return getDatasetCollectionById(collectionId);
|
|
}
|
|
);
|
|
const { isFetching: isFetchingData } = useQuery(
|
|
['getDatasetDataItemById', dataId],
|
|
() => {
|
|
if (dataId) return getDatasetDataItemById(dataId);
|
|
return null;
|
|
},
|
|
{
|
|
onSuccess(res) {
|
|
if (res) {
|
|
reset({
|
|
q: res.q,
|
|
a: res.a,
|
|
indexes: res.indexes
|
|
});
|
|
} else if (defaultValue) {
|
|
reset({
|
|
q: defaultValue.q,
|
|
a: defaultValue.a
|
|
});
|
|
}
|
|
},
|
|
onError(err) {
|
|
toast({
|
|
status: 'error',
|
|
title: t(getErrText(err))
|
|
});
|
|
onClose();
|
|
}
|
|
}
|
|
);
|
|
|
|
const maxToken = useMemo(() => {
|
|
const vectorModel =
|
|
vectorModelList.find((item) => item.model === collection.datasetId.vectorModel) ||
|
|
vectorModelList[0];
|
|
|
|
return vectorModel?.maxToken || 3000;
|
|
}, [collection.datasetId.vectorModel, vectorModelList]);
|
|
|
|
// import new data
|
|
const { mutate: sureImportData, isLoading: isImporting } = useRequest({
|
|
mutationFn: async (e: InputDataType) => {
|
|
if (!e.q) {
|
|
setCurrentTab(TabEnum.content);
|
|
return Promise.reject(t('dataset.data.input is empty'));
|
|
}
|
|
|
|
const totalLength = e.q.length + (e.a?.length || 0);
|
|
if (totalLength >= maxToken * 1.4) {
|
|
return Promise.reject(t('core.dataset.data.Too Long'));
|
|
}
|
|
|
|
const data = { ...e };
|
|
|
|
const dataId = await postInsertData2Dataset({
|
|
collectionId: collection._id,
|
|
q: e.q,
|
|
a: e.a,
|
|
// remove dataId
|
|
indexes:
|
|
e.indexes?.map((index) => ({
|
|
...index,
|
|
dataId: undefined
|
|
})) || []
|
|
});
|
|
|
|
return {
|
|
...data,
|
|
dataId
|
|
};
|
|
},
|
|
successToast: t('dataset.data.Input Success Tip'),
|
|
onSuccess(e) {
|
|
reset({
|
|
...e,
|
|
q: '',
|
|
a: '',
|
|
indexes: []
|
|
});
|
|
|
|
onSuccess(e);
|
|
},
|
|
errorToast: t('common.error.unKnow')
|
|
});
|
|
// update
|
|
const { mutate: onUpdateData, isLoading: isUpdating } = useRequest({
|
|
mutationFn: async (e: InputDataType) => {
|
|
if (!dataId) return e;
|
|
|
|
// not exactly same
|
|
await putDatasetDataById({
|
|
id: dataId,
|
|
...e,
|
|
indexes:
|
|
e.indexes?.map((index) =>
|
|
index.defaultIndex ? getDefaultIndex({ q: e.q, a: e.a, dataId: index.dataId }) : index
|
|
) || []
|
|
});
|
|
|
|
return {
|
|
dataId,
|
|
...e
|
|
};
|
|
},
|
|
successToast: t('dataset.data.Update Success Tip'),
|
|
errorToast: t('common.error.unKnow'),
|
|
onSuccess(data) {
|
|
onSuccess(data);
|
|
onClose();
|
|
}
|
|
});
|
|
// delete
|
|
const { mutate: onDeleteData, isLoading: isDeleting } = useRequest({
|
|
mutationFn: () => {
|
|
if (!onDelete || !dataId) return Promise.resolve(null);
|
|
return delOneDatasetDataById(dataId);
|
|
},
|
|
onSuccess() {
|
|
if (!onDelete) return;
|
|
onDelete();
|
|
onClose();
|
|
},
|
|
successToast: t('common.Delete Success'),
|
|
errorToast: t('common.error.unKnow')
|
|
});
|
|
|
|
const isLoading = useMemo(
|
|
() => isImporting || isUpdating || isFetchingData || isDeleting,
|
|
[isImporting, isUpdating, isFetchingData, isDeleting]
|
|
);
|
|
|
|
return (
|
|
<MyModal isOpen={true} isCentered w={'90vw'} maxW={'1440px'} h={'90vh'}>
|
|
<MyBox isLoading={isLoading} display={'flex'} h={'100%'}>
|
|
<Box p={5} bg={'myGray.50'} borderLeftRadius={'md'} borderRight={theme.borders.base}>
|
|
<RawSourceBox
|
|
w={'210px'}
|
|
className="textEllipsis3"
|
|
whiteSpace={'pre-wrap'}
|
|
sourceName={collection.sourceName}
|
|
sourceId={collection.sourceId}
|
|
mb={6}
|
|
fontSize={'sm'}
|
|
/>
|
|
<SideTabs
|
|
list={tabList}
|
|
activeId={currentTab}
|
|
onChange={async (e: any) => {
|
|
if (e === TabEnum.delete) {
|
|
return openConfirm(onDeleteData)();
|
|
}
|
|
if (e === TabEnum.doc) {
|
|
return window.open(getDocPath('/docs/course/dataset_engine'), '_blank');
|
|
}
|
|
setCurrentTab(e);
|
|
}}
|
|
/>
|
|
</Box>
|
|
<Flex flexDirection={'column'} pb={8} flex={1} h={'100%'}>
|
|
<Box fontSize={'lg'} px={5} py={3} fontWeight={'medium'}>
|
|
{currentTab === TabEnum.content && (
|
|
<>{dataId ? t('dataset.data.Update Data') : t('dataset.data.Input Data')}</>
|
|
)}
|
|
{currentTab === TabEnum.index && <> {t('dataset.data.Index Edit')}</>}
|
|
</Box>
|
|
<Box flex={1} px={9} overflow={'auto'}>
|
|
{currentTab === TabEnum.content && <InputTab maxToken={maxToken} register={register} />}
|
|
{currentTab === TabEnum.index && (
|
|
<Grid mt={3} gridTemplateColumns={['1fr', '1fr 1fr']} gridGap={4}>
|
|
{indexes?.map((index, i) => (
|
|
<Box
|
|
key={index.dataId || i}
|
|
p={4}
|
|
borderRadius={'md'}
|
|
border={
|
|
index.defaultIndex
|
|
? '1.5px solid var(--light-fastgpt-primary-opacity-01, rgba(51, 112, 255, 0.10))'
|
|
: '1.5px solid var(--Gray-Modern-200, #E8EBF0)'
|
|
}
|
|
bg={index.defaultIndex ? 'primary.50' : 'myGray.25'}
|
|
_hover={{
|
|
'& .delete': {
|
|
display: index.defaultIndex ? 'none' : 'block'
|
|
}
|
|
}}
|
|
>
|
|
<Flex mb={2}>
|
|
<Box
|
|
flex={1}
|
|
fontWeight={'medium'}
|
|
color={index.defaultIndex ? 'primary.700' : 'myGray.900'}
|
|
>
|
|
{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 fontSize={'sm'} fontWeight={'medium'} color={'myGray.600'}>
|
|
{t('core.dataset.data.Default Index Tip')}
|
|
</Box>
|
|
) : (
|
|
<Textarea
|
|
maxLength={maxToken}
|
|
fontSize={'sm'}
|
|
rows={10}
|
|
borderColor={'transparent'}
|
|
px={0}
|
|
pt={0}
|
|
_focus={{
|
|
px: 3,
|
|
py: 2,
|
|
borderColor: 'primary.500',
|
|
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)',
|
|
bg: 'white'
|
|
}}
|
|
placeholder={t('dataset.data.Index Placeholder')}
|
|
{...register(`indexes.${i}.text`, {
|
|
required: true
|
|
})}
|
|
/>
|
|
)}
|
|
</Box>
|
|
))}
|
|
<Flex
|
|
alignItems={'center'}
|
|
justifyContent={'center'}
|
|
borderRadius={'md'}
|
|
color={'myGray.600'}
|
|
fontWeight={'medium'}
|
|
border={'1.5px solid var(--Gray-Modern-200, #E8EBF0)'}
|
|
bg={'myGray.25'}
|
|
cursor={'pointer'}
|
|
_hover={{
|
|
bg: 'primary.50',
|
|
color: 'primary.600',
|
|
border:
|
|
'1.5px solid var(--light-fastgpt-primary-opacity-01, rgba(51, 112, 255, 0.10))'
|
|
}}
|
|
minH={'100px'}
|
|
onClick={() =>
|
|
appendIndexes({
|
|
defaultIndex: false,
|
|
text: '',
|
|
dataId: `${Date.now()}`
|
|
})
|
|
}
|
|
>
|
|
<MyIcon name={'common/addLight'} w={'18px'} mr={1.5} />
|
|
<Box>{t('dataset.data.Add Index')}</Box>
|
|
</Flex>
|
|
</Grid>
|
|
)}
|
|
</Box>
|
|
{/* footer */}
|
|
<Flex justifyContent={'flex-end'} px={9} mt={6}>
|
|
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
|
|
{t('common.Close')}
|
|
</Button>
|
|
<MyTooltip label={collection.canWrite ? '' : t('dataset.data.Can not edit')}>
|
|
<Button
|
|
isDisabled={!collection.canWrite}
|
|
// @ts-ignore
|
|
onClick={handleSubmit(dataId ? onUpdateData : sureImportData)}
|
|
>
|
|
{dataId ? t('common.Confirm Update') : t('common.Confirm Import')}
|
|
</Button>
|
|
</MyTooltip>
|
|
</Flex>
|
|
</Flex>
|
|
</MyBox>
|
|
<ConfirmModal />
|
|
</MyModal>
|
|
);
|
|
};
|
|
|
|
export default React.memo(InputDataModal);
|
|
|
|
const InputTab = ({
|
|
maxToken,
|
|
register
|
|
}: {
|
|
maxToken: number;
|
|
register: UseFormRegister<InputDataType>;
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<HStack h={'100%'} spacing={6}>
|
|
<Flex flexDirection={'column'} w={'50%'} h={'100%'}>
|
|
<Flex pt={3} pb={2} fontWeight={'medium'} fontSize={'md'} alignItems={'center'}>
|
|
<Box color={'red.600'}>*</Box>
|
|
<Box color={'myGray.900'}>{t('core.dataset.data.Main Content')}</Box>
|
|
<QuestionTip label={t('core.dataset.data.Data Content Tip')} ml={1} />
|
|
</Flex>
|
|
<Box flex={'1 0 0'}>
|
|
<Textarea
|
|
placeholder={t('core.dataset.data.Data Content Placeholder', { maxToken })}
|
|
maxLength={maxToken}
|
|
tabIndex={1}
|
|
bg={'myGray.50'}
|
|
h={'full'}
|
|
{...register(`q`, {
|
|
required: true
|
|
})}
|
|
/>
|
|
</Box>
|
|
</Flex>
|
|
<Flex flexDirection={'column'} w={'50%'} h={'100%'}>
|
|
<Flex pt={3} pb={2} fontWeight={'medium'} fontSize={'md'} alignItems={'center'}>
|
|
<Box color={'myGray.900'}>{t('core.dataset.data.Auxiliary Data')}</Box>
|
|
<QuestionTip label={t('core.dataset.data.Auxiliary Data Tip')} ml={1} />
|
|
</Flex>
|
|
<Box flex={'1 0 0'}>
|
|
<Textarea
|
|
placeholder={t('core.dataset.data.Auxiliary Data Placeholder', {
|
|
maxToken: maxToken * 1.5
|
|
})}
|
|
h={'100%'}
|
|
tabIndex={1}
|
|
bg={'myGray.50'}
|
|
maxLength={maxToken * 1.5}
|
|
{...register('a')}
|
|
/>
|
|
</Box>
|
|
</Flex>
|
|
</HStack>
|
|
);
|
|
};
|