import { memo, useCallback, useMemo, useState, } from 'react' import { RiCloseLine, RiHistoryLine, } from '@remixicon/react' import { useTranslation } from 'react-i18next' import { useShallow } from 'zustand/react/shallow' import { useStoreApi } from 'reactflow' import { useNodesReadOnly, useWorkflowHistory, } from '../hooks' import TipPopup from '../operator/tip-popup' import type { WorkflowHistoryState } from '../workflow-history-store' import cn from '@/utils/classnames' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import { useStore as useAppStore } from '@/app/components/app/store' type ChangeHistoryEntry = { label: string index: number state: Partial<WorkflowHistoryState> } 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 reactflowStore = useStoreApi() const { store, getHistoryLabel } = useWorkflowHistory() const { pastStates, futureStates, undo, redo, clear } = store.temporal.getState() const [currentHistoryStateIndex, setCurrentHistoryStateIndex] = useState<number>(0) const handleClearHistory = useCallback(() => { clear() setCurrentHistoryStateIndex(0) }, [clear]) const handleSetState = useCallback(({ index }: ChangeHistoryEntry) => { const { setEdges, setNodes } = reactflowStore.getState() 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 setEdges(edges) setNodes(nodes) }, [currentHistoryStateIndex, reactflowStore, redo, store, undo]) const calculateStepLabel = useCallback((index: number) => { if (!index) return const count = index < 0 ? index * -1 : index return `${index > 0 ? t('workflow.changeHistory.stepForward', { count }) : t('workflow.changeHistory.stepBackward', { count })}` } , [t]) const calculateChangeList: ChangeHistoryList = useMemo(() => { const filterList = (list: any, startIndex = 0, reverse = false) => list.map((state: Partial<WorkflowHistoryState>, index: number) => { return { label: state.workflowHistoryEvent && getHistoryLabel(state.workflowHistoryEvent), index: reverse ? list.length - 1 - index - startIndex : index - startIndex, state, } }).filter(Boolean) 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]) return ( ( <PortalToFollowElem placement='bottom-end' offset={{ mainAxis: 4, crossAxis: 131, }} open={open} onOpenChange={setOpen} > <PortalToFollowElemTrigger onClick={() => !nodesReadOnly && setOpen(v => !v)}> <TipPopup title={t('workflow.changeHistory.title')} > <div className={` flex items-center justify-center w-8 h-8 rounded-md hover:bg-black/5 cursor-pointer ${open && 'bg-primary-50'} ${nodesReadOnly && 'bg-primary-50 opacity-50 !cursor-not-allowed'} `} onClick={() => { if (nodesReadOnly) return setCurrentLogItem() setShowMessageLogModal(false) }} > <RiHistoryLine className={`w-4 h-4 hover:bg-black/5 hover:text-gray-700 ${open ? 'text-primary-600' : 'text-gray-500'}`} /> </div> </TipPopup> </PortalToFollowElemTrigger> <PortalToFollowElemContent className='z-[12]'> <div className='flex flex-col ml-2 min-w-[240px] max-w-[360px] bg-white border-[0.5px] border-gray-200 shadow-xl rounded-xl overflow-y-auto' > <div className='sticky top-0 bg-white flex items-center justify-between px-4 pt-3 text-base font-semibold text-gray-900'> <div className='grow'>{t('workflow.changeHistory.title')}</div> <div className='shrink-0 flex items-center justify-center w-6 h-6 cursor-pointer' onClick={() => { setCurrentLogItem() setShowMessageLogModal(false) setOpen(false) }} > <RiCloseLine className='w-4 h-4 text-gray-500' /> </div> </div> { ( <div className='p-2 overflow-y-auto' style={{ maxHeight: 'calc(1 / 2 * 100vh)', }} > { !calculateChangeList.statesCount && ( <div className='py-12'> <RiHistoryLine className='mx-auto mb-2 w-8 h-8 text-gray-300' /> <div className='text-center text-[13px] text-gray-400'> {t('workflow.changeHistory.placeholder')} </div> </div> ) } <div className='flex flex-col'> { calculateChangeList.futureStates.map((item: ChangeHistoryEntry) => ( <div key={item?.index} className={cn( 'flex mb-0.5 px-2 py-[7px] rounded-lg hover:bg-primary-50 cursor-pointer', item?.index === currentHistoryStateIndex && 'bg-primary-50', )} onClick={() => { handleSetState(item) setOpen(false) }} > <div> <div className={cn( 'flex items-center text-[13px] font-medium leading-[18px]', item?.index === currentHistoryStateIndex && 'text-primary-600', )} > {item?.label || t('workflow.changeHistory.sessionStart')} ({calculateStepLabel(item?.index)}{item?.index === currentHistoryStateIndex && t('workflow.changeHistory.currentState')}) </div> </div> </div> )) } { calculateChangeList.pastStates.map((item: ChangeHistoryEntry) => ( <div key={item?.index} className={cn( 'flex mb-0.5 px-2 py-[7px] rounded-lg hover:bg-primary-50 cursor-pointer', item?.index === calculateChangeList.statesCount - 1 && 'bg-primary-50', )} onClick={() => { handleSetState(item) setOpen(false) }} > <div> <div className={cn( 'flex items-center text-[13px] font-medium leading-[18px]', item?.index === calculateChangeList.statesCount - 1 && 'text-primary-600', )} > {item?.label || t('workflow.changeHistory.sessionStart')} ({calculateStepLabel(item?.index)}) </div> </div> </div> )) } </div> </div> ) } { !!calculateChangeList.statesCount && ( <> <div className="h-[1px] bg-gray-100" /> <div className={cn( 'flex my-0.5 px-2 py-[7px] rounded-lg cursor-pointer', 'hover:bg-red-50 hover:text-red-600', )} onClick={() => { handleClearHistory() setOpen(false) }} > <div> <div className={cn( 'flex items-center text-[13px] font-medium leading-[18px]', )} > {t('workflow.changeHistory.clearHistory')} </div> </div> </div> </> ) } <div className="px-3 w-[240px] py-2 text-xs text-gray-500" > <div className="flex items-center mb-1 h-[22px] font-medium uppercase">{t('workflow.changeHistory.hint')}</div> <div className="mb-1 text-gray-700 leading-[18px]">{t('workflow.changeHistory.hintText')}</div> </div> </div> </PortalToFollowElemContent> </PortalToFollowElem> ) ) } export default memo(ViewWorkflowHistory)