123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- 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)
|