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:
parent
3bcc3430fb
commit
3671e55001
@ -49,7 +49,7 @@ const Markdown = ({
|
|||||||
const formatSource = useMemo(() => {
|
const formatSource = useMemo(() => {
|
||||||
const formatSource = source
|
const formatSource = source
|
||||||
.replace(
|
.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'
|
'$1$3 $2$4'
|
||||||
) // Chinese and english chars separated by space
|
) // Chinese and english chars separated by space
|
||||||
.replace(/\n*(\[QUOTE SIGN\]\(.*\))/g, '$1');
|
.replace(/\n*(\[QUOTE SIGN\]\(.*\))/g, '$1');
|
||||||
|
|||||||
@ -9,11 +9,13 @@ import { formatChatValue2InputType } from '../utils';
|
|||||||
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||||
import { ChatBoxContext } from '../Provider';
|
import { ChatBoxContext } from '../Provider';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
|
import { SendPromptFnType } from '../type';
|
||||||
|
|
||||||
export type ChatControllerProps = {
|
export type ChatControllerProps = {
|
||||||
isLastChild: boolean;
|
isLastChild: boolean;
|
||||||
chat: ChatSiteItemType;
|
chat: ChatSiteItemType;
|
||||||
showVoiceIcon?: boolean;
|
showVoiceIcon?: boolean;
|
||||||
|
onSendMessage: SendPromptFnType;
|
||||||
onRetry?: () => void;
|
onRetry?: () => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
onMark?: () => void;
|
onMark?: () => void;
|
||||||
@ -25,7 +27,6 @@ export type ChatControllerProps = {
|
|||||||
|
|
||||||
const ChatController = ({
|
const ChatController = ({
|
||||||
chat,
|
chat,
|
||||||
isLastChild,
|
|
||||||
showVoiceIcon,
|
showVoiceIcon,
|
||||||
onReadUserDislike,
|
onReadUserDislike,
|
||||||
onCloseUserLike,
|
onCloseUserLike,
|
||||||
|
|||||||
@ -6,7 +6,11 @@ import { MessageCardStyle } from '../constants';
|
|||||||
import { formatChatValue2InputType } from '../utils';
|
import { formatChatValue2InputType } from '../utils';
|
||||||
import Markdown from '@/components/Markdown';
|
import Markdown from '@/components/Markdown';
|
||||||
import styles from '../index.module.scss';
|
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 FilesBlock from './FilesBox';
|
||||||
import { ChatBoxContext } from '../Provider';
|
import { ChatBoxContext } from '../Provider';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
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 MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { SendPromptFnType } from '../type';
|
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 = {
|
const colorMap = {
|
||||||
[ChatStatusEnum.loading]: {
|
[ChatStatusEnum.loading]: {
|
||||||
@ -42,26 +49,81 @@ type BasicProps = {
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
} & ChatControllerProps;
|
} & ChatControllerProps;
|
||||||
|
|
||||||
type UserItemType = BasicProps & {
|
type Props = BasicProps & {
|
||||||
type: ChatRoleEnum.Human;
|
type: ChatRoleEnum.Human | ChatRoleEnum.AI;
|
||||||
onSendMessage: undefined;
|
|
||||||
};
|
|
||||||
type AiItemType = BasicProps & {
|
|
||||||
type: ChatRoleEnum.AI;
|
|
||||||
onSendMessage: SendPromptFnType;
|
onSendMessage: SendPromptFnType;
|
||||||
};
|
};
|
||||||
type Props = UserItemType | AiItemType;
|
|
||||||
|
|
||||||
const ChatItem = ({
|
const RenderQuestionGuide = ({ questionGuides }: { questionGuides: string[] }) => {
|
||||||
type,
|
return (
|
||||||
avatar,
|
<Markdown
|
||||||
statusBoxData,
|
source={`\`\`\`${CodeClassNameEnum.questionGuide}
|
||||||
children,
|
${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,
|
isLastChild,
|
||||||
questionGuides = [],
|
isChatting,
|
||||||
onSendMessage,
|
onSendMessage,
|
||||||
...chatControllerProps
|
questionGuides
|
||||||
}: Props) => {
|
}: {
|
||||||
|
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 =
|
const styleMap: BoxProps =
|
||||||
type === ChatRoleEnum.Human
|
type === ChatRoleEnum.Human
|
||||||
? {
|
? {
|
||||||
@ -81,59 +143,84 @@ const ChatItem = ({
|
|||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
|
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
|
||||||
const { chat } = chatControllerProps;
|
|
||||||
const { copyData } = useCopyData();
|
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(() => {
|
const chatStatusMap = useMemo(() => {
|
||||||
if (!statusBoxData?.status) return;
|
if (!statusBoxData?.status) return;
|
||||||
return colorMap[statusBoxData.status];
|
return colorMap[statusBoxData.status];
|
||||||
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* control icon */}
|
{/* control icon */}
|
||||||
<Flex w={'100%'} alignItems={'center'} gap={2} justifyContent={styleMap.justifyContent}>
|
<Flex w={'100%'} alignItems={'center'} gap={2} justifyContent={styleMap.justifyContent}>
|
||||||
{isChatting && type === ChatRoleEnum.AI && isLastChild ? null : (
|
{isChatting && type === ChatRoleEnum.AI && isLastChild ? null : (
|
||||||
<Box order={styleMap.order} ml={styleMap.ml}>
|
<Box order={styleMap.order} ml={styleMap.ml}>
|
||||||
<ChatController {...chatControllerProps} isLastChild={isLastChild} />
|
<ChatController {...props} isLastChild={isLastChild} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<ChatAvatar src={avatar} type={type} />
|
<ChatAvatar src={avatar} type={type} />
|
||||||
|
|
||||||
|
{/* Workflow status */}
|
||||||
{!!chatStatusMap && statusBoxData && isLastChild && (
|
{!!chatStatusMap && statusBoxData && isLastChild && (
|
||||||
<Flex
|
<Flex
|
||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
@ -158,50 +245,66 @@ const ChatItem = ({
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
{/* content */}
|
{/* content */}
|
||||||
<Box
|
{splitAiResponseResults.map((value, i) => (
|
||||||
mt={['6px', 2]}
|
<Box
|
||||||
className="chat-box-card"
|
key={i}
|
||||||
textAlign={styleMap.textAlign}
|
mt={['6px', 2]}
|
||||||
_hover={{
|
className="chat-box-card"
|
||||||
'& .footer-copy': {
|
textAlign={styleMap.textAlign}
|
||||||
display: 'block'
|
_hover={{
|
||||||
}
|
'& .footer-copy': {
|
||||||
}}
|
display: 'block'
|
||||||
>
|
}
|
||||||
<Card
|
}}
|
||||||
{...MessageCardStyle}
|
|
||||||
bg={styleMap.bg}
|
|
||||||
borderRadius={styleMap.borderRadius}
|
|
||||||
textAlign={'left'}
|
|
||||||
>
|
>
|
||||||
{ContentCard}
|
<Card
|
||||||
{children}
|
{...MessageCardStyle}
|
||||||
{/* 对话框底部的复制按钮 */}
|
bg={styleMap.bg}
|
||||||
{type == ChatRoleEnum.AI && (!isChatting || (isChatting && !isLastChild)) && (
|
borderRadius={styleMap.borderRadius}
|
||||||
<Box
|
textAlign={'left'}
|
||||||
className="footer-copy"
|
>
|
||||||
display={['block', 'none']}
|
{type === ChatRoleEnum.Human && <HumanContentCard chatValue={value} />}
|
||||||
position={'absolute'}
|
{type === ChatRoleEnum.AI && (
|
||||||
bottom={0}
|
<AIContentCard
|
||||||
right={0}
|
chatValue={value}
|
||||||
transform={'translateX(100%)'}
|
dataId={chat.dataId}
|
||||||
>
|
isLastChild={isLastChild && i === splitAiResponseResults.length - 1}
|
||||||
<MyTooltip label={t('common:common.Copy')}>
|
isChatting={isChatting}
|
||||||
<MyIcon
|
onSendMessage={onSendMessage}
|
||||||
w={'1rem'}
|
questionGuides={questionGuides}
|
||||||
cursor="pointer"
|
/>
|
||||||
p="5px"
|
)}
|
||||||
bg="white"
|
{/* Example: Response tags. A set of dialogs only needs to be displayed once*/}
|
||||||
name={'copy'}
|
{i === splitAiResponseResults.length - 1 && <>{children}</>}
|
||||||
color={'myGray.500'}
|
{/* 对话框底部的复制按钮 */}
|
||||||
_hover={{ color: 'primary.600' }}
|
{type == ChatRoleEnum.AI &&
|
||||||
onClick={() => copyData(chatText)}
|
value[0]?.type !== 'interactive' &&
|
||||||
/>
|
(!isChatting || (isChatting && !isLastChild)) && (
|
||||||
</MyTooltip>
|
<Box
|
||||||
</Box>
|
className="footer-copy"
|
||||||
)}
|
display={['block', 'none']}
|
||||||
</Card>
|
position={'absolute'}
|
||||||
</Box>
|
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 type { StreamResponseType } from '@/web/common/api/fetch';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
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';
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||||
|
|
||||||
const ResponseTags = dynamic(() => import('./components/ResponseTags'));
|
const ResponseTags = dynamic(() => import('./components/ResponseTags'));
|
||||||
@ -574,233 +574,205 @@ const ChatBox = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
// retry input
|
// retry input
|
||||||
const retryInput = useCallback(
|
const retryInput = useMemoizedFn((dataId?: string) => {
|
||||||
(dataId?: string) => {
|
if (!dataId || !onDelMessage) return;
|
||||||
if (!dataId || !onDelMessage) return;
|
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const index = chatHistories.findIndex((item) => item.dataId === dataId);
|
const index = chatHistories.findIndex((item) => item.dataId === dataId);
|
||||||
const delHistory = chatHistories.slice(index);
|
const delHistory = chatHistories.slice(index);
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
delHistory.map((item) => {
|
delHistory.map((item) => {
|
||||||
if (item.dataId) {
|
if (item.dataId) {
|
||||||
return onDelMessage({ contentId: 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 true;
|
})
|
||||||
});
|
);
|
||||||
|
setChatHistories((state) => (index === 0 ? [] : state.slice(0, index)));
|
||||||
|
|
||||||
|
sendPrompt({
|
||||||
|
...formatChatValue2InputType(delHistory[0].value),
|
||||||
|
history: chatHistories.slice(0, index)
|
||||||
});
|
});
|
||||||
};
|
} catch (error) {
|
||||||
},
|
toast({
|
||||||
[onDelMessage, setChatHistories]
|
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
|
// admin mark
|
||||||
const onMark = useCallback(
|
const onMark = useMemoizedFn((chat: ChatSiteItemType, q = '') => {
|
||||||
(chat: ChatSiteItemType, q = '') => {
|
if (!showMarkIcon || chat.obj !== ChatRoleEnum.AI) return;
|
||||||
if (!showMarkIcon || chat.obj !== ChatRoleEnum.AI) return;
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (!chat.dataId) return;
|
if (!chat.dataId) return;
|
||||||
|
|
||||||
if (chat.adminFeedback) {
|
if (chat.adminFeedback) {
|
||||||
setAdminMarkData({
|
setAdminMarkData({
|
||||||
chatItemId: chat.dataId,
|
chatItemId: chat.dataId,
|
||||||
datasetId: chat.adminFeedback.datasetId,
|
datasetId: chat.adminFeedback.datasetId,
|
||||||
collectionId: chat.adminFeedback.collectionId,
|
collectionId: chat.adminFeedback.collectionId,
|
||||||
dataId: chat.adminFeedback.dataId,
|
dataId: chat.adminFeedback.dataId,
|
||||||
q: chat.adminFeedback.q || q || '',
|
q: chat.adminFeedback.q || q || '',
|
||||||
a: chat.adminFeedback.a
|
a: chat.adminFeedback.a
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setAdminMarkData({
|
setAdminMarkData({
|
||||||
chatItemId: chat.dataId,
|
chatItemId: chat.dataId,
|
||||||
q,
|
q,
|
||||||
a: formatChatValue2InputType(chat.value).text
|
a: formatChatValue2InputType(chat.value).text
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
});
|
||||||
[showMarkIcon]
|
const onAddUserLike = useMemoizedFn((chat: ChatSiteItemType) => {
|
||||||
);
|
if (
|
||||||
const onAddUserLike = useCallback(
|
feedbackType !== FeedbackTypeEnum.user ||
|
||||||
(chat: ChatSiteItemType) => {
|
chat.obj !== ChatRoleEnum.AI ||
|
||||||
if (
|
chat.userBadFeedback
|
||||||
feedbackType !== FeedbackTypeEnum.user ||
|
)
|
||||||
chat.obj !== ChatRoleEnum.AI ||
|
return;
|
||||||
chat.userBadFeedback
|
return () => {
|
||||||
)
|
if (!chat.dataId || !chatId || !appId) return;
|
||||||
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 () => {
|
return () => {
|
||||||
if (!chat.dataId || !chatId || !appId) return;
|
if (!chat.dataId || !chatId || !appId) return;
|
||||||
|
|
||||||
const isGoodFeedback = !!chat.userGoodFeedback;
|
|
||||||
setChatHistories((state) =>
|
setChatHistories((state) =>
|
||||||
state.map((chatItem) =>
|
state.map((chatItem) =>
|
||||||
chatItem.dataId === chat.dataId
|
chatItem.dataId === chat.dataId ? { ...chatItem, userBadFeedback: undefined } : chatItem
|
||||||
? {
|
|
||||||
...chatItem,
|
|
||||||
userGoodFeedback: isGoodFeedback ? undefined : 'yes'
|
|
||||||
}
|
|
||||||
: chatItem
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
updateChatUserFeedback({
|
updateChatUserFeedback({
|
||||||
appId,
|
appId,
|
||||||
chatId,
|
chatId,
|
||||||
teamId,
|
|
||||||
teamToken,
|
|
||||||
chatItemId: chat.dataId,
|
chatItemId: chat.dataId,
|
||||||
shareId,
|
shareId,
|
||||||
outLinkUid,
|
teamId,
|
||||||
userGoodFeedback: isGoodFeedback ? undefined : 'yes'
|
teamToken,
|
||||||
|
outLinkUid
|
||||||
});
|
});
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
},
|
} else {
|
||||||
[appId, chatId, feedbackType, outLinkUid, setChatHistories, shareId, teamId, teamToken]
|
return () => setFeedbackId(chat.dataId);
|
||||||
);
|
}
|
||||||
const onCloseUserLike = useCallback(
|
});
|
||||||
(chat: ChatSiteItemType) => {
|
const onReadUserDislike = useMemoizedFn((chat: ChatSiteItemType) => {
|
||||||
if (feedbackType !== FeedbackTypeEnum.admin) return;
|
if (feedbackType !== FeedbackTypeEnum.admin || chat.obj !== ChatRoleEnum.AI) return;
|
||||||
return () => {
|
return () => {
|
||||||
if (!chat.dataId || !chatId || !appId) 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) =>
|
setChatHistories((state) =>
|
||||||
state.map((chatItem) =>
|
state.map((chatItem) =>
|
||||||
chatItem.dataId === chat.dataId
|
chatItem.obj === ChatRoleEnum.AI && chatItem.dataId === chat.dataId
|
||||||
? { ...chatItem, userGoodFeedback: undefined }
|
? {
|
||||||
|
...chatItem,
|
||||||
|
customFeedbacks: chatItem.customFeedbacks?.filter((_, index) => index !== i)
|
||||||
|
}
|
||||||
: chatItem
|
: 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(
|
const showEmpty = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -817,7 +789,7 @@ const ChatBox = (
|
|||||||
welcomeText
|
welcomeText
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
const statusBoxData = useMemo(() => {
|
const statusBoxData = useCreation(() => {
|
||||||
if (!isChatting) return;
|
if (!isChatting) return;
|
||||||
const chatContent = chatHistories[chatHistories.length - 1];
|
const chatContent = chatHistories[chatHistories.length - 1];
|
||||||
if (!chatContent) return;
|
if (!chatContent) return;
|
||||||
@ -883,10 +855,9 @@ const ChatBox = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
return (
|
|
||||||
<Flex flexDirection={'column'} h={'100%'} position={'relative'}>
|
const RenderRecords = useMemo(() => {
|
||||||
<Script src="/js/html2pdf.bundle.min.js" strategy="lazyOnload"></Script>
|
return (
|
||||||
{/* chat box container */}
|
|
||||||
<Box ref={ChatBoxRef} flex={'1 0 0'} h={0} w={'100%'} overflow={'overlay'} px={[4, 0]} pb={3}>
|
<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'}>
|
<Box id="chat-container" maxW={['100%', '92%']} h={'100%'} mx={'auto'}>
|
||||||
{showEmpty && <Empty />}
|
{showEmpty && <Empty />}
|
||||||
@ -907,7 +878,7 @@ const ChatBox = (
|
|||||||
onRetry={retryInput(item.dataId)}
|
onRetry={retryInput(item.dataId)}
|
||||||
onDelete={delOneMessage(item.dataId)}
|
onDelete={delOneMessage(item.dataId)}
|
||||||
isLastChild={index === chatHistories.length - 1}
|
isLastChild={index === chatHistories.length - 1}
|
||||||
onSendMessage={undefined}
|
onSendMessage={sendPrompt}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{item.obj === ChatRoleEnum.AI && (
|
{item.obj === ChatRoleEnum.AI && (
|
||||||
@ -986,6 +957,42 @@ const ChatBox = (
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</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 */}
|
{/* message input */}
|
||||||
{onStartChat && chatStarted && active && appId && !isInteractive && (
|
{onStartChat && chatStarted && active && appId && !isInteractive && (
|
||||||
<ChatInput
|
<ChatInput
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
|
|||||||
import {
|
import {
|
||||||
AIChatItemValueItemType,
|
AIChatItemValueItemType,
|
||||||
ChatSiteItemType,
|
ChatSiteItemType,
|
||||||
|
ToolModuleResponseItemType,
|
||||||
UserChatItemValueItemType
|
UserChatItemValueItemType
|
||||||
} from '@fastgpt/global/core/chat/type';
|
} from '@fastgpt/global/core/chat/type';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
@ -23,190 +24,190 @@ import { SendPromptFnType } from '../ChatContainer/ChatBox/type';
|
|||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { ChatBoxContext } from '../ChatContainer/ChatBox/Provider';
|
import { ChatBoxContext } from '../ChatContainer/ChatBox/Provider';
|
||||||
import { setUserSelectResultToHistories } from '../ChatContainer/ChatBox/utils';
|
import { setUserSelectResultToHistories } from '../ChatContainer/ChatBox/utils';
|
||||||
|
import { InteractiveNodeResponseItemType } from '@fastgpt/global/core/workflow/template/system/userSelect/type';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
|
||||||
type props = {
|
type props = {
|
||||||
value: UserChatItemValueItemType | AIChatItemValueItemType;
|
value: UserChatItemValueItemType | AIChatItemValueItemType;
|
||||||
index: number;
|
|
||||||
chat: ChatSiteItemType;
|
|
||||||
isLastChild: boolean;
|
isLastChild: boolean;
|
||||||
isChatting: boolean;
|
isChatting: boolean;
|
||||||
questionGuides: string[];
|
onSendMessage: SendPromptFnType;
|
||||||
onSendMessage?: SendPromptFnType;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const AIResponseBox = ({
|
const RenderText = React.memo(function RenderText({
|
||||||
value,
|
showAnimation,
|
||||||
index,
|
text
|
||||||
chat,
|
}: {
|
||||||
isLastChild,
|
showAnimation: boolean;
|
||||||
isChatting,
|
text?: string;
|
||||||
questionGuides,
|
}) {
|
||||||
onSendMessage
|
let source = (text || '').trim();
|
||||||
}: props) => {
|
|
||||||
const chatHistories = useContextSelector(ChatBoxContext, (v) => v.chatHistories);
|
|
||||||
|
|
||||||
// Question guide
|
// First empty line
|
||||||
const RenderQuestionGuide = useMemo(() => {
|
// if (!source && !isLastChild) return null;
|
||||||
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]);
|
|
||||||
|
|
||||||
const Render = useMemo(() => {
|
return <Markdown source={source} showAnimation={showAnimation} />;
|
||||||
if (value.type === ChatItemValueTypeEnum.text && value.text) {
|
});
|
||||||
let source = (value.text?.content || '').trim();
|
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
|
return (
|
||||||
if (!source && chat.value.length > 1) return null;
|
<Accordion key={tool.id} allowToggle>
|
||||||
|
<AccordionItem borderTop={'none'} borderBottom={'none'}>
|
||||||
return (
|
<AccordionButton
|
||||||
<Markdown
|
w={'auto'}
|
||||||
source={source}
|
bg={'white'}
|
||||||
showAnimation={isLastChild && isChatting && index === chat.value.length - 1}
|
borderRadius={'md'}
|
||||||
/>
|
borderWidth={'1px'}
|
||||||
);
|
borderColor={'myGray.200'}
|
||||||
}
|
boxShadow={'1'}
|
||||||
if (value.type === ChatItemValueTypeEnum.tool && value.tools) {
|
pl={3}
|
||||||
return (
|
pr={2.5}
|
||||||
<Box>
|
_hover={{
|
||||||
{value.tools.map((tool) => {
|
bg: 'auto'
|
||||||
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)
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{option.value}
|
<Avatar src={tool.toolAvatar} w={'1.25rem'} h={'1.25rem'} borderRadius={'sm'} />
|
||||||
</Button>
|
<Box mx={2} fontSize={'sm'} color={'myGray.900'}>
|
||||||
);
|
{tool.toolName}
|
||||||
})}
|
</Box>
|
||||||
</Flex>
|
{showAnimation && !tool.response && <MyIcon name={'common/loading'} w={'14px'} />}
|
||||||
{/* Animation */}
|
<AccordionIcon color={'myGray.600'} ml={5} />
|
||||||
{isLastChild && isChatting && index === chat.value.length - 1 && (
|
</AccordionButton>
|
||||||
<Markdown source={''} showAnimation />
|
<AccordionPanel
|
||||||
)}
|
py={0}
|
||||||
</>
|
px={0}
|
||||||
);
|
mt={3}
|
||||||
}
|
borderRadius={'md'}
|
||||||
}, [chat.value.length, chatHistories, index, isChatting, isLastChild, onSendMessage, value]);
|
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 (
|
return (
|
||||||
<>
|
<Button
|
||||||
{Render}
|
key={option.key}
|
||||||
{RenderQuestionGuide}
|
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);
|
export default React.memo(AIResponseBox);
|
||||||
|
|||||||
@ -217,8 +217,8 @@ function ExportPopover({
|
|||||||
return (
|
return (
|
||||||
<MyPopover
|
<MyPopover
|
||||||
placement={'right-start'}
|
placement={'right-start'}
|
||||||
offset={[-5, 5]}
|
offset={[0, 0]}
|
||||||
hasArrow={false}
|
hasArrow
|
||||||
trigger={'hover'}
|
trigger={'hover'}
|
||||||
w={'8.6rem'}
|
w={'8.6rem'}
|
||||||
Trigger={
|
Trigger={
|
||||||
|
|||||||
@ -448,20 +448,16 @@ const RenderForm = ({
|
|||||||
setList((prevList) => {
|
setList((prevList) => {
|
||||||
if (!newKey) {
|
if (!newKey) {
|
||||||
setUpdateTrigger((prev) => !prev);
|
setUpdateTrigger((prev) => !prev);
|
||||||
toast({
|
// toast({
|
||||||
status: 'warning',
|
// status: 'warning',
|
||||||
title: t('common:core.module.http.Key cannot be empty')
|
// title: t('common:core.module.http.Key cannot be empty')
|
||||||
});
|
// });
|
||||||
return prevList;
|
} else if (prevList.find((item, i) => i !== index && item.key == newKey)) {
|
||||||
}
|
|
||||||
const checkExist = prevList.find((item, i) => i !== index && item.key == newKey);
|
|
||||||
if (checkExist) {
|
|
||||||
setUpdateTrigger((prev) => !prev);
|
setUpdateTrigger((prev) => !prev);
|
||||||
toast({
|
toast({
|
||||||
status: 'warning',
|
status: 'warning',
|
||||||
title: t('common:core.module.http.Key already exists')
|
title: t('common:core.module.http.Key already exists')
|
||||||
});
|
});
|
||||||
return prevList;
|
|
||||||
}
|
}
|
||||||
return prevList.map((item, i) => (i === index ? { ...item, key: newKey } : item));
|
return prevList.map((item, i) => (i === index ? { ...item, key: newKey } : item));
|
||||||
});
|
});
|
||||||
@ -470,14 +466,15 @@ const RenderForm = ({
|
|||||||
[t, toast]
|
[t, toast]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add new params/headers key
|
||||||
const handleAddNewProps = useCallback(
|
const handleAddNewProps = useCallback(
|
||||||
(key: string, value: string = '') => {
|
(value: string) => {
|
||||||
setList((prevList) => {
|
setList((prevList) => {
|
||||||
if (!key) {
|
if (!value) {
|
||||||
return prevList;
|
return prevList;
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkExist = prevList.find((item) => item.key === key);
|
const checkExist = prevList.find((item) => item.key === value);
|
||||||
if (checkExist) {
|
if (checkExist) {
|
||||||
setUpdateTrigger((prev) => !prev);
|
setUpdateTrigger((prev) => !prev);
|
||||||
toast({
|
toast({
|
||||||
@ -486,7 +483,7 @@ const RenderForm = ({
|
|||||||
});
|
});
|
||||||
return prevList;
|
return prevList;
|
||||||
}
|
}
|
||||||
return [...prevList, { key, type: 'string', value }];
|
return [...prevList, { key: value, type: 'string', value: '' }];
|
||||||
});
|
});
|
||||||
|
|
||||||
setShouldUpdateNode(true);
|
setShouldUpdateNode(true);
|
||||||
@ -520,17 +517,15 @@ const RenderForm = ({
|
|||||||
<Tr key={`${input.key}${index}`}>
|
<Tr key={`${input.key}${index}`}>
|
||||||
<Td p={0} w={'50%'} borderRight={'1px solid'} borderColor={'myGray.200'}>
|
<Td p={0} w={'50%'} borderRight={'1px solid'} borderColor={'myGray.200'}>
|
||||||
<HttpInput
|
<HttpInput
|
||||||
placeholder={
|
placeholder={t('common:textarea_variable_picker_tip')}
|
||||||
index !== list.length
|
|
||||||
? t('common:core.module.http.Props name_and_tips')
|
|
||||||
: t('common:core.module.http.Add props_and_tips')
|
|
||||||
}
|
|
||||||
value={item.key}
|
value={item.key}
|
||||||
variableLabels={variables}
|
variableLabels={variables}
|
||||||
variables={variables}
|
variables={variables}
|
||||||
onBlur={(val) => {
|
onBlur={(val) => {
|
||||||
handleKeyChange(index, val);
|
handleKeyChange(index, val);
|
||||||
if (index === list.length) {
|
|
||||||
|
// Last item blur, add the next item.
|
||||||
|
if (index === list.length && val) {
|
||||||
handleAddNewProps(val);
|
handleAddNewProps(val);
|
||||||
setUpdateTrigger((prev) => !prev);
|
setUpdateTrigger((prev) => !prev);
|
||||||
}
|
}
|
||||||
@ -541,11 +536,7 @@ const RenderForm = ({
|
|||||||
<Td p={0} w={'50%'}>
|
<Td p={0} w={'50%'}>
|
||||||
<Box display={'flex'} alignItems={'center'}>
|
<Box display={'flex'} alignItems={'center'}>
|
||||||
<HttpInput
|
<HttpInput
|
||||||
placeholder={
|
placeholder={t('common:textarea_variable_picker_tip')}
|
||||||
index !== list.length
|
|
||||||
? t('common:core.module.http.Props value_and_tips')
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
value={item.value}
|
value={item.value}
|
||||||
variables={variables}
|
variables={variables}
|
||||||
variableLabels={variables}
|
variableLabels={variables}
|
||||||
@ -694,6 +685,7 @@ const RenderBody = ({
|
|||||||
{(typeInput?.value === ContentTypes.xml || typeInput?.value === ContentTypes.raw) && (
|
{(typeInput?.value === ContentTypes.xml || typeInput?.value === ContentTypes.raw) && (
|
||||||
<PromptEditor
|
<PromptEditor
|
||||||
value={jsonBody.value}
|
value={jsonBody.value}
|
||||||
|
placeholder={t('common:textarea_variable_picker_tip')}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
startSts(() => {
|
startSts(() => {
|
||||||
onChangeNode({
|
onChangeNode({
|
||||||
|
|||||||
@ -204,7 +204,6 @@ const InputDataModal = ({
|
|||||||
a: '',
|
a: '',
|
||||||
indexes: []
|
indexes: []
|
||||||
});
|
});
|
||||||
console.log('执行onSuccess');
|
|
||||||
onSuccess(e);
|
onSuccess(e);
|
||||||
},
|
},
|
||||||
errorToast: t('common:common.error.unKnow')
|
errorToast: t('common:common.error.unKnow')
|
||||||
|
|||||||
@ -17,6 +17,8 @@ export const useCopyData = () => {
|
|||||||
title: string | null = t('common:common.Copy Successful'),
|
title: string | null = t('common:common.Copy Successful'),
|
||||||
duration = 1000
|
duration = 1000
|
||||||
) => {
|
) => {
|
||||||
|
data = data.trim();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ((hasHttps() || !isProduction) && navigator.clipboard) {
|
if ((hasHttps() || !isProduction) && navigator.clipboard) {
|
||||||
await navigator.clipboard.writeText(data);
|
await navigator.clipboard.writeText(data);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user