mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-29 09:44:47 +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:
@@ -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}`,
|
||||
|
Reference in New Issue
Block a user