mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-11 20:06:54 +08:00
feat: workflow remove preview mode (#3941)
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
useEdges,
|
||||
useNodes,
|
||||
} from 'reactflow'
|
||||
import cn from 'classnames'
|
||||
import BlockIcon from '../block-icon'
|
||||
import {
|
||||
useChecklist,
|
||||
@@ -28,7 +29,12 @@ import {
|
||||
} from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
|
||||
const WorkflowChecklist = () => {
|
||||
type WorkflowChecklistProps = {
|
||||
disabled: boolean
|
||||
}
|
||||
const WorkflowChecklist = ({
|
||||
disabled,
|
||||
}: WorkflowChecklistProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const nodes = useNodes<CommonNodeType>()
|
||||
@@ -46,8 +52,13 @@ const WorkflowChecklist = () => {
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
<div className='relative flex items-center justify-center p-0.5 w-8 h-8 rounded-lg border-[0.5px] border-gray-200 bg-white shadow-xs'>
|
||||
<PortalToFollowElemTrigger onClick={() => !disabled && setOpen(v => !v)}>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex items-center justify-center p-0.5 w-8 h-8 rounded-lg border-[0.5px] border-gray-200 bg-white shadow-xs',
|
||||
disabled && 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
group flex items-center justify-center w-full h-full rounded-md cursor-pointer
|
||||
|
||||
@@ -2,7 +2,6 @@ import { memo } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWorkflow } from '../hooks'
|
||||
import { Edit03 } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
|
||||
const EditingTitle = () => {
|
||||
@@ -13,12 +12,9 @@ const EditingTitle = () => {
|
||||
|
||||
return (
|
||||
<div className='flex items-center h-[18px] text-xs text-gray-500'>
|
||||
<Edit03 className='mr-1 w-3 h-3 text-gray-400' />
|
||||
{t('workflow.common.editing')}
|
||||
{
|
||||
!!draftUpdatedAt && (
|
||||
<>
|
||||
<span className='flex items-center mx-1'>·</span>
|
||||
{t('workflow.common.autoSaved')} {dayjs(draftUpdatedAt).format('HH:mm:ss')}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
useChecklistBeforePublish,
|
||||
useNodesReadOnly,
|
||||
useNodesSyncDraft,
|
||||
useWorkflowMode,
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
import AppPublisher from '../../app/app-publisher'
|
||||
@@ -21,12 +22,13 @@ import RunAndHistory from './run-and-history'
|
||||
import EditingTitle from './editing-title'
|
||||
import RunningTitle from './running-title'
|
||||
import RestoringTitle from './restoring-title'
|
||||
import ViewHistory from './view-history'
|
||||
import Checklist from './checklist'
|
||||
import { Grid01 } from '@/app/components/base/icons/src/vender/line/layout'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { publishWorkflow } from '@/service/workflow'
|
||||
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
const Header: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -38,18 +40,21 @@ const Header: FC = () => {
|
||||
nodesReadOnly,
|
||||
getNodesReadOnly,
|
||||
} = useNodesReadOnly()
|
||||
const isRestoring = useStore(s => s.isRestoring)
|
||||
const publishedAt = useStore(s => s.publishedAt)
|
||||
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
|
||||
const {
|
||||
handleLoadBackupDraft,
|
||||
handleRunSetting,
|
||||
handleBackupDraft,
|
||||
handleRestoreFromPublishedWorkflow,
|
||||
} = useWorkflowRun()
|
||||
const { handleCheckBeforePublish } = useChecklistBeforePublish()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const {
|
||||
normal,
|
||||
restoring,
|
||||
viewHistory,
|
||||
} = useWorkflowMode()
|
||||
|
||||
const handleShowFeatures = useCallback(() => {
|
||||
const {
|
||||
@@ -62,10 +67,6 @@ const Header: FC = () => {
|
||||
setShowFeaturesPanel(true)
|
||||
}, [workflowStore, getNodesReadOnly])
|
||||
|
||||
const handleGoBackToEdit = useCallback(() => {
|
||||
handleRunSetting(true)
|
||||
}, [handleRunSetting])
|
||||
|
||||
const handleCancelRestore = useCallback(() => {
|
||||
handleLoadBackupDraft()
|
||||
workflowStore.setState({ isRestoring: false })
|
||||
@@ -102,6 +103,11 @@ const Header: FC = () => {
|
||||
handleSyncWorkflowDraft(true)
|
||||
}, [handleSyncWorkflowDraft])
|
||||
|
||||
const handleGoBackToEdit = useCallback(() => {
|
||||
handleLoadBackupDraft()
|
||||
workflowStore.setState({ historyWorkflowData: undefined })
|
||||
}, [workflowStore, handleLoadBackupDraft])
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute top-0 left-0 z-10 flex items-center justify-between w-full px-3 h-14'
|
||||
@@ -116,39 +122,25 @@ const Header: FC = () => {
|
||||
)
|
||||
}
|
||||
{
|
||||
!nodesReadOnly && !isRestoring && <EditingTitle />
|
||||
normal && <EditingTitle />
|
||||
}
|
||||
{
|
||||
nodesReadOnly && !isRestoring && <RunningTitle />
|
||||
viewHistory && <RunningTitle />
|
||||
}
|
||||
{
|
||||
isRestoring && <RestoringTitle />
|
||||
restoring && <RestoringTitle />
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!isRestoring && (
|
||||
normal && (
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
nodesReadOnly && (
|
||||
<Button
|
||||
className={`
|
||||
mr-2 px-3 py-0 h-8 bg-white text-[13px] font-medium text-primary-600
|
||||
border-[0.5px] border-gray-200 shadow-xs
|
||||
`}
|
||||
onClick={handleGoBackToEdit}
|
||||
>
|
||||
<ArrowNarrowLeft className='w-4 h-4 mr-1' />
|
||||
{t('workflow.common.goBackToEdit')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<RunAndHistory />
|
||||
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
|
||||
<Button
|
||||
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
|
||||
${nodesReadOnly && !isRestoring && 'opacity-50 !cursor-not-allowed'}
|
||||
${nodesReadOnly && 'opacity-50 !cursor-not-allowed'}
|
||||
`}
|
||||
onClick={handleShowFeatures}
|
||||
>
|
||||
@@ -166,19 +158,32 @@ const Header: FC = () => {
|
||||
crossAxisOffset: 53,
|
||||
}}
|
||||
/>
|
||||
{
|
||||
!nodesReadOnly && (
|
||||
<>
|
||||
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
|
||||
<Checklist />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
|
||||
<Checklist disabled={nodesReadOnly} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
isRestoring && (
|
||||
viewHistory && (
|
||||
<div className='flex items-center'>
|
||||
<ViewHistory withText />
|
||||
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
|
||||
<Button
|
||||
type='primary'
|
||||
className={`
|
||||
mr-2 px-3 py-0 h-8 text-[13px] font-medium
|
||||
border-[0.5px] border-gray-200 shadow-xs
|
||||
`}
|
||||
onClick={handleGoBackToEdit}
|
||||
>
|
||||
<ArrowNarrowLeft className='w-4 h-4 mr-1' />
|
||||
{t('workflow.common.goBackToEdit')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
restoring && (
|
||||
<div className='flex items-center'>
|
||||
<Button
|
||||
className={`
|
||||
|
||||
@@ -2,14 +2,15 @@ import type { FC } from 'react'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import cn from 'classnames'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
useNodesSyncDraft,
|
||||
useWorkflowInteractions,
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
import {
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
} from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { Loading02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import { MessagePlay } from '@/app/components/base/icons/src/vender/line/communication'
|
||||
|
||||
const RunMode = memo(() => {
|
||||
const { t } = useTranslation()
|
||||
@@ -31,15 +33,12 @@ const RunMode = memo(() => {
|
||||
const featuresStore = useFeaturesStore()
|
||||
const {
|
||||
handleStopRun,
|
||||
handleRunSetting,
|
||||
handleRun,
|
||||
} = useWorkflowRun()
|
||||
const {
|
||||
doSyncWorkflowDraft,
|
||||
handleSyncWorkflowDraft,
|
||||
} = useNodesSyncDraft()
|
||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||
const showInputsPanel = useStore(s => s.showInputsPanel)
|
||||
const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running
|
||||
|
||||
const handleClick = useCallback(async () => {
|
||||
@@ -55,23 +54,23 @@ const RunMode = memo(() => {
|
||||
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
const startVariables = startNode?.data.variables || []
|
||||
const fileSettings = featuresStore!.getState().features.file
|
||||
const {
|
||||
setShowDebugAndPreviewPanel,
|
||||
setShowInputsPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (!startVariables.length && !fileSettings?.image?.enabled) {
|
||||
await doSyncWorkflowDraft()
|
||||
handleRunSetting()
|
||||
handleRun({ inputs: {}, files: [] })
|
||||
setShowDebugAndPreviewPanel(true)
|
||||
setShowInputsPanel(false)
|
||||
}
|
||||
else {
|
||||
workflowStore.setState({
|
||||
historyWorkflowData: undefined,
|
||||
showInputsPanel: true,
|
||||
})
|
||||
handleSyncWorkflowDraft(true)
|
||||
setShowDebugAndPreviewPanel(true)
|
||||
setShowInputsPanel(true)
|
||||
}
|
||||
}, [
|
||||
workflowStore,
|
||||
handleSyncWorkflowDraft,
|
||||
handleRunSetting,
|
||||
handleRun,
|
||||
doSyncWorkflowDraft,
|
||||
store,
|
||||
@@ -81,12 +80,11 @@ const RunMode = memo(() => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-1.5 h-7 rounded-md text-[13px] font-medium text-primary-600
|
||||
hover:bg-primary-50 cursor-pointer
|
||||
${showInputsPanel && 'bg-primary-50'}
|
||||
${isRunning && 'bg-primary-50 !cursor-not-allowed'}
|
||||
`}
|
||||
className={cn(
|
||||
'flex items-center px-1.5 h-7 rounded-md text-[13px] font-medium text-primary-600',
|
||||
'hover:bg-primary-50 cursor-pointer',
|
||||
isRunning && 'bg-primary-50 !cursor-not-allowed',
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{
|
||||
@@ -122,38 +120,34 @@ RunMode.displayName = 'RunMode'
|
||||
|
||||
const PreviewMode = memo(() => {
|
||||
const { t } = useTranslation()
|
||||
const { handleRunSetting } = useWorkflowRun()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
|
||||
|
||||
const handleClick = () => {
|
||||
handleSyncWorkflowDraft(true)
|
||||
handleRunSetting()
|
||||
const {
|
||||
showDebugAndPreviewPanel,
|
||||
setShowDebugAndPreviewPanel,
|
||||
setHistoryWorkflowData,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (showDebugAndPreviewPanel)
|
||||
handleCancelDebugAndPreviewPanel()
|
||||
else
|
||||
setShowDebugAndPreviewPanel(true)
|
||||
|
||||
setHistoryWorkflowData(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-1.5 h-7 rounded-md text-[13px] font-medium text-primary-600
|
||||
hover:bg-primary-50 cursor-pointer
|
||||
${nodesReadOnly && 'bg-primary-50 opacity-50 !cursor-not-allowed'}
|
||||
`}
|
||||
onClick={() => !nodesReadOnly && handleClick()}
|
||||
className={cn(
|
||||
'flex items-center px-1.5 h-7 rounded-md text-[13px] font-medium text-primary-600',
|
||||
'hover:bg-primary-50 cursor-pointer',
|
||||
)}
|
||||
onClick={() => handleClick()}
|
||||
>
|
||||
{
|
||||
nodesReadOnly
|
||||
? (
|
||||
<>
|
||||
{t('workflow.common.inPreview')}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Play className='mr-1 w-4 h-4' />
|
||||
{t('workflow.common.preview')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
<MessagePlay className='mr-1 w-4 h-4' />
|
||||
{t('workflow.common.debugAndPreview')}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { Play } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import { useIsChatMode } from '../hooks'
|
||||
import { useStore } from '../store'
|
||||
import { ClockPlay } from '@/app/components/base/icons/src/vender/line/time'
|
||||
|
||||
const RunningTitle = () => {
|
||||
const { t } = useTranslation()
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const isChatMode = useIsChatMode()
|
||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||
|
||||
return (
|
||||
<div className='flex items-center h-[18px] text-xs text-primary-600'>
|
||||
<Play className='mr-1 w-3 h-3' />
|
||||
{
|
||||
appDetail?.mode === 'advanced-chat'
|
||||
? t('workflow.common.inPreviewMode')
|
||||
: t('workflow.common.inRunMode')
|
||||
}
|
||||
<div className='flex items-center h-[18px] text-xs text-gray-500'>
|
||||
<ClockPlay className='mr-1 w-3 h-3 text-gray-500' />
|
||||
<span>{isChatMode ? `Test Chat#${historyWorkflowData?.sequence_number}` : `Test Run#${historyWorkflowData?.sequence_number}`}</span>
|
||||
<span className='mx-1'>·</span>
|
||||
<span className='text-gray-500'>Test Run#2</span>
|
||||
<span className='ml-1 uppercase flex items-center px-1 h-[18px] rounded-[5px] border border-indigo-300 bg-white/[0.48] text-[10px] font-semibold text-indigo-600'>
|
||||
{t('workflow.common.viewOnly')}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesInteractions,
|
||||
useWorkflow,
|
||||
useWorkflowInteractions,
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
import { WorkflowRunningStatus } from '../types'
|
||||
@@ -35,11 +37,22 @@ import {
|
||||
useWorkflowStore,
|
||||
} from '@/app/components/workflow/store'
|
||||
|
||||
const ViewHistory = () => {
|
||||
type ViewHistoryProps = {
|
||||
withText?: boolean
|
||||
}
|
||||
const ViewHistory = ({
|
||||
withText,
|
||||
}: ViewHistoryProps) => {
|
||||
const { t } = useTranslation()
|
||||
const isChatMode = useIsChatMode()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { formatTimeFromNow } = useWorkflow()
|
||||
const {
|
||||
handleNodesCancelSelected,
|
||||
} = useNodesInteractions()
|
||||
const {
|
||||
handleCancelDebugAndPreviewPanel,
|
||||
} = useWorkflowInteractions()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { appDetail, setCurrentLogItem, setShowMessageLogModal } = useAppStore(useShallow(state => ({
|
||||
appDetail: state.appDetail,
|
||||
@@ -57,31 +70,49 @@ const ViewHistory = () => {
|
||||
return (
|
||||
(
|
||||
<PortalToFollowElem
|
||||
placement='bottom-end'
|
||||
placement={withText ? 'bottom-start' : 'bottom-end'}
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 131,
|
||||
crossAxis: withText ? -8 : 10,
|
||||
}}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
<TooltipPlus
|
||||
popupContent={t('workflow.common.viewRunHistory')}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center w-7 h-7 rounded-md hover:bg-black/5 cursor-pointer
|
||||
${open && 'bg-primary-50'}
|
||||
`}
|
||||
onClick={() => {
|
||||
setCurrentLogItem()
|
||||
setShowMessageLogModal(false)
|
||||
}}
|
||||
>
|
||||
<ClockPlay className={`w-4 h-4 ${open ? 'text-primary-600' : 'text-gray-500'}`} />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
{
|
||||
withText && (
|
||||
<div className={cn(
|
||||
'flex items-center px-3 h-8 rounded-lg border-[0.5px] border-gray-200 bg-white shadow-xs',
|
||||
'text-[13px] font-medium text-primary-600 cursor-pointer',
|
||||
open && '!bg-primary-50',
|
||||
)}>
|
||||
<ClockPlay
|
||||
className={'mr-1 w-4 h-4'}
|
||||
/>
|
||||
{t('workflow.common.showRunHistory')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!withText && (
|
||||
<TooltipPlus
|
||||
popupContent={t('workflow.common.viewRunHistory')}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center w-7 h-7 rounded-md hover:bg-black/5 cursor-pointer
|
||||
${open && 'bg-primary-50'}
|
||||
`}
|
||||
onClick={() => {
|
||||
setCurrentLogItem()
|
||||
setShowMessageLogModal(false)
|
||||
}}
|
||||
>
|
||||
<ClockPlay className={`w-4 h-4 ${open ? 'text-primary-600' : 'text-gray-500'}`} />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[12]'>
|
||||
<div
|
||||
@@ -138,6 +169,8 @@ const ViewHistory = () => {
|
||||
})
|
||||
handleBackupDraft()
|
||||
setOpen(false)
|
||||
handleNodesCancelSelected()
|
||||
handleCancelDebugAndPreviewPanel()
|
||||
}}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -7,3 +7,5 @@ export * from './use-workflow'
|
||||
export * from './use-workflow-run'
|
||||
export * from './use-workflow-template'
|
||||
export * from './use-checklist'
|
||||
export * from './use-workflow-mode'
|
||||
export * from './use-workflow-interactions'
|
||||
|
||||
@@ -201,6 +201,20 @@ export const useEdgesInteractions = () => {
|
||||
setEdges(newEdges)
|
||||
}, [store])
|
||||
|
||||
const handleEdgeCancelRunningStatus = useCallback(() => {
|
||||
const {
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
edge.data._runned = false
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store])
|
||||
|
||||
return {
|
||||
handleEdgeEnter,
|
||||
handleEdgeLeave,
|
||||
@@ -208,5 +222,6 @@ export const useEdgesInteractions = () => {
|
||||
handleEdgeDelete,
|
||||
handleEdgesChange,
|
||||
handleVariableAssignerEdgesChange,
|
||||
handleEdgeCancelRunningStatus,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,9 +243,6 @@ export const useNodesInteractions = () => {
|
||||
}, [store, getNodesReadOnly])
|
||||
|
||||
const handleNodeSelect = useCallback((nodeId: string, cancelSelection?: boolean) => {
|
||||
if (getNodesReadOnly() && !workflowStore.getState().isRestoring)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
@@ -289,14 +286,11 @@ export const useNodesInteractions = () => {
|
||||
setEdges(newEdges)
|
||||
|
||||
handleSyncWorkflowDraft()
|
||||
}, [store, handleSyncWorkflowDraft, getNodesReadOnly, workflowStore])
|
||||
}, [store, handleSyncWorkflowDraft])
|
||||
|
||||
const handleNodeClick = useCallback<NodeMouseHandler>((_, node) => {
|
||||
if (getNodesReadOnly() && !workflowStore.getState().isRestoring)
|
||||
return
|
||||
|
||||
handleNodeSelect(node.id)
|
||||
}, [handleNodeSelect, getNodesReadOnly, workflowStore])
|
||||
}, [handleNodeSelect])
|
||||
|
||||
const handleNodeConnect = useCallback<OnConnect>(({
|
||||
source,
|
||||
@@ -834,6 +828,36 @@ export const useNodesInteractions = () => {
|
||||
handleNodeDelete(node.id)
|
||||
}, [getNodesReadOnly, handleNodeDelete, store, workflowStore])
|
||||
|
||||
const handleNodeCancelRunningStatus = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data._runningStatus = undefined
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [store])
|
||||
|
||||
const handleNodesCancelSelected = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data.selected = false
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [store])
|
||||
|
||||
return {
|
||||
handleNodeDragStart,
|
||||
handleNodeDrag,
|
||||
@@ -853,5 +877,7 @@ export const useNodesInteractions = () => {
|
||||
handleNodeCut,
|
||||
handleNodeDeleteSelected,
|
||||
handleNodePaste,
|
||||
handleNodeCancelRunningStatus,
|
||||
handleNodesCancelSelected,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,8 @@ export const useNodesSyncDraft = () => {
|
||||
}, [store, featuresStore, workflowStore])
|
||||
|
||||
const syncWorkflowDraftWhenPageClose = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
const postParams = getPostParams()
|
||||
|
||||
if (postParams) {
|
||||
@@ -89,16 +91,18 @@ export const useNodesSyncDraft = () => {
|
||||
JSON.stringify(postParams.params),
|
||||
)
|
||||
}
|
||||
}, [getPostParams, params.appId])
|
||||
}, [getPostParams, params.appId, getNodesReadOnly])
|
||||
|
||||
const doSyncWorkflowDraft = useCallback(async (appId?: string) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
const postParams = getPostParams(appId)
|
||||
|
||||
if (postParams) {
|
||||
const res = await syncWorkflowDraft(postParams)
|
||||
workflowStore.getState().setDraftUpdatedAt(res.updated_at)
|
||||
}
|
||||
}, [workflowStore, getPostParams])
|
||||
}, [workflowStore, getPostParams, getNodesReadOnly])
|
||||
|
||||
const handleSyncWorkflowDraft = useCallback((sync?: boolean, appId?: string) => {
|
||||
if (getNodesReadOnly())
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import { WORKFLOW_DATA_UPDATE } from '../constants'
|
||||
import type { WorkflowDataUpdator } from '../types'
|
||||
import {
|
||||
initialEdges,
|
||||
initialNodes,
|
||||
} from '../utils'
|
||||
import { useEdgesInteractions } from './use-edges-interactions'
|
||||
import { useNodesInteractions } from './use-nodes-interactions'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
export const useWorkflowInteractions = () => {
|
||||
const reactflow = useReactFlow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { handleNodeCancelRunningStatus } = useNodesInteractions()
|
||||
const { handleEdgeCancelRunningStatus } = useEdgesInteractions()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
const handleCancelDebugAndPreviewPanel = useCallback(() => {
|
||||
workflowStore.setState({
|
||||
showDebugAndPreviewPanel: false,
|
||||
})
|
||||
handleNodeCancelRunningStatus()
|
||||
handleEdgeCancelRunningStatus()
|
||||
}, [workflowStore, handleNodeCancelRunningStatus, handleEdgeCancelRunningStatus])
|
||||
|
||||
const handleUpdateWorkflowCanvas = useCallback((payload: WorkflowDataUpdator) => {
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
} = payload
|
||||
const { setViewport } = reactflow
|
||||
eventEmitter?.emit({
|
||||
type: WORKFLOW_DATA_UPDATE,
|
||||
payload: {
|
||||
nodes: initialNodes(nodes, edges),
|
||||
edges: initialEdges(edges, nodes),
|
||||
},
|
||||
} as any)
|
||||
setViewport(viewport)
|
||||
}, [eventEmitter, reactflow])
|
||||
|
||||
return {
|
||||
handleCancelDebugAndPreviewPanel,
|
||||
handleUpdateWorkflowCanvas,
|
||||
}
|
||||
}
|
||||
14
web/app/components/workflow/hooks/use-workflow-mode.ts
Normal file
14
web/app/components/workflow/hooks/use-workflow-mode.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useStore } from '../store'
|
||||
|
||||
export const useWorkflowMode = () => {
|
||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||
const isRestoring = useStore(s => s.isRestoring)
|
||||
return useMemo(() => {
|
||||
return {
|
||||
normal: !historyWorkflowData && !isRestoring,
|
||||
restoring: isRestoring,
|
||||
viewHistory: !!historyWorkflowData,
|
||||
}
|
||||
}, [historyWorkflowData, isRestoring])
|
||||
}
|
||||
@@ -5,11 +5,12 @@ import {
|
||||
} from 'reactflow'
|
||||
import produce from 'immer'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import { useNodesSyncDraft } from '../hooks'
|
||||
import {
|
||||
NodeRunningStatus,
|
||||
WorkflowRunningStatus,
|
||||
} from '../types'
|
||||
import { useWorkflow } from './use-workflow'
|
||||
import { useWorkflowInteractions } from './use-workflow-interactions'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import type { IOtherOptions } from '@/service/base'
|
||||
import { ssePost } from '@/service/base'
|
||||
@@ -24,7 +25,8 @@ export const useWorkflowRun = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const reactflow = useReactFlow()
|
||||
const featuresStore = useFeaturesStore()
|
||||
const { renderTreeFromRecord } = useWorkflow()
|
||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { handleUpdateWorkflowCanvas } = useWorkflowInteractions()
|
||||
|
||||
const handleBackupDraft = useCallback(() => {
|
||||
const {
|
||||
@@ -45,15 +47,11 @@ export const useWorkflowRun = () => {
|
||||
viewport: getViewport(),
|
||||
features,
|
||||
})
|
||||
doSyncWorkflowDraft()
|
||||
}
|
||||
}, [reactflow, workflowStore, store, featuresStore])
|
||||
}, [reactflow, workflowStore, store, featuresStore, doSyncWorkflowDraft])
|
||||
|
||||
const handleLoadBackupDraft = useCallback(() => {
|
||||
const {
|
||||
setNodes,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const { setViewport } = reactflow
|
||||
const {
|
||||
backupDraft,
|
||||
setBackupDraft,
|
||||
@@ -66,64 +64,32 @@ export const useWorkflowRun = () => {
|
||||
viewport,
|
||||
features,
|
||||
} = backupDraft
|
||||
setNodes(nodes)
|
||||
setEdges(edges)
|
||||
setViewport(viewport)
|
||||
handleUpdateWorkflowCanvas({
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
})
|
||||
featuresStore!.setState({ features })
|
||||
setBackupDraft(undefined)
|
||||
}
|
||||
}, [store, reactflow, workflowStore, featuresStore])
|
||||
}, [handleUpdateWorkflowCanvas, workflowStore, featuresStore])
|
||||
|
||||
const handleRunSetting = useCallback((shouldClear?: boolean) => {
|
||||
if (shouldClear) {
|
||||
workflowStore.setState({
|
||||
workflowRunningData: undefined,
|
||||
historyWorkflowData: undefined,
|
||||
showInputsPanel: false,
|
||||
})
|
||||
}
|
||||
else {
|
||||
workflowStore.setState({
|
||||
workflowRunningData: {
|
||||
result: {
|
||||
status: shouldClear ? '' : WorkflowRunningStatus.Waiting,
|
||||
},
|
||||
tracing: [],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
setNodes,
|
||||
getNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
|
||||
if (shouldClear) {
|
||||
handleLoadBackupDraft()
|
||||
}
|
||||
else {
|
||||
handleBackupDraft()
|
||||
const newNodes = produce(getNodes(), (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data._runningStatus = NodeRunningStatus.Waiting
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
edge.data._runned = false
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}
|
||||
}, [store, handleLoadBackupDraft, handleBackupDraft, workflowStore])
|
||||
|
||||
const handleRun = useCallback((
|
||||
const handleRun = useCallback(async (
|
||||
params: any,
|
||||
callback?: IOtherOptions,
|
||||
) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const newNodes = produce(getNodes(), (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data.selected = false
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
await doSyncWorkflowDraft()
|
||||
|
||||
const {
|
||||
onWorkflowStarted,
|
||||
onWorkflowFinished,
|
||||
@@ -151,15 +117,14 @@ export const useWorkflowRun = () => {
|
||||
let prevNodeId = ''
|
||||
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.result = {
|
||||
...draft?.result,
|
||||
setWorkflowRunningData({
|
||||
result: {
|
||||
status: WorkflowRunningStatus.Running,
|
||||
}
|
||||
}))
|
||||
},
|
||||
tracing: [],
|
||||
})
|
||||
|
||||
ssePost(
|
||||
url,
|
||||
@@ -174,8 +139,6 @@ export const useWorkflowRun = () => {
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
@@ -188,12 +151,6 @@ export const useWorkflowRun = () => {
|
||||
}
|
||||
}))
|
||||
|
||||
const newNodes = produce(getNodes(), (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data._runningStatus = NodeRunningStatus.Waiting
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
edge.data = {
|
||||
@@ -253,6 +210,7 @@ export const useWorkflowRun = () => {
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
transform,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
@@ -268,12 +226,12 @@ export const useWorkflowRun = () => {
|
||||
const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id)
|
||||
const currentNode = nodes[currentNodeIndex]
|
||||
const position = currentNode.position
|
||||
const zoom = 1
|
||||
const zoom = transform[2]
|
||||
|
||||
setViewport({
|
||||
x: (clientWidth - 400 - currentNode.width!) / 2 - position.x,
|
||||
y: (clientHeight - currentNode.height!) / 2 - position.y,
|
||||
zoom,
|
||||
x: (clientWidth - 400 - currentNode.width! * zoom) / 2 - position.x * zoom,
|
||||
y: (clientHeight - currentNode.height! * zoom) / 2 - position.y * zoom,
|
||||
zoom: transform[2],
|
||||
})
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
|
||||
@@ -329,7 +287,7 @@ export const useWorkflowRun = () => {
|
||||
...restCallback,
|
||||
},
|
||||
)
|
||||
}, [store, reactflow, workflowStore])
|
||||
}, [store, reactflow, workflowStore, doSyncWorkflowDraft])
|
||||
|
||||
const handleStopRun = useCallback((taskId: string) => {
|
||||
const appId = useAppStore.getState().appDetail?.id
|
||||
@@ -344,18 +302,21 @@ export const useWorkflowRun = () => {
|
||||
if (publishedWorkflow) {
|
||||
const nodes = publishedWorkflow.graph.nodes
|
||||
const edges = publishedWorkflow.graph.edges
|
||||
const viewport = publishedWorkflow.graph.viewport
|
||||
const viewport = publishedWorkflow.graph.viewport!
|
||||
|
||||
renderTreeFromRecord(nodes, edges, viewport)
|
||||
handleUpdateWorkflowCanvas({
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
})
|
||||
featuresStore?.setState({ features: publishedWorkflow.features })
|
||||
workflowStore.getState().setPublishedAt(publishedWorkflow.created_at)
|
||||
}
|
||||
}, [featuresStore, workflowStore, renderTreeFromRecord])
|
||||
}, [featuresStore, handleUpdateWorkflowCanvas, workflowStore])
|
||||
|
||||
return {
|
||||
handleBackupDraft,
|
||||
handleLoadBackupDraft,
|
||||
handleRunSetting,
|
||||
handleRun,
|
||||
handleStopRun,
|
||||
handleRestoreFromPublishedWorkflow,
|
||||
|
||||
@@ -16,15 +16,11 @@ import {
|
||||
} from 'reactflow'
|
||||
import type {
|
||||
Connection,
|
||||
Viewport,
|
||||
} from 'reactflow'
|
||||
import {
|
||||
getLayoutByDagre,
|
||||
initialEdges,
|
||||
initialNodes,
|
||||
} from '../utils'
|
||||
import type {
|
||||
Edge,
|
||||
Node,
|
||||
ValueSelector,
|
||||
} from '../types'
|
||||
@@ -39,7 +35,6 @@ import {
|
||||
import {
|
||||
AUTO_LAYOUT_OFFSET,
|
||||
SUPPORT_OUTPUT_VARS_NODE,
|
||||
WORKFLOW_DATA_UPDATE,
|
||||
} from '../constants'
|
||||
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
|
||||
import { useNodesExtraData } from './use-nodes-data'
|
||||
@@ -58,7 +53,6 @@ import {
|
||||
fetchAllCustomTools,
|
||||
} from '@/service/tools'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
export const useIsChatMode = () => {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
@@ -73,7 +67,6 @@ export const useWorkflow = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
const setPanelWidth = useCallback((width: number) => {
|
||||
localStorage.setItem('workflow-node-panel-width', `${width}`)
|
||||
@@ -323,23 +316,6 @@ export const useWorkflow = () => {
|
||||
return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow()
|
||||
}, [locale])
|
||||
|
||||
const renderTreeFromRecord = useCallback((nodes: Node[], edges: Edge[], viewport?: Viewport) => {
|
||||
const { setViewport } = reactflow
|
||||
|
||||
const nodesMap = nodes.map(node => ({ ...node, data: { ...node.data, selected: false } }))
|
||||
|
||||
eventEmitter?.emit({
|
||||
type: WORKFLOW_DATA_UPDATE,
|
||||
payload: {
|
||||
nodes: initialNodes(nodesMap, edges),
|
||||
edges: initialEdges(edges, nodesMap),
|
||||
},
|
||||
} as any)
|
||||
|
||||
if (viewport)
|
||||
setViewport(viewport)
|
||||
}, [reactflow, eventEmitter])
|
||||
|
||||
const getNode = useCallback((nodeId?: string) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
@@ -369,7 +345,6 @@ export const useWorkflow = () => {
|
||||
isNodeVarsUsedInNodes,
|
||||
isValidConnection,
|
||||
formatTimeFromNow,
|
||||
renderTreeFromRecord,
|
||||
getNode,
|
||||
getBeforeNodeById,
|
||||
enableShortcuts,
|
||||
@@ -510,11 +485,11 @@ export const useNodesReadOnly = () => {
|
||||
isRestoring,
|
||||
} = workflowStore.getState()
|
||||
|
||||
return workflowRunningData || historyWorkflowData || isRestoring
|
||||
return workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
nodesReadOnly: !!(workflowRunningData || historyWorkflowData || isRestoring),
|
||||
nodesReadOnly: !!(workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring),
|
||||
getNodesReadOnly,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ type Props = {
|
||||
value: any
|
||||
onChange: (value: any) => void
|
||||
className?: string
|
||||
autoFocus?: boolean
|
||||
}
|
||||
|
||||
const FormItem: FC<Props> = ({
|
||||
@@ -28,6 +29,7 @@ const FormItem: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
autoFocus,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { type } = payload
|
||||
@@ -87,6 +89,7 @@ const FormItem: FC<Props> = ({
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={t('appDebug.variableConig.inputPlaceholder')!}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -99,6 +102,7 @@ const FormItem: FC<Props> = ({
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={t('appDebug.variableConig.inputPlaceholder')!}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -110,6 +114,7 @@ const FormItem: FC<Props> = ({
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={t('appDebug.variableConig.inputPlaceholder')!}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -141,9 +146,9 @@ const FormItem: FC<Props> = ({
|
||||
type === InputVarType.files && (
|
||||
<TextGenerationImageUploader
|
||||
settings={{
|
||||
...fileSettings.image,
|
||||
...fileSettings?.image,
|
||||
detail: Resolution.high,
|
||||
}}
|
||||
} as any}
|
||||
onFilesChange={files => onChange(files.filter(file => file.progress !== -1).map(fileItem => ({
|
||||
type: 'image',
|
||||
transfer_method: fileItem.type,
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
import {
|
||||
cloneElement,
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import type { NodeProps } from '../../types'
|
||||
import {
|
||||
@@ -38,11 +39,24 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
}) => {
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const toolIcon = useToolIcon(data)
|
||||
|
||||
const {
|
||||
showRunningBorder,
|
||||
showSuccessBorder,
|
||||
showFailedBorder,
|
||||
} = useMemo(() => {
|
||||
return {
|
||||
showRunningBorder: data._runningStatus === NodeRunningStatus.Running && !data.selected,
|
||||
showSuccessBorder: data._runningStatus === NodeRunningStatus.Succeeded && !data.selected,
|
||||
showFailedBorder: data._runningStatus === NodeRunningStatus.Failed && !data.selected,
|
||||
}
|
||||
}, [data._runningStatus, data.selected])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex border-[2px] rounded-2xl
|
||||
${(data.selected && !data._runningStatus && !data._isInvalidConnection) ? 'border-primary-600' : 'border-transparent'}
|
||||
${(data.selected && !data._isInvalidConnection) ? 'border-primary-600' : 'border-transparent'}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
@@ -50,15 +64,14 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
group relative pb-1 w-[240px] bg-[#fcfdff] shadow-xs
|
||||
border border-transparent rounded-[15px]
|
||||
${!data._runningStatus && 'hover:shadow-lg'}
|
||||
${data._runningStatus === NodeRunningStatus.Running && '!border-primary-500'}
|
||||
${data._runningStatus === NodeRunningStatus.Succeeded && '!border-[#12B76A]'}
|
||||
${data._runningStatus === NodeRunningStatus.Failed && '!border-[#F04438]'}
|
||||
${data._runningStatus === NodeRunningStatus.Waiting && 'opacity-70'}
|
||||
${showRunningBorder && '!border-primary-500'}
|
||||
${showSuccessBorder && '!border-[#12B76A]'}
|
||||
${showFailedBorder && '!border-[#F04438]'}
|
||||
${data._isInvalidConnection && '!border-[#F04438]'}
|
||||
`}
|
||||
>
|
||||
{
|
||||
data.type !== BlockEnum.VariableAssigner && !data._runningStatus && (
|
||||
data.type !== BlockEnum.VariableAssigner && (
|
||||
<NodeTargetHandle
|
||||
id={id}
|
||||
data={data}
|
||||
@@ -68,7 +81,7 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && !data._runningStatus && (
|
||||
data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && (
|
||||
<NodeSourceHandle
|
||||
id={id}
|
||||
data={data}
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NextStep from './components/next-step'
|
||||
import PanelOperator from './components/panel-operator'
|
||||
@@ -32,6 +34,7 @@ import { canRunBySingle } from '@/app/components/workflow/utils'
|
||||
import { Play } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
|
||||
type BasePanelProps = {
|
||||
children: ReactElement
|
||||
@@ -43,6 +46,9 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { showMessageLogModal } = useAppStore(useShallow(state => ({
|
||||
showMessageLogModal: state.showMessageLogModal,
|
||||
})))
|
||||
const panelWidth = localStorage.getItem('workflow-node-panel-width') ? parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420
|
||||
const {
|
||||
setPanelWidth,
|
||||
@@ -82,7 +88,10 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id])
|
||||
|
||||
return (
|
||||
<div className='relative mr-2 h-full'>
|
||||
<div className={cn(
|
||||
'relative mr-2 h-full',
|
||||
showMessageLogModal && '!absolute !mr-0 w-[384px] overflow-hidden -top-[5px] right-[416px] z-0 shadow-lg border-[0.5px] border-gray-200 rounded-2xl transition-all',
|
||||
)}>
|
||||
<div
|
||||
ref={triggerRef}
|
||||
className='absolute top-1/2 -translate-y-1/2 -left-2 w-3 h-6 cursor-col-resize resize-x'>
|
||||
|
||||
@@ -5,18 +5,25 @@ import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useStore } from '../../store'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../../store'
|
||||
import { useWorkflowRun } from '../../hooks'
|
||||
import UserInput from './user-input'
|
||||
import Chat from '@/app/components/base/chat/chat'
|
||||
import type { ChatItem } from '@/app/components/base/chat/types'
|
||||
import { fetchConvesationMessages } from '@/service/debug'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
const ChatRecord = () => {
|
||||
const [fetched, setFetched] = useState(false)
|
||||
const [chatList, setChatList] = useState([])
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { handleLoadBackupDraft } = useWorkflowRun()
|
||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||
const currentConversationID = historyWorkflowData?.conversation_id
|
||||
|
||||
@@ -79,6 +86,15 @@ const ChatRecord = () => {
|
||||
<>
|
||||
<div className='shrink-0 flex items-center justify-between p-4 pb-1 text-base font-semibold text-gray-900'>
|
||||
{`TEST CHAT#${historyWorkflowData?.sequence_number}`}
|
||||
<div
|
||||
className='flex justify-center items-center w-6 h-6 cursor-pointer'
|
||||
onClick={() => {
|
||||
handleLoadBackupDraft()
|
||||
workflowStore.setState({ historyWorkflowData: undefined })
|
||||
}}
|
||||
>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='grow h-0'>
|
||||
<Chat
|
||||
|
||||
@@ -3,10 +3,17 @@ import {
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useKeyPress } from 'ahooks'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useEdgesInteractions,
|
||||
useNodesInteractions,
|
||||
useWorkflowInteractions,
|
||||
} from '../../hooks'
|
||||
import ChatWrapper from './chat-wrapper'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
export type ChatWrapperRefType = {
|
||||
handleRestart: () => void
|
||||
@@ -14,33 +21,56 @@ export type ChatWrapperRefType = {
|
||||
const DebugAndPreview = () => {
|
||||
const { t } = useTranslation()
|
||||
const chatRef = useRef({ handleRestart: () => {} })
|
||||
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
|
||||
const { handleNodeCancelRunningStatus } = useNodesInteractions()
|
||||
const { handleEdgeCancelRunningStatus } = useEdgesInteractions()
|
||||
|
||||
const handleRestartChat = () => {
|
||||
handleNodeCancelRunningStatus()
|
||||
handleEdgeCancelRunningStatus()
|
||||
chatRef.current.handleRestart()
|
||||
}
|
||||
|
||||
useKeyPress('shift.r', () => {
|
||||
chatRef.current.handleRestart()
|
||||
handleRestartChat()
|
||||
}, {
|
||||
exactMatch: true,
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex flex-col w-[400px] rounded-l-2xl h-full border border-black/[0.02] shadow-xl
|
||||
`}
|
||||
className={cn(
|
||||
'flex flex-col w-[400px] rounded-l-2xl h-full border border-black/[0.02]',
|
||||
)}
|
||||
style={{
|
||||
background: 'linear-gradient(156deg, rgba(242, 244, 247, 0.80) 0%, rgba(242, 244, 247, 0.00) 99.43%), var(--white, #FFF)',
|
||||
}}
|
||||
>
|
||||
<div className='shrink-0 flex items-center justify-between px-4 pt-3 pb-2 font-semibold text-gray-900'>
|
||||
<div className='shrink-0 flex items-center justify-between pl-4 pr-3 pt-3 pb-2 font-semibold text-gray-900'>
|
||||
{t('workflow.common.debugAndPreview').toLocaleUpperCase()}
|
||||
<Button
|
||||
className='pl-2.5 pr-[7px] h-8 bg-white border-[0.5px] border-gray-200 shadow-xs rounded-lg text-[13px] text-primary-600 font-semibold'
|
||||
onClick={() => chatRef.current.handleRestart()}
|
||||
>
|
||||
<RefreshCcw01 className='mr-1 w-3.5 h-3.5' />
|
||||
{t('common.operation.refresh')}
|
||||
<div className='ml-2 px-1 leading-[18px] rounded-md border border-gray-200 bg-gray-50 text-[11px] text-gray-500 font-medium'>Shift</div>
|
||||
<div className='ml-0.5 px-1 leading-[18px] rounded-md border border-gray-200 bg-gray-50 text-[11px] text-gray-500 font-medium'>R</div>
|
||||
</Button>
|
||||
<div className='flex items-center'>
|
||||
<Button
|
||||
className='px-2 h-8 bg-white border-[0.5px] border-gray-200 shadow-xs rounded-lg text-xs text-gray-700 font-medium'
|
||||
onClick={() => handleRestartChat()}
|
||||
>
|
||||
<RefreshCcw01 className='shrink-0 mr-1 w-3 h-3 text-gray-500' />
|
||||
<div
|
||||
className='grow truncate uppercase'
|
||||
title={t('common.operation.refresh') || ''}
|
||||
>
|
||||
{t('common.operation.refresh')}
|
||||
</div>
|
||||
<div className='shrink-0 ml-1 px-1 leading-[18px] rounded-md border border-gray-200 bg-gray-50 text-[11px] text-gray-500 font-medium'>Shift</div>
|
||||
<div className='shrink-0 ml-0.5 px-1 leading-[18px] rounded-md border border-gray-200 bg-gray-50 text-[11px] text-gray-500 font-medium'>R</div>
|
||||
</Button>
|
||||
<div className='mx-3 w-[1px] h-3.5 bg-gray-200'></div>
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 cursor-pointer'
|
||||
onClick={handleCancelDebugAndPreviewPanel}
|
||||
>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='grow rounded-b-2xl overflow-y-auto'>
|
||||
<ChatWrapper ref={chatRef} />
|
||||
|
||||
@@ -56,12 +56,13 @@ const UserInput = () => {
|
||||
expanded && (
|
||||
<div className='py-2 text-[13px] text-gray-900'>
|
||||
{
|
||||
variables.map(variable => (
|
||||
variables.map((variable, index) => (
|
||||
<div
|
||||
key={variable.variable}
|
||||
className='mb-2 last-of-type:mb-0'
|
||||
>
|
||||
<FormItem
|
||||
autoFocus={index === 0}
|
||||
payload={variable}
|
||||
value={inputs[variable.variable]}
|
||||
onChange={v => handleValueChange(variable.variable, v)}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import cn from 'classnames'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import type { CommonNodeType } from '../types'
|
||||
import { Panel as NodePanel } from '../nodes'
|
||||
@@ -23,9 +21,8 @@ const Panel: FC = () => {
|
||||
const nodes = useNodes<CommonNodeType>()
|
||||
const isChatMode = useIsChatMode()
|
||||
const selectedNode = nodes.find(node => node.data.selected)
|
||||
const showInputsPanel = useStore(s => s.showInputsPanel)
|
||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
|
||||
const isRestoring = useStore(s => s.isRestoring)
|
||||
const {
|
||||
enableShortcuts,
|
||||
@@ -37,28 +34,13 @@ const Panel: FC = () => {
|
||||
showMessageLogModal: state.showMessageLogModal,
|
||||
setShowMessageLogModal: state.setShowMessageLogModal,
|
||||
})))
|
||||
const {
|
||||
showNodePanel,
|
||||
showDebugAndPreviewPanel,
|
||||
showWorkflowPreview,
|
||||
} = useMemo(() => {
|
||||
return {
|
||||
showNodePanel: !!selectedNode && !workflowRunningData && !historyWorkflowData && !showInputsPanel,
|
||||
showDebugAndPreviewPanel: isChatMode && workflowRunningData && !historyWorkflowData,
|
||||
showWorkflowPreview: !isChatMode && !historyWorkflowData && (workflowRunningData || showInputsPanel),
|
||||
}
|
||||
}, [
|
||||
showInputsPanel,
|
||||
selectedNode,
|
||||
isChatMode,
|
||||
workflowRunningData,
|
||||
historyWorkflowData,
|
||||
])
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={-1}
|
||||
className='absolute top-14 right-0 bottom-2 flex z-10 outline-none'
|
||||
className={cn(
|
||||
'absolute top-14 right-0 bottom-2 flex z-10 outline-none',
|
||||
)}
|
||||
onFocus={disableShortcuts}
|
||||
onBlur={enableShortcuts}
|
||||
key={`${isRestoring}`}
|
||||
@@ -76,6 +58,11 @@ const Panel: FC = () => {
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!selectedNode && (
|
||||
<NodePanel {...selectedNode!} />
|
||||
)
|
||||
}
|
||||
{
|
||||
historyWorkflowData && !isChatMode && (
|
||||
<Record />
|
||||
@@ -87,20 +74,15 @@ const Panel: FC = () => {
|
||||
)
|
||||
}
|
||||
{
|
||||
showDebugAndPreviewPanel && (
|
||||
showDebugAndPreviewPanel && isChatMode && (
|
||||
<DebugAndPreview />
|
||||
)
|
||||
}
|
||||
{
|
||||
showWorkflowPreview && (
|
||||
showDebugAndPreviewPanel && !isChatMode && (
|
||||
<WorkflowPreview />
|
||||
)
|
||||
}
|
||||
{
|
||||
showNodePanel && (
|
||||
<NodePanel {...selectedNode!} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ const InputsPanel = ({ onRun }: Props) => {
|
||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||
const {
|
||||
handleRun,
|
||||
handleRunSetting,
|
||||
} = useWorkflowRun()
|
||||
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
const startVariables = startNode?.data.variables
|
||||
@@ -72,7 +71,6 @@ const InputsPanel = ({ onRun }: Props) => {
|
||||
|
||||
const doRun = () => {
|
||||
onRun()
|
||||
handleRunSetting()
|
||||
handleRun({ inputs, files })
|
||||
}
|
||||
|
||||
@@ -87,12 +85,13 @@ const InputsPanel = ({ onRun }: Props) => {
|
||||
<>
|
||||
<div className='px-4 pb-2'>
|
||||
{
|
||||
variables.map(variable => (
|
||||
variables.map((variable, index) => (
|
||||
<div
|
||||
key={variable.variable}
|
||||
className='mb-2 last-of-type:mb-0'
|
||||
>
|
||||
<FormItem
|
||||
autoFocus={index === 0}
|
||||
className='!block'
|
||||
payload={variable}
|
||||
value={inputs[variable.variable]}
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
import { memo } from 'react'
|
||||
import { memo, useCallback } from 'react'
|
||||
import type { WorkflowDataUpdator } from '../types'
|
||||
import Run from '../run'
|
||||
import { useStore } from '../store'
|
||||
import { useWorkflowInteractions } from '../hooks'
|
||||
|
||||
const Record = () => {
|
||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||
const { handleUpdateWorkflowCanvas } = useWorkflowInteractions()
|
||||
|
||||
const handleResultCallback = useCallback((res: any) => {
|
||||
const graph: WorkflowDataUpdator = res.graph
|
||||
handleUpdateWorkflowCanvas({
|
||||
nodes: graph.nodes,
|
||||
edges: graph.edges,
|
||||
viewport: graph.viewport,
|
||||
})
|
||||
}, [handleUpdateWorkflowCanvas])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col w-[400px] h-full rounded-l-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#${historyWorkflowData?.sequence_number}`}
|
||||
</div>
|
||||
<Run runID={historyWorkflowData?.id || ''} />
|
||||
<Run
|
||||
runID={historyWorkflowData?.id || ''}
|
||||
getResultCallback={handleResultCallback}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import OutputPanel from '../run/output-panel'
|
||||
import ResultPanel from '../run/result-panel'
|
||||
import TracingPanel from '../run/tracing-panel'
|
||||
import {
|
||||
useWorkflowRun,
|
||||
useWorkflowInteractions,
|
||||
} from '../hooks'
|
||||
import { useStore } from '../store'
|
||||
import {
|
||||
@@ -22,9 +22,10 @@ import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
const WorkflowPreview = () => {
|
||||
const { t } = useTranslation()
|
||||
const { handleRunSetting } = useWorkflowRun()
|
||||
const showInputsPanel = useStore(s => s.showInputsPanel)
|
||||
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
|
||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||
const showInputsPanel = useStore(s => s.showInputsPanel)
|
||||
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
|
||||
const [currentTab, setCurrentTab] = useState<string>(showInputsPanel ? 'INPUT' : 'TRACING')
|
||||
|
||||
const switchTab = async (tab: string) => {
|
||||
@@ -34,6 +35,11 @@ const WorkflowPreview = () => {
|
||||
const [height, setHieght] = useState(0)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (showDebugAndPreviewPanel && showInputsPanel)
|
||||
setCurrentTab('INPUT')
|
||||
}, [showDebugAndPreviewPanel, showInputsPanel])
|
||||
|
||||
const adjustResultHeight = () => {
|
||||
if (ref.current)
|
||||
setHieght(ref.current?.clientHeight - 16 - 16 - 2 - 1)
|
||||
@@ -49,11 +55,9 @@ const WorkflowPreview = () => {
|
||||
`}>
|
||||
<div className='flex items-center justify-between p-4 pb-1 text-base font-semibold text-gray-900'>
|
||||
{`Test Run${!workflowRunningData?.result.sequence_number ? '' : `#${workflowRunningData?.result.sequence_number}`}`}
|
||||
{showInputsPanel && workflowRunningData?.result?.status !== WorkflowRunningStatus.Running && (
|
||||
<div className='p-1 cursor-pointer' onClick={() => handleRunSetting(true)}>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
)}
|
||||
<div className='p-1 cursor-pointer' onClick={() => handleCancelDebugAndPreviewPanel()}>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='grow relative flex flex-col'>
|
||||
<div className='shrink-0 flex items-center px-4 border-b-[0.5px] border-[rgba(0,0,0,0.05)]'>
|
||||
@@ -107,7 +111,7 @@ const WorkflowPreview = () => {
|
||||
'grow bg-white h-0 overflow-y-auto rounded-b-2xl',
|
||||
(currentTab === 'RESULT' || currentTab === 'TRACING') && '!bg-gray-50',
|
||||
)}>
|
||||
{currentTab === 'INPUT' && (
|
||||
{currentTab === 'INPUT' && showInputsPanel && (
|
||||
<InputsPanel onRun={() => switchTab('RESULT')} />
|
||||
)}
|
||||
{currentTab === 'RESULT' && (
|
||||
|
||||
@@ -23,9 +23,9 @@ type Shape = {
|
||||
appId: string
|
||||
panelWidth: number
|
||||
workflowRunningData?: WorkflowRunningData
|
||||
setWorkflowRunningData: (workflowData: WorkflowRunningData) => void
|
||||
setWorkflowRunningData: (workflowData?: WorkflowRunningData) => void
|
||||
historyWorkflowData?: HistoryWorkflowData
|
||||
setHistoryWorkflowData: (historyWorkflowData: HistoryWorkflowData) => void
|
||||
setHistoryWorkflowData: (historyWorkflowData?: HistoryWorkflowData) => void
|
||||
showRunHistory: boolean
|
||||
setShowRunHistory: (showRunHistory: boolean) => void
|
||||
showFeaturesPanel: boolean
|
||||
@@ -68,6 +68,8 @@ type Shape = {
|
||||
setClipboardElements: (clipboardElements: Node[]) => void
|
||||
shortcutsDisabled: boolean
|
||||
setShortcutsDisabled: (shortcutsDisabled: boolean) => void
|
||||
showDebugAndPreviewPanel: boolean
|
||||
setShowDebugAndPreviewPanel: (showDebugAndPreviewPanel: boolean) => void
|
||||
}
|
||||
|
||||
export const createWorkflowStore = () => {
|
||||
@@ -117,6 +119,8 @@ export const createWorkflowStore = () => {
|
||||
setClipboardElements: clipboardElements => set(() => ({ clipboardElements })),
|
||||
shortcutsDisabled: false,
|
||||
setShortcutsDisabled: shortcutsDisabled => set(() => ({ shortcutsDisabled })),
|
||||
showDebugAndPreviewPanel: false,
|
||||
setShowDebugAndPreviewPanel: showDebugAndPreviewPanel => set(() => ({ showDebugAndPreviewPanel })),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
Edge as ReactFlowEdge,
|
||||
Node as ReactFlowNode,
|
||||
Viewport,
|
||||
} from 'reactflow'
|
||||
import type { TransferMethod } from '@/types/app'
|
||||
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
|
||||
@@ -60,6 +61,12 @@ export type NodePanelProps<T> = {
|
||||
}
|
||||
export type Edge = ReactFlowEdge<CommonEdgeType>
|
||||
|
||||
export type WorkflowDataUpdator = {
|
||||
nodes: Node[]
|
||||
edges: Edge[]
|
||||
viewport: Viewport
|
||||
}
|
||||
|
||||
export type ValueSelector = string[] // [nodeId, key | obj key path]
|
||||
|
||||
export type Variable = {
|
||||
|
||||
@@ -79,7 +79,9 @@ const getCycleEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
return cycleEdges
|
||||
}
|
||||
|
||||
export const initialNodes = (nodes: Node[], edges: Edge[]) => {
|
||||
export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
|
||||
const nodes = cloneDeep(originNodes)
|
||||
const edges = cloneDeep(originEdges)
|
||||
const firstNode = nodes[0]
|
||||
|
||||
if (!firstNode?.position) {
|
||||
@@ -121,7 +123,9 @@ export const initialNodes = (nodes: Node[], edges: Edge[]) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const initialEdges = (edges: Edge[], nodes: Node[]) => {
|
||||
export const initialEdges = (originEdges: Edge[], originNodes: Node[]) => {
|
||||
const nodes = cloneDeep(originNodes)
|
||||
const edges = cloneDeep(originEdges)
|
||||
let selectedNode: Node | null = null
|
||||
const nodesMap = nodes.reduce((acc, node) => {
|
||||
acc[node.id] = node
|
||||
|
||||
Reference in New Issue
Block a user