4.8.13 feature (#3118)
* 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>
This commit is contained in:
24
projects/app/src/components/Markdown/codeBlock/Iframe.tsx
Normal file
24
projects/app/src/components/Markdown/codeBlock/Iframe.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useMarkdownWidth } from '../hooks';
|
||||
|
||||
const IframeBlock = ({ code }: { code: string }) => {
|
||||
const { width, Ref } = useMarkdownWidth();
|
||||
return (
|
||||
<Box w={width} ref={Ref}>
|
||||
<iframe
|
||||
src={code}
|
||||
sandbox="allow-scripts allow-forms allow-popups allow-downloads allow-presentation allow-storage-access-by-user-activation"
|
||||
referrerPolicy="no-referrer"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
minHeight: '70vh',
|
||||
border: 'none'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default IframeBlock;
|
||||
@@ -0,0 +1,25 @@
|
||||
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useMarkdownWidth } from '../hooks';
|
||||
|
||||
const MermaidBlock = ({ code }: { code: string }) => {
|
||||
const { width, Ref } = useMarkdownWidth();
|
||||
return (
|
||||
<Box w={width} ref={Ref}>
|
||||
<iframe
|
||||
src={code}
|
||||
sandbox="allow-scripts allow-forms allow-popups allow-downloads allow-presentation allow-storage-access-by-user-activation"
|
||||
referrerPolicy="no-referrer"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
minHeight: '70vh',
|
||||
border: 'none'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MermaidBlock;
|
||||
34
projects/app/src/components/Markdown/hooks.ts
Normal file
34
projects/app/src/components/Markdown/hooks.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useScreen } from '@fastgpt/web/hooks/useScreen';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export const useMarkdownWidth = () => {
|
||||
const Ref = useRef<HTMLDivElement>(null);
|
||||
const [width, setWidth] = useState(400);
|
||||
const { screenWidth } = useScreen();
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const findMarkdownDom = useCallback(() => {
|
||||
if (!Ref.current) return;
|
||||
|
||||
// 一直找到 parent = markdown 的元素
|
||||
let parent = Ref.current?.parentElement;
|
||||
while (parent && !parent.className.includes('chat-box-card')) {
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
|
||||
const ChatItemDom = parent?.parentElement;
|
||||
const clientWidth = ChatItemDom?.clientWidth ? ChatItemDom.clientWidth - (isPc ? 90 : 60) : 500;
|
||||
setWidth(clientWidth);
|
||||
return parent?.parentElement;
|
||||
}, [isPc]);
|
||||
|
||||
useEffect(() => {
|
||||
findMarkdownDom();
|
||||
}, [findMarkdownDom, screenWidth, Ref.current]);
|
||||
|
||||
return {
|
||||
Ref,
|
||||
width
|
||||
};
|
||||
};
|
||||
@@ -22,6 +22,7 @@ const CodeLight = dynamic(() => import('./CodeLight'), { ssr: false });
|
||||
const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'), { ssr: false });
|
||||
const MdImage = dynamic(() => import('./img/Image'), { ssr: false });
|
||||
const EChartsCodeBlock = dynamic(() => import('./img/EChartsCodeBlock'), { ssr: false });
|
||||
const IframeCodeBlock = dynamic(() => import('./codeBlock/Iframe'), { ssr: false });
|
||||
|
||||
const ChatGuide = dynamic(() => import('./chat/Guide'), { ssr: false });
|
||||
const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false });
|
||||
@@ -29,11 +30,13 @@ const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false
|
||||
const Markdown = ({
|
||||
source = '',
|
||||
showAnimation = false,
|
||||
isDisabled = false
|
||||
isDisabled = false,
|
||||
forbidZhFormat = false
|
||||
}: {
|
||||
source?: string;
|
||||
showAnimation?: boolean;
|
||||
isDisabled?: boolean;
|
||||
forbidZhFormat?: boolean;
|
||||
}) => {
|
||||
const components = useMemo<any>(
|
||||
() => ({
|
||||
@@ -46,15 +49,35 @@ const Markdown = ({
|
||||
);
|
||||
|
||||
const formatSource = useMemo(() => {
|
||||
const formatSource = source
|
||||
if (showAnimation || forbidZhFormat) return source;
|
||||
|
||||
// 保护 URL 格式:https://, http://, /api/xxx
|
||||
const urlPlaceholders: string[] = [];
|
||||
const textWithProtectedUrls = source.replace(
|
||||
/(https?:\/\/[^\s<]+[^<.,:;"')\]\s]|\/api\/[^\s]+)(?=\s|$)/g,
|
||||
(match) => {
|
||||
urlPlaceholders.push(match);
|
||||
return `__URL_${urlPlaceholders.length - 1}__`;
|
||||
}
|
||||
);
|
||||
|
||||
// 处理中文与英文数字之间的分词
|
||||
const textWithSpaces = textWithProtectedUrls
|
||||
.replace(
|
||||
/([\u4e00-\u9fa5\u3000-\u303f])([a-zA-Z0-9])|([a-zA-Z0-9])([\u4e00-\u9fa5\u3000-\u303f])/g,
|
||||
'$1$3 $2$4'
|
||||
) // Chinese and english chars separated by space
|
||||
)
|
||||
// 处理引用标记
|
||||
.replace(/\n*(\[QUOTE SIGN\]\(.*\))/g, '$1');
|
||||
|
||||
return formatSource;
|
||||
}, [source]);
|
||||
// 还原 URL
|
||||
const finalText = textWithSpaces.replace(
|
||||
/__URL_(\d+)__/g,
|
||||
(_, index) => urlPlaceholders[parseInt(index)]
|
||||
);
|
||||
|
||||
return finalText;
|
||||
}, [forbidZhFormat, showAnimation, source]);
|
||||
|
||||
const urlTransform = useCallback((val: string) => {
|
||||
return val;
|
||||
@@ -101,6 +124,9 @@ function Code(e: any) {
|
||||
if (codeType === CodeClassNameEnum.echarts) {
|
||||
return <EChartsCodeBlock code={strChildren} />;
|
||||
}
|
||||
if (codeType === CodeClassNameEnum.iframe) {
|
||||
return <IframeCodeBlock code={strChildren} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeLight className={className} codeBlock={codeBlock} match={match}>
|
||||
|
||||
@@ -5,7 +5,8 @@ export enum CodeClassNameEnum {
|
||||
echarts = 'echarts',
|
||||
quote = 'quote',
|
||||
files = 'files',
|
||||
latex = 'latex'
|
||||
latex = 'latex',
|
||||
iframe = 'iframe'
|
||||
}
|
||||
|
||||
function htmlTableToLatex(html: string) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Image, Skeleton, ImageProps } from '@chakra-ui/react';
|
||||
import { Skeleton, ImageProps } from '@chakra-ui/react';
|
||||
import CustomImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
|
||||
export const MyImage = (props: ImageProps) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -13,7 +14,7 @@ export const MyImage = (props: ImageProps) => {
|
||||
justifyContent={'center'}
|
||||
my={1}
|
||||
>
|
||||
<Image
|
||||
<CustomImage
|
||||
display={'inline-block'}
|
||||
borderRadius={'md'}
|
||||
alt={''}
|
||||
|
||||
@@ -82,7 +82,7 @@ const AIChatSettingsModal = ({
|
||||
{t('common:core.ai.AI settings')}
|
||||
{feConfigs?.docUrl && (
|
||||
<Link
|
||||
href={getDocPath('/docs/course/ai_settings/')}
|
||||
href={getDocPath('/docs/guide/course/ai_settings/')}
|
||||
target={'_blank'}
|
||||
ml={1}
|
||||
textDecoration={'underline'}
|
||||
|
||||
@@ -68,7 +68,9 @@ const SettingLLMModel = ({
|
||||
<Button
|
||||
w={'100%'}
|
||||
justifyContent={'flex-start'}
|
||||
variant={'whiteBase'}
|
||||
variant={'whitePrimaryOutline'}
|
||||
size={'lg'}
|
||||
fontSize={'sm'}
|
||||
bg={bg}
|
||||
_active={{
|
||||
transform: 'none'
|
||||
@@ -81,8 +83,9 @@ const SettingLLMModel = ({
|
||||
w={'18px'}
|
||||
/>
|
||||
}
|
||||
rightIcon={<MyIcon name={'common/select'} w={'1rem'} />}
|
||||
pl={4}
|
||||
rightIcon={<MyIcon name={'common/select'} w={'1.2rem'} color={'myGray.500'} />}
|
||||
px={3}
|
||||
pr={2}
|
||||
onClick={onOpenAIChatSetting}
|
||||
>
|
||||
<Box flex={1} textAlign={'left'}>
|
||||
|
||||
@@ -59,7 +59,9 @@ const FileSelect = ({
|
||||
return (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'core/app/simpleMode/file'} mr={2} w={'20px'} />
|
||||
<FormLabel {...labelStyle}>{t('app:file_upload')}</FormLabel>
|
||||
<FormLabel color={'myGray.600'} {...labelStyle}>
|
||||
{t('app:file_upload')}
|
||||
</FormLabel>
|
||||
<ChatFunctionTip type={'file'} />
|
||||
<Box flex={1} />
|
||||
<MyTooltip label={t('app:config_file_upload')}>
|
||||
@@ -68,6 +70,7 @@ const FileSelect = ({
|
||||
iconSpacing={1}
|
||||
size={'sm'}
|
||||
mr={'-5px'}
|
||||
color={'myGray.600'}
|
||||
onClick={onOpen}
|
||||
>
|
||||
{formLabel}
|
||||
|
||||
@@ -87,7 +87,7 @@ const InputGuideConfig = ({
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'core/app/inputGuides'} mr={2} w={'20px'} />
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel>{chatT('input_guide')}</FormLabel>
|
||||
<FormLabel color={'myGray.600'}>{chatT('input_guide')}</FormLabel>
|
||||
<ChatFunctionTip type={'inputGuide'} />
|
||||
</Flex>
|
||||
<Box flex={1} />
|
||||
@@ -97,6 +97,7 @@ const InputGuideConfig = ({
|
||||
iconSpacing={1}
|
||||
size={'sm'}
|
||||
mr={'-5px'}
|
||||
color={'myGray.600'}
|
||||
onClick={onOpen}
|
||||
>
|
||||
{formLabel}
|
||||
@@ -145,7 +146,7 @@ const InputGuideConfig = ({
|
||||
<Flex mt={8} alignItems={'center'}>
|
||||
<FormLabel>{chatT('custom_input_guide_url')}</FormLabel>
|
||||
<Flex
|
||||
onClick={() => window.open(getDocPath('/docs/course/chat_input_guide'))}
|
||||
onClick={() => window.open(getDocPath('/docs/guide/course/chat_input_guide/'))}
|
||||
color={'primary.700'}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
|
||||
@@ -11,7 +11,7 @@ const QGSwitch = (props: SwitchProps) => {
|
||||
return (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'core/chat/QGFill'} mr={2} w={'20px'} />
|
||||
<FormLabel>{t('common:core.app.Question Guide')}</FormLabel>
|
||||
<FormLabel color={'myGray.600'}>{t('common:core.app.Question Guide')}</FormLabel>
|
||||
<ChatFunctionTip type={'nextQuestion'} />
|
||||
<Box flex={1} />
|
||||
<Switch {...props} />
|
||||
|
||||
@@ -270,7 +270,7 @@ const ScheduledTriggerConfig = ({
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'core/app/schedulePlan'} w={'20px'} />
|
||||
<HStack ml={2} flex={1} spacing={1}>
|
||||
<FormLabel>{t('common:core.app.Interval timer run')}</FormLabel>
|
||||
<FormLabel color={'myGray.600'}>{t('common:core.app.Interval timer run')}</FormLabel>
|
||||
<QuestionTip label={t('common:core.app.Interval timer tip')} />
|
||||
</HStack>
|
||||
<MyTooltip label={t('common:core.app.Config schedule plan')}>
|
||||
@@ -279,6 +279,7 @@ const ScheduledTriggerConfig = ({
|
||||
iconSpacing={1}
|
||||
size={'sm'}
|
||||
mr={'-5px'}
|
||||
color={'myGray.600'}
|
||||
onClick={onOpen}
|
||||
>
|
||||
{formatLabel}
|
||||
|
||||
@@ -13,6 +13,7 @@ import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { defaultTTSConfig } from '@fastgpt/global/core/app/constants';
|
||||
import ChatFunctionTip from './Tip';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
|
||||
const TTSSelect = ({
|
||||
value = defaultTTSConfig,
|
||||
@@ -82,7 +83,7 @@ const TTSSelect = ({
|
||||
return (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'core/app/simpleMode/tts'} mr={2} w={'20px'} />
|
||||
<FormLabel>{t('common:core.app.TTS')}</FormLabel>
|
||||
<FormLabel color={'myGray.600'}>{t('common:core.app.TTS')}</FormLabel>
|
||||
<ChatFunctionTip type={'tts'} />
|
||||
<Box flex={1} />
|
||||
<MyTooltip label={t('common:core.app.Select TTS')}>
|
||||
@@ -92,6 +93,7 @@ const TTSSelect = ({
|
||||
size={'sm'}
|
||||
mr={'-5px'}
|
||||
onClick={onOpen}
|
||||
color={'myGray.600'}
|
||||
>
|
||||
{formLabel}
|
||||
</Button>
|
||||
@@ -132,7 +134,7 @@ const TTSSelect = ({
|
||||
<Flex mt={10} justifyContent={'end'}>
|
||||
{audioPlaying ? (
|
||||
<Flex>
|
||||
<Image src="/icon/speaking.gif" w={'24px'} alt={''} />
|
||||
<MyImage src="/icon/speaking.gif" w={'24px'} alt={''} />
|
||||
<Button
|
||||
ml={2}
|
||||
variant={'grayBase'}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { Box, Flex, Image } from '@chakra-ui/react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useRef } from 'react';
|
||||
@@ -58,8 +58,8 @@ const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => {
|
||||
[FnTypeEnum.visionModel]: {
|
||||
icon: '/imgs/app/question.svg',
|
||||
title: t('app:vision_model_title'),
|
||||
desc: t('app:llm_use_vision_tip'),
|
||||
imgUrl: '/imgs/app/visionModel.png'
|
||||
desc: t('app:open_vision_function_tip'),
|
||||
imgUrl: '/imgs/app/visionModel.svg'
|
||||
},
|
||||
[FnTypeEnum.instruction]: {
|
||||
icon: '/imgs/app/help.svg',
|
||||
@@ -77,7 +77,7 @@ const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => {
|
||||
label={
|
||||
<Box pt={2}>
|
||||
<Flex alignItems={'flex-start'}>
|
||||
<Image src={data.icon} w={'36px'} alt={''} />
|
||||
<MyImage src={data.icon} w={'36px'} alt={''} />
|
||||
<Box ml={3}>
|
||||
<Box fontWeight="bold">{data.title}</Box>
|
||||
<Box fontSize={'xs'} color={'myGray.500'}>
|
||||
@@ -85,7 +85,7 @@ const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => {
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Image src={data.imgUrl} w={'100%'} minH={['auto', '250px']} mt={2} alt={''} />
|
||||
<MyImage src={data.imgUrl} w={'100%'} minH={['auto', '250px']} mt={2} alt={''} />
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -65,10 +65,6 @@ const VariableEdit = ({
|
||||
const { setValue, reset, watch, getValues } = form;
|
||||
const value = getValues();
|
||||
const type = watch('type');
|
||||
const valueType = watch('valueType');
|
||||
const max = watch('max');
|
||||
const min = watch('min');
|
||||
const defaultValue = watch('defaultValue');
|
||||
|
||||
const inputTypeList = useMemo(
|
||||
() =>
|
||||
@@ -173,7 +169,9 @@ const VariableEdit = ({
|
||||
{/* Row box */}
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'core/app/simpleMode/variable'} w={'20px'} />
|
||||
<FormLabel ml={2}>{t('common:core.module.Variable')}</FormLabel>
|
||||
<FormLabel ml={2} color={'myGray.600'}>
|
||||
{t('common:core.module.Variable')}
|
||||
</FormLabel>
|
||||
<ChatFunctionTip type={'variable'} />
|
||||
<Box flex={1} />
|
||||
<Button
|
||||
@@ -181,6 +179,7 @@ const VariableEdit = ({
|
||||
leftIcon={<SmallAddIcon />}
|
||||
iconSpacing={1}
|
||||
size={'sm'}
|
||||
color={'myGray.600'}
|
||||
mr={'-5px'}
|
||||
onClick={() => {
|
||||
reset(addVariable());
|
||||
@@ -193,60 +192,83 @@ const VariableEdit = ({
|
||||
{formatVariables.length > 0 && (
|
||||
<Box mt={2} borderRadius={'md'} overflow={'hidden'} borderWidth={'1px'} borderBottom="none">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Table bg={'white'}>
|
||||
<Thead h={8}>
|
||||
<Tr>
|
||||
<Th
|
||||
borderRadius={'none !important'}
|
||||
fontSize={'mini'}
|
||||
bg={'myGray.50'}
|
||||
p={0}
|
||||
px={4}
|
||||
fontWeight={'medium'}
|
||||
>
|
||||
{t('workflow:Variable_name')}
|
||||
</Th>
|
||||
<Th fontSize={'mini'} bg={'myGray.50'} p={0} px={4} fontWeight={'medium'}>
|
||||
{t('common:common.Require Input')}
|
||||
</Th>
|
||||
<Th
|
||||
fontSize={'mini'}
|
||||
borderRadius={'none !important'}
|
||||
w={'18px !important'}
|
||||
bg={'myGray.50'}
|
||||
p={0}
|
||||
/>
|
||||
<Th fontSize={'mini'}>{t('workflow:Variable_name')}</Th>
|
||||
<Th fontSize={'mini'}>{t('app:global_variables_desc')}</Th>
|
||||
<Th fontSize={'mini'}>{t('common:common.Require Input')}</Th>
|
||||
<Th fontSize={'mini'} borderRadius={'none !important'}></Th>
|
||||
px={4}
|
||||
fontWeight={'medium'}
|
||||
>
|
||||
{t('common:common.Operation')}
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{formatVariables.map((item) => (
|
||||
<Tr key={item.id}>
|
||||
<Td p={0} pl={3}>
|
||||
<MyIcon name={item.icon as any} w={'16px'} color={'myGray.500'} />
|
||||
</Td>
|
||||
<Td>{item.key}</Td>
|
||||
<Td
|
||||
maxW={'200px'}
|
||||
fontSize={'sm'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
wordBreak={'break-all'}
|
||||
px={0}
|
||||
p={0}
|
||||
px={4}
|
||||
h={8}
|
||||
color={'myGray.900'}
|
||||
fontSize={'mini'}
|
||||
fontWeight={'medium'}
|
||||
>
|
||||
{item.description || '-'}
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={item.icon as any} w={'16px'} color={'myGray.400'} mr={2} />
|
||||
{item.key}
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td>{item.required ? '✔' : '-'}</Td>
|
||||
<Td>
|
||||
<MyIcon
|
||||
mr={3}
|
||||
name={'common/settingLight'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
const formattedItem = {
|
||||
...item,
|
||||
list: item.enums || []
|
||||
};
|
||||
reset(formattedItem);
|
||||
}}
|
||||
/>
|
||||
<MyIcon
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() =>
|
||||
onChange(variables.filter((variable) => variable.id !== item.id))
|
||||
}
|
||||
/>
|
||||
<Td p={0} px={4} h={8} color={'myGray.900'} fontSize={'mini'}>
|
||||
<Flex alignItems={'center'}>
|
||||
{item.required ? (
|
||||
<MyIcon name={'check'} w={'16px'} color={'myGray.900'} mr={2} />
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td p={0} px={4} h={8} color={'myGray.600'} fontSize={'mini'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon
|
||||
mr={3}
|
||||
name={'common/settingLight'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
const formattedItem = {
|
||||
...item,
|
||||
list: item.enums || []
|
||||
};
|
||||
reset(formattedItem);
|
||||
}}
|
||||
/>
|
||||
<MyIcon
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() =>
|
||||
onChange(variables.filter((variable) => variable.id !== item.id))
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
@@ -337,11 +359,7 @@ const VariableEdit = ({
|
||||
type={'variable'}
|
||||
isEdit={!!value.key}
|
||||
inputType={type}
|
||||
valueType={valueType}
|
||||
defaultValue={defaultValue}
|
||||
defaultValueType={defaultValueType}
|
||||
max={max}
|
||||
min={min}
|
||||
onClose={() => reset({})}
|
||||
onSubmitSuccess={onSubmitSuccess}
|
||||
onSubmitError={onSubmitError}
|
||||
|
||||
@@ -13,17 +13,20 @@ const WelcomeTextConfig = (props: TextareaProps) => {
|
||||
<>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'core/app/simpleMode/chat'} w={'20px'} />
|
||||
<FormLabel ml={2}>{t('common:core.app.Welcome Text')}</FormLabel>
|
||||
<FormLabel ml={2} color={'myGray.600'}>
|
||||
{t('common:core.app.Welcome Text')}
|
||||
</FormLabel>
|
||||
<ChatFunctionTip type={'welcome'} />
|
||||
</Flex>
|
||||
<MyTextarea
|
||||
className="nowheel"
|
||||
iconSrc={'core/app/simpleMode/chat'}
|
||||
title={t('common:core.app.Welcome Text')}
|
||||
mt={2}
|
||||
mt={1.5}
|
||||
rows={6}
|
||||
fontSize={'sm'}
|
||||
bg={'myGray.50'}
|
||||
bg={'white'}
|
||||
minW={'384px'}
|
||||
placeholder={t('common:core.app.tip.welcomeTextTip')}
|
||||
autoHeight
|
||||
minH={100}
|
||||
|
||||
@@ -34,7 +34,7 @@ const WhisperConfig = ({
|
||||
return (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'core/app/simpleMode/whisper'} mr={2} w={'20px'} />
|
||||
<FormLabel>{t('common:core.app.Whisper')}</FormLabel>
|
||||
<FormLabel color={'myGray.600'}>{t('common:core.app.Whisper')}</FormLabel>
|
||||
<Box flex={1} />
|
||||
<MyTooltip label={t('common:core.app.Config whisper')}>
|
||||
<Button
|
||||
@@ -42,6 +42,7 @@ const WhisperConfig = ({
|
||||
iconSpacing={1}
|
||||
size={'sm'}
|
||||
mr={'-5px'}
|
||||
color={'myGray.600'}
|
||||
onClick={onOpen}
|
||||
>
|
||||
{formLabel}
|
||||
|
||||
@@ -8,7 +8,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { ChatBoxInputFormType, ChatBoxInputType, SendPromptFnType } from '../type';
|
||||
import { textareaMinH } from '../constants';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { useFieldArray, UseFormReturn } from 'react-hook-form';
|
||||
import { ChatBoxContext } from '../Provider';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
@@ -17,6 +17,7 @@ import { documentFileType } from '@fastgpt/global/common/file/constants';
|
||||
import FilePreview from '../../components/FilePreview';
|
||||
import { useFileUpload } from '../hooks/useFileUpload';
|
||||
import ComplianceTip from '@/components/common/ComplianceTip/index';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
|
||||
const InputGuideBox = dynamic(() => import('./InputGuideBox'));
|
||||
|
||||
@@ -44,6 +45,7 @@ const ChatInput = ({
|
||||
}) => {
|
||||
const { isPc } = useSystem();
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { setValue, watch, control } = chatForm;
|
||||
const inputValue = watch('input');
|
||||
@@ -58,6 +60,10 @@ const ChatInput = ({
|
||||
fileSelectConfig
|
||||
} = useContextSelector(ChatBoxContext, (v) => v);
|
||||
|
||||
const fileCtrl = useFieldArray({
|
||||
control,
|
||||
name: 'files'
|
||||
});
|
||||
const {
|
||||
File,
|
||||
onOpenSelectFile,
|
||||
@@ -69,15 +75,15 @@ const ChatInput = ({
|
||||
showSelectFile,
|
||||
showSelectImg,
|
||||
removeFiles,
|
||||
replaceFiles
|
||||
replaceFiles,
|
||||
hasFileUploading
|
||||
} = useFileUpload({
|
||||
outLinkAuthData,
|
||||
chatId: chatId || '',
|
||||
fileSelectConfig,
|
||||
control
|
||||
fileCtrl
|
||||
});
|
||||
const havInput = !!inputValue || fileList.length > 0;
|
||||
const hasFileUploading = fileList.some((item) => !item.url);
|
||||
const canSendMessage = havInput && !hasFileUploading;
|
||||
|
||||
// Upload files
|
||||
@@ -202,7 +208,7 @@ const ChatInput = ({
|
||||
<MyTooltip label={selectFileLabel}>
|
||||
<MyIcon name={selectFileIcon as any} w={'18px'} color={'myGray.600'} />
|
||||
</MyTooltip>
|
||||
<File onSelect={(files) => onSelectFile({ files, fileList })} />
|
||||
<File onSelect={(files) => onSelectFile({ files })} />
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
@@ -278,9 +284,10 @@ const ChatInput = ({
|
||||
.filter((file) => {
|
||||
return file && fileTypeFilter(file);
|
||||
}) as File[];
|
||||
onSelectFile({ files, fileList });
|
||||
onSelectFile({ files });
|
||||
|
||||
if (files.length > 0) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
@@ -431,7 +438,36 @@ const ChatInput = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<Box m={['0 auto', '10px auto']} w={'100%'} maxW={['auto', 'min(800px, 100%)']} px={[0, 5]}>
|
||||
<Box
|
||||
m={['0 auto', '10px auto']}
|
||||
w={'100%'}
|
||||
maxW={['auto', 'min(800px, 100%)']}
|
||||
px={[0, 5]}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!(showSelectFile || showSelectImg)) return;
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
|
||||
const droppedFiles = files.filter((file) => fileTypeFilter(file));
|
||||
if (droppedFiles.length > 0) {
|
||||
onSelectFile({ files: droppedFiles });
|
||||
}
|
||||
|
||||
const invalidFileName = files
|
||||
.filter((file) => !fileTypeFilter(file))
|
||||
.map((file) => file.name)
|
||||
.join(', ');
|
||||
if (invalidFileName) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('chat:unsupported_file_type'),
|
||||
description: invalidFileName
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
pt={fileList.length > 0 ? '0' : ['14px', '18px']}
|
||||
pb={['14px', '18px']}
|
||||
@@ -468,7 +504,7 @@ const ChatInput = ({
|
||||
{RenderTranslateLoading}
|
||||
|
||||
{/* file preview */}
|
||||
<Box px={[2, 4]}>
|
||||
<Box px={[1, 3]}>
|
||||
<FilePreview fileList={fileList} removeFiles={removeFiles} />
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ export type ChatProviderProps = OutLinkChatAuthProps & {
|
||||
|
||||
// not chat test params
|
||||
chatId?: string;
|
||||
chatType: 'log' | 'chat' | 'share' | 'team';
|
||||
showRawSource: boolean;
|
||||
showNodeStatus: boolean;
|
||||
};
|
||||
|
||||
type useChatStoreType = OutLinkChatAuthProps &
|
||||
@@ -137,7 +140,9 @@ const Provider = ({
|
||||
chatHistories,
|
||||
setChatHistories,
|
||||
variablesForm,
|
||||
|
||||
chatType = 'chat',
|
||||
showRawSource,
|
||||
showNodeStatus,
|
||||
chatConfig = {},
|
||||
children,
|
||||
...props
|
||||
@@ -239,7 +244,10 @@ const Provider = ({
|
||||
chatInputGuide,
|
||||
outLinkAuthData,
|
||||
variablesForm,
|
||||
getHistoryResponseData
|
||||
getHistoryResponseData,
|
||||
chatType,
|
||||
showRawSource,
|
||||
showNodeStatus
|
||||
};
|
||||
|
||||
return <ChatBoxContext.Provider value={value}>{children}</ChatBoxContext.Provider>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCopyData } from '@/web/common/hooks/useCopyData';
|
||||
import { Flex, FlexProps, Image, css, useTheme } from '@chakra-ui/react';
|
||||
import { Flex, FlexProps, css, useTheme } from '@chakra-ui/react';
|
||||
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import React, { useMemo } from 'react';
|
||||
@@ -9,6 +9,7 @@ import { formatChatValue2InputType } from '../utils';
|
||||
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { ChatBoxContext } from '../Provider';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
|
||||
export type ChatControllerProps = {
|
||||
isLastChild: boolean;
|
||||
@@ -124,7 +125,7 @@ const ChatController = ({
|
||||
onClick={cancelAudio}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Image
|
||||
<MyImage
|
||||
src="/icon/speaking.gif"
|
||||
w={'23px'}
|
||||
alt={''}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, BoxProps, Card, Flex } from '@chakra-ui/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import ChatController, { type ChatControllerProps } from './ChatController';
|
||||
import ChatAvatar from './ChatAvatar';
|
||||
import { MessageCardStyle } from '../constants';
|
||||
@@ -22,6 +22,9 @@ import { useTranslation } from 'next-i18next';
|
||||
import { AIChatItemValueItemType, ChatItemValueItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { CodeClassNameEnum } from '@/components/Markdown/utils';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { formatTimeToChatItemTime } from '@fastgpt/global/common/string/time';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const colorMap = {
|
||||
[ChatStatusEnum.loading]: {
|
||||
@@ -110,8 +113,10 @@ const AIContentCard = React.memo(function AIContentCard({
|
||||
const ChatItem = (props: Props) => {
|
||||
const { type, avatar, statusBoxData, children, isLastChild, questionGuides = [], chat } = props;
|
||||
|
||||
const styleMap: BoxProps =
|
||||
type === ChatRoleEnum.Human
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const styleMap: BoxProps = {
|
||||
...(type === ChatRoleEnum.Human
|
||||
? {
|
||||
order: 0,
|
||||
borderRadius: '8px 0 8px 8px',
|
||||
@@ -125,10 +130,17 @@ const ChatItem = (props: Props) => {
|
||||
justifyContent: 'flex-start',
|
||||
textAlign: 'left',
|
||||
bg: 'myGray.50'
|
||||
};
|
||||
|
||||
}),
|
||||
fontSize: 'mini',
|
||||
fontWeight: '400',
|
||||
color: 'myGray.500'
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
|
||||
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
|
||||
const showNodeStatus = useContextSelector(ChatBoxContext, (v) => v.showNodeStatus);
|
||||
const isChatLog = chatType === 'log';
|
||||
|
||||
const { copyData } = useCopyData();
|
||||
|
||||
@@ -196,18 +208,38 @@ const ChatItem = (props: Props) => {
|
||||
}, [chat.obj, chat.value, isChatting]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
_hover={{
|
||||
'& .time-label': {
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* control icon */}
|
||||
<Flex w={'100%'} alignItems={'center'} gap={2} justifyContent={styleMap.justifyContent}>
|
||||
<Flex w={'100%'} alignItems={'flex-end'} gap={2} justifyContent={styleMap.justifyContent}>
|
||||
{isChatting && type === ChatRoleEnum.AI && isLastChild ? null : (
|
||||
<Box order={styleMap.order} ml={styleMap.ml}>
|
||||
<Flex order={styleMap.order} ml={styleMap.ml} align={'center'} gap={'0.62rem'}>
|
||||
{chat.time && (isPc || isChatLog) && (
|
||||
<Box
|
||||
order={type === ChatRoleEnum.AI ? 2 : 0}
|
||||
className={'time-label'}
|
||||
fontSize={styleMap.fontSize}
|
||||
color={styleMap.color}
|
||||
fontWeight={styleMap.fontWeight}
|
||||
display={isChatLog ? 'block' : 'none'}
|
||||
>
|
||||
{t(formatTimeToChatItemTime(chat.time) as any, {
|
||||
time: dayjs(chat.time).format('HH:mm')
|
||||
}).replace('#', ':')}
|
||||
</Box>
|
||||
)}
|
||||
<ChatController {...props} isLastChild={isLastChild} />
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
<ChatAvatar src={avatar} type={type} />
|
||||
|
||||
{/* Workflow status */}
|
||||
{!!chatStatusMap && statusBoxData && isLastChild && (
|
||||
{!!chatStatusMap && statusBoxData && isLastChild && showNodeStatus && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
px={3}
|
||||
@@ -290,7 +322,7 @@ const ChatItem = (props: Props) => {
|
||||
</Card>
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ const ContextModal = ({ onClose, dataId }: { onClose: () => void; dataId: string
|
||||
border={'base'}
|
||||
_notLast={{ mb: 2 }}
|
||||
position={'relative'}
|
||||
bg={i % 2 === 0 ? 'white' : 'myGray.50'}
|
||||
>
|
||||
<Box fontWeight={'bold'}>{item.obj}</Box>
|
||||
<Box>{item.value}</Box>
|
||||
|
||||
@@ -6,16 +6,19 @@ import { useTranslation } from 'next-i18next';
|
||||
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import QuoteItem from '@/components/core/dataset/QuoteItem';
|
||||
import RawSourceBox from '@/components/core/dataset/RawSourceBox';
|
||||
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
|
||||
|
||||
const QuoteModal = ({
|
||||
rawSearch = [],
|
||||
onClose,
|
||||
showDetail,
|
||||
canEditDataset,
|
||||
showRawSource,
|
||||
metadata
|
||||
}: {
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
onClose: () => void;
|
||||
showDetail: boolean;
|
||||
canEditDataset: boolean;
|
||||
showRawSource: boolean;
|
||||
metadata?: {
|
||||
collectionId: string;
|
||||
sourceId?: string;
|
||||
@@ -42,13 +45,13 @@ const QuoteModal = ({
|
||||
h={['90vh', '80vh']}
|
||||
isCentered
|
||||
minW={['90vw', '600px']}
|
||||
iconSrc={!!metadata ? undefined : '/imgs/modal/quote.svg'}
|
||||
iconSrc={!!metadata ? undefined : getWebReqUrl('/imgs/modal/quote.svg')}
|
||||
title={
|
||||
<Box>
|
||||
{metadata ? (
|
||||
<RawSourceBox {...metadata} canView={showDetail} />
|
||||
<RawSourceBox {...metadata} canView={showRawSource} />
|
||||
) : (
|
||||
<>{t('core.chat.Quote Amount', { amount: rawSearch.length })}</>
|
||||
<>{t('common:core.chat.Quote Amount', { amount: rawSearch.length })}</>
|
||||
)}
|
||||
<Box fontSize={'xs'} color={'myGray.500'} fontWeight={'normal'}>
|
||||
{t('common:core.chat.quote.Quote Tip')}
|
||||
@@ -57,7 +60,11 @@ const QuoteModal = ({
|
||||
}
|
||||
>
|
||||
<ModalBody>
|
||||
<QuoteList rawSearch={filterResults} showDetail={showDetail} />
|
||||
<QuoteList
|
||||
rawSearch={filterResults}
|
||||
canEditDataset={canEditDataset}
|
||||
canViewSource={showRawSource}
|
||||
/>
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
</>
|
||||
@@ -68,10 +75,12 @@ export default QuoteModal;
|
||||
|
||||
export const QuoteList = React.memo(function QuoteList({
|
||||
rawSearch = [],
|
||||
showDetail
|
||||
canEditDataset,
|
||||
canViewSource
|
||||
}: {
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
showDetail: boolean;
|
||||
canEditDataset: boolean;
|
||||
canViewSource: boolean;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -88,7 +97,11 @@ export const QuoteList = React.memo(function QuoteList({
|
||||
_hover={{ '& .hover-data': { display: 'flex' } }}
|
||||
bg={i % 2 === 0 ? 'white' : 'myWhite.500'}
|
||||
>
|
||||
<QuoteItem quoteItem={item} canViewSource={showDetail} linkToDataset={showDetail} />
|
||||
<QuoteItem
|
||||
quoteItem={item}
|
||||
canViewSource={canViewSource}
|
||||
canEditDataset={canEditDataset}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -7,12 +7,13 @@ import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
|
||||
import ChatBoxDivider from '@/components/core/chat/Divider';
|
||||
import { strIsLink } from '@fastgpt/global/common/string/tools';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils';
|
||||
import { useSize } from 'ahooks';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatBoxContext } from '../Provider';
|
||||
|
||||
const QuoteModal = dynamic(() => import('./QuoteModal'));
|
||||
const ContextModal = dynamic(() => import('./ContextModal'));
|
||||
@@ -20,11 +21,9 @@ const WholeResponseModal = dynamic(() => import('../../../components/WholeRespon
|
||||
|
||||
const ResponseTags = ({
|
||||
showTags,
|
||||
showDetail,
|
||||
historyItem
|
||||
}: {
|
||||
showTags: boolean;
|
||||
showDetail: boolean;
|
||||
historyItem: ChatSiteItemType;
|
||||
}) => {
|
||||
const { isPc } = useSystem();
|
||||
@@ -37,6 +36,7 @@ const ResponseTags = ({
|
||||
totalRunningTime: runningTime = 0,
|
||||
historyPreviewLength = 0
|
||||
} = useMemo(() => addStatisticalDataToHistoryItem(historyItem), [historyItem]);
|
||||
|
||||
const [quoteModalData, setQuoteModalData] = useState<{
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
metadata?: {
|
||||
@@ -47,6 +47,10 @@ const ResponseTags = ({
|
||||
}>();
|
||||
const [quoteFolded, setQuoteFolded] = useState<boolean>(true);
|
||||
|
||||
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
|
||||
const showRawSource = useContextSelector(ChatBoxContext, (v) => v.showRawSource);
|
||||
const notSharePage = useMemo(() => chatType !== 'share', [chatType]);
|
||||
|
||||
const {
|
||||
isOpen: isOpenWholeModal,
|
||||
onOpen: onOpenWholeModal,
|
||||
@@ -77,13 +81,20 @@ const ResponseTags = ({
|
||||
sourceName: item.sourceName,
|
||||
sourceId: item.sourceId,
|
||||
icon: getSourceNameIcon({ sourceId: item.sourceId, sourceName: item.sourceName }),
|
||||
canReadQuote: showDetail || strIsLink(item.sourceId),
|
||||
collectionId: item.collectionId
|
||||
}));
|
||||
}, [quoteList, showDetail]);
|
||||
}, [quoteList]);
|
||||
|
||||
const notEmptyTags =
|
||||
quoteList.length > 0 ||
|
||||
(llmModuleAccount === 1 && notSharePage) ||
|
||||
(llmModuleAccount > 1 && notSharePage) ||
|
||||
(isPc && runningTime > 0) ||
|
||||
notSharePage;
|
||||
|
||||
return !showTags ? null : (
|
||||
<>
|
||||
{/* quote */}
|
||||
{sourceList.length > 0 && (
|
||||
<>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
||||
@@ -176,7 +187,8 @@ const ResponseTags = ({
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
{showDetail && (
|
||||
|
||||
{notEmptyTags && (
|
||||
<Flex alignItems={'center'} mt={3} flexWrap={'wrap'} gap={2}>
|
||||
{quoteList.length > 0 && (
|
||||
<MyTooltip label={t('chat:view_citations')}>
|
||||
@@ -190,7 +202,7 @@ const ResponseTags = ({
|
||||
</MyTag>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{llmModuleAccount === 1 && (
|
||||
{llmModuleAccount === 1 && notSharePage && (
|
||||
<>
|
||||
{historyPreviewLength > 0 && (
|
||||
<MyTooltip label={t('chat:click_contextual_preview')}>
|
||||
@@ -206,12 +218,11 @@ const ResponseTags = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{llmModuleAccount > 1 && (
|
||||
{llmModuleAccount > 1 && notSharePage && (
|
||||
<MyTag type="borderSolid" colorSchema="blue">
|
||||
{t('chat:multiple_AI_conversations')}
|
||||
</MyTag>
|
||||
)}
|
||||
|
||||
{isPc && runningTime > 0 && (
|
||||
<MyTooltip label={t('chat:module_runtime_and')}>
|
||||
<MyTag colorSchema="purple" type="borderSolid" cursor={'default'}>
|
||||
@@ -219,29 +230,32 @@ const ResponseTags = ({
|
||||
</MyTag>
|
||||
</MyTooltip>
|
||||
)}
|
||||
<MyTooltip label={t('common:core.chat.response.Read complete response tips')}>
|
||||
<MyTag
|
||||
colorSchema="gray"
|
||||
type="borderSolid"
|
||||
cursor={'pointer'}
|
||||
onClick={onOpenWholeModal}
|
||||
>
|
||||
{t('common:core.chat.response.Read complete response')}
|
||||
</MyTag>
|
||||
</MyTooltip>
|
||||
|
||||
{notSharePage && (
|
||||
<MyTooltip label={t('common:core.chat.response.Read complete response tips')}>
|
||||
<MyTag
|
||||
colorSchema="gray"
|
||||
type="borderSolid"
|
||||
cursor={'pointer'}
|
||||
onClick={onOpenWholeModal}
|
||||
>
|
||||
{t('common:core.chat.response.Read complete response')}
|
||||
</MyTag>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{!!quoteModalData && (
|
||||
<QuoteModal
|
||||
{...quoteModalData}
|
||||
showDetail={showDetail}
|
||||
canEditDataset={notSharePage}
|
||||
showRawSource={showRawSource}
|
||||
onClose={() => setQuoteModalData(undefined)}
|
||||
/>
|
||||
)}
|
||||
{isOpenContextModal && <ContextModal dataId={dataId} onClose={onCloseContextModal} />}
|
||||
{isOpenWholeModal && (
|
||||
<WholeResponseModal dataId={dataId} showDetail={showDetail} onClose={onCloseWholeModal} />
|
||||
)}
|
||||
{isOpenWholeModal && <WholeResponseModal dataId={dataId} onClose={onCloseWholeModal} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { useDeepCompareEffect } from 'ahooks';
|
||||
import { VariableItemType } from '@fastgpt/global/core/app/type';
|
||||
import MyTextarea from '@/components/common/Textarea/MyTextarea';
|
||||
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
|
||||
|
||||
export const VariableInputItem = ({
|
||||
item,
|
||||
@@ -64,14 +65,14 @@ export const VariableInputItem = ({
|
||||
minH={40}
|
||||
maxH={160}
|
||||
bg={'myGray.50'}
|
||||
{...register(item.key, {
|
||||
{...register(`variables.${item.key}`, {
|
||||
required: item.required
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.textarea && (
|
||||
<Textarea
|
||||
{...register(item.key, {
|
||||
{...register(`variables.${item.key}`, {
|
||||
required: item.required
|
||||
})}
|
||||
rows={5}
|
||||
@@ -82,9 +83,9 @@ export const VariableInputItem = ({
|
||||
|
||||
{item.type === VariableInputEnum.select && (
|
||||
<Controller
|
||||
key={item.key}
|
||||
key={`variables.${item.key}`}
|
||||
control={control}
|
||||
name={item.key}
|
||||
name={`variables.${item.key}`}
|
||||
rules={{ required: item.required }}
|
||||
render={({ field: { ref, value } }) => {
|
||||
return (
|
||||
@@ -96,7 +97,7 @@ export const VariableInputItem = ({
|
||||
value: item.value
|
||||
}))}
|
||||
value={value}
|
||||
onchange={(e) => setValue(item.key, e)}
|
||||
onchange={(e) => setValue(`variables.${item.key}`, e)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
@@ -104,27 +105,19 @@ export const VariableInputItem = ({
|
||||
)}
|
||||
{item.type === VariableInputEnum.numberInput && (
|
||||
<Controller
|
||||
key={item.key}
|
||||
key={`variables.${item.key}`}
|
||||
control={control}
|
||||
name={item.key}
|
||||
name={`variables.${item.key}`}
|
||||
rules={{ required: item.required, min: item.min, max: item.max }}
|
||||
render={({ field: { ref, value, onChange } }) => (
|
||||
<NumberInput
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<MyNumberInput
|
||||
step={1}
|
||||
min={item.min}
|
||||
max={item.max}
|
||||
bg={'white'}
|
||||
rounded={'md'}
|
||||
clampValueOnBlur={false}
|
||||
value={value}
|
||||
onChange={(valueString) => onChange(Number(valueString))}
|
||||
>
|
||||
<NumberInputField ref={ref} bg={'white'} />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -22,7 +22,7 @@ const WelcomeBox = ({ welcomeText }: { welcomeText: string }) => {
|
||||
bg={'white'}
|
||||
boxShadow={'0 0 8px rgba(0,0,0,0.15)'}
|
||||
>
|
||||
<Markdown source={`~~~guide \n${welcomeText}`} />
|
||||
<Markdown source={`~~~guide \n${welcomeText}`} forbidZhFormat />
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -9,21 +9,22 @@ import { getFileIcon } from '@fastgpt/global/common/file/icon';
|
||||
import { formatFileSize } from '@fastgpt/global/common/file/tools';
|
||||
import { clone } from 'lodash';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { Control, useFieldArray } from 'react-hook-form';
|
||||
import { UseFieldArrayReturn } from 'react-hook-form';
|
||||
import { ChatBoxInputFormType, UserInputFileItemType } from '../type';
|
||||
import { AppFileSelectConfigType } from '@fastgpt/global/core/app/type';
|
||||
import { documentFileType } from '@fastgpt/global/common/file/constants';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
|
||||
interface UseFileUploadOptions {
|
||||
outLinkAuthData: any;
|
||||
type UseFileUploadOptions = {
|
||||
outLinkAuthData: OutLinkChatAuthProps;
|
||||
chatId: string;
|
||||
fileSelectConfig: AppFileSelectConfigType;
|
||||
control: Control<ChatBoxInputFormType, any>;
|
||||
}
|
||||
fileCtrl: UseFieldArrayReturn<ChatBoxInputFormType, 'files', 'id'>;
|
||||
};
|
||||
|
||||
export const useFileUpload = (props: UseFileUploadOptions) => {
|
||||
const { outLinkAuthData, chatId, fileSelectConfig, control } = props;
|
||||
const { outLinkAuthData, chatId, fileSelectConfig, fileCtrl } = props;
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
@@ -32,16 +33,16 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
|
||||
update: updateFiles,
|
||||
remove: removeFiles,
|
||||
fields: fileList,
|
||||
replace: replaceFiles
|
||||
} = useFieldArray({
|
||||
control: control,
|
||||
name: 'files'
|
||||
});
|
||||
replace: replaceFiles,
|
||||
append: appendFiles
|
||||
} = fileCtrl;
|
||||
const hasFileUploading = fileList.some((item) => !item.url);
|
||||
|
||||
const showSelectFile = fileSelectConfig?.canSelectFile;
|
||||
const showSelectImg = fileSelectConfig?.canSelectImg;
|
||||
const maxSelectFiles = fileSelectConfig?.maxFiles ?? 10;
|
||||
const maxSize = (feConfigs?.uploadFileMaxSize || 1024) * 1024 * 1024; // nkb
|
||||
const canSelectFileAmount = maxSelectFiles - fileList.length;
|
||||
|
||||
const { icon: selectFileIcon, label: selectFileLabel } = useMemo(() => {
|
||||
if (showSelectFile && showSelectImg) {
|
||||
@@ -66,11 +67,11 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
|
||||
const { File, onOpen: onOpenSelectFile } = useSelectFile({
|
||||
fileType: `${showSelectImg ? 'image/*,' : ''} ${showSelectFile ? documentFileType : ''}`,
|
||||
multiple: true,
|
||||
maxCount: maxSelectFiles
|
||||
maxCount: canSelectFileAmount
|
||||
});
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
async ({ files, fileList }: { files: File[]; fileList: UserInputFileItemType[] }) => {
|
||||
async ({ files }: { files: File[] }) => {
|
||||
if (!files || files.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -129,22 +130,11 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
|
||||
)
|
||||
);
|
||||
|
||||
// Document, image
|
||||
const concatFileList = clone(
|
||||
fileList.concat(loadFiles).sort((a, b) => {
|
||||
if (a.type === ChatFileTypeEnum.image && b.type === ChatFileTypeEnum.file) {
|
||||
return 1;
|
||||
} else if (a.type === ChatFileTypeEnum.file && b.type === ChatFileTypeEnum.image) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
);
|
||||
replaceFiles(concatFileList);
|
||||
appendFiles(loadFiles);
|
||||
|
||||
return loadFiles;
|
||||
},
|
||||
[maxSelectFiles, replaceFiles, toast, t, maxSize]
|
||||
[maxSelectFiles, appendFiles, toast, t, maxSize]
|
||||
);
|
||||
|
||||
const uploadFiles = async () => {
|
||||
@@ -198,10 +188,23 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
|
||||
removeFiles(errorFileIndex);
|
||||
};
|
||||
|
||||
const sortFileList = useMemo(() => {
|
||||
// Sort: Document, image
|
||||
const sortResult = clone(fileList).sort((a, b) => {
|
||||
if (a.type === ChatFileTypeEnum.image && b.type === ChatFileTypeEnum.file) {
|
||||
return 1;
|
||||
} else if (a.type === ChatFileTypeEnum.file && b.type === ChatFileTypeEnum.image) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return sortResult;
|
||||
}, [fileList]);
|
||||
|
||||
return {
|
||||
File,
|
||||
onOpenSelectFile,
|
||||
fileList,
|
||||
fileList: sortFileList,
|
||||
onSelectFile,
|
||||
uploadFiles,
|
||||
selectFileIcon,
|
||||
@@ -209,6 +212,7 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
|
||||
showSelectFile,
|
||||
showSelectImg,
|
||||
removeFiles,
|
||||
replaceFiles
|
||||
replaceFiles,
|
||||
hasFileUploading
|
||||
};
|
||||
};
|
||||
|
||||
@@ -67,6 +67,9 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useCreation, useMemoizedFn, useThrottleFn } from 'ahooks';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils';
|
||||
import { formatTimeToChatItemTime } from '@fastgpt/global/common/string/time';
|
||||
import dayjs from 'dayjs';
|
||||
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
|
||||
|
||||
const ResponseTags = dynamic(() => import('./components/ResponseTags'));
|
||||
const FeedbackModal = dynamic(() => import('./components/FeedbackModal'));
|
||||
@@ -108,6 +111,18 @@ type Props = OutLinkChatAuthProps &
|
||||
onDelMessage?: (e: { contentId: string }) => void;
|
||||
};
|
||||
|
||||
const ChatTimeBox = ({ time }: { time: Date }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box w={'100%'} fontSize={'mini'} textAlign={'center'} color={'myGray.500'} fontWeight={'400'}>
|
||||
{t(formatTimeToChatItemTime(time) as any, {
|
||||
time: dayjs(time).format('HH#mm')
|
||||
}).replace('#', ':')}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ChatBox = (
|
||||
{
|
||||
feedbackType = FeedbackTypeEnum.hidden,
|
||||
@@ -393,7 +408,7 @@ const ChatBox = (
|
||||
isInteractivePrompt = false
|
||||
}) => {
|
||||
variablesForm.handleSubmit(
|
||||
async (variables) => {
|
||||
async ({ variables }) => {
|
||||
if (!onStartChat) return;
|
||||
if (isChatting) {
|
||||
toast({
|
||||
@@ -436,6 +451,7 @@ const ChatBox = (
|
||||
{
|
||||
dataId: getNanoid(24),
|
||||
obj: ChatRoleEnum.Human,
|
||||
time: new Date(),
|
||||
value: [
|
||||
...files.map((file) => ({
|
||||
type: ChatItemValueTypeEnum.file,
|
||||
@@ -510,6 +526,12 @@ const ChatBox = (
|
||||
generatingMessage: (e) => generatingMessage({ ...e, autoTTSResponse }),
|
||||
variables: requestVariables
|
||||
});
|
||||
if (responseData?.[responseData.length - 1]?.error) {
|
||||
toast({
|
||||
title: t(responseData[responseData.length - 1].error?.message),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
isNewChatReplace.current = isNewChat;
|
||||
|
||||
@@ -521,6 +543,7 @@ const ChatBox = (
|
||||
return {
|
||||
...item,
|
||||
status: ChatStatusEnum.finish,
|
||||
time: new Date(),
|
||||
responseData: item.responseData
|
||||
? mergeChatResponseData([...item.responseData, ...responseData])
|
||||
: responseData
|
||||
@@ -543,6 +566,7 @@ const ChatBox = (
|
||||
// tts audio
|
||||
autoTTSResponse && splitText2Audio(responseText, true);
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
toast({
|
||||
title: t(getErrText(err, 'core.chat.error.Chat error') as any),
|
||||
status: 'error',
|
||||
@@ -562,6 +586,7 @@ const ChatBox = (
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
time: new Date(),
|
||||
status: ChatStatusEnum.finish
|
||||
};
|
||||
})
|
||||
@@ -877,88 +902,97 @@ const ChatBox = (
|
||||
{/* chat history */}
|
||||
<Box id={'history'}>
|
||||
{chatHistories.map((item, index) => (
|
||||
<Box key={item.dataId} py={5}>
|
||||
{item.obj === ChatRoleEnum.Human && (
|
||||
<ChatItem
|
||||
type={item.obj}
|
||||
avatar={userAvatar}
|
||||
chat={item}
|
||||
onRetry={retryInput(item.dataId)}
|
||||
onDelete={delOneMessage(item.dataId)}
|
||||
isLastChild={index === chatHistories.length - 1}
|
||||
/>
|
||||
)}
|
||||
{item.obj === ChatRoleEnum.AI && (
|
||||
<>
|
||||
<>
|
||||
{/* 并且时间和上一条的time相差超过十分钟 */}
|
||||
{index !== 0 &&
|
||||
item.time &&
|
||||
chatHistories[index - 1].time !== undefined &&
|
||||
new Date(item.time).getTime() -
|
||||
new Date(chatHistories[index - 1].time!).getTime() >
|
||||
10 * 60 * 1000 && <ChatTimeBox time={item.time} />}
|
||||
|
||||
<Box key={item.dataId} py={6}>
|
||||
{item.obj === ChatRoleEnum.Human && (
|
||||
<ChatItem
|
||||
type={item.obj}
|
||||
avatar={appAvatar}
|
||||
avatar={userAvatar}
|
||||
chat={item}
|
||||
onRetry={retryInput(item.dataId)}
|
||||
onDelete={delOneMessage(item.dataId)}
|
||||
isLastChild={index === chatHistories.length - 1}
|
||||
{...{
|
||||
showVoiceIcon,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
teamId,
|
||||
teamToken,
|
||||
statusBoxData,
|
||||
questionGuides,
|
||||
onMark: onMark(
|
||||
item,
|
||||
formatChatValue2InputType(chatHistories[index - 1]?.value)?.text
|
||||
),
|
||||
onAddUserLike: onAddUserLike(item),
|
||||
onCloseUserLike: onCloseUserLike(item),
|
||||
onAddUserDislike: onAddUserDislike(item),
|
||||
onReadUserDislike: onReadUserDislike(item)
|
||||
}}
|
||||
>
|
||||
<ResponseTags
|
||||
showTags={index !== chatHistories.length - 1 || !isChatting}
|
||||
showDetail={!shareId && !teamId}
|
||||
historyItem={item}
|
||||
/>
|
||||
/>
|
||||
)}
|
||||
{item.obj === ChatRoleEnum.AI && (
|
||||
<>
|
||||
<ChatItem
|
||||
type={item.obj}
|
||||
avatar={appAvatar}
|
||||
chat={item}
|
||||
isLastChild={index === chatHistories.length - 1}
|
||||
{...{
|
||||
showVoiceIcon,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
teamId,
|
||||
teamToken,
|
||||
statusBoxData,
|
||||
questionGuides,
|
||||
onMark: onMark(
|
||||
item,
|
||||
formatChatValue2InputType(chatHistories[index - 1]?.value)?.text
|
||||
),
|
||||
onAddUserLike: onAddUserLike(item),
|
||||
onCloseUserLike: onCloseUserLike(item),
|
||||
onAddUserDislike: onAddUserDislike(item),
|
||||
onReadUserDislike: onReadUserDislike(item)
|
||||
}}
|
||||
>
|
||||
<ResponseTags
|
||||
showTags={index !== chatHistories.length - 1 || !isChatting}
|
||||
historyItem={item}
|
||||
/>
|
||||
|
||||
{/* custom feedback */}
|
||||
{item.customFeedbacks && item.customFeedbacks.length > 0 && (
|
||||
<Box>
|
||||
<ChatBoxDivider
|
||||
icon={'core/app/customFeedback'}
|
||||
text={t('common:core.app.feedback.Custom feedback')}
|
||||
/>
|
||||
{item.customFeedbacks.map((text, i) => (
|
||||
<Box key={i}>
|
||||
<MyTooltip
|
||||
label={t('common:core.app.feedback.close custom feedback')}
|
||||
>
|
||||
<Checkbox
|
||||
onChange={onCloseCustomFeedback(item, i)}
|
||||
icon={<MyIcon name={'common/check'} w={'12px'} />}
|
||||
{/* custom feedback */}
|
||||
{item.customFeedbacks && item.customFeedbacks.length > 0 && (
|
||||
<Box>
|
||||
<ChatBoxDivider
|
||||
icon={'core/app/customFeedback'}
|
||||
text={t('common:core.app.feedback.Custom feedback')}
|
||||
/>
|
||||
{item.customFeedbacks.map((text, i) => (
|
||||
<Box key={i}>
|
||||
<MyTooltip
|
||||
label={t('common:core.app.feedback.close custom feedback')}
|
||||
>
|
||||
{text}
|
||||
</Checkbox>
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
{/* admin mark content */}
|
||||
{showMarkIcon && item.adminFeedback && (
|
||||
<Box fontSize={'sm'}>
|
||||
<ChatBoxDivider
|
||||
icon="core/app/markLight"
|
||||
text={t('common:core.chat.Admin Mark Content')}
|
||||
/>
|
||||
<Box whiteSpace={'pre-wrap'}>
|
||||
<Box color={'black'}>{item.adminFeedback.q}</Box>
|
||||
<Box color={'myGray.600'}>{item.adminFeedback.a}</Box>
|
||||
<Checkbox
|
||||
onChange={onCloseCustomFeedback(item, i)}
|
||||
icon={<MyIcon name={'common/check'} w={'12px'} />}
|
||||
>
|
||||
{text}
|
||||
</Checkbox>
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</ChatItem>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{/* admin mark content */}
|
||||
{showMarkIcon && item.adminFeedback && (
|
||||
<Box fontSize={'sm'}>
|
||||
<ChatBoxDivider
|
||||
icon="core/app/markLight"
|
||||
text={t('common:core.chat.Admin Mark Content')}
|
||||
/>
|
||||
<Box whiteSpace={'pre-wrap'}>
|
||||
<Box color={'black'}>{item.adminFeedback.q}</Box>
|
||||
<Box color={'myGray.600'}>{item.adminFeedback.a}</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</ChatItem>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -996,7 +1030,7 @@ const ChatBox = (
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'} position={'relative'}>
|
||||
<Script src="/js/html2pdf.bundle.min.js" strategy="lazyOnload"></Script>
|
||||
<Script src={getWebReqUrl('/js/html2pdf.bundle.min.js')} strategy="lazyOnload"></Script>
|
||||
{/* chat box container */}
|
||||
{RenderRecords}
|
||||
{/* message input */}
|
||||
|
||||
@@ -20,9 +20,9 @@ export type UserInputFileItemType = {
|
||||
|
||||
export type ChatBoxInputFormType = {
|
||||
input: string;
|
||||
files: UserInputFileItemType[];
|
||||
files: UserInputFileItemType[]; // global files
|
||||
chatStarted: boolean;
|
||||
[key: string]: any;
|
||||
variables: Record<string, any>;
|
||||
};
|
||||
|
||||
export type ChatBoxInputType = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Controller, useFieldArray } from 'react-hook-form';
|
||||
import RenderPluginInput from './renderPluginInput';
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
@@ -14,7 +14,8 @@ import { useFileUpload } from '../../ChatBox/hooks/useFileUpload';
|
||||
import FilePreview from '../../components/FilePreview';
|
||||
import { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { ChatBoxInputFormType, UserInputFileItemType } from '../../ChatBox/type';
|
||||
import { ChatBoxInputFormType } from '../../ChatBox/type';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
|
||||
const RenderInput = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -29,9 +30,7 @@ const RenderInput = () => {
|
||||
isChatting,
|
||||
chatConfig,
|
||||
chatId,
|
||||
outLinkAuthData,
|
||||
restartInputStore,
|
||||
setRestartInputStore
|
||||
outLinkAuthData
|
||||
} = useContextSelector(PluginRunContext, (v) => v);
|
||||
|
||||
const {
|
||||
@@ -42,6 +41,11 @@ const RenderInput = () => {
|
||||
formState: { errors }
|
||||
} = variablesForm;
|
||||
|
||||
/* ===> Global files(abandon) */
|
||||
const fileCtrl = useFieldArray({
|
||||
control: variablesForm.control,
|
||||
name: 'files'
|
||||
});
|
||||
const {
|
||||
File,
|
||||
onOpenSelectFile,
|
||||
@@ -52,46 +56,76 @@ const RenderInput = () => {
|
||||
showSelectFile,
|
||||
showSelectImg,
|
||||
removeFiles,
|
||||
replaceFiles
|
||||
hasFileUploading
|
||||
} = useFileUpload({
|
||||
outLinkAuthData,
|
||||
chatId: chatId || '',
|
||||
fileSelectConfig: chatConfig?.fileSelectConfig,
|
||||
control
|
||||
fileCtrl
|
||||
});
|
||||
const isDisabledInput = histories.length > 0;
|
||||
|
||||
useRequest2(uploadFiles, {
|
||||
manual: false,
|
||||
errorToast: t('common:upload_file_error'),
|
||||
refreshDeps: [fileList, outLinkAuthData, chatId]
|
||||
});
|
||||
/* Global files(abandon) <=== */
|
||||
|
||||
const [restartData, setRestartData] = useState<ChatBoxInputFormType>();
|
||||
const onClickNewChat = useCallback(
|
||||
(e: ChatBoxInputFormType, files: UserInputFileItemType[] = []) => {
|
||||
setRestartInputStore({
|
||||
...e,
|
||||
files
|
||||
});
|
||||
(e: ChatBoxInputFormType) => {
|
||||
setRestartData(e);
|
||||
onNewChat?.();
|
||||
},
|
||||
[onNewChat, setRestartInputStore]
|
||||
[onNewChat, setRestartData]
|
||||
);
|
||||
|
||||
// Get plugin input components
|
||||
const formatPluginInputs = useMemo(() => {
|
||||
if (histories.length === 0) return pluginInputs;
|
||||
try {
|
||||
const historyValue = histories[0]?.value as UserChatItemValueItemType[];
|
||||
const inputValueString = historyValue.find((item) => item.type === 'text')?.text?.content;
|
||||
|
||||
if (!inputValueString) return pluginInputs;
|
||||
return JSON.parse(inputValueString) as FlowNodeInputItemType[];
|
||||
} catch (error) {
|
||||
console.error('Failed to parse input value:', error);
|
||||
return pluginInputs;
|
||||
}
|
||||
}, [histories, pluginInputs]);
|
||||
|
||||
// Reset input value
|
||||
useEffect(() => {
|
||||
// Set last run value
|
||||
if (!isDisabledInput && restartInputStore) {
|
||||
reset(restartInputStore);
|
||||
// Set config default value
|
||||
if (histories.length === 0) {
|
||||
if (restartData) {
|
||||
reset(restartData);
|
||||
setRestartData(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultFormValues = formatPluginInputs.reduce(
|
||||
(acc, input) => {
|
||||
acc[input.key] = input.defaultValue;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
);
|
||||
|
||||
reset({
|
||||
files: [],
|
||||
variables: defaultFormValues
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Set history to default value
|
||||
const historyVariables = (() => {
|
||||
const historyValue = histories[0]?.value as UserChatItemValueItemType[];
|
||||
if (!historyValue) return undefined;
|
||||
|
||||
const defaultFormValues = pluginInputs.reduce(
|
||||
(acc, input) => {
|
||||
acc[input.key] = input.defaultValue;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
);
|
||||
|
||||
const historyFormValues = (() => {
|
||||
if (!isDisabledInput) return undefined;
|
||||
const historyValue = histories[0].value;
|
||||
try {
|
||||
const inputValueString = historyValue.find((item) => item.type === 'text')?.text?.content;
|
||||
return (
|
||||
@@ -115,32 +149,25 @@ const RenderInput = () => {
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
// Parse history file
|
||||
const historyFileList = (() => {
|
||||
if (!isDisabledInput) return [];
|
||||
const historyValue = histories[0].value as UserChatItemValueItemType[];
|
||||
return historyValue.filter((item) => item.type === 'file').map((item) => item.file);
|
||||
const historyValue = histories[0]?.value as UserChatItemValueItemType[];
|
||||
return historyValue?.filter((item) => item.type === 'file').map((item) => item.file);
|
||||
})();
|
||||
|
||||
reset({
|
||||
...(historyFormValues || defaultFormValues),
|
||||
variables: historyVariables,
|
||||
files: historyFileList
|
||||
});
|
||||
}, [getValues, histories, isDisabledInput, pluginInputs, replaceFiles, reset, restartInputStore]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [histories]);
|
||||
|
||||
const hasFileUploading = useMemo(() => {
|
||||
return fileList.some((item) => !item.url);
|
||||
}, [fileList]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
useRequest2(uploadFiles, {
|
||||
manual: false,
|
||||
errorToast: t('common:upload_file_error'),
|
||||
refreshDeps: [fileList, outLinkAuthData, chatId]
|
||||
});
|
||||
const fileUploading = uploading || hasFileUploading;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
{/* instruction */}
|
||||
{chatConfig?.instruction && (
|
||||
<Box
|
||||
@@ -155,7 +182,7 @@ const RenderInput = () => {
|
||||
<Markdown source={chatConfig.instruction} />
|
||||
</Box>
|
||||
)}
|
||||
{/* file select */}
|
||||
{/* file select(Abandoned) */}
|
||||
{(showSelectFile || showSelectImg) && (
|
||||
<Box mb={5}>
|
||||
<Flex alignItems={'center'}>
|
||||
@@ -175,7 +202,7 @@ const RenderInput = () => {
|
||||
{t('chat:select')}
|
||||
</Button>
|
||||
)}
|
||||
<File onSelect={(files) => onSelectFile({ files, fileList })} />
|
||||
<File onSelect={(files) => onSelectFile({ files })} />
|
||||
</Flex>
|
||||
<FilePreview
|
||||
fileList={fileList}
|
||||
@@ -184,12 +211,12 @@ const RenderInput = () => {
|
||||
</Box>
|
||||
)}
|
||||
{/* Filed */}
|
||||
{pluginInputs.map((input) => {
|
||||
{formatPluginInputs.map((input) => {
|
||||
return (
|
||||
<Controller
|
||||
key={input.key}
|
||||
key={`variables.${input.key}`}
|
||||
control={control}
|
||||
name={input.key}
|
||||
name={`variables.${input.key}`}
|
||||
rules={{
|
||||
validate: (value) => {
|
||||
if (!input.required) return true;
|
||||
@@ -207,6 +234,7 @@ const RenderInput = () => {
|
||||
isDisabled={isDisabledInput}
|
||||
isInvalid={errors && Object.keys(errors).includes(input.key)}
|
||||
input={input}
|
||||
setUploading={setUploading}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
@@ -217,13 +245,14 @@ const RenderInput = () => {
|
||||
{onStartChat && onNewChat && (
|
||||
<Flex justifyContent={'end'} mt={8}>
|
||||
<Button
|
||||
isLoading={isChatting || hasFileUploading}
|
||||
isLoading={isChatting}
|
||||
isDisabled={fileUploading}
|
||||
onClick={() => {
|
||||
handleSubmit((e) => {
|
||||
if (isDisabledInput) {
|
||||
onClickNewChat(e, fileList);
|
||||
onClickNewChat(e);
|
||||
} else {
|
||||
onSubmit(e, fileList);
|
||||
onSubmit(e);
|
||||
}
|
||||
})();
|
||||
}}
|
||||
@@ -232,7 +261,7 @@ const RenderInput = () => {
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const RenderResponseDetail = () => {
|
||||
<>{t('chat:in_progress')}</>
|
||||
) : (
|
||||
<Box flex={'1 0 0'} h={'100%'} overflow={'auto'}>
|
||||
<ResponseBox useMobile={true} response={responseData} showDetail={true} />
|
||||
<ResponseBox useMobile={true} response={responseData} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,36 +1,150 @@
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
NumberDecrementStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
Switch,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import { Box, Button, Flex, Switch, Textarea } from '@chakra-ui/react';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useFileUpload } from '../../ChatBox/hooks/useFileUpload';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { PluginRunContext } from '../context';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import FilePreview from '../../components/FilePreview';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { useFieldArray } from 'react-hook-form';
|
||||
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
|
||||
|
||||
const FileSelector = ({
|
||||
input,
|
||||
setUploading,
|
||||
onChange,
|
||||
value
|
||||
}: {
|
||||
input: FlowNodeInputItemType;
|
||||
setUploading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
onChange: (...event: any[]) => void;
|
||||
value: any;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { variablesForm, histories, chatId, outLinkAuthData } = useContextSelector(
|
||||
PluginRunContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
const fileCtrl = useFieldArray({
|
||||
control: variablesForm.control,
|
||||
name: `variables.${input.key}`
|
||||
});
|
||||
const {
|
||||
File,
|
||||
fileList,
|
||||
selectFileIcon,
|
||||
uploadFiles,
|
||||
onOpenSelectFile,
|
||||
onSelectFile,
|
||||
removeFiles,
|
||||
replaceFiles,
|
||||
hasFileUploading
|
||||
} = useFileUpload({
|
||||
outLinkAuthData,
|
||||
chatId: chatId || '',
|
||||
fileSelectConfig: {
|
||||
canSelectFile: input.canSelectFile ?? true,
|
||||
canSelectImg: input.canSelectImg ?? false,
|
||||
maxFiles: input.maxFiles ?? 5
|
||||
},
|
||||
// @ts-ignore
|
||||
fileCtrl
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!Array.isArray(value)) {
|
||||
replaceFiles([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// compare file names and update if different
|
||||
const valueFileNames = value.map((item) => item.name);
|
||||
const currentFileNames = fileList.map((item) => item.name);
|
||||
if (!isEqual(valueFileNames, currentFileNames)) {
|
||||
replaceFiles(value);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value]);
|
||||
|
||||
const isDisabledInput = histories.length > 0;
|
||||
useRequest2(uploadFiles, {
|
||||
manual: false,
|
||||
errorToast: t('common:upload_file_error'),
|
||||
refreshDeps: [fileList, outLinkAuthData, chatId]
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setUploading(hasFileUploading);
|
||||
onChange(
|
||||
fileList.map((item) => ({
|
||||
type: item.type,
|
||||
name: item.name,
|
||||
url: item.url,
|
||||
icon: item.icon
|
||||
}))
|
||||
);
|
||||
}, [fileList, hasFileUploading, onChange, setUploading]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box position={'relative'}>
|
||||
{input.required && (
|
||||
<Box position={'absolute'} left={-2} top={'-1px'} color={'red.600'}>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
<FormLabel fontWeight={'500'}>{t(input.label as any)}</FormLabel>
|
||||
</Box>
|
||||
{input.description && <QuestionTip ml={2} label={t(input.description as any)} />}
|
||||
<Box flex={1} />
|
||||
{/* 有历史记录,说明是已经跑过了,不能再新增了 */}
|
||||
<Button
|
||||
isDisabled={histories.length !== 0}
|
||||
leftIcon={<MyIcon name={selectFileIcon as any} w={'16px'} />}
|
||||
variant={'whiteBase'}
|
||||
onClick={() => {
|
||||
onOpenSelectFile();
|
||||
}}
|
||||
>
|
||||
{t('chat:select')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<FilePreview fileList={fileList} removeFiles={isDisabledInput ? undefined : removeFiles} />
|
||||
{fileList.length === 0 && <EmptyTip py={0} mt={3} text={t('chat:not_select_file')} />}
|
||||
|
||||
<File onSelect={(files) => onSelectFile({ files })} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const RenderPluginInput = ({
|
||||
value,
|
||||
onChange,
|
||||
isDisabled,
|
||||
isInvalid,
|
||||
input
|
||||
input,
|
||||
setUploading
|
||||
}: {
|
||||
value: any;
|
||||
onChange: () => void;
|
||||
onChange: (...event: any[]) => void;
|
||||
isDisabled?: boolean;
|
||||
isInvalid: boolean;
|
||||
input: FlowNodeInputItemType;
|
||||
setUploading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const inputType = input.renderTypeList[0];
|
||||
@@ -44,6 +158,12 @@ const RenderPluginInput = ({
|
||||
<MySelect list={input.list} value={value} onchange={onChange} isDisabled={isDisabled} />
|
||||
);
|
||||
}
|
||||
if (inputType === FlowNodeInputTypeEnum.fileSelect) {
|
||||
return (
|
||||
<FileSelector onChange={onChange} input={input} setUploading={setUploading} value={value} />
|
||||
);
|
||||
}
|
||||
|
||||
if (input.valueType === WorkflowIOValueTypeEnum.string) {
|
||||
return (
|
||||
<Textarea
|
||||
@@ -59,20 +179,17 @@ const RenderPluginInput = ({
|
||||
}
|
||||
if (input.valueType === WorkflowIOValueTypeEnum.number) {
|
||||
return (
|
||||
<NumberInput
|
||||
<MyNumberInput
|
||||
step={1}
|
||||
min={input.min}
|
||||
max={input.max}
|
||||
bg={'myGray.50'}
|
||||
isDisabled={isDisabled}
|
||||
isInvalid={isInvalid}
|
||||
>
|
||||
<NumberInputField value={value} onChange={onChange} defaultValue={input.defaultValue} />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
defaultValue={input.defaultValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (input.valueType === WorkflowIOValueTypeEnum.boolean) {
|
||||
@@ -100,22 +217,26 @@ const RenderPluginInput = ({
|
||||
);
|
||||
})();
|
||||
|
||||
return !!render ? (
|
||||
<Box _notLast={{ mb: 4 }} px={1}>
|
||||
<Flex alignItems={'center'} mb={1}>
|
||||
<Box position={'relative'}>
|
||||
{input.required && (
|
||||
<Box position={'absolute'} left={-2} top={'-1px'} color={'red.600'}>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
{t(input.label as any)}
|
||||
</Box>
|
||||
{input.description && <QuestionTip ml={2} label={t(input.description as any)} />}
|
||||
</Flex>
|
||||
return (
|
||||
<Box _notLast={{ mb: 4 }}>
|
||||
{/* label */}
|
||||
{inputType !== FlowNodeInputTypeEnum.fileSelect && (
|
||||
<Flex alignItems={'center'} mb={1}>
|
||||
<Box position={'relative'}>
|
||||
{input.required && (
|
||||
<Box position={'absolute'} left={-2} top={'-1px'} color={'red.600'}>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
<FormLabel fontWeight={'500'}>{t(input.label as any)}</FormLabel>
|
||||
</Box>
|
||||
{input.description && <QuestionTip ml={2} label={t(input.description as any)} />}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{render}
|
||||
</Box>
|
||||
) : null;
|
||||
);
|
||||
};
|
||||
|
||||
export default RenderPluginInput;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ReactNode, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import React, { ReactNode, useCallback, useMemo, useRef } from 'react';
|
||||
import { createContext } from 'use-context-selector';
|
||||
import { PluginRunBoxProps } from './type';
|
||||
import {
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from '@fastgpt/global/core/chat/type';
|
||||
import { FieldValues, useForm } from 'react-hook-form';
|
||||
import { PluginRunBoxTabEnum } from './constants';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
@@ -16,17 +15,16 @@ import { generatingMessageProps } from '../type';
|
||||
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import { ChatBoxInputFormType, UserInputFileItemType } from '../ChatBox/type';
|
||||
import { ChatBoxInputFormType } from '../ChatBox/type';
|
||||
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
|
||||
import { getPluginRunUserQuery } from '@fastgpt/global/core/workflow/utils';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
type PluginRunContextType = OutLinkChatAuthProps &
|
||||
PluginRunBoxProps & {
|
||||
isChatting: boolean;
|
||||
onSubmit: (e: ChatBoxInputFormType, files?: UserInputFileItemType[]) => Promise<any>;
|
||||
onSubmit: (e: ChatBoxInputFormType) => Promise<any>;
|
||||
outLinkAuthData: OutLinkChatAuthProps;
|
||||
restartInputStore?: ChatBoxInputFormType;
|
||||
setRestartInputStore: React.Dispatch<React.SetStateAction<ChatBoxInputFormType | undefined>>;
|
||||
};
|
||||
|
||||
export const PluginRunContext = createContext<PluginRunContextType>({
|
||||
@@ -59,8 +57,6 @@ const PluginRunContextProvider = ({
|
||||
}: PluginRunBoxProps & { children: ReactNode }) => {
|
||||
const { pluginInputs, onStartChat, setHistories, histories, setTab } = props;
|
||||
|
||||
const [restartInputStore, setRestartInputStore] = useState<ChatBoxInputFormType>();
|
||||
|
||||
const { toast } = useToast();
|
||||
const chatController = useRef(new AbortController());
|
||||
const { t } = useTranslation();
|
||||
@@ -80,9 +76,7 @@ const PluginRunContextProvider = ({
|
||||
);
|
||||
|
||||
const variablesForm = useForm<ChatBoxInputFormType>({
|
||||
defaultValues: {
|
||||
files: []
|
||||
}
|
||||
defaultValues: {}
|
||||
});
|
||||
|
||||
const generatingMessage = useCallback(
|
||||
@@ -179,8 +173,8 @@ const PluginRunContextProvider = ({
|
||||
[histories]
|
||||
);
|
||||
|
||||
const { runAsync: onSubmit } = useRequest2(
|
||||
async (e: ChatBoxInputFormType, files?: UserInputFileItemType[]) => {
|
||||
const onSubmit = useCallback(
|
||||
async ({ variables, files }: ChatBoxInputFormType) => {
|
||||
if (!onStartChat) return;
|
||||
if (isChatting) {
|
||||
toast({
|
||||
@@ -199,7 +193,7 @@ const PluginRunContextProvider = ({
|
||||
{
|
||||
...getPluginRunUserQuery({
|
||||
pluginInputs,
|
||||
variables: e,
|
||||
variables,
|
||||
files: files as RuntimeUserPromptType['files']
|
||||
}),
|
||||
status: 'finish'
|
||||
@@ -233,12 +227,33 @@ const PluginRunContextProvider = ({
|
||||
});
|
||||
|
||||
try {
|
||||
// Remove files icon
|
||||
const formatVariables = cloneDeep(variables);
|
||||
for (const key in formatVariables) {
|
||||
if (Array.isArray(formatVariables[key])) {
|
||||
formatVariables[key].forEach((item) => {
|
||||
if (item.url && item.icon) {
|
||||
delete item.icon;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { responseData } = await onStartChat({
|
||||
messages: messages,
|
||||
messages,
|
||||
controller: chatController.current,
|
||||
generatingMessage,
|
||||
variables: e
|
||||
variables: {
|
||||
files,
|
||||
...formatVariables
|
||||
}
|
||||
});
|
||||
if (responseData?.[responseData.length - 1]?.error) {
|
||||
toast({
|
||||
title: responseData[responseData.length - 1].error?.message,
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
setHistories((state) =>
|
||||
state.map((item, index) => {
|
||||
@@ -262,7 +277,18 @@ const PluginRunContextProvider = ({
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
abortRequest,
|
||||
generatingMessage,
|
||||
isChatting,
|
||||
onStartChat,
|
||||
pluginInputs,
|
||||
setHistories,
|
||||
setTab,
|
||||
t,
|
||||
toast
|
||||
]
|
||||
);
|
||||
|
||||
const contextValue: PluginRunContextType = {
|
||||
@@ -270,9 +296,7 @@ const PluginRunContextProvider = ({
|
||||
isChatting,
|
||||
onSubmit,
|
||||
outLinkAuthData,
|
||||
variablesForm,
|
||||
restartInputStore,
|
||||
setRestartInputStore
|
||||
variablesForm
|
||||
};
|
||||
return <PluginRunContext.Provider value={contextValue}>{children}</PluginRunContext.Provider>;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
|
||||
const RenderFilePreview = ({
|
||||
fileList,
|
||||
@@ -18,13 +19,12 @@ const RenderFilePreview = ({
|
||||
|
||||
return fileList.length > 0 ? (
|
||||
<Flex
|
||||
maxH={'250px'}
|
||||
overflowY={'auto'}
|
||||
overflow={'visible'}
|
||||
wrap={'wrap'}
|
||||
pt={3}
|
||||
userSelect={'none'}
|
||||
mb={fileList.length > 0 ? 2 : 0}
|
||||
pr={0.5}
|
||||
gap={'6px'}
|
||||
>
|
||||
{fileList.map((item, index) => {
|
||||
const isFile = item.type === ChatFileTypeEnum.file;
|
||||
@@ -33,11 +33,8 @@ const RenderFilePreview = ({
|
||||
<MyBox
|
||||
key={index}
|
||||
maxW={isFile ? 56 : 14}
|
||||
w={isFile ? '50%' : '12.5%'}
|
||||
w={isFile ? 'calc(50% - 3px)' : '12.5%'}
|
||||
aspectRatio={isFile ? 4 : 1}
|
||||
pr={1.5}
|
||||
pb={1.5}
|
||||
mb={0.5}
|
||||
>
|
||||
<Box
|
||||
border={'sm'}
|
||||
@@ -74,7 +71,7 @@ const RenderFilePreview = ({
|
||||
/>
|
||||
)}
|
||||
{isImage && (
|
||||
<Image
|
||||
<MyImage
|
||||
alt={'img'}
|
||||
src={item.icon}
|
||||
w={'full'}
|
||||
|
||||
@@ -28,13 +28,24 @@ export const useChat = (params?: { chatId?: string; appId: string; type?: GetCha
|
||||
|
||||
// Reset to empty input
|
||||
const data = variablesForm.getValues();
|
||||
for (const key in data) {
|
||||
data[key] = '';
|
||||
|
||||
// Reset the old variables to empty
|
||||
const resetVariables: Record<string, any> = {};
|
||||
for (const key in data.variables) {
|
||||
resetVariables[key] = (() => {
|
||||
if (Array.isArray(data.variables[key])) {
|
||||
return [];
|
||||
}
|
||||
return '';
|
||||
})();
|
||||
}
|
||||
|
||||
variablesForm.reset({
|
||||
...data,
|
||||
...variables
|
||||
variables: {
|
||||
...resetVariables,
|
||||
...variables
|
||||
}
|
||||
});
|
||||
},
|
||||
[variablesForm]
|
||||
@@ -42,8 +53,8 @@ export const useChat = (params?: { chatId?: string; appId: string; type?: GetCha
|
||||
|
||||
const clearChatRecords = useCallback(() => {
|
||||
const data = variablesForm.getValues();
|
||||
for (const key in data) {
|
||||
variablesForm.setValue(key, '');
|
||||
for (const key in data.variables) {
|
||||
variablesForm.setValue(`variables.${key}`, '');
|
||||
}
|
||||
|
||||
ChatBoxRef.current?.restartChat?.();
|
||||
|
||||
@@ -39,6 +39,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import MyTextarea from '@/components/common/Textarea/MyTextarea';
|
||||
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
|
||||
|
||||
type props = {
|
||||
value: UserChatItemValueItemType | AIChatItemValueItemType;
|
||||
@@ -244,25 +245,15 @@ const RenderUserFormInteractive = React.memo(function RenderFormInput({
|
||||
/>
|
||||
)}
|
||||
{input.type === FlowNodeInputTypeEnum.numberInput && (
|
||||
<NumberInput
|
||||
step={1}
|
||||
<MyNumberInput
|
||||
min={input.min}
|
||||
max={input.max}
|
||||
isDisabled={interactive.params.submitted}
|
||||
bg={'white'}
|
||||
rounded={'md'}
|
||||
>
|
||||
<NumberInputField
|
||||
bg={'white'}
|
||||
{...register(input.label, {
|
||||
required: input.required
|
||||
})}
|
||||
/>
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
register={register}
|
||||
name={input.label}
|
||||
isRequired={input.required}
|
||||
/>
|
||||
)}
|
||||
{input.type === FlowNodeInputTypeEnum.select && (
|
||||
<Controller
|
||||
|
||||
@@ -31,12 +31,10 @@ type sideTabItemType = {
|
||||
/* Per response value */
|
||||
export const WholeResponseContent = ({
|
||||
activeModule,
|
||||
hideTabs,
|
||||
showDetail
|
||||
hideTabs
|
||||
}: {
|
||||
activeModule: ChatHistoryItemResType;
|
||||
hideTabs?: boolean;
|
||||
showDetail: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -233,10 +231,14 @@ export const WholeResponseContent = ({
|
||||
{activeModule.quoteList && activeModule.quoteList.length > 0 && (
|
||||
<Row
|
||||
label={t('common:core.chat.response.module quoteList')}
|
||||
rawDom={<QuoteList showDetail={showDetail} rawSearch={activeModule.quoteList} />}
|
||||
rawDom={<QuoteList canEditDataset canViewSource rawSearch={activeModule.quoteList} />}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{/* dataset concat */}
|
||||
<>
|
||||
<Row label={t('chat:response.dataset_concat_length')} value={activeModule?.concatLength} />
|
||||
</>
|
||||
{/* classify question */}
|
||||
<>
|
||||
<Row
|
||||
@@ -527,12 +529,10 @@ const SideTabItem = ({
|
||||
/* Modal main container */
|
||||
export const ResponseBox = React.memo(function ResponseBox({
|
||||
response,
|
||||
showDetail,
|
||||
hideTabs = false,
|
||||
useMobile = false
|
||||
}: {
|
||||
response: ChatHistoryItemResType[];
|
||||
showDetail: boolean;
|
||||
hideTabs?: boolean;
|
||||
useMobile?: boolean;
|
||||
}) {
|
||||
@@ -655,11 +655,7 @@ export const ResponseBox = React.memo(function ResponseBox({
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flex={'5 0 0'} w={0} height={'100%'}>
|
||||
<WholeResponseContent
|
||||
activeModule={activeModule}
|
||||
hideTabs={hideTabs}
|
||||
showDetail={showDetail}
|
||||
/>
|
||||
<WholeResponseContent activeModule={activeModule} hideTabs={hideTabs} />
|
||||
</Box>
|
||||
</Flex>
|
||||
) : (
|
||||
@@ -719,11 +715,7 @@ export const ResponseBox = React.memo(function ResponseBox({
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box flex={'1 0 0'}>
|
||||
<WholeResponseContent
|
||||
activeModule={activeModule}
|
||||
hideTabs={hideTabs}
|
||||
showDetail={showDetail}
|
||||
/>
|
||||
<WholeResponseContent activeModule={activeModule} hideTabs={hideTabs} />
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
@@ -733,15 +725,7 @@ export const ResponseBox = React.memo(function ResponseBox({
|
||||
);
|
||||
});
|
||||
|
||||
const WholeResponseModal = ({
|
||||
showDetail,
|
||||
onClose,
|
||||
dataId
|
||||
}: {
|
||||
showDetail: boolean;
|
||||
onClose: () => void;
|
||||
dataId: string;
|
||||
}) => {
|
||||
const WholeResponseModal = ({ onClose, dataId }: { onClose: () => void; dataId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { getHistoryResponseData } = useContextSelector(ChatBoxContext, (v) => v);
|
||||
@@ -770,7 +754,7 @@ const WholeResponseModal = ({
|
||||
}
|
||||
>
|
||||
{!!response?.length ? (
|
||||
<ResponseBox response={response} showDetail={showDetail} />
|
||||
<ResponseBox response={response} />
|
||||
) : (
|
||||
<EmptyTip text={t('chat:no_workflow_response')} />
|
||||
)}
|
||||
|
||||
@@ -45,11 +45,11 @@ const scoreTheme: Record<
|
||||
const QuoteItem = ({
|
||||
quoteItem,
|
||||
canViewSource,
|
||||
linkToDataset
|
||||
canEditDataset
|
||||
}: {
|
||||
quoteItem: SearchDataResponseItemType;
|
||||
canViewSource?: boolean;
|
||||
linkToDataset?: boolean;
|
||||
canEditDataset?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [editInputData, setEditInputData] = useState<{ dataId: string; collectionId: string }>();
|
||||
@@ -110,89 +110,64 @@ const QuoteItem = ({
|
||||
>
|
||||
<Flex alignItems={'center'} mb={3} flexWrap={'wrap'} gap={3}>
|
||||
{score?.primaryScore && (
|
||||
<>
|
||||
{canViewSource ? (
|
||||
<MyTooltip label={t(SearchScoreTypeMap[score.primaryScore.type]?.desc as any)}>
|
||||
<Flex
|
||||
px={'12px'}
|
||||
py={'5px'}
|
||||
borderRadius={'md'}
|
||||
color={'primary.700'}
|
||||
bg={'primary.50'}
|
||||
borderWidth={'1px'}
|
||||
borderColor={'primary.200'}
|
||||
alignItems={'center'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Box>#{score.primaryScore.index + 1}</Box>
|
||||
<Box
|
||||
borderRightColor={'primary.700'}
|
||||
borderRightWidth={'1px'}
|
||||
h={'14px'}
|
||||
mx={2}
|
||||
/>
|
||||
<Box>
|
||||
{t(SearchScoreTypeMap[score.primaryScore.type]?.label as any)}
|
||||
{SearchScoreTypeMap[score.primaryScore.type]?.showScore
|
||||
? ` ${score.primaryScore.value.toFixed(4)}`
|
||||
: ''}
|
||||
</Box>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
) : (
|
||||
<Flex
|
||||
px={'12px'}
|
||||
py={'1px'}
|
||||
mr={4}
|
||||
borderRadius={'md'}
|
||||
color={'primary.700'}
|
||||
bg={'primary.50'}
|
||||
borderWidth={'1px'}
|
||||
borderColor={'primary.200'}
|
||||
alignItems={'center'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Box>#{score.primaryScore.index + 1}</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{canViewSource &&
|
||||
score.secondaryScore.map((item, i) => (
|
||||
<MyTooltip key={item.type} label={t(SearchScoreTypeMap[item.type]?.desc as any)}>
|
||||
<Box fontSize={'xs'}>
|
||||
<Flex alignItems={'flex-start'} lineHeight={1.2} mb={1}>
|
||||
<Box
|
||||
px={'5px'}
|
||||
borderWidth={'1px'}
|
||||
borderRadius={'sm'}
|
||||
mr={'2px'}
|
||||
{...(scoreTheme[i] && scoreTheme[i])}
|
||||
>
|
||||
<Box transform={'scale(0.9)'}>#{item.index + 1}</Box>
|
||||
</Box>
|
||||
<Box transform={'scale(0.9)'}>
|
||||
{t(SearchScoreTypeMap[item.type]?.label as any)}: {item.value.toFixed(4)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box h={'4px'}>
|
||||
{SearchScoreTypeMap[item.type]?.showScore && (
|
||||
<Progress
|
||||
value={item.value * 100}
|
||||
h={'4px'}
|
||||
w={'100%'}
|
||||
size="sm"
|
||||
borderRadius={'20px'}
|
||||
{...(scoreTheme[i] && {
|
||||
colorScheme: scoreTheme[i].colorScheme
|
||||
})}
|
||||
bg="#E8EBF0"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<MyTooltip label={t(SearchScoreTypeMap[score.primaryScore.type]?.desc as any)}>
|
||||
<Flex
|
||||
px={'12px'}
|
||||
py={'5px'}
|
||||
borderRadius={'md'}
|
||||
color={'primary.700'}
|
||||
bg={'primary.50'}
|
||||
borderWidth={'1px'}
|
||||
borderColor={'primary.200'}
|
||||
alignItems={'center'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Box>#{score.primaryScore.index + 1}</Box>
|
||||
<Box borderRightColor={'primary.700'} borderRightWidth={'1px'} h={'14px'} mx={2} />
|
||||
<Box>
|
||||
{t(SearchScoreTypeMap[score.primaryScore.type]?.label as any)}
|
||||
{SearchScoreTypeMap[score.primaryScore.type]?.showScore
|
||||
? ` ${score.primaryScore.value.toFixed(4)}`
|
||||
: ''}
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
))}
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{score.secondaryScore.map((item, i) => (
|
||||
<MyTooltip key={item.type} label={t(SearchScoreTypeMap[item.type]?.desc as any)}>
|
||||
<Box fontSize={'xs'}>
|
||||
<Flex alignItems={'flex-start'} lineHeight={1.2} mb={1}>
|
||||
<Box
|
||||
px={'5px'}
|
||||
borderWidth={'1px'}
|
||||
borderRadius={'sm'}
|
||||
mr={'2px'}
|
||||
{...(scoreTheme[i] && scoreTheme[i])}
|
||||
>
|
||||
<Box transform={'scale(0.9)'}>#{item.index + 1}</Box>
|
||||
</Box>
|
||||
<Box transform={'scale(0.9)'}>
|
||||
{t(SearchScoreTypeMap[item.type]?.label as any)}: {item.value.toFixed(4)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box h={'4px'}>
|
||||
{SearchScoreTypeMap[item.type]?.showScore && (
|
||||
<Progress
|
||||
value={item.value * 100}
|
||||
h={'4px'}
|
||||
w={'100%'}
|
||||
size="sm"
|
||||
borderRadius={'20px'}
|
||||
{...(scoreTheme[i] && {
|
||||
colorScheme: scoreTheme[i].colorScheme
|
||||
})}
|
||||
bg="#E8EBF0"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
<Box flex={'1 0 0'}>
|
||||
@@ -200,73 +175,71 @@ const QuoteItem = ({
|
||||
<Box color={'myGray.600'}>{quoteItem.a}</Box>
|
||||
</Box>
|
||||
|
||||
{canViewSource && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
flexWrap={'wrap'}
|
||||
mt={3}
|
||||
gap={4}
|
||||
color={'myGray.500'}
|
||||
fontSize={'xs'}
|
||||
>
|
||||
<MyTooltip label={t('common:core.dataset.Quote Length')}>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />
|
||||
{quoteItem.q.length + (quoteItem.a?.length || 0)}
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
<RawSourceBox
|
||||
fontWeight={'bold'}
|
||||
color={'black'}
|
||||
collectionId={quoteItem.collectionId}
|
||||
sourceName={quoteItem.sourceName}
|
||||
sourceId={quoteItem.sourceId}
|
||||
canView={canViewSource}
|
||||
/>
|
||||
<Box flex={1} />
|
||||
{quoteItem.id && (
|
||||
<MyTooltip label={t('common:core.dataset.data.Edit')}>
|
||||
<Box
|
||||
className="hover-data"
|
||||
visibility={'hidden'}
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
>
|
||||
<MyIcon
|
||||
name={'edit'}
|
||||
w={['16px', '18px']}
|
||||
h={['16px', '18px']}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.600'}
|
||||
_hover={{
|
||||
color: 'primary.600'
|
||||
}}
|
||||
onClick={() =>
|
||||
setEditInputData({
|
||||
dataId: quoteItem.id,
|
||||
collectionId: quoteItem.collectionId
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{linkToDataset && (
|
||||
<Link
|
||||
as={NextLink}
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
flexWrap={'wrap'}
|
||||
mt={3}
|
||||
gap={4}
|
||||
color={'myGray.500'}
|
||||
fontSize={'xs'}
|
||||
>
|
||||
<MyTooltip label={t('common:core.dataset.Quote Length')}>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />
|
||||
{quoteItem.q.length + (quoteItem.a?.length || 0)}
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
<RawSourceBox
|
||||
fontWeight={'bold'}
|
||||
color={'black'}
|
||||
collectionId={quoteItem.collectionId}
|
||||
sourceName={quoteItem.sourceName}
|
||||
sourceId={quoteItem.sourceId}
|
||||
canView={canViewSource}
|
||||
/>
|
||||
<Box flex={1} />
|
||||
{quoteItem.id && canEditDataset && (
|
||||
<MyTooltip label={t('common:core.dataset.data.Edit')}>
|
||||
<Box
|
||||
className="hover-data"
|
||||
visibility={'hidden'}
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
color={'primary.500'}
|
||||
href={`/dataset/detail?datasetId=${quoteItem.datasetId}¤tTab=dataCard&collectionId=${quoteItem.collectionId}`}
|
||||
justifyContent={'center'}
|
||||
>
|
||||
{t('common:core.dataset.Go Dataset')}
|
||||
<MyIcon name={'common/rightArrowLight'} w={'10px'} />
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
<MyIcon
|
||||
name={'edit'}
|
||||
w={['16px', '18px']}
|
||||
h={['16px', '18px']}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.600'}
|
||||
_hover={{
|
||||
color: 'primary.600'
|
||||
}}
|
||||
onClick={() =>
|
||||
setEditInputData({
|
||||
dataId: quoteItem.id,
|
||||
collectionId: quoteItem.collectionId
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{canEditDataset && (
|
||||
<Link
|
||||
as={NextLink}
|
||||
className="hover-data"
|
||||
visibility={'hidden'}
|
||||
alignItems={'center'}
|
||||
color={'primary.500'}
|
||||
href={`/dataset/detail?datasetId=${quoteItem.datasetId}¤tTab=dataCard&collectionId=${quoteItem.collectionId}`}
|
||||
>
|
||||
{t('common:core.dataset.Go Dataset')}
|
||||
<MyIcon name={'common/rightArrowLight'} w={'10px'} />
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
</MyBox>
|
||||
|
||||
{editInputData && (
|
||||
|
||||
@@ -6,19 +6,23 @@ import { getCollectionSourceAndOpen } from '@/web/core/dataset/hooks/readCollect
|
||||
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { ShareChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
|
||||
type Props = BoxProps & {
|
||||
sourceName?: string;
|
||||
collectionId: string;
|
||||
sourceId?: string;
|
||||
canView?: boolean;
|
||||
};
|
||||
type Props = BoxProps &
|
||||
ShareChatAuthProps & {
|
||||
sourceName?: string;
|
||||
collectionId: string;
|
||||
sourceId?: string;
|
||||
canView?: boolean;
|
||||
};
|
||||
|
||||
const RawSourceBox = ({
|
||||
sourceId,
|
||||
collectionId,
|
||||
sourceName = '',
|
||||
canView = true,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
...props
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -27,7 +31,11 @@ const RawSourceBox = ({
|
||||
const canPreview = !!sourceId && canView;
|
||||
|
||||
const icon = useMemo(() => getSourceNameIcon({ sourceId, sourceName }), [sourceId, sourceName]);
|
||||
const read = getCollectionSourceAndOpen(collectionId);
|
||||
const read = getCollectionSourceAndOpen({
|
||||
collectionId,
|
||||
shareId,
|
||||
outLinkUid
|
||||
});
|
||||
|
||||
return (
|
||||
<MyTooltip
|
||||
|
||||
@@ -101,7 +101,7 @@ const LafAccountModal = ({
|
||||
<Box fontSize={'sm'} color={'myGray.500'}>
|
||||
<Box>{t('common:support.user.Laf account intro')}</Box>
|
||||
<Box textDecoration={'underline'}>
|
||||
<Link href={getDocPath('/docs/workflow/modules/laf/')} isExternal>
|
||||
<Link href={getDocPath('/docs/guide/workbench/workflow/laf/')} isExternal>
|
||||
{t('common:support.user.Laf account course')}
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
@@ -61,6 +61,7 @@ const DefaultPermissionList = ({
|
||||
}
|
||||
}}
|
||||
fontSize={styles?.fontSize}
|
||||
fontWeight={styles?.fontWeight}
|
||||
/>
|
||||
</Box>
|
||||
<ConfirmModal />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getCaptchaPic } from '@/web/support/user/api';
|
||||
import { Button, Input, Image, ModalBody, ModalFooter, Skeleton } from '@chakra-ui/react';
|
||||
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
@@ -42,7 +43,7 @@ const SendCodeAuthModal = ({
|
||||
justifyContent={'center'}
|
||||
my={1}
|
||||
>
|
||||
<Image
|
||||
<MyImage
|
||||
borderRadius={'md'}
|
||||
w={'100%'}
|
||||
h={'200px'}
|
||||
|
||||
@@ -13,6 +13,7 @@ import '@/web/styles/reset.scss';
|
||||
import NextHead from '@/components/common/NextHead';
|
||||
import { ReactElement, useEffect } from 'react';
|
||||
import { NextPage } from 'next';
|
||||
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
|
||||
|
||||
type NextPageWithLayout = NextPage & {
|
||||
setLayout?: (page: ReactElement) => JSX.Element;
|
||||
@@ -49,7 +50,7 @@ function App({ Component, pageProps }: AppPropsWithLayout) {
|
||||
process.env.SYSTEM_DESCRIPTION ||
|
||||
`${title}${t('app:intro')}`
|
||||
}
|
||||
icon={feConfigs?.favicon || process.env.SYSTEM_FAVICON}
|
||||
icon={getWebReqUrl(feConfigs?.favicon || process.env.SYSTEM_FAVICON)}
|
||||
/>
|
||||
{scripts?.map((item, i) => <Script key={i} strategy="lazyOnload" {...item}></Script>)}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Link,
|
||||
Progress,
|
||||
Grid,
|
||||
Image,
|
||||
BoxProps
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@@ -45,6 +44,7 @@ import StandardPlanContentList from '@/components/support/wallet/StandardPlanCon
|
||||
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
|
||||
const StandDetailModal = dynamic(() => import('./standardDetailModal'));
|
||||
const TeamMenu = dynamic(() => import('@/components/support/user/team/TeamMenu'));
|
||||
@@ -653,7 +653,7 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
onClick={onOpenLaf}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Image src="/imgs/workflow/laf.png" w={'18px'} alt="laf" />
|
||||
<MyImage src="/imgs/workflow/laf.png" w={'18px'} alt="laf" />
|
||||
<Box ml={2} flex={1}>
|
||||
{'laf' + t('common:navbar.Account')}
|
||||
</Box>
|
||||
|
||||
@@ -43,7 +43,7 @@ const InformTable = () => {
|
||||
<Flex alignItems={'center'}>
|
||||
<Box fontWeight={'bold'}>{item.title}</Box>
|
||||
<Box ml={2} color={'myGray.500'} flex={'1 0 0'}>
|
||||
({formatTimeToChatTime(item.time)})
|
||||
({t(formatTimeToChatTime(item.time) as any).replace('#', ':')})
|
||||
</Box>
|
||||
{!item.read && (
|
||||
<Button
|
||||
|
||||
@@ -13,6 +13,7 @@ import { serviceSideProps } from '@/web/common/utils/i18n';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Script from 'next/script';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
|
||||
|
||||
const Promotion = dynamic(() => import('./components/Promotion'));
|
||||
const UsageTable = dynamic(() => import('./components/UsageTable'));
|
||||
@@ -128,7 +129,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script src="/js/qrcode.min.js" strategy="lazyOnload"></Script>
|
||||
<Script src={getWebReqUrl('/js/qrcode.min.js')} strategy="lazyOnload"></Script>
|
||||
<PageContainer>
|
||||
<Flex flexDirection={['column', 'row']} h={'100%'} pt={[4, 0]}>
|
||||
{isPc ? (
|
||||
|
||||
@@ -17,16 +17,8 @@ async function handler(
|
||||
req: ApiRequestProps<getHistoriesBody, getHistoriesQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<PaginationResponse<getHistoriesResponse>> {
|
||||
const {
|
||||
appId,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
teamId,
|
||||
teamToken,
|
||||
offset,
|
||||
pageSize,
|
||||
source = ChatSourceEnum.online
|
||||
} = req.body as getHistoriesBody;
|
||||
const { appId, shareId, outLinkUid, teamId, teamToken, offset, pageSize, source } =
|
||||
req.body as getHistoriesBody;
|
||||
|
||||
const match = await (async () => {
|
||||
if (shareId && outLinkUid) {
|
||||
@@ -35,7 +27,6 @@ async function handler(
|
||||
return {
|
||||
shareId,
|
||||
outLinkUid: uid,
|
||||
source: ChatSourceEnum.share,
|
||||
updateTime: {
|
||||
$gte: new Date(new Date().setDate(new Date().getDate() - 30))
|
||||
}
|
||||
@@ -55,7 +46,7 @@ async function handler(
|
||||
return {
|
||||
tmbId,
|
||||
appId,
|
||||
source: source
|
||||
source
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getChatItems } from '@fastgpt/service/core/chat/controller';
|
||||
import { authChatCrud } from '@/service/support/permission/auth/chat';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
|
||||
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { filterPublicNodeResponseData } from '@fastgpt/global/core/chat/utils';
|
||||
import { authOutLink } from '@/service/support/permission/auth/outLink';
|
||||
import { GetChatTypeEnum } from '@/global/core/chat/constants';
|
||||
@@ -67,13 +67,11 @@ async function handler(
|
||||
})();
|
||||
|
||||
const fieldMap = {
|
||||
[GetChatTypeEnum.normal]: `dataId obj value adminFeedback userBadFeedback userGoodFeedback ${
|
||||
[GetChatTypeEnum.normal]: `dataId obj value adminFeedback userBadFeedback userGoodFeedback time ${
|
||||
DispatchNodeResponseKeyEnum.nodeResponse
|
||||
} ${loadCustomFeedbacks ? 'customFeedbacks' : ''}`,
|
||||
[GetChatTypeEnum.outLink]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback ${
|
||||
shareChat?.responseDetail || isPlugin ? `${DispatchNodeResponseKeyEnum.nodeResponse}` : ''
|
||||
} `,
|
||||
[GetChatTypeEnum.team]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}`
|
||||
[GetChatTypeEnum.outLink]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback time ${DispatchNodeResponseKeyEnum.nodeResponse}`,
|
||||
[GetChatTypeEnum.team]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback time ${DispatchNodeResponseKeyEnum.nodeResponse}`
|
||||
};
|
||||
|
||||
const { total, histories } = await getChatItems({
|
||||
@@ -85,10 +83,14 @@ async function handler(
|
||||
});
|
||||
|
||||
// Remove important information
|
||||
if (type === 'outLink' && app.type !== AppTypeEnum.plugin) {
|
||||
if (shareChat && app.type !== AppTypeEnum.plugin) {
|
||||
histories.forEach((item) => {
|
||||
if (item.obj === ChatRoleEnum.AI) {
|
||||
item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData });
|
||||
|
||||
if (shareChat.showNodeStatus === false) {
|
||||
item.value = item.value.filter((v) => v.type !== ChatItemValueTypeEnum.tool);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import type { InitChatResponse, InitOutLinkChatProps } from '@/global/core/chat/api.d';
|
||||
import { getGuideModule, getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
|
||||
import { getChatModelNameListByModules } from '@/service/core/app/workflow';
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
import { authOutLink } from '@/service/support/permission/auth/outLink';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
@@ -54,7 +53,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
storeWelcomeText: chat?.welcomeText,
|
||||
isPublicFetch: false
|
||||
}),
|
||||
chatModels: getChatModelNameListByModules(nodes),
|
||||
name: app.name,
|
||||
avatar: app.avatar,
|
||||
intro: app.intro,
|
||||
|
||||
@@ -5,12 +5,13 @@ import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constant
|
||||
import { createFileToken } from '@fastgpt/service/support/permission/controller';
|
||||
import { BucketNameEnum, ReadFileBaseUrl } from '@fastgpt/global/common/file/constants';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { ShareChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
|
||||
export type readCollectionSourceQuery = {
|
||||
export type readCollectionSourceQuery = {};
|
||||
|
||||
export type readCollectionSourceBody = {
|
||||
collectionId: string;
|
||||
};
|
||||
|
||||
export type readCollectionSourceBody = {};
|
||||
} & ShareChatAuthProps;
|
||||
|
||||
export type readCollectionSourceResponse = {
|
||||
type: 'url';
|
||||
@@ -24,7 +25,7 @@ async function handler(
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
collectionId: req.query.collectionId,
|
||||
collectionId: req.body.collectionId,
|
||||
per: ReadPermissionVal
|
||||
});
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export type OutLinkUpdateResponse = {};
|
||||
async function handler(
|
||||
req: ApiRequestProps<OutLinkUpdateBody, OutLinkUpdateQuery>
|
||||
): Promise<OutLinkUpdateResponse> {
|
||||
const { _id, name, responseDetail, limit, app } = req.body;
|
||||
const { _id, name, responseDetail, limit, app, showRawSource, showNodeStatus } = req.body;
|
||||
|
||||
if (!_id) {
|
||||
return Promise.reject(CommonErrEnum.missingParams);
|
||||
@@ -35,6 +35,8 @@ async function handler(
|
||||
await MongoOutLink.findByIdAndUpdate(_id, {
|
||||
name,
|
||||
responseDetail,
|
||||
showRawSource,
|
||||
showNodeStatus,
|
||||
limit,
|
||||
app
|
||||
});
|
||||
|
||||
@@ -63,6 +63,8 @@ import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/u
|
||||
type FastGptWebChatProps = {
|
||||
chatId?: string; // undefined: get histories from messages, '': new chat, 'xxxxx': get histories from db
|
||||
appId?: string;
|
||||
customUid?: string; // non-undefined: will be the priority provider for the logger.
|
||||
metadata?: Record<string, any>;
|
||||
};
|
||||
|
||||
export type Props = ChatCompletionCreateParams &
|
||||
@@ -81,10 +83,12 @@ type AuthResponseType = {
|
||||
user: UserModelSchema;
|
||||
app: AppSchema;
|
||||
responseDetail?: boolean;
|
||||
showNodeStatus?: boolean;
|
||||
authType: `${AuthUserTypeEnum}`;
|
||||
apikey?: string;
|
||||
canWrite: boolean;
|
||||
outLinkUserId?: string;
|
||||
sourceName?: string;
|
||||
};
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -99,6 +103,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
let {
|
||||
chatId,
|
||||
appId,
|
||||
customUid,
|
||||
// share chat
|
||||
shareId,
|
||||
outLinkUid,
|
||||
@@ -110,7 +115,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
detail = false,
|
||||
messages = [],
|
||||
variables = {},
|
||||
responseChatItemId = getNanoid()
|
||||
responseChatItemId = getNanoid(),
|
||||
metadata
|
||||
} = req.body as Props;
|
||||
|
||||
const originIp = requestIp.getClientIp(req);
|
||||
@@ -122,7 +128,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
throw new Error('messages is not array');
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
Web params: chatId + [Human]
|
||||
API params: chatId + [Human]
|
||||
API params: [histories, Human]
|
||||
@@ -139,41 +145,52 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
return JSON.stringify(variables);
|
||||
})();
|
||||
|
||||
/*
|
||||
/*
|
||||
1. auth app permission
|
||||
2. auth balance
|
||||
3. get app
|
||||
4. parse outLink token
|
||||
*/
|
||||
const { teamId, tmbId, user, app, responseDetail, authType, apikey, canWrite, outLinkUserId } =
|
||||
await (async () => {
|
||||
// share chat
|
||||
if (shareId && outLinkUid) {
|
||||
return authShareChat({
|
||||
shareId,
|
||||
outLinkUid,
|
||||
chatId,
|
||||
ip: originIp,
|
||||
question: startHookText
|
||||
});
|
||||
}
|
||||
// team space chat
|
||||
if (spaceTeamId && appId && teamToken) {
|
||||
return authTeamSpaceChat({
|
||||
teamId: spaceTeamId,
|
||||
teamToken,
|
||||
appId,
|
||||
chatId
|
||||
});
|
||||
}
|
||||
|
||||
/* parse req: api or token */
|
||||
return authHeaderRequest({
|
||||
req,
|
||||
const {
|
||||
teamId,
|
||||
tmbId,
|
||||
user,
|
||||
app,
|
||||
responseDetail,
|
||||
authType,
|
||||
sourceName,
|
||||
apikey,
|
||||
canWrite,
|
||||
outLinkUserId = customUid,
|
||||
showNodeStatus
|
||||
} = await (async () => {
|
||||
// share chat
|
||||
if (shareId && outLinkUid) {
|
||||
return authShareChat({
|
||||
shareId,
|
||||
outLinkUid,
|
||||
chatId,
|
||||
ip: originIp,
|
||||
question: startHookText
|
||||
});
|
||||
}
|
||||
// team space chat
|
||||
if (spaceTeamId && appId && teamToken) {
|
||||
return authTeamSpaceChat({
|
||||
teamId: spaceTeamId,
|
||||
teamToken,
|
||||
appId,
|
||||
chatId
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
/* parse req: api or token */
|
||||
return authHeaderRequest({
|
||||
req,
|
||||
appId,
|
||||
chatId
|
||||
});
|
||||
})();
|
||||
const isPlugin = app.type === AppTypeEnum.plugin;
|
||||
|
||||
// Check message type
|
||||
@@ -241,7 +258,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
res,
|
||||
detail,
|
||||
streamResponse: stream,
|
||||
id: chatId
|
||||
id: chatId,
|
||||
showNodeStatus
|
||||
});
|
||||
|
||||
/* start flow controller */
|
||||
@@ -330,10 +348,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
newTitle,
|
||||
shareId,
|
||||
outLinkUid: outLinkUserId,
|
||||
source,
|
||||
source: sourceName || source,
|
||||
content: [userQuestion, aiResponse],
|
||||
metadata: {
|
||||
originIp
|
||||
originIp,
|
||||
...metadata
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -445,7 +464,7 @@ const authShareChat = async ({
|
||||
shareId: string;
|
||||
chatId?: string;
|
||||
}): Promise<AuthResponseType> => {
|
||||
const { teamId, tmbId, user, appId, authType, responseDetail, uid } =
|
||||
const { teamId, tmbId, user, appId, authType, responseDetail, showNodeStatus, uid, sourceName } =
|
||||
await authOutLinkChatStart(data);
|
||||
const app = await MongoApp.findById(appId).lean();
|
||||
|
||||
@@ -460,6 +479,7 @@ const authShareChat = async ({
|
||||
}
|
||||
|
||||
return {
|
||||
sourceName,
|
||||
teamId,
|
||||
tmbId,
|
||||
user,
|
||||
@@ -468,7 +488,8 @@ const authShareChat = async ({
|
||||
apikey: '',
|
||||
authType,
|
||||
canWrite: false,
|
||||
outLinkUserId: uid
|
||||
outLinkUserId: uid,
|
||||
showNodeStatus
|
||||
};
|
||||
};
|
||||
const authTeamSpaceChat = async ({
|
||||
@@ -527,6 +548,7 @@ const authHeaderRequest = async ({
|
||||
teamId,
|
||||
tmbId,
|
||||
authType,
|
||||
sourceName,
|
||||
apikey
|
||||
} = await authCert({
|
||||
req,
|
||||
@@ -593,6 +615,7 @@ const authHeaderRequest = async ({
|
||||
responseDetail: true,
|
||||
apikey,
|
||||
authType,
|
||||
sourceName,
|
||||
canWrite: true
|
||||
};
|
||||
};
|
||||
|
||||
@@ -180,6 +180,9 @@ const DetailLogsModal = ({
|
||||
chatConfig={chat?.app?.chatConfig}
|
||||
appId={appId}
|
||||
chatId={chatId}
|
||||
chatType="log"
|
||||
showRawSource
|
||||
showNodeStatus
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -129,15 +129,16 @@ const Logs = () => {
|
||||
onClick={() => setDetailLogsId(item.id)}
|
||||
>
|
||||
<Td>
|
||||
<Box>{t(ChatSourceMap[item.source]?.name || ('UnKnow' as any))}</Box>
|
||||
{/* @ts-ignore */}
|
||||
<Box>{t(ChatSourceMap[item.source]?.name) || item.source}</Box>
|
||||
<Box color={'myGray.500'}>{dayjs(item.time).format('YYYY/MM/DD HH:mm')}</Box>
|
||||
</Td>
|
||||
<Td>
|
||||
<Box>
|
||||
{item.source === 'share' ? (
|
||||
{!!item.outLinkUid ? (
|
||||
item.outLinkUid
|
||||
) : (
|
||||
<Tag key={item._id} type={'fill'} colorSchema="white">
|
||||
<HStack>
|
||||
<Avatar
|
||||
src={teamMembers.find((v) => v.tmbId === item.tmbId)?.avatar}
|
||||
w="1.25rem"
|
||||
@@ -145,7 +146,7 @@ const Logs = () => {
|
||||
<Box fontSize={'sm'} ml={1}>
|
||||
{teamMembers.find((v) => v.tmbId === item.tmbId)?.memberName}
|
||||
</Box>
|
||||
</Tag>
|
||||
</HStack>
|
||||
)}
|
||||
</Box>
|
||||
</Td>
|
||||
|
||||
@@ -29,6 +29,11 @@ import { useDebounceEffect } from 'ahooks';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import SaveButton from '../Workflow/components/SaveButton';
|
||||
import PublishHistories from '../PublishHistoriesSlider';
|
||||
import {
|
||||
WorkflowNodeEdgeContext,
|
||||
WorkflowInitContext
|
||||
} from '../WorkflowComponents/context/workflowInitContext';
|
||||
import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext';
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -44,27 +49,31 @@ const Header = () => {
|
||||
onClose: onCloseBackConfirm
|
||||
} = useDisclosure();
|
||||
|
||||
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
|
||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
||||
const {
|
||||
flowData2StoreData,
|
||||
flowData2StoreDataAndCheck,
|
||||
setWorkflowTestData,
|
||||
setShowHistoryModal,
|
||||
showHistoryModal,
|
||||
nodes,
|
||||
edges,
|
||||
past,
|
||||
future,
|
||||
setPast,
|
||||
onSwitchTmpVersion,
|
||||
onSwitchCloudVersion
|
||||
} = useContextSelector(WorkflowContext, (v) => v);
|
||||
const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal);
|
||||
const setShowHistoryModal = useContextSelector(
|
||||
WorkflowEventContext,
|
||||
(v) => v.setShowHistoryModal
|
||||
);
|
||||
|
||||
const { lastAppListRouteType } = useSystemStore();
|
||||
|
||||
const [isPublished, setIsPublished] = useState(false);
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
const savedSnapshot =
|
||||
future.findLast((snapshot) => snapshot.isSaved) ||
|
||||
[...future].reverse().find((snapshot) => snapshot.isSaved) ||
|
||||
past.find((snapshot) => snapshot.isSaved);
|
||||
|
||||
const val = compareSnapshot(
|
||||
@@ -145,7 +154,6 @@ const Header = () => {
|
||||
)}
|
||||
<Flex
|
||||
mt={[2, 0]}
|
||||
py={3}
|
||||
pl={[2, 4]}
|
||||
pr={[2, 6]}
|
||||
borderBottom={'base'}
|
||||
@@ -163,12 +171,20 @@ const Header = () => {
|
||||
})}
|
||||
>
|
||||
{/* back */}
|
||||
<MyIcon
|
||||
name={'common/leftArrowLight'}
|
||||
w={'1.75rem'}
|
||||
cursor={'pointer'}
|
||||
onClick={isPublished ? onBack : onOpenBackConfirm}
|
||||
/>
|
||||
<Box
|
||||
_hover={{
|
||||
bg: 'myGray.200'
|
||||
}}
|
||||
p={0.5}
|
||||
borderRadius={'sm'}
|
||||
>
|
||||
<MyIcon
|
||||
name={'common/leftArrowLight'}
|
||||
w={6}
|
||||
cursor={'pointer'}
|
||||
onClick={isPublished ? onBack : onOpenBackConfirm}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* app info */}
|
||||
<Box ml={1}>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { pluginSystemModuleTemplates } from '@fastgpt/global/core/workflow/template/constants';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
|
||||
import WorkflowContextProvider, { WorkflowContext } from '../WorkflowComponents/context';
|
||||
import { ReactFlowCustomProvider, WorkflowContext } from '../WorkflowComponents/context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext, TabEnum } from '../context';
|
||||
import { useMount } from 'ahooks';
|
||||
@@ -13,13 +13,15 @@ import dynamic from 'next/dynamic';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import Flow from '../WorkflowComponents/Flow';
|
||||
import { t } from 'i18next';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
const Logs = dynamic(() => import('../Logs/index'));
|
||||
const PublishChannel = dynamic(() => import('../Publish'));
|
||||
|
||||
const WorkflowEdit = () => {
|
||||
const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e);
|
||||
const isV2Workflow = appDetail?.version === 'v2';
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
showCancel: false,
|
||||
@@ -64,9 +66,9 @@ const WorkflowEdit = () => {
|
||||
|
||||
const Render = () => {
|
||||
return (
|
||||
<WorkflowContextProvider basicNodeTemplates={pluginSystemModuleTemplates}>
|
||||
<ReactFlowCustomProvider templates={pluginSystemModuleTemplates}>
|
||||
<WorkflowEdit />
|
||||
</WorkflowContextProvider>
|
||||
</ReactFlowCustomProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -90,7 +90,10 @@ const FeiShuEditModal = ({
|
||||
<Box color="myGray.600">{t('publish:feishu_api')}</Box>
|
||||
{feConfigs?.docUrl && (
|
||||
<Link
|
||||
href={feConfigs.openAPIDocUrl || getDocPath('/docs/course/feishu')}
|
||||
href={
|
||||
feConfigs.openAPIDocUrl ||
|
||||
getDocPath('/docs/use-cases/external-integration/feishu/')
|
||||
}
|
||||
target={'_blank'}
|
||||
ml={2}
|
||||
color={'primary.500'}
|
||||
|
||||
@@ -73,7 +73,10 @@ const FeiShu = ({ appId }: { appId: string }) => {
|
||||
</Box>
|
||||
{feConfigs?.docUrl && (
|
||||
<Link
|
||||
href={feConfigs.openAPIDocUrl || getDocPath('/docs/course/feishu')}
|
||||
href={
|
||||
feConfigs.openAPIDocUrl ||
|
||||
getDocPath('/docs/use-cases/external-integration/feishu/')
|
||||
}
|
||||
target={'_blank'}
|
||||
color={'primary.500'}
|
||||
fontSize={'sm'}
|
||||
@@ -147,7 +150,7 @@ const FeiShu = ({ appId }: { appId: string }) => {
|
||||
)}
|
||||
<Td>
|
||||
{item.lastTime
|
||||
? t(formatTimeToChatTime(item.lastTime) as any)
|
||||
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
|
||||
: t('common:common.Un used')}
|
||||
</Td>
|
||||
<Td display={'flex'} alignItems={'center'}>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { OutLinkSchema } from '@fastgpt/global/support/outLink/type';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Flex, FlexProps, Grid, Image, ModalBody, Switch, useTheme } from '@chakra-ui/react';
|
||||
import { Box, Flex, FlexProps, Grid, ModalBody, Switch, useTheme } from '@chakra-ui/react';
|
||||
import MyRadio from '@/components/common/MyRadio';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
@@ -10,6 +10,7 @@ import { useCopyData } from '@/web/common/hooks/useCopyData';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { fileToBase64 } from '@/web/common/file/utils';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
|
||||
enum UsingWayEnum {
|
||||
link = 'link',
|
||||
@@ -29,15 +30,15 @@ const SelectUsingWayModal = ({ share, onClose }: { share: OutLinkSchema; onClose
|
||||
|
||||
const VariableTypeList = [
|
||||
{
|
||||
title: <Image src={'/imgs/outlink/link.svg'} alt={''} />,
|
||||
title: <MyImage src={'/imgs/outlink/link.svg'} alt={''} />,
|
||||
value: UsingWayEnum.link
|
||||
},
|
||||
{
|
||||
title: <Image src={'/imgs/outlink/iframe.svg'} alt={''} />,
|
||||
title: <MyImage src={'/imgs/outlink/iframe.svg'} alt={''} />,
|
||||
value: UsingWayEnum.iframe
|
||||
},
|
||||
{
|
||||
title: <Image src={'/imgs/outlink/script.svg'} alt={''} />,
|
||||
title: <MyImage src={'/imgs/outlink/script.svg'} alt={''} />,
|
||||
value: UsingWayEnum.script
|
||||
}
|
||||
];
|
||||
@@ -162,7 +163,7 @@ console.log("Chat box loaded")
|
||||
</Flex>
|
||||
<Flex {...gridItemStyle}>
|
||||
<Box flex={1}>{t('common:core.app.outLink.Script Open Icon')}</Box>
|
||||
<Image
|
||||
<MyImage
|
||||
src={getValues('scriptOpenIcon')}
|
||||
alt={''}
|
||||
w={'20px'}
|
||||
@@ -173,7 +174,7 @@ console.log("Chat box loaded")
|
||||
</Flex>
|
||||
<Flex {...gridItemStyle}>
|
||||
<Box flex={1}>{t('common:core.app.outLink.Script Close Icon')}</Box>
|
||||
<Image
|
||||
<MyImage
|
||||
src={getValues('scriptCloseIcon')}
|
||||
alt={''}
|
||||
w={'20px'}
|
||||
|
||||
@@ -32,7 +32,6 @@ import { useCopyData } from '@/web/common/hooks/useCopyData';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { defaultOutLinkForm } from '@/web/core/app/constants';
|
||||
import type { OutLinkEditType, OutLinkSchema } from '@fastgpt/global/support/outLink/type.d';
|
||||
import { useRequest } from '@/web/common/hooks/useRequest';
|
||||
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
@@ -48,6 +47,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
|
||||
const SelectUsingWayModal = dynamic(() => import('./SelectUsingWayModal'));
|
||||
|
||||
@@ -150,7 +150,9 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
|
||||
</>
|
||||
)}
|
||||
<Td>
|
||||
{item.lastTime ? formatTimeToChatTime(item.lastTime) : t('common:common.Un used')}
|
||||
{item.lastTime
|
||||
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
|
||||
: t('common:common.Un used')}
|
||||
</Td>
|
||||
<Td display={'flex'} alignItems={'center'}>
|
||||
<Button
|
||||
@@ -181,7 +183,9 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
|
||||
setEditLinkData({
|
||||
_id: item._id,
|
||||
name: item.name,
|
||||
responseDetail: item.responseDetail,
|
||||
responseDetail: item.responseDetail ?? false,
|
||||
showRawSource: item.showRawSource ?? false,
|
||||
showNodeStatus: item.showNodeStatus ?? false,
|
||||
limit: item.limit
|
||||
})
|
||||
},
|
||||
@@ -270,27 +274,30 @@ function EditLinkModal({
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
watch,
|
||||
handleSubmit: submitShareChat
|
||||
} = useForm({
|
||||
defaultValues: defaultData
|
||||
});
|
||||
|
||||
const responseDetail = watch('responseDetail');
|
||||
const showRawSource = watch('showRawSource');
|
||||
|
||||
const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
|
||||
|
||||
const { mutate: onclickCreate, isLoading: creating } = useRequest({
|
||||
mutationFn: async (e: OutLinkEditType) =>
|
||||
const { runAsync: onclickCreate, loading: creating } = useRequest2(
|
||||
async (e: OutLinkEditType) =>
|
||||
createShareChat({
|
||||
...e,
|
||||
appId,
|
||||
type
|
||||
}),
|
||||
errorToast: t('common:common.Create Failed'),
|
||||
onSuccess: onCreate
|
||||
});
|
||||
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
|
||||
mutationFn: (e: OutLinkEditType) => {
|
||||
return putShareChat(e);
|
||||
},
|
||||
{
|
||||
errorToast: t('common:common.Create Failed'),
|
||||
onSuccess: onCreate
|
||||
}
|
||||
);
|
||||
const { runAsync: onclickUpdate, loading: updating } = useRequest2(putShareChat, {
|
||||
errorToast: t('common:common.Update Failed'),
|
||||
onSuccess: onEdit
|
||||
});
|
||||
@@ -300,101 +307,133 @@ function EditLinkModal({
|
||||
isOpen={true}
|
||||
iconSrc="/imgs/modal/shareFill.svg"
|
||||
title={isEdit ? publishT('edit_link') : publishT('create_link')}
|
||||
maxW={['90vw', '700px']}
|
||||
w={'100%'}
|
||||
h={['90vh', 'auto']}
|
||||
>
|
||||
<ModalBody>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 90px'}>{t('common:Name')}</FormLabel>
|
||||
<Input
|
||||
placeholder={publishT('link_name')}
|
||||
maxLength={20}
|
||||
{...register('name', {
|
||||
required: t('common:common.name_is_empty') || 'name_is_empty'
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
{feConfigs?.isPlus && (
|
||||
<>
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<FormLabel flex={'0 0 90px'} alignItems={'center'}>
|
||||
{t('common:common.Expired Time')}
|
||||
</FormLabel>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
defaultValue={
|
||||
defaultData.limit?.expiredTime
|
||||
? dayjs(defaultData.limit?.expiredTime).format('YYYY-MM-DDTHH:mm')
|
||||
: ''
|
||||
}
|
||||
onChange={(e) => {
|
||||
setValue('limit.expiredTime', new Date(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex flex={'0 0 90px'} alignItems={'center'}>
|
||||
<FormLabel>QPM</FormLabel>
|
||||
<QuestionTip ml={1} label={publishT('qpm_tips' || '')}></QuestionTip>
|
||||
</Flex>
|
||||
<Input
|
||||
max={1000}
|
||||
{...register('limit.QPM', {
|
||||
min: 0,
|
||||
max: 1000,
|
||||
valueAsNumber: true,
|
||||
required: publishT('qpm_is_empty') || ''
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex flex={'0 0 90px'} alignItems={'center'}>
|
||||
<FormLabel>{t('common:support.outlink.Max usage points')}</FormLabel>
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('common:support.outlink.Max usage points tip')}
|
||||
></QuestionTip>
|
||||
</Flex>
|
||||
<Input
|
||||
{...register('limit.maxUsagePoints', {
|
||||
min: -1,
|
||||
max: 10000000,
|
||||
valueAsNumber: true,
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex flex={'0 0 90px'} alignItems={'center'}>
|
||||
<FormLabel>{publishT('token_auth')}</FormLabel>
|
||||
<QuestionTip ml={1} label={publishT('token_auth_tips') || ''}></QuestionTip>
|
||||
</Flex>
|
||||
<Input
|
||||
placeholder={publishT('token_auth_tips') || ''}
|
||||
fontSize={'sm'}
|
||||
{...register('limit.hookUrl')}
|
||||
/>
|
||||
</Flex>
|
||||
<Link
|
||||
href={getDocPath('/docs/development/openapi/share')}
|
||||
target={'_blank'}
|
||||
fontSize={'xs'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
{publishT('token_auth_use_cases')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex flex={'0 0 90px'} alignItems={'center'}>
|
||||
<FormLabel>{t('common:support.outlink.share.Response Quote')}</FormLabel>
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('support.outlink.share.Response Quote tips' || '')}
|
||||
></QuestionTip>
|
||||
<ModalBody
|
||||
p={6}
|
||||
display={['block', 'flex']}
|
||||
flex={['1 0 0', 'auto']}
|
||||
overflow={'auto'}
|
||||
gap={4}
|
||||
>
|
||||
<Box pr={[0, 4]} flex={1} borderRight={['0px', '1px']} borderColor={['', 'myGray.150']}>
|
||||
<Box fontSize={'sm'} fontWeight={'500'} color={'myGray.600'}>
|
||||
{t('publish:basic_info')}
|
||||
</Box>
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<FormLabel flex={'0 0 90px'}>{t('common:Name')}</FormLabel>
|
||||
<Input
|
||||
placeholder={publishT('link_name')}
|
||||
maxLength={20}
|
||||
{...register('name', {
|
||||
required: t('common:common.name_is_empty') || 'name_is_empty'
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Switch {...register('responseDetail')} />
|
||||
</Flex>
|
||||
{feConfigs?.isPlus && (
|
||||
<>
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<FormLabel flex={'0 0 90px'} alignItems={'center'}>
|
||||
{t('common:common.Expired Time')}
|
||||
</FormLabel>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
defaultValue={
|
||||
defaultData.limit?.expiredTime
|
||||
? dayjs(defaultData.limit?.expiredTime).format('YYYY-MM-DDTHH:mm')
|
||||
: ''
|
||||
}
|
||||
onChange={(e) => {
|
||||
setValue('limit.expiredTime', new Date(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex flex={'0 0 90px'} alignItems={'center'}>
|
||||
<FormLabel>QPM</FormLabel>
|
||||
<QuestionTip ml={1} label={publishT('qpm_tips' || '')}></QuestionTip>
|
||||
</Flex>
|
||||
<Input
|
||||
max={1000}
|
||||
{...register('limit.QPM', {
|
||||
min: 0,
|
||||
max: 1000,
|
||||
valueAsNumber: true,
|
||||
required: publishT('qpm_is_empty') || ''
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex flex={'0 0 90px'} alignItems={'center'}>
|
||||
<FormLabel>{t('common:support.outlink.Max usage points')}</FormLabel>
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('common:support.outlink.Max usage points tip')}
|
||||
></QuestionTip>
|
||||
</Flex>
|
||||
<Input
|
||||
{...register('limit.maxUsagePoints', {
|
||||
min: -1,
|
||||
max: 10000000,
|
||||
valueAsNumber: true,
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex flex={'0 0 90px'} alignItems={'center'}>
|
||||
<FormLabel>{publishT('token_auth')}</FormLabel>
|
||||
<QuestionTip ml={1} label={publishT('token_auth_tips') || ''}></QuestionTip>
|
||||
</Flex>
|
||||
<Input
|
||||
placeholder={publishT('token_auth_tips') || ''}
|
||||
fontSize={'sm'}
|
||||
{...register('limit.hookUrl')}
|
||||
/>
|
||||
</Flex>
|
||||
<Link
|
||||
href={getDocPath('/docs/development/openapi/share')}
|
||||
target={'_blank'}
|
||||
fontSize={'xs'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
{publishT('token_auth_use_cases')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box flex={1} pt={[6, 0]}>
|
||||
<Box fontSize={'sm'} fontWeight={'500'} color={'myGray.600'}>
|
||||
{t('publish:private_config')}
|
||||
</Box>
|
||||
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
|
||||
<FormLabel>{t('publish:show_node')}</FormLabel>
|
||||
<Switch {...register('showNodeStatus')} />
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel>{t('common:support.outlink.share.Response Quote')}</FormLabel>
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('common:support.outlink.share.Response Quote tips' || '')}
|
||||
></QuestionTip>
|
||||
</Flex>
|
||||
<Switch {...register('responseDetail')} isChecked={responseDetail} />
|
||||
</Flex>
|
||||
{/* <Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel>{t('common:support.outlink.share.show_complete_quote')}</FormLabel>
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('common:support.outlink.share.show_complete_quote_tips' || '')}
|
||||
></QuestionTip>
|
||||
</Flex>
|
||||
<Switch {...register('showRawSource')} isChecked={showRawSource} />
|
||||
</Flex> */}
|
||||
</Box>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
|
||||
@@ -96,7 +96,10 @@ const OffiAccountEditModal = ({
|
||||
<Box color="myGray.600">{t('publish:official_account.params')}</Box>
|
||||
{feConfigs?.docUrl && (
|
||||
<Link
|
||||
href={feConfigs.openAPIDocUrl || getDocPath('/docs/course/official_account')}
|
||||
href={
|
||||
feConfigs.openAPIDocUrl ||
|
||||
getDocPath('/docs/use-cases/external-integration/official_account/')
|
||||
}
|
||||
target={'_blank'}
|
||||
ml={2}
|
||||
color={'primary.500'}
|
||||
|
||||
@@ -75,7 +75,10 @@ const OffiAccount = ({ appId }: { appId: string }) => {
|
||||
|
||||
{feConfigs?.docUrl && (
|
||||
<Link
|
||||
href={feConfigs.openAPIDocUrl || getDocPath('/docs/course/official_account')}
|
||||
href={
|
||||
feConfigs.openAPIDocUrl ||
|
||||
getDocPath('/docs/use-cases/external-integration/official_account/')
|
||||
}
|
||||
target={'_blank'}
|
||||
ml={2}
|
||||
color={'primary.500'}
|
||||
@@ -150,7 +153,7 @@ const OffiAccount = ({ appId }: { appId: string }) => {
|
||||
)}
|
||||
<Td>
|
||||
{item.lastTime
|
||||
? t(formatTimeToChatTime(item.lastTime) as any)
|
||||
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
|
||||
: t('common:common.Un used')}
|
||||
</Td>
|
||||
<Td display={'flex'} alignItems={'center'}>
|
||||
|
||||
@@ -126,7 +126,7 @@ const Wecom = ({ appId }: { appId: string }) => {
|
||||
)}
|
||||
<Td>
|
||||
{item.lastTime
|
||||
? t(formatTimeToChatTime(item.lastTime) as any)
|
||||
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
|
||||
: t('common:common.Un used')}
|
||||
</Td>
|
||||
<Td display={'flex'} alignItems={'center'}>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Box, Image, Flex, ModalBody } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
|
||||
export type ShowShareLinkModalProps = {
|
||||
shareLink: string;
|
||||
@@ -40,7 +41,7 @@ function ShowShareLinkModal({ shareLink, onClose, img }: ShowShareLinkModalProps
|
||||
</Box>
|
||||
</Box>
|
||||
<Box mt="4" borderRadius="0.5rem" border="1px" borderStyle="solid" borderColor="myGray.200">
|
||||
<Image src={img} borderRadius="0.5rem" alt="" />
|
||||
<MyImage src={img} borderRadius="0.5rem" alt="" />
|
||||
</Box>
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
|
||||
@@ -51,6 +51,7 @@ const RouteTab = () => {
|
||||
px={2}
|
||||
py={0.5}
|
||||
fontWeight={'medium'}
|
||||
borderRadius={'sm'}
|
||||
{...(currentTab === tab.id
|
||||
? {
|
||||
color: 'primary.700'
|
||||
@@ -59,8 +60,7 @@ const RouteTab = () => {
|
||||
color: 'myGray.600',
|
||||
cursor: 'pointer',
|
||||
_hover: {
|
||||
bg: 'myGray.200',
|
||||
borderRadius: 'md'
|
||||
bg: 'myGray.200'
|
||||
},
|
||||
onClick: () => setCurrentTab(tab.id)
|
||||
})}
|
||||
|
||||
@@ -84,7 +84,7 @@ const AppCard = () => {
|
||||
>
|
||||
{appDetail.intro || t('common:core.app.tip.Add a intro to app')}
|
||||
</Box>
|
||||
<HStack alignItems={'flex-end'}>
|
||||
<HStack alignItems={'center'}>
|
||||
<Button
|
||||
size={['sm', 'md']}
|
||||
variant={'whitePrimary'}
|
||||
@@ -107,7 +107,7 @@ const AppCard = () => {
|
||||
<MyMenu
|
||||
Button={
|
||||
<IconButton
|
||||
variant={'whiteBase'}
|
||||
variant={'whitePrimary'}
|
||||
size={['smSquare', 'mdSquare']}
|
||||
icon={<MyIcon name={'more'} w={'1rem'} />}
|
||||
aria-label={''}
|
||||
|
||||
@@ -43,9 +43,6 @@ const TTSSelect = dynamic(() => import('@/components/core/app/TTSSelect'));
|
||||
const QGSwitch = dynamic(() => import('@/components/core/app/QGSwitch'));
|
||||
const WhisperConfig = dynamic(() => import('@/components/core/app/WhisperConfig'));
|
||||
const InputGuideConfig = dynamic(() => import('@/components/core/app/InputGuideConfig'));
|
||||
const ScheduledTriggerConfig = dynamic(
|
||||
() => import('@/components/core/app/ScheduledTriggerConfig')
|
||||
);
|
||||
const WelcomeTextConfig = dynamic(() => import('@/components/core/app/WelcomeTextConfig'));
|
||||
const FileSelectConfig = dynamic(() => import('@/components/core/app/FileSelect'));
|
||||
|
||||
@@ -455,22 +452,6 @@ const EditForm = ({
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* timer trigger */}
|
||||
<Box {...BoxStyles} borderBottom={'none'}>
|
||||
<ScheduledTriggerConfig
|
||||
value={appForm.chatConfig.scheduledTriggerConfig}
|
||||
onChange={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
scheduledTriggerConfig: e
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{isOpenDatasetSelect && (
|
||||
|
||||
@@ -387,6 +387,7 @@ const RenderList = React.memo(function RenderList({
|
||||
isInvalid={errors && Object.keys(errors).includes(input.key)}
|
||||
onChange={onChange}
|
||||
input={input}
|
||||
setUploading={() => {}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -29,6 +29,11 @@ import { useDebounceEffect } from 'ahooks';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import SaveButton from './components/SaveButton';
|
||||
import PublishHistories from '../PublishHistoriesSlider';
|
||||
import {
|
||||
WorkflowNodeEdgeContext,
|
||||
WorkflowInitContext
|
||||
} from '../WorkflowComponents/context/workflowInitContext';
|
||||
import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext';
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -48,20 +53,23 @@ const Header = () => {
|
||||
onClose: onCloseBackConfirm
|
||||
} = useDisclosure();
|
||||
|
||||
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
|
||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
||||
const {
|
||||
flowData2StoreData,
|
||||
flowData2StoreDataAndCheck,
|
||||
setWorkflowTestData,
|
||||
setShowHistoryModal,
|
||||
showHistoryModal,
|
||||
nodes,
|
||||
edges,
|
||||
past,
|
||||
future,
|
||||
setPast,
|
||||
onSwitchTmpVersion,
|
||||
onSwitchCloudVersion
|
||||
} = useContextSelector(WorkflowContext, (v) => v);
|
||||
const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal);
|
||||
const setShowHistoryModal = useContextSelector(
|
||||
WorkflowEventContext,
|
||||
(v) => v.setShowHistoryModal
|
||||
);
|
||||
|
||||
const { lastAppListRouteType } = useSystemStore();
|
||||
|
||||
@@ -70,7 +78,7 @@ const Header = () => {
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
const savedSnapshot =
|
||||
future.findLast((snapshot) => snapshot.isSaved) ||
|
||||
[...future].reverse().find((snapshot) => snapshot.isSaved) ||
|
||||
past.find((snapshot) => snapshot.isSaved);
|
||||
|
||||
const val = compareSnapshot(
|
||||
@@ -151,7 +159,6 @@ const Header = () => {
|
||||
)}
|
||||
<Flex
|
||||
mt={[2, 0]}
|
||||
py={3}
|
||||
pl={[2, 4]}
|
||||
pr={[2, 6]}
|
||||
borderBottom={'base'}
|
||||
@@ -169,12 +176,20 @@ const Header = () => {
|
||||
})}
|
||||
>
|
||||
{/* back */}
|
||||
<MyIcon
|
||||
name={'common/leftArrowLight'}
|
||||
w={'1.75rem'}
|
||||
cursor={'pointer'}
|
||||
onClick={isPublished ? onBack : onOpenBackConfirm}
|
||||
/>
|
||||
<Box
|
||||
_hover={{
|
||||
bg: 'myGray.200'
|
||||
}}
|
||||
p={0.5}
|
||||
borderRadius={'sm'}
|
||||
>
|
||||
<MyIcon
|
||||
name={'common/leftArrowLight'}
|
||||
w={6}
|
||||
cursor={'pointer'}
|
||||
onClick={isPublished ? onBack : onOpenBackConfirm}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* app info */}
|
||||
<Box ml={1}>
|
||||
|
||||
@@ -11,15 +11,18 @@ import { Flex } from '@chakra-ui/react';
|
||||
import { workflowBoxStyles } from '../constants';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import Flow from '../WorkflowComponents/Flow';
|
||||
import { t } from 'i18next';
|
||||
import { ReactFlowCustomProvider } from '../WorkflowComponents/context/index';
|
||||
|
||||
const Logs = dynamic(() => import('../Logs/index'));
|
||||
const PublishChannel = dynamic(() => import('../Publish'));
|
||||
|
||||
const WorkflowEdit = () => {
|
||||
const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e);
|
||||
const isV2Workflow = appDetail?.version === 'v2';
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
showCancel: false,
|
||||
@@ -64,9 +67,9 @@ const WorkflowEdit = () => {
|
||||
|
||||
const Render = () => {
|
||||
return (
|
||||
<WorkflowContextProvider basicNodeTemplates={appSystemModuleTemplates}>
|
||||
<ReactFlowCustomProvider templates={appSystemModuleTemplates}>
|
||||
<WorkflowEdit />
|
||||
</WorkflowContextProvider>
|
||||
</ReactFlowCustomProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,18 +4,18 @@ import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext, TabEnum } from '../context';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { WorkflowContext } from './context';
|
||||
import { filterSensitiveNodesData } from '@/web/core/workflow/utils';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useCopyData } from '@/web/common/hooks/useCopyData';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import { publishStatusStyle } from '../constants';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import { fileDownload } from '@/web/common/file/utils';
|
||||
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
|
||||
const ImportSettings = dynamic(() => import('./Flow/ImportSettings'));
|
||||
|
||||
@@ -31,83 +31,115 @@ const AppCard = ({
|
||||
|
||||
const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } =
|
||||
useContextSelector(AppContext, (v) => v);
|
||||
const { showHistoryModal } = useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
|
||||
|
||||
const InfoMenu = useCallback(
|
||||
({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<MyMenu
|
||||
width={150}
|
||||
Button={children}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'edit',
|
||||
label: t('app:edit_info'),
|
||||
onClick: onOpenInfoEdit
|
||||
},
|
||||
{
|
||||
icon: 'support/team/key',
|
||||
label: t('common:common.Role'),
|
||||
onClick: onOpenInfoEdit
|
||||
}
|
||||
]
|
||||
},
|
||||
...(!showHistoryModal && currentTab === TabEnum.appEdit
|
||||
? [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
label: t('app:import_configs'),
|
||||
icon: 'common/importLight',
|
||||
onClick: onOpenImport
|
||||
},
|
||||
{
|
||||
label: ExportPopover({
|
||||
chatConfig: appDetail.chatConfig,
|
||||
appName: appDetail.name
|
||||
}),
|
||||
menuItemStyles: {
|
||||
p: 0,
|
||||
cursor: 'default'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(appDetail.permission.hasWritePer && feConfigs?.show_team_chat
|
||||
? [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'support/team/memberLight',
|
||||
label: t('common:common.Team Tags Set'),
|
||||
onClick: onOpenTeamTagModal
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(appDetail.permission.isOwner
|
||||
? [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
type: 'danger' as 'danger',
|
||||
icon: 'delete',
|
||||
label: t('common:common.Delete'),
|
||||
onClick: onDelApp
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]}
|
||||
/>
|
||||
<MyPopover
|
||||
placement={'bottom-end'}
|
||||
hasArrow={false}
|
||||
offset={[2, 4]}
|
||||
w={'116px'}
|
||||
trigger={'hover'}
|
||||
Trigger={children}
|
||||
>
|
||||
{({ onClose }) => (
|
||||
<Box p={1.5}>
|
||||
<MyBox
|
||||
display={'flex'}
|
||||
size={'md'}
|
||||
px={1}
|
||||
py={1.5}
|
||||
rounded={'4px'}
|
||||
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
onClick={onOpenInfoEdit}
|
||||
>
|
||||
<MyIcon name={'edit'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('app:edit_info')}</Box>
|
||||
</MyBox>
|
||||
<MyBox
|
||||
display={'flex'}
|
||||
size={'md'}
|
||||
px={1}
|
||||
py={1.5}
|
||||
rounded={'4px'}
|
||||
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
onClick={onOpenInfoEdit}
|
||||
>
|
||||
<MyIcon name={'support/team/key'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('app:Role_setting')}</Box>
|
||||
</MyBox>
|
||||
<Box w={'full'} h={'1px'} bg={'myGray.200'} my={1} />
|
||||
<MyBox
|
||||
display={'flex'}
|
||||
size={'md'}
|
||||
px={1}
|
||||
py={1.5}
|
||||
rounded={'4px'}
|
||||
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
onClick={onOpenImport}
|
||||
>
|
||||
<MyIcon name={'common/importLight'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('app:import_configs')}</Box>
|
||||
</MyBox>
|
||||
<MyBox
|
||||
display={'flex'}
|
||||
size={'md'}
|
||||
px={1}
|
||||
py={1.5}
|
||||
rounded={'4px'}
|
||||
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
>
|
||||
{ExportPopover({
|
||||
chatConfig: appDetail.chatConfig,
|
||||
appName: appDetail.name
|
||||
})}
|
||||
</MyBox>
|
||||
<Box w={'full'} h={'1px'} bg={'myGray.200'} my={1} />
|
||||
{appDetail.permission.hasWritePer && feConfigs?.show_team_chat && (
|
||||
<>
|
||||
<MyBox
|
||||
display={'flex'}
|
||||
size={'md'}
|
||||
px={1}
|
||||
py={1.5}
|
||||
rounded={'4px'}
|
||||
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
onClick={onOpenTeamTagModal}
|
||||
>
|
||||
<MyIcon name={'core/dataset/tag'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('app:Team_Tags')}</Box>
|
||||
</MyBox>
|
||||
<Box w={'full'} h={'1px'} bg={'myGray.200'} my={1} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{appDetail.permission.isOwner && (
|
||||
<MyBox
|
||||
display={'flex'}
|
||||
size={'md'}
|
||||
px={1}
|
||||
py={1.5}
|
||||
rounded={'4px'}
|
||||
color={'red.600'}
|
||||
_hover={{ bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
onClick={onDelApp}
|
||||
>
|
||||
<MyIcon name={'delete'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('common:common.Delete')}</Box>
|
||||
</MyBox>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</MyPopover>
|
||||
);
|
||||
},
|
||||
[
|
||||
@@ -117,7 +149,6 @@ const AppCard = ({
|
||||
appDetail.permission.isOwner,
|
||||
currentTab,
|
||||
feConfigs?.show_team_chat,
|
||||
showHistoryModal,
|
||||
onDelApp,
|
||||
onOpenImport,
|
||||
onOpenInfoEdit,
|
||||
@@ -129,21 +160,26 @@ const AppCard = ({
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<HStack>
|
||||
<InfoMenu>
|
||||
<Avatar src={appDetail.avatar} w={'1.75rem'} borderRadius={'md'} />
|
||||
</InfoMenu>
|
||||
<Avatar src={appDetail.avatar} w={'1.75rem'} borderRadius={'md'} />
|
||||
<Box>
|
||||
<InfoMenu>
|
||||
<HStack spacing={1} cursor={'pointer'}>
|
||||
<HStack
|
||||
spacing={1}
|
||||
cursor={'pointer'}
|
||||
pl={1}
|
||||
ml={-1}
|
||||
borderRadius={'xs'}
|
||||
_hover={{ bg: 'myGray.150' }}
|
||||
>
|
||||
<Box color={'myGray.900'}>{appDetail.name}</Box>
|
||||
<MyIcon name={'common/select'} w={'1rem'} />
|
||||
<MyIcon name={'common/select'} w={'1rem'} color={'myGray.500'} />
|
||||
</HStack>
|
||||
</InfoMenu>
|
||||
{showSaveStatus && (
|
||||
<Flex alignItems={'center'} h={'20px'} fontSize={'mini'} lineHeight={1}>
|
||||
<Flex alignItems={'center'} fontSize={'mini'} lineHeight={1}>
|
||||
<MyTag
|
||||
py={0}
|
||||
px={0}
|
||||
px={1}
|
||||
showDot
|
||||
bg={'transparent'}
|
||||
colorSchema={
|
||||
@@ -211,15 +247,19 @@ function ExportPopover({
|
||||
return (
|
||||
<MyPopover
|
||||
placement={'right-start'}
|
||||
offset={[0, 0]}
|
||||
offset={[0, 20]}
|
||||
hasArrow
|
||||
trigger={'hover'}
|
||||
w={'8.6rem'}
|
||||
Trigger={
|
||||
<Flex align={'center'} w={'100%'} py={2} px={3}>
|
||||
<Avatar src={'export'} borderRadius={'sm'} w={'1rem'} mr={3} />
|
||||
{t('app:export_configs')}
|
||||
</Flex>
|
||||
// <Flex align={'center'} w={'100%'} py={2} px={3}>
|
||||
// <Avatar src={'export'} borderRadius={'sm'} w={'1rem'} mr={3} />
|
||||
// {t('app:export_configs')}
|
||||
// </Flex>
|
||||
<MyBox display={'flex'} size={'md'} rounded={'4px'} cursor={'pointer'}>
|
||||
<MyIcon name={'export'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('app:export_configs')}</Box>
|
||||
</MyBox>
|
||||
}
|
||||
>
|
||||
{({ onClose }) => (
|
||||
|
||||
@@ -50,6 +50,7 @@ import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart';
|
||||
import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd';
|
||||
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { WorkflowNodeEdgeContext } from '../context/workflowInitContext';
|
||||
|
||||
type ModuleTemplateListProps = {
|
||||
isOpen: boolean;
|
||||
@@ -79,10 +80,10 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
const [parentId, setParentId] = useState<ParentIdType>('');
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const { feConfigs } = useSystemStore();
|
||||
const { basicNodeTemplates, hasToolNode, nodeList, appId } = useContextSelector(
|
||||
WorkflowContext,
|
||||
(v) => v
|
||||
);
|
||||
const basicNodeTemplates = useContextSelector(WorkflowContext, (v) => v.basicNodeTemplates);
|
||||
const hasToolNode = useContextSelector(WorkflowContext, (v) => v.hasToolNode);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const appId = useContextSelector(WorkflowContext, (v) => v.appId);
|
||||
|
||||
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
|
||||
manual: !feConfigs.isPlus
|
||||
@@ -217,105 +218,120 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
}
|
||||
);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
zIndex={2}
|
||||
display={isOpen ? 'block' : 'none'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
w={`${sliderWidth}px`}
|
||||
maxW={'100%'}
|
||||
onClick={onClose}
|
||||
fontSize={'sm'}
|
||||
/>
|
||||
<MyBox
|
||||
isLoading={isLoading}
|
||||
display={'flex'}
|
||||
zIndex={3}
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={'10px'}
|
||||
left={0}
|
||||
pt={'20px'}
|
||||
pb={4}
|
||||
h={isOpen ? 'calc(100% - 20px)' : '0'}
|
||||
w={isOpen ? ['100%', `${sliderWidth}px`] : '0'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'0 20px 20px 0'}
|
||||
transition={'.2s ease'}
|
||||
userSelect={'none'}
|
||||
overflow={isOpen ? 'none' : 'hidden'}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box px={'5'} mb={3} whiteSpace={'nowrap'} overflow={'hidden'}>
|
||||
{/* Tabs */}
|
||||
<Flex flex={'1 0 0'} alignItems={'center'} gap={3}>
|
||||
<Box flex={'1 0 0'}>
|
||||
<FillRowTabs
|
||||
list={[
|
||||
{
|
||||
icon: 'core/modules/basicNode',
|
||||
label: t('common:core.module.template.Basic Node'),
|
||||
value: TemplateTypeEnum.basic
|
||||
},
|
||||
{
|
||||
icon: 'core/modules/systemPlugin',
|
||||
label: t('common:core.module.template.System Plugin'),
|
||||
value: TemplateTypeEnum.systemPlugin
|
||||
},
|
||||
{
|
||||
icon: 'core/modules/teamPlugin',
|
||||
label: t('common:core.module.template.Team app'),
|
||||
value: TemplateTypeEnum.teamPlugin
|
||||
}
|
||||
]}
|
||||
width={'100%'}
|
||||
py={'5px'}
|
||||
value={templateType}
|
||||
onChange={(e) => {
|
||||
loadNodeTemplates({
|
||||
type: e as TemplateTypeEnum,
|
||||
parentId: ''
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{/* close icon */}
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.700'} />}
|
||||
borderColor={'myGray.300'}
|
||||
variant={'grayBase'}
|
||||
aria-label={''}
|
||||
onClick={onClose}
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
zIndex={2}
|
||||
display={isOpen ? 'block' : 'none'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
w={`${sliderWidth}px`}
|
||||
maxW={'100%'}
|
||||
onClick={onClose}
|
||||
fontSize={'sm'}
|
||||
/>
|
||||
<MyBox
|
||||
isLoading={isLoading}
|
||||
display={'flex'}
|
||||
zIndex={3}
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={'10px'}
|
||||
left={0}
|
||||
pt={'20px'}
|
||||
pb={4}
|
||||
h={isOpen ? 'calc(100% - 20px)' : '0'}
|
||||
w={isOpen ? ['100%', `${sliderWidth}px`] : '0'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'0 20px 20px 0'}
|
||||
transition={'.2s ease'}
|
||||
userSelect={'none'}
|
||||
overflow={isOpen ? 'none' : 'hidden'}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box px={'5'} mb={3} whiteSpace={'nowrap'} overflow={'hidden'}>
|
||||
{/* Tabs */}
|
||||
<Flex flex={'1 0 0'} alignItems={'center'} gap={3}>
|
||||
<Box flex={'1 0 0'}>
|
||||
<FillRowTabs
|
||||
list={[
|
||||
{
|
||||
icon: 'core/modules/basicNode',
|
||||
label: t('common:core.module.template.Basic Node'),
|
||||
value: TemplateTypeEnum.basic
|
||||
},
|
||||
{
|
||||
icon: 'core/modules/systemPlugin',
|
||||
label: t('common:core.module.template.System Plugin'),
|
||||
value: TemplateTypeEnum.systemPlugin
|
||||
},
|
||||
{
|
||||
icon: 'core/modules/teamPlugin',
|
||||
label: t('common:core.module.template.Team app'),
|
||||
value: TemplateTypeEnum.teamPlugin
|
||||
}
|
||||
]}
|
||||
width={'100%'}
|
||||
py={'5px'}
|
||||
value={templateType}
|
||||
onChange={(e) => {
|
||||
loadNodeTemplates({
|
||||
type: e as TemplateTypeEnum,
|
||||
parentId: ''
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
{/* Search */}
|
||||
{(templateType === TemplateTypeEnum.teamPlugin ||
|
||||
templateType === TemplateTypeEnum.systemPlugin) && (
|
||||
<Flex mt={2} alignItems={'center'} h={10}>
|
||||
<InputGroup mr={4} h={'full'}>
|
||||
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
|
||||
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
h={'full'}
|
||||
bg={'myGray.50'}
|
||||
placeholder={
|
||||
templateType === TemplateTypeEnum.teamPlugin
|
||||
? t('common:plugin.Search_app')
|
||||
: t('common:plugin.Search plugin')
|
||||
}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
<Box flex={1} />
|
||||
{templateType === TemplateTypeEnum.teamPlugin && (
|
||||
</Box>
|
||||
{/* close icon */}
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.700'} />}
|
||||
borderColor={'myGray.300'}
|
||||
variant={'grayBase'}
|
||||
aria-label={''}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Flex>
|
||||
{/* Search */}
|
||||
{(templateType === TemplateTypeEnum.teamPlugin ||
|
||||
templateType === TemplateTypeEnum.systemPlugin) && (
|
||||
<Flex mt={2} alignItems={'center'} h={10}>
|
||||
<InputGroup mr={4} h={'full'}>
|
||||
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
|
||||
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
h={'full'}
|
||||
bg={'myGray.50'}
|
||||
placeholder={
|
||||
templateType === TemplateTypeEnum.teamPlugin
|
||||
? t('common:plugin.Search_app')
|
||||
: t('common:plugin.Search plugin')
|
||||
}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
<Box flex={1} />
|
||||
{templateType === TemplateTypeEnum.teamPlugin && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'primary.600'
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
onClick={() => router.push('/app/list')}
|
||||
gap={1}
|
||||
>
|
||||
<Box>{t('common:create')}</Box>
|
||||
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
|
||||
</Flex>
|
||||
)}
|
||||
{templateType === TemplateTypeEnum.systemPlugin &&
|
||||
feConfigs.systemPluginCourseUrl && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
@@ -323,68 +339,35 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
color: 'primary.600'
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
onClick={() => router.push('/app/list')}
|
||||
onClick={() => window.open(feConfigs.systemPluginCourseUrl)}
|
||||
gap={1}
|
||||
>
|
||||
<Box>{t('common:create')}</Box>
|
||||
<Box>{t('common:plugin.contribute')}</Box>
|
||||
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
|
||||
</Flex>
|
||||
)}
|
||||
{templateType === TemplateTypeEnum.systemPlugin &&
|
||||
feConfigs.systemPluginCourseUrl && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'primary.600'
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
onClick={() => window.open(feConfigs.systemPluginCourseUrl)}
|
||||
gap={1}
|
||||
>
|
||||
<Box>{t('common:plugin.contribute')}</Box>
|
||||
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
{/* paths */}
|
||||
{(templateType === TemplateTypeEnum.teamPlugin ||
|
||||
templateType === TemplateTypeEnum.systemPlugin) &&
|
||||
!searchKey &&
|
||||
parentId && (
|
||||
<Flex alignItems={'center'} mt={2}>
|
||||
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
|
||||
</Flex>
|
||||
)}
|
||||
{/* paths */}
|
||||
{(templateType === TemplateTypeEnum.teamPlugin ||
|
||||
templateType === TemplateTypeEnum.systemPlugin) &&
|
||||
!searchKey &&
|
||||
parentId && (
|
||||
<Flex alignItems={'center'} mt={2}>
|
||||
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
<RenderList
|
||||
templates={templates}
|
||||
type={templateType}
|
||||
onClose={onClose}
|
||||
parentId={parentId}
|
||||
setParentId={onUpdateParentId}
|
||||
/>
|
||||
</MyBox>
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
isOpen,
|
||||
onClose,
|
||||
isLoading,
|
||||
t,
|
||||
templateType,
|
||||
feConfigs.systemPluginCourseUrl,
|
||||
searchKey,
|
||||
parentId,
|
||||
paths,
|
||||
onUpdateParentId,
|
||||
templates,
|
||||
loadNodeTemplates,
|
||||
router
|
||||
]);
|
||||
|
||||
return Render;
|
||||
</Box>
|
||||
<RenderList
|
||||
templates={templates}
|
||||
type={templateType}
|
||||
onClose={onClose}
|
||||
parentId={parentId}
|
||||
setParentId={onUpdateParentId}
|
||||
/>
|
||||
</MyBox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NodeTemplatesModal);
|
||||
@@ -403,9 +386,11 @@ const RenderList = React.memo(function RenderList({
|
||||
const isSystemPlugin = type === TemplateTypeEnum.systemPlugin;
|
||||
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
const { toast } = useToast();
|
||||
const { reactFlowWrapper, setNodes, nodeList } = useContextSelector(WorkflowContext, (v) => v);
|
||||
const { computedNewNodeName } = useWorkflowUtils();
|
||||
const { toast } = useToast();
|
||||
|
||||
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
|
||||
const formatTemplates = useMemo<NodeTemplateListType>(() => {
|
||||
const copy: NodeTemplateListType = cloneDeep(workflowNodeTemplateList);
|
||||
@@ -426,8 +411,6 @@ const RenderList = React.memo(function RenderList({
|
||||
template: NodeTemplateListItemType;
|
||||
position: XYPosition;
|
||||
}) => {
|
||||
if (!reactFlowWrapper?.current) return;
|
||||
|
||||
// Load template node
|
||||
const templateNode = await (async () => {
|
||||
try {
|
||||
@@ -465,7 +448,8 @@ const RenderList = React.memo(function RenderList({
|
||||
|
||||
// Add default values to some inputs
|
||||
const defaultValueMap: Record<string, any> = {
|
||||
[NodeInputKeyEnum.userChatInput]: undefined
|
||||
[NodeInputKeyEnum.userChatInput]: undefined,
|
||||
[NodeInputKeyEnum.fileUrlList]: undefined
|
||||
};
|
||||
nodeList.forEach((node) => {
|
||||
if (node.flowNodeType === FlowNodeTypeEnum.workflowStart) {
|
||||
@@ -473,6 +457,9 @@ const RenderList = React.memo(function RenderList({
|
||||
node.nodeId,
|
||||
NodeOutputKeyEnum.userChatInput
|
||||
];
|
||||
defaultValueMap[NodeInputKeyEnum.fileUrlList] = [
|
||||
[node.nodeId, NodeOutputKeyEnum.userFiles]
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -535,16 +522,7 @@ const RenderList = React.memo(function RenderList({
|
||||
return newState;
|
||||
});
|
||||
},
|
||||
[
|
||||
reactFlowWrapper,
|
||||
screenToFlowPosition,
|
||||
nodeList,
|
||||
computedNewNodeName,
|
||||
t,
|
||||
setNodes,
|
||||
setLoading,
|
||||
toast
|
||||
]
|
||||
[screenToFlowPosition, nodeList, computedNewNodeName, t, setNodes, setLoading, toast]
|
||||
);
|
||||
|
||||
const gridStyle = useMemo(() => {
|
||||
@@ -567,133 +545,118 @@ const RenderList = React.memo(function RenderList({
|
||||
};
|
||||
}, [type]);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return templates.length === 0 ? (
|
||||
<EmptyTip text={t('app:module.No Modules')} />
|
||||
) : (
|
||||
<Box flex={'1 0 0'} overflow={'overlay'} px={'5'}>
|
||||
<Box mx={'auto'}>
|
||||
{formatTemplates.map((item, i) => (
|
||||
<Box
|
||||
key={item.type}
|
||||
css={css({
|
||||
span: {
|
||||
display: 'block'
|
||||
}
|
||||
})}
|
||||
_notLast={{ mb: 5 }}
|
||||
>
|
||||
{item.label && formatTemplates.length > 1 && (
|
||||
<Flex>
|
||||
<Box fontSize={'sm'} mb={3} fontWeight={'500'} flex={1} color={'myGray.900'}>
|
||||
{t(item.label as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
return templates.length === 0 ? (
|
||||
<EmptyTip text={t('app:module.No Modules')} />
|
||||
) : (
|
||||
<Box flex={'1 0 0'} overflow={'overlay'} px={'5'}>
|
||||
<Box mx={'auto'}>
|
||||
{formatTemplates.map((item, i) => (
|
||||
<Box
|
||||
key={item.type}
|
||||
css={css({
|
||||
span: {
|
||||
display: 'block'
|
||||
}
|
||||
})}
|
||||
_notLast={{ mb: 5 }}
|
||||
>
|
||||
{item.label && formatTemplates.length > 1 && (
|
||||
<Flex>
|
||||
<Box fontSize={'sm'} mb={3} fontWeight={'500'} flex={1} color={'myGray.900'}>
|
||||
{t(item.label as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2}>
|
||||
{item.list.map((template) => (
|
||||
<MyTooltip
|
||||
key={template.id}
|
||||
placement={'right'}
|
||||
label={
|
||||
<Box py={2}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar
|
||||
src={template.avatar}
|
||||
w={'1.75rem'}
|
||||
objectFit={'contain'}
|
||||
borderRadius={'sm'}
|
||||
/>
|
||||
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
|
||||
{t(template.name as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
{t(template.intro as any) || t('common:core.workflow.Not intro')}
|
||||
</Box>
|
||||
{isSystemPlugin && <CostTooltip cost={template.currentCost} />}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
py={gridStyle.py}
|
||||
px={3}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
borderRadius={'sm'}
|
||||
draggable={!template.isFolder}
|
||||
onDragEnd={(e) => {
|
||||
if (e.clientX < sliderWidth) return;
|
||||
onAddNode({
|
||||
template,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (template.isFolder) {
|
||||
return setParentId(template.id);
|
||||
}
|
||||
if (isPc) {
|
||||
return onAddNode({
|
||||
template,
|
||||
position: { x: sliderWidth * 1.5, y: 200 }
|
||||
});
|
||||
}
|
||||
onAddNode({
|
||||
template,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
src={template.avatar}
|
||||
w={gridStyle.avatarSize}
|
||||
objectFit={'contain'}
|
||||
borderRadius={'sm'}
|
||||
/>
|
||||
<Box ml={3} flex={'1'}>
|
||||
<Box color={'myGray.900'} fontWeight={'500'} fontSize={'sm'} flex={'1 0 0'}>
|
||||
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2}>
|
||||
{item.list.map((template) => (
|
||||
<MyTooltip
|
||||
key={template.id}
|
||||
placement={'right'}
|
||||
label={
|
||||
<Box py={2}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar
|
||||
src={template.avatar}
|
||||
w={'1.75rem'}
|
||||
objectFit={'contain'}
|
||||
borderRadius={'sm'}
|
||||
/>
|
||||
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
|
||||
{t(template.name as any)}
|
||||
</Box>
|
||||
{gridStyle.authorInName && template.author !== undefined && (
|
||||
<Box fontSize={'xs'} mt={0.5} color={'myGray.500'}>
|
||||
{`by ${template.author || feConfigs.systemTitle}`}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
{t(template.intro as any) || t('common:core.workflow.Not intro')}
|
||||
</Box>
|
||||
|
||||
{gridStyle.authorInRight && template.authorAvatar && template.author && (
|
||||
<HStack spacing={1} maxW={'120px'}>
|
||||
<Avatar src={template.authorAvatar} w={'1rem'} borderRadius={'50%'} />
|
||||
<Box fontSize={'xs'} className="textEllipsis">
|
||||
{template.author}
|
||||
</Box>
|
||||
</HStack>
|
||||
{isSystemPlugin && <CostTooltip cost={template.currentCost} />}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
py={gridStyle.py}
|
||||
px={3}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
borderRadius={'sm'}
|
||||
draggable={!template.isFolder}
|
||||
onDragEnd={(e) => {
|
||||
if (e.clientX < sliderWidth) return;
|
||||
onAddNode({
|
||||
template,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (template.isFolder) {
|
||||
return setParentId(template.id);
|
||||
}
|
||||
if (isPc) {
|
||||
return onAddNode({
|
||||
template,
|
||||
position: { x: sliderWidth * 1.5, y: 200 }
|
||||
});
|
||||
}
|
||||
onAddNode({
|
||||
template,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
src={template.avatar}
|
||||
w={gridStyle.avatarSize}
|
||||
objectFit={'contain'}
|
||||
borderRadius={'sm'}
|
||||
/>
|
||||
<Box ml={3} flex={'1'}>
|
||||
<Box color={'myGray.900'} fontWeight={'500'} fontSize={'sm'} flex={'1 0 0'}>
|
||||
{t(template.name as any)}
|
||||
</Box>
|
||||
{gridStyle.authorInName && template.author !== undefined && (
|
||||
<Box fontSize={'xs'} mt={0.5} color={'myGray.500'}>
|
||||
{`by ${template.author || feConfigs.systemTitle}`}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}, [
|
||||
feConfigs.systemTitle,
|
||||
formatTemplates,
|
||||
gridStyle,
|
||||
isPc,
|
||||
isSystemPlugin,
|
||||
onAddNode,
|
||||
onClose,
|
||||
setParentId,
|
||||
t,
|
||||
templates.length
|
||||
]);
|
||||
</Box>
|
||||
|
||||
return Render;
|
||||
{gridStyle.authorInRight && template.authorAvatar && template.author && (
|
||||
<HStack spacing={1} maxW={'120px'}>
|
||||
<Avatar src={template.authorAvatar} w={'1rem'} borderRadius={'50%'} />
|
||||
<Box fontSize={'xs'} className="textEllipsis">
|
||||
{template.author}
|
||||
</Box>
|
||||
</HStack>
|
||||
)}
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,12 +6,15 @@ import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/w
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
import { useThrottleEffect } from 'ahooks';
|
||||
import { WorkflowNodeEdgeContext, WorkflowInitContext } from '../../context/workflowInitContext';
|
||||
import { WorkflowEventContext } from '../../context/workflowEventContext';
|
||||
|
||||
const ButtonEdge = (props: EdgeProps) => {
|
||||
const { nodes, nodeList, onEdgesChange, workflowDebugData, hoverEdgeId } = useContextSelector(
|
||||
WorkflowContext,
|
||||
(v) => v
|
||||
);
|
||||
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
|
||||
const onEdgesChange = useContextSelector(WorkflowNodeEdgeContext, (v) => v.onEdgesChange);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const workflowDebugData = useContextSelector(WorkflowContext, (v) => v.workflowDebugData);
|
||||
const hoverEdgeId = useContextSelector(WorkflowEventContext, (v) => v.hoverEdgeId);
|
||||
|
||||
const {
|
||||
id,
|
||||
@@ -31,9 +34,12 @@ const ButtonEdge = (props: EdgeProps) => {
|
||||
|
||||
// If parentNode is folded, the edge will not be displayed
|
||||
const parentNode = useMemo(() => {
|
||||
return nodeList.find(
|
||||
(node) => (node.nodeId === source || node.nodeId === target) && node.parentNodeId
|
||||
);
|
||||
for (const node of nodeList) {
|
||||
if ((node.nodeId === source || node.nodeId === target) && node.parentNodeId) {
|
||||
return nodeList.find((parent) => parent.nodeId === node.parentNodeId);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}, [nodeList, source, target]);
|
||||
|
||||
const defaultZIndex = useMemo(
|
||||
@@ -116,13 +122,13 @@ const ButtonEdge = (props: EdgeProps) => {
|
||||
(edge) => edge.sourceHandle === sourceHandleId && edge.targetHandle === targetHandleId
|
||||
);
|
||||
if (!targetEdge) {
|
||||
if (highlightEdge) return '#3370ff';
|
||||
if (highlightEdge) return '#487FFF';
|
||||
return '#94B5FF';
|
||||
}
|
||||
|
||||
// debug mode
|
||||
const colorMap = {
|
||||
[RuntimeEdgeStatusEnum.active]: '#39CC83',
|
||||
[RuntimeEdgeStatusEnum.active]: '#487FFF',
|
||||
[RuntimeEdgeStatusEnum.waiting]: '#5E8FFF',
|
||||
[RuntimeEdgeStatusEnum.skipped]: '#8A95A7'
|
||||
};
|
||||
@@ -154,10 +160,10 @@ const ButtonEdge = (props: EdgeProps) => {
|
||||
position={'absolute'}
|
||||
transform={`translate(-55%, -50%) translate(${labelX}px,${labelY}px)`}
|
||||
pointerEvents={'all'}
|
||||
w={'17px'}
|
||||
h={'17px'}
|
||||
w={'18px'}
|
||||
h={'18px'}
|
||||
bg={'white'}
|
||||
borderRadius={'17px'}
|
||||
borderRadius={'18px'}
|
||||
cursor={'pointer'}
|
||||
zIndex={defaultZIndex + 1000}
|
||||
onClick={() => onDelConnect(id)}
|
||||
|
||||
@@ -6,10 +6,8 @@ const Container = ({ children, ...props }: BoxProps) => {
|
||||
return (
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
px={4}
|
||||
mx={2}
|
||||
mb={2}
|
||||
py={'10px'}
|
||||
mx={3}
|
||||
p={4}
|
||||
position={'relative'}
|
||||
bg={'myGray.50'}
|
||||
border={'1px solid #F0F1F6'}
|
||||
|
||||
@@ -7,76 +7,76 @@ import { CommentNode } from '@fastgpt/global/core/workflow/template/system/comme
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
import { useReactFlow } from 'reactflow';
|
||||
import { WorkflowNodeEdgeContext } from '../../context/workflowInitContext';
|
||||
import { WorkflowEventContext } from '../../context/workflowEventContext';
|
||||
|
||||
type ContextMenuProps = {
|
||||
top: number;
|
||||
left: number;
|
||||
};
|
||||
|
||||
const ContextMenu = ({ top, left }: ContextMenuProps) => {
|
||||
const ContextMenu = () => {
|
||||
const { t } = useTranslation();
|
||||
const setNodes = useContextSelector(WorkflowContext, (ctx) => ctx.setNodes);
|
||||
const setMenu = useContextSelector(WorkflowContext, (ctx) => ctx.setMenu);
|
||||
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
|
||||
const menu = useContextSelector(WorkflowEventContext, (v) => v.menu);
|
||||
const setMenu = useContextSelector(WorkflowEventContext, (ctx) => ctx.setMenu);
|
||||
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
const newNode = nodeTemplate2FlowNode({
|
||||
template: CommentNode,
|
||||
position: screenToFlowPosition({ x: left, y: top }),
|
||||
position: screenToFlowPosition({ x: menu?.left ?? 0, y: menu?.top ?? 0 }),
|
||||
t
|
||||
});
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
<Box
|
||||
position="absolute"
|
||||
top={`${top - 6}px`}
|
||||
left={`${left + 10}px`}
|
||||
width={0}
|
||||
height={0}
|
||||
borderLeft="6px solid transparent"
|
||||
borderRight="6px solid transparent"
|
||||
borderBottom="6px solid white"
|
||||
zIndex={2}
|
||||
filter="drop-shadow(0px -1px 2px rgba(0, 0, 0, 0.1))"
|
||||
/>
|
||||
<Flex
|
||||
position={'absolute'}
|
||||
top={top}
|
||||
left={left}
|
||||
bg={'white'}
|
||||
w={'120px'}
|
||||
height={9}
|
||||
p={1}
|
||||
rounded={'md'}
|
||||
boxShadow={'0px 2px 4px 0px #A1A7B340'}
|
||||
className="context-menu"
|
||||
alignItems={'center'}
|
||||
color={'myGray.600'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'primary.500'
|
||||
}}
|
||||
onClick={() => {
|
||||
setMenu(null);
|
||||
setNodes((state) => {
|
||||
const newState = state
|
||||
.map((node) => ({
|
||||
...node,
|
||||
selected: false
|
||||
}))
|
||||
// @ts-ignore
|
||||
.concat(newNode);
|
||||
return newState;
|
||||
});
|
||||
}}
|
||||
zIndex={1}
|
||||
>
|
||||
<MyIcon name="comment" w={'16px'} h={'16px'} ml={1} />
|
||||
<Box fontSize={'12px'} fontWeight={'500'} ml={1.5}>
|
||||
{t('workflow:context_menu.add_comment')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
!!menu && (
|
||||
<Box position="relative">
|
||||
<Box
|
||||
position="absolute"
|
||||
top={`${menu.top - 6}px`}
|
||||
left={`${menu.left + 10}px`}
|
||||
width={0}
|
||||
height={0}
|
||||
borderLeft="6px solid transparent"
|
||||
borderRight="6px solid transparent"
|
||||
borderBottom="6px solid white"
|
||||
zIndex={2}
|
||||
filter="drop-shadow(0px -1px 2px rgba(0, 0, 0, 0.1))"
|
||||
/>
|
||||
<Flex
|
||||
position={'absolute'}
|
||||
top={menu.top}
|
||||
left={menu.left}
|
||||
bg={'white'}
|
||||
w={'120px'}
|
||||
height={9}
|
||||
p={1}
|
||||
rounded={'md'}
|
||||
boxShadow={'0px 2px 4px 0px #A1A7B340'}
|
||||
className="context-menu"
|
||||
alignItems={'center'}
|
||||
color={'myGray.600'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'primary.500'
|
||||
}}
|
||||
onClick={() => {
|
||||
setMenu(null);
|
||||
setNodes((state) => {
|
||||
const newState = state
|
||||
.map((node) => ({
|
||||
...node,
|
||||
selected: false
|
||||
}))
|
||||
// @ts-ignore
|
||||
.concat(newNode);
|
||||
return newState;
|
||||
});
|
||||
}}
|
||||
zIndex={1}
|
||||
>
|
||||
<MyIcon name="comment" w={'16px'} h={'16px'} ml={1} />
|
||||
<Box fontSize={'12px'} fontWeight={'500'} ml={1.5}>
|
||||
{t('workflow:context_menu.add_comment')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Background,
|
||||
ControlButton,
|
||||
@@ -15,8 +15,9 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import styles from './index.module.scss';
|
||||
import { maxZoom, minZoom } from '../index';
|
||||
import { maxZoom, minZoom } from '../../constants';
|
||||
import { useKeyPress } from 'ahooks';
|
||||
import { WorkflowEventContext } from '../../context/workflowEventContext';
|
||||
|
||||
const buttonStyle = {
|
||||
border: 'none',
|
||||
@@ -27,31 +28,41 @@ const buttonStyle = {
|
||||
const FlowController = React.memo(function FlowController() {
|
||||
const { fitView, zoomIn, zoomOut } = useReactFlow();
|
||||
const { zoom } = useViewport();
|
||||
const {
|
||||
undo,
|
||||
redo,
|
||||
canRedo,
|
||||
canUndo,
|
||||
workflowControlMode,
|
||||
setWorkflowControlMode,
|
||||
mouseInCanvas,
|
||||
nodeList
|
||||
} = useContextSelector(WorkflowContext, (v) => v);
|
||||
const undo = useContextSelector(WorkflowContext, (v) => v.undo);
|
||||
const redo = useContextSelector(WorkflowContext, (v) => v.redo);
|
||||
const canRedo = useContextSelector(WorkflowContext, (v) => v.canRedo);
|
||||
const canUndo = useContextSelector(WorkflowContext, (v) => v.canUndo);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const workflowControlMode = useContextSelector(
|
||||
WorkflowEventContext,
|
||||
(v) => v.workflowControlMode
|
||||
);
|
||||
const setWorkflowControlMode = useContextSelector(
|
||||
WorkflowEventContext,
|
||||
(v) => v.setWorkflowControlMode
|
||||
);
|
||||
const mouseInCanvas = useContextSelector(WorkflowEventContext, (v) => v.mouseInCanvas);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isMac = !window ? false : window.navigator.userAgent.toLocaleLowerCase().includes('mac');
|
||||
|
||||
// Controller shortcut key
|
||||
useKeyPress(['ctrl.z', 'meta.z'], (e) => {
|
||||
useKeyPress(['ctrl.z', 'meta.z', 'ctrl.shift.z', 'meta.shift.z', 'ctrl.y', 'meta.y'], (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!mouseInCanvas) return;
|
||||
undo();
|
||||
});
|
||||
useKeyPress(['ctrl.shift.z', 'meta.shift.z', 'ctrl.y', 'meta.y'], (e) => {
|
||||
if (!mouseInCanvas) return;
|
||||
redo();
|
||||
|
||||
const isRedo = (e.key.toLowerCase() === 'z' && e.shiftKey) || e.key.toLowerCase() === 'y';
|
||||
|
||||
if (isRedo) {
|
||||
redo();
|
||||
} else {
|
||||
undo();
|
||||
}
|
||||
});
|
||||
|
||||
useKeyPress(['ctrl.add', 'meta.add', 'ctrl.equalsign', 'meta.equalsign'], (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!mouseInCanvas) return;
|
||||
zoomIn();
|
||||
});
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Box, StackProps, HStack } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
const IOTitle = ({ text, ...props }: { text?: 'Input' | 'Output' | string } & StackProps) => {
|
||||
return (
|
||||
<HStack fontSize={'md'} alignItems={'center'} fontWeight={'medium'} mb={3} {...props}>
|
||||
<HStack fontSize={'md'} alignItems={'center'} fontWeight={'medium'} mb={4} {...props}>
|
||||
<Box w={'3px'} h={'14px'} borderRadius={'13px'} bg={'primary.600'} />
|
||||
<Box color={'myGray.900'}>{text}</Box>
|
||||
</HStack>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { storeNodes2RuntimeNodes } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import { useCallback, useState, useMemo, useEffect } from 'react';
|
||||
import { useCallback, useState, useMemo } from 'react';
|
||||
import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
@@ -27,13 +27,14 @@ import {
|
||||
} from '@fastgpt/global/core/workflow/constants';
|
||||
import { checkInputIsReference } from '@fastgpt/global/core/workflow/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext, getWorkflowStore } from '../../context';
|
||||
import { WorkflowContext } from '../../context';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { AppContext } from '../../../context';
|
||||
import { VariableInputItem } from '@/components/core/chat/ChatContainer/ChatBox/components/VariableInput';
|
||||
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
|
||||
import MyTextarea from '@/components/common/Textarea/MyTextarea';
|
||||
import { WorkflowNodeEdgeContext } from '../../context/workflowInitContext';
|
||||
|
||||
const MyRightDrawer = dynamic(
|
||||
() => import('@fastgpt/web/components/common/MyDrawer/MyRightDrawer')
|
||||
@@ -49,9 +50,10 @@ export const useDebug = () => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
|
||||
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
|
||||
const getNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.getNodes);
|
||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
||||
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
|
||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
||||
const onStartNodeDebug = useContextSelector(WorkflowContext, (v) => v.onStartNodeDebug);
|
||||
|
||||
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||
@@ -76,7 +78,7 @@ export const useDebug = () => {
|
||||
const [runtimeEdges, setRuntimeEdges] = useState<RuntimeEdgeItemType[]>();
|
||||
|
||||
const flowData2StoreDataAndCheck = useCallback(async () => {
|
||||
const { nodes } = await getWorkflowStore();
|
||||
const nodes = getNodes();
|
||||
|
||||
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
|
||||
if (!checkResults) {
|
||||
|
||||
@@ -5,14 +5,18 @@ import { useTranslation } from 'next-i18next';
|
||||
import { Node, useKeyPress } from 'reactflow';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext, getWorkflowStore } from '../../context';
|
||||
import { useWorkflowUtils } from './useUtils';
|
||||
import { useKeyPress as useKeyPressEffect } from 'ahooks';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { WorkflowNodeEdgeContext } from '../../context/workflowInitContext';
|
||||
import { WorkflowEventContext } from '../../context/workflowEventContext';
|
||||
|
||||
export const useKeyboard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { setNodes, mouseInCanvas } = useContextSelector(WorkflowContext, (v) => v);
|
||||
const getNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.getNodes);
|
||||
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
|
||||
const mouseInCanvas = useContextSelector(WorkflowEventContext, (v) => v.mouseInCanvas);
|
||||
|
||||
const { copyData } = useCopyData();
|
||||
const { computedNewNodeName } = useWorkflowUtils();
|
||||
|
||||
@@ -33,14 +37,14 @@ export const useKeyboard = () => {
|
||||
|
||||
const onCopy = useCallback(async () => {
|
||||
if (hasInputtingElement()) return;
|
||||
const { nodes } = await getWorkflowStore();
|
||||
const nodes = getNodes();
|
||||
|
||||
const selectedNodes = nodes.filter(
|
||||
(node) => node.selected && !node.data?.isError && node.data?.unique !== true
|
||||
);
|
||||
if (selectedNodes.length === 0) return;
|
||||
copyData(JSON.stringify(selectedNodes), t('common:core.workflow.Copy node'));
|
||||
}, [copyData, hasInputtingElement, t]);
|
||||
}, [copyData, getNodes, hasInputtingElement, t]);
|
||||
|
||||
const onParse = useCallback(async () => {
|
||||
if (hasInputtingElement()) return;
|
||||
|
||||
@@ -24,13 +24,17 @@ import { useKeyboard } from './useKeyboard';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
import { THelperLine } from '@fastgpt/global/core/workflow/type';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useDebounceEffect, useMemoizedFn } from 'ahooks';
|
||||
import {
|
||||
Input_Template_Node_Height,
|
||||
Input_Template_Node_Width
|
||||
} from '@fastgpt/global/core/workflow/template/input';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { WorkflowNodeEdgeContext, WorkflowInitContext } from '../../context/workflowInitContext';
|
||||
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||
import { AppContext } from '../../../context';
|
||||
import { WorkflowEventContext } from '../../context/workflowEventContext';
|
||||
|
||||
/*
|
||||
Compute helper lines for snapping nodes to each other
|
||||
@@ -271,26 +275,48 @@ export const useWorkflow = () => {
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { isDowningCtrl } = useKeyboard();
|
||||
const {
|
||||
setConnectingEdge,
|
||||
edges,
|
||||
nodes,
|
||||
nodeList,
|
||||
onNodesChange,
|
||||
setEdges,
|
||||
onChangeNode,
|
||||
onEdgesChange,
|
||||
setHoverEdgeId,
|
||||
setMenu
|
||||
} = useContextSelector(WorkflowContext, (v) => v);
|
||||
const appDetail = useContextSelector(AppContext, (e) => e.appDetail);
|
||||
|
||||
const nodes = useContextSelector(WorkflowInitContext, (state) => state.nodes);
|
||||
const onNodesChange = useContextSelector(WorkflowNodeEdgeContext, (state) => state.onNodesChange);
|
||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (state) => state.edges);
|
||||
const setEdges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setEdges);
|
||||
const onEdgesChange = useContextSelector(WorkflowNodeEdgeContext, (v) => v.onEdgesChange);
|
||||
const { setConnectingEdge, nodeList, onChangeNode, pushPastSnapshot } = useContextSelector(
|
||||
WorkflowContext,
|
||||
(v) => v
|
||||
);
|
||||
const setHoverEdgeId = useContextSelector(WorkflowEventContext, (v) => v.setHoverEdgeId);
|
||||
const setMenu = useContextSelector(WorkflowEventContext, (v) => v.setMenu);
|
||||
|
||||
const { getIntersectingNodes } = useReactFlow();
|
||||
const { isDowningCtrl } = useKeyboard();
|
||||
|
||||
// Loop node size and position
|
||||
const resetParentNodeSizeAndPosition = useMemoizedFn((rect: Rect, parentId: string) => {
|
||||
const width = rect.width + 110 > 900 ? rect.width + 110 : 900;
|
||||
const height = rect.height + 380 > 900 ? rect.height + 380 : 900;
|
||||
const resetParentNodeSizeAndPosition = useMemoizedFn((parentId: string) => {
|
||||
const { childNodes, loopNode } = nodes.reduce(
|
||||
(acc, node) => {
|
||||
if (node.data.parentNodeId === parentId) {
|
||||
acc.childNodes.push(node);
|
||||
}
|
||||
if (node.id === parentId) {
|
||||
acc.loopNode = node;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ childNodes: [] as Node[], loopNode: undefined as Node<FlowNodeItemType> | undefined }
|
||||
);
|
||||
|
||||
if (!loopNode) return;
|
||||
|
||||
const rect = getNodesBounds(childNodes);
|
||||
// Calculate parent node size with minimum width/height constraints
|
||||
const width = Math.max(rect.width + 80, 840);
|
||||
const height = Math.max(rect.height + 80, 600);
|
||||
|
||||
const offsetHeight =
|
||||
loopNode.data.inputs.find((input) => input.key === NodeInputKeyEnum.loopNodeInputHeight)
|
||||
?.value ?? 83;
|
||||
|
||||
// Update parentNode size and position
|
||||
onChangeNode({
|
||||
@@ -311,15 +337,14 @@ export const useWorkflow = () => {
|
||||
value: height
|
||||
}
|
||||
});
|
||||
|
||||
// Update parentNode position
|
||||
onNodesChange([
|
||||
{
|
||||
id: parentId,
|
||||
type: 'position',
|
||||
position: {
|
||||
x: rect.x - 50,
|
||||
y: rect.y - 280
|
||||
x: rect.x - 70,
|
||||
y: rect.y - offsetHeight - 240
|
||||
}
|
||||
}
|
||||
]);
|
||||
@@ -369,15 +394,6 @@ export const useWorkflow = () => {
|
||||
|
||||
// Check if a node is placed on top of a loop node
|
||||
const checkNodeOverLoopNode = useMemoizedFn((node: Node) => {
|
||||
if (!node) return;
|
||||
|
||||
// 获取所有与当前节点相交的节点
|
||||
const intersections = getIntersectingNodes(node);
|
||||
// 获取所有与当前节点相交的节点中,类型为 loop 的节点且它不能是折叠状态
|
||||
const parentNode = intersections.find(
|
||||
(item) => !item.data.isFolded && item.type === FlowNodeTypeEnum.loop
|
||||
);
|
||||
|
||||
const unSupportedTypes = [
|
||||
FlowNodeTypeEnum.workflowStart,
|
||||
FlowNodeTypeEnum.loop,
|
||||
@@ -386,7 +402,16 @@ export const useWorkflow = () => {
|
||||
FlowNodeTypeEnum.systemConfig
|
||||
];
|
||||
|
||||
if (parentNode && !node.data.parentNodeId) {
|
||||
if (!node || node.data.parentNodeId) return;
|
||||
|
||||
// 获取所有与当前节点相交的节点
|
||||
const intersections = getIntersectingNodes(node);
|
||||
// 获取所有与当前节点相交的节点中,类型为 loop 的节点且它不能是折叠状态
|
||||
const parentNode = intersections.find(
|
||||
(item) => !item.data.isFolded && item.type === FlowNodeTypeEnum.loop
|
||||
);
|
||||
|
||||
if (parentNode) {
|
||||
if (unSupportedTypes.includes(node.type as FlowNodeTypeEnum)) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
@@ -404,10 +429,6 @@ export const useWorkflow = () => {
|
||||
setEdges((state) =>
|
||||
state.filter((edge) => edge.source !== node.id && edge.target !== node.id)
|
||||
);
|
||||
|
||||
const childNodes = [...nodes.filter((n) => n.data.parentNodeId === parentNode.id), node];
|
||||
const rect = getNodesBounds(childNodes);
|
||||
resetParentNodeSizeAndPosition(rect, parentNode.id);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -462,7 +483,7 @@ export const useWorkflow = () => {
|
||||
const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId);
|
||||
checkNodeHelpLine(change, childNodes);
|
||||
|
||||
resetParentNodeSizeAndPosition(getNodesBounds(childNodes), parentId);
|
||||
resetParentNodeSizeAndPosition(parentId);
|
||||
}
|
||||
// If node is parent node, move parent node and child nodes
|
||||
else if (parentNode[node.data.flowNodeType]) {
|
||||
@@ -591,8 +612,36 @@ export const useWorkflow = () => {
|
||||
state
|
||||
)
|
||||
);
|
||||
|
||||
// Add default input
|
||||
const node = nodeList.find((n) => n.nodeId === connect.target);
|
||||
if (!node) return;
|
||||
|
||||
// 1. Add file input
|
||||
if (
|
||||
node.flowNodeType === FlowNodeTypeEnum.chatNode ||
|
||||
node.flowNodeType === FlowNodeTypeEnum.tools ||
|
||||
node.flowNodeType === FlowNodeTypeEnum.appModule
|
||||
) {
|
||||
const input = node.inputs.find((i) => i.key === NodeInputKeyEnum.fileUrlList);
|
||||
if (input && (!input?.value || input.value.length === 0)) {
|
||||
const workflowStartNode = nodeList.find(
|
||||
(n) => n.flowNodeType === FlowNodeTypeEnum.workflowStart
|
||||
);
|
||||
if (!workflowStartNode) return;
|
||||
onChangeNode({
|
||||
nodeId: node.nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.fileUrlList,
|
||||
value: {
|
||||
...input,
|
||||
value: [[workflowStartNode.nodeId, NodeOutputKeyEnum.userFiles]]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[setEdges]
|
||||
[nodeList, onChangeNode, setEdges]
|
||||
);
|
||||
const customOnConnect = useCallback(
|
||||
(connect: Connection) => {
|
||||
@@ -642,6 +691,23 @@ export const useWorkflow = () => {
|
||||
setMenu(null);
|
||||
}, [setMenu]);
|
||||
|
||||
// Watch
|
||||
// Auto save snapshot
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
if (nodes.length === 0 || !appDetail.chatConfig) return;
|
||||
|
||||
pushPastSnapshot({
|
||||
pastNodes: nodes,
|
||||
pastEdges: edges,
|
||||
customTitle: formatTime2YMDHMS(new Date()),
|
||||
chatConfig: appDetail.chatConfig
|
||||
});
|
||||
},
|
||||
[nodes, edges, appDetail.chatConfig],
|
||||
{ wait: 500 }
|
||||
);
|
||||
|
||||
return {
|
||||
handleNodesChange,
|
||||
handleEdgeChange,
|
||||
@@ -655,7 +721,8 @@ export const useWorkflow = () => {
|
||||
helperLineVertical,
|
||||
onNodeDragStop,
|
||||
onPaneContextMenu,
|
||||
onPaneClick
|
||||
onPaneClick,
|
||||
resetParentNodeSizeAndPosition
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import ReactFlow, { NodeProps, ReactFlowProvider, SelectionMode } from 'reactflow';
|
||||
import ReactFlow, { NodeProps, SelectionMode } from 'reactflow';
|
||||
import { Box, IconButton, useDisclosure } from '@chakra-ui/react';
|
||||
import { SmallCloseIcon } from '@chakra-ui/icons';
|
||||
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
@@ -11,16 +11,14 @@ import NodeTemplatesModal from './NodeTemplatesModal';
|
||||
|
||||
import 'reactflow/dist/style.css';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
|
||||
import { connectionLineStyle, defaultEdgeOptions } from '../constants';
|
||||
import { connectionLineStyle, defaultEdgeOptions, maxZoom, minZoom } from '../constants';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../context';
|
||||
import { useWorkflow } from './hooks/useWorkflow';
|
||||
import HelperLines from './components/HelperLines';
|
||||
import FlowController from './components/FlowController';
|
||||
import ContextMenu from './components/ContextMenu';
|
||||
|
||||
export const minZoom = 0.1;
|
||||
export const maxZoom = 1.5;
|
||||
import { WorkflowNodeEdgeContext, WorkflowInitContext } from '../context/workflowInitContext';
|
||||
import { WorkflowEventContext } from '../context/workflowEventContext';
|
||||
|
||||
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
|
||||
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
|
||||
@@ -66,9 +64,12 @@ const edgeTypes = {
|
||||
};
|
||||
|
||||
const Workflow = () => {
|
||||
const { nodes, edges, menu, reactFlowWrapper, workflowControlMode } = useContextSelector(
|
||||
WorkflowContext,
|
||||
(v) => v
|
||||
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
|
||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
||||
const reactFlowWrapper = useContextSelector(WorkflowEventContext, (v) => v.reactFlowWrapper);
|
||||
const workflowControlMode = useContextSelector(
|
||||
WorkflowEventContext,
|
||||
(v) => v.workflowControlMode
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -125,7 +126,7 @@ const Workflow = () => {
|
||||
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
|
||||
</>
|
||||
|
||||
{menu && <ContextMenu {...menu} />}
|
||||
<ContextMenu />
|
||||
<ReactFlow
|
||||
ref={reactFlowWrapper}
|
||||
fitView
|
||||
@@ -169,12 +170,4 @@ const Workflow = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const Render = () => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<Workflow />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Render);
|
||||
export default React.memo(Workflow);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { Background, NodeProps } from 'reactflow';
|
||||
import NodeCard from '../render/NodeCard';
|
||||
import Container from '../../components/Container';
|
||||
@@ -15,28 +15,101 @@ import RenderInput from '../render/RenderInput';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import RenderOutput from '../render/RenderOutput';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { Input_Template_Children_Node_List } from '@fastgpt/global/core/workflow/template/input';
|
||||
import {
|
||||
ArrayTypeMap,
|
||||
NodeInputKeyEnum,
|
||||
VARIABLE_NODE_ID,
|
||||
WorkflowIOValueTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/constants';
|
||||
import {
|
||||
Input_Template_Children_Node_List,
|
||||
Input_Template_LOOP_NODE_OFFSET
|
||||
} from '@fastgpt/global/core/workflow/template/input';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../../context';
|
||||
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
|
||||
import { AppContext } from '../../../../context';
|
||||
import { isValidArrayReferenceValue } from '@fastgpt/global/core/workflow/utils';
|
||||
import { ReferenceArrayValueType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { useWorkflow } from '../../hooks/useWorkflow';
|
||||
import { useSize } from 'ahooks';
|
||||
|
||||
const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { nodeId, inputs, outputs, isFolded } = data;
|
||||
const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||
|
||||
const { nodeWidth, nodeHeight } = useMemo(() => {
|
||||
const { resetParentNodeSizeAndPosition } = useWorkflow();
|
||||
|
||||
const {
|
||||
nodeWidth,
|
||||
nodeHeight,
|
||||
loopInputArray,
|
||||
loopNodeInputHeight = Input_Template_LOOP_NODE_OFFSET
|
||||
} = useMemo(() => {
|
||||
return {
|
||||
nodeWidth: inputs.find((input) => input.key === NodeInputKeyEnum.nodeWidth)?.value,
|
||||
nodeHeight: inputs.find((input) => input.key === NodeInputKeyEnum.nodeHeight)?.value
|
||||
nodeHeight: inputs.find((input) => input.key === NodeInputKeyEnum.nodeHeight)?.value,
|
||||
loopInputArray: inputs.find((input) => input.key === NodeInputKeyEnum.loopInputArray),
|
||||
loopNodeInputHeight: inputs.find(
|
||||
(input) => input.key === NodeInputKeyEnum.loopNodeInputHeight
|
||||
)
|
||||
};
|
||||
}, [inputs]);
|
||||
|
||||
// Update array input type
|
||||
// Computed the reference value type
|
||||
const newValueType = useMemo(() => {
|
||||
if (!loopInputArray) return WorkflowIOValueTypeEnum.arrayAny;
|
||||
const value = loopInputArray.value as ReferenceArrayValueType;
|
||||
|
||||
if (
|
||||
!value ||
|
||||
value.length === 0 ||
|
||||
!isValidArrayReferenceValue(
|
||||
value,
|
||||
nodeList.map((node) => node.nodeId)
|
||||
)
|
||||
)
|
||||
return WorkflowIOValueTypeEnum.arrayAny;
|
||||
|
||||
const globalVariables = getWorkflowGlobalVariables({
|
||||
nodes: nodeList,
|
||||
chatConfig: appDetail.chatConfig
|
||||
});
|
||||
|
||||
const valueType = ((value) => {
|
||||
if (value?.[0] === VARIABLE_NODE_ID) {
|
||||
return globalVariables.find((item) => item.key === value[1])?.valueType;
|
||||
} else {
|
||||
const node = nodeList.find((node) => node.nodeId === value?.[0]);
|
||||
const output = node?.outputs.find((output) => output.id === value?.[1]);
|
||||
return output?.valueType;
|
||||
}
|
||||
})(value[0]);
|
||||
return ArrayTypeMap[valueType as keyof typeof ArrayTypeMap] ?? WorkflowIOValueTypeEnum.arrayAny;
|
||||
}, [appDetail.chatConfig, loopInputArray, nodeList]);
|
||||
useEffect(() => {
|
||||
if (!loopInputArray) return;
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.loopInputArray,
|
||||
value: {
|
||||
...loopInputArray,
|
||||
valueType: newValueType
|
||||
}
|
||||
});
|
||||
}, [newValueType]);
|
||||
|
||||
// Update childrenNodeIdList
|
||||
const childrenNodeIdList = useMemo(() => {
|
||||
return JSON.stringify(
|
||||
nodeList.filter((node) => node.parentNodeId === nodeId).map((node) => node.nodeId)
|
||||
);
|
||||
}, [nodeId, nodeList]);
|
||||
|
||||
useEffect(() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
@@ -47,31 +120,52 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
value: JSON.parse(childrenNodeIdList)
|
||||
}
|
||||
});
|
||||
resetParentNodeSizeAndPosition(nodeId);
|
||||
}, [childrenNodeIdList]);
|
||||
|
||||
// Update loop node offset value
|
||||
const inputBoxRef = useRef<HTMLDivElement>(null);
|
||||
const size = useSize(inputBoxRef);
|
||||
useEffect(() => {
|
||||
if (!size?.height) return;
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'replaceInput',
|
||||
key: NodeInputKeyEnum.loopNodeInputHeight,
|
||||
value: {
|
||||
...loopNodeInputHeight,
|
||||
value: size.height
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
resetParentNodeSizeAndPosition(nodeId);
|
||||
}, 50);
|
||||
}, [size?.height]);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<NodeCard
|
||||
selected={selected}
|
||||
maxW="full"
|
||||
{...(!isFolded && {
|
||||
minW: '900px',
|
||||
minH: '900px',
|
||||
w: nodeWidth,
|
||||
h: nodeHeight
|
||||
})}
|
||||
menuForbid={{ copy: true }}
|
||||
{...data}
|
||||
>
|
||||
<NodeCard selected={selected} maxW="full" menuForbid={{ copy: true }} {...data}>
|
||||
<Container position={'relative'} flex={1}>
|
||||
<IOTitle text={t('common:common.Input')} />
|
||||
<Box mb={6} maxW={'360'}>
|
||||
<Box mb={6} maxW={'500px'} ref={inputBoxRef}>
|
||||
<RenderInput nodeId={nodeId} flowInputList={inputs} />
|
||||
</Box>
|
||||
<FormLabel required fontWeight={'medium'} mb={3} color={'myGray.600'}>
|
||||
{t('workflow:loop_body')}
|
||||
</FormLabel>
|
||||
<Box flex={1} position={'relative'} border={'base'} bg={'myGray.100'} rounded={'8px'}>
|
||||
<Box
|
||||
flex={1}
|
||||
position={'relative'}
|
||||
border={'base'}
|
||||
bg={'myGray.100'}
|
||||
rounded={'8px'}
|
||||
{...(!isFolded && {
|
||||
minW: nodeWidth,
|
||||
minH: nodeHeight
|
||||
})}
|
||||
>
|
||||
<Background />
|
||||
</Box>
|
||||
</Container>
|
||||
@@ -80,7 +174,7 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
}, [selected, nodeWidth, nodeHeight, data, t, nodeId, inputs, outputs]);
|
||||
}, [selected, isFolded, nodeWidth, nodeHeight, data, t, nodeId, inputs, outputs]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
@@ -80,7 +80,7 @@ const NodeLoopEnd = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
debug: true
|
||||
}}
|
||||
>
|
||||
<Box px={4} pb={4}>
|
||||
<Box px={4} pb={4} pt={2}>
|
||||
{inputItem && <Reference item={inputItem} nodeId={nodeId} />}
|
||||
</Box>
|
||||
</NodeCard>
|
||||
|
||||
@@ -8,24 +8,27 @@ import { WorkflowContext } from '../../../context';
|
||||
import {
|
||||
NodeInputKeyEnum,
|
||||
NodeOutputKeyEnum,
|
||||
toolValueTypeList,
|
||||
WorkflowIOValueTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/constants';
|
||||
import { Box, Flex, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import {
|
||||
FlowNodeOutputTypeEnum,
|
||||
FlowValueTypeMap
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
const typeMap = {
|
||||
[WorkflowIOValueTypeEnum.arrayString]: WorkflowIOValueTypeEnum.string,
|
||||
[WorkflowIOValueTypeEnum.arrayNumber]: WorkflowIOValueTypeEnum.number,
|
||||
[WorkflowIOValueTypeEnum.arrayBoolean]: WorkflowIOValueTypeEnum.boolean,
|
||||
[WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.object,
|
||||
[WorkflowIOValueTypeEnum.arrayAny]: WorkflowIOValueTypeEnum.any
|
||||
[WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.object
|
||||
};
|
||||
|
||||
const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { nodeId } = data;
|
||||
const { nodeId, outputs } = data;
|
||||
const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
const loopStartNode = useMemo(
|
||||
@@ -39,12 +42,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const parentArrayInput = parentNode?.inputs.find(
|
||||
(input) => input.key === NodeInputKeyEnum.loopInputArray
|
||||
);
|
||||
return parentArrayInput?.value
|
||||
? (nodeList
|
||||
.find((node) => node.nodeId === parentArrayInput?.value[0])
|
||||
?.outputs.find((output) => output.id === parentArrayInput?.value[1])
|
||||
?.valueType as keyof typeof typeMap)
|
||||
: undefined;
|
||||
return typeMap[parentArrayInput?.valueType as keyof typeof typeMap];
|
||||
}, [loopStartNode?.parentNodeId, nodeList]);
|
||||
|
||||
// Auth update loopStartInput output
|
||||
@@ -71,7 +69,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
key: NodeOutputKeyEnum.loopStartInput,
|
||||
label: t('workflow:Array_element'),
|
||||
type: FlowNodeOutputTypeEnum.static,
|
||||
valueType: typeMap[loopItemInputType as keyof typeof typeMap]
|
||||
valueType: loopItemInputType
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -83,7 +81,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
key: NodeOutputKeyEnum.loopStartInput,
|
||||
value: {
|
||||
...loopArrayOutput,
|
||||
valueType: typeMap[loopItemInputType as keyof typeof typeMap]
|
||||
valueType: loopItemInputType
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -100,23 +98,21 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
debug: true
|
||||
}}
|
||||
>
|
||||
<Box px={4} w={'420px'} h={'116px'}>
|
||||
{!loopItemInputType ? (
|
||||
<EmptyTip text={t('workflow:loop_start_tip')} py={0} mt={4} iconSize={'32px'} />
|
||||
) : (
|
||||
<Box bg={'white'} borderRadius={'md'} overflow={'hidden'} border={'base'}>
|
||||
<TableContainer>
|
||||
<Table bg={'white'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th borderBottomLeftRadius={'none !important'}>
|
||||
{t('workflow:Variable_name')}
|
||||
</Th>
|
||||
<Th>{t('common:core.workflow.Value type')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
<Tr>
|
||||
<Box px={4} pt={2} w={'420px'}>
|
||||
<Box bg={'white'} borderRadius={'md'} overflow={'hidden'} border={'base'}>
|
||||
<TableContainer>
|
||||
<Table bg={'white'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th borderBottomLeftRadius={'none !important'}>
|
||||
{t('workflow:Variable_name')}
|
||||
</Th>
|
||||
<Th>{t('common:core.workflow.Value type')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{outputs.map((output) => (
|
||||
<Tr key={output.id}>
|
||||
<Td>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon
|
||||
@@ -125,20 +121,20 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
mr={1}
|
||||
color={'primary.600'}
|
||||
/>
|
||||
{t('workflow:Array_element')}
|
||||
{t(output.label as any)}
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td>{typeMap[loopItemInputType]}</Td>
|
||||
{output.valueType && <Td>{FlowValueTypeMap[output.valueType]?.label}</Td>}
|
||||
</Tr>
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
)}
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
</Box>
|
||||
</NodeCard>
|
||||
);
|
||||
}, [data, loopItemInputType, selected, t]);
|
||||
}, [data, outputs, selected, t]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ const NodeAnswer = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
|
||||
|
||||
return (
|
||||
<NodeCard minW={'400px'} selected={selected} {...data}>
|
||||
<NodeCard selected={selected} {...data}>
|
||||
<Container>
|
||||
{isTool && (
|
||||
<>
|
||||
|
||||
@@ -99,7 +99,7 @@ const NodeCQNode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
nodeId={nodeId}
|
||||
handleId={getHandleId(nodeId, 'source', item.key)}
|
||||
position={Position.Right}
|
||||
translate={[26, 0]}
|
||||
translate={[34, 0]}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -7,14 +7,13 @@ import RenderInput from './render/RenderInput';
|
||||
import { Box, Button, Flex, HStack } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
import { NodeInputKeyEnum, VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { getOneQuoteInputTemplate } from '@fastgpt/global/core/workflow/template/system/datasetConcat';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import MySlider from '@/components/Slider';
|
||||
import {
|
||||
FlowNodeInputItemType,
|
||||
ReferenceValueProps
|
||||
ReferenceItemValueType
|
||||
} from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import RenderOutput from './render/RenderOutput';
|
||||
import IOTitle from '../components/IOTitle';
|
||||
@@ -24,94 +23,13 @@ import { ReferSelector, useReference } from './render/RenderInput/templates/Refe
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import ValueTypeLabel from './render/ValueTypeLabel';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { isWorkflowStartOutput } from '@fastgpt/global/core/workflow/template/system/workflowStart';
|
||||
import { getWebLLMModel } from '@/web/common/system/utils';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
|
||||
const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { llmModelList } = useSystemStore();
|
||||
const { nodeId, inputs, outputs } = data;
|
||||
const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
const Reference = useMemoizedFn(
|
||||
({ nodeId, inputChildren }: { nodeId: string; inputChildren: FlowNodeInputItemType }) => {
|
||||
const { t } = useTranslation();
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const { referenceList, formatValue } = useReference({
|
||||
nodeId,
|
||||
valueType: inputChildren.valueType,
|
||||
value: inputChildren.value
|
||||
});
|
||||
|
||||
const onSelect = useCallback(
|
||||
(e: ReferenceValueProps) => {
|
||||
const workflowStartNode = nodeList.find(
|
||||
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
|
||||
);
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'replaceInput',
|
||||
key: inputChildren.key,
|
||||
value: {
|
||||
...inputChildren,
|
||||
value:
|
||||
e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1])
|
||||
? [VARIABLE_NODE_ID, e[1]]
|
||||
: e
|
||||
}
|
||||
});
|
||||
},
|
||||
[inputChildren, nodeId, nodeList, onChangeNode]
|
||||
);
|
||||
|
||||
const onDel = useCallback(() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'delInput',
|
||||
key: inputChildren.key
|
||||
});
|
||||
}, [inputChildren.key, nodeId, onChangeNode]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems={'center'} mb={1}>
|
||||
<FormLabel required={inputChildren.required}>{t(inputChildren.label as any)}</FormLabel>
|
||||
{/* value */}
|
||||
<ValueTypeLabel
|
||||
valueType={inputChildren.valueType}
|
||||
valueDesc={inputChildren.valueDesc}
|
||||
/>
|
||||
|
||||
<MyIcon
|
||||
className="delete"
|
||||
name={'delete'}
|
||||
w={'14px'}
|
||||
color={'myGray.500'}
|
||||
cursor={'pointer'}
|
||||
ml={2}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={onDel}
|
||||
/>
|
||||
</Flex>
|
||||
<ReferSelector
|
||||
placeholder={t(
|
||||
(inputChildren.referencePlaceholder as any) ||
|
||||
t('common:core.module.Dataset quote.select')
|
||||
)}
|
||||
list={referenceList}
|
||||
value={formatValue}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const CustomComponent = useMemo(() => {
|
||||
const quoteList = inputs.filter((item) => item.canEdit);
|
||||
const tokenLimit = (() => {
|
||||
@@ -184,7 +102,7 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
<Box mt={2}>
|
||||
{quoteList.map((children) => (
|
||||
<Box key={children.key} _notLast={{ mb: 3 }}>
|
||||
<Reference nodeId={nodeId} inputChildren={children} />
|
||||
<VariableSelector nodeId={nodeId} inputChildren={children} />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
@@ -192,7 +110,7 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [Reference, inputs, nodeId, nodeList, onChangeNode, t, llmModelList]);
|
||||
}, [inputs, nodeId, nodeList, onChangeNode, t]);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
@@ -212,3 +130,75 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
return Render;
|
||||
};
|
||||
export default React.memo(NodeDatasetConcat);
|
||||
|
||||
const VariableSelector = ({
|
||||
nodeId,
|
||||
inputChildren
|
||||
}: {
|
||||
nodeId: string;
|
||||
inputChildren: FlowNodeInputItemType;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
const { referenceList } = useReference({
|
||||
nodeId,
|
||||
valueType: inputChildren.valueType
|
||||
});
|
||||
|
||||
const onSelect = useCallback(
|
||||
(e?: ReferenceItemValueType) => {
|
||||
if (!e) return;
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'replaceInput',
|
||||
key: inputChildren.key,
|
||||
value: {
|
||||
...inputChildren,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
},
|
||||
[inputChildren, nodeId, onChangeNode]
|
||||
);
|
||||
|
||||
const onDel = useCallback(() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'delInput',
|
||||
key: inputChildren.key
|
||||
});
|
||||
}, [inputChildren.key, nodeId, onChangeNode]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems={'center'} mb={1}>
|
||||
<FormLabel required={inputChildren.required}>{t(inputChildren.label as any)}</FormLabel>
|
||||
{/* value */}
|
||||
<ValueTypeLabel valueType={inputChildren.valueType} valueDesc={inputChildren.valueDesc} />
|
||||
|
||||
<MyIcon
|
||||
className="delete"
|
||||
name={'delete'}
|
||||
w={'14px'}
|
||||
color={'myGray.500'}
|
||||
cursor={'pointer'}
|
||||
ml={2}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={onDel}
|
||||
/>
|
||||
</Flex>
|
||||
<ReferSelector
|
||||
placeholder={t(
|
||||
(inputChildren.referencePlaceholder as any) ||
|
||||
t('common:core.module.Dataset quote.select')
|
||||
)}
|
||||
list={referenceList}
|
||||
value={inputChildren.value}
|
||||
onSelect={onSelect}
|
||||
isArray={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -60,7 +60,8 @@ const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
|
||||
</Box>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'whitePrimary'}
|
||||
variant={'ghost'}
|
||||
color={'myGray.600'}
|
||||
leftIcon={<AddIcon fontSize={'10px'} />}
|
||||
onClick={() => setEditExtractField(defaultField)}
|
||||
>
|
||||
@@ -78,12 +79,10 @@ const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
|
||||
<Table bg={'white'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th bg={'myGray.50'} borderRadius={'none !important'}>
|
||||
{t('common:item_name')}
|
||||
</Th>
|
||||
<Th bg={'myGray.50'}>{t('common:item_description')}</Th>
|
||||
<Th bg={'myGray.50'}>{t('common:required')}</Th>
|
||||
<Th bg={'myGray.50'} borderRadius={'none !important'}></Th>
|
||||
<Th borderRadius={'none !important'}>{t('common:item_name')}</Th>
|
||||
<Th>{t('common:item_description')}</Th>
|
||||
<Th>{t('common:required')}</Th>
|
||||
<Th borderRadius={'none !important'}></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
|
||||
@@ -46,11 +46,6 @@ const InputFormEditModal = ({
|
||||
|
||||
const inputType = watch('type') || FlowNodeInputTypeEnum.input;
|
||||
|
||||
const maxLength = watch('maxLength');
|
||||
const max = watch('max');
|
||||
const min = watch('min');
|
||||
const defaultInputValue = watch('defaultValue');
|
||||
|
||||
const inputTypeList = [
|
||||
{
|
||||
icon: 'core/workflow/inputType/input',
|
||||
@@ -187,14 +182,9 @@ const InputFormEditModal = ({
|
||||
type={'formInput'}
|
||||
isEdit={isEdit}
|
||||
inputType={inputType}
|
||||
maxLength={maxLength}
|
||||
max={max}
|
||||
min={min}
|
||||
defaultValue={defaultInputValue}
|
||||
onClose={onClose}
|
||||
onSubmitSuccess={onSubmitSuccess}
|
||||
onSubmitError={onSubmitError}
|
||||
valueType={defaultValueType}
|
||||
/>
|
||||
</Flex>
|
||||
</MyModal>
|
||||
|
||||
@@ -49,6 +49,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { getEditorVariables } from '../../../utils';
|
||||
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
|
||||
import { WorkflowNodeEdgeContext } from '../../../context/workflowInitContext';
|
||||
const CurlImportModal = dynamic(() => import('./CurlImportModal'));
|
||||
|
||||
const defaultFormBody = {
|
||||
@@ -80,9 +81,10 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const { isOpen: isOpenCurl, onOpen: onOpenCurl, onClose: onCloseCurl } = useDisclosure();
|
||||
@@ -256,8 +258,9 @@ export function RenderHttpProps({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedTab, setSelectedTab] = useState(TabEnum.params);
|
||||
|
||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
||||
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import Container from '../../components/Container';
|
||||
import { MinusIcon, SmallAddIcon } from '@chakra-ui/icons';
|
||||
import { IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { ReferenceItemValueType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ReferSelector, useReference } from '../render/RenderInput/templates/Reference';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
@@ -62,6 +62,7 @@ const ListItem = ({
|
||||
position={'relative'}
|
||||
transform={snapshot.isDragging ? `scale(${getZoom()})` : ''}
|
||||
transformOrigin={'top left'}
|
||||
mb={2}
|
||||
>
|
||||
<Container w={snapshot.isDragging ? '' : 'full'} className="nodrag">
|
||||
<Flex mb={4} alignItems={'center'}>
|
||||
@@ -122,7 +123,7 @@ const ListItem = ({
|
||||
<Flex gap={2} mb={2} alignItems={'center'}>
|
||||
{/* variable reference */}
|
||||
<Box minW={'250px'}>
|
||||
<Reference
|
||||
<VariableSelector
|
||||
nodeId={nodeId}
|
||||
variable={item.variable}
|
||||
onSelect={(e) => {
|
||||
@@ -255,7 +256,6 @@ const ListItem = ({
|
||||
}}
|
||||
variant={'link'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
my={3}
|
||||
color={'primary.600'}
|
||||
>
|
||||
{t('common:core.module.input.add')}
|
||||
@@ -266,7 +266,7 @@ const ListItem = ({
|
||||
nodeId={nodeId}
|
||||
handleId={handleId}
|
||||
position={Position.Right}
|
||||
translate={[18, 0]}
|
||||
translate={[5, 0]}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
@@ -302,29 +302,29 @@ const ListItem = ({
|
||||
|
||||
export default React.memo(ListItem);
|
||||
|
||||
const Reference = ({
|
||||
const VariableSelector = ({
|
||||
nodeId,
|
||||
variable,
|
||||
onSelect
|
||||
}: {
|
||||
nodeId: string;
|
||||
variable?: ReferenceValueProps;
|
||||
onSelect: (e: ReferenceValueProps) => void;
|
||||
variable?: ReferenceItemValueType;
|
||||
onSelect: (e?: ReferenceItemValueType) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { referenceList, formatValue } = useReference({
|
||||
const { referenceList } = useReference({
|
||||
nodeId,
|
||||
valueType: WorkflowIOValueTypeEnum.any,
|
||||
value: variable
|
||||
valueType: WorkflowIOValueTypeEnum.any
|
||||
});
|
||||
|
||||
return (
|
||||
<ReferSelector
|
||||
placeholder={t('common:select_reference_variable')}
|
||||
list={referenceList}
|
||||
value={formatValue}
|
||||
value={variable}
|
||||
onSelect={onSelect}
|
||||
isArray={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -336,7 +336,7 @@ const ConditionSelect = ({
|
||||
onSelect
|
||||
}: {
|
||||
condition?: VariableConditionEnum;
|
||||
variable?: ReferenceValueProps;
|
||||
variable?: ReferenceItemValueType;
|
||||
onSelect: (e: VariableConditionEnum) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -414,7 +414,7 @@ const ConditionValueInput = ({
|
||||
onChange
|
||||
}: {
|
||||
value?: string;
|
||||
variable?: ReferenceValueProps;
|
||||
variable?: ReferenceItemValueType;
|
||||
condition?: VariableConditionEnum;
|
||||
onChange: (e: string) => void;
|
||||
}) => {
|
||||
|
||||
@@ -48,7 +48,7 @@ const NodeIfElse = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
|
||||
return (
|
||||
<NodeCard selected={selected} maxW={'1000px'} {...data}>
|
||||
<Box px={4} cursor={'default'}>
|
||||
<Flex flexDirection={'column'} cursor={'default'}>
|
||||
<DndDrag<IfElseListItemType>
|
||||
onDragEndCb={(list: IfElseListItemType[]) => onUpdateIfElseList(list)}
|
||||
dataList={ifElseList}
|
||||
@@ -98,12 +98,12 @@ const NodeIfElse = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
nodeId={nodeId}
|
||||
handleId={elseHandleId}
|
||||
position={Position.Right}
|
||||
translate={[26, 0]}
|
||||
translate={[18, 0]}
|
||||
/>
|
||||
</Flex>
|
||||
</Container>
|
||||
</Box>
|
||||
<Box py={3} px={6}>
|
||||
</Flex>
|
||||
<Box py={3} px={4}>
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
w={'full'}
|
||||
|
||||
@@ -11,7 +11,6 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { useBoolean } from 'ahooks';
|
||||
import InputTypeConfig from './InputTypeConfig';
|
||||
|
||||
export const defaultInput: FlowNodeInputItemType = {
|
||||
@@ -23,7 +22,10 @@ export const defaultInput: FlowNodeInputItemType = {
|
||||
label: '',
|
||||
description: '',
|
||||
defaultValue: '',
|
||||
list: [{ label: '', value: '' }]
|
||||
list: [{ label: '', value: '' }],
|
||||
maxFiles: 5,
|
||||
canSelectFile: true,
|
||||
canSelectImg: true
|
||||
};
|
||||
|
||||
const FieldEditModal = ({
|
||||
@@ -108,6 +110,13 @@ const FieldEditModal = ({
|
||||
])
|
||||
],
|
||||
[
|
||||
{
|
||||
icon: 'core/workflow/inputType/file',
|
||||
label: t('app:file_upload'),
|
||||
value: [FlowNodeInputTypeEnum.fileSelect],
|
||||
defaultValueType: WorkflowIOValueTypeEnum.arrayString,
|
||||
description: t('app:file_upload_tip')
|
||||
},
|
||||
{
|
||||
icon: 'core/workflow/inputType/customVariable',
|
||||
label: t('common:core.workflow.inputType.custom'),
|
||||
@@ -130,19 +139,10 @@ const FieldEditModal = ({
|
||||
const form = useForm({
|
||||
defaultValues: defaultValue
|
||||
});
|
||||
const { getValues, setValue, watch, reset } = form;
|
||||
const { setValue, watch, reset } = form;
|
||||
|
||||
const renderTypeList = watch('renderTypeList');
|
||||
const inputType = renderTypeList[0] || FlowNodeInputTypeEnum.reference;
|
||||
const valueType = watch('valueType');
|
||||
|
||||
const [isToolInput, { toggle: setIsToolInput }] = useBoolean(!!getValues('toolDescription'));
|
||||
|
||||
const maxLength = watch('maxLength');
|
||||
const max = watch('max');
|
||||
const min = watch('min');
|
||||
const selectValueTypeList = watch('customInputConfig.selectValueTypeList');
|
||||
const defaultInputValue = watch('defaultValue');
|
||||
|
||||
const defaultValueType = useMemo(
|
||||
() =>
|
||||
@@ -190,8 +190,8 @@ const FieldEditModal = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Focus remove toolDescription
|
||||
if (isToolInput && data.renderTypeList.includes(FlowNodeInputTypeEnum.reference)) {
|
||||
// Get toolDescription and removes the types of some unusable tools
|
||||
if (data.toolDescription && data.renderTypeList.includes(FlowNodeInputTypeEnum.reference)) {
|
||||
data.toolDescription = data.description;
|
||||
} else {
|
||||
data.toolDescription = undefined;
|
||||
@@ -211,18 +211,7 @@ const FieldEditModal = ({
|
||||
reset(defaultInput);
|
||||
}
|
||||
},
|
||||
[
|
||||
defaultValue.key,
|
||||
defaultValueType,
|
||||
isEdit,
|
||||
isToolInput,
|
||||
keys,
|
||||
onSubmit,
|
||||
t,
|
||||
toast,
|
||||
onClose,
|
||||
reset
|
||||
]
|
||||
[defaultValue.key, defaultValueType, isEdit, keys, onSubmit, t, toast, onClose, reset]
|
||||
);
|
||||
const onSubmitError = useCallback(
|
||||
(e: Object) => {
|
||||
@@ -241,7 +230,7 @@ const FieldEditModal = ({
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
iconSrc="/imgs/workflow/extract.png"
|
||||
title={isEdit ? t('workflow:edit_input') : t('workflow:add_new_input')}
|
||||
@@ -321,14 +310,6 @@ const FieldEditModal = ({
|
||||
isEdit={isEdit}
|
||||
onClose={onClose}
|
||||
inputType={inputType}
|
||||
maxLength={maxLength}
|
||||
max={max}
|
||||
min={min}
|
||||
selectValueTypeList={selectValueTypeList}
|
||||
defaultValue={defaultInputValue}
|
||||
isToolInput={isToolInput}
|
||||
setIsToolInput={setIsToolInput}
|
||||
valueType={valueType}
|
||||
defaultValueType={defaultValueType}
|
||||
onSubmitSuccess={onSubmitSuccess}
|
||||
onSubmitError={onSubmitError}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
Button,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
HStack,
|
||||
Input,
|
||||
NumberDecrementStepper,
|
||||
@@ -23,8 +22,6 @@ import {
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowValueTypeMap
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import MultipleSelect from '@fastgpt/web/components/common/MySelect/MultipleSelect';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
@@ -35,8 +32,12 @@ import { useTranslation } from 'react-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag';
|
||||
import MyTextarea from '@/components/common/Textarea/MyTextarea';
|
||||
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
|
||||
|
||||
type ListValueType = { id: string; value: string; label: string }[];
|
||||
import ChatFunctionTip from '@/components/core/app/Tip';
|
||||
import MySlider from '@/components/Slider';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
|
||||
const InputTypeConfig = ({
|
||||
form,
|
||||
@@ -44,36 +45,18 @@ const InputTypeConfig = ({
|
||||
onClose,
|
||||
type,
|
||||
inputType,
|
||||
maxLength,
|
||||
max,
|
||||
min,
|
||||
selectValueTypeList,
|
||||
defaultValue,
|
||||
isToolInput,
|
||||
setIsToolInput,
|
||||
valueType,
|
||||
defaultValueType,
|
||||
onSubmitSuccess,
|
||||
onSubmitError
|
||||
}: {
|
||||
// Common fields
|
||||
form: UseFormReturn<any>;
|
||||
form: UseFormReturn<any, any>;
|
||||
isEdit: boolean;
|
||||
onClose: () => void;
|
||||
type: 'plugin' | 'formInput' | 'variable';
|
||||
inputType: FlowNodeInputTypeEnum | VariableInputEnum;
|
||||
|
||||
maxLength?: number;
|
||||
max?: number;
|
||||
min?: number;
|
||||
|
||||
selectValueTypeList?: WorkflowIOValueTypeEnum[];
|
||||
defaultValue?: string;
|
||||
|
||||
// Plugin-specific fields
|
||||
isToolInput?: boolean;
|
||||
setIsToolInput?: () => void;
|
||||
valueType?: WorkflowIOValueTypeEnum;
|
||||
defaultValueType?: WorkflowIOValueTypeEnum;
|
||||
|
||||
// Update methods
|
||||
@@ -82,9 +65,7 @@ const InputTypeConfig = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultListValue = { label: t('common:None'), value: '' };
|
||||
|
||||
const { register, setValue, handleSubmit, control, watch } = form;
|
||||
const listValue: ListValueType = watch('list');
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const typeLabels = {
|
||||
name: {
|
||||
@@ -99,6 +80,18 @@ const InputTypeConfig = ({
|
||||
}
|
||||
};
|
||||
|
||||
const { register, setValue, handleSubmit, control, watch } = form;
|
||||
const maxLength = watch('maxLength');
|
||||
const max = watch('max');
|
||||
const min = watch('min');
|
||||
const selectValueTypeList = watch('customInputConfig.selectValueTypeList');
|
||||
const defaultValue = watch('defaultValue');
|
||||
const valueType = watch('valueType');
|
||||
|
||||
const toolDescription = watch('toolDescription');
|
||||
const isToolInput = !!toolDescription;
|
||||
|
||||
const listValue = watch('list') ?? [];
|
||||
const {
|
||||
fields: selectEnums,
|
||||
append: appendEnums,
|
||||
@@ -166,6 +159,10 @@ const InputTypeConfig = ({
|
||||
return type === 'plugin' && list.includes(inputType as FlowNodeInputTypeEnum);
|
||||
}, [inputType, type]);
|
||||
|
||||
// File select
|
||||
const maxFiles = watch('maxFiles');
|
||||
const maxSelectFiles = Math.min(feConfigs?.uploadFileMaxAmount ?? 20, 50);
|
||||
|
||||
return (
|
||||
<Stack flex={1} borderLeft={'1px solid #F0F1F6'} justifyContent={'space-between'}>
|
||||
<Flex flexDirection={'column'} p={8} pb={2} gap={4} flex={'1 0 0'} overflow={'auto'}>
|
||||
@@ -175,6 +172,7 @@ const InputTypeConfig = ({
|
||||
</FormLabel>
|
||||
<Input
|
||||
bg={'myGray.50'}
|
||||
maxLength={30}
|
||||
placeholder="appointment/sql"
|
||||
{...register('label', {
|
||||
required: true
|
||||
@@ -189,7 +187,9 @@ const InputTypeConfig = ({
|
||||
bg={'myGray.50'}
|
||||
placeholder={t('workflow:field_description_placeholder')}
|
||||
rows={3}
|
||||
{...register('description', { required: isToolInput ? true : false })}
|
||||
{...register('description', {
|
||||
required: showIsToolInput && isToolInput ? true : false
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
@@ -213,7 +213,7 @@ const InputTypeConfig = ({
|
||||
</Box>
|
||||
) : (
|
||||
<Box fontSize={'14px'} mb={2}>
|
||||
{defaultValueType}
|
||||
{defaultValueType ? t(FlowValueTypeMap[defaultValueType]?.label as any) : ''}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
@@ -236,7 +236,7 @@ const InputTypeConfig = ({
|
||||
<Switch
|
||||
isChecked={isToolInput}
|
||||
onChange={(e) => {
|
||||
setIsToolInput && setIsToolInput();
|
||||
setValue('toolDescription', e.target.checked ? 'sign' : '');
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
@@ -249,8 +249,6 @@ const InputTypeConfig = ({
|
||||
{t('common:core.module.Max Length')}
|
||||
</FormLabel>
|
||||
<MyNumberInput
|
||||
flex={'1 0 0'}
|
||||
bg={'myGray.50'}
|
||||
placeholder={t('common:core.module.Max Length placeholder')}
|
||||
value={maxLength}
|
||||
max={50000}
|
||||
@@ -269,8 +267,6 @@ const InputTypeConfig = ({
|
||||
{t('common:core.module.Max Value')}
|
||||
</FormLabel>
|
||||
<MyNumberInput
|
||||
flex={'1 0 0'}
|
||||
bg={'myGray.50'}
|
||||
value={max}
|
||||
onChange={(e) => {
|
||||
// @ts-ignore
|
||||
@@ -283,8 +279,6 @@ const InputTypeConfig = ({
|
||||
{t('common:core.module.Min Value')}
|
||||
</FormLabel>
|
||||
<MyNumberInput
|
||||
flex={'1 0 0'}
|
||||
bg={'myGray.50'}
|
||||
value={min}
|
||||
onChange={(e) => {
|
||||
// @ts-ignore
|
||||
@@ -302,18 +296,15 @@ const InputTypeConfig = ({
|
||||
</FormLabel>
|
||||
<Flex alignItems={'start'} flex={1} h={10}>
|
||||
{inputType === FlowNodeInputTypeEnum.numberInput && (
|
||||
<NumberInput flex={1} step={1} min={min} max={max} position={'relative'}>
|
||||
<NumberInputField
|
||||
{...register('defaultValue', {
|
||||
min: min,
|
||||
max: max
|
||||
})}
|
||||
/>
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
<MyNumberInput
|
||||
value={defaultValue}
|
||||
min={min}
|
||||
max={max}
|
||||
onChange={(e) => {
|
||||
// @ts-ignore
|
||||
setValue('defaultValue', e || '');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.input && (
|
||||
<MyTextarea
|
||||
@@ -347,7 +338,7 @@ const InputTypeConfig = ({
|
||||
value: item.value
|
||||
}))}
|
||||
value={
|
||||
defaultValue && listValue.map((item) => item.value).includes(defaultValue)
|
||||
defaultValue && listValue.map((item: any) => item.value).includes(defaultValue)
|
||||
? defaultValue
|
||||
: ''
|
||||
}
|
||||
@@ -363,12 +354,12 @@ const InputTypeConfig = ({
|
||||
|
||||
{inputType === FlowNodeInputTypeEnum.addInputParam && (
|
||||
<>
|
||||
<Flex alignItems={'center'}>
|
||||
{/* <Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 132px'} fontWeight={'medium'}>
|
||||
{t('common:core.module.Input Type')}
|
||||
</FormLabel>
|
||||
<Box fontSize={'14px'}>{t('workflow:only_the_reference_type_is_supported')}</Box>
|
||||
</Flex>
|
||||
</Flex> */}
|
||||
<Box>
|
||||
<HStack mb={1}>
|
||||
<FormLabel fontWeight={'medium'}>{t('workflow:optional_value_type')}</FormLabel>
|
||||
@@ -395,7 +386,9 @@ const InputTypeConfig = ({
|
||||
.map((id) => mergedSelectEnums.find((item) => item.id === id))
|
||||
.filter(Boolean) as { id: string; value: string }[];
|
||||
removeEnums();
|
||||
newSelectEnums.forEach((item) => appendEnums(item));
|
||||
newSelectEnums.forEach((item) =>
|
||||
appendEnums({ label: item.value, value: item.value })
|
||||
);
|
||||
|
||||
// 防止最后一个元素被focus
|
||||
setTimeout(() => {
|
||||
@@ -511,6 +504,60 @@ const InputTypeConfig = ({
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{inputType === FlowNodeInputTypeEnum.fileSelect && (
|
||||
<>
|
||||
<Flex alignItems={'center'} minH={'40px'}>
|
||||
<FormLabel flex={'0 0 132px'} fontWeight={'medium'}>
|
||||
{t('app:document_upload')}
|
||||
</FormLabel>
|
||||
<Switch
|
||||
{...register('canSelectFile', {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Box w={'full'} minH={'40px'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 132px'} fontWeight={'medium'}>
|
||||
{t('app:image_upload')}
|
||||
</FormLabel>
|
||||
<Switch
|
||||
{...register('canSelectImg', {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex color={'myGray.500'}>
|
||||
<Box fontSize={'xs'}>{t('app:image_upload_tip')}</Box>
|
||||
<ChatFunctionTip type="visionModel" />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<HStack>
|
||||
<FormLabel fontWeight={'medium'}>{t('app:upload_file_max_amount')}</FormLabel>
|
||||
<QuestionTip label={t('app:upload_file_max_amount_tip')} />
|
||||
</HStack>
|
||||
|
||||
<Box mt={5}>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '1', value: 1 },
|
||||
{ label: `${maxSelectFiles}`, value: maxSelectFiles }
|
||||
]}
|
||||
width={'100%'}
|
||||
min={1}
|
||||
max={maxSelectFiles}
|
||||
step={1}
|
||||
value={maxFiles ?? 5}
|
||||
onChange={(e) => {
|
||||
setValue('maxFiles', e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex justify={'flex-end'} gap={3} pb={8} pr={8}>
|
||||
@@ -520,10 +567,7 @@ const InputTypeConfig = ({
|
||||
<Button
|
||||
variant={'primaryOutline'}
|
||||
fontWeight={'medium'}
|
||||
onClick={handleSubmit(
|
||||
(data: FlowNodeInputItemType) => onSubmitSuccess(data, 'confirm'),
|
||||
onSubmitError
|
||||
)}
|
||||
onClick={handleSubmit((data) => onSubmitSuccess(data, 'confirm'), onSubmitError)}
|
||||
w={20}
|
||||
>
|
||||
{t('common:common.Confirm')}
|
||||
@@ -531,10 +575,7 @@ const InputTypeConfig = ({
|
||||
{!isEdit && (
|
||||
<Button
|
||||
fontWeight={'medium'}
|
||||
onClick={handleSubmit(
|
||||
(data: FlowNodeInputItemType) => onSubmitSuccess(data, 'continue'),
|
||||
onSubmitError
|
||||
)}
|
||||
onClick={handleSubmit((data) => onSubmitSuccess(data, 'continue'), onSubmitError)}
|
||||
w={20}
|
||||
>
|
||||
{t('common:common.Continue_Adding')}
|
||||
@@ -545,4 +586,4 @@ const InputTypeConfig = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(InputTypeConfig);
|
||||
export default InputTypeConfig;
|
||||
|
||||
@@ -53,24 +53,28 @@ const NodePluginConfig = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
[chatConfig, setAppDetail]
|
||||
);
|
||||
|
||||
return (
|
||||
<NodeCard
|
||||
selected={selected}
|
||||
menuForbid={{
|
||||
debug: true,
|
||||
copy: true,
|
||||
delete: true
|
||||
}}
|
||||
{...data}
|
||||
>
|
||||
<Container w={'360px'}>
|
||||
<Instruction {...componentsProps} />
|
||||
<Box pt={4}>
|
||||
<FileSelectConfig {...componentsProps} />
|
||||
</Box>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<NodeCard
|
||||
selected={selected}
|
||||
menuForbid={{
|
||||
debug: true,
|
||||
copy: true,
|
||||
delete: true
|
||||
}}
|
||||
{...data}
|
||||
>
|
||||
<Container w={'360px'}>
|
||||
<Instruction {...componentsProps} />
|
||||
<Box pt={4}>
|
||||
<FileSelectConfig {...componentsProps} />
|
||||
</Box>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
}, [componentsProps, data, selected]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
export default React.memo(NodePluginConfig);
|
||||
|
||||
@@ -114,46 +118,57 @@ function Instruction({ chatConfig: { instruction }, setAppDetail }: ComponentPro
|
||||
}
|
||||
|
||||
function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) {
|
||||
const { t } = useTranslation();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const nodes = useContextSelector(WorkflowContext, (v) => v.nodes);
|
||||
const pluginInputNode = nodes.find((item) => item.type === FlowNodeTypeEnum.pluginInput)!;
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const pluginInputNode = nodeList.find(
|
||||
(item) => item.flowNodeType === FlowNodeTypeEnum.pluginInput
|
||||
)!;
|
||||
|
||||
return (
|
||||
<FileSelect
|
||||
value={fileSelectConfig}
|
||||
color={'myGray.600'}
|
||||
fontWeight={'medium'}
|
||||
fontSize={'14px'}
|
||||
onChange={(e) => {
|
||||
setAppDetail((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
fileSelectConfig: e
|
||||
}
|
||||
}));
|
||||
<>
|
||||
<FileSelect
|
||||
value={fileSelectConfig}
|
||||
color={'myGray.600'}
|
||||
fontWeight={'medium'}
|
||||
fontSize={'sm'}
|
||||
onChange={(e) => {
|
||||
setAppDetail((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
fileSelectConfig: e
|
||||
}
|
||||
}));
|
||||
|
||||
// Dynamic add or delete userFilesInput
|
||||
const canUploadFiles = e.canSelectFile || e.canSelectImg;
|
||||
const repeatKey = pluginInputNode?.data.outputs.find(
|
||||
(item) => item.key === userFilesInput.key
|
||||
);
|
||||
if (canUploadFiles) {
|
||||
!repeatKey &&
|
||||
onChangeNode({
|
||||
nodeId: pluginInputNode.id,
|
||||
type: 'addOutput',
|
||||
value: userFilesInput
|
||||
});
|
||||
} else {
|
||||
repeatKey &&
|
||||
onChangeNode({
|
||||
nodeId: pluginInputNode.id,
|
||||
type: 'delOutput',
|
||||
key: userFilesInput.key
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
// Dynamic add or delete userFilesInput
|
||||
const canUploadFiles = e.canSelectFile || e.canSelectImg;
|
||||
const repeatKey = pluginInputNode?.outputs.find(
|
||||
(item) => item.key === userFilesInput.key
|
||||
);
|
||||
if (canUploadFiles) {
|
||||
!repeatKey &&
|
||||
onChangeNode({
|
||||
nodeId: pluginInputNode.nodeId,
|
||||
type: 'addOutput',
|
||||
value: {
|
||||
...userFilesInput,
|
||||
label: t('workflow:plugin.global_file_input')
|
||||
}
|
||||
});
|
||||
} else {
|
||||
repeatKey &&
|
||||
onChangeNode({
|
||||
nodeId: pluginInputNode.nodeId,
|
||||
type: 'delOutput',
|
||||
key: userFilesInput.key
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box fontSize={'mini'} color={'myGray.500'}>
|
||||
{t('workflow:plugin_file_abandon_tip')}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ const NodePluginInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
{!!outputs.length && (
|
||||
{outputs.length != inputs.length && (
|
||||
<Container>
|
||||
<IOTitle text={t('common:common.Output')} />
|
||||
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
|
||||
|
||||
@@ -2,18 +2,12 @@ import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from '../render/NodeCard';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import Container from '../../components/Container';
|
||||
import { FlowNodeInputItemType, ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { VARIABLE_NODE_ID, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { FlowNodeInputItemType, ReferenceValueType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import RenderInput from '../render/RenderInput';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../../context';
|
||||
import IOTitle from '../../components/IOTitle';
|
||||
@@ -24,7 +18,6 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { isWorkflowStartOutput } from '@fastgpt/global/core/workflow/template/system/workflowStart';
|
||||
import PluginOutputEditModal, { defaultOutput } from './PluginOutputEditModal';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
|
||||
@@ -113,38 +106,28 @@ function Reference({
|
||||
content: workflowT('confirm_delete_field_tip')
|
||||
});
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
|
||||
const [editField, setEditField] = useState<FlowNodeInputItemType>();
|
||||
|
||||
const onSelect = useCallback(
|
||||
(e: ReferenceValueProps) => {
|
||||
const workflowStartNode = nodeList.find(
|
||||
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
|
||||
);
|
||||
|
||||
const value =
|
||||
e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1])
|
||||
? [VARIABLE_NODE_ID, e[1]]
|
||||
: e;
|
||||
|
||||
(e?: ReferenceValueType) => {
|
||||
if (!e) return;
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: input.key,
|
||||
value: {
|
||||
...input,
|
||||
value
|
||||
value: e
|
||||
}
|
||||
});
|
||||
},
|
||||
[input, nodeId, nodeList, onChangeNode]
|
||||
[input, nodeId, onChangeNode]
|
||||
);
|
||||
|
||||
const { referenceList, formatValue } = useReference({
|
||||
const { referenceList } = useReference({
|
||||
nodeId,
|
||||
valueType: input.valueType,
|
||||
value: input.value
|
||||
valueType: input.valueType
|
||||
});
|
||||
|
||||
const onUpdateField = useCallback(
|
||||
@@ -217,8 +200,9 @@ function Reference({
|
||||
<ReferSelector
|
||||
placeholder={t((input.referencePlaceholder as any) || 'select_reference_variable')}
|
||||
list={referenceList}
|
||||
value={formatValue}
|
||||
value={input.value}
|
||||
onSelect={onSelect}
|
||||
isArray={input.valueType?.includes('array')}
|
||||
/>
|
||||
|
||||
{!!editField && (
|
||||
|
||||
@@ -15,7 +15,7 @@ import { WorkflowContext } from '../../context';
|
||||
const NodeSimple = ({
|
||||
data,
|
||||
selected,
|
||||
minW = '350px',
|
||||
minW = '524px',
|
||||
maxW
|
||||
}: NodeProps<FlowNodeItemType> & { minW?: string | number; maxW?: string | number }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -21,6 +21,7 @@ import WelcomeTextConfig from '@/components/core/app/WelcomeTextConfig';
|
||||
import FileSelect from '@/components/core/app/FileSelect';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { userFilesInput } from '@fastgpt/global/core/workflow/template/system/workflowStart';
|
||||
import Container from '../components/Container';
|
||||
|
||||
type ComponentProps = {
|
||||
chatConfig: AppChatConfigType;
|
||||
@@ -28,7 +29,8 @@ type ComponentProps = {
|
||||
};
|
||||
|
||||
const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||
const setAppDetail = useContextSelector(AppContext, (v) => v.setAppDetail);
|
||||
|
||||
const chatConfig = useMemo<AppChatConfigType>(() => {
|
||||
return getAppChatConfig({
|
||||
@@ -46,45 +48,48 @@ const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
[chatConfig, setAppDetail]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NodeCard
|
||||
minW={'300px'}
|
||||
selected={selected}
|
||||
menuForbid={{
|
||||
debug: true,
|
||||
copy: true,
|
||||
delete: true
|
||||
}}
|
||||
{...data}
|
||||
>
|
||||
<Box px={4} py={'10px'} position={'relative'} borderRadius={'md'} className="nodrag">
|
||||
<WelcomeText {...componentsProps} />
|
||||
<Box pt={4}>
|
||||
<ChatStartVariable {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={3} pt={3} borderTop={'base'}>
|
||||
<FileSelectConfig {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={3} pt={3} borderTop={'base'}>
|
||||
<TTSGuide {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={3} pt={3} borderTop={'base'}>
|
||||
<WhisperGuide {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={3} pt={3} borderTop={'base'}>
|
||||
<QuestionGuide {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={3} pt={3} borderTop={'base'}>
|
||||
<ScheduledTrigger {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={3} pt={3} borderTop={'base'}>
|
||||
<QuestionInputGuide {...componentsProps} />
|
||||
</Box>
|
||||
</Box>
|
||||
</NodeCard>
|
||||
</>
|
||||
);
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<NodeCard
|
||||
selected={selected}
|
||||
menuForbid={{
|
||||
debug: true,
|
||||
copy: true,
|
||||
delete: true
|
||||
}}
|
||||
{...data}
|
||||
>
|
||||
<Container>
|
||||
<WelcomeText {...componentsProps} />
|
||||
<Box mt={2} pt={2}>
|
||||
<ChatStartVariable {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
|
||||
<FileSelectConfig {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
|
||||
<TTSGuide {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
|
||||
<WhisperGuide {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={3} pt={4} borderTop={'base'} borderColor={'myGray.200'}>
|
||||
<QuestionGuide {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={4} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
|
||||
<ScheduledTrigger {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
|
||||
<QuestionInputGuide {...componentsProps} />
|
||||
</Box>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
</>
|
||||
);
|
||||
}, [componentsProps, data, selected]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(NodeUserGuide);
|
||||
@@ -217,8 +222,10 @@ function QuestionInputGuide({ chatConfig: { chatInputGuide }, setAppDetail }: Co
|
||||
|
||||
function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) {
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const nodes = useContextSelector(WorkflowContext, (v) => v.nodes);
|
||||
const workflowStartNode = nodes.find((item) => item.type === FlowNodeTypeEnum.workflowStart)!;
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const workflowStartNode = nodeList.find(
|
||||
(item) => item.flowNodeType === FlowNodeTypeEnum.workflowStart
|
||||
)!;
|
||||
|
||||
return (
|
||||
<FileSelect
|
||||
@@ -234,20 +241,20 @@ function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: Co
|
||||
|
||||
// Dynamic add or delete userFilesInput
|
||||
const canUploadFiles = e.canSelectFile || e.canSelectImg;
|
||||
const repeatKey = workflowStartNode?.data.outputs.find(
|
||||
const repeatKey = workflowStartNode?.outputs.find(
|
||||
(item) => item.key === userFilesInput.key
|
||||
);
|
||||
if (canUploadFiles) {
|
||||
!repeatKey &&
|
||||
onChangeNode({
|
||||
nodeId: workflowStartNode.id,
|
||||
nodeId: workflowStartNode.nodeId,
|
||||
type: 'addOutput',
|
||||
value: userFilesInput
|
||||
});
|
||||
} else {
|
||||
repeatKey &&
|
||||
onChangeNode({
|
||||
nodeId: workflowStartNode.id,
|
||||
nodeId: workflowStartNode.nodeId,
|
||||
type: 'delOutput',
|
||||
key: userFilesInput.key
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ const NodeTools = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
<Box position={'relative'}>
|
||||
<Box borderBottomLeftRadius={'md'} borderBottomRadius={'md'} overflow={'hidden'}>
|
||||
<Box mb={-4} borderBottomLeftRadius={'md'} borderBottomRadius={'md'} overflow={'hidden'}>
|
||||
<Divider
|
||||
showBorderBottom={false}
|
||||
icon={<MyIcon name="phoneTabbar/tool" w={'16px'} h={'16px'} />}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user