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:
Archer
2024-11-13 11:29:53 +08:00
committed by GitHub
parent e1f5483432
commit e9d52ada73
449 changed files with 7626 additions and 4180 deletions

View 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;

View File

@@ -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;

View 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
};
};

View File

@@ -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}>

View File

@@ -5,7 +5,8 @@ export enum CodeClassNameEnum {
echarts = 'echarts',
quote = 'quote',
files = 'files',
latex = 'latex'
latex = 'latex',
iframe = 'iframe'
}
function htmlTableToLatex(html: string) {

View File

@@ -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={''}

View File

@@ -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'}

View File

@@ -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'}>

View File

@@ -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}

View File

@@ -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'}

View File

@@ -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} />

View File

@@ -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}

View File

@@ -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'}

View File

@@ -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>
}
/>

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>;

View File

@@ -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={''}

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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>
))}
</>

View File

@@ -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} />}
</>
);
};

View File

@@ -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}
/>
)}
/>
)}

View File

@@ -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>

View File

@@ -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
};
};

View File

@@ -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 */}

View File

@@ -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 = {

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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>;
};

View File

@@ -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'}

View File

@@ -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?.();

View File

@@ -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

View File

@@ -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')} />
)}

View File

@@ -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}&currentTab=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}&currentTab=dataCard&collectionId=${quoteItem.collectionId}`}
>
{t('common:core.dataset.Go Dataset')}
<MyIcon name={'common/rightArrowLight'} w={'10px'} />
</Link>
)}
</Flex>
</MyBox>
{editInputData && (

View File

@@ -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

View File

@@ -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>

View File

@@ -61,6 +61,7 @@ const DefaultPermissionList = ({
}
}}
fontSize={styles?.fontSize}
fontWeight={styles?.fontWeight}
/>
</Box>
<ConfirmModal />

View File

@@ -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'}

View File

@@ -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>)}

View File

@@ -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>

View File

@@ -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

View File

@@ -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 ? (

View File

@@ -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
};
}
})();

View File

@@ -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);
}
}
});
}

View File

@@ -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,

View File

@@ -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
});

View File

@@ -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
});

View File

@@ -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
};
};

View File

@@ -180,6 +180,9 @@ const DetailLogsModal = ({
chatConfig={chat?.app?.chatConfig}
appId={appId}
chatId={chatId}
chatType="log"
showRawSource
showNodeStatus
/>
)}
</Box>

View File

@@ -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>

View File

@@ -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}>

View File

@@ -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>
);
};

View File

@@ -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'}

View File

@@ -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'}>

View File

@@ -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'}

View File

@@ -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>

View File

@@ -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'}

View File

@@ -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'}>

View File

@@ -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'}>

View File

@@ -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>

View File

@@ -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)
})}

View File

@@ -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={''}

View File

@@ -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 && (

View File

@@ -387,6 +387,7 @@ const RenderList = React.memo(function RenderList({
isInvalid={errors && Object.keys(errors).includes(input.key)}
onChange={onChange}
input={input}
setUploading={() => {}}
/>
);
}}

View File

@@ -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}>

View File

@@ -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>
);
};

View File

@@ -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 }) => (

View File

@@ -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>
);
});

View File

@@ -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)}

View File

@@ -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'}

View File

@@ -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>
)
);
};

View File

@@ -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();
});

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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
};
};

View File

@@ -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);

View File

@@ -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;
};

View File

@@ -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>

View File

@@ -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;
};

View File

@@ -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 && (
<>

View File

@@ -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>

View File

@@ -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}
/>
</>
);
};

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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;
}) => {

View File

@@ -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'}

View File

@@ -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}

View File

@@ -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;

View File

@@ -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>
</>
);
}

View File

@@ -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} />

View File

@@ -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 && (

View File

@@ -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();

View File

@@ -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
});

View File

@@ -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