import type { WorkflowHistoryState } from '../store/workflow/history-slice' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverClose, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { memo, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useShallow } from 'zustand/react/shallow' import { useStore as useAppStore } from '@/app/components/app/store' import Divider from '../../base/divider' import { collaborationManager } from '../collaboration/core/collaboration-manager' import { useNodesReadOnly, useWorkflowHistory } from '../hooks' import { useCollaborativeWorkflow } from '../hooks/use-collaborative-workflow' import TipPopup from '../operator/tip-popup' type ChangeHistoryEntry = { label: string index: number state: Partial } type ChangeHistoryList = { pastStates: ChangeHistoryEntry[] futureStates: ChangeHistoryEntry[] statesCount: number } const ViewWorkflowHistory = () => { const { t } = useTranslation() const [open, setOpen] = useState(false) const { nodesReadOnly } = useNodesReadOnly() const { setCurrentLogItem, setShowMessageLogModal } = useAppStore( useShallow((state) => ({ appDetail: state.appDetail, setCurrentLogItem: state.setCurrentLogItem, setShowMessageLogModal: state.setShowMessageLogModal, })), ) const collaborativeWorkflow = useCollaborativeWorkflow() const { store, getHistoryLabel } = useWorkflowHistory() const { pastStates, futureStates, undo, redo, clear } = store.temporal.getState() const [currentHistoryStateIndex, setCurrentHistoryStateIndex] = useState(0) const handleClearHistory = useCallback(() => { clear() setCurrentHistoryStateIndex(0) }, [clear]) const handleSetState = useCallback( ({ index }: ChangeHistoryEntry) => { const diff = currentHistoryStateIndex + index if (diff === 0) return if (diff < 0) undo(diff * -1) else redo(diff) const { edges, nodes } = store.getState() if (edges.length === 0 && nodes.length === 0) return const shouldBroadcast = collaborationManager.isConnected() const { setEdges, setNodes } = collaborativeWorkflow.getState() setEdges(edges, shouldBroadcast) setNodes(nodes, shouldBroadcast, 'history:jump') if (collaborationManager.isConnected()) collaborationManager.emitHistoryAction('jump') }, [collaborativeWorkflow, currentHistoryStateIndex, redo, store, undo], ) const calculateStepLabel = useCallback( (index: number) => { if (!index) return const count = index < 0 ? index * -1 : index return `${index > 0 ? t(($) => $['changeHistory.stepForward'], { ns: 'workflow', count }) : t(($) => $['changeHistory.stepBackward'], { ns: 'workflow', count })}` }, [t], ) const calculateChangeList: ChangeHistoryList = useMemo(() => { const filterList = ( list: Array | undefined>, startIndex = 0, reverse = false, ) => list.flatMap((state, index) => { if (!state) return [] const nodes = state.nodes || store.getState().nodes || [] const nodeId = state.workflowHistoryEventMeta?.nodeId const targetTitle = nodes.find((n) => n.id === nodeId)?.data?.title ?? '' return [ { label: state.workflowHistoryEvent ? getHistoryLabel(state.workflowHistoryEvent) : '', index: reverse ? list.length - 1 - index - startIndex : index - startIndex, state: { ...state, workflowHistoryEventMeta: state.workflowHistoryEventMeta ? { ...state.workflowHistoryEventMeta, nodeTitle: state.workflowHistoryEventMeta.nodeTitle || targetTitle, } : undefined, }, }, ] }) const historyData = { pastStates: filterList(pastStates, pastStates.length).reverse(), futureStates: filterList( [ ...futureStates, !pastStates.length && !futureStates.length ? undefined : store.getState(), ].filter(Boolean), 0, true, ), statesCount: 0, } historyData.statesCount = pastStates.length + futureStates.length return { ...historyData, statesCount: pastStates.length + futureStates.length, } }, [futureStates, getHistoryLabel, pastStates, store]) const composeHistoryItemLabel = useCallback( (nodeTitle: string | undefined, baseLabel: string) => { if (!nodeTitle) return baseLabel return `${nodeTitle} ${baseLabel}` }, [], ) return ( { if (nodesReadOnly) return setOpen(nextOpen) }} > $['changeHistory.title'], { ns: 'workflow' })}> {/* Tooltip and Popover share data-popup-open on this button, so read Popover state directly. */} cn( popoverOpen && 'bg-state-accent-active text-text-accent hover:bg-state-accent-active hover:text-text-accent', ) } render={ } onClick={() => { setCurrentLogItem() setShowMessageLogModal(false) }} />
{!calculateChangeList.statesCount && (
{t(($) => $['changeHistory.placeholder'], { ns: 'workflow' })}
)}
{calculateChangeList.futureStates.map((item: ChangeHistoryEntry) => ( ))} {calculateChangeList.pastStates.map((item: ChangeHistoryEntry) => ( ))}
{!!calculateChangeList.statesCount && (
)}
{t(($) => $['changeHistory.hint'], { ns: 'workflow' })}
{t(($) => $['changeHistory.hintText'], { ns: 'workflow' })}
) } export default memo(ViewWorkflowHistory)