Perf input guide (#1557)

* perf: input guide code

* perf: input guide ui

* Chat input guide api

* Update app chat config store

* perf: app chat config field

* perf: app context

* perf: params

* fix: ts

* perf: filter private config

* perf: filter private config

* perf: import workflow

* perf: limit max tip amount
This commit is contained in:
Archer
2024-05-21 17:52:04 +08:00
committed by GitHub
parent 8e8ceb7439
commit fb368a581c
123 changed files with 2124 additions and 1805 deletions

View File

@@ -1,9 +1,9 @@
import { useSpeech } from '@/web/common/hooks/useSpeech';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { Box, Flex, Image, Spinner, Textarea } from '@chakra-ui/react';
import React, { useRef, useEffect, useCallback, useTransition } from 'react';
import React, { useRef, useEffect, useCallback } from 'react';
import { useTranslation } from 'next-i18next';
import MyTooltip from '../MyTooltip';
import MyTooltip from '../../MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
@@ -12,18 +12,16 @@ import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
import { addDays } from 'date-fns';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { ChatBoxInputFormType, ChatBoxInputType, UserInputFileItemType } from './type';
import { textareaMinH } from './constants';
import { ChatBoxInputFormType, ChatBoxInputType, UserInputFileItemType } from '../type';
import { textareaMinH } from '../constants';
import { UseFormReturn, useFieldArray } from 'react-hook-form';
import { useChatProviderStore } from './Provider';
import QuestionGuide from './components/QustionGuide';
import { useQuery } from '@tanstack/react-query';
import { getMyQuestionGuides } from '@/web/core/app/api';
import { getAppQGuideCustomURL } from '@/web/core/app/utils';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import { useChatProviderStore } from '../Provider';
import dynamic from 'next/dynamic';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
const MessageInput = ({
const InputGuideBox = dynamic(() => import('./InputGuideBox'));
const ChatInput = ({
onSendMessage,
onStop,
TextareaDom,
@@ -38,7 +36,7 @@ const MessageInput = ({
TextareaDom: React.MutableRefObject<HTMLTextAreaElement | null>;
resetInputVal: (val: ChatBoxInputType) => void;
chatForm: UseFormReturn<ChatBoxInputFormType>;
appId?: string;
appId: string;
}) => {
const { setValue, watch, control } = chatForm;
const inputValue = watch('input');
@@ -53,12 +51,19 @@ const MessageInput = ({
name: 'files'
});
const { shareId, outLinkUid, teamId, teamToken, isChatting, whisperConfig, autoTTSResponse } =
useChatProviderStore();
const {
shareId,
outLinkUid,
teamId,
teamToken,
isChatting,
whisperConfig,
autoTTSResponse,
chatInputGuide
} = useChatProviderStore();
const { isPc, whisperModel } = useSystemStore();
const canvasRef = useRef<HTMLCanvasElement>(null);
const { t } = useTranslation();
const { appDetail } = useAppStore();
const havInput = !!inputValue || fileList.length > 0;
const hasFileUploading = fileList.some((item) => !item.url);
@@ -150,9 +155,9 @@ const MessageInput = ({
);
/* on send */
const handleSend = async () => {
const handleSend = async (val?: string) => {
if (!canSendMessage) return;
const textareaValue = TextareaDom.current?.value || '';
const textareaValue = val || TextareaDom.current?.value || '';
onSendMessage({
text: textareaValue.trim(),
@@ -211,23 +216,6 @@ const MessageInput = ({
startSpeak(finishWhisperTranscription);
}, [finishWhisperTranscription, isSpeaking, startSpeak, stopSpeak]);
const { data } = useQuery(
[appId, inputValue],
async () => {
if (!appId) return { list: [], total: 0 };
return getMyQuestionGuides({
appId,
customURL: getAppQGuideCustomURL(appDetail),
pageSize: 5,
current: 1,
searchKey: inputValue
});
},
{
enabled: !!appId
}
);
return (
<Box m={['0 auto', '10px auto']} w={'100%'} maxW={['auto', 'min(800px, 100%)']} px={[0, 5]}>
<Box
@@ -248,6 +236,20 @@ const MessageInput = ({
borderTopColor: 'rgba(0,0,0,0.15)'
})}
>
{/* Chat input guide box */}
{chatInputGuide.open && (
<InputGuideBox
appId={appId}
text={inputValue}
onSelect={(e) => {
setValue('input', e);
}}
onSend={(e) => {
handleSend(e);
}}
/>
)}
{/* translate loading */}
<Flex
position={'absolute'}
@@ -266,21 +268,6 @@ const MessageInput = ({
{t('core.chat.Converting to text')}
</Flex>
{/* popup */}
{havInput && (
<QuestionGuide
guides={data?.list || []}
setDropdownValue={(value) => setValue('input', value)}
bottom={'100%'}
top={'auto'}
left={0}
right={0}
mb={2}
overflowY={'auto'}
boxShadow={'sm'}
/>
)}
{/* file preview */}
<Flex wrap={'wrap'} px={[2, 4]} userSelect={'none'}>
{fileList.map((item, index) => (
@@ -415,12 +402,7 @@ const MessageInput = ({
// @ts-ignore
e.key === 'a' && e.ctrlKey && e.target?.select();
if (
(isPc || window !== parent) &&
e.keyCode === 13 &&
!e.shiftKey &&
!(havInput && data?.list.length && data?.list.length > 0)
) {
if ((isPc || window !== parent) && e.keyCode === 13 && !e.shiftKey) {
handleSend();
e.preventDefault();
}
@@ -556,4 +538,4 @@ const MessageInput = ({
);
};
export default React.memo(MessageInput);
export default React.memo(ChatInput);

View File

@@ -0,0 +1,111 @@
import { Box, Flex } from '@chakra-ui/react';
import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useI18n } from '@/web/context/I18n';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { queryChatInputGuideList } from '@/web/core/chat/inputGuide/api';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next';
import HighlightText from '@fastgpt/web/components/common/String/HighlightText';
import { useChatProviderStore } from '../Provider';
export default function InputGuideBox({
appId,
text,
onSelect,
onSend
}: {
appId: string;
text: string;
onSelect: (text: string) => void;
onSend: (text: string) => void;
}) {
const { t } = useTranslation();
const { chatT } = useI18n();
const { chatInputGuide } = useChatProviderStore();
const { data = [] } = useRequest2(
async () => {
if (!text) return [];
return await queryChatInputGuideList(
{
appId,
searchKey: text
},
chatInputGuide.customUrl ? chatInputGuide.customUrl : undefined
);
},
{
refreshDeps: [text],
throttleWait: 300
}
);
const filterData = data.filter((item) => item !== text).slice(0, 5);
return filterData.length ? (
<Box
bg={'white'}
boxShadow={'lg'}
borderWidth={'1px'}
borderColor={'borderColor.base'}
p={2}
borderRadius={'md'}
position={'absolute'}
top={-3}
w={'100%'}
zIndex={150}
transform={'translateY(-100%)'}
>
<Flex alignItems={'center'} fontSize={'sm'} color={'myGray.600'} gap={2} mb={2} px={2}>
<MyIcon name={'union'} />
<Box>{chatT('Input guide')}</Box>
</Flex>
{data.map((item, index) => (
<Flex
alignItems={'center'}
as={'li'}
key={item}
px={4}
py={3}
borderRadius={'sm'}
cursor={'pointer'}
overflow={'auto'}
_notLast={{
mb: 1
}}
bg={'myGray.50'}
color={'myGray.600'}
_hover={{
bg: 'primary.50',
color: 'primary.600',
'.send-icon': {
display: 'block'
}
}}
onClick={() => onSelect(item)}
>
<Box fontSize={'sm'} flex={'1 0 0'}>
<HighlightText rawText={item} matchText={text} />
</Box>
<MyTooltip label={t('core.chat.markdown.Send Question')}>
<MyIcon
className="send-icon"
display={'none'}
name={'chatSend'}
boxSize={4}
color={'myGray.500'}
_hover={{
color: 'primary.600'
}}
onClick={(e) => {
e.stopPropagation();
onSend(item);
}}
/>
</MyTooltip>
</Flex>
))}
</Box>
) : null;
}

View File

@@ -2,17 +2,23 @@ import React, { useContext, createContext, useState, useMemo, useEffect, useCall
import { useAudioPlay } from '@/web/common/utils/voice';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { splitGuideModule } from '@fastgpt/global/core/workflow/utils';
import {
AppChatConfigType,
AppTTSConfigType,
AppWhisperConfigType,
ChatInputGuideConfigType,
VariableItemType
} from '@fastgpt/global/core/app/type';
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import {
defaultChatInputGuideConfig,
defaultTTSConfig,
defaultWhisperConfig
} from '@fastgpt/global/core/app/constants';
type useChatStoreType = OutLinkChatAuthProps & {
welcomeText: string;
variableNodes: VariableItemType[];
variableList: VariableItemType[];
questionGuide: boolean;
ttsConfig: AppTTSConfigType;
whisperConfig: AppWhisperConfigType;
@@ -38,10 +44,11 @@ type useChatStoreType = OutLinkChatAuthProps & {
chatHistories: ChatSiteItemType[];
setChatHistories: React.Dispatch<React.SetStateAction<ChatSiteItemType[]>>;
isChatting: boolean;
chatInputGuide: ChatInputGuideConfigType;
};
const StateContext = createContext<useChatStoreType>({
welcomeText: '',
variableNodes: [],
variableList: [],
questionGuide: false,
ttsConfig: {
type: 'none',
@@ -87,11 +94,15 @@ const StateContext = createContext<useChatStoreType>({
},
finishSegmentedAudio: function (): void {
throw new Error('Function not implemented.');
},
chatInputGuide: {
open: false,
customUrl: ''
}
});
export type ChatProviderProps = OutLinkChatAuthProps & {
userGuideModule?: StoreNodeItemType;
chatConfig?: AppChatConfigType;
// not chat test params
chatId?: string;
@@ -105,15 +116,19 @@ const Provider = ({
outLinkUid,
teamId,
teamToken,
userGuideModule,
chatConfig = {},
children
}: ChatProviderProps) => {
const [chatHistories, setChatHistories] = useState<ChatSiteItemType[]>([]);
const { welcomeText, variableNodes, questionGuide, ttsConfig, whisperConfig } = useMemo(
() => splitGuideModule(userGuideModule),
[userGuideModule]
);
const {
welcomeText = '',
variables = [],
questionGuide = false,
ttsConfig = defaultTTSConfig,
whisperConfig = defaultWhisperConfig,
chatInputGuide = defaultChatInputGuideConfig
} = useMemo(() => chatConfig, [chatConfig]);
// segment audio
const [audioPlayingChatId, setAudioPlayingChatId] = useState<string>();
@@ -150,7 +165,7 @@ const Provider = ({
teamId,
teamToken,
welcomeText,
variableNodes,
variableList: variables,
questionGuide,
ttsConfig,
whisperConfig,
@@ -167,7 +182,8 @@ const Provider = ({
setAudioPlayingChatId,
chatHistories,
setChatHistories,
isChatting
isChatting,
chatInputGuide
};
return <StateContext.Provider value={value}>{children}</StateContext.Provider>;

View File

@@ -1,98 +0,0 @@
import { Box, BoxProps, Flex } from '@chakra-ui/react';
import { EditorVariablePickerType } from '@fastgpt/web/components/common/Textarea/PromptEditor/type';
import React, { useCallback, useEffect } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useI18n } from '@/web/context/I18n';
export default function QuestionGuide({
guides,
setDropdownValue,
...props
}: {
guides: string[];
setDropdownValue?: (value: string) => void;
} & BoxProps) {
const [highlightedIndex, setHighlightedIndex] = React.useState(0);
const { appT } = useI18n();
const handleKeyDown = useCallback(
(event: any) => {
if (event.keyCode === 38) {
setHighlightedIndex((prevIndex) => Math.max(prevIndex - 1, 0));
} else if (event.keyCode === 40) {
setHighlightedIndex((prevIndex) => Math.min(prevIndex + 1, guides.length - 1));
} else if (event.keyCode === 13 && guides[highlightedIndex]) {
setDropdownValue?.(guides[highlightedIndex]);
event.preventDefault();
}
},
[highlightedIndex, setDropdownValue, guides]
);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);
return guides.length ? (
<Box
bg={'white'}
boxShadow={'lg'}
borderWidth={'1px'}
borderColor={'borderColor.base'}
p={2}
borderRadius={'md'}
position={'absolute'}
top={'100%'}
w={'auto'}
zIndex={99999}
maxH={'300px'}
overflow={'auto'}
className="nowheel"
{...props}
>
<Flex alignItems={'center'} fontSize={'sm'} color={'myGray.600'} gap={2} mb={2} px={2}>
<MyIcon name={'union'} />
<Box>{appT('modules.Input Guide')}</Box>
</Flex>
{guides.map((item, index) => (
<Flex
alignItems={'center'}
as={'li'}
key={item}
px={4}
py={3}
borderRadius={'sm'}
cursor={'pointer'}
maxH={'300px'}
overflow={'auto'}
_notLast={{
mb: 1
}}
{...(highlightedIndex === index
? {
bg: 'primary.50',
color: 'primary.600'
}
: {
bg: 'myGray.50',
color: 'myGray.600'
})}
onMouseDown={(e) => {
e.preventDefault();
setDropdownValue?.(item);
}}
onMouseEnter={() => {
setHighlightedIndex(index);
}}
>
<Box fontSize={'sm'}>{item}</Box>
</Flex>
))}
</Box>
) : null;
}

View File

@@ -12,12 +12,12 @@ import { ChatBoxInputFormType } from '../type.d';
const VariableInput = ({
appAvatar,
variableNodes,
variableList,
chatForm,
onSubmitVariables
}: {
appAvatar?: string;
variableNodes: VariableItemType[];
variableList: VariableItemType[];
onSubmitVariables: (e: Record<string, any>) => void;
chatForm: UseFormReturn<ChatBoxInputFormType>;
}) => {
@@ -40,7 +40,7 @@ const VariableInput = ({
bg={'white'}
boxShadow={'0 0 8px rgba(0,0,0,0.15)'}
>
{variableNodes.map((item) => (
{variableList.map((item) => (
<Box key={item.id} mb={4}>
<Box as={'label'} display={'inline-block'} position={'relative'} mb={1}>
{item.label}

View File

@@ -45,7 +45,7 @@ import type {
ChatBoxInputType,
ChatBoxInputFormType
} from './type.d';
import MessageInput from './MessageInput';
import ChatInput from './Input/ChatInput';
import ChatBoxDivider from '../core/chat/Divider';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { getNanoid } from '@fastgpt/global/common/string/tools';
@@ -59,6 +59,7 @@ import ChatItem from './components/ChatItem';
import dynamic from 'next/dynamic';
import { useCreation } from 'ahooks';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
const ResponseTags = dynamic(() => import('./ResponseTags'));
const FeedbackModal = dynamic(() => import('./FeedbackModal'));
@@ -81,7 +82,7 @@ type Props = OutLinkChatAuthProps & {
showEmptyIntro?: boolean;
appAvatar?: string;
userAvatar?: string;
userGuideModule?: StoreNodeItemType;
chatConfig?: AppChatConfigType;
showFileSelector?: boolean;
active?: boolean; // can use
appId: string;
@@ -149,7 +150,7 @@ const ChatBox = (
const {
welcomeText,
variableNodes,
variableList,
questionGuide,
startSegmentedAudio,
finishSegmentedAudio,
@@ -174,8 +175,8 @@ const ChatBox = (
/* variable */
const filterVariableNodes = useCreation(
() => variableNodes.filter((item) => item.type !== VariableInputEnum.custom),
[variableNodes]
() => variableList.filter((item) => item.type !== VariableInputEnum.custom),
[variableList]
);
// 滚动到底部
@@ -390,9 +391,9 @@ const ChatBox = (
return;
}
// delete invalid variables 只保留在 variableNodes 中的变量
// delete invalid variables 只保留在 variableList 中的变量
const requestVariables: Record<string, any> = {};
variableNodes?.forEach((item) => {
variableList?.forEach((item) => {
requestVariables[item.key] = variables[item.key] || '';
});
@@ -566,7 +567,7 @@ const ChatBox = (
startSegmentedAudio,
t,
toast,
variableNodes
variableList
]
);
@@ -907,7 +908,7 @@ const ChatBox = (
{!!filterVariableNodes?.length && (
<VariableInput
appAvatar={appAvatar}
variableNodes={filterVariableNodes}
variableList={filterVariableNodes}
chatForm={chatForm}
onSubmitVariables={(data) => {
setValue('chatStarted', true);
@@ -1000,7 +1001,7 @@ const ChatBox = (
</Box>
{/* message input */}
{onStartChat && (chatStarted || filterVariableNodes.length === 0) && active && (
<MessageInput
<ChatInput
onSendMessage={sendPrompt}
onStop={() => chatController.current?.abort('stop')}
TextareaDom={TextareaDom}

View File

@@ -13,10 +13,19 @@ import Auth from './auth';
const Navbar = dynamic(() => import('./navbar'));
const NavbarPhone = dynamic(() => import('./navbarPhone'));
const UpdateInviteModal = dynamic(() => import('@/components/support/user/team/UpdateInviteModal'));
const NotSufficientModal = dynamic(() => import('@/components/support/wallet/NotSufficientModal'));
const SystemMsgModal = dynamic(() => import('@/components/support/user/inform/SystemMsgModal'));
const ImportantInform = dynamic(() => import('@/components/support/user/inform/ImportantInform'));
const UpdateInviteModal = dynamic(
() => import('@/components/support/user/team/UpdateInviteModal'),
{ ssr: false }
);
const NotSufficientModal = dynamic(() => import('@/components/support/wallet/NotSufficientModal'), {
ssr: false
});
const SystemMsgModal = dynamic(() => import('@/components/support/user/inform/SystemMsgModal'), {
ssr: false
});
const ImportantInform = dynamic(() => import('@/components/support/user/inform/ImportantInform'), {
ssr: false
});
const pcUnShowLayoutRoute: Record<string, boolean> = {
'/': true,
@@ -114,12 +123,17 @@ const Layout = ({ children }: { children: JSX.Element }) => {
</>
)}
</Box>
{!!userInfo && <UpdateInviteModal />}
{isNotSufficientModal && !isHideNavbar && <NotSufficientModal />}
{!!userInfo && <SystemMsgModal />}
{!!userInfo && importantInforms.length > 0 && (
<ImportantInform informs={importantInforms} refetch={refetchUnRead} />
{feConfigs?.isPlus && (
<>
{!!userInfo && <UpdateInviteModal />}
{isNotSufficientModal && !isHideNavbar && <NotSufficientModal />}
{!!userInfo && <SystemMsgModal />}
{!!userInfo && importantInforms.length > 0 && (
<ImportantInform informs={importantInforms} refetch={refetchUnRead} />
)}
</>
)}
<Loading loading={loading} zIndex={999999} />
</>
);

View File

@@ -3,24 +3,28 @@ import { Flex, Input, InputProps } from '@chakra-ui/react';
interface Props extends InputProps {
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
}
const MyInput = ({ leftIcon, ...props }: Props) => {
const MyInput = ({ leftIcon, rightIcon, ...props }: Props) => {
return (
<Flex position={'relative'} alignItems={'center'}>
<Input w={'100%'} pl={leftIcon ? '30px !important' : 3} {...props} />
<Flex h={'100%'} position={'relative'} alignItems={'center'}>
<Input
w={'100%'}
pl={leftIcon ? '34px !important' : 3}
pr={rightIcon ? '34px !important' : 3}
{...props}
/>
{leftIcon && (
<Flex
alignItems={'center'}
position={'absolute'}
left={3}
w={'20px'}
zIndex={10}
transform={'translateY(1.5px)'}
>
<Flex alignItems={'center'} position={'absolute'} left={3} w={'20px'} zIndex={10}>
{leftIcon}
</Flex>
)}
{rightIcon && (
<Flex alignItems={'center'} position={'absolute'} right={3} w={'20px'} zIndex={10}>
{rightIcon}
</Flex>
)}
</Flex>
);
};

View File

@@ -1,479 +0,0 @@
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
import {
Box,
Button,
Flex,
ModalBody,
useDisclosure,
Switch,
Input,
Textarea,
InputGroup,
InputRightElement,
Checkbox,
useCheckboxGroup,
ModalFooter,
BoxProps
} from '@chakra-ui/react';
import React, { ChangeEvent, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'next-i18next';
import type { AppQuestionGuideTextConfigType } from '@fastgpt/global/core/app/type.d';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import MyInput from '@/components/MyInput';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useI18n } from '@/web/context/I18n';
import { fileDownload } from '@/web/common/file/utils';
import { getDocPath } from '@/web/common/system/doc';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { getMyQuestionGuides } from '@/web/core/app/api';
import { getAppQGuideCustomURL } from '@/web/core/app/utils';
import { useQuery } from '@tanstack/react-query';
const csvTemplate = `"第一列内容"
"必填列"
"只会将第一列内容导入,其余列会被忽略"
"AIGC发展分为几个阶段"
`;
const QGuidesConfig = ({
value,
onChange
}: {
value: AppQuestionGuideTextConfigType;
onChange: (e: AppQuestionGuideTextConfigType) => void;
}) => {
const { t } = useTranslation();
const { appT, commonT } = useI18n();
const { isOpen, onOpen, onClose } = useDisclosure();
const { isOpen: isOpenTexts, onOpen: onOpenTexts, onClose: onCloseTexts } = useDisclosure();
const isOpenQuestionGuide = value.open;
const { appDetail } = useAppStore();
const [searchKey, setSearchKey] = React.useState<string>('');
const { data } = useQuery(
[appDetail._id, searchKey],
async () => {
return getMyQuestionGuides({
appId: appDetail._id,
customURL: getAppQGuideCustomURL(appDetail),
pageSize: 30,
current: 1,
searchKey
});
},
{
enabled: !!appDetail._id
}
);
useEffect(() => {
onChange({
...value,
textList: data?.list || []
});
}, [data]);
const formLabel = useMemo(() => {
if (!isOpenQuestionGuide) {
return t('core.app.whisper.Close');
}
return t('core.app.whisper.Open');
}, [t, isOpenQuestionGuide]);
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/inputGuides'} mr={2} w={'20px'} />
<Box fontWeight={'medium'}>{appT('modules.Question Guide')}</Box>
<Box flex={1} />
<MyTooltip label={appT('modules.Config question guide')}>
<Button
variant={'transparentBase'}
iconSpacing={1}
size={'sm'}
mr={'-5px'}
onClick={onOpen}
>
{formLabel}
</Button>
</MyTooltip>
<MyModal
title={appT('modules.Question Guide')}
iconSrc="core/app/inputGuides"
isOpen={isOpen}
onClose={onClose}
>
<ModalBody px={[5, 16]} pt={[4, 8]} w={'500px'}>
<Flex justifyContent={'space-between'} alignItems={'center'}>
{appT('modules.Question Guide Switch')}
<Switch
isChecked={isOpenQuestionGuide}
size={'lg'}
onChange={(e) => {
onChange({
...value,
open: e.target.checked
});
}}
/>
</Flex>
{isOpenQuestionGuide && (
<>
<Flex mt={8} alignItems={'center'}>
{appT('modules.Question Guide Texts')}
<Box fontSize={'xs'} px={2} bg={'myGray.100'} ml={1} rounded={'full'}>
{value.textList.length || 0}
</Box>
<Box flex={'1 0 0'} />
<Button
variant={'whiteBase'}
size={'sm'}
leftIcon={<MyIcon boxSize={'4'} name={'common/settingLight'} />}
onClick={() => {
onOpenTexts();
onClose();
}}
>
{appT('modules.Config Texts')}
</Button>
</Flex>
<>
<Flex mt={8} alignItems={'center'}>
{appT('modules.Custom question guide URL')}
<Flex
onClick={() => window.open(getDocPath('/docs/course/custom_link'))}
color={'primary.700'}
alignItems={'center'}
cursor={'pointer'}
>
<MyIcon name={'book'} ml={4} mr={1} />
{commonT('common.Documents')}
</Flex>
<Box flex={'1 0 0'} />
</Flex>
<Textarea
mt={2}
bg={'myGray.50'}
defaultValue={value.customURL}
onBlur={(e) =>
onChange({
...value,
customURL: e.target.value
})
}
/>
</>
</>
)}
</ModalBody>
<ModalFooter px={[5, 16]} pb={[4, 8]}>
<Button onClick={() => onClose()}>{commonT('common.Confirm')}</Button>
</ModalFooter>
</MyModal>
{isOpenTexts && (
<TextConfigModal
onCloseTexts={onCloseTexts}
onOpen={onOpen}
value={value}
onChange={onChange}
setSearchKey={setSearchKey}
/>
)}
</Flex>
);
};
export default React.memo(QGuidesConfig);
const TextConfigModal = ({
onCloseTexts,
onOpen,
value,
onChange,
setSearchKey
}: {
onCloseTexts: () => void;
onOpen: () => void;
value: AppQuestionGuideTextConfigType;
onChange: (e: AppQuestionGuideTextConfigType) => void;
setSearchKey: (key: string) => void;
}) => {
const { appT, commonT } = useI18n();
const fileInputRef = useRef<HTMLInputElement>(null);
const [checkboxValue, setCheckboxValue] = React.useState<string[]>([]);
const [isEditIndex, setIsEditIndex] = React.useState(-1);
const [isAdding, setIsAdding] = React.useState(false);
const [showIcons, setShowIcons] = React.useState<number | null>(null);
const { getCheckboxProps } = useCheckboxGroup();
const handleFileSelected = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e: ProgressEvent<FileReader>) => {
const content = e.target?.result as string;
const rows = content.split('\n');
const texts = rows.map((row) => row.split(',')[0]);
const newText = texts.filter((row) => value.textList.indexOf(row) === -1 && !!row);
onChange({
...value,
textList: [...newText, ...value.textList]
});
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
reader.readAsText(file);
}
};
const allSelected = useMemo(() => {
return value.textList.length === checkboxValue.length && value.textList.length !== 0;
}, [value.textList, checkboxValue]);
return (
<MyModal
title={appT('modules.Config Texts')}
iconSrc="core/app/inputGuides"
isOpen={true}
onClose={() => {
setCheckboxValue([]);
onCloseTexts();
onOpen();
}}
>
<ModalBody w={'500px'} px={0}>
<Flex gap={4} px={8} alignItems={'center'} borderBottom={'1px solid #E8EBF0'} pb={4}>
<Box flex={1}>
<MyInput
leftIcon={<MyIcon name={'common/searchLight'} boxSize={4} />}
bg={'myGray.50'}
w={'full'}
h={9}
placeholder={commonT('common.Search')}
onChange={(e) => setSearchKey(e.target.value)}
/>
</Box>
<Input
type="file"
accept=".csv"
style={{ display: 'none' }}
ref={fileInputRef}
onChange={handleFileSelected}
/>
<Button
onClick={() => {
fileInputRef.current?.click();
}}
variant={'whiteBase'}
size={'sm'}
leftIcon={<MyIcon name={'common/importLight'} boxSize={4} />}
>
{commonT('common.Import')}
</Button>
<Box
cursor={'pointer'}
onClick={() => {
fileDownload({
text: csvTemplate,
type: 'text/csv;charset=utf-8',
filename: 'questionGuide_template.csv'
});
}}
>
<QuestionTip ml={-2} label={appT('modules.Only support CSV')} />
</Box>
</Flex>
<Box mt={4}>
<Flex justifyContent={'space-between'} px={8}>
<Flex alignItems={'center'}>
<Checkbox
sx={{
'.chakra-checkbox__control': {
bg: allSelected ? 'primary.50' : 'none',
boxShadow: allSelected && '0 0 0 2px #F0F4FF',
_hover: {
bg: 'primary.50'
},
border: allSelected && '1px solid #3370FF',
color: 'primary.600'
},
svg: {
strokeWidth: '1px !important'
}
}}
value={'all'}
size={'lg'}
mr={2}
isChecked={allSelected}
onChange={(e) => {
if (e.target.checked) {
setCheckboxValue(value.textList);
} else {
setCheckboxValue([]);
}
}}
/>
<Box fontSize={'sm'} color={'myGray.600'} fontWeight={'medium'}>
{commonT('common.Select all')}
</Box>
</Flex>
<Flex gap={4}>
<Button
variant={'whiteBase'}
display={checkboxValue.length === 0 ? 'none' : 'flex'}
size={'sm'}
leftIcon={<MyIcon name={'delete'} boxSize={4} />}
onClick={() => {
setCheckboxValue([]);
onChange({
...value,
textList: value.textList.filter((_) => !checkboxValue.includes(_))
});
}}
>
{commonT('common.Delete')}
</Button>
<Button
display={checkboxValue.length !== 0 ? 'none' : 'flex'}
onClick={() => {
onChange({
...value,
textList: ['', ...value.textList]
});
setIsEditIndex(0);
setIsAdding(true);
}}
size={'sm'}
leftIcon={<MyIcon name={'common/addLight'} boxSize={4} />}
>
{commonT('common.Add')}
</Button>
</Flex>
</Flex>
<Box h={'400px'} pb={4} overflow={'auto'} px={8}>
{value.textList.map((text, index) => {
const selected = checkboxValue.includes(text);
return (
<Flex
key={index}
alignItems={'center'}
h={10}
mt={2}
onMouseEnter={() => setShowIcons(index)}
onMouseLeave={() => setShowIcons(null)}
>
<Checkbox
{...getCheckboxProps({ value: text })}
sx={{
'.chakra-checkbox__control': {
bg: selected ? 'primary.50' : 'none',
boxShadow: selected ? '0 0 0 2px #F0F4FF' : 'none',
_hover: {
bg: 'primary.50'
},
border: selected && '1px solid #3370FF',
color: 'primary.600'
},
svg: {
strokeWidth: '1px !important'
}
}}
size={'lg'}
mr={2}
isChecked={selected}
onChange={(e) => {
if (e.target.checked) {
setCheckboxValue([...checkboxValue, text]);
} else {
setCheckboxValue(checkboxValue.filter((_) => _ !== text));
}
}}
/>
{index === isEditIndex ? (
<InputGroup alignItems={'center'} h={'full'}>
<Input
autoFocus
h={'full'}
defaultValue={text}
onBlur={(e) => {
setIsEditIndex(-1);
if (
!e.target.value ||
(value.textList.indexOf(e.target.value) !== -1 &&
value.textList.indexOf(e.target.value) !== index)
) {
isAdding &&
onChange({
...value,
textList: value.textList.filter((_, i) => i !== index)
});
} else {
onChange({
...value,
textList: value.textList?.map((v, i) =>
i !== index ? v : e.target.value
)
});
}
setIsAdding(false);
}}
/>
<InputRightElement alignItems={'center'} pr={4} display={'flex'}>
<MyIcon name={'save'} boxSize={4} cursor={'pointer'} />
</InputRightElement>
</InputGroup>
) : (
<Flex
h={10}
w={'full'}
rounded={'md'}
px={4}
bg={'myGray.50'}
alignItems={'center'}
border={'1px solid #F0F1F6'}
_hover={{ border: '1px solid #94B5FF' }}
>
{text}
<Box flex={1} />
{checkboxValue.length === 0 && (
<Box display={showIcons === index ? 'flex' : 'none'}>
<MyIcon
name={'edit'}
boxSize={4}
mr={2}
color={'myGray.600'}
cursor={'pointer'}
onClick={() => setIsEditIndex(index)}
/>
<MyIcon
name={'delete'}
boxSize={4}
color={'myGray.600'}
cursor={'pointer'}
onClick={() => {
const temp = value.textList?.filter((_, i) => i !== index);
onChange({
...value,
textList: temp
});
}}
/>
</Box>
)}
</Flex>
)}
</Flex>
);
})}
</Box>
</Box>
</ModalBody>
</MyModal>
);
};

View File

@@ -92,20 +92,15 @@ const ScheduledTriggerConfig = ({
value,
onChange
}: {
value: AppScheduledTriggerConfigType | null;
onChange: (e: AppScheduledTriggerConfigType | null) => void;
value?: AppScheduledTriggerConfigType;
onChange: (e?: AppScheduledTriggerConfigType) => void;
}) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const { register, setValue, watch } = useForm<AppScheduledTriggerConfigType>({
defaultValues: {
cronString: value?.cronString || '',
timezone: value?.timezone,
defaultPrompt: value?.defaultPrompt || ''
}
});
const timezone = watch('timezone');
const cronString = watch('cronString');
const timezone = value?.timezone;
const cronString = value?.cronString;
const defaultPrompt = value?.defaultPrompt || '';
const cronSelectList = useRef<MultipleSelectProps['list']>([
{
@@ -130,15 +125,39 @@ const ScheduledTriggerConfig = ({
}
]);
const onUpdate = useCallback(
({
cronString,
timezone,
defaultPrompt
}: {
cronString?: string;
timezone?: string;
defaultPrompt?: string;
}) => {
if (!cronString) {
onChange(undefined);
return;
}
onChange({
...value,
cronString,
timezone: timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
defaultPrompt: defaultPrompt || ''
});
},
[onChange, value]
);
/* cron string to config field */
const cronConfig = useMemo(() => {
if (!cronString) {
return null;
return;
}
const cronField = cronParser2Fields(cronString);
if (!cronField) {
return null;
return;
}
if (cronField.dayOfMonth.length !== 31) {
@@ -169,19 +188,22 @@ const ScheduledTriggerConfig = ({
const cronConfig2cronString = useCallback(
(e: CronFieldType) => {
if (e[0] === CronJobTypeEnum.month) {
setValue('cronString', `0 ${e[2]} ${e[1]} * *`);
} else if (e[0] === CronJobTypeEnum.week) {
setValue('cronString', `0 ${e[2]} * * ${e[1]}`);
} else if (e[0] === CronJobTypeEnum.day) {
setValue('cronString', `0 ${e[1]} * * *`);
} else if (e[0] === CronJobTypeEnum.interval) {
setValue('cronString', `0 */${e[1]} * * *`);
} else {
setValue('cronString', '');
}
const str = (() => {
if (e[0] === CronJobTypeEnum.month) {
return `0 ${e[2]} ${e[1]} * *`;
} else if (e[0] === CronJobTypeEnum.week) {
return `0 ${e[2]} * * ${e[1]}`;
} else if (e[0] === CronJobTypeEnum.day) {
return `0 ${e[1]} * * *`;
} else if (e[0] === CronJobTypeEnum.interval) {
return `0 */${e[1]} * * *`;
} else {
return '';
}
})();
onUpdate({ cronString: str });
},
[setValue]
[onUpdate]
);
// cron config to show label
@@ -216,22 +238,9 @@ const ScheduledTriggerConfig = ({
return t('common.Not open');
}, [cronField, isOpenSchedule, t]);
// update value
watch((data) => {
if (!data.cronString) {
onChange(null);
return;
}
onChange({
cronString: data.cronString,
timezone: data.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
defaultPrompt: data.defaultPrompt || ''
});
});
useEffect(() => {
if (!value?.timezone) {
setValue('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);
onUpdate({ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone });
}
}, []);
@@ -272,9 +281,9 @@ const ScheduledTriggerConfig = ({
isChecked={isOpenSchedule}
onChange={(e) => {
if (e.target.checked) {
setValue('cronString', defaultCronString);
onUpdate({ cronString: defaultCronString });
} else {
setValue('cronString', '');
onUpdate({ cronString: '' });
}
}}
/>
@@ -300,7 +309,7 @@ const ScheduledTriggerConfig = ({
<TimezoneSelect
value={timezone}
onChange={(e) => {
setValue('timezone', e);
onUpdate({ timezone: e });
}}
/>
</Box>
@@ -308,10 +317,13 @@ const ScheduledTriggerConfig = ({
<Box mt={5}>
<Box>{t('core.app.schedule.Default prompt')}</Box>
<Textarea
{...register('defaultPrompt')}
value={defaultPrompt}
rows={8}
bg={'myGray.50'}
placeholder={t('core.app.schedule.Default prompt placeholder')}
onChange={(e) => {
onUpdate({ defaultPrompt: e.target.value });
}}
/>
</Box>
</>
@@ -323,13 +335,13 @@ const ScheduledTriggerConfig = ({
}, [
cronConfig2cronString,
cronField,
defaultPrompt,
formatLabel,
isOpen,
isOpenSchedule,
onClose,
onOpen,
register,
setValue,
onUpdate,
t,
timezone
]);

View File

@@ -11,12 +11,13 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MySlider from '@/components/Slider';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { defaultTTSConfig } from '@fastgpt/global/core/app/constants';
const TTSSelect = ({
value,
value = defaultTTSConfig,
onChange
}: {
value: AppTTSConfigType;
value?: AppTTSConfigType;
onChange: (e: AppTTSConfigType) => void;
}) => {
const { t } = useTranslation();

View File

@@ -39,10 +39,10 @@ import MyRadio from '@/components/common/MyRadio';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
const VariableEdit = ({
variables,
variables = [],
onChange
}: {
variables: VariableItemType[];
variables?: VariableItemType[];
onChange: (data: VariableItemType[]) => void;
}) => {
const { t } = useTranslation();

View File

@@ -6,14 +6,15 @@ import { useTranslation } from 'next-i18next';
import type { AppWhisperConfigType } from '@fastgpt/global/core/app/type.d';
import MyModal from '@fastgpt/web/components/common/MyModal';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { defaultWhisperConfig } from '@fastgpt/global/core/app/constants';
const WhisperConfig = ({
isOpenAudio,
value,
value = defaultWhisperConfig,
onChange
}: {
isOpenAudio: boolean;
value: AppWhisperConfigType;
value?: AppWhisperConfigType;
onChange: (e: AppWhisperConfigType) => void;
}) => {
const { t } = useTranslation();

View File

@@ -0,0 +1,482 @@
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
import {
Box,
Button,
Flex,
ModalBody,
useDisclosure,
Switch,
Textarea,
Checkbox,
HStack
} from '@chakra-ui/react';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import type { ChatInputGuideConfigType } from '@fastgpt/global/core/app/type.d';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyInput from '@/components/MyInput';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useI18n } from '@/web/context/I18n';
import { fileDownload } from '@/web/common/file/utils';
import { getDocPath } from '@/web/common/system/doc';
import {
delChatInputGuide,
getChatInputGuideList,
getCountChatInputGuideTotal,
postChatInputGuides,
putChatInputGuide
} from '@/web/core/chat/inputGuide/api';
import { useQuery } from '@tanstack/react-query';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { readCsvRawText } from '@fastgpt/web/common/file/utils';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useRequest } from 'ahooks';
import HighlightText from '@fastgpt/web/components/common/String/HighlightText';
import { defaultChatInputGuideConfig } from '@fastgpt/global/core/app/constants';
const csvTemplate = `"第一列内容"
"只会将第一列内容导入,其余列会被忽略"
"AIGC发展分为几个阶段"`;
const InputGuideConfig = ({
appId,
value = defaultChatInputGuideConfig,
onChange
}: {
appId: string;
value?: ChatInputGuideConfigType;
onChange: (e: ChatInputGuideConfigType) => void;
}) => {
const { t } = useTranslation();
const { chatT, commonT } = useI18n();
const { isOpen, onOpen, onClose } = useDisclosure();
const {
isOpen: isOpenLexiconConfig,
onOpen: onOpenLexiconConfig,
onClose: onCloseLexiconConfig
} = useDisclosure();
const isOpenQuestionGuide = value.open;
const { data } = useQuery(
[appId, isOpenLexiconConfig],
() => {
return getCountChatInputGuideTotal({
appId
});
},
{
enabled: !!appId
}
);
const total = data?.total || 0;
const formLabel = useMemo(() => {
if (!isOpenQuestionGuide) {
return t('core.app.whisper.Close');
}
return t('core.app.whisper.Open');
}, [t, isOpenQuestionGuide]);
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/inputGuides'} mr={2} w={'20px'} />
<HStack>
<Box>{chatT('Input guide')}</Box>
<QuestionTip label={chatT('Input guide tip')} />
</HStack>
<Box flex={1} />
<MyTooltip label={chatT('Config input guide')}>
<Button
variant={'transparentBase'}
iconSpacing={1}
size={'sm'}
mr={'-5px'}
onClick={onOpen}
>
{formLabel}
</Button>
</MyTooltip>
<MyModal
title={chatT('Input guide')}
iconSrc="core/app/inputGuides"
isOpen={isOpen}
onClose={onClose}
>
<ModalBody px={[5, 16]} py={[4, 8]} w={'500px'}>
<Flex justifyContent={'space-between'} alignItems={'center'}>
{t('Is open')}
<Switch
isChecked={isOpenQuestionGuide}
size={'lg'}
onChange={(e) => {
onChange({
...value,
open: e.target.checked
});
}}
/>
</Flex>
{isOpenQuestionGuide && (
<>
<Flex mt={8} alignItems={'center'}>
{chatT('Input guide lexicon')}
<Box fontSize={'xs'} px={2} bg={'myGray.100'} ml={1} rounded={'full'}>
{total}
</Box>
<Box flex={'1 0 0'} />
<Button
variant={'whiteBase'}
size={'sm'}
leftIcon={<MyIcon boxSize={'4'} name={'common/settingLight'} />}
onClick={() => {
onOpenLexiconConfig();
}}
>
{chatT('Config input guide lexicon')}
</Button>
</Flex>
<>
<Flex mt={8} alignItems={'center'}>
{chatT('Custom input guide url')}
<Flex
onClick={() => window.open(getDocPath('/docs/course/chat_input_guide'))}
color={'primary.700'}
alignItems={'center'}
cursor={'pointer'}
>
<MyIcon name={'book'} ml={4} mr={1} />
{commonT('common.Documents')}
</Flex>
<Box flex={'1 0 0'} />
</Flex>
<Textarea
mt={2}
bg={'myGray.50'}
defaultValue={value.customUrl}
onBlur={(e) =>
onChange({
...value,
customUrl: e.target.value
})
}
/>
</>
</>
)}
</ModalBody>
</MyModal>
{isOpenLexiconConfig && <LexiconConfigModal appId={appId} onClose={onCloseLexiconConfig} />}
</Flex>
);
};
export default React.memo(InputGuideConfig);
const LexiconConfigModal = ({ appId, onClose }: { appId: string; onClose: () => void }) => {
const { chatT, commonT } = useI18n();
const { t } = useTranslation();
const { toast } = useToast();
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.csv'
});
const [newData, setNewData] = useState<string>();
const [selectedRows, setSelectedRows] = useState<string[]>([]);
const [editDataId, setEditDataId] = useState<string>();
const [searchKey, setSearchKey] = useState('');
const {
list,
setData,
ScrollList,
isLoading: isRequesting,
fetchData,
scroll2Top
} = useScrollPagination(getChatInputGuideList, {
refreshDeps: [searchKey],
debounceWait: 300,
itemHeight: 46,
overscan: 20,
pageSize: 20,
defaultParams: {
appId,
searchKey
}
});
const { run: createNewData, loading: isCreating } = useRequest2(
(textList: string[]) => {
if (textList.filter(Boolean).length === 0) {
return Promise.resolve();
}
scroll2Top();
return postChatInputGuides({
appId,
textList
}).then((res) => {
if (res.insertLength < textList.length) {
toast({
status: 'warning',
title: chatT('Insert input guide, Some data already exists', { len: res.insertLength })
});
} else {
toast({
status: 'success',
title: t('common.Add Success')
});
}
fetchData(1);
});
},
{
manual: true,
onSuccess() {
setNewData(undefined);
},
errorToast: t('error.Create failed')
}
);
const onUpdateData = ({ text, dataId }: { text: string; dataId: string }) => {
setData((state) =>
state.map((item) => {
if (item._id === dataId) {
return {
...item,
text
};
}
return item;
})
);
if (text) {
putChatInputGuide({
appId,
text,
dataId
});
}
setEditDataId(undefined);
};
const onDeleteData = (dataIdList: string[]) => {
setData((state) => state.filter((item) => !dataIdList.includes(item._id)));
delChatInputGuide({
appId,
dataIdList
});
};
const onSelectFile = async (files: File[]) => {
const file = files?.[0];
if (file) {
const list = await readCsvRawText({ file });
const textList = list.map((item) => item[0]?.trim() || '').filter(Boolean);
createNewData(textList);
}
};
const isLoading = isRequesting || isCreating;
return (
<MyModal
title={chatT('Config input guide lexicon title')}
iconSrc="core/app/inputGuides"
isOpen={true}
onClose={onClose}
isLoading={isLoading}
h={'600px'}
w={'500px'}
>
<Flex gap={4} px={8} py={4} mb={4} alignItems={'center'} borderBottom={'base'}>
<Box flex={1}>
<MyInput
leftIcon={<MyIcon name={'common/searchLight'} boxSize={4} color={'myGray.500'} />}
bg={'myGray.50'}
w={'full'}
h={9}
placeholder={commonT('common.Search')}
onChange={(e) => setSearchKey(e.target.value)}
/>
</Box>
<Button
onClick={onOpenSelectFile}
variant={'whiteBase'}
size={'sm'}
leftIcon={<MyIcon name={'common/importLight'} boxSize={4} />}
>
{commonT('common.Import')}
</Button>
<Box
cursor={'pointer'}
onClick={() => {
fileDownload({
text: csvTemplate,
type: 'text/csv;charset=utf-8',
filename: 'questionGuide_template.csv'
});
}}
>
<QuestionTip ml={-2} label={chatT('Csv input lexicon tip')} />
</Box>
</Flex>
<Box px={8}>
{/* button */}
<Flex mb={1} justifyContent={'space-between'}>
<Box flex={1} />
<Flex gap={4}>
<Button
variant={'whiteBase'}
display={selectedRows.length === 0 ? 'none' : 'flex'}
size={'sm'}
leftIcon={<MyIcon name={'delete'} boxSize={4} />}
onClick={() => {
onDeleteData(selectedRows);
setSelectedRows([]);
}}
>
{commonT('common.Delete')}
</Button>
<Button
display={selectedRows.length !== 0 ? 'none' : 'flex'}
onClick={() => {
setNewData('');
}}
size={'sm'}
leftIcon={<MyIcon name={'common/addLight'} boxSize={4} />}
>
{commonT('common.Add')}
</Button>
</Flex>
</Flex>
{/* new data input */}
{newData !== undefined && (
<Box mt={5} ml={list.length > 0 ? 7 : 0}>
<MyInput
autoFocus
rightIcon={<MyIcon name={'save'} w={'14px'} cursor={'pointer'} />}
placeholder={chatT('New input guide lexicon')}
onBlur={(e) => {
createNewData([e.target.value.trim()]);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
createNewData([e.currentTarget.value.trim()]);
}
}}
/>
</Box>
)}
</Box>
<ScrollList
px={8}
flex={'1 0 0'}
EmptyChildren={<EmptyTip text={chatT('Chat input guide lexicon is empty')} />}
>
{list.map((data, index) => {
const item = data.data;
const selected = selectedRows.includes(item._id);
const edited = editDataId === item._id;
return (
<Flex
key={index}
alignItems={'center'}
h={10}
mt={3}
_hover={{
'& .icon-list': {
display: 'flex'
}
}}
>
<Checkbox
size={'lg'}
mr={2}
isChecked={selected}
onChange={(e) => {
if (e.target.checked) {
setSelectedRows([...selectedRows, item._id]);
} else {
setSelectedRows(selectedRows.filter((id) => id !== item._id));
}
}}
/>
{edited ? (
<Box h={'full'} flex={'1 0 0'}>
<MyInput
autoFocus
defaultValue={item.text}
rightIcon={<MyIcon name={'save'} boxSize={4} cursor={'pointer'} />}
onBlur={(e) => {
onUpdateData({
text: e.target.value.trim(),
dataId: item._id
});
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
onUpdateData({
text: e.currentTarget.value.trim(),
dataId: item._id
});
}
}}
/>
</Box>
) : (
<Flex
h={'40px'}
w={0}
flex={'1 0 0'}
rounded={'md'}
px={4}
bg={'myGray.50'}
alignItems={'center'}
border={'base'}
_hover={{ borderColor: 'primary.300' }}
>
<Box className="textEllipsis" w={0} flex={'1 0 0'}>
<HighlightText rawText={item.text} matchText={searchKey} />
</Box>
{selectedRows.length === 0 && (
<Box className="icon-list" display={'none'}>
<MyIcon
name={'edit'}
boxSize={4}
mr={2}
color={'myGray.600'}
cursor={'pointer'}
onClick={() => setEditDataId(item._id)}
/>
<MyIcon
name={'delete'}
boxSize={4}
color={'myGray.600'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
onClick={() => onDeleteData([item._id])}
/>
</Box>
)}
</Flex>
)}
</Flex>
);
})}
</ScrollList>
<File onSelect={onSelectFile} />
</MyModal>
);
};

View File

@@ -29,7 +29,8 @@ import {
initWorkflowEdgeStatus,
storeNodes2RuntimeNodes
} from '@fastgpt/global/core/workflow/runtime/utils';
import { getGuideModule } from '@fastgpt/global/core/workflow/utils';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
export type ChatTestComponentRef = {
resetChatTest: () => void;
@@ -37,13 +38,11 @@ export type ChatTestComponentRef = {
const ChatTest = (
{
app,
isOpen,
nodes = [],
edges = [],
onClose
}: {
app: AppSchema;
isOpen: boolean;
nodes?: StoreNodeItemType[];
edges?: StoreEdgeItemType[];
@@ -54,6 +53,7 @@ const ChatTest = (
const { t } = useTranslation();
const ChatBoxRef = useRef<ComponentRef>(null);
const { userInfo } = useUserStore();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const startChat = useCallback(
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
@@ -70,8 +70,8 @@ const ChatTest = (
nodes: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)),
edges: initWorkflowEdgeStatus(edges),
variables,
appId: app._id,
appName: `调试-${app.name}`,
appId: appDetail._id,
appName: `调试-${appDetail.name}`,
mode: 'test'
},
onMessage: generatingMessage,
@@ -80,7 +80,7 @@ const ChatTest = (
return { responseText, responseData, newVariables };
},
[app._id, app.name, edges, nodes]
[appDetail._id, appDetail.name, edges, nodes]
);
useImperativeHandle(ref, () => ({
@@ -139,11 +139,11 @@ const ChatTest = (
<Box flex={1}>
<ChatBox
ref={ChatBoxRef}
appId={app._id}
appAvatar={app.avatar}
appId={appDetail._id}
appAvatar={appDetail.avatar}
userAvatar={userInfo?.avatar}
showMarkIcon
userGuideModule={getGuideModule(nodes)}
chatConfig={appDetail.chatConfig}
showFileSelector={checkChatSupportSelectFileByModules(nodes)}
onStartChat={startChat}
onDelMessage={() => {}}

View File

@@ -5,7 +5,7 @@ import { useCallback, useState } from 'react';
import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { flowNode2StoreNodes } from '../../utils';
import { uiWorkflow2StoreWorkflow } from '../../utils';
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import dynamic from 'next/dynamic';
@@ -52,7 +52,7 @@ export const useDebug = () => {
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
if (!checkResults) {
const storeNodes = flowNode2StoreNodes({ nodes, edges });
const storeNodes = uiWorkflow2StoreWorkflow({ nodes, edges });
return JSON.stringify(storeNodes);
} else {

View File

@@ -39,6 +39,7 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { useMemoizedFn } from 'ahooks';
import { AppContext } from '@/web/core/app/context/appContext';
const CurlImportModal = dynamic(() => import('./CurlImportModal'));
export const HttpHeaders = [
@@ -251,6 +252,7 @@ export function RenderHttpProps({
const { t } = useTranslation();
const [selectedTab, setSelectedTab] = useState(TabEnum.params);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const requestMethods = inputs.find((item) => item.key === NodeInputKeyEnum.httpMethod)?.value;
const params = inputs.find((item) => item.key === NodeInputKeyEnum.httpParams);
@@ -262,7 +264,11 @@ export function RenderHttpProps({
// get variable
const variables = useMemo(() => {
const globalVariables = getWorkflowGlobalVariables(nodeList, t);
const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
const moduleVariables = formatEditorVariablePickerIcon(
inputs

View File

@@ -30,6 +30,7 @@ import { SourceHandle } from '../render/Handle';
import { Position, useReactFlow } from 'reactflow';
import { getReferenceDataValueType } from '@/web/core/workflow/utils';
import DragIcon from '@fastgpt/web/components/common/DndDrag/DragIcon';
import { AppContext } from '@/web/core/app/context/appContext';
const ListItem = ({
provided,
@@ -342,15 +343,17 @@ const ConditionSelect = ({
}) => {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
// get condition type
const valueType = useMemo(() => {
return getReferenceDataValueType({
variable,
nodeList,
chatConfig: appDetail.chatConfig,
t
});
}, [nodeList, t, variable]);
}, [appDetail.chatConfig, nodeList, t, variable]);
const conditionList = useMemo(() => {
if (valueType === WorkflowIOValueTypeEnum.string) return stringConditionList;

View File

@@ -1,16 +1,15 @@
import React, { useMemo, useTransition } from 'react';
import React, { Dispatch, useMemo, useTransition } from 'react';
import { NodeProps } from 'reactflow';
import { Box, Flex, Textarea, useTheme } from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { FlowNodeItemType, StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { welcomeTextTip } from '@fastgpt/global/core/workflow/template/tip';
import QGSwitch from '@/components/core/app/QGSwitch';
import TTSSelect from '@/components/core/app/TTSSelect';
import WhisperConfig from '@/components/core/app/WhisperConfig';
import QGuidesConfig from '@/components/core/app/QGuidesConfig';
import { splitGuideModule } from '@fastgpt/global/core/workflow/utils';
import InputGuideConfig from '@/components/core/chat/appConfig/InputGuideConfig';
import { getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import { TTSTypeEnum } from '@/web/core/app/constants';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -19,12 +18,35 @@ import NodeCard from './render/NodeCard';
import ScheduledTriggerConfig from '@/components/core/app/ScheduledTriggerConfig';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { VariableItemType } from '@fastgpt/global/core/app/type';
import { AppChatConfigType, AppDetailType, VariableItemType } from '@fastgpt/global/core/app/type';
import { useMemoizedFn } from 'ahooks';
import VariableEdit from '@/components/core/app/VariableEdit';
import { AppContext } from '@/web/core/app/context/appContext';
type ComponentProps = {
chatConfig: AppChatConfigType;
setAppDetail: Dispatch<React.SetStateAction<AppDetailType>>;
};
const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const theme = useTheme();
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
const chatConfig = useMemo<AppChatConfigType>(() => {
return getAppChatConfig({
chatConfig: appDetail.chatConfig,
systemConfigNode: data,
isPublicFetch: true
});
}, [data, appDetail]);
const componentsProps = useMemo(
() => ({
chatConfig,
setAppDetail
}),
[chatConfig, setAppDetail]
);
return (
<>
@@ -40,24 +62,24 @@ const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
{...data}
>
<Box px={4} py={'10px'} position={'relative'} borderRadius={'md'} className="nodrag">
<WelcomeText data={data} />
<WelcomeText {...componentsProps} />
<Box pt={4}>
<ChatStartVariable data={data} />
<ChatStartVariable {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<TTSGuide data={data} />
<TTSGuide {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<WhisperGuide data={data} />
<WhisperGuide {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<QuestionGuide data={data} />
<QuestionGuide {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<ScheduledTrigger data={data} />
<ScheduledTrigger {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<QuestionInputGuide data={data} />
<QuestionInputGuide {...componentsProps} />
</Box>
</Box>
</NodeCard>
@@ -67,13 +89,9 @@ const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
export default React.memo(NodeUserGuide);
function WelcomeText({ data }: { data: FlowNodeItemType }) {
function WelcomeText({ chatConfig: { welcomeText }, setAppDetail }: ComponentProps) {
const { t } = useTranslation();
const { inputs, nodeId } = data;
const [, startTst] = useTransition();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const welcomeText = inputs.find((item) => item.key === NodeInputKeyEnum.welcomeText);
return (
<>
@@ -84,181 +102,136 @@ function WelcomeText({ data }: { data: FlowNodeItemType }) {
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
</Flex>
{welcomeText && (
<Textarea
className="nodrag"
rows={6}
fontSize={'12px'}
resize={'both'}
defaultValue={welcomeText.value}
bg={'myWhite.500'}
placeholder={t(welcomeTextTip)}
onChange={(e) => {
startTst(() => {
onChangeNode({
nodeId,
key: NodeInputKeyEnum.welcomeText,
type: 'updateInput',
value: {
...welcomeText,
value: e.target.value
}
});
});
}}
/>
)}
<Textarea
className="nodrag"
rows={6}
fontSize={'12px'}
resize={'both'}
defaultValue={welcomeText}
bg={'myWhite.500'}
placeholder={t(welcomeTextTip)}
onChange={(e) => {
startTst(() => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
welcomeText: e.target.value
}
}));
});
}}
/>
</>
);
}
function ChatStartVariable({ data }: { data: FlowNodeItemType }) {
const { inputs, nodeId } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const variables = useMemo(
() =>
(inputs.find((item) => item.key === NodeInputKeyEnum.variables)
?.value as VariableItemType[]) || [],
[inputs]
);
function ChatStartVariable({ chatConfig: { variables = [] }, setAppDetail }: ComponentProps) {
const updateVariables = useMemoizedFn((value: VariableItemType[]) => {
// update system config node
onChangeNode({
nodeId,
key: NodeInputKeyEnum.variables,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === NodeInputKeyEnum.variables),
value
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
variables: value
}
});
}));
});
return <VariableEdit variables={variables} onChange={(e) => updateVariables(e)} />;
}
function QuestionGuide({ data }: { data: FlowNodeItemType }) {
const { inputs, nodeId } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const questionGuide = useMemo(
() =>
(inputs.find((item) => item.key === NodeInputKeyEnum.questionGuide)?.value as boolean) ||
false,
[inputs]
);
function QuestionGuide({ chatConfig: { questionGuide = false }, setAppDetail }: ComponentProps) {
return (
<QGSwitch
isChecked={questionGuide}
size={'md'}
onChange={(e) => {
const value = e.target.checked;
onChangeNode({
nodeId,
key: NodeInputKeyEnum.questionGuide,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === NodeInputKeyEnum.questionGuide),
value
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
questionGuide: value
}
});
}));
}}
/>
);
}
function TTSGuide({ data }: { data: FlowNodeItemType }) {
const { inputs, nodeId } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { ttsConfig } = splitGuideModule({ inputs } as StoreNodeItemType);
function TTSGuide({ chatConfig: { ttsConfig }, setAppDetail }: ComponentProps) {
return (
<TTSSelect
value={ttsConfig}
onChange={(e) => {
onChangeNode({
nodeId,
key: NodeInputKeyEnum.tts,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === NodeInputKeyEnum.tts),
value: e
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
ttsConfig: e
}
});
}));
}}
/>
);
}
function WhisperGuide({ data }: { data: FlowNodeItemType }) {
const { inputs, nodeId } = data;
function WhisperGuide({ chatConfig: { whisperConfig, ttsConfig }, setAppDetail }: ComponentProps) {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { ttsConfig, whisperConfig } = splitGuideModule({ inputs } as StoreNodeItemType);
return (
<WhisperConfig
isOpenAudio={ttsConfig.type !== TTSTypeEnum.none}
isOpenAudio={ttsConfig?.type !== TTSTypeEnum.none}
value={whisperConfig}
onChange={(e) => {
onChangeNode({
nodeId,
key: NodeInputKeyEnum.whisper,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === NodeInputKeyEnum.whisper),
value: e
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
whisperConfig: e
}
});
}));
}}
/>
);
}
function ScheduledTrigger({ data }: { data: FlowNodeItemType }) {
const { inputs, nodeId } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { scheduledTriggerConfig } = splitGuideModule({ inputs } as StoreNodeItemType);
function ScheduledTrigger({
chatConfig: { scheduledTriggerConfig },
setAppDetail
}: ComponentProps) {
return (
<ScheduledTriggerConfig
value={scheduledTriggerConfig}
onChange={(e) => {
onChangeNode({
nodeId,
key: NodeInputKeyEnum.scheduleTrigger,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === NodeInputKeyEnum.scheduleTrigger),
value: e
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
scheduledTriggerConfig: e
}
});
}));
}}
/>
);
}
function QuestionInputGuide({ data }: { data: FlowNodeItemType }) {
const { inputs, nodeId } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { questionGuideText } = splitGuideModule({ inputs } as StoreNodeItemType);
function QuestionInputGuide({ chatConfig: { chatInputGuide }, setAppDetail }: ComponentProps) {
const appId = useContextSelector(WorkflowContext, (v) => v.appId);
return (
<QGuidesConfig
value={questionGuideText}
return appId ? (
<InputGuideConfig
appId={appId}
value={chatInputGuide}
onChange={(e) => {
onChangeNode({
nodeId,
key: NodeInputKeyEnum.questionGuideText,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === NodeInputKeyEnum.questionGuideText),
value: e
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
chatInputGuide: e
}
});
}));
}}
/>
);
) : null;
}

View File

@@ -32,6 +32,7 @@ import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { ReferSelector, useReference } from './render/RenderInput/templates/Reference';
import { getReferenceDataValueType } from '@/web/core/workflow/utils';
import { isReferenceValue } from '@fastgpt/global/core/workflow/utils';
import { AppContext } from '@/web/core/app/context/appContext';
const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { inputs = [], nodeId } = data;
@@ -39,6 +40,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const updateList = useMemo(
() =>
@@ -85,6 +87,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
const valueType = getReferenceDataValueType({
variable: updateItem.variable,
nodeList,
chatConfig: appDetail.chatConfig,
t
});

View File

@@ -13,14 +13,20 @@ import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { AppContext } from '@/web/core/app/context/appContext';
const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, outputs } = data;
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const variablesOutputs = useCreation(() => {
const variables = getWorkflowGlobalVariables(nodeList, t);
const variables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
return variables.map<FlowNodeOutputItemType>((item) => ({
id: item.key,

View File

@@ -7,15 +7,21 @@ import { WorkflowContext } from '@/components/core/workflow/context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { useCreation } from 'ahooks';
import { useTranslation } from 'next-i18next';
import { AppContext } from '@/web/core/app/context/appContext';
const JsonEditor = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { appDetail } = useContextSelector(AppContext, (v) => v);
// get variable
const variables = useCreation(() => {
const globalVariables = getWorkflowGlobalVariables(nodeList, t);
const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
const moduleVariables = formatEditorVariablePickerIcon(
inputs

View File

@@ -14,6 +14,7 @@ import dynamic from 'next/dynamic';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppContext } from '@/web/core/app/context/appContext';
const MultipleRowSelect = dynamic(
() => import('@fastgpt/web/components/common/MySelect/MultipleRowSelect')
@@ -98,6 +99,7 @@ export const useReference = ({
value?: any;
}) => {
const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
@@ -106,6 +108,7 @@ export const useReference = ({
nodeId,
nodes: nodeList,
edges: edges,
chatConfig: appDetail.chatConfig,
t
});

View File

@@ -5,11 +5,6 @@ import MyModal from '@fastgpt/web/components/common/MyModal';
import { useForm } from 'react-hook-form';
import { PromptTemplateItem } from '@fastgpt/global/core/ai/type';
import { useTranslation } from 'next-i18next';
import {
formatEditorVariablePickerIcon,
getGuideModule,
splitGuideModule
} from '@fastgpt/global/core/workflow/utils';
import { ModalBody } from '@chakra-ui/react';
import MyTooltip from '@/components/MyTooltip';
import {
@@ -22,12 +17,12 @@ import PromptTemplate from '@/components/PromptTemplate';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Reference from './Reference';
import { getSystemVariables } from '@/web/core/app/utils';
import ValueTypeLabel from '../../ValueTypeLabel';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { useCreation } from 'ahooks';
import { AppContext } from '@/web/core/app/context/appContext';
const LabelStyles: BoxProps = {
fontSize: ['sm', 'md']
@@ -52,9 +47,14 @@ const SettingQuotePrompt = (props: RenderInputProps) => {
});
const aiChatQuoteTemplate = watch('quoteTemplate');
const aiChatQuotePrompt = watch('quotePrompt');
const { appDetail } = useContextSelector(AppContext, (v) => v);
const variables = useCreation(() => {
const globalVariables = getWorkflowGlobalVariables(nodeList, t);
const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
return globalVariables;
}, [nodeList, t]);

View File

@@ -7,15 +7,21 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { useCreation } from 'ahooks';
import { AppContext } from '@/web/core/app/context/appContext';
const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { appDetail } = useContextSelector(AppContext, (v) => v);
// get variable
const variables = useCreation(() => {
const globalVariables = getWorkflowGlobalVariables(nodeList, t);
const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
const moduleVariables = formatEditorVariablePickerIcon(
inputs

View File

@@ -8,7 +8,6 @@ import { Box, Button, Flex } from '@chakra-ui/react';
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
@@ -16,6 +15,7 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { AppContext } from '@/web/core/app/context/appContext';
const PublishHistoriesSlider = () => {
const { t } = useTranslation();
@@ -23,7 +23,7 @@ const PublishHistoriesSlider = () => {
content: t('core.workflow.publish.OnRevert version confirm')
});
const { appDetail, setAppDetail } = useAppStore();
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
const appId = useContextSelector(WorkflowContext, (e) => e.appId);
const setIsShowVersionHistories = useContextSelector(
WorkflowContext,
@@ -73,11 +73,11 @@ const PublishHistoriesSlider = () => {
editEdges: appDetail.edges
});
setAppDetail({
...appDetail,
setAppDetail((state) => ({
...state,
modules: data.nodes,
edges: data.edges
});
}));
onCloseSlider(data);
}

View File

@@ -13,7 +13,7 @@ import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/wor
import { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useCreation, useMemoizedFn } from 'ahooks';
import { useMemoizedFn } from 'ahooks';
import React, {
Dispatch,
SetStateAction,
@@ -32,11 +32,13 @@ import {
useEdgesState,
useNodesState
} from 'reactflow';
import { createContext } from 'use-context-selector';
import { createContext, useContextSelector } from 'use-context-selector';
import { defaultRunningStatus } from './constants';
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import { AppContext } from '@/web/core/app/context/appContext';
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
@@ -83,7 +85,11 @@ type WorkflowContextType = {
toolInputs: FlowNodeInputItemType[];
commonInputs: FlowNodeInputItemType[];
};
initData: (e: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => Promise<void>;
initData: (e: {
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
chatConfig?: AppChatConfigType;
}) => Promise<void>;
// debug
workflowDebugData:
@@ -223,6 +229,7 @@ const WorkflowContextProvider = ({
const { appId, pluginId } = value;
const { toast } = useToast();
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const setAppDetail = useContextSelector(AppContext, (v) => v.setAppDetail);
/* edge */
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
@@ -426,12 +433,18 @@ const WorkflowContextProvider = ({
};
};
const initData = useMemoizedFn(
async (e: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => {
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item })) || []);
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []);
const initData = useMemoizedFn(async (e: Parameters<WorkflowContextType['initData']>[0]) => {
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item })) || []);
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []);
const chatConfig = e.chatConfig;
if (chatConfig) {
setAppDetail((state) => ({
...state,
chatConfig
}));
}
);
});
/* debug */
const [workflowDebugData, setWorkflowDebugData] = useState<DebugDataType>();

View File

@@ -4,7 +4,7 @@ import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { FlowNodeItemType, StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { type Node, type Edge } from 'reactflow';
export const flowNode2StoreNodes = ({
export const uiWorkflow2StoreWorkflow = ({
nodes,
edges
}: {

View File

@@ -15,7 +15,7 @@ import {
import Avatar from '@/components/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '.';
import {

View File

@@ -7,7 +7,7 @@ import {
TeamMemberStatusMap
} from '@fastgpt/global/support/user/team/constant';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '.';
import { useUserStore } from '@/web/support/user/useUserStore';