Explorar el Código

feat: workflow remove preview mode (#3941)

zxhlyh hace 1 año
padre
commit
8e4989ed03
Se han modificado 33 ficheros con 547 adiciones y 307 borrados
  1. 5 0
      web/app/components/base/icons/assets/vender/line/communication/message-play.svg
  2. 39 0
      web/app/components/base/icons/src/vender/line/communication/MessagePlay.json
  3. 16 0
      web/app/components/base/icons/src/vender/line/communication/MessagePlay.tsx
  4. 1 0
      web/app/components/base/icons/src/vender/line/communication/index.ts
  5. 14 3
      web/app/components/workflow/header/checklist.tsx
  6. 0 4
      web/app/components/workflow/header/editing-title.tsx
  7. 40 35
      web/app/components/workflow/header/index.tsx
  8. 37 43
      web/app/components/workflow/header/run-and-history.tsx
  9. 11 11
      web/app/components/workflow/header/running-title.tsx
  10. 52 19
      web/app/components/workflow/header/view-history.tsx
  11. 2 0
      web/app/components/workflow/hooks/index.ts
  12. 15 0
      web/app/components/workflow/hooks/use-edges-interactions.ts
  13. 34 8
      web/app/components/workflow/hooks/use-nodes-interactions.ts
  14. 6 2
      web/app/components/workflow/hooks/use-nodes-sync-draft.ts
  15. 50 0
      web/app/components/workflow/hooks/use-workflow-interactions.ts
  16. 14 0
      web/app/components/workflow/hooks/use-workflow-mode.ts
  17. 41 80
      web/app/components/workflow/hooks/use-workflow-run.ts
  18. 2 27
      web/app/components/workflow/hooks/use-workflow.ts
  19. 7 2
      web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx
  20. 20 7
      web/app/components/workflow/nodes/_base/node.tsx
  21. 10 1
      web/app/components/workflow/nodes/_base/panel.tsx
  22. 17 1
      web/app/components/workflow/panel/chat-record/index.tsx
  23. 44 14
      web/app/components/workflow/panel/debug-and-preview/index.tsx
  24. 2 1
      web/app/components/workflow/panel/debug-and-preview/user-input.tsx
  25. 13 31
      web/app/components/workflow/panel/index.tsx
  26. 2 3
      web/app/components/workflow/panel/inputs-panel.tsx
  27. 17 2
      web/app/components/workflow/panel/record.tsx
  28. 13 9
      web/app/components/workflow/panel/workflow-preview.tsx
  29. 6 2
      web/app/components/workflow/store.ts
  30. 7 0
      web/app/components/workflow/types.ts
  31. 6 2
      web/app/components/workflow/utils.ts
  32. 2 0
      web/i18n/en-US/workflow.ts
  33. 2 0
      web/i18n/zh-Hans/workflow.ts

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
web/app/components/base/icons/assets/vender/line/communication/message-play.svg


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 39 - 0
web/app/components/base/icons/src/vender/line/communication/MessagePlay.json


+ 16 - 0
web/app/components/base/icons/src/vender/line/communication/MessagePlay.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './MessagePlay.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'MessagePlay'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/vender/line/communication/index.ts

@@ -4,3 +4,4 @@ export { default as ChatBot } from './ChatBot'
 export { default as CuteRobot } from './CuteRobot'
 export { default as MessageCheckRemove } from './MessageCheckRemove'
 export { default as MessageFastPlus } from './MessageFastPlus'
+export { default as MessagePlay } from './MessagePlay'

+ 14 - 3
web/app/components/workflow/header/checklist.tsx

@@ -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 

+ 0 - 4
web/app/components/workflow/header/editing-title.tsx

@@ -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')}
           </>
         )

+ 40 - 35
web/app/components/workflow/header/index.tsx

@@ -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>
+        )
+      }
+      {
+        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>
         )
       }
       {
-        isRestoring && (
+        restoring && (
           <div className='flex items-center'>
             <Button
               className={`

+ 37 - 43
web/app/components/workflow/header/run-and-history.tsx

@@ -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>
   )
 })

+ 11 - 11
web/app/components/workflow/header/running-title.tsx

@@ -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>
   )
 }

+ 52 - 19
web/app/components/workflow/header/view-history.tsx

@@ -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()
                         }}
                       >
                         {

+ 2 - 0
web/app/components/workflow/hooks/index.ts

@@ -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'

+ 15 - 0
web/app/components/workflow/hooks/use-edges-interactions.ts

@@ -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,
   }
 }

+ 34 - 8
web/app/components/workflow/hooks/use-nodes-interactions.ts

@@ -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,
   }
 }

+ 6 - 2
web/app/components/workflow/hooks/use-nodes-sync-draft.ts

@@ -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())

+ 50 - 0
web/app/components/workflow/hooks/use-workflow-interactions.ts

@@ -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 - 0
web/app/components/workflow/hooks/use-workflow-mode.ts

@@ -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])
+}

+ 41 - 80
web/app/components/workflow/hooks/use-workflow-run.ts

@@ -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,16 +47,12 @@ 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,
     } = workflowStore.getState()
@@ -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])
-
-  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: [],
-        },
-      })
-    }
+  }, [handleUpdateWorkflowCanvas, workflowStore, featuresStore])
 
+  const handleRun = useCallback(async (
+    params: any,
+    callback?: IOtherOptions,
+  ) => {
     const {
-      setNodes,
       getNodes,
-      edges,
-      setEdges,
+      setNodes,
     } = 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
-        })
+    const newNodes = produce(getNodes(), (draft) => {
+      draft.forEach((node) => {
+        node.data.selected = false
       })
-      setEdges(newEdges)
-    }
-  }, [store, handleLoadBackupDraft, handleBackupDraft, workflowStore])
+    })
+    setNodes(newNodes)
+    await doSyncWorkflowDraft()
 
-  const handleRun = useCallback((
-    params: any,
-    callback?: IOtherOptions,
-  ) => {
     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,

+ 2 - 27
web/app/components/workflow/hooks/use-workflow.ts

@@ -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,
   }
 }

+ 7 - 2
web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx

@@ -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,

+ 20 - 7
web/app/components/workflow/nodes/_base/node.tsx

@@ -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}

+ 10 - 1
web/app/components/workflow/nodes/_base/panel.tsx

@@ -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'>

+ 17 - 1
web/app/components/workflow/panel/chat-record/index.tsx

@@ -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

+ 44 - 14
web/app/components/workflow/panel/debug-and-preview/index.tsx

@@ -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()
 
-  useKeyPress('shift.r', () => {
+  const handleRestartChat = () => {
+    handleNodeCancelRunningStatus()
+    handleEdgeCancelRunningStatus()
     chatRef.current.handleRestart()
+  }
+
+  useKeyPress('shift.r', () => {
+    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} />

+ 2 - 1
web/app/components/workflow/panel/debug-and-preview/user-input.tsx

@@ -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)}

+ 13 - 31
web/app/components/workflow/panel/index.tsx

@@ -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}`}
@@ -77,6 +59,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>
   )
 }

+ 2 - 3
web/app/components/workflow/panel/inputs-panel.tsx

@@ -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]}

+ 17 - 2
web/app/components/workflow/panel/record.tsx

@@ -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>
   )
 }

+ 13 - 9
web/app/components/workflow/panel/workflow-preview.tsx

@@ -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' && (

+ 6 - 2
web/app/components/workflow/store.ts

@@ -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 })),
   }))
 }
 

+ 7 - 0
web/app/components/workflow/types.ts

@@ -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 = {

+ 6 - 2
web/app/components/workflow/utils.ts

@@ -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

+ 2 - 0
web/i18n/en-US/workflow.ts

@@ -49,6 +49,8 @@ const translation = {
     processData: 'Process Data',
     input: 'Input',
     output: 'Output',
+    viewOnly: 'View Only',
+    showRunHistory: 'Show Run History',
   },
   errorMsg: {
     fieldRequired: '{{field}} is required',

+ 2 - 0
web/i18n/zh-Hans/workflow.ts

@@ -49,6 +49,8 @@ const translation = {
     processData: '数据处理',
     input: '输入',
     output: '输出',
+    viewOnly: '只读',
+    showRunHistory: '显示运行历史',
   },
   errorMsg: {
     fieldRequired: '{{field}} 不能为空',