mirror of
https://github.com/labring/FastGPT.git
synced 2025-08-01 11:58:38 +00:00
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:
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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')}
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@@ -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>;
|
||||
};
|
||||
|
||||
|
@@ -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;
|
||||
};
|
||||
|
||||
|
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
Reference in New Issue
Block a user