mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 21:13:50 +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:
83
projects/app/public/imgs/app/userSelect.svg
Normal file
83
projects/app/public/imgs/app/userSelect.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 22 KiB |
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
@@ -9,8 +9,7 @@ import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
|
||||
import { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import { removeEmptyUserInput } from '@fastgpt/global/core/chat/utils';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
@@ -22,11 +21,18 @@ import { NextAPI } from '@/service/middleware/entry';
|
||||
import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt';
|
||||
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
|
||||
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
|
||||
import {
|
||||
getWorkflowEntryNodeIds,
|
||||
initWorkflowEdgeStatus,
|
||||
rewriteNodeOutputByHistories,
|
||||
storeNodes2RuntimeNodes
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
|
||||
export type Props = {
|
||||
messages: ChatCompletionMessageParam[];
|
||||
nodes: RuntimeNodeItemType[];
|
||||
edges: RuntimeEdgeItemType[];
|
||||
nodes: StoreNodeItemType[];
|
||||
edges: StoreEdgeItemType[];
|
||||
variables: Record<string, any>;
|
||||
appId: string;
|
||||
appName: string;
|
||||
@@ -52,8 +58,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
chatConfig
|
||||
} = req.body as Props;
|
||||
try {
|
||||
// [histories, user]
|
||||
const chatMessages = GPTMessages2Chats(messages);
|
||||
|
||||
const userInput = chatMessages.pop()?.value as UserChatItemValueItemType[] | undefined;
|
||||
|
||||
/* user auth */
|
||||
@@ -64,6 +70,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
authToken: true
|
||||
})
|
||||
]);
|
||||
// auth balance
|
||||
const { user } = await getUserChatInfoAndAuthTeamPoints(tmbId);
|
||||
|
||||
const isPlugin = app.type === AppTypeEnum.plugin;
|
||||
|
||||
if (!Array.isArray(nodes)) {
|
||||
@@ -73,18 +82,19 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
throw new Error('Edges is not array');
|
||||
}
|
||||
|
||||
let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, chatMessages));
|
||||
|
||||
// Plugin need to replace inputs
|
||||
if (isPlugin) {
|
||||
nodes = updatePluginInputByVariables(nodes, variables);
|
||||
variables = removePluginInputVariables(variables, nodes);
|
||||
runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables);
|
||||
variables = removePluginInputVariables(variables, runtimeNodes);
|
||||
} else {
|
||||
if (!userInput) {
|
||||
throw new Error('Params Error');
|
||||
}
|
||||
}
|
||||
|
||||
// auth balance
|
||||
const { user } = await getUserChatInfoAndAuthTeamPoints(tmbId);
|
||||
runtimeNodes = rewriteNodeOutputByHistories(chatMessages, runtimeNodes);
|
||||
|
||||
/* start process */
|
||||
const { flowResponses, flowUsages } = await dispatchWorkFlow({
|
||||
@@ -95,8 +105,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
tmbId,
|
||||
user,
|
||||
app,
|
||||
runtimeNodes: nodes,
|
||||
runtimeEdges: edges,
|
||||
runtimeNodes,
|
||||
runtimeEdges: initWorkflowEdgeStatus(edges, chatMessages),
|
||||
variables,
|
||||
query: removeEmptyUserInput(userInput),
|
||||
chatConfig,
|
||||
|
@@ -13,7 +13,7 @@ import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
|
||||
import type { ChatCompletionCreateParams } from '@fastgpt/global/core/ai/type.d';
|
||||
import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type.d';
|
||||
import {
|
||||
getDefaultEntryNodeIds,
|
||||
getWorkflowEntryNodeIds,
|
||||
getMaxHistoryLimitFromNodes,
|
||||
initWorkflowEdgeStatus,
|
||||
storeNodes2RuntimeNodes,
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
getPluginRunContent
|
||||
} from '@fastgpt/global/core/app/plugin/utils';
|
||||
import { getSystemTime } from '@fastgpt/global/common/time/timezone';
|
||||
import { rewriteNodeOutputByHistories } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
|
||||
type FastGptWebChatProps = {
|
||||
chatId?: string; // undefined: get histories from messages, '': new chat, 'xxxxx': get histories from db
|
||||
@@ -225,24 +226,22 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
appId: app._id,
|
||||
chatId,
|
||||
limit,
|
||||
field: `dataId obj value`
|
||||
field: `dataId obj value nodeOutputs`
|
||||
}),
|
||||
getAppLatestVersion(app._id, app)
|
||||
]);
|
||||
const newHistories = concatHistories(histories, chatMessages);
|
||||
|
||||
// Get runtimeNodes
|
||||
const runtimeNodes = isPlugin
|
||||
? updatePluginInputByVariables(
|
||||
storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)),
|
||||
variables
|
||||
)
|
||||
: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes));
|
||||
let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, newHistories));
|
||||
|
||||
const runtimeVariables = removePluginInputVariables(
|
||||
variables,
|
||||
storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes))
|
||||
);
|
||||
if (isPlugin) {
|
||||
// Rewrite plugin run params variables
|
||||
variables = removePluginInputVariables(variables, runtimeNodes);
|
||||
runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables);
|
||||
}
|
||||
|
||||
runtimeNodes = rewriteNodeOutputByHistories(newHistories, runtimeNodes);
|
||||
|
||||
/* start flow controller */
|
||||
const { flowResponses, flowUsages, assistantResponses, newVariables } = await (async () => {
|
||||
@@ -258,8 +257,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
chatId,
|
||||
responseChatItemId,
|
||||
runtimeNodes,
|
||||
runtimeEdges: initWorkflowEdgeStatus(edges),
|
||||
variables: runtimeVariables,
|
||||
runtimeEdges: initWorkflowEdgeStatus(edges, newHistories),
|
||||
variables,
|
||||
query: removeEmptyUserInput(userQuestion.value),
|
||||
chatConfig,
|
||||
histories: newHistories,
|
||||
|
@@ -145,7 +145,7 @@ const Header = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('common:core.workflow.Debug')}
|
||||
{t('common:core.workflow.run_test')}
|
||||
</Button>
|
||||
|
||||
{!historiesDefaultData && (
|
||||
|
@@ -146,7 +146,7 @@ const Header = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('common:core.workflow.Debug')}
|
||||
{t('common:core.workflow.run_test')}
|
||||
</Button>
|
||||
|
||||
{!historiesDefaultData && (
|
||||
|
@@ -56,7 +56,8 @@ const nodeTypes: Record<FlowNodeTypeEnum, any> = {
|
||||
[FlowNodeTypeEnum.lafModule]: dynamic(() => import('./nodes/NodeLaf')),
|
||||
[FlowNodeTypeEnum.ifElseNode]: dynamic(() => import('./nodes/NodeIfElse')),
|
||||
[FlowNodeTypeEnum.variableUpdate]: dynamic(() => import('./nodes/NodeVariableUpdate')),
|
||||
[FlowNodeTypeEnum.code]: dynamic(() => import('./nodes/NodeCode'))
|
||||
[FlowNodeTypeEnum.code]: dynamic(() => import('./nodes/NodeCode')),
|
||||
[FlowNodeTypeEnum.userSelect]: dynamic(() => import('./nodes/NodeUserSelect'))
|
||||
};
|
||||
const edgeTypes = {
|
||||
[EDGE_TYPE]: ButtonEdge
|
||||
|
@@ -0,0 +1,144 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { NodeProps, Position } from 'reactflow';
|
||||
import { Box, Button, HStack, Input } from '@chakra-ui/react';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
|
||||
import Container from '../components/Container';
|
||||
import RenderInput from './render/RenderInput';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { SourceHandle } from './render/Handle';
|
||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
import { UserSelectOptionItemType } from '@fastgpt/global/core/workflow/template/system/userSelect/type';
|
||||
import IOTitle from '../components/IOTitle';
|
||||
import RenderOutput from './render/RenderOutput';
|
||||
|
||||
const NodeUserSelect = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { nodeId, inputs, outputs } = data;
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const CustomComponent = useMemo(
|
||||
() => ({
|
||||
[NodeInputKeyEnum.userSelectOptions]: ({
|
||||
key: optionKey,
|
||||
value = [],
|
||||
...props
|
||||
}: FlowNodeInputItemType) => {
|
||||
const options = value as UserSelectOptionItemType[];
|
||||
return (
|
||||
<Box>
|
||||
{options.map((item, i) => (
|
||||
<Box key={item.key} mb={4}>
|
||||
<HStack spacing={1}>
|
||||
<MyTooltip label={t('common:common.Delete')}>
|
||||
<MyIcon
|
||||
mt={0.5}
|
||||
name={'minus'}
|
||||
w={'0.8rem'}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.600'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: optionKey,
|
||||
value: {
|
||||
...props,
|
||||
key: optionKey,
|
||||
value: options.filter((input) => input.key !== item.key)
|
||||
}
|
||||
});
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'delOutput',
|
||||
key: item.key
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Box color={'myGray.600'} fontWeight={'medium'} fontSize={'sm'}>
|
||||
{t('common:option') + (i + 1)}
|
||||
</Box>
|
||||
</HStack>
|
||||
<Box position={'relative'}>
|
||||
<Input
|
||||
mt={1}
|
||||
defaultValue={item.value}
|
||||
bg={'white'}
|
||||
fontSize={'sm'}
|
||||
onChange={(e) => {
|
||||
const newVal = options.map((val) =>
|
||||
val.key === item.key
|
||||
? {
|
||||
...val,
|
||||
value: e.target.value
|
||||
}
|
||||
: val
|
||||
);
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: optionKey,
|
||||
value: {
|
||||
...props,
|
||||
key: optionKey,
|
||||
value: newVal
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={getHandleId(nodeId, 'source', item.key)}
|
||||
position={Position.Right}
|
||||
translate={[26, 0]}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
<Button
|
||||
fontSize={'sm'}
|
||||
leftIcon={<MyIcon name={'common/addLight'} w={4} />}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: optionKey,
|
||||
value: {
|
||||
...props,
|
||||
key: optionKey,
|
||||
value: options.concat({ value: '', key: getNanoid() })
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('common:core.module.Add_option')}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}),
|
||||
[nodeId, onChangeNode, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<NodeCard minW={'400px'} selected={selected} {...data}>
|
||||
<Container>
|
||||
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
|
||||
</Container>
|
||||
<Container>
|
||||
<IOTitle text={t('common:common.Output')} />
|
||||
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeUserSelect);
|
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Box, Button, Card, Flex } from '@chakra-ui/react';
|
||||
import { Box, Button, Card, Flex, Image } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
|
||||
@@ -42,7 +42,6 @@ type Props = FlowNodeItemType & {
|
||||
|
||||
const NodeCard = (props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -70,7 +69,7 @@ const NodeCard = (props: Props) => {
|
||||
// custom title edit
|
||||
const { onOpenModal: onOpenCustomTitleModal, EditModal: EditTitleModal } = useEditTitle({
|
||||
title: t('common:common.Custom Title'),
|
||||
placeholder: appT('module.Custom Title Tip') || ''
|
||||
placeholder: t('app:module.Custom Title Tip') || ''
|
||||
});
|
||||
|
||||
const showToolHandle = useMemo(
|
||||
@@ -166,7 +165,7 @@ const NodeCard = (props: Props) => {
|
||||
onSuccess: (e) => {
|
||||
if (!e) {
|
||||
return toast({
|
||||
title: appT('modules.Title is required'),
|
||||
title: t('app:modules.Title is required'),
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
@@ -183,7 +182,7 @@ const NodeCard = (props: Props) => {
|
||||
)}
|
||||
<Box flex={1} />
|
||||
{hasNewVersion && (
|
||||
<MyTooltip label={appT('app.modules.click to update')}>
|
||||
<MyTooltip label={t('app:app.modules.click to update')}>
|
||||
<Button
|
||||
bg={'yellow.50'}
|
||||
color={'yellow.600'}
|
||||
@@ -197,11 +196,29 @@ const NodeCard = (props: Props) => {
|
||||
_hover={{ bg: 'yellow.100' }}
|
||||
onClick={onOpenConfirmSync(onClickSyncVersion)}
|
||||
>
|
||||
<Box>{appT('app.modules.has new version')}</Box>
|
||||
<Box>{t('app:app.modules.has new version')}</Box>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</Button>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{!!nodeTemplate?.diagram && (
|
||||
<MyTooltip
|
||||
label={
|
||||
<Image src={nodeTemplate?.diagram} w={'100%'} minH={['auto', '200px']} alt={''} />
|
||||
}
|
||||
>
|
||||
<Box
|
||||
fontSize={'sm'}
|
||||
color={'primary.700'}
|
||||
p={1}
|
||||
rounded={'sm'}
|
||||
cursor={'default'}
|
||||
_hover={{ bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
>
|
||||
{t('common:core.module.Diagram')}
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Flex>
|
||||
<MenuRender nodeId={nodeId} menuForbid={menuForbid} />
|
||||
<NodeIntro nodeId={nodeId} intro={intro} />
|
||||
@@ -217,9 +234,9 @@ const NodeCard = (props: Props) => {
|
||||
name,
|
||||
menuForbid,
|
||||
hasNewVersion,
|
||||
appT,
|
||||
onOpenConfirmSync,
|
||||
onClickSyncVersion,
|
||||
nodeTemplate?.diagram,
|
||||
intro,
|
||||
ConfirmSyncModal,
|
||||
onOpenCustomTitleModal,
|
||||
|
@@ -621,7 +621,6 @@ const WorkflowContextProvider = ({
|
||||
},
|
||||
appId
|
||||
});
|
||||
// console.log({ finishedEdges, finishedNodes, nextStepRunNodes, flowResponses });
|
||||
// 5. Store debug result
|
||||
const newStoreDebugData = {
|
||||
runtimeNodes: finishedNodes,
|
||||
|
@@ -2,13 +2,7 @@ import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import React from 'react';
|
||||
import type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type';
|
||||
import { streamFetch } from '@/web/common/api/fetch';
|
||||
import { checkChatSupportSelectFileByModules } from '@/web/core/chat/utils';
|
||||
import {
|
||||
getDefaultEntryNodeIds,
|
||||
getMaxHistoryLimitFromNodes,
|
||||
initWorkflowEdgeStatus,
|
||||
storeNodes2RuntimeNodes
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { getMaxHistoryLimitFromNodes } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from './context';
|
||||
@@ -47,8 +41,8 @@ export const useChatTest = ({
|
||||
data: {
|
||||
// Send histories and user messages
|
||||
messages: messages.slice(-historyMaxLen - 2),
|
||||
nodes: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)),
|
||||
edges: initWorkflowEdgeStatus(edges),
|
||||
nodes,
|
||||
edges,
|
||||
variables,
|
||||
appId: appDetail._id,
|
||||
appName: `调试-${appDetail.name}`,
|
||||
|
@@ -6,7 +6,7 @@ import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { delay } from '@fastgpt/global/common/system/utils';
|
||||
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import {
|
||||
getDefaultEntryNodeIds,
|
||||
getWorkflowEntryNodeIds,
|
||||
initWorkflowEdgeStatus,
|
||||
storeNodes2RuntimeNodes
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
@@ -38,7 +38,7 @@ export const getScheduleTriggerApp = async () => {
|
||||
teamId: String(app.teamId),
|
||||
tmbId: String(app.tmbId),
|
||||
app,
|
||||
runtimeNodes: storeNodes2RuntimeNodes(app.modules, getDefaultEntryNodeIds(app.modules)),
|
||||
runtimeNodes: storeNodes2RuntimeNodes(app.modules, getWorkflowEntryNodeIds(app.modules)),
|
||||
runtimeEdges: initWorkflowEdgeStatus(app.edges),
|
||||
variables: {},
|
||||
query: [
|
||||
|
@@ -201,6 +201,11 @@ export const streamFetch = ({
|
||||
event,
|
||||
variables: parseJson
|
||||
});
|
||||
} else if (event === SseResponseEventEnum.interactive) {
|
||||
responseQueue.push({
|
||||
event,
|
||||
...parseJson
|
||||
});
|
||||
} else if (event === SseResponseEventEnum.error) {
|
||||
if (parseJson.statusText === TeamErrEnum.aiPointsNotEnough) {
|
||||
useSystemStore.getState().setIsNotSufficientModal(true);
|
||||
|
Reference in New Issue
Block a user