fix: checklist

This commit is contained in:
StyleZhang 2024-04-02 19:06:05 +08:00
parent fb2fa625b4
commit 0202469254
9 changed files with 176 additions and 130 deletions

View File

@ -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<CommonNodeType>()
const edges = useEdges()
const nodesExtraData = useNodesExtraData()
const edges = useEdges<CommonEdgeType>()
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 (
<PortalToFollowElem

View File

@ -10,6 +10,7 @@ import {
useWorkflowStore,
} from '../store'
import {
useChecklistBeforePublish,
useNodesReadOnly,
useNodesSyncDraft,
useWorkflowRun,
@ -43,10 +44,10 @@ const Header: FC = () => {
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(() => {

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Node, 'data' | 'pos
sourcePosition: Position.Right,
} as Node
}
export const getValidTreeNodes = (nodes: Node[], edges: Edge[]) => {
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,
}
}

View File

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

View File

@ -36,6 +36,8 @@ const translation = {
setVarValuePlaceholder: '设置变量值',
needConnecttip: '此节点尚未连接到其他节点',
maxTreeDepth: '每个分支最大限制 {{depth}} 个节点',
needEndNode: '请添加结束节点',
needAnswerNode: '请添加直接回复节点',
workflowProcess: '工作流',
notRunning: '尚未运行',
previewPlaceholder: '在下面的框中输入内容开始调试聊天机器人',