User select node (#2397)

* feat: add user select node (#2300)

* feat: add user select node

* fix

* type

* fix

* fix

* fix

* perf: user select code

* perf: user select histories

* perf: i18n

---------

Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer
2024-08-15 12:27:04 +08:00
committed by GitHub
parent f8b8fcc172
commit fdeb1590d7
51 changed files with 1060 additions and 184 deletions

View File

@@ -18,7 +18,12 @@ import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { uploadFile2DB } from '@/web/common/file/controller';
import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { ChatBoxInputFormType, ChatBoxInputType, UserInputFileItemType } from '../type';
import {
ChatBoxInputFormType,
ChatBoxInputType,
SendPromptFnType,
UserInputFileItemType
} from '../type';
import { textareaMinH } from '../constants';
import { UseFormReturn, useFieldArray } from 'react-hook-form';
import { ChatBoxContext } from '../Provider';
@@ -51,7 +56,7 @@ const ChatInput = ({
chatForm,
appId
}: {
onSendMessage: (val: ChatBoxInputType & { autoTTSResponse?: boolean }) => void;
onSendMessage: SendPromptFnType;
onStop: () => void;
TextareaDom: React.MutableRefObject<HTMLTextAreaElement | null>;
resetInputVal: (val: ChatBoxInputType) => void;

View File

@@ -15,6 +15,8 @@ import { useCopyData } from '@/web/common/hooks/useCopyData';
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';
const colorMap = {
[ChatStatusEnum.loading]: {
bg: 'myGray.100',
@@ -30,16 +32,7 @@ const colorMap = {
}
};
const ChatItem = ({
type,
avatar,
statusBoxData,
children,
isLastChild,
questionGuides = [],
...chatControllerProps
}: {
type: ChatRoleEnum.Human | ChatRoleEnum.AI;
type BasicProps = {
avatar?: string;
statusBoxData?: {
status: `${ChatStatusEnum}`;
@@ -47,7 +40,28 @@ const ChatItem = ({
};
questionGuides?: string[];
children?: React.ReactNode;
} & ChatControllerProps) => {
} & ChatControllerProps;
type UserItemType = BasicProps & {
type: ChatRoleEnum.Human;
onSendMessage: undefined;
};
type AiItemType = BasicProps & {
type: ChatRoleEnum.AI;
onSendMessage: SendPromptFnType;
};
type Props = UserItemType | AiItemType;
const ChatItem = ({
type,
avatar,
statusBoxData,
children,
isLastChild,
questionGuides = [],
onSendMessage,
...chatControllerProps
}: Props) => {
const styleMap: BoxProps =
type === ChatRoleEnum.Human
? {
@@ -96,12 +110,13 @@ const ChatItem = ({
isLastChild={isLastChild}
isChatting={isChatting}
questionGuides={questionGuides}
onSendMessage={onSendMessage}
/>
);
})}
</Flex>
);
}, [chat, isChatting, isLastChild, questionGuides, type]);
}, [chat, isChatting, isLastChild, onSendMessage, questionGuides, type]);
const chatStatusMap = useMemo(() => {
if (!statusBoxData?.status) return;

View File

@@ -11,7 +11,6 @@ import React, {
import Script from 'next/script';
import type {
AIChatItemValueItemType,
ChatHistoryItemResType,
ChatSiteItemType,
UserChatItemValueItemType
} from '@fastgpt/global/core/chat/type.d';
@@ -34,7 +33,12 @@ import type { AdminMarkType } from './components/SelectMarkCollection';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { postQuestionGuide } from '@/web/core/ai/api';
import type { ComponentRef, ChatBoxInputType, ChatBoxInputFormType } from './type.d';
import type {
ComponentRef,
ChatBoxInputType,
ChatBoxInputFormType,
SendPromptFnType
} from './type.d';
import type { StartChatFnProps, generatingMessageProps } from '../type';
import ChatInput from './Input/ChatInput';
import ChatBoxDivider from '../../Divider';
@@ -151,6 +155,16 @@ 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]);
// compute variable input is finish.
const chatForm = useForm<ChatBoxInputFormType>({
defaultValues: {
@@ -201,6 +215,7 @@ const ChatBox = (
status,
name,
tool,
interactive,
autoTTSResponse,
variables
}: generatingMessageProps & { autoTTSResponse?: boolean }) => {
@@ -287,6 +302,16 @@ const ChatBox = (
};
} else if (event === SseResponseEventEnum.updateVariables && variables) {
variablesForm.reset(variables);
} else if (event === SseResponseEventEnum.interactive) {
const val: AIChatItemValueItemType = {
type: ChatItemValueTypeEnum.interactive,
interactive
};
return {
...item,
value: item.value.concat(val)
};
}
return item;
@@ -355,16 +380,8 @@ const ChatBox = (
/**
* user confirm send prompt
*/
const sendPrompt = useCallback(
({
text = '',
files = [],
history = chatHistories,
autoTTSResponse = false
}: ChatBoxInputType & {
autoTTSResponse?: boolean;
history?: ChatSiteItemType[];
}) => {
const sendPrompt: SendPromptFnType = useCallback(
({ text = '', files = [], history = chatHistories, autoTTSResponse = false }) => {
variablesForm.handleSubmit(
async (variables) => {
if (!onStartChat) return;
@@ -898,6 +915,7 @@ const ChatBox = (
onRetry={retryInput(item.dataId)}
onDelete={delOneMessage(item.dataId)}
isLastChild={index === chatHistories.length - 1}
onSendMessage={undefined}
/>
)}
{item.obj === ChatRoleEnum.AI && (
@@ -907,7 +925,8 @@ const ChatBox = (
avatar={appAvatar}
chat={item}
isLastChild={index === chatHistories.length - 1}
{...(item.obj === ChatRoleEnum.AI && {
onSendMessage={sendPrompt}
{...{
showVoiceIcon,
shareId,
outLinkUid,
@@ -923,7 +942,7 @@ const ChatBox = (
onCloseUserLike: onCloseUserLike(item),
onAddUserDislike: onAddUserDislike(item),
onReadUserDislike: onReadUserDislike(item)
})}
}}
>
<ResponseTags
showTags={index !== chatHistories.length - 1 || !isChatting}
@@ -973,7 +992,7 @@ const ChatBox = (
</Box>
</Box>
{/* message input */}
{onStartChat && chatStarted && active && appId && (
{onStartChat && chatStarted && active && appId && !isInteractive && (
<ChatInput
onSendMessage={sendPrompt}
onStop={() => chatController.current?.abort('stop')}

View File

@@ -29,6 +29,16 @@ export type ChatBoxInputType = {
files?: UserInputFileItemType[];
};
export type SendPromptFnType = ({
text,
files,
history,
autoTTSResponse
}: ChatBoxInputType & {
autoTTSResponse?: boolean;
history?: ChatSiteItemType[];
}) => void;
export type ComponentRef = {
restartChat: () => void;
scrollToBottom: (behavior?: 'smooth' | 'auto') => void;

View File

@@ -1,7 +1,7 @@
import { ChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { ChatItemValueItemType, ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import { ChatBoxInputType, UserInputFileItemType } from './type';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { getFileIcon } from '@fastgpt/global/common/file/icon';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): ChatBoxInputType => {
if (!value) {
@@ -37,3 +37,37 @@ export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): Chat
files
};
};
export const setUserSelectResultToHistories = (
histories: ChatSiteItemType[],
selectVal: string
): ChatSiteItemType[] => {
if (histories.length === 0) return histories;
// @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;
return {
...val,
interactive: {
...val.interactive,
params: {
...val.interactive.params,
userSelectedVal: val.interactive.params.userSelectOptions.find(
(item) => item.value === selectVal
)?.value
}
}
};
});
return {
...item,
value
};
});
};

View File

@@ -1,6 +1,7 @@
import { StreamResponseType } from '@/web/common/api/fetch';
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
import { ChatSiteItemType, ToolModuleResponseItemType } from '@fastgpt/global/core/chat/type';
import { InteractiveNodeResponseItemType } from '@fastgpt/global/core/workflow/template/system/userSelect/type';
export type generatingMessageProps = {
event: SseResponseEventEnum;
@@ -8,6 +9,7 @@ export type generatingMessageProps = {
name?: string;
status?: 'running' | 'finish';
tool?: ToolModuleResponseItemType;
interactive?: InteractiveNodeResponseItemType;
variables?: Record<string, any>;
};

View File

@@ -6,7 +6,9 @@ import {
AccordionIcon,
AccordionItem,
AccordionPanel,
Box
Box,
Button,
Flex
} from '@chakra-ui/react';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
import {
@@ -17,6 +19,10 @@ import {
import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { SendPromptFnType } from '../ChatContainer/ChatBox/type';
import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../ChatContainer/ChatBox/Provider';
import { setUserSelectResultToHistories } from '../ChatContainer/ChatBox/utils';
type props = {
value: UserChatItemValueItemType | AIChatItemValueItemType;
@@ -25,10 +31,21 @@ type props = {
isLastChild: boolean;
isChatting: boolean;
questionGuides: string[];
onSendMessage?: SendPromptFnType;
};
const AIResponseBox = ({ value, index, chat, isLastChild, isChatting, questionGuides }: props) => {
if (value.text) {
const AIResponseBox = ({
value,
index,
chat,
isLastChild,
isChatting,
questionGuides,
onSendMessage
}: props) => {
const chatHistories = useContextSelector(ChatBoxContext, (v) => v.chatHistories);
if (value.type === ChatItemValueTypeEnum.text && value.text) {
let source = (value.text?.content || '').trim();
// First empty line
@@ -126,6 +143,45 @@ ${toolResponse}`}
</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;
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;
};

View File

@@ -17,6 +17,7 @@ import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../ChatContainer/ChatBox/Provider';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getFileIcon } from '@fastgpt/global/common/file/icon';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
type sideTabItemType = {
moduleLogo?: string;
@@ -124,7 +125,11 @@ const WholeResponseModal = ({
</Flex>
}
>
{response?.length && <ResponseBox response={response} showDetail={showDetail} />}
{!!response?.length ? (
<ResponseBox response={response} showDetail={showDetail} />
) : (
<EmptyTip text={t('chat:no_workflow_response')} />
)}
</MyModal>
);
};
@@ -480,6 +485,12 @@ export const WholeResponseContent = ({
value={activeModule?.readFilesResult}
/>
</>
{/* user select */}
<Row
label={t('common:core.chat.response.user_select_result')}
value={activeModule?.userSelectResult}
/>
</Box>
)}
</>