This commit is contained in:
StyleZhang
2024-03-20 13:49:02 +08:00
parent 2a75258836
commit 2697454a8e
22 changed files with 390 additions and 225 deletions

View File

@@ -8,7 +8,10 @@ import {
useStore,
useWorkflowStore,
} from '../store'
import { useWorkflowRun } from '../hooks'
import {
useNodesReadOnly,
useWorkflowRun,
} from '../hooks'
import RunAndHistory from './run-and-history'
import EditingTitle from './editing-title'
import RunningTitle from './running-title'
@@ -24,7 +27,10 @@ const Header: FC = () => {
const workflowStore = useWorkflowStore()
const appDetail = useAppStore(s => s.appDetail)
const appSidebarExpand = useAppStore(s => s.appSidebarExpand)
const runningStatus = useStore(s => s.runningStatus)
const {
nodesReadOnly,
getNodesReadOnly,
} = useNodesReadOnly()
const isRestoring = useStore(s => s.isRestoring)
const {
handleRunSetting,
@@ -32,11 +38,11 @@ const Header: FC = () => {
} = useWorkflowRun()
const handleShowFeatures = useCallback(() => {
if (runningStatus)
if (getNodesReadOnly())
return
workflowStore.setState({ showFeaturesPanel: true })
}, [runningStatus, workflowStore])
}, [getNodesReadOnly, workflowStore])
const handleGoBackToEdit = useCallback(() => {
handleRunSetting(true)
@@ -65,10 +71,10 @@ const Header: FC = () => {
)
}
{
!runningStatus && !isRestoring && <EditingTitle />
!nodesReadOnly && !isRestoring && <EditingTitle />
}
{
runningStatus && !isRestoring && <RunningTitle />
nodesReadOnly && !isRestoring && <RunningTitle />
}
{
isRestoring && <RestoringTitle />
@@ -78,7 +84,7 @@ const Header: FC = () => {
!isRestoring && (
<div className='flex items-center'>
{
runningStatus && (
nodesReadOnly && (
<Button
className={`
mr-2 px-3 py-0 h-8 bg-white text-[13px] font-medium text-primary-600
@@ -97,7 +103,7 @@ const Header: FC = () => {
className={`
mr-2 px-3 py-0 h-8 bg-white text-[13px] font-medium text-gray-700
border-[0.5px] border-gray-200 shadow-xs
${runningStatus && '!cursor-not-allowed opacity-50'}
${nodesReadOnly && '!cursor-not-allowed opacity-50'}
`}
onClick={handleShowFeatures}
>
@@ -115,7 +121,6 @@ const Header: FC = () => {
className={`
px-3 py-0 h-8 bg-white text-[13px] font-medium text-gray-700
border-[0.5px] border-gray-200 shadow-xs
${runningStatus && '!cursor-not-allowed opacity-50'}
`}
onClick={handleShowFeatures}
>

View File

@@ -9,6 +9,7 @@ import {
useWorkflowStore,
} from '../store'
import {
useNodesReadOnly,
useNodesSyncDraft,
useWorkflow,
useWorkflowRun,
@@ -31,7 +32,10 @@ const Publish = () => {
const { formatTimeFromNow } = useWorkflow()
const { handleCheckBeforePublish } = useWorkflowRun()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const runningStatus = useStore(s => s.runningStatus)
const {
nodesReadOnly,
getNodesReadOnly,
} = useNodesReadOnly()
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
const publishedAt = useStore(s => s.publishedAt)
const [open, setOpen] = useState(false)
@@ -61,7 +65,7 @@ const Publish = () => {
}, [workflowStore])
const handleTrigger = useCallback(() => {
if (runningStatus)
if (getNodesReadOnly())
return
if (open)
@@ -72,7 +76,7 @@ const Publish = () => {
setOpen(true)
setPublished(false)
}
}, [runningStatus, open, handleSyncWorkflowDraft])
}, [getNodesReadOnly, open, handleSyncWorkflowDraft])
return (
<PortalToFollowElem
@@ -89,7 +93,7 @@ const Publish = () => {
type='primary'
className={`
px-3 py-0 h-8 text-[13px] font-medium
${runningStatus && 'cursor-not-allowed opacity-50'}
${nodesReadOnly && 'cursor-not-allowed opacity-50'}
`}
>
{t('workflow.common.publish')}

View File

@@ -7,6 +7,7 @@ import {
} from '../store'
import {
useIsChatMode,
useNodesReadOnly,
useNodesSyncDraft,
useWorkflowRun,
} from '../hooks'
@@ -25,9 +26,9 @@ const RunMode = memo(() => {
const workflowStore = useWorkflowStore()
const { handleStopRun } = useWorkflowRun()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const runningStatus = useStore(s => s.runningStatus)
const workflowRunningData = useStore(s => s.workflowRunningData)
const showInputsPanel = useStore(s => s.showInputsPanel)
const isRunning = runningStatus === WorkflowRunningStatus.Running
const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running
const handleClick = () => {
workflowStore.setState({ showInputsPanel: true })
@@ -65,7 +66,7 @@ const RunMode = memo(() => {
isRunning && (
<div
className='flex items-center justify-center ml-0.5 w-7 h-7 cursor-pointer hover:bg-black/5 rounded-md'
onClick={handleStopRun}
onClick={() => handleStopRun(workflowRunningData?.task_id || '')}
>
<StopCircle className='w-4 h-4 text-gray-500' />
</div>
@@ -80,7 +81,7 @@ const PreviewMode = memo(() => {
const { t } = useTranslation()
const { handleRunSetting } = useWorkflowRun()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const runningStatus = useStore(s => s.runningStatus)
const { nodesReadOnly } = useNodesReadOnly()
const handleClick = () => {
handleSyncWorkflowDraft(true)
@@ -92,12 +93,12 @@ const PreviewMode = memo(() => {
className={`
flex items-center px-1.5 h-7 rounded-md text-[13px] font-medium text-primary-600
hover:bg-primary-50 cursor-pointer
${runningStatus && 'bg-primary-50 opacity-50 !cursor-not-allowed'}
${nodesReadOnly && 'bg-primary-50 opacity-50 !cursor-not-allowed'}
`}
onClick={() => !runningStatus && handleClick()}
onClick={() => !nodesReadOnly && handleClick()}
>
{
runningStatus
nodesReadOnly
? (
<>
{t('workflow.common.inPreview')}
@@ -140,7 +141,7 @@ const RunAndHistory: FC = () => {
${showRunHistory && 'bg-primary-50'}
`}
onClick={() => {
workflowStore.setState({ showRunHistory: !showRunHistory, workflowRunId: '' })
workflowStore.setState({ showRunHistory: !showRunHistory })
setCurrentLogItem()
setShowMessageLogModal(false)
}}

View File

@@ -8,7 +8,6 @@ import {
getConnectedEdges,
useStoreApi,
} from 'reactflow'
import { useWorkflowStore } from '../store'
import type {
Edge,
Node,
@@ -16,16 +15,15 @@ import type {
import { BlockEnum } from '../types'
import { getNodesConnectedSourceOrTargetHandleIdsMap } from '../utils'
import { useNodesSyncDraft } from './use-nodes-sync-draft'
import { useNodesReadOnly } from './use-workflow'
export const useEdgesInteractions = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { getNodesReadOnly } = useNodesReadOnly()
const handleEdgeEnter = useCallback<EdgeMouseHandler>((_, edge) => {
const { runningStatus } = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
const {
@@ -38,12 +36,10 @@ export const useEdgesInteractions = () => {
currentEdge.data._hovering = true
})
setEdges(newEdges)
}, [store, workflowStore])
}, [store, getNodesReadOnly])
const handleEdgeLeave = useCallback<EdgeMouseHandler>((_, edge) => {
const { runningStatus } = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
const {
@@ -56,12 +52,10 @@ export const useEdgesInteractions = () => {
currentEdge.data._hovering = false
})
setEdges(newEdges)
}, [store, workflowStore])
}, [store, getNodesReadOnly])
const handleEdgeDeleteByDeleteBranch = useCallback((nodeId: string, branchId: string) => {
const { runningStatus } = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
const {
@@ -92,12 +86,10 @@ export const useEdgesInteractions = () => {
})
setEdges(newEdges)
handleSyncWorkflowDraft()
}, [store, handleSyncWorkflowDraft, workflowStore])
}, [store, handleSyncWorkflowDraft, getNodesReadOnly])
const handleEdgeDelete = useCallback(() => {
const { runningStatus } = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
const {
@@ -127,12 +119,10 @@ export const useEdgesInteractions = () => {
})
setEdges(newEdges)
handleSyncWorkflowDraft()
}, [store, workflowStore, handleSyncWorkflowDraft])
}, [store, getNodesReadOnly, handleSyncWorkflowDraft])
const handleEdgesChange = useCallback<OnEdgesChange>((changes) => {
const { runningStatus } = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
const {
@@ -147,7 +137,7 @@ export const useEdgesInteractions = () => {
})
})
setEdges(newEdges)
}, [store, workflowStore])
}, [store, getNodesReadOnly])
const handleVariableAssignerEdgesChange = useCallback((nodeId: string, variables: any) => {
const {

View File

@@ -1,8 +1,8 @@
import { useCallback } from 'react'
import produce from 'immer'
import { useStoreApi } from 'reactflow'
import { useWorkflowStore } from '../store'
import { useNodesSyncDraft } from './use-nodes-sync-draft'
import { useNodesReadOnly } from './use-workflow'
type NodeDataUpdatePayload = {
id: string
@@ -11,8 +11,8 @@ type NodeDataUpdatePayload = {
export const useNodeDataUpdate = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { getNodesReadOnly } = useNodesReadOnly()
const handleNodeDataUpdate = useCallback(({ id, data }: NodeDataUpdatePayload) => {
const {
@@ -28,14 +28,12 @@ export const useNodeDataUpdate = () => {
}, [store])
const handleNodeDataUpdateWithSyncDraft = useCallback((payload: NodeDataUpdatePayload) => {
const { runningStatus } = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
handleNodeDataUpdate(payload)
handleSyncWorkflowDraft()
}, [handleSyncWorkflowDraft, handleNodeDataUpdate, workflowStore])
}, [handleSyncWorkflowDraft, handleNodeDataUpdate, getNodesReadOnly])
return {
handleNodeDataUpdate,

View File

@@ -32,7 +32,10 @@ import {
} from '../utils'
import { useNodesExtraData } from './use-nodes-data'
import { useNodesSyncDraft } from './use-nodes-sync-draft'
import { useWorkflow } from './use-workflow'
import {
useNodesReadOnly,
useWorkflow,
} from './use-workflow'
export const useNodesInteractions = () => {
const { t } = useTranslation()
@@ -41,25 +44,21 @@ export const useNodesInteractions = () => {
const nodesExtraData = useNodesExtraData()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { getAfterNodesInSameBranch } = useWorkflow()
const { getNodesReadOnly } = useNodesReadOnly()
const dragNodeStartPosition = useRef({ x: 0, y: 0 } as { x: number; y: number })
const connectingNodeRef = useRef<{ nodeId: string; handleType: HandleType } | null>(null)
const handleNodeDragStart = useCallback<NodeDragHandler>((_, node) => {
workflowStore.setState({ nodeAnimation: false })
const {
runningStatus,
} = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
dragNodeStartPosition.current = { x: node.position.x, y: node.position.y }
}, [workflowStore])
}, [workflowStore, getNodesReadOnly])
const handleNodeDrag = useCallback<NodeDragHandler>((e, node: Node) => {
const { runningStatus } = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
const {
@@ -159,16 +158,15 @@ export const useNodesInteractions = () => {
})
setNodes(newNodes)
}, [store, workflowStore])
}, [store, workflowStore, getNodesReadOnly])
const handleNodeDragStop = useCallback<NodeDragHandler>((_, node) => {
const {
runningStatus,
setHelpLineHorizontal,
setHelpLineVertical,
} = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
const { x, y } = dragNodeStartPosition.current
@@ -177,12 +175,10 @@ export const useNodesInteractions = () => {
setHelpLineVertical()
handleSyncWorkflowDraft()
}
}, [handleSyncWorkflowDraft, workflowStore])
}, [handleSyncWorkflowDraft, workflowStore, getNodesReadOnly])
const handleNodeEnter = useCallback<NodeMouseHandler>((_, node) => {
const { runningStatus } = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
const {
@@ -217,12 +213,10 @@ export const useNodesInteractions = () => {
})
})
setEdges(newEdges)
}, [store, nodesExtraData, workflowStore])
}, [store, nodesExtraData, getNodesReadOnly])
const handleNodeLeave = useCallback<NodeMouseHandler>(() => {
const { runningStatus } = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
const {
@@ -243,12 +237,10 @@ export const useNodesInteractions = () => {
})
})
setEdges(newEdges)
}, [store, workflowStore])
}, [store, getNodesReadOnly])
const handleNodeSelect = useCallback((nodeId: string, cancelSelection?: boolean) => {
const { runningStatus } = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
const {
@@ -272,18 +264,14 @@ export const useNodesInteractions = () => {
})
setNodes(newNodes)
handleSyncWorkflowDraft()
}, [store, handleSyncWorkflowDraft, workflowStore])
}, [store, handleSyncWorkflowDraft, getNodesReadOnly])
const handleNodeClick = useCallback<NodeMouseHandler>((_, node) => {
const {
runningStatus,
} = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
handleNodeSelect(node.id)
}, [handleNodeSelect, workflowStore])
}, [handleNodeSelect, getNodesReadOnly])
const handleNodeConnect = useCallback<OnConnect>(({
source,
@@ -291,9 +279,7 @@ export const useNodesInteractions = () => {
target,
targetHandle,
}) => {
const { runningStatus } = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
const {
@@ -344,7 +330,7 @@ export const useNodesInteractions = () => {
})
setEdges(newEdges)
handleSyncWorkflowDraft()
}, [store, handleSyncWorkflowDraft, workflowStore])
}, [store, handleSyncWorkflowDraft, getNodesReadOnly])
const handleNodeConnectStart = useCallback<OnConnectStart>((_, { nodeId, handleType }) => {
if (nodeId && handleType) {
@@ -360,9 +346,7 @@ export const useNodesInteractions = () => {
}, [])
const handleNodeDelete = useCallback((nodeId: string) => {
const { runningStatus } = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
const {
@@ -393,7 +377,7 @@ export const useNodesInteractions = () => {
})
setEdges(newEdges)
handleSyncWorkflowDraft()
}, [store, handleSyncWorkflowDraft, workflowStore])
}, [store, handleSyncWorkflowDraft, getNodesReadOnly])
const handleNodeAdd = useCallback<OnNodeAdd>((
{
@@ -409,9 +393,7 @@ export const useNodesInteractions = () => {
nextNodeTargetHandle,
},
) => {
const { runningStatus } = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
if (nodeType === BlockEnum.VariableAssigner)
@@ -593,7 +575,7 @@ export const useNodesInteractions = () => {
setEdges(newEdges)
}
handleSyncWorkflowDraft()
}, [store, handleSyncWorkflowDraft, getAfterNodesInSameBranch, workflowStore, t])
}, [store, handleSyncWorkflowDraft, getAfterNodesInSameBranch, getNodesReadOnly, t])
const handleNodeChange = useCallback((
currentNodeId: string,
@@ -601,9 +583,7 @@ export const useNodesInteractions = () => {
sourceHandle: string,
toolDefaultValue?: ToolDefaultValue,
) => {
const { runningStatus } = workflowStore.getState()
if (runningStatus)
if (getNodesReadOnly())
return
const {
@@ -659,7 +639,7 @@ export const useNodesInteractions = () => {
})
setEdges(newEdges)
handleSyncWorkflowDraft()
}, [store, handleSyncWorkflowDraft, workflowStore, t])
}, [store, handleSyncWorkflowDraft, getNodesReadOnly, t])
return {
handleNodeDragStart,

View File

@@ -9,6 +9,7 @@ import {
useWorkflowStore,
} from '../store'
import { BlockEnum } from '../types'
import { useNodesReadOnly } from './use-workflow'
import { syncWorkflowDraft } from '@/service/workflow'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import { useStore as useAppStore } from '@/app/components/app/store'
@@ -18,6 +19,7 @@ export const useNodesSyncDraft = () => {
const workflowStore = useWorkflowStore()
const reactFlow = useReactFlow()
const featuresStore = useFeaturesStore()
const { getNodesReadOnly } = useNodesReadOnly()
const debouncedSyncWorkflowDraft = useStore(s => s.debouncedSyncWorkflowDraft)
const doSyncWorkflowDraft = useCallback(() => {
@@ -78,19 +80,14 @@ export const useNodesSyncDraft = () => {
}, [store, reactFlow, featuresStore, workflowStore])
const handleSyncWorkflowDraft = useCallback((sync?: boolean) => {
const {
runningStatus,
isRestoring,
} = workflowStore.getState()
if (runningStatus || isRestoring)
if (getNodesReadOnly())
return
if (sync)
doSyncWorkflowDraft()
else
debouncedSyncWorkflowDraft(doSyncWorkflowDraft)
}, [debouncedSyncWorkflowDraft, doSyncWorkflowDraft, workflowStore])
}, [debouncedSyncWorkflowDraft, doSyncWorkflowDraft, getNodesReadOnly])
return {
handleSyncWorkflowDraft,

View File

@@ -71,10 +71,23 @@ export const useWorkflowRun = () => {
}, [store, reactflow, workflowStore])
const handleRunSetting = useCallback((shouldClear?: boolean) => {
workflowStore.setState({ runningStatus: shouldClear ? undefined : WorkflowRunningStatus.Waiting })
workflowStore.setState({ taskId: '' })
workflowStore.setState({ currentSequenceNumber: 0 })
workflowStore.setState({ workflowRunId: '' })
if (shouldClear) {
workflowStore.setState({
workflowRunningData: undefined,
historyWorkflowData: undefined,
})
}
else {
workflowStore.setState({
workflowRunningData: {
result: {
status: shouldClear ? '' : WorkflowRunningStatus.Waiting,
},
tracing: [],
},
})
}
const {
setNodes,
getNodes,
@@ -141,11 +154,19 @@ export const useWorkflowRun = () => {
},
{
onWorkflowStarted: (params) => {
const { task_id, workflow_run_id, data } = params
workflowStore.setState({ runningStatus: WorkflowRunningStatus.Running })
workflowStore.setState({ taskId: task_id })
workflowStore.setState({ currentSequenceNumber: data.sequence_number })
workflowStore.setState({ workflowRunId: workflow_run_id })
const { task_id, data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.task_id = task_id
draft.result = {
...draft?.result,
...data,
}
}))
const newNodes = produce(getNodes(), (draft) => {
draft.forEach((node) => {
node.data._runningStatus = NodeRunningStatus.Waiting
@@ -158,13 +179,31 @@ export const useWorkflowRun = () => {
},
onWorkflowFinished: (params) => {
const { data } = params
workflowStore.setState({ runningStatus: data.status as WorkflowRunningStatus })
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.result = {
...draft.result,
...data,
}
}))
if (onWorkflowFinished)
onWorkflowFinished(params)
},
onNodeStarted: (params) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.tracing!.push(data as any)
}))
const nodes = getNodes()
const {
setViewport,
@@ -196,6 +235,17 @@ export const useWorkflowRun = () => {
},
onNodeFinished: (params) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const currentIndex = draft.tracing!.findIndex(trace => trace.node_id === data.node_id)
if (currentIndex > -1 && draft.tracing)
draft.tracing[currentIndex] = data as any
}))
const newNodes = produce(getNodes(), (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)!
@@ -211,12 +261,11 @@ export const useWorkflowRun = () => {
)
}, [store, reactflow, workflowStore])
const handleStopRun = useCallback(() => {
const handleStopRun = useCallback((taskId: string) => {
const appId = useAppStore.getState().appDetail?.id
const taskId = workflowStore.getState().taskId
stopWorkflowRun(`/apps/${appId}/workflow-runs/tasks/${taskId}/stop`)
}, [workflowStore])
}, [])
const handleRestoreFromPublishedWorkflow = useCallback(async () => {
const appDetail = useAppStore.getState().appDetail

View File

@@ -20,8 +20,14 @@ import {
getLayoutByDagre,
} from '../utils'
import type { Node } from '../types'
import { BlockEnum } from '../types'
import { useWorkflowStore } from '../store'
import {
BlockEnum,
WorkflowRunningStatus,
} from '../types'
import {
useStore,
useWorkflowStore,
} from '../store'
import {
AUTO_LAYOUT_OFFSET,
START_INITIAL_POSITION,
@@ -346,3 +352,38 @@ export const useWorkflowInit = () => {
return data
}
export const useWorkflowReadOnly = () => {
const workflowStore = useWorkflowStore()
const workflowRunningData = useStore(s => s.workflowRunningData)
const getWorkflowReadOnly = useCallback(() => {
return workflowStore.getState().workflowRunningData?.result.status === WorkflowRunningStatus.Running
}, [workflowStore])
return {
workflowReadOnly: workflowRunningData?.result.status === WorkflowRunningStatus.Running,
getWorkflowReadOnly,
}
}
export const useNodesReadOnly = () => {
const workflowStore = useWorkflowStore()
const workflowRunningData = useStore(s => s.workflowRunningData)
const historyWorkflowData = useStore(s => s.historyWorkflowData)
const isRestoring = useStore(s => s.isRestoring)
const getNodesReadOnly = useCallback(() => {
const {
workflowRunningData,
historyWorkflowData,
isRestoring,
} = workflowStore.getState()
return workflowRunningData || historyWorkflowData || isRestoring
}, [workflowStore])
return {
nodesReadOnly: workflowRunningData || historyWorkflowData || isRestoring,
getNodesReadOnly,
}
}

View File

@@ -18,14 +18,15 @@ import type {
Edge,
Node,
} from './types'
import { WorkflowRunningStatus } from './types'
import { WorkflowContextProvider } from './context'
import {
useEdgesInteractions,
useNodesInteractions,
useNodesReadOnly,
useNodesSyncDraft,
useWorkflow,
useWorkflowInit,
useWorkflowReadOnly,
} from './hooks'
import Header from './header'
import CustomNode from './nodes'
@@ -62,9 +63,10 @@ const Workflow: FC<WorkflowProps> = memo(({
viewport,
}) => {
const showFeaturesPanel = useStore(state => state.showFeaturesPanel)
const runningStatus = useStore(s => s.runningStatus)
const nodeAnimation = useStore(s => s.nodeAnimation)
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { workflowReadOnly } = useWorkflowReadOnly()
const { nodesReadOnly } = useNodesReadOnly()
useEffect(() => {
setAutoFreeze(false)
@@ -106,7 +108,7 @@ const Workflow: FC<WorkflowProps> = memo(({
id='workflow-container'
className={`
relative w-full min-w-[960px] h-full bg-[#F0F2F7]
${runningStatus === WorkflowRunningStatus.Running && 'workflow-panel-animation'}
${workflowReadOnly && 'workflow-panel-animation'}
${nodeAnimation && 'workflow-node-animation'}
`}
>
@@ -138,14 +140,14 @@ const Workflow: FC<WorkflowProps> = memo(({
defaultViewport={viewport}
multiSelectionKeyCode={null}
deleteKeyCode={null}
nodesDraggable={!runningStatus}
nodesConnectable={!runningStatus}
nodesFocusable={!runningStatus}
edgesFocusable={!runningStatus}
panOnDrag={runningStatus !== WorkflowRunningStatus.Running}
zoomOnPinch={runningStatus !== WorkflowRunningStatus.Running}
zoomOnScroll={runningStatus !== WorkflowRunningStatus.Running}
zoomOnDoubleClick={runningStatus !== WorkflowRunningStatus.Running}
nodesDraggable={!nodesReadOnly}
nodesConnectable={!nodesReadOnly}
nodesFocusable={!nodesReadOnly}
edgesFocusable={!nodesReadOnly}
panOnDrag={!workflowReadOnly}
zoomOnPinch={!workflowReadOnly}
zoomOnScroll={!workflowReadOnly}
zoomOnDoubleClick={!workflowReadOnly}
isValidConnection={isValidConnection}
>
<Background

View File

@@ -1,19 +1,24 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { MiniMap } from 'reactflow'
import { useWorkflow } from '../hooks'
import { useStore } from '../store'
import {
useNodesReadOnly,
useWorkflow,
} from '../hooks'
import ZoomInOut from './zoom-in-out'
import { OrganizeGrid } from '@/app/components/base/icons/src/vender/line/layout'
import TooltipPlus from '@/app/components/base/tooltip-plus'
const Operator = () => {
const { t } = useTranslation()
const runningStatus = useStore(s => s.runningStatus)
const { handleLayout } = useWorkflow()
const {
nodesReadOnly,
getNodesReadOnly,
} = useNodesReadOnly()
const goLayout = () => {
if (runningStatus)
if (getNodesReadOnly())
return
handleLayout()
}
@@ -35,7 +40,7 @@ const Operator = () => {
<div
className={`
ml-[1px] flex items-center justify-center w-8 h-8 cursor-pointer hover:bg-black/5 rounded-lg
${runningStatus && '!cursor-not-allowed opacity-50'}
${nodesReadOnly && '!cursor-not-allowed opacity-50'}
`}
onClick={goLayout}
>

View File

@@ -10,9 +10,10 @@ import {
useReactFlow,
useViewport,
} from 'reactflow'
import { useNodesSyncDraft } from '../hooks'
import { useStore } from '../store'
import { WorkflowRunningStatus } from '../types'
import {
useNodesSyncDraft,
useWorkflowReadOnly,
} from '../hooks'
import {
PortalToFollowElem,
PortalToFollowElemContent,
@@ -32,7 +33,10 @@ const ZoomInOut: FC = () => {
const { zoom } = useViewport()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const [open, setOpen] = useState(false)
const runningStatus = useStore(s => s.runningStatus)
const {
workflowReadOnly,
getWorkflowReadOnly,
} = useWorkflowReadOnly()
const ZOOM_IN_OUT_OPTIONS = [
[
@@ -64,8 +68,9 @@ const ZoomInOut: FC = () => {
]
const handleZoom = (type: string) => {
if (runningStatus === WorkflowRunningStatus.Running)
if (workflowReadOnly)
return
if (type === 'in')
zoomIn()
@@ -85,10 +90,11 @@ const ZoomInOut: FC = () => {
}
const handleTrigger = useCallback(() => {
if (runningStatus === WorkflowRunningStatus.Running)
if (getWorkflowReadOnly())
return
setOpen(v => !v)
}, [runningStatus])
}, [getWorkflowReadOnly])
return (
<PortalToFollowElem
@@ -104,7 +110,7 @@ const ZoomInOut: FC = () => {
<div className={`
flex items-center px-2 h-8 cursor-pointer text-[13px] hover:bg-gray-50 rounded-lg
${open && 'bg-gray-50'}
${runningStatus === WorkflowRunningStatus.Running && '!cursor-not-allowed opacity-50'}
${workflowReadOnly && '!cursor-not-allowed opacity-50'}
`}>
<SearchLg className='mr-1 w-4 h-4' />
<div className='w-[34px]'>{parseFloat(`${zoom * 100}`).toFixed(0)}%</div>

View File

@@ -17,7 +17,8 @@ const ChatRecord = () => {
const [fetched, setFetched] = useState(false)
const [chatList, setChatList] = useState([])
const appDetail = useAppStore(s => s.appDetail)
const currentConversationID = useStore(s => s.currentConversationID)
const historyWorkflowData = useStore(s => s.historyWorkflowData)
const currentConversationID = historyWorkflowData?.conversation_id
const chatMessageList = useMemo(() => {
const res: ChatItem[] = []

View File

@@ -33,7 +33,6 @@ const ChatWrapper = forwardRef<ChatWrapperRefType>((_, ref) => {
const workflowStore = useWorkflowStore()
const featuresStore = useFeaturesStore()
const inputs = useStore(s => s.inputs)
const workflowRunId = useStore(s => s.workflowRunId)
const { handleStopRun } = useWorkflowRun()
const features = featuresStore!.getState().features
@@ -84,8 +83,8 @@ const ChatWrapper = forwardRef<ChatWrapperRefType>((_, ref) => {
const doStop = useCallback(() => {
handleStop()
handleStopRun()
}, [handleStop, handleStopRun])
handleStopRun(workflowStore.getState().workflowRunningData?.task_id || '')
}, [handleStop, handleStopRun, workflowStore])
useImperativeHandle(ref, () => {
return {
@@ -96,7 +95,7 @@ const ChatWrapper = forwardRef<ChatWrapperRefType>((_, ref) => {
return (
<Chat
config={config as any}
chatList={chatList.map(item => ({ ...item, workflow_run_id: workflowRunId }))}
chatList={chatList}
isResponding={isResponding}
chatContainerclassName='px-4'
chatContainerInnerClassName='pt-6'

View File

@@ -13,30 +13,37 @@ import DebugAndPreview from './debug-and-preview'
import RunHistory from './run-history'
import Record from './record'
import InputsPanel from './inputs-panel'
import WorkflowPreview from './workflow-preview'
import { useStore as useAppStore } from '@/app/components/app/store'
import MessageLogModal from '@/app/components/base/message-log-modal'
const Panel: FC = () => {
const nodes = useNodes<CommonNodeType>()
const isChatMode = useIsChatMode()
const runningStatus = useStore(s => s.runningStatus)
const workflowRunId = useStore(s => s.workflowRunId)
const selectedNode = nodes.find(node => node.data.selected)
const showRunHistory = useStore(state => state.showRunHistory)
const showInputsPanel = useStore(s => s.showInputsPanel)
const currentConversationID = useStore(s => s.currentConversationID)
const workflowRunningData = useStore(s => s.workflowRunningData)
const historyWorkflowData = useStore(s => s.historyWorkflowData)
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal } = useAppStore()
const {
showWorkflowInfoPanel,
showNodePanel,
showDebugAndPreviewPanel,
showWorkflowPreview,
} = useMemo(() => {
return {
showWorkflowInfoPanel: !selectedNode && !runningStatus,
showNodePanel: !!selectedNode && !runningStatus,
showDebugAndPreviewPanel: isChatMode && runningStatus && !currentConversationID,
showWorkflowInfoPanel: !selectedNode && !workflowRunningData && !historyWorkflowData,
showNodePanel: !!selectedNode && !workflowRunningData && !historyWorkflowData,
showDebugAndPreviewPanel: isChatMode && workflowRunningData && !historyWorkflowData,
showWorkflowPreview: !isChatMode && workflowRunningData && !historyWorkflowData,
}
}, [selectedNode, isChatMode, runningStatus, currentConversationID])
}, [
selectedNode,
isChatMode,
workflowRunningData,
historyWorkflowData,
])
return (
<div
@@ -45,11 +52,6 @@ const Panel: FC = () => {
${(showRunHistory || showDebugAndPreviewPanel) && '!pr-0'}
`}
>
{
showInputsPanel && (
<InputsPanel />
)
}
{
showMessageLogModal && (
<MessageLogModal
@@ -64,13 +66,23 @@ const Panel: FC = () => {
)
}
{
runningStatus && !isChatMode && workflowRunId && (
historyWorkflowData && (
<Record />
)
}
{
runningStatus && isChatMode && showRunHistory && currentConversationID && (
<Record />
showDebugAndPreviewPanel && (
<DebugAndPreview />
)
}
{
showInputsPanel && (
<InputsPanel />
)
}
{
showWorkflowPreview && (
<WorkflowPreview />
)
}
{
@@ -83,11 +95,6 @@ const Panel: FC = () => {
<WorkflowInfo />
)
}
{
showDebugAndPreviewPanel && (
<DebugAndPreview />
)
}
{
showRunHistory && (
<RunHistory />

View File

@@ -6,8 +6,7 @@ import ChatRecord from './chat-record'
const Record = () => {
const isChatMode = useIsChatMode()
const currentSequenceNumber = useStore(s => s.currentSequenceNumber)
const workflowRunId = useStore(s => s.workflowRunId)
const historyWorkflowData = useStore(s => s.historyWorkflowData)
return (
<div className={`
@@ -15,12 +14,12 @@ const Record = () => {
${isChatMode ? 'w-[320px]' : 'w-[400px]'}
`}>
<div className='flex items-center justify-between p-4 pb-1 text-base font-semibold text-gray-900'>
{`Test ${isChatMode ? 'Chat' : 'Run'}#${currentSequenceNumber}`}
{`Test ${isChatMode ? 'Chat' : 'Run'}#${historyWorkflowData?.sequence_number}`}
</div>
{
isChatMode
? <ChatRecord />
: <Run runID={workflowRunId} />
: <Run runID={historyWorkflowData?.id || ''} />
}
</div>
)

View File

@@ -11,7 +11,7 @@ import {
import { CheckCircle, XClose } from '@/app/components/base/icons/src/vender/line/general'
import { AlertCircle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
import {
useStore as useRunHistoryStore,
useStore,
useWorkflowStore,
} from '@/app/components/workflow/store'
import { useStore as useAppStore } from '@/app/components/app/store'
@@ -25,7 +25,7 @@ const RunHistory = () => {
const { formatTimeFromNow } = useWorkflow()
const { handleBackupDraft } = useWorkflowRun()
const workflowStore = useWorkflowStore()
const workflowRunId = useRunHistoryStore(state => state.workflowRunId)
const historyWorkflowData = useStore(s => s.historyWorkflowData)
const { data: runList, isLoading: runListLoading } = useSWR((appDetail && !isChatMode) ? `/apps/${appDetail.id}/workflow-runs` : null, fetchWorkflowRunHistory)
const { data: chatList, isLoading: chatListLoading } = useSWR((appDetail && isChatMode) ? `/apps/${appDetail.id}/advanced-chat/workflow-runs` : null, fetcChatRunHistory)
@@ -45,8 +45,7 @@ const RunHistory = () => {
onClick={() => {
workflowStore.setState({
showRunHistory: false,
workflowRunId: '',
currentConversationID: '',
historyWorkflowData: undefined,
})
setCurrentLogItem()
setShowMessageLogModal(false)
@@ -69,14 +68,11 @@ const RunHistory = () => {
key={item.id}
className={cn(
'flex mb-0.5 px-2 py-[7px] rounded-lg hover:bg-primary-50 cursor-pointer',
item.id === workflowRunId && 'bg-primary-50',
item.id === historyWorkflowData?.id && 'bg-primary-50',
)}
onClick={() => {
workflowStore.setState({
currentSequenceNumber: item.sequence_number,
workflowRunId: item.id,
currentConversationID: item.conversation_id,
runningStatus: item.status as WorkflowRunningStatus,
historyWorkflowData: item,
})
handleBackupDraft()
}}
@@ -95,7 +91,7 @@ const RunHistory = () => {
<div
className={cn(
'flex items-center text-[13px] font-medium leading-[18px]',
item.id === workflowRunId && 'text-primary-600',
item.id === historyWorkflowData?.id && 'text-primary-600',
)}
>
{`Test ${isChatMode ? 'Chat' : 'Run'}#${item.sequence_number}`}

View File

@@ -0,0 +1,67 @@
import {
memo,
useState,
} from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import ResultPanel from '../run/result-panel'
import TracingPanel from '../run/tracing-panel'
import { useStore } from '../store'
const WorkflowPreview = () => {
const { t } = useTranslation()
const [currentTab, setCurrentTab] = useState<string>('TRACING')
const workflowRunningData = useStore(s => s.workflowRunningData)
const switchTab = async (tab: string) => {
setCurrentTab(tab)
}
return (
<div className={`
flex flex-col w-[420px] h-full rounded-2xl border-[0.5px] border-gray-200 shadow-xl bg-white
`}>
<div className='flex items-center justify-between p-4 pb-1 text-base font-semibold text-gray-900'>
Test Run#${workflowRunningData?.result.sequence_number}
</div>
<div>
<div className='shrink-0 flex items-center px-4 border-b-[0.5px] border-[rgba(0,0,0,0.05)]'>
<div
className={cn(
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
currentTab === 'RESULT' && '!border-[rgb(21,94,239)] text-gray-700',
)}
onClick={() => switchTab('RESULT')}
>{t('runLog.result')}</div>
<div
className={cn(
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
currentTab === 'TRACING' && '!border-[rgb(21,94,239)] text-gray-700',
)}
onClick={() => switchTab('TRACING')}
>{t('runLog.tracing')}</div>
</div>
{currentTab === 'RESULT' && (
<ResultPanel
inputs={workflowRunningData?.result?.inputs}
outputs={workflowRunningData?.result?.outputs}
status={workflowRunningData?.result?.status || ''}
error={workflowRunningData?.result?.error}
elapsed_time={workflowRunningData?.result?.elapsed_time}
total_tokens={workflowRunningData?.result?.total_tokens}
created_at={workflowRunningData?.result?.created_at}
created_by={''}
steps={workflowRunningData?.result?.total_steps}
/>
)}
{currentTab === 'TRACING' && (
<TracingPanel
list={workflowRunningData?.tracing || []}
/>
)}
</div>
</div>
)
}
export default memo(WorkflowPreview)

View File

@@ -13,7 +13,7 @@ import type { NodeTracing } from '@/types/workflow'
import type { WorkflowRunDetailResponse } from '@/models/log'
import { useStore as useAppStore } from '@/app/components/app/store'
type RunProps = {
export type RunProps = {
activeTab?: 'RESULT' | 'TRACING'
runID: string
}

View File

@@ -56,7 +56,7 @@ const NodePanel: FC<Props> = ({ nodeInfo, collapsed = true }) => {
<BlockIcon className='shrink-0 mr-2' type={nodeInfo.node_type} />
<div className='grow text-gray-700 text-[13px] leading-[16px] font-semibold truncate' title={nodeInfo.title}>{nodeInfo.title}</div>
{nodeInfo.status !== 'running' && (
<div className='shrink-0 text-gray-500 text-xs leading-[18px]'>{`${getTime(nodeInfo.elapsed_time)} · ${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens`}</div>
<div className='shrink-0 text-gray-500 text-xs leading-[18px]'>{`${getTime(nodeInfo.elapsed_time || 0)} · ${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens`}</div>
)}
{nodeInfo.status === 'succeeded' && (
<CheckCircle className='shrink-0 ml-2 w-3.5 h-3.5 text-[#12B76A]' />

View File

@@ -16,75 +16,63 @@ import type {
} from './block-selector/types'
import type {
Edge,
HistoryWorkflowData,
Node,
RunFile,
WorkflowRunningStatus,
WorkflowRunningData,
} from './types'
import { WorkflowContext } from './context'
type State = {
taskId: string
currentSequenceNumber: number
workflowRunId: string
currentConversationID: string
type Shape = {
workflowRunningData?: WorkflowRunningData
setWorkflowRunningData: (workflowData: WorkflowRunningData) => void
historyWorkflowData?: HistoryWorkflowData
setHistoryWorkflowData: (historyWorkflowData: HistoryWorkflowData) => void
showRunHistory: boolean
setShowRunHistory: (showRunHistory: boolean) => void
showFeaturesPanel: boolean
setShowFeaturesPanel: (showFeaturesPanel: boolean) => void
helpLineHorizontal?: HelpLineHorizontalPosition
setHelpLineHorizontal: (helpLineHorizontal?: HelpLineHorizontalPosition) => void
helpLineVertical?: HelpLineVerticalPosition
setHelpLineVertical: (helpLineVertical?: HelpLineVerticalPosition) => void
toolsets: CollectionWithExpanded[]
setToolsets: (toolsets: CollectionWithExpanded[]) => void
toolsMap: ToolsMap
setToolsMap: (toolsMap: Record<string, ToolInWorkflow[]>) => void
draftUpdatedAt: number
setDraftUpdatedAt: (draftUpdatedAt: number) => void
publishedAt: number
runningStatus?: WorkflowRunningStatus
setPublishedAt: (publishedAt: number) => void
showInputsPanel: boolean
setShowInputsPanel: (showInputsPanel: boolean) => void
inputs: Record<string, string>
setInputs: (inputs: Record<string, string>) => void
files: RunFile[]
setFiles: (files: RunFile[]) => void
backupDraft?: {
nodes: Node[]
edges: Edge[]
viewport: Viewport
}
setBackupDraft: (backupDraft?: Shape['backupDraft']) => void
notInitialWorkflow: boolean
nodesDefaultConfigs: Record<string, any>
nodeAnimation: boolean
isRestoring: boolean
}
type Action = {
setTaskId: (taskId: string) => void
setCurrentSequenceNumber: (currentSequenceNumber: number) => void
setWorkflowRunId: (workflowRunId: string) => void
setCurrentConversationID: (currentConversationID: string) => void
setShowRunHistory: (showRunHistory: boolean) => void
setShowFeaturesPanel: (showFeaturesPanel: boolean) => void
setHelpLineHorizontal: (helpLineHorizontal?: HelpLineHorizontalPosition) => void
setHelpLineVertical: (helpLineVertical?: HelpLineVerticalPosition) => void
setToolsets: (toolsets: CollectionWithExpanded[]) => void
setToolsMap: (toolsMap: Record<string, ToolInWorkflow[]>) => void
setDraftUpdatedAt: (draftUpdatedAt: number) => void
setPublishedAt: (publishedAt: number) => void
setRunningStatus: (runningStatus?: WorkflowRunningStatus) => void
setShowInputsPanel: (showInputsPanel: boolean) => void
setInputs: (inputs: Record<string, string>) => void
setFiles: (files: RunFile[]) => void
setBackupDraft: (backupDraft?: State['backupDraft']) => void
setNotInitialWorkflow: (notInitialWorkflow: boolean) => void
nodesDefaultConfigs: Record<string, any>
setNodesDefaultConfigs: (nodesDefaultConfigs: Record<string, any>) => void
nodeAnimation: boolean
setNodeAnimation: (nodeAnimation: boolean) => void
isRestoring: boolean
setIsRestoring: (isRestoring: boolean) => void
debouncedSyncWorkflowDraft: (fn: () => void) => void
}
export const createWorkflowStore = () => {
return create<State & Action>(set => ({
taskId: '',
setTaskId: taskId => set(() => ({ taskId })),
currentSequenceNumber: 0,
setCurrentSequenceNumber: currentSequenceNumber => set(() => ({ currentSequenceNumber })),
workflowRunId: '',
setWorkflowRunId: workflowRunId => set(() => ({ workflowRunId })),
currentConversationID: '',
setCurrentConversationID: currentConversationID => set(() => ({ currentConversationID })),
return create<Shape>(set => ({
workflowData: undefined,
setWorkflowRunningData: workflowRunningData => set(() => ({ workflowRunningData })),
historyWorkflowData: undefined,
setHistoryWorkflowData: historyWorkflowData => set(() => ({ historyWorkflowData })),
showRunHistory: false,
setShowRunHistory: showRunHistory => set(() => ({ showRunHistory })),
showFeaturesPanel: false,
@@ -101,8 +89,6 @@ export const createWorkflowStore = () => {
setDraftUpdatedAt: draftUpdatedAt => set(() => ({ draftUpdatedAt: draftUpdatedAt ? draftUpdatedAt * 1000 : 0 })),
publishedAt: 0,
setPublishedAt: publishedAt => set(() => ({ publishedAt: publishedAt ? publishedAt * 1000 : 0 })),
runningStatus: undefined,
setRunningStatus: runningStatus => set(() => ({ runningStatus })),
showInputsPanel: false,
setShowInputsPanel: showInputsPanel => set(() => ({ showInputsPanel })),
inputs: {},
@@ -125,7 +111,7 @@ export const createWorkflowStore = () => {
}))
}
export function useStore<T>(selector: (state: State & Action) => T): T {
export function useStore<T>(selector: (state: Shape) => T): T {
const store = useContext(WorkflowContext)
if (!store)
throw new Error('Missing WorkflowContext.Provider in the tree')

View File

@@ -5,6 +5,7 @@ import type {
import type { TransferMethod } from '@/types/app'
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
import type { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import type { NodeTracing } from '@/types/workflow'
export enum BlockEnum {
Start = 'start',
@@ -219,3 +220,34 @@ export type RunFile = {
url?: string
upload_file_id?: string
}
export type WorkflowRunningData = {
task_id?: string
message_id?: string
conversation_id?: string
result: {
sequence_number?: number
workflow_id?: string
inputs?: string
process_data?: string
outputs?: string
status: string
error?: string
elapsed_time?: number
total_tokens?: number
created_at?: number
created_by?: string
finished_at?: number
steps?: number
showSteps?: boolean
total_steps?: number
}
tracing?: NodeTracing[]
}
export type HistoryWorkflowData = {
id: string
sequence_number: number
status: string
conversation_id?: string
}