Update userselect ux (#2610)
* perf: user select ux and api * perf: http variables replace code * perf: http variables replace code * perf: chat box question guide adapt interactive * remove comment
This commit is contained in:
@@ -45,7 +45,7 @@ import ChatBoxDivider from '../../Divider';
|
||||
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { formatChatValue2InputType } from './utils';
|
||||
import { checkIsInteractiveByHistories, formatChatValue2InputType } from './utils';
|
||||
import { textareaMinH } from './constants';
|
||||
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import ChatProvider, { ChatBoxContext, ChatProviderProps } from './Provider';
|
||||
@@ -156,15 +156,11 @@ const ChatBox = (
|
||||
isChatting
|
||||
} = useContextSelector(ChatBoxContext, (v) => v);
|
||||
|
||||
const isInteractive = useMemo(() => {
|
||||
const lastAIHistory = chatHistories[chatHistories.length - 1];
|
||||
if (!lastAIHistory) return false;
|
||||
const lastAIMessage = lastAIHistory.value as AIChatItemValueItemType[];
|
||||
const interactiveContent = lastAIMessage?.find(
|
||||
(item) => item.type === ChatItemValueTypeEnum.interactive
|
||||
)?.interactive?.params;
|
||||
return !!interactiveContent;
|
||||
}, [chatHistories]);
|
||||
// Workflow running, there are user input or selection
|
||||
const isInteractive = useMemo(
|
||||
() => checkIsInteractiveByHistories(chatHistories),
|
||||
[chatHistories]
|
||||
);
|
||||
|
||||
// compute variable input is finish.
|
||||
const chatForm = useForm<ChatBoxInputFormType>({
|
||||
@@ -343,16 +339,15 @@ const ChatBox = (
|
||||
|
||||
// create question guide
|
||||
const createQuestionGuide = useCallback(
|
||||
async ({ history }: { history: ChatSiteItemType[] }) => {
|
||||
async ({ histories }: { histories: ChatSiteItemType[] }) => {
|
||||
if (!questionGuide || chatController.current?.signal?.aborted) return;
|
||||
|
||||
try {
|
||||
const abortSignal = new AbortController();
|
||||
questionGuideController.current = abortSignal;
|
||||
|
||||
const result = await postQuestionGuide(
|
||||
{
|
||||
messages: chats2GPTMessages({ messages: history, reserveId: false }).slice(-6),
|
||||
messages: chats2GPTMessages({ messages: histories, reserveId: false }).slice(-6),
|
||||
shareId,
|
||||
outLinkUid,
|
||||
teamId,
|
||||
@@ -464,8 +459,9 @@ const ChatBox = (
|
||||
}
|
||||
];
|
||||
|
||||
// 插入内容
|
||||
setChatHistories(newChatList);
|
||||
const isInteractive = checkIsInteractiveByHistories(history);
|
||||
// Update histories(Interactive input does not require new session rounds)
|
||||
setChatHistories(isInteractive ? newChatList.slice(0, -2) : newChatList);
|
||||
|
||||
// 清空输入内容
|
||||
resetInputVal({});
|
||||
@@ -476,6 +472,7 @@ const ChatBox = (
|
||||
const abortSignal = new AbortController();
|
||||
chatController.current = abortSignal;
|
||||
|
||||
// Last empty ai message will be removed
|
||||
const messages = chats2GPTMessages({ messages: newChatList, reserveId: true });
|
||||
|
||||
const {
|
||||
@@ -483,7 +480,7 @@ const ChatBox = (
|
||||
responseText,
|
||||
isNewChat = false
|
||||
} = await onStartChat({
|
||||
messages: messages.slice(0, -1),
|
||||
messages: messages,
|
||||
responseChatItemId: responseChatId,
|
||||
controller: abortSignal,
|
||||
generatingMessage: (e) => generatingMessage({ ...e, autoTTSResponse }),
|
||||
@@ -492,35 +489,29 @@ const ChatBox = (
|
||||
|
||||
isNewChatReplace.current = isNewChat;
|
||||
|
||||
// set finish status
|
||||
setChatHistories((state) =>
|
||||
state.map((item, index) => {
|
||||
// Set last chat finish status
|
||||
let newChatHistories: ChatSiteItemType[] = [];
|
||||
setChatHistories((state) => {
|
||||
newChatHistories = state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish',
|
||||
responseData
|
||||
responseData: item.responseData
|
||||
? [...item.responseData, ...responseData]
|
||||
: responseData
|
||||
};
|
||||
})
|
||||
);
|
||||
setTimeout(() => {
|
||||
createQuestionGuide({
|
||||
history: newChatList.map((item, i) =>
|
||||
i === newChatList.length - 1
|
||||
? {
|
||||
...item,
|
||||
value: [
|
||||
{
|
||||
type: ChatItemValueTypeEnum.text,
|
||||
text: {
|
||||
content: responseText
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
: item
|
||||
)
|
||||
});
|
||||
return newChatHistories;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (!checkIsInteractiveByHistories(newChatHistories)) {
|
||||
createQuestionGuide({
|
||||
histories: newChatHistories
|
||||
});
|
||||
}
|
||||
|
||||
generatingScroll();
|
||||
isPc && TextareaDom.current?.focus();
|
||||
}, 100);
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { ChatItemValueItemType, ChatSiteItemType } from '@fastgpt/global/core/chat/type';
|
||||
import {
|
||||
AIChatItemValueItemType,
|
||||
ChatItemValueItemType,
|
||||
ChatSiteItemType
|
||||
} from '@fastgpt/global/core/chat/type';
|
||||
import { ChatBoxInputType, UserInputFileItemType } from './type';
|
||||
import { getFileIcon } from '@fastgpt/global/common/file/icon';
|
||||
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { ChatItemValueTypeEnum, ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
|
||||
|
||||
export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): ChatBoxInputType => {
|
||||
if (!value) {
|
||||
@@ -38,6 +42,20 @@ export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): Chat
|
||||
};
|
||||
};
|
||||
|
||||
export const checkIsInteractiveByHistories = (chatHistories: ChatSiteItemType[]) => {
|
||||
const lastAIHistory = chatHistories[chatHistories.length - 1];
|
||||
if (!lastAIHistory) return false;
|
||||
|
||||
const lastMessageValue = lastAIHistory.value[
|
||||
lastAIHistory.value.length - 1
|
||||
] as AIChatItemValueItemType;
|
||||
|
||||
return (
|
||||
lastMessageValue.type === ChatItemValueTypeEnum.interactive &&
|
||||
!!lastMessageValue?.interactive?.params
|
||||
);
|
||||
};
|
||||
|
||||
export const setUserSelectResultToHistories = (
|
||||
histories: ChatSiteItemType[],
|
||||
selectVal: string
|
||||
@@ -47,9 +65,14 @@ export const setUserSelectResultToHistories = (
|
||||
// @ts-ignore
|
||||
return histories.map((item, i) => {
|
||||
if (i !== histories.length - 1) return item;
|
||||
item.value;
|
||||
const value = item.value.map((val) => {
|
||||
if (val.type !== ChatItemValueTypeEnum.interactive || !val.interactive) return val;
|
||||
|
||||
const value = item.value.map((val, i) => {
|
||||
if (
|
||||
i !== item.value.length - 1 ||
|
||||
val.type !== ChatItemValueTypeEnum.interactive ||
|
||||
!val.interactive
|
||||
)
|
||||
return val;
|
||||
|
||||
return {
|
||||
...val,
|
||||
@@ -67,6 +90,7 @@ export const setUserSelectResultToHistories = (
|
||||
|
||||
return {
|
||||
...item,
|
||||
status: ChatStatusEnum.loading,
|
||||
value
|
||||
};
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
ChatSiteItemType,
|
||||
UserChatItemValueItemType
|
||||
} from '@fastgpt/global/core/chat/type';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { SendPromptFnType } from '../ChatContainer/ChatBox/type';
|
||||
@@ -45,144 +45,168 @@ const AIResponseBox = ({
|
||||
}: props) => {
|
||||
const chatHistories = useContextSelector(ChatBoxContext, (v) => v.chatHistories);
|
||||
|
||||
if (value.type === ChatItemValueTypeEnum.text && value.text) {
|
||||
let source = (value.text?.content || '').trim();
|
||||
|
||||
// First empty line
|
||||
if (!source && chat.value.length > 1) return null;
|
||||
|
||||
// computed question guide
|
||||
// Question guide
|
||||
const RenderQuestionGuide = useMemo(() => {
|
||||
if (
|
||||
isLastChild &&
|
||||
!isChatting &&
|
||||
questionGuides.length > 0 &&
|
||||
index === chat.value.length - 1
|
||||
) {
|
||||
source = `${source}
|
||||
\`\`\`${CodeClassNameEnum.questionGuide}
|
||||
${JSON.stringify(questionGuides)}`;
|
||||
return (
|
||||
<Markdown
|
||||
source={`\`\`\`${CodeClassNameEnum.questionGuide}
|
||||
${JSON.stringify(questionGuides)}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [chat.value.length, index, isChatting, isLastChild, questionGuides]);
|
||||
|
||||
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;
|
||||
}
|
||||
})();
|
||||
const Render = useMemo(() => {
|
||||
if (value.type === ChatItemValueTypeEnum.text && value.text) {
|
||||
let source = (value.text?.content || '').trim();
|
||||
|
||||
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'
|
||||
// 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)
|
||||
});
|
||||
}}
|
||||
>
|
||||
<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 (
|
||||
<Flex flexDirection={'column'} gap={2} minW={'200px'} maxW={'250px'}>
|
||||
{value.interactive.params.userSelectOptions?.map((option) => {
|
||||
const selected = option.value === value.interactive?.params?.userSelectedVal;
|
||||
{option.value}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
{/* Animation */}
|
||||
{isLastChild && isChatting && index === chat.value.length - 1 && (
|
||||
<Markdown source={''} showAnimation />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}, [chat.value.length, chatHistories, index, isChatting, isLastChild, onSendMessage, value]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={option.key}
|
||||
variant={'whitePrimary'}
|
||||
isDisabled={!isLastChild && 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}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
return (
|
||||
<>
|
||||
{Render}
|
||||
{RenderQuestionGuide}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AIResponseBox);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user