checklist

This commit is contained in:
zxhlyh 2025-05-20 16:52:21 +08:00
parent cf73faf174
commit eff123a11c
17 changed files with 165 additions and 56 deletions

View File

@ -1,4 +1,8 @@
import { memo } from 'react'
import {
memo,
useCallback,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowDownSLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
@ -7,20 +11,31 @@ import {
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { useNodesSyncDraft } from '@/app/components/workflow/hooks'
import Popup from './popup'
const Publisher = () => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const handleOpenChange = useCallback((newOpen: boolean) => {
if (newOpen)
handleSyncWorkflowDraft()
setOpen(newOpen)
}, [handleSyncWorkflowDraft])
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 4,
crossAxis: 40,
}}
>
<PortalToFollowElemTrigger>
<PortalToFollowElemTrigger onClick={() => handleOpenChange(!open)}>
<Button variant='primary'>
{t('workflow.common.publish')}
<RiArrowDownSLine className='h-4 w-4' />

View File

@ -9,11 +9,22 @@ import {
RiPlayCircleLine,
RiTerminalBoxLine,
} from '@remixicon/react'
import { useKeyPress } from 'ahooks'
import { useTranslation } from 'react-i18next'
import { useStore } from '@/app/components/workflow/store'
import {
useStore,
useWorkflowStore,
} from '@/app/components/workflow/store'
import Button from '@/app/components/base/button'
import { useFormatTimeFromNow } from '@/app/components/workflow/hooks'
import {
useChecklistBeforePublish,
useFormatTimeFromNow,
} from '@/app/components/workflow/hooks'
import Divider from '@/app/components/base/divider'
import { getKeyboardKeyCodeBySystem } from '@/app/components/workflow/utils'
import { usePublishWorkflow } from '@/service/use-workflow'
import type { PublishWorkflowParams } from '@/types/workflow'
import { useToastContext } from '@/app/components/base/toast'
const PUBLISH_SHORTCUT = ['⌘', '⇧', 'P']
@ -22,16 +33,40 @@ const Popup = () => {
const [published, setPublished] = useState(false)
const publishedAt = useStore(s => s.publishedAt)
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
const pipelineId = useStore(s => s.pipelineId)
const { formatTimeFromNow } = useFormatTimeFromNow()
const { handleCheckBeforePublish } = useChecklistBeforePublish()
const { mutateAsync: publishWorkflow } = usePublishWorkflow()
const { notify } = useToastContext()
const workflowStore = useWorkflowStore()
const handlePublish = useCallback(async () => {
try {
const handlePublish = useCallback(async (params?: PublishWorkflowParams) => {
if (await handleCheckBeforePublish()) {
const res = await publishWorkflow({
url: `/rag/pipelines/${pipelineId}/workflows/publish`,
title: params?.title || '',
releaseNotes: params?.releaseNotes || '',
})
setPublished(true)
if (res) {
notify({ type: 'success', message: t('common.api.actionSuccess') })
workflowStore.getState().setPublishedAt(res.created_at)
}
}
catch {
setPublished(false)
else {
throw new Error('Checklist failed')
}
}, [])
}, [workflowStore, notify, t, publishWorkflow, pipelineId, handleCheckBeforePublish])
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (e) => {
e.preventDefault()
if (published)
return
handlePublish()
},
{ exactMatch: true, useCapture: true },
)
return (
<div className='w-[320px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl shadow-shadow-shadow-5'>

View File

@ -12,8 +12,21 @@ export const useAvailableNodesMetaData = () => {
const mergedNodesMetaData = useMemo(() => [
...WORKFLOW_COMMON_NODES,
knowledgeBaseDefault,
dataSourceDefault,
{
...dataSourceDefault,
metaData: {
...dataSourceDefault.metaData,
isStart: true,
},
},
{
...knowledgeBaseDefault,
metaData: {
...knowledgeBaseDefault.metaData,
isRequired: true,
isUndeletable: true,
},
},
], [])
const prefixLink = useMemo(() => {

View File

@ -1,6 +1,25 @@
import { useTranslation } from 'react-i18next'
import { generateNewNode } from '@/app/components/workflow/utils'
import {
START_INITIAL_POSITION,
} from '@/app/components/workflow/constants'
import type { KnowledgeBaseNodeType } from '@/app/components/workflow/nodes/knowledge-base/types'
import knowledgeBaseDefault from '@/app/components/workflow/nodes/knowledge-base/default'
export const usePipelineTemplate = () => {
const { t } = useTranslation()
const { newNode: knowledgeBaseNode } = generateNewNode({
data: {
...knowledgeBaseDefault.defaultValue as KnowledgeBaseNodeType,
type: knowledgeBaseDefault.metaData.type,
title: t(`workflow.blocks.${knowledgeBaseDefault.metaData.type}`),
},
position: START_INITIAL_POSITION,
})
return {
nodes: [],
nodes: [knowledgeBaseNode],
edges: [],
}
}

View File

@ -78,7 +78,7 @@ const FeaturesTrigger = () => {
setShowFeaturesPanel(!showFeaturesPanel)
}, [workflowStore, getNodesReadOnly])
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory(appDetail!.id)
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory()
const updateAppDetail = useCallback(async () => {
try {
@ -89,10 +89,11 @@ const FeaturesTrigger = () => {
console.error(error)
}
}, [appID, setAppDetail])
const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!)
const { mutateAsync: publishWorkflow } = usePublishWorkflow()
const onPublish = useCallback(async (params?: PublishWorkflowParams) => {
if (await handleCheckBeforePublish()) {
const res = await publishWorkflow({
url: `/apps/${appID}/workflows/publish`,
title: params?.title || '',
releaseNotes: params?.releaseNotes || '',
})
@ -107,7 +108,7 @@ const FeaturesTrigger = () => {
else {
throw new Error('Checklist failed')
}
}, [handleCheckBeforePublish, notify, t, workflowStore, publishWorkflow, resetWorkflowVersionHistory, updateAppDetail])
}, [handleCheckBeforePublish, notify, appID, t, workflowStore, publishWorkflow, resetWorkflowVersionHistory, updateAppDetail])
const onPublisherToggle = useCallback((state: boolean) => {
if (state)

View File

@ -15,11 +15,35 @@ export const useAvailableNodesMetaData = () => {
const mergedNodesMetaData = useMemo(() => [
...WORKFLOW_COMMON_NODES,
StartDefault,
{
...StartDefault,
metaData: {
...StartDefault.metaData,
isStart: true,
isRequired: true,
isUndeletable: true,
},
},
...(
isChatMode
? [AnswerDefault]
: [EndDefault]
? [
{
...AnswerDefault,
metaData: {
...AnswerDefault.metaData,
isRequired: true,
},
},
]
: [
{
...EndDefault,
metaData: {
...EndDefault.metaData,
isRequired: true,
},
},
]
),
], [isChatMode])

View File

@ -21,6 +21,9 @@ const NodeSelectorWrapper = (props: NodeSelectorProps) => {
if (block.metaData.type === BlockEnum.DataSource)
return false
if (block.metaData.type === BlockEnum.Tool)
return false
if (block.metaData.type === BlockEnum.IterationStart)
return false

View File

@ -71,7 +71,7 @@ export const SUPPORT_OUTPUT_VARS_NODE = [
BlockEnum.HttpRequest, BlockEnum.Tool, BlockEnum.VariableAssigner, BlockEnum.VariableAggregator, BlockEnum.QuestionClassifier,
BlockEnum.ParameterExtractor, BlockEnum.Iteration, BlockEnum.Loop,
BlockEnum.DocExtractor, BlockEnum.ListFilter,
BlockEnum.Agent,
BlockEnum.Agent, BlockEnum.DataSource,
]
export const LLM_OUTPUT_STRUCT: Var[] = [

View File

@ -19,6 +19,7 @@ import assignerDefault from '@/app/components/workflow/nodes/assigner/default'
import httpRequestDefault from '@/app/components/workflow/nodes/http/default'
import parameterExtractorDefault from '@/app/components/workflow/nodes/parameter-extractor/default'
import listOperatorDefault from '@/app/components/workflow/nodes/list-operator/default'
import toolDefault from '@/app/components/workflow/nodes/tool/default'
export const WORKFLOW_COMMON_NODES = [
llmDefault,
@ -39,4 +40,5 @@ export const WORKFLOW_COMMON_NODES = [
parameterExtractorDefault,
httpRequestDefault,
listOperatorDefault,
toolDefault,
]

View File

@ -21,13 +21,13 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean)
} = useNodesMetaData()
const availableNodesType = useMemo(() => availableNodes.map(node => node.metaData.type), [availableNodes])
const availablePrevBlocks = useMemo(() => {
if (!nodeType || nodeType === BlockEnum.Start)
if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource)
return []
return availableNodesType
}, [availableNodesType, nodeType])
const availableNextBlocks = useMemo(() => {
if (!nodeType || nodeType === BlockEnum.End || nodeType === BlockEnum.LoopEnd)
if (!nodeType || nodeType === BlockEnum.End || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
return []
return availableNodesType
@ -35,11 +35,11 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean)
const getAvailableBlocks = useCallback((nodeType?: BlockEnum, inContainer?: boolean) => {
let availablePrevBlocks = availableNodesType
if (!nodeType || nodeType === BlockEnum.Start)
if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource)
availablePrevBlocks = []
let availableNextBlocks = availableNodesType
if (!nodeType || nodeType === BlockEnum.End || nodeType === BlockEnum.LoopEnd)
if (!nodeType || nodeType === BlockEnum.End || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
availableNextBlocks = []
return {

View File

@ -21,7 +21,6 @@ import {
MAX_TREE_DEPTH,
} from '../constants'
import type { ToolNodeType } from '../nodes/tool/types'
import { useIsChatMode } from './use-workflow'
import { useNodesMetaData } from './use-nodes-meta-data'
import { useToastContext } from '@/app/components/base/toast'
import { CollectionType } from '@/app/components/tools/types'
@ -38,7 +37,6 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const { t } = useTranslation()
const language = useGetLanguage()
const { nodesMap: nodesExtraData } = useNodesMetaData()
const isChatMode = useIsChatMode()
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
@ -101,7 +99,6 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
if (node.type === CUSTOM_NODE) {
const checkData = getCheckData(node.data)
const { errorMessage } = nodesExtraData![node.data.type].checkValid(checkData, t, moreDataForCheckValid)
if (errorMessage || !validNodes.find(n => n.id === node.id)) {
list.push({
id: node.id,
@ -115,26 +112,21 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
}
}
if (isChatMode && !nodes.find(node => node.data.type === BlockEnum.Answer)) {
list.push({
id: 'answer-need-added',
type: BlockEnum.Answer,
title: t('workflow.blocks.answer'),
errorMessage: t('workflow.common.needAnswerNode'),
})
}
const isRequiredNodesType = Object.keys(nodesExtraData!).filter((key: any) => (nodesExtraData as any)[key].metaData.isRequired)
if (!isChatMode && !nodes.find(node => node.data.type === BlockEnum.End)) {
list.push({
id: 'end-need-added',
type: BlockEnum.End,
title: t('workflow.blocks.end'),
errorMessage: t('workflow.common.needEndNode'),
})
}
isRequiredNodesType.forEach((type: string) => {
if (!nodes.find(node => node.data.type === type)) {
list.push({
id: `${type}-need-added`,
type,
title: t(`workflow.blocks.${type}`),
errorMessage: t('workflow.common.needAdd', { node: t(`workflow.blocks.${type}`) }),
})
}
})
return list
}, [nodes, edges, isChatMode, buildInTools, customTools, workflowTools, language, nodesExtraData, t, strategyProviders, getCheckData])
}, [nodes, edges, buildInTools, customTools, workflowTools, language, nodesExtraData, t, strategyProviders, getCheckData])
return needWarningNodes
}
@ -146,7 +138,6 @@ export const useChecklistBeforePublish = () => {
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
const { notify } = useToastContext()
const isChatMode = useIsChatMode()
const store = useStoreApi()
const { nodesMap: nodesExtraData } = useNodesMetaData()
const { data: strategyProviders } = useStrategyProviders()
@ -241,18 +232,18 @@ export const useChecklistBeforePublish = () => {
}
}
if (isChatMode && !nodes.find(node => node.data.type === BlockEnum.Answer)) {
notify({ type: 'error', message: t('workflow.common.needAnswerNode') })
return false
}
const isRequiredNodesType = Object.keys(nodesExtraData!).filter((key: any) => (nodesExtraData as any)[key].metaData.isRequired)
if (!isChatMode && !nodes.find(node => node.data.type === BlockEnum.End)) {
notify({ type: 'error', message: t('workflow.common.needEndNode') })
return false
for(let i = 0; i < isRequiredNodesType.length; i++) {
const type = isRequiredNodesType[i]
if (!nodes.find(node => node.data.type === type)) {
notify({ type: 'error', message: t('workflow.common.needAdd', { node: t(`workflow.blocks.${type}`) }) })
return false
}
}
return true
}, [store, isChatMode, notify, t, buildInTools, customTools, workflowTools, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData])
}, [store, notify, t, buildInTools, customTools, workflowTools, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData])
return {
handleCheckBeforePublish,

View File

@ -549,7 +549,7 @@ export const useNodesInteractions = () => {
if (!currentNode)
return
if (currentNode.data.type === BlockEnum.Start)
if (nodesMetaDataMap?.[currentNode.data.type as BlockEnum].metaData.isUndeletable)
return
if (currentNode.data.type === BlockEnum.Iteration) {
@ -656,7 +656,7 @@ export const useNodesInteractions = () => {
else
saveStateToHistory(WorkflowHistoryEvent.NodeDelete)
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, t])
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, t, nodesMetaDataMap])
const handleNodeAdd = useCallback<OnNodeAdd>((
{

View File

@ -302,6 +302,9 @@ export type NodeDefault<T = {}> = {
author: string
description?: string
helpLinkUri?: string
isRequired?: boolean
isUndeletable?: boolean
isStart?: boolean
}
defaultValue: Partial<T>
checkValid: (payload: T, t: any, moreDataForCheckValid?: any) => { isValid: boolean; errorMessage?: string }

View File

@ -46,6 +46,7 @@ const translation = {
setVarValuePlaceholder: 'Set variable',
needConnectTip: 'This step is not connected to anything',
maxTreeDepth: 'Maximum limit of {{depth}} nodes per branch',
needAdd: '{{node}} block must be added',
needEndNode: 'The End block must be added',
needAnswerNode: 'The Answer block must be added',
workflowProcess: 'Workflow Process',

View File

@ -45,6 +45,7 @@ const translation = {
setVarValuePlaceholder: '设置变量值',
needConnectTip: '此节点尚未连接到其他节点',
maxTreeDepth: '每个分支最大限制 {{depth}} 个节点',
needAdd: '必须添加{{node}}节点',
needEndNode: '必须添加结束节点',
needAnswerNode: '必须添加直接回复节点',
workflowProcess: '工作流',

View File

@ -76,10 +76,10 @@ export const useDeleteWorkflow = () => {
})
}
export const usePublishWorkflow = (appId: string) => {
export const usePublishWorkflow = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'publish'],
mutationFn: (params: PublishWorkflowParams) => post<CommonResponse & { created_at: number }>(`/apps/${appId}/workflows/publish`, {
mutationFn: (params: PublishWorkflowParams) => post<CommonResponse & { created_at: number }>(params.url, {
body: {
marked_name: params.title,
marked_comment: params.releaseNotes,

View File

@ -345,6 +345,7 @@ export type WorkflowConfigResponse = {
}
export type PublishWorkflowParams = {
url: string
title: string
releaseNotes: string
}