Browse Source

feat: workflow remove preview mode (#3941)

tags/0.6.6
zxhlyh 1 year ago
parent
commit
8e4989ed03
No account linked to committer's email address
33 changed files with 547 additions and 307 deletions
  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

+ 5
- 0
web/app/components/base/icons/assets/vender/line/communication/message-play.svg View File

<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Left Icon">
<path id="Vector" d="M7.83333 2.66683H5.7C4.5799 2.66683 4.01984 2.66683 3.59202 2.88482C3.21569 3.07656 2.90973 3.38252 2.71799 3.75885C2.5 4.18667 2.5 4.74672 2.5 5.86683V9.3335C2.5 9.95348 2.5 10.2635 2.56815 10.5178C2.75308 11.208 3.29218 11.7471 3.98236 11.932C4.2367 12.0002 4.54669 12.0002 5.16667 12.0002V13.5572C5.16667 13.9124 5.16667 14.09 5.23949 14.1812C5.30282 14.2606 5.39885 14.3067 5.50036 14.3066C5.61708 14.3065 5.75578 14.1955 6.03317 13.9736L7.62348 12.7014C7.94834 12.4415 8.11078 12.3115 8.29166 12.2191C8.45213 12.1371 8.62295 12.0772 8.79948 12.041C8.99845 12.0002 9.20646 12.0002 9.6225 12.0002H10.6333C11.7534 12.0002 12.3135 12.0002 12.7413 11.7822C13.1176 11.5904 13.4236 11.2845 13.6153 10.9081C13.8333 10.4803 13.8333 9.92027 13.8333 8.80016V8.66683M11.6551 6.472L14.8021 4.44889C15.0344 4.29958 15.1505 4.22493 15.1906 4.13C15.2257 4.04706 15.2257 3.95347 15.1906 3.87052C15.1505 3.7756 15.0344 3.70094 14.8021 3.55163L11.6551 1.52852C11.3874 1.35646 11.2536 1.27043 11.1429 1.27833C11.0465 1.28522 10.9578 1.33365 10.8998 1.41105C10.8333 1.49987 10.8333 1.65896 10.8333 1.97715V6.02337C10.8333 6.34156 10.8333 6.50066 10.8998 6.58948C10.9578 6.66688 11.0465 6.71531 11.1429 6.72219C11.2536 6.7301 11.3874 6.64407 11.6551 6.472Z" stroke="#155EEF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

+ 39
- 0
web/app/components/base/icons/src/vender/line/communication/MessagePlay.json View File

{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "17",
"height": "16",
"viewBox": "0 0 17 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Left Icon"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector",
"d": "M7.83333 2.66683H5.7C4.5799 2.66683 4.01984 2.66683 3.59202 2.88482C3.21569 3.07656 2.90973 3.38252 2.71799 3.75885C2.5 4.18667 2.5 4.74672 2.5 5.86683V9.3335C2.5 9.95348 2.5 10.2635 2.56815 10.5178C2.75308 11.208 3.29218 11.7471 3.98236 11.932C4.2367 12.0002 4.54669 12.0002 5.16667 12.0002V13.5572C5.16667 13.9124 5.16667 14.09 5.23949 14.1812C5.30282 14.2606 5.39885 14.3067 5.50036 14.3066C5.61708 14.3065 5.75578 14.1955 6.03317 13.9736L7.62348 12.7014C7.94834 12.4415 8.11078 12.3115 8.29166 12.2191C8.45213 12.1371 8.62295 12.0772 8.79948 12.041C8.99845 12.0002 9.20646 12.0002 9.6225 12.0002H10.6333C11.7534 12.0002 12.3135 12.0002 12.7413 11.7822C13.1176 11.5904 13.4236 11.2845 13.6153 10.9081C13.8333 10.4803 13.8333 9.92027 13.8333 8.80016V8.66683M11.6551 6.472L14.8021 4.44889C15.0344 4.29958 15.1505 4.22493 15.1906 4.13C15.2257 4.04706 15.2257 3.95347 15.1906 3.87052C15.1505 3.7756 15.0344 3.70094 14.8021 3.55163L11.6551 1.52852C11.3874 1.35646 11.2536 1.27043 11.1429 1.27833C11.0465 1.28522 10.9578 1.33365 10.8998 1.41105C10.8333 1.49987 10.8333 1.65896 10.8333 1.97715V6.02337C10.8333 6.34156 10.8333 6.50066 10.8998 6.58948C10.9578 6.66688 11.0465 6.71531 11.1429 6.72219C11.2536 6.7301 11.3874 6.64407 11.6551 6.472Z",
"stroke": "currentColor",
"stroke-width": "1.5",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
}
]
},
"name": "MessagePlay"
}

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

// 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 View File

export { default as CuteRobot } from './CuteRobot' export { default as CuteRobot } from './CuteRobot'
export { default as MessageCheckRemove } from './MessageCheckRemove' export { default as MessageCheckRemove } from './MessageCheckRemove'
export { default as MessageFastPlus } from './MessageFastPlus' export { default as MessageFastPlus } from './MessageFastPlus'
export { default as MessagePlay } from './MessagePlay'

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

useEdges, useEdges,
useNodes, useNodes,
} from 'reactflow' } from 'reactflow'
import cn from 'classnames'
import BlockIcon from '../block-icon' import BlockIcon from '../block-icon'
import { import {
useChecklist, useChecklist,
} from '@/app/components/base/icons/src/vender/line/general' } from '@/app/components/base/icons/src/vender/line/general'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback' 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 { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const nodes = useNodes<CommonNodeType>() const nodes = useNodes<CommonNodeType>()
open={open} open={open}
onOpenChange={setOpen} 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 <div
className={` className={`
group flex items-center justify-center w-full h-full rounded-md cursor-pointer group flex items-center justify-center w-full h-full rounded-md cursor-pointer

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

import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useWorkflow } from '../hooks' import { useWorkflow } from '../hooks'
import { Edit03 } from '@/app/components/base/icons/src/vender/solid/general'
import { useStore } from '@/app/components/workflow/store' import { useStore } from '@/app/components/workflow/store'


const EditingTitle = () => { const EditingTitle = () => {


return ( return (
<div className='flex items-center h-[18px] text-xs text-gray-500'> <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 && ( !!draftUpdatedAt && (
<> <>
<span className='flex items-center mx-1'>·</span>
{t('workflow.common.autoSaved')} {dayjs(draftUpdatedAt).format('HH:mm:ss')} {t('workflow.common.autoSaved')} {dayjs(draftUpdatedAt).format('HH:mm:ss')}
</> </>
) )

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

useChecklistBeforePublish, useChecklistBeforePublish,
useNodesReadOnly, useNodesReadOnly,
useNodesSyncDraft, useNodesSyncDraft,
useWorkflowMode,
useWorkflowRun, useWorkflowRun,
} from '../hooks' } from '../hooks'
import AppPublisher from '../../app/app-publisher' import AppPublisher from '../../app/app-publisher'
import EditingTitle from './editing-title' import EditingTitle from './editing-title'
import RunningTitle from './running-title' import RunningTitle from './running-title'
import RestoringTitle from './restoring-title' import RestoringTitle from './restoring-title'
import ViewHistory from './view-history'
import Checklist from './checklist' import Checklist from './checklist'
import { Grid01 } from '@/app/components/base/icons/src/vender/line/layout' import { Grid01 } from '@/app/components/base/icons/src/vender/line/layout'
import Button from '@/app/components/base/button' 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 { useStore as useAppStore } from '@/app/components/app/store'
import { publishWorkflow } from '@/service/workflow' import { publishWorkflow } from '@/service/workflow'
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'


const Header: FC = () => { const Header: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
nodesReadOnly, nodesReadOnly,
getNodesReadOnly, getNodesReadOnly,
} = useNodesReadOnly() } = useNodesReadOnly()
const isRestoring = useStore(s => s.isRestoring)
const publishedAt = useStore(s => s.publishedAt) const publishedAt = useStore(s => s.publishedAt)
const draftUpdatedAt = useStore(s => s.draftUpdatedAt) const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
const { const {
handleLoadBackupDraft, handleLoadBackupDraft,
handleRunSetting,
handleBackupDraft, handleBackupDraft,
handleRestoreFromPublishedWorkflow, handleRestoreFromPublishedWorkflow,
} = useWorkflowRun() } = useWorkflowRun()
const { handleCheckBeforePublish } = useChecklistBeforePublish() const { handleCheckBeforePublish } = useChecklistBeforePublish()
const { handleSyncWorkflowDraft } = useNodesSyncDraft() const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
const {
normal,
restoring,
viewHistory,
} = useWorkflowMode()


const handleShowFeatures = useCallback(() => { const handleShowFeatures = useCallback(() => {
const { const {
setShowFeaturesPanel(true) setShowFeaturesPanel(true)
}, [workflowStore, getNodesReadOnly]) }, [workflowStore, getNodesReadOnly])


const handleGoBackToEdit = useCallback(() => {
handleRunSetting(true)
}, [handleRunSetting])

const handleCancelRestore = useCallback(() => { const handleCancelRestore = useCallback(() => {
handleLoadBackupDraft() handleLoadBackupDraft()
workflowStore.setState({ isRestoring: false }) workflowStore.setState({ isRestoring: false })
handleSyncWorkflowDraft(true) handleSyncWorkflowDraft(true)
}, [handleSyncWorkflowDraft]) }, [handleSyncWorkflowDraft])


const handleGoBackToEdit = useCallback(() => {
handleLoadBackupDraft()
workflowStore.setState({ historyWorkflowData: undefined })
}, [workflowStore, handleLoadBackupDraft])

return ( return (
<div <div
className='absolute top-0 left-0 z-10 flex items-center justify-between w-full px-3 h-14' className='absolute top-0 left-0 z-10 flex items-center justify-between w-full px-3 h-14'
) )
} }
{ {
!nodesReadOnly && !isRestoring && <EditingTitle />
normal && <EditingTitle />
} }
{ {
nodesReadOnly && !isRestoring && <RunningTitle />
viewHistory && <RunningTitle />
} }
{ {
isRestoring && <RestoringTitle />
restoring && <RestoringTitle />
} }
</div> </div>
{ {
!isRestoring && (
normal && (
<div className='flex items-center'> <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 /> <RunAndHistory />
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div> <div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
<Button <Button
className={` className={`
mr-2 px-3 py-0 h-8 bg-white text-[13px] font-medium text-gray-700 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 border-[0.5px] border-gray-200 shadow-xs
${nodesReadOnly && !isRestoring && 'opacity-50 !cursor-not-allowed'}
${nodesReadOnly && 'opacity-50 !cursor-not-allowed'}
`} `}
onClick={handleShowFeatures} onClick={handleShowFeatures}
> >
crossAxisOffset: 53, 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> </div>
) )
} }
{ {
isRestoring && (
restoring && (
<div className='flex items-center'> <div className='flex items-center'>
<Button <Button
className={` className={`

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

import { memo, useCallback } from 'react' import { memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useStoreApi } from 'reactflow' import { useStoreApi } from 'reactflow'
import cn from 'classnames'
import { import {
useStore, useStore,
useWorkflowStore, useWorkflowStore,
} from '../store' } from '../store'
import { import {
useIsChatMode, useIsChatMode,
useNodesReadOnly,
useNodesSyncDraft, useNodesSyncDraft,
useWorkflowInteractions,
useWorkflowRun, useWorkflowRun,
} from '../hooks' } from '../hooks'
import { import {
} from '@/app/components/base/icons/src/vender/line/mediaAndDevices' } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import { Loading02 } from '@/app/components/base/icons/src/vender/line/general' import { Loading02 } from '@/app/components/base/icons/src/vender/line/general'
import { useFeaturesStore } from '@/app/components/base/features/hooks' import { useFeaturesStore } from '@/app/components/base/features/hooks'
import { MessagePlay } from '@/app/components/base/icons/src/vender/line/communication'


const RunMode = memo(() => { const RunMode = memo(() => {
const { t } = useTranslation() const { t } = useTranslation()
const featuresStore = useFeaturesStore() const featuresStore = useFeaturesStore()
const { const {
handleStopRun, handleStopRun,
handleRunSetting,
handleRun, handleRun,
} = useWorkflowRun() } = useWorkflowRun()
const { const {
doSyncWorkflowDraft, doSyncWorkflowDraft,
handleSyncWorkflowDraft,
} = useNodesSyncDraft() } = useNodesSyncDraft()
const workflowRunningData = useStore(s => s.workflowRunningData) const workflowRunningData = useStore(s => s.workflowRunningData)
const showInputsPanel = useStore(s => s.showInputsPanel)
const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running


const handleClick = useCallback(async () => { const handleClick = useCallback(async () => {
const startNode = nodes.find(node => node.data.type === BlockEnum.Start) const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const startVariables = startNode?.data.variables || [] const startVariables = startNode?.data.variables || []
const fileSettings = featuresStore!.getState().features.file const fileSettings = featuresStore!.getState().features.file
const {
setShowDebugAndPreviewPanel,
setShowInputsPanel,
} = workflowStore.getState()


if (!startVariables.length && !fileSettings?.image?.enabled) { if (!startVariables.length && !fileSettings?.image?.enabled) {
await doSyncWorkflowDraft() await doSyncWorkflowDraft()
handleRunSetting()
handleRun({ inputs: {}, files: [] }) handleRun({ inputs: {}, files: [] })
setShowDebugAndPreviewPanel(true)
setShowInputsPanel(false)
} }
else { else {
workflowStore.setState({
historyWorkflowData: undefined,
showInputsPanel: true,
})
handleSyncWorkflowDraft(true)
setShowDebugAndPreviewPanel(true)
setShowInputsPanel(true)
} }
}, [ }, [
workflowStore, workflowStore,
handleSyncWorkflowDraft,
handleRunSetting,
handleRun, handleRun,
doSyncWorkflowDraft, doSyncWorkflowDraft,
store, store,
return ( return (
<> <>
<div <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} onClick={handleClick}
> >
{ {


const PreviewMode = memo(() => { const PreviewMode = memo(() => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleRunSetting } = useWorkflowRun()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { nodesReadOnly } = useNodesReadOnly()
const workflowStore = useWorkflowStore()
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()


const handleClick = () => { const handleClick = () => {
handleSyncWorkflowDraft(true)
handleRunSetting()
const {
showDebugAndPreviewPanel,
setShowDebugAndPreviewPanel,
setHistoryWorkflowData,
} = workflowStore.getState()

if (showDebugAndPreviewPanel)
handleCancelDebugAndPreviewPanel()
else
setShowDebugAndPreviewPanel(true)

setHistoryWorkflowData(undefined)
} }


return ( return (
<div <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> </div>
) )
}) })

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

import { memo } from 'react' import { memo } from 'react'
import { useTranslation } from 'react-i18next' 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 RunningTitle = () => {
const { t } = useTranslation() const { t } = useTranslation()
const appDetail = useAppStore(state => state.appDetail)
const isChatMode = useIsChatMode()
const historyWorkflowData = useStore(s => s.historyWorkflowData)


return ( 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='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> </div>
) )
} }

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

import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
import { import {
useIsChatMode, useIsChatMode,
useNodesInteractions,
useWorkflow, useWorkflow,
useWorkflowInteractions,
useWorkflowRun, useWorkflowRun,
} from '../hooks' } from '../hooks'
import { WorkflowRunningStatus } from '../types' import { WorkflowRunningStatus } from '../types'
useWorkflowStore, useWorkflowStore,
} from '@/app/components/workflow/store' } from '@/app/components/workflow/store'


const ViewHistory = () => {
type ViewHistoryProps = {
withText?: boolean
}
const ViewHistory = ({
withText,
}: ViewHistoryProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const isChatMode = useIsChatMode() const isChatMode = useIsChatMode()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const { formatTimeFromNow } = useWorkflow() const { formatTimeFromNow } = useWorkflow()
const {
handleNodesCancelSelected,
} = useNodesInteractions()
const {
handleCancelDebugAndPreviewPanel,
} = useWorkflowInteractions()
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()
const { appDetail, setCurrentLogItem, setShowMessageLogModal } = useAppStore(useShallow(state => ({ const { appDetail, setCurrentLogItem, setShowMessageLogModal } = useAppStore(useShallow(state => ({
appDetail: state.appDetail, appDetail: state.appDetail,
return ( return (
( (
<PortalToFollowElem <PortalToFollowElem
placement='bottom-end'
placement={withText ? 'bottom-start' : 'bottom-end'}
offset={{ offset={{
mainAxis: 4, mainAxis: 4,
crossAxis: 131,
crossAxis: withText ? -8 : 10,
}} }}
open={open} open={open}
onOpenChange={setOpen} onOpenChange={setOpen}
> >
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}> <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> </PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[12]'> <PortalToFollowElemContent className='z-[12]'>
<div <div
}) })
handleBackupDraft() handleBackupDraft()
setOpen(false) setOpen(false)
handleNodesCancelSelected()
handleCancelDebugAndPreviewPanel()
}} }}
> >
{ {

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

export * from './use-workflow-run' export * from './use-workflow-run'
export * from './use-workflow-template' export * from './use-workflow-template'
export * from './use-checklist' 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 View File

setEdges(newEdges) setEdges(newEdges)
}, [store]) }, [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 { return {
handleEdgeEnter, handleEdgeEnter,
handleEdgeLeave, handleEdgeLeave,
handleEdgeDelete, handleEdgeDelete,
handleEdgesChange, handleEdgesChange,
handleVariableAssignerEdgesChange, handleVariableAssignerEdgesChange,
handleEdgeCancelRunningStatus,
} }
} }

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

}, [store, getNodesReadOnly]) }, [store, getNodesReadOnly])


const handleNodeSelect = useCallback((nodeId: string, cancelSelection?: boolean) => { const handleNodeSelect = useCallback((nodeId: string, cancelSelection?: boolean) => {
if (getNodesReadOnly() && !workflowStore.getState().isRestoring)
return

const { const {
getNodes, getNodes,
setNodes, setNodes,
setEdges(newEdges) setEdges(newEdges)


handleSyncWorkflowDraft() handleSyncWorkflowDraft()
}, [store, handleSyncWorkflowDraft, getNodesReadOnly, workflowStore])
}, [store, handleSyncWorkflowDraft])


const handleNodeClick = useCallback<NodeMouseHandler>((_, node) => { const handleNodeClick = useCallback<NodeMouseHandler>((_, node) => {
if (getNodesReadOnly() && !workflowStore.getState().isRestoring)
return

handleNodeSelect(node.id) handleNodeSelect(node.id)
}, [handleNodeSelect, getNodesReadOnly, workflowStore])
}, [handleNodeSelect])


const handleNodeConnect = useCallback<OnConnect>(({ const handleNodeConnect = useCallback<OnConnect>(({
source, source,
handleNodeDelete(node.id) handleNodeDelete(node.id)
}, [getNodesReadOnly, handleNodeDelete, store, workflowStore]) }, [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 { return {
handleNodeDragStart, handleNodeDragStart,
handleNodeDrag, handleNodeDrag,
handleNodeCut, handleNodeCut,
handleNodeDeleteSelected, handleNodeDeleteSelected,
handleNodePaste, handleNodePaste,
handleNodeCancelRunningStatus,
handleNodesCancelSelected,
} }
} }

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

}, [store, featuresStore, workflowStore]) }, [store, featuresStore, workflowStore])


const syncWorkflowDraftWhenPageClose = useCallback(() => { const syncWorkflowDraftWhenPageClose = useCallback(() => {
if (getNodesReadOnly())
return
const postParams = getPostParams() const postParams = getPostParams()


if (postParams) { if (postParams) {
JSON.stringify(postParams.params), JSON.stringify(postParams.params),
) )
} }
}, [getPostParams, params.appId])
}, [getPostParams, params.appId, getNodesReadOnly])


const doSyncWorkflowDraft = useCallback(async (appId?: string) => { const doSyncWorkflowDraft = useCallback(async (appId?: string) => {
if (getNodesReadOnly())
return
const postParams = getPostParams(appId) const postParams = getPostParams(appId)


if (postParams) { if (postParams) {
const res = await syncWorkflowDraft(postParams) const res = await syncWorkflowDraft(postParams)
workflowStore.getState().setDraftUpdatedAt(res.updated_at) workflowStore.getState().setDraftUpdatedAt(res.updated_at)
} }
}, [workflowStore, getPostParams])
}, [workflowStore, getPostParams, getNodesReadOnly])


const handleSyncWorkflowDraft = useCallback((sync?: boolean, appId?: string) => { const handleSyncWorkflowDraft = useCallback((sync?: boolean, appId?: string) => {
if (getNodesReadOnly()) if (getNodesReadOnly())

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

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 View File

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 View File

} from 'reactflow' } from 'reactflow'
import produce from 'immer' import produce from 'immer'
import { useWorkflowStore } from '../store' import { useWorkflowStore } from '../store'
import { useNodesSyncDraft } from '../hooks'
import { import {
NodeRunningStatus, NodeRunningStatus,
WorkflowRunningStatus, WorkflowRunningStatus,
} from '../types' } from '../types'
import { useWorkflow } from './use-workflow'
import { useWorkflowInteractions } from './use-workflow-interactions'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import type { IOtherOptions } from '@/service/base' import type { IOtherOptions } from '@/service/base'
import { ssePost } from '@/service/base' import { ssePost } from '@/service/base'
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()
const reactflow = useReactFlow() const reactflow = useReactFlow()
const featuresStore = useFeaturesStore() const featuresStore = useFeaturesStore()
const { renderTreeFromRecord } = useWorkflow()
const { doSyncWorkflowDraft } = useNodesSyncDraft()
const { handleUpdateWorkflowCanvas } = useWorkflowInteractions()


const handleBackupDraft = useCallback(() => { const handleBackupDraft = useCallback(() => {
const { const {
viewport: getViewport(), viewport: getViewport(),
features, features,
}) })
doSyncWorkflowDraft()
} }
}, [reactflow, workflowStore, store, featuresStore])
}, [reactflow, workflowStore, store, featuresStore, doSyncWorkflowDraft])


const handleLoadBackupDraft = useCallback(() => { const handleLoadBackupDraft = useCallback(() => {
const {
setNodes,
setEdges,
} = store.getState()
const { setViewport } = reactflow
const { const {
backupDraft, backupDraft,
setBackupDraft, setBackupDraft,
viewport, viewport,
features, features,
} = backupDraft } = backupDraft
setNodes(nodes)
setEdges(edges)
setViewport(viewport)
handleUpdateWorkflowCanvas({
nodes,
edges,
viewport,
})
featuresStore!.setState({ features }) featuresStore!.setState({ features })
setBackupDraft(undefined) 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 { const {
setNodes,
getNodes, getNodes,
edges,
setEdges,
setNodes,
} = store.getState() } = 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 { const {
onWorkflowStarted, onWorkflowStarted,
onWorkflowFinished, onWorkflowFinished,
let prevNodeId = '' let prevNodeId = ''


const { const {
workflowRunningData,
setWorkflowRunningData, setWorkflowRunningData,
} = workflowStore.getState() } = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.result = {
...draft?.result,
setWorkflowRunningData({
result: {
status: WorkflowRunningStatus.Running, status: WorkflowRunningStatus.Running,
}
}))
},
tracing: [],
})


ssePost( ssePost(
url, url,
setWorkflowRunningData, setWorkflowRunningData,
} = workflowStore.getState() } = workflowStore.getState()
const { const {
getNodes,
setNodes,
edges, edges,
setEdges, setEdges,
} = store.getState() } = store.getState()
} }
})) }))


const newNodes = produce(getNodes(), (draft) => {
draft.forEach((node) => {
node.data._runningStatus = NodeRunningStatus.Waiting
})
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => { const newEdges = produce(edges, (draft) => {
draft.forEach((edge) => { draft.forEach((edge) => {
edge.data = { edge.data = {
setNodes, setNodes,
edges, edges,
setEdges, setEdges,
transform,
} = store.getState() } = store.getState()
const nodes = getNodes() const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => { setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id) const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id)
const currentNode = nodes[currentNodeIndex] const currentNode = nodes[currentNodeIndex]
const position = currentNode.position const position = currentNode.position
const zoom = 1
const zoom = transform[2]


setViewport({ 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) => { const newNodes = produce(nodes, (draft) => {
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
...restCallback, ...restCallback,
}, },
) )
}, [store, reactflow, workflowStore])
}, [store, reactflow, workflowStore, doSyncWorkflowDraft])


const handleStopRun = useCallback((taskId: string) => { const handleStopRun = useCallback((taskId: string) => {
const appId = useAppStore.getState().appDetail?.id const appId = useAppStore.getState().appDetail?.id
if (publishedWorkflow) { if (publishedWorkflow) {
const nodes = publishedWorkflow.graph.nodes const nodes = publishedWorkflow.graph.nodes
const edges = publishedWorkflow.graph.edges 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 }) featuresStore?.setState({ features: publishedWorkflow.features })
workflowStore.getState().setPublishedAt(publishedWorkflow.created_at) workflowStore.getState().setPublishedAt(publishedWorkflow.created_at)
} }
}, [featuresStore, workflowStore, renderTreeFromRecord])
}, [featuresStore, handleUpdateWorkflowCanvas, workflowStore])


return { return {
handleBackupDraft, handleBackupDraft,
handleLoadBackupDraft, handleLoadBackupDraft,
handleRunSetting,
handleRun, handleRun,
handleStopRun, handleStopRun,
handleRestoreFromPublishedWorkflow, handleRestoreFromPublishedWorkflow,

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

} from 'reactflow' } from 'reactflow'
import type { import type {
Connection, Connection,
Viewport,
} from 'reactflow' } from 'reactflow'
import { import {
getLayoutByDagre, getLayoutByDagre,
initialEdges,
initialNodes,
} from '../utils' } from '../utils'
import type { import type {
Edge,
Node, Node,
ValueSelector, ValueSelector,
} from '../types' } from '../types'
import { import {
AUTO_LAYOUT_OFFSET, AUTO_LAYOUT_OFFSET,
SUPPORT_OUTPUT_VARS_NODE, SUPPORT_OUTPUT_VARS_NODE,
WORKFLOW_DATA_UPDATE,
} from '../constants' } from '../constants'
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils' import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
import { useNodesExtraData } from './use-nodes-data' import { useNodesExtraData } from './use-nodes-data'
fetchAllCustomTools, fetchAllCustomTools,
} from '@/service/tools' } from '@/service/tools'
import I18n from '@/context/i18n' import I18n from '@/context/i18n'
import { useEventEmitterContextContext } from '@/context/event-emitter'


export const useIsChatMode = () => { export const useIsChatMode = () => {
const appDetail = useAppStore(s => s.appDetail) const appDetail = useAppStore(s => s.appDetail)
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()
const nodesExtraData = useNodesExtraData() const nodesExtraData = useNodesExtraData()
const { handleSyncWorkflowDraft } = useNodesSyncDraft() const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { eventEmitter } = useEventEmitterContextContext()


const setPanelWidth = useCallback((width: number) => { const setPanelWidth = useCallback((width: number) => {
localStorage.setItem('workflow-node-panel-width', `${width}`) localStorage.setItem('workflow-node-panel-width', `${width}`)
return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow() return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow()
}, [locale]) }, [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 getNode = useCallback((nodeId?: string) => {
const { getNodes } = store.getState() const { getNodes } = store.getState()
const nodes = getNodes() const nodes = getNodes()
isNodeVarsUsedInNodes, isNodeVarsUsedInNodes,
isValidConnection, isValidConnection,
formatTimeFromNow, formatTimeFromNow,
renderTreeFromRecord,
getNode, getNode,
getBeforeNodeById, getBeforeNodeById,
enableShortcuts, enableShortcuts,
isRestoring, isRestoring,
} = workflowStore.getState() } = workflowStore.getState()


return workflowRunningData || historyWorkflowData || isRestoring
return workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring
}, [workflowStore]) }, [workflowStore])


return { return {
nodesReadOnly: !!(workflowRunningData || historyWorkflowData || isRestoring),
nodesReadOnly: !!(workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring),
getNodesReadOnly, getNodesReadOnly,
} }
} }

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

value: any value: any
onChange: (value: any) => void onChange: (value: any) => void
className?: string className?: string
autoFocus?: boolean
} }


const FormItem: FC<Props> = ({ const FormItem: FC<Props> = ({
value, value,
onChange, onChange,
className, className,
autoFocus,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { type } = payload const { type } = payload
value={value || ''} value={value || ''}
onChange={e => onChange(e.target.value)} onChange={e => onChange(e.target.value)}
placeholder={t('appDebug.variableConig.inputPlaceholder')!} placeholder={t('appDebug.variableConig.inputPlaceholder')!}
autoFocus={autoFocus}
/> />
) )
} }
value={value || ''} value={value || ''}
onChange={e => onChange(e.target.value)} onChange={e => onChange(e.target.value)}
placeholder={t('appDebug.variableConig.inputPlaceholder')!} placeholder={t('appDebug.variableConig.inputPlaceholder')!}
autoFocus={autoFocus}
/> />
) )
} }
value={value || ''} value={value || ''}
onChange={e => onChange(e.target.value)} onChange={e => onChange(e.target.value)}
placeholder={t('appDebug.variableConig.inputPlaceholder')!} placeholder={t('appDebug.variableConig.inputPlaceholder')!}
autoFocus={autoFocus}
/> />
) )
} }
type === InputVarType.files && ( type === InputVarType.files && (
<TextGenerationImageUploader <TextGenerationImageUploader
settings={{ settings={{
...fileSettings.image,
...fileSettings?.image,
detail: Resolution.high, detail: Resolution.high,
}}
} as any}
onFilesChange={files => onChange(files.filter(file => file.progress !== -1).map(fileItem => ({ onFilesChange={files => onChange(files.filter(file => file.progress !== -1).map(fileItem => ({
type: 'image', type: 'image',
transfer_method: fileItem.type, transfer_method: fileItem.type,

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

import { import {
cloneElement, cloneElement,
memo, memo,
useMemo,
} from 'react' } from 'react'
import type { NodeProps } from '../../types' import type { NodeProps } from '../../types'
import { import {
}) => { }) => {
const { nodesReadOnly } = useNodesReadOnly() const { nodesReadOnly } = useNodesReadOnly()
const toolIcon = useToolIcon(data) 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 ( return (
<div <div
className={` className={`
flex border-[2px] rounded-2xl 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 <div
group relative pb-1 w-[240px] bg-[#fcfdff] shadow-xs group relative pb-1 w-[240px] bg-[#fcfdff] shadow-xs
border border-transparent rounded-[15px] border border-transparent rounded-[15px]
${!data._runningStatus && 'hover:shadow-lg'} ${!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._isInvalidConnection && '!border-[#F04438]'}
`} `}
> >
{ {
data.type !== BlockEnum.VariableAssigner && !data._runningStatus && (
data.type !== BlockEnum.VariableAssigner && (
<NodeTargetHandle <NodeTargetHandle
id={id} id={id}
data={data} data={data}
) )
} }
{ {
data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && !data._runningStatus && (
data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && (
<NodeSourceHandle <NodeSourceHandle
id={id} id={id}
data={data} data={data}

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

memo, memo,
useCallback, useCallback,
} from 'react' } from 'react'
import cn from 'classnames'
import { useShallow } from 'zustand/react/shallow'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import NextStep from './components/next-step' import NextStep from './components/next-step'
import PanelOperator from './components/panel-operator' import PanelOperator from './components/panel-operator'
import { Play } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' import { Play } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import TooltipPlus from '@/app/components/base/tooltip-plus' import TooltipPlus from '@/app/components/base/tooltip-plus'
import type { Node } from '@/app/components/workflow/types' import type { Node } from '@/app/components/workflow/types'
import { useStore as useAppStore } from '@/app/components/app/store'


type BasePanelProps = { type BasePanelProps = {
children: ReactElement children: ReactElement
children, children,
}) => { }) => {
const { t } = useTranslation() 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 panelWidth = localStorage.getItem('workflow-node-panel-width') ? parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420
const { const {
setPanelWidth, setPanelWidth,
}, [handleNodeDataUpdateWithSyncDraft, id]) }, [handleNodeDataUpdateWithSyncDraft, id])


return ( 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 <div
ref={triggerRef} ref={triggerRef}
className='absolute top-1/2 -translate-y-1/2 -left-2 w-3 h-6 cursor-col-resize resize-x'> 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 View File

useMemo, useMemo,
useState, useState,
} from 'react' } from 'react'
import { useStore } from '../../store'
import {
useStore,
useWorkflowStore,
} from '../../store'
import { useWorkflowRun } from '../../hooks'
import UserInput from './user-input' import UserInput from './user-input'
import Chat from '@/app/components/base/chat/chat' import Chat from '@/app/components/base/chat/chat'
import type { ChatItem } from '@/app/components/base/chat/types' import type { ChatItem } from '@/app/components/base/chat/types'
import { fetchConvesationMessages } from '@/service/debug' import { fetchConvesationMessages } from '@/service/debug'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { XClose } from '@/app/components/base/icons/src/vender/line/general'


const ChatRecord = () => { const ChatRecord = () => {
const [fetched, setFetched] = useState(false) const [fetched, setFetched] = useState(false)
const [chatList, setChatList] = useState([]) const [chatList, setChatList] = useState([])
const appDetail = useAppStore(s => s.appDetail) const appDetail = useAppStore(s => s.appDetail)
const workflowStore = useWorkflowStore()
const { handleLoadBackupDraft } = useWorkflowRun()
const historyWorkflowData = useStore(s => s.historyWorkflowData) const historyWorkflowData = useStore(s => s.historyWorkflowData)
const currentConversationID = historyWorkflowData?.conversation_id const currentConversationID = historyWorkflowData?.conversation_id


<> <>
<div className='shrink-0 flex items-center justify-between p-4 pb-1 text-base font-semibold text-gray-900'> <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}`} {`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>
<div className='grow h-0'> <div className='grow h-0'>
<Chat <Chat

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

useRef, useRef,
} from 'react' } from 'react'
import { useKeyPress } from 'ahooks' import { useKeyPress } from 'ahooks'
import cn from 'classnames'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import {
useEdgesInteractions,
useNodesInteractions,
useWorkflowInteractions,
} from '../../hooks'
import ChatWrapper from './chat-wrapper' import ChatWrapper from './chat-wrapper'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows' 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 = { export type ChatWrapperRefType = {
handleRestart: () => void handleRestart: () => void
const DebugAndPreview = () => { const DebugAndPreview = () => {
const { t } = useTranslation() const { t } = useTranslation()
const chatRef = useRef({ handleRestart: () => {} }) const chatRef = useRef({ handleRestart: () => {} })
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
const { handleNodeCancelRunningStatus } = useNodesInteractions()
const { handleEdgeCancelRunningStatus } = useEdgesInteractions()


useKeyPress('shift.r', () => {
const handleRestartChat = () => {
handleNodeCancelRunningStatus()
handleEdgeCancelRunningStatus()
chatRef.current.handleRestart() chatRef.current.handleRestart()
}

useKeyPress('shift.r', () => {
handleRestartChat()
}, { }, {
exactMatch: true, exactMatch: true,
}) })


return ( return (
<div <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={{ style={{
background: 'linear-gradient(156deg, rgba(242, 244, 247, 0.80) 0%, rgba(242, 244, 247, 0.00) 99.43%), var(--white, #FFF)', 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()} {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>
<div className='grow rounded-b-2xl overflow-y-auto'> <div className='grow rounded-b-2xl overflow-y-auto'>
<ChatWrapper ref={chatRef} /> <ChatWrapper ref={chatRef} />

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

expanded && ( expanded && (
<div className='py-2 text-[13px] text-gray-900'> <div className='py-2 text-[13px] text-gray-900'>
{ {
variables.map(variable => (
variables.map((variable, index) => (
<div <div
key={variable.variable} key={variable.variable}
className='mb-2 last-of-type:mb-0' className='mb-2 last-of-type:mb-0'
> >
<FormItem <FormItem
autoFocus={index === 0}
payload={variable} payload={variable}
value={inputs[variable.variable]} value={inputs[variable.variable]}
onChange={v => handleValueChange(variable.variable, v)} onChange={v => handleValueChange(variable.variable, v)}

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

import type { FC } from 'react' import type { FC } from 'react'
import {
memo,
useMemo,
} from 'react'
import { memo } from 'react'
import { useNodes } from 'reactflow' import { useNodes } from 'reactflow'
import cn from 'classnames'
import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
import type { CommonNodeType } from '../types' import type { CommonNodeType } from '../types'
import { Panel as NodePanel } from '../nodes' import { Panel as NodePanel } from '../nodes'
const nodes = useNodes<CommonNodeType>() const nodes = useNodes<CommonNodeType>()
const isChatMode = useIsChatMode() const isChatMode = useIsChatMode()
const selectedNode = nodes.find(node => node.data.selected) 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 historyWorkflowData = useStore(s => s.historyWorkflowData)
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
const isRestoring = useStore(s => s.isRestoring) const isRestoring = useStore(s => s.isRestoring)
const { const {
enableShortcuts, enableShortcuts,
showMessageLogModal: state.showMessageLogModal, showMessageLogModal: state.showMessageLogModal,
setShowMessageLogModal: state.setShowMessageLogModal, 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 ( return (
<div <div
tabIndex={-1} 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} onFocus={disableShortcuts}
onBlur={enableShortcuts} onBlur={enableShortcuts}
key={`${isRestoring}`} key={`${isRestoring}`}
/> />
) )
} }
{
!!selectedNode && (
<NodePanel {...selectedNode!} />
)
}
{ {
historyWorkflowData && !isChatMode && ( historyWorkflowData && !isChatMode && (
<Record /> <Record />
) )
} }
{ {
showDebugAndPreviewPanel && (
showDebugAndPreviewPanel && isChatMode && (
<DebugAndPreview /> <DebugAndPreview />
) )
} }
{ {
showWorkflowPreview && (
showDebugAndPreviewPanel && !isChatMode && (
<WorkflowPreview /> <WorkflowPreview />
) )
} }
{
showNodePanel && (
<NodePanel {...selectedNode!} />
)
}
</div> </div>
) )
} }

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

const workflowRunningData = useStore(s => s.workflowRunningData) const workflowRunningData = useStore(s => s.workflowRunningData)
const { const {
handleRun, handleRun,
handleRunSetting,
} = useWorkflowRun() } = useWorkflowRun()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start) const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const startVariables = startNode?.data.variables const startVariables = startNode?.data.variables


const doRun = () => { const doRun = () => {
onRun() onRun()
handleRunSetting()
handleRun({ inputs, files }) handleRun({ inputs, files })
} }


<> <>
<div className='px-4 pb-2'> <div className='px-4 pb-2'>
{ {
variables.map(variable => (
variables.map((variable, index) => (
<div <div
key={variable.variable} key={variable.variable}
className='mb-2 last-of-type:mb-0' className='mb-2 last-of-type:mb-0'
> >
<FormItem <FormItem
autoFocus={index === 0}
className='!block' className='!block'
payload={variable} payload={variable}
value={inputs[variable.variable]} value={inputs[variable.variable]}

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

import { memo } from 'react'
import { memo, useCallback } from 'react'
import type { WorkflowDataUpdator } from '../types'
import Run from '../run' import Run from '../run'
import { useStore } from '../store' import { useStore } from '../store'
import { useWorkflowInteractions } from '../hooks'


const Record = () => { const Record = () => {
const historyWorkflowData = useStore(s => s.historyWorkflowData) 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 ( 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 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'> <div className='flex items-center justify-between p-4 pb-1 text-base font-semibold text-gray-900'>
{`Test Run#${historyWorkflowData?.sequence_number}`} {`Test Run#${historyWorkflowData?.sequence_number}`}
</div> </div>
<Run runID={historyWorkflowData?.id || ''} />
<Run
runID={historyWorkflowData?.id || ''}
getResultCallback={handleResultCallback}
/>
</div> </div>
) )
} }

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

import ResultPanel from '../run/result-panel' import ResultPanel from '../run/result-panel'
import TracingPanel from '../run/tracing-panel' import TracingPanel from '../run/tracing-panel'
import { import {
useWorkflowRun,
useWorkflowInteractions,
} from '../hooks' } from '../hooks'
import { useStore } from '../store' import { useStore } from '../store'
import { import {


const WorkflowPreview = () => { const WorkflowPreview = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleRunSetting } = useWorkflowRun()
const showInputsPanel = useStore(s => s.showInputsPanel)
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
const workflowRunningData = useStore(s => s.workflowRunningData) 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 [currentTab, setCurrentTab] = useState<string>(showInputsPanel ? 'INPUT' : 'TRACING')


const switchTab = async (tab: string) => { const switchTab = async (tab: string) => {
const [height, setHieght] = useState(0) const [height, setHieght] = useState(0)
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)


useEffect(() => {
if (showDebugAndPreviewPanel && showInputsPanel)
setCurrentTab('INPUT')
}, [showDebugAndPreviewPanel, showInputsPanel])

const adjustResultHeight = () => { const adjustResultHeight = () => {
if (ref.current) if (ref.current)
setHieght(ref.current?.clientHeight - 16 - 16 - 2 - 1) setHieght(ref.current?.clientHeight - 16 - 16 - 2 - 1)
`}> `}>
<div className='flex items-center justify-between p-4 pb-1 text-base font-semibold text-gray-900'> <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}`}`} {`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>
<div className='grow relative flex flex-col'> <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)]'> <div className='shrink-0 flex items-center px-4 border-b-[0.5px] border-[rgba(0,0,0,0.05)]'>
'grow bg-white h-0 overflow-y-auto rounded-b-2xl', 'grow bg-white h-0 overflow-y-auto rounded-b-2xl',
(currentTab === 'RESULT' || currentTab === 'TRACING') && '!bg-gray-50', (currentTab === 'RESULT' || currentTab === 'TRACING') && '!bg-gray-50',
)}> )}>
{currentTab === 'INPUT' && (
{currentTab === 'INPUT' && showInputsPanel && (
<InputsPanel onRun={() => switchTab('RESULT')} /> <InputsPanel onRun={() => switchTab('RESULT')} />
)} )}
{currentTab === 'RESULT' && ( {currentTab === 'RESULT' && (

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

appId: string appId: string
panelWidth: number panelWidth: number
workflowRunningData?: WorkflowRunningData workflowRunningData?: WorkflowRunningData
setWorkflowRunningData: (workflowData: WorkflowRunningData) => void
setWorkflowRunningData: (workflowData?: WorkflowRunningData) => void
historyWorkflowData?: HistoryWorkflowData historyWorkflowData?: HistoryWorkflowData
setHistoryWorkflowData: (historyWorkflowData: HistoryWorkflowData) => void
setHistoryWorkflowData: (historyWorkflowData?: HistoryWorkflowData) => void
showRunHistory: boolean showRunHistory: boolean
setShowRunHistory: (showRunHistory: boolean) => void setShowRunHistory: (showRunHistory: boolean) => void
showFeaturesPanel: boolean showFeaturesPanel: boolean
setClipboardElements: (clipboardElements: Node[]) => void setClipboardElements: (clipboardElements: Node[]) => void
shortcutsDisabled: boolean shortcutsDisabled: boolean
setShortcutsDisabled: (shortcutsDisabled: boolean) => void setShortcutsDisabled: (shortcutsDisabled: boolean) => void
showDebugAndPreviewPanel: boolean
setShowDebugAndPreviewPanel: (showDebugAndPreviewPanel: boolean) => void
} }


export const createWorkflowStore = () => { export const createWorkflowStore = () => {
setClipboardElements: clipboardElements => set(() => ({ clipboardElements })), setClipboardElements: clipboardElements => set(() => ({ clipboardElements })),
shortcutsDisabled: false, shortcutsDisabled: false,
setShortcutsDisabled: shortcutsDisabled => set(() => ({ shortcutsDisabled })), setShortcutsDisabled: shortcutsDisabled => set(() => ({ shortcutsDisabled })),
showDebugAndPreviewPanel: false,
setShowDebugAndPreviewPanel: showDebugAndPreviewPanel => set(() => ({ showDebugAndPreviewPanel })),
})) }))
} }



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

import type { import type {
Edge as ReactFlowEdge, Edge as ReactFlowEdge,
Node as ReactFlowNode, Node as ReactFlowNode,
Viewport,
} from 'reactflow' } from 'reactflow'
import type { TransferMethod } from '@/types/app' import type { TransferMethod } from '@/types/app'
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types' import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
} }
export type Edge = ReactFlowEdge<CommonEdgeType> 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 ValueSelector = string[] // [nodeId, key | obj key path]


export type Variable = { export type Variable = {

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

return cycleEdges 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] const firstNode = nodes[0]


if (!firstNode?.position) { if (!firstNode?.position) {
}) })
} }


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 let selectedNode: Node | null = null
const nodesMap = nodes.reduce((acc, node) => { const nodesMap = nodes.reduce((acc, node) => {
acc[node.id] = node acc[node.id] = node

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

processData: 'Process Data', processData: 'Process Data',
input: 'Input', input: 'Input',
output: 'Output', output: 'Output',
viewOnly: 'View Only',
showRunHistory: 'Show Run History',
}, },
errorMsg: { errorMsg: {
fieldRequired: '{{field}} is required', fieldRequired: '{{field}} is required',

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

processData: '数据处理', processData: '数据处理',
input: '输入', input: '输入',
output: '输出', output: '输出',
viewOnly: '只读',
showRunHistory: '显示运行历史',
}, },
errorMsg: { errorMsg: {
fieldRequired: '{{field}} 不能为空', fieldRequired: '{{field}} 不能为空',

Loading…
Cancel
Save