mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 13:03:50 +00:00
4.8.10 test (#2618)
* perf: menu arrow ui * perf: http node placeholder * perf: http node form input * perf: chatBox performance
This commit is contained in:
@@ -49,7 +49,7 @@ const Markdown = ({
|
||||
const formatSource = useMemo(() => {
|
||||
const formatSource = source
|
||||
.replace(
|
||||
/([\u4e00-\u9fa5\u3000-\u303f])([\w\u0020-\u007e])|([a-zA-Z0-9\u0020-\u007e])([\u4e00-\u9fa5\u3000-\u303f])/g,
|
||||
/([\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');
|
||||
|
@@ -9,11 +9,13 @@ import { formatChatValue2InputType } from '../utils';
|
||||
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { ChatBoxContext } from '../Provider';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { SendPromptFnType } from '../type';
|
||||
|
||||
export type ChatControllerProps = {
|
||||
isLastChild: boolean;
|
||||
chat: ChatSiteItemType;
|
||||
showVoiceIcon?: boolean;
|
||||
onSendMessage: SendPromptFnType;
|
||||
onRetry?: () => void;
|
||||
onDelete?: () => void;
|
||||
onMark?: () => void;
|
||||
@@ -25,7 +27,6 @@ export type ChatControllerProps = {
|
||||
|
||||
const ChatController = ({
|
||||
chat,
|
||||
isLastChild,
|
||||
showVoiceIcon,
|
||||
onReadUserDislike,
|
||||
onCloseUserLike,
|
||||
|
@@ -6,7 +6,11 @@ import { MessageCardStyle } from '../constants';
|
||||
import { formatChatValue2InputType } from '../utils';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import styles from '../index.module.scss';
|
||||
import { ChatRoleEnum, ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import {
|
||||
ChatItemValueTypeEnum,
|
||||
ChatRoleEnum,
|
||||
ChatStatusEnum
|
||||
} from '@fastgpt/global/core/chat/constants';
|
||||
import FilesBlock from './FilesBox';
|
||||
import { ChatBoxContext } from '../Provider';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
@@ -16,6 +20,9 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { SendPromptFnType } from '../type';
|
||||
import { AIChatItemValueItemType, ChatItemValueItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { CodeClassNameEnum } from '@/components/Markdown/utils';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
const colorMap = {
|
||||
[ChatStatusEnum.loading]: {
|
||||
@@ -42,26 +49,81 @@ type BasicProps = {
|
||||
children?: React.ReactNode;
|
||||
} & ChatControllerProps;
|
||||
|
||||
type UserItemType = BasicProps & {
|
||||
type: ChatRoleEnum.Human;
|
||||
onSendMessage: undefined;
|
||||
};
|
||||
type AiItemType = BasicProps & {
|
||||
type: ChatRoleEnum.AI;
|
||||
type Props = BasicProps & {
|
||||
type: ChatRoleEnum.Human | ChatRoleEnum.AI;
|
||||
onSendMessage: SendPromptFnType;
|
||||
};
|
||||
type Props = UserItemType | AiItemType;
|
||||
|
||||
const ChatItem = ({
|
||||
type,
|
||||
avatar,
|
||||
statusBoxData,
|
||||
children,
|
||||
const RenderQuestionGuide = ({ questionGuides }: { questionGuides: string[] }) => {
|
||||
return (
|
||||
<Markdown
|
||||
source={`\`\`\`${CodeClassNameEnum.questionGuide}
|
||||
${JSON.stringify(questionGuides)}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const HumanContentCard = React.memo(
|
||||
function HumanContentCard({ chatValue }: { chatValue: ChatItemValueItemType[] }) {
|
||||
const { text, files = [] } = formatChatValue2InputType(chatValue);
|
||||
return (
|
||||
<Flex flexDirection={'column'} gap={4}>
|
||||
{files.length > 0 && <FilesBlock files={files} />}
|
||||
{text && <Markdown source={text} />}
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => isEqual(prevProps.chatValue, nextProps.chatValue)
|
||||
);
|
||||
const AIContentCard = React.memo(function AIContentCard({
|
||||
chatValue,
|
||||
dataId,
|
||||
isLastChild,
|
||||
questionGuides = [],
|
||||
isChatting,
|
||||
onSendMessage,
|
||||
...chatControllerProps
|
||||
}: Props) => {
|
||||
questionGuides
|
||||
}: {
|
||||
dataId: string;
|
||||
chatValue: ChatItemValueItemType[];
|
||||
isLastChild: boolean;
|
||||
isChatting: boolean;
|
||||
onSendMessage: SendPromptFnType;
|
||||
questionGuides: string[];
|
||||
}) {
|
||||
return (
|
||||
<Flex flexDirection={'column'} gap={2}>
|
||||
{chatValue.map((value, i) => {
|
||||
const key = `${dataId}-ai-${i}`;
|
||||
|
||||
return (
|
||||
<AIResponseBox
|
||||
key={key}
|
||||
value={value}
|
||||
isLastChild={isLastChild && i === chatValue.length - 1}
|
||||
isChatting={isChatting}
|
||||
onSendMessage={onSendMessage}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{isLastChild && questionGuides.length > 0 && (
|
||||
<RenderQuestionGuide questionGuides={questionGuides} />
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
const ChatItem = (props: Props) => {
|
||||
const {
|
||||
type,
|
||||
avatar,
|
||||
statusBoxData,
|
||||
children,
|
||||
isLastChild,
|
||||
questionGuides = [],
|
||||
onSendMessage,
|
||||
chat
|
||||
} = props;
|
||||
|
||||
const styleMap: BoxProps =
|
||||
type === ChatRoleEnum.Human
|
||||
? {
|
||||
@@ -81,59 +143,84 @@ const ChatItem = ({
|
||||
|
||||
const { t } = useTranslation();
|
||||
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
|
||||
const { chat } = chatControllerProps;
|
||||
|
||||
const { copyData } = useCopyData();
|
||||
const chatText = useMemo(() => formatChatValue2InputType(chat.value).text || '', [chat.value]);
|
||||
const ContentCard = useMemo(() => {
|
||||
if (type === 'Human') {
|
||||
const { text, files = [] } = formatChatValue2InputType(chat.value);
|
||||
return (
|
||||
<Flex flexDirection={'column'} gap={4}>
|
||||
{files.length > 0 && <FilesBlock files={files} />}
|
||||
{text && <Markdown source={text} />}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
/* AI */
|
||||
return (
|
||||
<Flex flexDirection={'column'} key={chat.dataId} gap={2}>
|
||||
{chat.value.map((value, i) => {
|
||||
const key = `${chat.dataId}-ai-${i}`;
|
||||
|
||||
return (
|
||||
<AIResponseBox
|
||||
key={key}
|
||||
value={value}
|
||||
index={i}
|
||||
chat={chat}
|
||||
isLastChild={isLastChild}
|
||||
isChatting={isChatting}
|
||||
questionGuides={questionGuides}
|
||||
onSendMessage={onSendMessage}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
}, [chat, isChatting, isLastChild, onSendMessage, questionGuides, type]);
|
||||
|
||||
const chatStatusMap = useMemo(() => {
|
||||
if (!statusBoxData?.status) return;
|
||||
return colorMap[statusBoxData.status];
|
||||
}, [statusBoxData?.status]);
|
||||
|
||||
/*
|
||||
1. The interactive node is divided into n dialog boxes.
|
||||
2. Auto-complete the last textnode
|
||||
*/
|
||||
const splitAiResponseResults = useMemo(() => {
|
||||
if (chat.obj !== ChatRoleEnum.AI) return [chat.value];
|
||||
|
||||
// Remove empty text node
|
||||
const filterList = chat.value.filter((item, i) => {
|
||||
if (item.type === ChatItemValueTypeEnum.text && !item.text?.content?.trim()) {
|
||||
return false;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
const groupedValues: AIChatItemValueItemType[][] = [];
|
||||
let currentGroup: AIChatItemValueItemType[] = [];
|
||||
|
||||
filterList.forEach((value) => {
|
||||
if (value.type === 'interactive') {
|
||||
if (currentGroup.length > 0) {
|
||||
groupedValues.push(currentGroup);
|
||||
currentGroup = [];
|
||||
}
|
||||
|
||||
groupedValues.push([value]);
|
||||
} else {
|
||||
currentGroup.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
if (currentGroup.length > 0) {
|
||||
groupedValues.push(currentGroup);
|
||||
}
|
||||
|
||||
// Check last group is interactive, Auto add a empty text node(animation)
|
||||
const lastGroup = groupedValues[groupedValues.length - 1];
|
||||
if (isChatting) {
|
||||
if (
|
||||
(lastGroup &&
|
||||
lastGroup[lastGroup.length - 1] &&
|
||||
lastGroup[lastGroup.length - 1].type === ChatItemValueTypeEnum.interactive) ||
|
||||
groupedValues.length === 0
|
||||
) {
|
||||
groupedValues.push([
|
||||
{
|
||||
type: ChatItemValueTypeEnum.text,
|
||||
text: {
|
||||
content: ''
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return groupedValues;
|
||||
}, [chat.obj, chat.value, isChatting]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* control icon */}
|
||||
<Flex w={'100%'} alignItems={'center'} gap={2} justifyContent={styleMap.justifyContent}>
|
||||
{isChatting && type === ChatRoleEnum.AI && isLastChild ? null : (
|
||||
<Box order={styleMap.order} ml={styleMap.ml}>
|
||||
<ChatController {...chatControllerProps} isLastChild={isLastChild} />
|
||||
<ChatController {...props} isLastChild={isLastChild} />
|
||||
</Box>
|
||||
)}
|
||||
<ChatAvatar src={avatar} type={type} />
|
||||
|
||||
{/* Workflow status */}
|
||||
{!!chatStatusMap && statusBoxData && isLastChild && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
@@ -158,50 +245,66 @@ const ChatItem = ({
|
||||
)}
|
||||
</Flex>
|
||||
{/* content */}
|
||||
<Box
|
||||
mt={['6px', 2]}
|
||||
className="chat-box-card"
|
||||
textAlign={styleMap.textAlign}
|
||||
_hover={{
|
||||
'& .footer-copy': {
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
{...MessageCardStyle}
|
||||
bg={styleMap.bg}
|
||||
borderRadius={styleMap.borderRadius}
|
||||
textAlign={'left'}
|
||||
{splitAiResponseResults.map((value, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
mt={['6px', 2]}
|
||||
className="chat-box-card"
|
||||
textAlign={styleMap.textAlign}
|
||||
_hover={{
|
||||
'& .footer-copy': {
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{ContentCard}
|
||||
{children}
|
||||
{/* 对话框底部的复制按钮 */}
|
||||
{type == ChatRoleEnum.AI && (!isChatting || (isChatting && !isLastChild)) && (
|
||||
<Box
|
||||
className="footer-copy"
|
||||
display={['block', 'none']}
|
||||
position={'absolute'}
|
||||
bottom={0}
|
||||
right={0}
|
||||
transform={'translateX(100%)'}
|
||||
>
|
||||
<MyTooltip label={t('common:common.Copy')}>
|
||||
<MyIcon
|
||||
w={'1rem'}
|
||||
cursor="pointer"
|
||||
p="5px"
|
||||
bg="white"
|
||||
name={'copy'}
|
||||
color={'myGray.500'}
|
||||
_hover={{ color: 'primary.600' }}
|
||||
onClick={() => copyData(chatText)}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
<Card
|
||||
{...MessageCardStyle}
|
||||
bg={styleMap.bg}
|
||||
borderRadius={styleMap.borderRadius}
|
||||
textAlign={'left'}
|
||||
>
|
||||
{type === ChatRoleEnum.Human && <HumanContentCard chatValue={value} />}
|
||||
{type === ChatRoleEnum.AI && (
|
||||
<AIContentCard
|
||||
chatValue={value}
|
||||
dataId={chat.dataId}
|
||||
isLastChild={isLastChild && i === splitAiResponseResults.length - 1}
|
||||
isChatting={isChatting}
|
||||
onSendMessage={onSendMessage}
|
||||
questionGuides={questionGuides}
|
||||
/>
|
||||
)}
|
||||
{/* Example: Response tags. A set of dialogs only needs to be displayed once*/}
|
||||
{i === splitAiResponseResults.length - 1 && <>{children}</>}
|
||||
{/* 对话框底部的复制按钮 */}
|
||||
{type == ChatRoleEnum.AI &&
|
||||
value[0]?.type !== 'interactive' &&
|
||||
(!isChatting || (isChatting && !isLastChild)) && (
|
||||
<Box
|
||||
className="footer-copy"
|
||||
display={['block', 'none']}
|
||||
position={'absolute'}
|
||||
bottom={0}
|
||||
right={0}
|
||||
transform={'translateX(100%)'}
|
||||
>
|
||||
<MyTooltip label={t('common:common.Copy')}>
|
||||
<MyIcon
|
||||
w={'1rem'}
|
||||
cursor="pointer"
|
||||
p="5px"
|
||||
bg="white"
|
||||
name={'copy'}
|
||||
color={'myGray.500'}
|
||||
_hover={{ color: 'primary.600' }}
|
||||
onClick={() => copyData(formatChatValue2InputType(value).text ?? '')}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -56,7 +56,7 @@ import dynamic from 'next/dynamic';
|
||||
import type { StreamResponseType } from '@/web/common/api/fetch';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useThrottleFn } from 'ahooks';
|
||||
import { useCreation, useMemoizedFn, useThrottleFn, useTrackedEffect } from 'ahooks';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
const ResponseTags = dynamic(() => import('./components/ResponseTags'));
|
||||
@@ -574,233 +574,205 @@ const ChatBox = (
|
||||
);
|
||||
|
||||
// retry input
|
||||
const retryInput = useCallback(
|
||||
(dataId?: string) => {
|
||||
if (!dataId || !onDelMessage) return;
|
||||
const retryInput = useMemoizedFn((dataId?: string) => {
|
||||
if (!dataId || !onDelMessage) return;
|
||||
|
||||
return async () => {
|
||||
setLoading(true);
|
||||
const index = chatHistories.findIndex((item) => item.dataId === dataId);
|
||||
const delHistory = chatHistories.slice(index);
|
||||
try {
|
||||
await Promise.all(
|
||||
delHistory.map((item) => {
|
||||
if (item.dataId) {
|
||||
return onDelMessage({ contentId: item.dataId });
|
||||
}
|
||||
})
|
||||
);
|
||||
setChatHistories((state) => (index === 0 ? [] : state.slice(0, index)));
|
||||
|
||||
sendPrompt({
|
||||
...formatChatValue2InputType(delHistory[0].value),
|
||||
history: chatHistories.slice(0, index)
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: getErrText(error, 'Retry failed')
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
},
|
||||
[chatHistories, onDelMessage, sendPrompt, setChatHistories, setLoading, toast]
|
||||
);
|
||||
// delete one message(One human and the ai response)
|
||||
const delOneMessage = useCallback(
|
||||
(dataId?: string) => {
|
||||
if (!dataId || !onDelMessage) return;
|
||||
return () => {
|
||||
setChatHistories((state) => {
|
||||
let aiIndex = -1;
|
||||
|
||||
return state.filter((chat, i) => {
|
||||
if (chat.dataId === dataId) {
|
||||
aiIndex = i + 1;
|
||||
onDelMessage({
|
||||
contentId: dataId
|
||||
});
|
||||
return false;
|
||||
} else if (aiIndex === i && chat.obj === ChatRoleEnum.AI && chat.dataId) {
|
||||
onDelMessage({
|
||||
contentId: chat.dataId
|
||||
});
|
||||
return false;
|
||||
return async () => {
|
||||
setLoading(true);
|
||||
const index = chatHistories.findIndex((item) => item.dataId === dataId);
|
||||
const delHistory = chatHistories.slice(index);
|
||||
try {
|
||||
await Promise.all(
|
||||
delHistory.map((item) => {
|
||||
if (item.dataId) {
|
||||
return onDelMessage({ contentId: item.dataId });
|
||||
}
|
||||
return true;
|
||||
});
|
||||
})
|
||||
);
|
||||
setChatHistories((state) => (index === 0 ? [] : state.slice(0, index)));
|
||||
|
||||
sendPrompt({
|
||||
...formatChatValue2InputType(delHistory[0].value),
|
||||
history: chatHistories.slice(0, index)
|
||||
});
|
||||
};
|
||||
},
|
||||
[onDelMessage, setChatHistories]
|
||||
);
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: getErrText(error, 'Retry failed')
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
});
|
||||
// delete one message(One human and the ai response)
|
||||
const delOneMessage = useMemoizedFn((dataId?: string) => {
|
||||
if (!dataId || !onDelMessage) return;
|
||||
return () => {
|
||||
setChatHistories((state) => {
|
||||
let aiIndex = -1;
|
||||
|
||||
return state.filter((chat, i) => {
|
||||
if (chat.dataId === dataId) {
|
||||
aiIndex = i + 1;
|
||||
onDelMessage({
|
||||
contentId: dataId
|
||||
});
|
||||
return false;
|
||||
} else if (aiIndex === i && chat.obj === ChatRoleEnum.AI && chat.dataId) {
|
||||
onDelMessage({
|
||||
contentId: chat.dataId
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
// admin mark
|
||||
const onMark = useCallback(
|
||||
(chat: ChatSiteItemType, q = '') => {
|
||||
if (!showMarkIcon || chat.obj !== ChatRoleEnum.AI) return;
|
||||
const onMark = useMemoizedFn((chat: ChatSiteItemType, q = '') => {
|
||||
if (!showMarkIcon || chat.obj !== ChatRoleEnum.AI) return;
|
||||
|
||||
return () => {
|
||||
if (!chat.dataId) return;
|
||||
return () => {
|
||||
if (!chat.dataId) return;
|
||||
|
||||
if (chat.adminFeedback) {
|
||||
setAdminMarkData({
|
||||
chatItemId: chat.dataId,
|
||||
datasetId: chat.adminFeedback.datasetId,
|
||||
collectionId: chat.adminFeedback.collectionId,
|
||||
dataId: chat.adminFeedback.dataId,
|
||||
q: chat.adminFeedback.q || q || '',
|
||||
a: chat.adminFeedback.a
|
||||
});
|
||||
} else {
|
||||
setAdminMarkData({
|
||||
chatItemId: chat.dataId,
|
||||
q,
|
||||
a: formatChatValue2InputType(chat.value).text
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
[showMarkIcon]
|
||||
);
|
||||
const onAddUserLike = useCallback(
|
||||
(chat: ChatSiteItemType) => {
|
||||
if (
|
||||
feedbackType !== FeedbackTypeEnum.user ||
|
||||
chat.obj !== ChatRoleEnum.AI ||
|
||||
chat.userBadFeedback
|
||||
)
|
||||
return;
|
||||
if (chat.adminFeedback) {
|
||||
setAdminMarkData({
|
||||
chatItemId: chat.dataId,
|
||||
datasetId: chat.adminFeedback.datasetId,
|
||||
collectionId: chat.adminFeedback.collectionId,
|
||||
dataId: chat.adminFeedback.dataId,
|
||||
q: chat.adminFeedback.q || q || '',
|
||||
a: chat.adminFeedback.a
|
||||
});
|
||||
} else {
|
||||
setAdminMarkData({
|
||||
chatItemId: chat.dataId,
|
||||
q,
|
||||
a: formatChatValue2InputType(chat.value).text
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
const onAddUserLike = useMemoizedFn((chat: ChatSiteItemType) => {
|
||||
if (
|
||||
feedbackType !== FeedbackTypeEnum.user ||
|
||||
chat.obj !== ChatRoleEnum.AI ||
|
||||
chat.userBadFeedback
|
||||
)
|
||||
return;
|
||||
return () => {
|
||||
if (!chat.dataId || !chatId || !appId) return;
|
||||
|
||||
const isGoodFeedback = !!chat.userGoodFeedback;
|
||||
setChatHistories((state) =>
|
||||
state.map((chatItem) =>
|
||||
chatItem.dataId === chat.dataId
|
||||
? {
|
||||
...chatItem,
|
||||
userGoodFeedback: isGoodFeedback ? undefined : 'yes'
|
||||
}
|
||||
: chatItem
|
||||
)
|
||||
);
|
||||
try {
|
||||
updateChatUserFeedback({
|
||||
appId,
|
||||
chatId,
|
||||
teamId,
|
||||
teamToken,
|
||||
chatItemId: chat.dataId,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
userGoodFeedback: isGoodFeedback ? undefined : 'yes'
|
||||
});
|
||||
} catch (error) {}
|
||||
};
|
||||
});
|
||||
const onCloseUserLike = useMemoizedFn((chat: ChatSiteItemType) => {
|
||||
if (feedbackType !== FeedbackTypeEnum.admin) return;
|
||||
return () => {
|
||||
if (!chat.dataId || !chatId || !appId) return;
|
||||
setChatHistories((state) =>
|
||||
state.map((chatItem) =>
|
||||
chatItem.dataId === chat.dataId ? { ...chatItem, userGoodFeedback: undefined } : chatItem
|
||||
)
|
||||
);
|
||||
updateChatUserFeedback({
|
||||
appId,
|
||||
teamId,
|
||||
teamToken,
|
||||
chatId,
|
||||
chatItemId: chat.dataId,
|
||||
userGoodFeedback: undefined
|
||||
});
|
||||
};
|
||||
});
|
||||
const onAddUserDislike = useMemoizedFn((chat: ChatSiteItemType) => {
|
||||
if (
|
||||
feedbackType !== FeedbackTypeEnum.user ||
|
||||
chat.obj !== ChatRoleEnum.AI ||
|
||||
chat.userGoodFeedback
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (chat.userBadFeedback) {
|
||||
return () => {
|
||||
if (!chat.dataId || !chatId || !appId) return;
|
||||
|
||||
const isGoodFeedback = !!chat.userGoodFeedback;
|
||||
setChatHistories((state) =>
|
||||
state.map((chatItem) =>
|
||||
chatItem.dataId === chat.dataId
|
||||
? {
|
||||
...chatItem,
|
||||
userGoodFeedback: isGoodFeedback ? undefined : 'yes'
|
||||
}
|
||||
: chatItem
|
||||
chatItem.dataId === chat.dataId ? { ...chatItem, userBadFeedback: undefined } : chatItem
|
||||
)
|
||||
);
|
||||
try {
|
||||
updateChatUserFeedback({
|
||||
appId,
|
||||
chatId,
|
||||
teamId,
|
||||
teamToken,
|
||||
chatItemId: chat.dataId,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
userGoodFeedback: isGoodFeedback ? undefined : 'yes'
|
||||
teamId,
|
||||
teamToken,
|
||||
outLinkUid
|
||||
});
|
||||
} catch (error) {}
|
||||
};
|
||||
},
|
||||
[appId, chatId, feedbackType, outLinkUid, setChatHistories, shareId, teamId, teamToken]
|
||||
);
|
||||
const onCloseUserLike = useCallback(
|
||||
(chat: ChatSiteItemType) => {
|
||||
if (feedbackType !== FeedbackTypeEnum.admin) return;
|
||||
return () => {
|
||||
if (!chat.dataId || !chatId || !appId) return;
|
||||
} else {
|
||||
return () => setFeedbackId(chat.dataId);
|
||||
}
|
||||
});
|
||||
const onReadUserDislike = useMemoizedFn((chat: ChatSiteItemType) => {
|
||||
if (feedbackType !== FeedbackTypeEnum.admin || chat.obj !== ChatRoleEnum.AI) return;
|
||||
return () => {
|
||||
if (!chat.dataId) return;
|
||||
setReadFeedbackData({
|
||||
chatItemId: chat.dataId || '',
|
||||
content: chat.userBadFeedback || ''
|
||||
});
|
||||
};
|
||||
});
|
||||
const onCloseCustomFeedback = useMemoizedFn((chat: ChatSiteItemType, i: number) => {
|
||||
return (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked && appId && chatId && chat.dataId) {
|
||||
closeCustomFeedback({
|
||||
appId,
|
||||
chatId,
|
||||
chatItemId: chat.dataId,
|
||||
index: i
|
||||
});
|
||||
// update dom
|
||||
setChatHistories((state) =>
|
||||
state.map((chatItem) =>
|
||||
chatItem.dataId === chat.dataId
|
||||
? { ...chatItem, userGoodFeedback: undefined }
|
||||
chatItem.obj === ChatRoleEnum.AI && chatItem.dataId === chat.dataId
|
||||
? {
|
||||
...chatItem,
|
||||
customFeedbacks: chatItem.customFeedbacks?.filter((_, index) => index !== i)
|
||||
}
|
||||
: chatItem
|
||||
)
|
||||
);
|
||||
updateChatUserFeedback({
|
||||
appId,
|
||||
teamId,
|
||||
teamToken,
|
||||
chatId,
|
||||
chatItemId: chat.dataId,
|
||||
userGoodFeedback: undefined
|
||||
});
|
||||
};
|
||||
},
|
||||
[appId, chatId, feedbackType, setChatHistories, teamId, teamToken]
|
||||
);
|
||||
const onAddUserDislike = useCallback(
|
||||
(chat: ChatSiteItemType) => {
|
||||
if (
|
||||
feedbackType !== FeedbackTypeEnum.user ||
|
||||
chat.obj !== ChatRoleEnum.AI ||
|
||||
chat.userGoodFeedback
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (chat.userBadFeedback) {
|
||||
return () => {
|
||||
if (!chat.dataId || !chatId || !appId) return;
|
||||
setChatHistories((state) =>
|
||||
state.map((chatItem) =>
|
||||
chatItem.dataId === chat.dataId
|
||||
? { ...chatItem, userBadFeedback: undefined }
|
||||
: chatItem
|
||||
)
|
||||
);
|
||||
try {
|
||||
updateChatUserFeedback({
|
||||
appId,
|
||||
chatId,
|
||||
chatItemId: chat.dataId,
|
||||
shareId,
|
||||
teamId,
|
||||
teamToken,
|
||||
outLinkUid
|
||||
});
|
||||
} catch (error) {}
|
||||
};
|
||||
} else {
|
||||
return () => setFeedbackId(chat.dataId);
|
||||
}
|
||||
},
|
||||
[appId, chatId, feedbackType, outLinkUid, setChatHistories, shareId, teamId, teamToken]
|
||||
);
|
||||
const onReadUserDislike = useCallback(
|
||||
(chat: ChatSiteItemType) => {
|
||||
if (feedbackType !== FeedbackTypeEnum.admin || chat.obj !== ChatRoleEnum.AI) return;
|
||||
return () => {
|
||||
if (!chat.dataId) return;
|
||||
setReadFeedbackData({
|
||||
chatItemId: chat.dataId || '',
|
||||
content: chat.userBadFeedback || ''
|
||||
});
|
||||
};
|
||||
},
|
||||
[feedbackType]
|
||||
);
|
||||
const onCloseCustomFeedback = useCallback(
|
||||
(chat: ChatSiteItemType, i: number) => {
|
||||
return (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked && appId && chatId && chat.dataId) {
|
||||
closeCustomFeedback({
|
||||
appId,
|
||||
chatId,
|
||||
chatItemId: chat.dataId,
|
||||
index: i
|
||||
});
|
||||
// update dom
|
||||
setChatHistories((state) =>
|
||||
state.map((chatItem) =>
|
||||
chatItem.obj === ChatRoleEnum.AI && chatItem.dataId === chat.dataId
|
||||
? {
|
||||
...chatItem,
|
||||
customFeedbacks: chatItem.customFeedbacks?.filter((_, index) => index !== i)
|
||||
}
|
||||
: chatItem
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
},
|
||||
[appId, chatId, setChatHistories]
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const showEmpty = useMemo(
|
||||
() =>
|
||||
@@ -817,7 +789,7 @@ const ChatBox = (
|
||||
welcomeText
|
||||
]
|
||||
);
|
||||
const statusBoxData = useMemo(() => {
|
||||
const statusBoxData = useCreation(() => {
|
||||
if (!isChatting) return;
|
||||
const chatContent = chatHistories[chatHistories.length - 1];
|
||||
if (!chatContent) return;
|
||||
@@ -883,10 +855,9 @@ const ChatBox = (
|
||||
});
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'} position={'relative'}>
|
||||
<Script src="/js/html2pdf.bundle.min.js" strategy="lazyOnload"></Script>
|
||||
{/* chat box container */}
|
||||
|
||||
const RenderRecords = useMemo(() => {
|
||||
return (
|
||||
<Box ref={ChatBoxRef} flex={'1 0 0'} h={0} w={'100%'} overflow={'overlay'} px={[4, 0]} pb={3}>
|
||||
<Box id="chat-container" maxW={['100%', '92%']} h={'100%'} mx={'auto'}>
|
||||
{showEmpty && <Empty />}
|
||||
@@ -907,7 +878,7 @@ const ChatBox = (
|
||||
onRetry={retryInput(item.dataId)}
|
||||
onDelete={delOneMessage(item.dataId)}
|
||||
isLastChild={index === chatHistories.length - 1}
|
||||
onSendMessage={undefined}
|
||||
onSendMessage={sendPrompt}
|
||||
/>
|
||||
)}
|
||||
{item.obj === ChatRoleEnum.AI && (
|
||||
@@ -986,6 +957,42 @@ const ChatBox = (
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}, [
|
||||
appAvatar,
|
||||
chatForm,
|
||||
chatHistories,
|
||||
chatStarted,
|
||||
delOneMessage,
|
||||
isChatting,
|
||||
onAddUserDislike,
|
||||
onAddUserLike,
|
||||
onCloseCustomFeedback,
|
||||
onCloseUserLike,
|
||||
onMark,
|
||||
onReadUserDislike,
|
||||
outLinkUid,
|
||||
questionGuides,
|
||||
retryInput,
|
||||
sendPrompt,
|
||||
shareId,
|
||||
showEmpty,
|
||||
showMarkIcon,
|
||||
showVoiceIcon,
|
||||
statusBoxData,
|
||||
t,
|
||||
teamId,
|
||||
teamToken,
|
||||
userAvatar,
|
||||
variableList?.length,
|
||||
welcomeText
|
||||
]);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'} position={'relative'}>
|
||||
<Script src="/js/html2pdf.bundle.min.js" strategy="lazyOnload"></Script>
|
||||
{/* chat box container */}
|
||||
{RenderRecords}
|
||||
{/* message input */}
|
||||
{onStartChat && chatStarted && active && appId && !isInteractive && (
|
||||
<ChatInput
|
||||
|
@@ -14,6 +14,7 @@ import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import {
|
||||
AIChatItemValueItemType,
|
||||
ChatSiteItemType,
|
||||
ToolModuleResponseItemType,
|
||||
UserChatItemValueItemType
|
||||
} from '@fastgpt/global/core/chat/type';
|
||||
import React, { useMemo } from 'react';
|
||||
@@ -23,190 +24,190 @@ import { SendPromptFnType } from '../ChatContainer/ChatBox/type';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatBoxContext } from '../ChatContainer/ChatBox/Provider';
|
||||
import { setUserSelectResultToHistories } from '../ChatContainer/ChatBox/utils';
|
||||
import { InteractiveNodeResponseItemType } from '@fastgpt/global/core/workflow/template/system/userSelect/type';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
type props = {
|
||||
value: UserChatItemValueItemType | AIChatItemValueItemType;
|
||||
index: number;
|
||||
chat: ChatSiteItemType;
|
||||
isLastChild: boolean;
|
||||
isChatting: boolean;
|
||||
questionGuides: string[];
|
||||
onSendMessage?: SendPromptFnType;
|
||||
onSendMessage: SendPromptFnType;
|
||||
};
|
||||
|
||||
const AIResponseBox = ({
|
||||
value,
|
||||
index,
|
||||
chat,
|
||||
isLastChild,
|
||||
isChatting,
|
||||
questionGuides,
|
||||
onSendMessage
|
||||
}: props) => {
|
||||
const chatHistories = useContextSelector(ChatBoxContext, (v) => v.chatHistories);
|
||||
const RenderText = React.memo(function RenderText({
|
||||
showAnimation,
|
||||
text
|
||||
}: {
|
||||
showAnimation: boolean;
|
||||
text?: string;
|
||||
}) {
|
||||
let source = (text || '').trim();
|
||||
|
||||
// Question guide
|
||||
const RenderQuestionGuide = useMemo(() => {
|
||||
if (
|
||||
isLastChild &&
|
||||
!isChatting &&
|
||||
questionGuides.length > 0 &&
|
||||
index === chat.value.length - 1
|
||||
) {
|
||||
return (
|
||||
<Markdown
|
||||
source={`\`\`\`${CodeClassNameEnum.questionGuide}
|
||||
${JSON.stringify(questionGuides)}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [chat.value.length, index, isChatting, isLastChild, questionGuides]);
|
||||
// First empty line
|
||||
// if (!source && !isLastChild) return null;
|
||||
|
||||
const Render = useMemo(() => {
|
||||
if (value.type === ChatItemValueTypeEnum.text && value.text) {
|
||||
let source = (value.text?.content || '').trim();
|
||||
return <Markdown source={source} showAnimation={showAnimation} />;
|
||||
});
|
||||
const RenderTool = React.memo(
|
||||
function RenderTool({
|
||||
showAnimation,
|
||||
tools
|
||||
}: {
|
||||
showAnimation: boolean;
|
||||
tools: ToolModuleResponseItemType[];
|
||||
}) {
|
||||
return (
|
||||
<Box>
|
||||
{tools.map((tool) => {
|
||||
const toolParams = (() => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(tool.params), null, 2);
|
||||
} catch (error) {
|
||||
return tool.params;
|
||||
}
|
||||
})();
|
||||
const toolResponse = (() => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(tool.response), null, 2);
|
||||
} catch (error) {
|
||||
return tool.response;
|
||||
}
|
||||
})();
|
||||
|
||||
// First empty line
|
||||
if (!source && chat.value.length > 1) return null;
|
||||
|
||||
return (
|
||||
<Markdown
|
||||
source={source}
|
||||
showAnimation={isLastChild && isChatting && index === chat.value.length - 1}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (value.type === ChatItemValueTypeEnum.tool && value.tools) {
|
||||
return (
|
||||
<Box>
|
||||
{value.tools.map((tool) => {
|
||||
const toolParams = (() => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(tool.params), null, 2);
|
||||
} catch (error) {
|
||||
return tool.params;
|
||||
}
|
||||
})();
|
||||
const toolResponse = (() => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(tool.response), null, 2);
|
||||
} catch (error) {
|
||||
return tool.response;
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<Accordion key={tool.id} allowToggle>
|
||||
<AccordionItem borderTop={'none'} borderBottom={'none'}>
|
||||
<AccordionButton
|
||||
w={'auto'}
|
||||
bg={'white'}
|
||||
borderRadius={'md'}
|
||||
borderWidth={'1px'}
|
||||
borderColor={'myGray.200'}
|
||||
boxShadow={'1'}
|
||||
pl={3}
|
||||
pr={2.5}
|
||||
_hover={{
|
||||
bg: 'auto'
|
||||
}}
|
||||
>
|
||||
<Avatar src={tool.toolAvatar} w={'1.25rem'} h={'1.25rem'} borderRadius={'sm'} />
|
||||
<Box mx={2} fontSize={'sm'} color={'myGray.900'}>
|
||||
{tool.toolName}
|
||||
</Box>
|
||||
{isChatting && !tool.response && <MyIcon name={'common/loading'} w={'14px'} />}
|
||||
<AccordionIcon color={'myGray.600'} ml={5} />
|
||||
</AccordionButton>
|
||||
<AccordionPanel
|
||||
py={0}
|
||||
px={0}
|
||||
mt={3}
|
||||
borderRadius={'md'}
|
||||
overflow={'hidden'}
|
||||
maxH={'500px'}
|
||||
overflowY={'auto'}
|
||||
>
|
||||
{toolParams && toolParams !== '{}' && (
|
||||
<Box mb={3}>
|
||||
<Markdown
|
||||
source={`~~~json#Input
|
||||
${toolParams}`}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{toolResponse && (
|
||||
<Markdown
|
||||
source={`~~~json#Response
|
||||
${toolResponse}`}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
if (
|
||||
value.type === ChatItemValueTypeEnum.interactive &&
|
||||
value.interactive &&
|
||||
value.interactive.type === 'userSelect'
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
{value.interactive?.params?.description && (
|
||||
<Markdown source={value.interactive.params.description} />
|
||||
)}
|
||||
<Flex flexDirection={'column'} gap={2} w={'250px'}>
|
||||
{value.interactive.params.userSelectOptions?.map((option) => {
|
||||
const selected = option.value === value.interactive?.params?.userSelectedVal;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={option.key}
|
||||
variant={'whitePrimary'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
isDisabled={value.interactive?.params?.userSelectedVal !== undefined}
|
||||
{...(selected
|
||||
? {
|
||||
_disabled: {
|
||||
cursor: 'default',
|
||||
borderColor: 'primary.300',
|
||||
bg: 'primary.50 !important',
|
||||
color: 'primary.600'
|
||||
}
|
||||
}
|
||||
: {})}
|
||||
onClick={() => {
|
||||
onSendMessage?.({
|
||||
text: option.value,
|
||||
history: setUserSelectResultToHistories(chatHistories, option.value)
|
||||
});
|
||||
return (
|
||||
<Accordion key={tool.id} allowToggle>
|
||||
<AccordionItem borderTop={'none'} borderBottom={'none'}>
|
||||
<AccordionButton
|
||||
w={'auto'}
|
||||
bg={'white'}
|
||||
borderRadius={'md'}
|
||||
borderWidth={'1px'}
|
||||
borderColor={'myGray.200'}
|
||||
boxShadow={'1'}
|
||||
pl={3}
|
||||
pr={2.5}
|
||||
_hover={{
|
||||
bg: 'auto'
|
||||
}}
|
||||
>
|
||||
{option.value}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
{/* Animation */}
|
||||
{isLastChild && isChatting && index === chat.value.length - 1 && (
|
||||
<Markdown source={''} showAnimation />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}, [chat.value.length, chatHistories, index, isChatting, isLastChild, onSendMessage, value]);
|
||||
<Avatar src={tool.toolAvatar} w={'1.25rem'} h={'1.25rem'} borderRadius={'sm'} />
|
||||
<Box mx={2} fontSize={'sm'} color={'myGray.900'}>
|
||||
{tool.toolName}
|
||||
</Box>
|
||||
{showAnimation && !tool.response && <MyIcon name={'common/loading'} w={'14px'} />}
|
||||
<AccordionIcon color={'myGray.600'} ml={5} />
|
||||
</AccordionButton>
|
||||
<AccordionPanel
|
||||
py={0}
|
||||
px={0}
|
||||
mt={3}
|
||||
borderRadius={'md'}
|
||||
overflow={'hidden'}
|
||||
maxH={'500px'}
|
||||
overflowY={'auto'}
|
||||
>
|
||||
{toolParams && toolParams !== '{}' && (
|
||||
<Box mb={3}>
|
||||
<Markdown
|
||||
source={`~~~json#Input
|
||||
${toolParams}`}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{toolResponse && (
|
||||
<Markdown
|
||||
source={`~~~json#Response
|
||||
${toolResponse}`}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => isEqual(prevProps, nextProps)
|
||||
);
|
||||
const RenderInteractive = React.memo(
|
||||
function RenderInteractive({
|
||||
isChatting,
|
||||
interactive,
|
||||
onSendMessage,
|
||||
chatHistories
|
||||
}: {
|
||||
isChatting: boolean;
|
||||
interactive: InteractiveNodeResponseItemType;
|
||||
onSendMessage: SendPromptFnType;
|
||||
chatHistories: ChatSiteItemType[];
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{interactive?.params?.description && <Markdown source={interactive.params.description} />}
|
||||
<Flex flexDirection={'column'} gap={2} w={'250px'}>
|
||||
{interactive.params.userSelectOptions?.map((option) => {
|
||||
const selected = option.value === interactive?.params?.userSelectedVal;
|
||||
|
||||
return (
|
||||
<>
|
||||
{Render}
|
||||
{RenderQuestionGuide}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
key={option.key}
|
||||
variant={'whitePrimary'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
isDisabled={interactive?.params?.userSelectedVal !== undefined}
|
||||
{...(selected
|
||||
? {
|
||||
_disabled: {
|
||||
cursor: 'default',
|
||||
borderColor: 'primary.300',
|
||||
bg: 'primary.50 !important',
|
||||
color: 'primary.600'
|
||||
}
|
||||
}
|
||||
: {})}
|
||||
onClick={() => {
|
||||
onSendMessage?.({
|
||||
text: option.value,
|
||||
history: setUserSelectResultToHistories(chatHistories, option.value)
|
||||
});
|
||||
}}
|
||||
>
|
||||
{option.value}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
},
|
||||
(
|
||||
prevProps,
|
||||
nextProps // isChatting 更新时候,onSendMessage 和 chatHistories 肯定都更新了,这里不需要额外的刷新
|
||||
) =>
|
||||
prevProps.isChatting === nextProps.isChatting &&
|
||||
isEqual(prevProps.interactive, nextProps.interactive)
|
||||
);
|
||||
|
||||
const AIResponseBox = ({ value, isLastChild, isChatting, onSendMessage }: props) => {
|
||||
const chatHistories = useContextSelector(ChatBoxContext, (v) => v.chatHistories);
|
||||
|
||||
if (value.type === ChatItemValueTypeEnum.text && value.text)
|
||||
return <RenderText showAnimation={isChatting && isLastChild} text={value.text.content} />;
|
||||
if (value.type === ChatItemValueTypeEnum.tool && value.tools)
|
||||
return <RenderTool showAnimation={isChatting && isLastChild} tools={value.tools} />;
|
||||
if (
|
||||
value.type === ChatItemValueTypeEnum.interactive &&
|
||||
value.interactive &&
|
||||
value.interactive.type === 'userSelect'
|
||||
)
|
||||
return (
|
||||
<RenderInteractive
|
||||
isChatting={isChatting}
|
||||
interactive={value.interactive}
|
||||
onSendMessage={onSendMessage}
|
||||
chatHistories={chatHistories}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AIResponseBox);
|
||||
|
@@ -217,8 +217,8 @@ function ExportPopover({
|
||||
return (
|
||||
<MyPopover
|
||||
placement={'right-start'}
|
||||
offset={[-5, 5]}
|
||||
hasArrow={false}
|
||||
offset={[0, 0]}
|
||||
hasArrow
|
||||
trigger={'hover'}
|
||||
w={'8.6rem'}
|
||||
Trigger={
|
||||
|
@@ -448,20 +448,16 @@ const RenderForm = ({
|
||||
setList((prevList) => {
|
||||
if (!newKey) {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('common:core.module.http.Key cannot be empty')
|
||||
});
|
||||
return prevList;
|
||||
}
|
||||
const checkExist = prevList.find((item, i) => i !== index && item.key == newKey);
|
||||
if (checkExist) {
|
||||
// toast({
|
||||
// status: 'warning',
|
||||
// title: t('common:core.module.http.Key cannot be empty')
|
||||
// });
|
||||
} else if (prevList.find((item, i) => i !== index && item.key == newKey)) {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('common:core.module.http.Key already exists')
|
||||
});
|
||||
return prevList;
|
||||
}
|
||||
return prevList.map((item, i) => (i === index ? { ...item, key: newKey } : item));
|
||||
});
|
||||
@@ -470,14 +466,15 @@ const RenderForm = ({
|
||||
[t, toast]
|
||||
);
|
||||
|
||||
// Add new params/headers key
|
||||
const handleAddNewProps = useCallback(
|
||||
(key: string, value: string = '') => {
|
||||
(value: string) => {
|
||||
setList((prevList) => {
|
||||
if (!key) {
|
||||
if (!value) {
|
||||
return prevList;
|
||||
}
|
||||
|
||||
const checkExist = prevList.find((item) => item.key === key);
|
||||
const checkExist = prevList.find((item) => item.key === value);
|
||||
if (checkExist) {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
toast({
|
||||
@@ -486,7 +483,7 @@ const RenderForm = ({
|
||||
});
|
||||
return prevList;
|
||||
}
|
||||
return [...prevList, { key, type: 'string', value }];
|
||||
return [...prevList, { key: value, type: 'string', value: '' }];
|
||||
});
|
||||
|
||||
setShouldUpdateNode(true);
|
||||
@@ -520,17 +517,15 @@ const RenderForm = ({
|
||||
<Tr key={`${input.key}${index}`}>
|
||||
<Td p={0} w={'50%'} borderRight={'1px solid'} borderColor={'myGray.200'}>
|
||||
<HttpInput
|
||||
placeholder={
|
||||
index !== list.length
|
||||
? t('common:core.module.http.Props name_and_tips')
|
||||
: t('common:core.module.http.Add props_and_tips')
|
||||
}
|
||||
placeholder={t('common:textarea_variable_picker_tip')}
|
||||
value={item.key}
|
||||
variableLabels={variables}
|
||||
variables={variables}
|
||||
onBlur={(val) => {
|
||||
handleKeyChange(index, val);
|
||||
if (index === list.length) {
|
||||
|
||||
// Last item blur, add the next item.
|
||||
if (index === list.length && val) {
|
||||
handleAddNewProps(val);
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
}
|
||||
@@ -541,11 +536,7 @@ const RenderForm = ({
|
||||
<Td p={0} w={'50%'}>
|
||||
<Box display={'flex'} alignItems={'center'}>
|
||||
<HttpInput
|
||||
placeholder={
|
||||
index !== list.length
|
||||
? t('common:core.module.http.Props value_and_tips')
|
||||
: ''
|
||||
}
|
||||
placeholder={t('common:textarea_variable_picker_tip')}
|
||||
value={item.value}
|
||||
variables={variables}
|
||||
variableLabels={variables}
|
||||
@@ -694,6 +685,7 @@ const RenderBody = ({
|
||||
{(typeInput?.value === ContentTypes.xml || typeInput?.value === ContentTypes.raw) && (
|
||||
<PromptEditor
|
||||
value={jsonBody.value}
|
||||
placeholder={t('common:textarea_variable_picker_tip')}
|
||||
onChange={(e) => {
|
||||
startSts(() => {
|
||||
onChangeNode({
|
||||
|
@@ -204,7 +204,6 @@ const InputDataModal = ({
|
||||
a: '',
|
||||
indexes: []
|
||||
});
|
||||
console.log('执行onSuccess');
|
||||
onSuccess(e);
|
||||
},
|
||||
errorToast: t('common:common.error.unKnow')
|
||||
|
@@ -17,6 +17,8 @@ export const useCopyData = () => {
|
||||
title: string | null = t('common:common.Copy Successful'),
|
||||
duration = 1000
|
||||
) => {
|
||||
data = data.trim();
|
||||
|
||||
try {
|
||||
if ((hasHttps() || !isProduction) && navigator.clipboard) {
|
||||
await navigator.clipboard.writeText(data);
|
||||
|
Reference in New Issue
Block a user