* chore(ui): login page & workflow page (#3046) * login page & number input & multirow select & llm select * workflow * adjust nodes * New file upload (#3058) * feat: toolNode aiNode readFileNode adapt new version * update docker-compose * update tip * feat: adapt new file version * perf: file input * fix: ts * feat: add chat history time label (#3024) * feat:add chat and logs time * feat: add chat history time label * code perf * code perf --------- Co-authored-by: 勤劳上班的卑微小张 <jiazhan.zhang@ggimage.com> * add chatType (#3060) * pref: slow query of full text search (#3044) * Adapt findLast api;perf: markdown zh format. (#3066) * perf: context code * fix: adapt findLast api * perf: commercial plugin run error * perf: markdown zh format * perf: dockerfile proxy (#3067) * fix ui (#3065) * fix ui * fix * feat: support array reference multi-select (#3041) * feat: support array reference multi-select * fix build * fix * fix loop multi-select * adjust condition * fix get value * array and non-array conversion * fix plugin input * merge func * feat: iframe code block;perf: workflow selector type (#3076) * feat: iframe code block * perf: workflow selector type * node pluginoutput check (#3074) * feat: View will move when workflow check error;fix: ui refresh error when continuous file upload (#3077) * fix: plugin output check * fix: ui refresh error when continuous file upload * feat: View will move when workflow check error * add dispatch try catch (#3075) * perf: workflow context split (#3083) * perf: workflow context split * perf: context * 4.8.13 test (#3085) * perf: workflow node ui * chat iframe url * feat: support sub route config (#3071) * feat: support sub route config * dockerfile * fix upload * delete unused code * 4.8.13 test (#3087) * fix: image expired * fix: datacard navbar ui * perf: build action * fix: workflow file upload refresh (#3088) * fix: http tool response (#3097) * loop node dynamic height (#3092) * loop node dynamic height * fix * fix * feat: support push chat log (#3093) * feat: custom uid/metadata * to: custom info * fix: chat push latest * feat: add chat log envs * refactor: move timer to pushChatLog * fix: using precise log --------- Co-authored-by: Finley Ge <m13203533462@163.com> * 4.8.13 test (#3098) * perf: loop node refresh * rename context * comment * fix: ts * perf: push chat log * array reference check & node ui (#3100) * feat: loop start add index (#3101) * feat: loop start add index * update doc * 4.8.13 test (#3102) * fix: loop index;edge parent check * perf: reference invalid check * fix: ts * fix: plugin select files and ai response check (#3104) * fix: plugin select files and ai response check * perf: text editor selector;tool call tip;remove invalid image url; * perf: select file * perf: drop files * feat: source id prefix env (#3103) * 4.8.13 test (#3106) * perf: select file * perf: drop files * perf: env template * 4.8.13 test (#3107) * perf: select file * perf: drop files * fix: imple mode adapt files * perf: push chat log (#3109) * fix: share page load title error (#3111) * 4.8.13 perf (#3112) * fix: share page load title error * update file input doc * perf: auto add file urls * perf: auto ser loop node offset height * 4.8.13 test (#3117) * perf: plugin * updat eaction * feat: add more share config (#3120) * feat: add more share config * add i18n en * fix: missing subroute (#3121) * perf: outlink config (#3128) * update action * perf: outlink config * fix: ts (#3129) * 更新 docSite 文档内容 (#3131) * fix: null pointer (#3130) * fix: null pointer * perf: not input text * update doc url * perf: outlink default value (#3134) * update doc (#3136) * 4.8.13 test (#3137) * update doc * perf: completions chat api * Restore docSite content based on upstream/4.8.13-dev (#3138) * Restore docSite content based on upstream/4.8.13-dev * 4813.md缺少更正 * update doc (#3141) --------- Co-authored-by: heheer <heheer@sealos.io> Co-authored-by: papapatrick <109422393+Patrickill@users.noreply.github.com> Co-authored-by: 勤劳上班的卑微小张 <jiazhan.zhang@ggimage.com> Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com> Co-authored-by: a.e. <49438478+I-Info@users.noreply.github.com> Co-authored-by: Finley Ge <m13203533462@163.com> Co-authored-by: Jiangween <145003935+Jiangween@users.noreply.github.com>
559 lines
16 KiB
TypeScript
559 lines
16 KiB
TypeScript
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import { Box, Flex, Button, Textarea, useTheme, Grid, HStack } from '@chakra-ui/react';
|
|
import {
|
|
Control,
|
|
FieldArrayWithId,
|
|
UseFieldArrayAppend,
|
|
UseFieldArrayRemove,
|
|
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 '@fastgpt/web/components/common/MyTooltip';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useTranslation } from 'next-i18next';
|
|
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
|
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
|
import { getDefaultIndex, getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
|
|
import { DatasetDataIndexItemType } from '@fastgpt/global/core/dataset/type';
|
|
import DeleteIcon from '@fastgpt/web/components/common/Icon/delete';
|
|
import { defaultCollectionDetail } from '@/web/core/dataset/constants';
|
|
import { getDocPath } from '@/web/common/system/doc';
|
|
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';
|
|
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
|
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
|
|
import styles from './styles.module.scss';
|
|
|
|
export type InputDataType = {
|
|
q: string;
|
|
a: string;
|
|
indexes: (Omit<DatasetDataIndexItemType, 'dataId'> & {
|
|
dataId?: string; // pg data id
|
|
})[];
|
|
};
|
|
|
|
enum TabEnum {
|
|
content = 'content',
|
|
index = 'index'
|
|
}
|
|
|
|
const InputDataModal = ({
|
|
collectionId,
|
|
dataId,
|
|
defaultValue,
|
|
onClose,
|
|
onSuccess
|
|
}: {
|
|
collectionId: string;
|
|
dataId?: string;
|
|
defaultValue?: { q: string; a?: string };
|
|
onClose: () => void;
|
|
onSuccess: (data: InputDataType & { dataId: string }) => void;
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const theme = useTheme();
|
|
const { toast } = useToast();
|
|
const [currentTab, setCurrentTab] = useState(TabEnum.content);
|
|
const { vectorModelList } = useSystemStore();
|
|
const { isPc } = useSystem();
|
|
const { register, handleSubmit, reset, control } = useForm<InputDataType>();
|
|
const {
|
|
fields: indexes,
|
|
append: appendIndexes,
|
|
remove: removeIndexes
|
|
} = useFieldArray({
|
|
control,
|
|
name: 'indexes'
|
|
});
|
|
|
|
const tabList = [
|
|
{
|
|
label: (
|
|
<Flex align={'center'}>
|
|
<Box>{t('common:dataset.data.edit.divide_content')}</Box>
|
|
</Flex>
|
|
),
|
|
value: TabEnum.content
|
|
},
|
|
{
|
|
label: (
|
|
<Flex align={'center'}>
|
|
<Box>{t('common:dataset.data.edit.Index', { amount: indexes.length })}</Box>
|
|
<MyTooltip label={t('common:core.app.tool_label.view_doc')}>
|
|
<MyIcon
|
|
name={'book'}
|
|
w={'1rem'}
|
|
mr={'0.38rem'}
|
|
color={'myGray.500'}
|
|
ml={1}
|
|
onClick={() =>
|
|
window.open(getDocPath('/docs/guide/knowledge_base/dataset_engine/'), '_blank')
|
|
}
|
|
_hover={{
|
|
color: 'primary.600',
|
|
cursor: 'pointer'
|
|
}}
|
|
/>
|
|
</MyTooltip>
|
|
</Flex>
|
|
),
|
|
value: TabEnum.index
|
|
}
|
|
];
|
|
|
|
const { ConfirmModal, openConfirm } = useConfirm({
|
|
content: t('common: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) as any)
|
|
});
|
|
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('common:dataset.data.input is empty'));
|
|
}
|
|
|
|
const totalLength = e.q.length + (e.a?.length || 0);
|
|
if (totalLength >= maxToken * 1.4) {
|
|
return Promise.reject(t('common: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('common:dataset.data.Input Success Tip'),
|
|
onSuccess(e) {
|
|
reset({
|
|
...e,
|
|
q: '',
|
|
a: '',
|
|
indexes: []
|
|
});
|
|
onSuccess(e);
|
|
},
|
|
errorToast: t('common:common.error.unKnow')
|
|
});
|
|
|
|
// update
|
|
const { runAsync: onUpdateData, loading: isUpdating } = useRequest2(
|
|
async (e: InputDataType) => {
|
|
if (!dataId) return Promise.reject(t('common:common.error.unKnow'));
|
|
|
|
// not exactly same
|
|
await putDatasetDataById({
|
|
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('common:dataset.data.Update Success Tip'),
|
|
onSuccess(data) {
|
|
onSuccess(data);
|
|
onClose();
|
|
}
|
|
}
|
|
);
|
|
|
|
const isLoading = isFetchingData;
|
|
|
|
const icon = useMemo(
|
|
() => getSourceNameIcon({ sourceName: collection.sourceName, sourceId: collection.sourceId }),
|
|
[collection]
|
|
);
|
|
return (
|
|
<MyModal
|
|
isOpen={true}
|
|
isCentered
|
|
w={['20rem', '64rem']}
|
|
onClose={() => onClose()}
|
|
closeOnOverlayClick={false}
|
|
maxW={'1440px'}
|
|
h={'46.25rem'}
|
|
title={
|
|
<Flex ml={-3}>
|
|
<MyIcon name={icon as any} w={['16px', '20px']} mr={2} />
|
|
<Box
|
|
className={'textEllipsis'}
|
|
wordBreak={'break-all'}
|
|
fontSize={'md'}
|
|
maxW={['200px', '50vw']}
|
|
fontWeight={'500'}
|
|
color={'myGray.900'}
|
|
whiteSpace={'nowrap'}
|
|
overflow={'hidden'}
|
|
textOverflow={'ellipsis'}
|
|
>
|
|
{collection.sourceName || t('common:common.UnKnow Source')}
|
|
</Box>
|
|
</Flex>
|
|
}
|
|
>
|
|
<MyBox
|
|
display={'flex'}
|
|
flexDir={'column'}
|
|
isLoading={isLoading}
|
|
h={'100%'}
|
|
py={[6, '1.5rem']}
|
|
px={[5, '3.25rem']}
|
|
>
|
|
<Flex justify={'space-between'} gap={4} w={'100%'}>
|
|
<Flex justify={'space-between'} pb={4}>
|
|
<LightRowTabs<TabEnum>
|
|
list={tabList}
|
|
p={0}
|
|
value={currentTab}
|
|
onChange={(e: TabEnum) => setCurrentTab(e)}
|
|
/>
|
|
</Flex>
|
|
{currentTab === TabEnum.index && (
|
|
<Button
|
|
variant={'whiteBase'}
|
|
boxShadow={'1'}
|
|
p={0}
|
|
onClick={() =>
|
|
appendIndexes({
|
|
defaultIndex: false,
|
|
text: '',
|
|
dataId: `${Date.now()}`
|
|
})
|
|
}
|
|
>
|
|
<Flex px={'0.62rem'} py={2}>
|
|
<MyIcon name={'common/addLight'} w={'1rem'} mr={'0.38rem'} />
|
|
{t('common:add_new')}
|
|
</Flex>
|
|
</Button>
|
|
)}
|
|
</Flex>
|
|
<Box w={'100%'} flexGrow={1} overflow={'scroll'}>
|
|
{currentTab === TabEnum.content && <InputTab maxToken={maxToken} register={register} />}
|
|
{currentTab === TabEnum.index && (
|
|
<DataIndex
|
|
register={register}
|
|
maxToken={maxToken}
|
|
appendIndexes={appendIndexes}
|
|
removeIndexes={removeIndexes}
|
|
indexes={indexes}
|
|
/>
|
|
)}
|
|
</Box>
|
|
|
|
<Flex justifyContent={'flex-end'} pt={8} pb={[8, 0]} h={[24, 16]}>
|
|
<MyTooltip
|
|
label={collection.permission.hasWritePer ? '' : t('common:dataset.data.Can not edit')}
|
|
>
|
|
<Button
|
|
isDisabled={!collection.permission.hasWritePer}
|
|
isLoading={isImporting || isUpdating}
|
|
// @ts-ignore
|
|
onClick={handleSubmit(dataId ? onUpdateData : sureImportData)}
|
|
>
|
|
{dataId ? t('common:common.Confirm Update') : t('common:common.Confirm Import')}
|
|
</Button>
|
|
</MyTooltip>
|
|
</Flex>
|
|
</MyBox>
|
|
<ConfirmModal />
|
|
</MyModal>
|
|
);
|
|
};
|
|
|
|
export default React.memo(InputDataModal);
|
|
|
|
const InputTab = ({
|
|
maxToken,
|
|
register
|
|
}: {
|
|
maxToken: number;
|
|
register: UseFormRegister<InputDataType>;
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<>
|
|
<Flex h={'100%'} gap={6} flexDir={['column', 'row']} w={'100%'}>
|
|
<Flex flexDir={'column'} flex={1}>
|
|
<Flex mb={2} fontWeight={'medium'} fontSize={'sm'} alignItems={'center'} h={8}>
|
|
<Box color={'red.600'}>*</Box>
|
|
<Box color={'myGray.900'}>{t('common:core.dataset.data.Main Content')}</Box>
|
|
<QuestionTip label={t('common:core.dataset.data.Data Content Tip')} ml={1} />
|
|
</Flex>
|
|
<Box
|
|
borderRadius={'md'}
|
|
border={'1.5px solid var(--Gray-Modern-200, #E8EBF0)'}
|
|
bg={'myGray.25'}
|
|
flex={1}
|
|
>
|
|
<Textarea
|
|
resize={'none'}
|
|
placeholder={t('core.dataset.data.Data Content Placeholder', { maxToken })}
|
|
className={styles.scrollbar}
|
|
maxLength={maxToken}
|
|
h={'100%'}
|
|
tabIndex={1}
|
|
_focus={{
|
|
borderColor: 'primary.500',
|
|
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)',
|
|
bg: 'white'
|
|
}}
|
|
borderColor={'transparent'}
|
|
bg={'myGray.25'}
|
|
{...register(`q`, {
|
|
required: true
|
|
})}
|
|
/>
|
|
</Box>
|
|
</Flex>
|
|
<Flex flex={1} flexDir={'column'}>
|
|
<Flex mb={2} fontWeight={'medium'} fontSize={'sm'} alignItems={'center'} h={8}>
|
|
<Box color={'myGray.900'}>{t('common:core.dataset.data.Auxiliary Data')}</Box>
|
|
<QuestionTip label={t('common:core.dataset.data.Auxiliary Data Tip')} ml={1} />
|
|
</Flex>
|
|
<Box
|
|
borderRadius={'md'}
|
|
border={'1.5px solid '}
|
|
borderColor={'myGray.200'}
|
|
bg={'myGray.25'}
|
|
flex={1}
|
|
>
|
|
<Textarea
|
|
resize={'none'}
|
|
placeholder={t('core.dataset.data.Auxiliary Data Placeholder', {
|
|
maxToken: maxToken * 1.5
|
|
})}
|
|
className={styles.scrollbar}
|
|
borderColor={'transparent'}
|
|
h={'100%'}
|
|
tabIndex={1}
|
|
bg={'myGray.25'}
|
|
maxLength={maxToken * 1.5}
|
|
{...register('a')}
|
|
/>
|
|
</Box>
|
|
</Flex>
|
|
</Flex>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const DataIndex = ({
|
|
maxToken,
|
|
register,
|
|
indexes,
|
|
appendIndexes,
|
|
removeIndexes
|
|
}: {
|
|
maxToken: number;
|
|
register: UseFormRegister<InputDataType>;
|
|
indexes: FieldArrayWithId<InputDataType, 'indexes', 'id'>[];
|
|
appendIndexes: UseFieldArrayAppend<InputDataType, 'indexes'>;
|
|
removeIndexes: UseFieldArrayRemove;
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<>
|
|
<Flex mt={3} gap={3} flexDir={'column'}>
|
|
<Box
|
|
p={4}
|
|
borderRadius={'md'}
|
|
border={'1.5px solid var(--light-fastgpt-primary-opacity-01, rgba(51, 112, 255, 0.10))'}
|
|
bg={'primary.50'}
|
|
>
|
|
<Flex mb={2}>
|
|
<Box flex={1} fontWeight={'medium'} fontSize={'sm'} color={'primary.700'}>
|
|
{t('common:dataset.data.Default Index')}
|
|
</Box>
|
|
</Flex>
|
|
<Box fontSize={'sm'} fontWeight={'medium'} color={'myGray.600'}>
|
|
{t('common:core.dataset.data.Default Index Tip')}
|
|
</Box>
|
|
</Box>
|
|
{indexes?.map((index, i) => {
|
|
return (
|
|
!index.defaultIndex && (
|
|
<Box
|
|
key={index.dataId || i}
|
|
p={4}
|
|
borderRadius={'md'}
|
|
border={'1.5px solid var(--Gray-Modern-200, #E8EBF0)'}
|
|
bg={'myGray.25'}
|
|
_hover={{
|
|
'& .delete': {
|
|
display: 'block'
|
|
}
|
|
}}
|
|
>
|
|
<Flex mb={2}>
|
|
<Box flex={1} fontWeight={'medium'} fontSize={'sm'} color={'myGray.900'}>
|
|
{t('dataset.data.Custom Index Number', { number: i })}
|
|
</Box>
|
|
<DeleteIcon
|
|
onClick={() => {
|
|
if (indexes.length <= 1) {
|
|
appendIndexes(getDefaultIndex({ dataId: `${Date.now()}` }));
|
|
}
|
|
removeIndexes(i);
|
|
}}
|
|
/>
|
|
</Flex>
|
|
<DataIndexTextArea index={i} maxToken={maxToken} register={register} />
|
|
</Box>
|
|
)
|
|
);
|
|
})}
|
|
</Flex>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const DataIndexTextArea = ({
|
|
index,
|
|
maxToken,
|
|
register
|
|
}: {
|
|
index: number;
|
|
maxToken: number;
|
|
register: UseFormRegister<InputDataType>;
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const TextareaDom = useRef<HTMLTextAreaElement | null>(null);
|
|
const {
|
|
ref: TextareaRef,
|
|
required,
|
|
name,
|
|
onChange: onTextChange,
|
|
onBlur
|
|
} = register(`indexes.${index}.text`, { required: true });
|
|
const textareaMinH = '40px';
|
|
useEffect(() => {
|
|
if (TextareaDom.current) {
|
|
TextareaDom.current.style.height = textareaMinH;
|
|
TextareaDom.current.style.height = `${TextareaDom.current.scrollHeight + 5}px`;
|
|
}
|
|
}, []);
|
|
const autoHeight = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
if (e.target) {
|
|
e.target.style.height = textareaMinH;
|
|
e.target.style.height = `${e.target.scrollHeight + 5}px`;
|
|
}
|
|
}, []);
|
|
return (
|
|
<Textarea
|
|
maxLength={maxToken}
|
|
borderColor={'transparent'}
|
|
className={styles.scrollbar}
|
|
minH={textareaMinH}
|
|
px={0}
|
|
pt={0}
|
|
isRequired={required}
|
|
whiteSpace={'pre-wrap'}
|
|
resize={'none'}
|
|
_focus={{
|
|
px: 3,
|
|
py: 1,
|
|
borderColor: 'primary.500',
|
|
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)',
|
|
bg: 'white'
|
|
}}
|
|
placeholder={t('common:dataset.data.Index Placeholder')}
|
|
ref={(e) => {
|
|
if (e) TextareaDom.current = e;
|
|
TextareaRef(e);
|
|
}}
|
|
required
|
|
name={name}
|
|
onChange={(e) => {
|
|
autoHeight(e);
|
|
onTextChange(e);
|
|
}}
|
|
onFocus={autoHeight}
|
|
onBlur={onBlur}
|
|
/>
|
|
);
|
|
};
|