feat: last run frontend (#21369)

The frontend of feat: Persist Variables for Enhanced Debugging Workflow (#20699).

Co-authored-by: jZonG <jzongcode@gmail.com>
This commit is contained in:
Joel
2025-06-24 09:10:30 +08:00
committed by GitHub
parent 10b738a296
commit 1a1bfd4048
122 changed files with 5888 additions and 2061 deletions

View File

@@ -0,0 +1,241 @@
import { fetchNodeInspectVars } from '@/service/workflow'
import { useStore, useWorkflowStore } from '../store'
import type { ValueSelector } from '../types'
import type { VarInInspect } from '@/types/workflow'
import { VarInInspectType } from '@/types/workflow'
import {
useConversationVarValues,
useDeleteAllInspectorVars,
useDeleteInspectVar,
useDeleteNodeInspectorVars,
useEditInspectorVar,
useInvalidateConversationVarValues,
useInvalidateSysVarValues,
useLastRun,
useResetConversationVar,
useResetToLastRunValue,
useSysVarValues,
} from '@/service/use-workflow'
import { useCallback, useEffect, useState } from 'react'
import { isConversationVar, isENV, isSystemVar } from '../nodes/_base/components/variable/utils'
import produce from 'immer'
import type { Node } from '@/app/components/workflow/types'
import { useNodesInteractionsWithoutSync } from './use-nodes-interactions-without-sync'
import { useEdgesInteractionsWithoutSync } from './use-edges-interactions-without-sync'
const useInspectVarsCrud = () => {
const workflowStore = useWorkflowStore()
const nodesWithInspectVars = useStore(s => s.nodesWithInspectVars)
const {
appId,
setNodeInspectVars,
setInspectVarValue,
renameInspectVarName: renameInspectVarNameInStore,
deleteAllInspectVars: deleteAllInspectVarsInStore,
deleteNodeInspectVars: deleteNodeInspectVarsInStore,
deleteInspectVar: deleteInspectVarInStore,
setNodesWithInspectVars,
resetToLastRunVar: resetToLastRunVarInStore,
} = workflowStore.getState()
const { data: conversationVars } = useConversationVarValues(appId)
const invalidateConversationVarValues = useInvalidateConversationVarValues(appId)
const { mutateAsync: doResetConversationVar } = useResetConversationVar(appId)
const { mutateAsync: doResetToLastRunValue } = useResetToLastRunValue(appId)
const { data: systemVars } = useSysVarValues(appId)
const invalidateSysVarValues = useInvalidateSysVarValues(appId)
const { mutateAsync: doDeleteAllInspectorVars } = useDeleteAllInspectorVars(appId)
const { mutate: doDeleteNodeInspectorVars } = useDeleteNodeInspectorVars(appId)
const { mutate: doDeleteInspectVar } = useDeleteInspectVar(appId)
const { mutateAsync: doEditInspectorVar } = useEditInspectorVar(appId)
const { handleCancelNodeSuccessStatus } = useNodesInteractionsWithoutSync()
const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
const getNodeInspectVars = useCallback((nodeId: string) => {
const node = nodesWithInspectVars.find(node => node.nodeId === nodeId)
return node
}, [nodesWithInspectVars])
const getVarId = useCallback((nodeId: string, varName: string) => {
const node = getNodeInspectVars(nodeId)
if (!node)
return undefined
const varId = node.vars.find((varItem) => {
return varItem.selector[1] === varName
})?.id
return varId
}, [getNodeInspectVars])
const getInspectVar = useCallback((nodeId: string, name: string): VarInInspect | undefined => {
const node = getNodeInspectVars(nodeId)
if (!node)
return undefined
const variable = node.vars.find((varItem) => {
return varItem.name === name
})
return variable
}, [getNodeInspectVars])
const hasSetInspectVar = useCallback((nodeId: string, name: string, sysVars: VarInInspect[], conversationVars: VarInInspect[]) => {
const isEnv = isENV([nodeId])
if (isEnv) // always have value
return true
const isSys = isSystemVar([nodeId])
if (isSys)
return sysVars.some(varItem => varItem.selector?.[1]?.replace('sys.', '') === name)
const isChatVar = isConversationVar([nodeId])
if (isChatVar)
return conversationVars.some(varItem => varItem.selector?.[1] === name)
return getInspectVar(nodeId, name) !== undefined
}, [getInspectVar])
const hasNodeInspectVars = useCallback((nodeId: string) => {
return !!getNodeInspectVars(nodeId)
}, [getNodeInspectVars])
const fetchInspectVarValue = async (selector: ValueSelector) => {
const nodeId = selector[0]
const isSystemVar = nodeId === 'sys'
const isConversationVar = nodeId === 'conversation'
if (isSystemVar) {
invalidateSysVarValues()
return
}
if (isConversationVar) {
invalidateConversationVarValues()
return
}
const vars = await fetchNodeInspectVars(appId, nodeId)
setNodeInspectVars(nodeId, vars)
}
// after last run would call this
const appendNodeInspectVars = (nodeId: string, payload: VarInInspect[], allNodes: Node[]) => {
const nodes = produce(nodesWithInspectVars, (draft) => {
const nodeInfo = allNodes.find(node => node.id === nodeId)
if (nodeInfo) {
const index = draft.findIndex(node => node.nodeId === nodeId)
if (index === -1) {
draft.push({
nodeId,
nodeType: nodeInfo.data.type,
title: nodeInfo.data.title,
vars: payload,
})
}
else {
draft[index].vars = payload
}
}
})
setNodesWithInspectVars(nodes)
handleCancelNodeSuccessStatus(nodeId)
}
const hasNodeInspectVar = (nodeId: string, varId: string) => {
const targetNode = nodesWithInspectVars.find(item => item.nodeId === nodeId)
if(!targetNode || !targetNode.vars)
return false
return targetNode.vars.some(item => item.id === varId)
}
const deleteInspectVar = async (nodeId: string, varId: string) => {
if(hasNodeInspectVar(nodeId, varId)) {
await doDeleteInspectVar(varId)
deleteInspectVarInStore(nodeId, varId)
}
}
const resetConversationVar = async (varId: string) => {
await doResetConversationVar(varId)
invalidateConversationVarValues()
}
const deleteNodeInspectorVars = async (nodeId: string) => {
if (hasNodeInspectVars(nodeId)) {
await doDeleteNodeInspectorVars(nodeId)
deleteNodeInspectVarsInStore(nodeId)
}
}
const deleteAllInspectorVars = async () => {
await doDeleteAllInspectorVars()
await invalidateConversationVarValues()
await invalidateSysVarValues()
deleteAllInspectVarsInStore()
handleEdgeCancelRunningStatus()
}
const editInspectVarValue = useCallback(async (nodeId: string, varId: string, value: any) => {
await doEditInspectorVar({
varId,
value,
})
setInspectVarValue(nodeId, varId, value)
if (nodeId === VarInInspectType.conversation)
invalidateConversationVarValues()
if (nodeId === VarInInspectType.system)
invalidateSysVarValues()
}, [doEditInspectorVar, invalidateConversationVarValues, invalidateSysVarValues, setInspectVarValue])
const [currNodeId, setCurrNodeId] = useState<string | null>(null)
const [currEditVarId, setCurrEditVarId] = useState<string | null>(null)
const { data } = useLastRun(appId, currNodeId || '', !!currNodeId)
useEffect(() => {
if (data && currNodeId && currEditVarId) {
const inspectVar = getNodeInspectVars(currNodeId)?.vars?.find(item => item.id === currEditVarId)
resetToLastRunVarInStore(currNodeId, currEditVarId, data.outputs?.[inspectVar?.selector?.[1] || ''])
}
}, [data, currNodeId, currEditVarId, getNodeInspectVars, editInspectVarValue, resetToLastRunVarInStore])
const renameInspectVarName = async (nodeId: string, oldName: string, newName: string) => {
const varId = getVarId(nodeId, oldName)
if (!varId)
return
const newSelector = [nodeId, newName]
await doEditInspectorVar({
varId,
name: newName,
})
renameInspectVarNameInStore(nodeId, varId, newSelector)
}
const isInspectVarEdited = useCallback((nodeId: string, name: string) => {
const inspectVar = getInspectVar(nodeId, name)
if (!inspectVar)
return false
return inspectVar.edited
}, [getInspectVar])
const resetToLastRunVar = async (nodeId: string, varId: string) => {
await doResetToLastRunValue(varId)
setCurrNodeId(nodeId)
setCurrEditVarId(varId)
}
return {
conversationVars: conversationVars || [],
systemVars: systemVars || [],
nodesWithInspectVars,
hasNodeInspectVars,
hasSetInspectVar,
fetchInspectVarValue,
editInspectVarValue,
renameInspectVarName,
appendNodeInspectVars,
deleteInspectVar,
deleteNodeInspectorVars,
deleteAllInspectorVars,
isInspectVarEdited,
resetToLastRunVar,
invalidateSysVarValues,
resetConversationVar,
invalidateConversationVarValues,
}
}
export default useInspectVarsCrud

View File

@@ -1,6 +1,7 @@
import { useCallback } from 'react'
import produce from 'immer'
import { useStoreApi } from 'reactflow'
import { NodeRunningStatus } from '../types'
export const useNodesInteractionsWithoutSync = () => {
const store = useStoreApi()
@@ -21,7 +22,41 @@ export const useNodesInteractionsWithoutSync = () => {
setNodes(newNodes)
}, [store])
const handleCancelAllNodeSuccessStatus = useCallback(() => {
const {
getNodes,
setNodes,
} = store.getState()
const nodes = getNodes()
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
if(node.data._runningStatus === NodeRunningStatus.Succeeded)
node.data._runningStatus = undefined
})
})
setNodes(newNodes)
}, [store])
const handleCancelNodeSuccessStatus = useCallback((nodeId: string) => {
const {
getNodes,
setNodes,
} = store.getState()
const newNodes = produce(getNodes(), (draft) => {
const node = draft.find(n => n.id === nodeId)
if (node && node.data._runningStatus === NodeRunningStatus.Succeeded) {
node.data._runningStatus = undefined
node.data._waitingRun = false
}
})
setNodes(newNodes)
}, [store])
return {
handleNodeCancelRunningStatus,
handleCancelAllNodeSuccessStatus,
handleCancelNodeSuccessStatus,
}
}

View File

@@ -60,6 +60,7 @@ import {
useWorkflowReadOnly,
} from './use-workflow'
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
import useInspectVarsCrud from './use-inspect-vars-crud'
export const useNodesInteractions = () => {
const { t } = useTranslation()
@@ -288,7 +289,9 @@ export const useNodesInteractions = () => {
setEdges(newEdges)
}, [store, workflowStore, getNodesReadOnly])
const handleNodeSelect = useCallback((nodeId: string, cancelSelection?: boolean) => {
const handleNodeSelect = useCallback((nodeId: string, cancelSelection?: boolean, initShowLastRunTab?: boolean) => {
if(initShowLastRunTab)
workflowStore.setState({ initShowLastRunTab: true })
const {
getNodes,
setNodes,
@@ -530,6 +533,8 @@ export const useNodesInteractions = () => {
setEnteringNodePayload(undefined)
}, [store, handleNodeConnect, getNodesReadOnly, workflowStore, reactflow])
const { deleteNodeInspectorVars } = useInspectVarsCrud()
const handleNodeDelete = useCallback((nodeId: string) => {
if (getNodesReadOnly())
return
@@ -551,6 +556,7 @@ export const useNodesInteractions = () => {
if (currentNode.data.type === BlockEnum.Start)
return
deleteNodeInspectorVars(nodeId)
if (currentNode.data.type === BlockEnum.Iteration) {
const iterationChildren = nodes.filter(node => node.parentId === currentNode.id)
@@ -655,7 +661,7 @@ export const useNodesInteractions = () => {
else
saveStateToHistory(WorkflowHistoryEvent.NodeDelete)
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, t])
}, [getNodesReadOnly, store, deleteNodeInspectorVars, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, t])
const handleNodeAdd = useCallback<OnNodeAdd>((
{

View File

@@ -11,6 +11,7 @@ import {
useEdgesInteractions,
useNodesInteractions,
useNodesSyncDraft,
useWorkflowCanvasMaximize,
useWorkflowMoveMode,
useWorkflowOrganize,
useWorkflowStartRun,
@@ -35,6 +36,7 @@ export const useShortcuts = (): void => {
handleModePointer,
} = useWorkflowMoveMode()
const { handleLayout } = useWorkflowOrganize()
const { handleToggleMaximizeCanvas } = useWorkflowCanvasMaximize()
const {
zoomTo,
@@ -145,6 +147,16 @@ export const useShortcuts = (): void => {
}
}, { exactMatch: true, useCapture: true })
useKeyPress('f', (e) => {
if (shouldHandleShortcut(e)) {
e.preventDefault()
handleToggleMaximizeCanvas()
}
}, {
exactMatch: true,
useCapture: true,
})
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.1`, (e) => {
if (shouldHandleShortcut(e)) {
e.preventDefault()

View File

@@ -401,3 +401,29 @@ export const useDSL = () => {
handleExportDSL,
}
}
export const useWorkflowCanvasMaximize = () => {
const { eventEmitter } = useEventEmitterContextContext()
const maximizeCanvas = useStore(s => s.maximizeCanvas)
const setMaximizeCanvas = useStore(s => s.setMaximizeCanvas)
const {
getNodesReadOnly,
} = useNodesReadOnly()
const handleToggleMaximizeCanvas = useCallback(() => {
if (getNodesReadOnly())
return
setMaximizeCanvas(!maximizeCanvas)
localStorage.setItem('workflow-canvas-maximize', String(!maximizeCanvas))
eventEmitter?.emit({
type: 'workflow-canvas-maximize',
payload: !maximizeCanvas,
} as any)
}, [eventEmitter, getNodesReadOnly, maximizeCanvas, setMaximizeCanvas])
return {
handleToggleMaximizeCanvas,
}
}

View File

@@ -59,10 +59,6 @@ export const useWorkflow = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const nodesExtraData = useNodesExtraData()
const setPanelWidth = useCallback((width: number) => {
localStorage.setItem('workflow-node-panel-width', `${width}`)
workflowStore.setState({ panelWidth: width })
}, [workflowStore])
const getTreeLeafNodes = useCallback((nodeId: string) => {
const {
@@ -399,7 +395,6 @@ export const useWorkflow = () => {
}, [store])
return {
setPanelWidth,
getTreeLeafNodes,
getBeforeNodesInSameBranch,
getBeforeNodesInSameBranchIncludeParent,
@@ -497,6 +492,8 @@ export const useToolIcon = (data: Node['data']) => {
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
const toolIcon = useMemo(() => {
if(!data)
return ''
if (data.type === BlockEnum.Tool) {
let targetTools = buildInTools
if (data.provider_type === CollectionType.builtIn)