From 02024692544a2afe541cee1c85a04316ea0b2cfd Mon Sep 17 00:00:00 2001 From: StyleZhang Date: Tue, 2 Apr 2024 19:06:05 +0800 Subject: [PATCH] fix: checklist --- .../components/workflow/header/checklist.tsx | 51 ++------ web/app/components/workflow/header/index.tsx | 6 +- web/app/components/workflow/hooks/index.ts | 2 + .../workflow/hooks/use-checklist.ts | 114 ++++++++++++++++++ .../workflow/hooks/use-workflow-run.ts | 40 +----- .../components/workflow/hooks/use-workflow.ts | 45 ------- web/app/components/workflow/utils.ts | 44 ++++++- web/i18n/en-US/workflow.ts | 2 + web/i18n/zh-Hans/workflow.ts | 2 + 9 files changed, 176 insertions(+), 130 deletions(-) create mode 100644 web/app/components/workflow/hooks/use-checklist.ts diff --git a/web/app/components/workflow/header/checklist.tsx b/web/app/components/workflow/header/checklist.tsx index e43ab2f5b2..b84219316b 100644 --- a/web/app/components/workflow/header/checklist.tsx +++ b/web/app/components/workflow/header/checklist.tsx @@ -1,23 +1,21 @@ import { memo, - useMemo, useState, } from 'react' import { useTranslation } from 'react-i18next' import { - getIncomers, - getOutgoers, useEdges, useNodes, } from 'reactflow' import BlockIcon from '../block-icon' import { - useNodesExtraData, + useChecklist, useNodesInteractions, } from '../hooks' -import type { CommonNodeType } from '../types' -import { BlockEnum } from '../types' -import { useStore } from '../store' +import type { + CommonEdgeType, + CommonNodeType, +} from '../types' import { PortalToFollowElem, PortalToFollowElemContent, @@ -34,44 +32,9 @@ const WorkflowChecklist = () => { const { t } = useTranslation() const [open, setOpen] = useState(false) const nodes = useNodes() - const edges = useEdges() - const nodesExtraData = useNodesExtraData() + const edges = useEdges() + const needWarningNodes = useChecklist(nodes, edges) const { handleNodeSelect } = useNodesInteractions() - const buildInTools = useStore(s => s.buildInTools) - const customTools = useStore(s => s.customTools) - - const needWarningNodes = useMemo(() => { - const list = [] - - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i] - const incomers = getIncomers(node, nodes, edges) - const outgoers = getOutgoers(node, nodes, edges) - const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t) - let toolIcon - - if (node.data.type === BlockEnum.Tool) { - if (node.data.provider_type === 'builtin') - toolIcon = buildInTools.find(tool => tool.id === node.data.provider_id)?.icon - - if (node.data.provider_type === 'custom') - toolIcon = customTools.find(tool => tool.id === node.data.provider_id)?.icon - } - - if (errorMessage || ((!incomers.length && !outgoers.length))) { - list.push({ - id: node.id, - type: node.data.type, - title: node.data.title, - toolIcon, - unConnected: !incomers.length && !outgoers.length, - errorMessage, - }) - } - } - - return list - }, [t, nodes, edges, nodesExtraData, buildInTools, customTools]) return ( { const { handleLoadBackupDraft, handleRunSetting, - handleCheckBeforePublish, handleBackupDraft, handleRestoreFromPublishedWorkflow, } = useWorkflowRun() + const { handleCheckBeforePublish } = useChecklistBeforePublish() const { handleSyncWorkflowDraft } = useNodesSyncDraft() const { notify } = useContext(ToastContext) @@ -84,6 +85,9 @@ const Header: FC = () => { workflowStore.getState().setPublishedAt(res.created_at) } } + else { + throw new Error('Checklist failed') + } }, [appID, handleCheckBeforePublish, notify, t, workflowStore]) const onStartRestoring = useCallback(() => { diff --git a/web/app/components/workflow/hooks/index.ts b/web/app/components/workflow/hooks/index.ts index f1caffef8d..7c72ddcae3 100644 --- a/web/app/components/workflow/hooks/index.ts +++ b/web/app/components/workflow/hooks/index.ts @@ -5,3 +5,5 @@ export * from './use-nodes-data' export * from './use-nodes-sync-draft' export * from './use-workflow' export * from './use-workflow-run' +export * from './use-workflow-template' +export * from './use-checklist' diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts new file mode 100644 index 0000000000..770cc71229 --- /dev/null +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -0,0 +1,114 @@ +import { + useCallback, + useMemo, +} from 'react' +import { useTranslation } from 'react-i18next' +import { useStoreApi } from 'reactflow' +import type { + Edge, + Node, +} from '../types' +import { BlockEnum } from '../types' +import { useStore } from '../store' +import { getValidTreeNodes } from '../utils' +import { MAX_TREE_DEEPTH } from '../constants' +import { useIsChatMode } from './use-workflow' +import { useNodesExtraData } from './use-nodes-data' +import { useToastContext } from '@/app/components/base/toast' + +export const useChecklist = (nodes: Node[], edges: Edge[]) => { + const { t } = useTranslation() + const nodesExtraData = useNodesExtraData() + const buildInTools = useStore(s => s.buildInTools) + const customTools = useStore(s => s.customTools) + + const needWarningNodes = useMemo(() => { + const list = [] + const { validNodes } = getValidTreeNodes(nodes, edges) + + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i] + const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t) + let toolIcon + + if (node.data.type === BlockEnum.Tool) { + if (node.data.provider_type === 'builtin') + toolIcon = buildInTools.find(tool => tool.id === node.data.provider_id)?.icon + + if (node.data.provider_type === 'custom') + toolIcon = customTools.find(tool => tool.id === node.data.provider_id)?.icon + } + + if (errorMessage || !validNodes.find(n => n.id === node.id)) { + list.push({ + id: node.id, + type: node.data.type, + title: node.data.title, + toolIcon, + unConnected: !validNodes.find(n => n.id === node.id), + errorMessage, + }) + } + } + + return list + }, [t, nodes, edges, nodesExtraData, buildInTools, customTools]) + + return needWarningNodes +} + +export const useChecklistBeforePublish = () => { + const { t } = useTranslation() + const { notify } = useToastContext() + const isChatMode = useIsChatMode() + const store = useStoreApi() + const nodesExtraData = useNodesExtraData() + + const handleCheckBeforePublish = useCallback(() => { + const { + getNodes, + edges, + } = store.getState() + const nodes = getNodes() + const { + validNodes, + maxDepth, + } = getValidTreeNodes(nodes, edges) + + if (maxDepth > MAX_TREE_DEEPTH) { + notify({ type: 'error', message: t('workflow.common.maxTreeDepth', { depth: MAX_TREE_DEEPTH }) }) + return false + } + + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i] + const { errorMessage } = nodesExtraData[node.data.type as BlockEnum].checkValid(node.data, t) + + if (errorMessage) { + notify({ type: 'error', message: `[${node.data.title}] ${errorMessage}` }) + return false + } + + if (!validNodes.find(n => n.id === node.id)) { + notify({ type: 'error', message: `[${node.data.title}] ${t('workflow.common.needConnecttip')}` }) + return false + } + } + + if (isChatMode && !nodes.find(node => node.data.type === BlockEnum.Answer)) { + notify({ type: 'error', message: t('workflow.common.needAnswerNode') }) + return false + } + + if (!isChatMode && !nodes.find(node => node.data.type === BlockEnum.End)) { + notify({ type: 'error', message: t('workflow.common.needEndNode') }) + return false + } + + return true + }, [nodesExtraData, notify, t, store, isChatMode]) + + return { + handleCheckBeforePublish, + } +} diff --git a/web/app/components/workflow/hooks/use-workflow-run.ts b/web/app/components/workflow/hooks/use-workflow-run.ts index d74c4a7d7c..75c437fac7 100644 --- a/web/app/components/workflow/hooks/use-workflow-run.ts +++ b/web/app/components/workflow/hooks/use-workflow-run.ts @@ -3,15 +3,12 @@ import { useReactFlow, useStoreApi, } from 'reactflow' -import { useTranslation } from 'react-i18next' import produce from 'immer' import { useWorkflowStore } from '../store' import { NodeRunningStatus, WorkflowRunningStatus, } from '../types' -import { MAX_TREE_DEEPTH } from '../constants' -import { useNodesExtraData } from './use-nodes-data' import { useWorkflow } from './use-workflow' import { useStore as useAppStore } from '@/app/components/app/store' import type { IOtherOptions } from '@/service/base' @@ -21,20 +18,13 @@ import { stopWorkflowRun, } from '@/service/workflow' import { useFeaturesStore } from '@/app/components/base/features/hooks' -import { useToastContext } from '@/app/components/base/toast' export const useWorkflowRun = () => { - const { t } = useTranslation() - const { notify } = useToastContext() const store = useStoreApi() const workflowStore = useWorkflowStore() const reactflow = useReactFlow() const featuresStore = useFeaturesStore() - const nodesExtraData = useNodesExtraData() - const { - getValidTreeNodes, - renderTreeFromRecord, - } = useWorkflow() + const { renderTreeFromRecord } = useWorkflow() const handleBackupDraft = useCallback(() => { const { @@ -331,33 +321,6 @@ export const useWorkflowRun = () => { } }, [featuresStore, workflowStore, renderTreeFromRecord]) - const handleCheckBeforePublish = useCallback(() => { - const { - validNodes, - maxDepth, - } = getValidTreeNodes() - - if (!validNodes.length) - return false - - if (maxDepth > MAX_TREE_DEEPTH) { - notify({ type: 'error', message: t('workflow.common.maxTreeDepth', { depth: MAX_TREE_DEEPTH }) }) - return false - } - - for (let i = 0; i < validNodes.length; i++) { - const node = validNodes[i] - const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t) - - if (errorMessage) { - notify({ type: 'error', message: `[${node.data.title}] ${errorMessage}` }) - return false - } - } - - return true - }, [getValidTreeNodes, nodesExtraData, notify, t]) - return { handleBackupDraft, handleLoadBackupDraft, @@ -365,6 +328,5 @@ export const useWorkflowRun = () => { handleRun, handleStopRun, handleRestoreFromPublishedWorkflow, - handleCheckBeforePublish, } } diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index 0093f3fd6b..328f7ae514 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -283,50 +283,6 @@ export const useWorkflow = () => { return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow() }, [locale]) - const getValidTreeNodes = useCallback(() => { - const { - getNodes, - edges, - } = store.getState() - const nodes = getNodes() - - const startNode = nodes.find(node => node.data.type === BlockEnum.Start) - - if (!startNode) { - return { - validNodes: [], - maxDepth: 0, - } - } - - const list: Node[] = [startNode] - let maxDepth = 1 - - const traverse = (root: Node, depth: number) => { - if (depth > maxDepth) - maxDepth = depth - - const outgoers = getOutgoers(root, nodes, edges) - - if (outgoers.length) { - outgoers.forEach((outgoer) => { - list.push(outgoer) - traverse(outgoer, depth + 1) - }) - } - else { - list.push(root) - } - } - - traverse(startNode, maxDepth) - - return { - validNodes: uniqBy(list, 'id'), - maxDepth, - } - }, [store]) - const renderTreeFromRecord = useCallback((nodes: Node[], edges: Edge[], viewport?: Viewport) => { const { setNodes } = store.getState() const { setViewport, setEdges } = reactflow @@ -356,7 +312,6 @@ export const useWorkflow = () => { isNodeVarsUsedInNodes, isValidConnection, formatTimeFromNow, - getValidTreeNodes, renderTreeFromRecord, getNode, } diff --git a/web/app/components/workflow/utils.ts b/web/app/components/workflow/utils.ts index 8cfcd434a2..a3e47a410e 100644 --- a/web/app/components/workflow/utils.ts +++ b/web/app/components/workflow/utils.ts @@ -1,9 +1,13 @@ import { Position, getConnectedEdges, + getOutgoers, } from 'reactflow' import dagre from 'dagre' -import { cloneDeep } from 'lodash-es' +import { + cloneDeep, + uniqBy, +} from 'lodash-es' import type { Edge, Node, @@ -186,3 +190,41 @@ export const generateNewNode = ({ data, position, id }: Pick { + const startNode = nodes.find(node => node.data.type === BlockEnum.Start) + + if (!startNode) { + return { + validNodes: [], + maxDepth: 0, + } + } + + const list: Node[] = [startNode] + let maxDepth = 1 + + const traverse = (root: Node, depth: number) => { + if (depth > maxDepth) + maxDepth = depth + + const outgoers = getOutgoers(root, nodes, edges) + + if (outgoers.length) { + outgoers.forEach((outgoer) => { + list.push(outgoer) + traverse(outgoer, depth + 1) + }) + } + else { + list.push(root) + } + } + + traverse(startNode, maxDepth) + + return { + validNodes: uniqBy(list, 'id'), + maxDepth, + } +} diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 6f0fe3708f..5e764c933e 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -36,6 +36,8 @@ const translation = { setVarValuePlaceholder: 'Set variable', needConnecttip: 'This step is not connected to anything', maxTreeDepth: 'Maximum limit of {{depth}} nodes per branch', + needEndNode: 'Please add an end node', + needAnswerNode: 'Please add a answer node', workflowProcess: 'Workflow Process', notRunning: 'Not running yet', previewPlaceholder: 'Enter content in the box below to start debugging the Chatbot', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 8fa02a5645..268b666ab2 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -36,6 +36,8 @@ const translation = { setVarValuePlaceholder: '设置变量值', needConnecttip: '此节点尚未连接到其他节点', maxTreeDepth: '每个分支最大限制 {{depth}} 个节点', + needEndNode: '请添加结束节点', + needAnswerNode: '请添加直接回复节点', workflowProcess: '工作流', notRunning: '尚未运行', previewPlaceholder: '在下面的框中输入内容开始调试聊天机器人',