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

@@ -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,

View File

@@ -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,

View File

@@ -145,7 +145,7 @@ const Header = () => {
}
}}
>
{t('common:core.workflow.Debug')}
{t('common:core.workflow.run_test')}
</Button>
{!historiesDefaultData && (

View File

@@ -146,7 +146,7 @@ const Header = () => {
}
}}
>
{t('common:core.workflow.Debug')}
{t('common:core.workflow.run_test')}
</Button>
{!historiesDefaultData && (

View File

@@ -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

View File

@@ -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);

View File

@@ -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,

View File

@@ -621,7 +621,6 @@ const WorkflowContextProvider = ({
},
appId
});
// console.log({ finishedEdges, finishedNodes, nextStepRunNodes, flowResponses });
// 5. Store debug result
const newStoreDebugData = {
runtimeNodes: finishedNodes,

View File

@@ -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}`,