feat: workflow remove preview mode (#3941)

This commit is contained in:
zxhlyh 2024-04-28 17:09:56 +08:00 committed by GitHub
parent 0940f01634
commit 8e4989ed03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 549 additions and 309 deletions

View File

@ -0,0 +1,5 @@
<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>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,39 @@
{
"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"
}

View File

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

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@ import {
useChecklistBeforePublish,
useNodesReadOnly,
useNodesSyncDraft,
useWorkflowMode,
useWorkflowRun,
} from '../hooks'
import AppPublisher from '../../app/app-publisher'
@ -21,12 +22,13 @@ import RunAndHistory from './run-and-history'
import EditingTitle from './editing-title'
import RunningTitle from './running-title'
import RestoringTitle from './restoring-title'
import ViewHistory from './view-history'
import Checklist from './checklist'
import { Grid01 } from '@/app/components/base/icons/src/vender/line/layout'
import Button from '@/app/components/base/button'
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
import { useStore as useAppStore } from '@/app/components/app/store'
import { publishWorkflow } from '@/service/workflow'
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
const Header: FC = () => {
const { t } = useTranslation()
@ -38,18 +40,21 @@ const Header: FC = () => {
nodesReadOnly,
getNodesReadOnly,
} = useNodesReadOnly()
const isRestoring = useStore(s => s.isRestoring)
const publishedAt = useStore(s => s.publishedAt)
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
const {
handleLoadBackupDraft,
handleRunSetting,
handleBackupDraft,
handleRestoreFromPublishedWorkflow,
} = useWorkflowRun()
const { handleCheckBeforePublish } = useChecklistBeforePublish()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { notify } = useContext(ToastContext)
const {
normal,
restoring,
viewHistory,
} = useWorkflowMode()
const handleShowFeatures = useCallback(() => {
const {
@ -62,10 +67,6 @@ const Header: FC = () => {
setShowFeaturesPanel(true)
}, [workflowStore, getNodesReadOnly])
const handleGoBackToEdit = useCallback(() => {
handleRunSetting(true)
}, [handleRunSetting])
const handleCancelRestore = useCallback(() => {
handleLoadBackupDraft()
workflowStore.setState({ isRestoring: false })
@ -102,6 +103,11 @@ const Header: FC = () => {
handleSyncWorkflowDraft(true)
}, [handleSyncWorkflowDraft])
const handleGoBackToEdit = useCallback(() => {
handleLoadBackupDraft()
workflowStore.setState({ historyWorkflowData: undefined })
}, [workflowStore, handleLoadBackupDraft])
return (
<div
className='absolute top-0 left-0 z-10 flex items-center justify-between w-full px-3 h-14'
@ -116,39 +122,25 @@ const Header: FC = () => {
)
}
{
!nodesReadOnly && !isRestoring && <EditingTitle />
normal && <EditingTitle />
}
{
nodesReadOnly && !isRestoring && <RunningTitle />
viewHistory && <RunningTitle />
}
{
isRestoring && <RestoringTitle />
restoring && <RestoringTitle />
}
</div>
{
!isRestoring && (
normal && (
<div className='flex items-center'>
{
nodesReadOnly && (
<Button
className={`
mr-2 px-3 py-0 h-8 bg-white text-[13px] font-medium text-primary-600
border-[0.5px] border-gray-200 shadow-xs
`}
onClick={handleGoBackToEdit}
>
<ArrowNarrowLeft className='w-4 h-4 mr-1' />
{t('workflow.common.goBackToEdit')}
</Button>
)
}
<RunAndHistory />
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
<Button
className={`
mr-2 px-3 py-0 h-8 bg-white text-[13px] font-medium text-gray-700
border-[0.5px] border-gray-200 shadow-xs
${nodesReadOnly && !isRestoring && 'opacity-50 !cursor-not-allowed'}
${nodesReadOnly && 'opacity-50 !cursor-not-allowed'}
`}
onClick={handleShowFeatures}
>
@ -166,19 +158,32 @@ const Header: FC = () => {
crossAxisOffset: 53,
}}
/>
{
!nodesReadOnly && (
<>
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
<Checklist />
</>
)
}
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
<Checklist disabled={nodesReadOnly} />
</div>
)
}
{
isRestoring && (
viewHistory && (
<div className='flex items-center'>
<ViewHistory withText />
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
<Button
type='primary'
className={`
mr-2 px-3 py-0 h-8 text-[13px] font-medium
border-[0.5px] border-gray-200 shadow-xs
`}
onClick={handleGoBackToEdit}
>
<ArrowNarrowLeft className='w-4 h-4 mr-1' />
{t('workflow.common.goBackToEdit')}
</Button>
</div>
)
}
{
restoring && (
<div className='flex items-center'>
<Button
className={`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,14 @@
import { useMemo } from 'react'
import { useStore } from '../store'
export const useWorkflowMode = () => {
const historyWorkflowData = useStore(s => s.historyWorkflowData)
const isRestoring = useStore(s => s.isRestoring)
return useMemo(() => {
return {
normal: !historyWorkflowData && !isRestoring,
restoring: isRestoring,
viewHistory: !!historyWorkflowData,
}
}, [historyWorkflowData, isRestoring])
}

View File

@ -5,11 +5,12 @@ import {
} from 'reactflow'
import produce from 'immer'
import { useWorkflowStore } from '../store'
import { useNodesSyncDraft } from '../hooks'
import {
NodeRunningStatus,
WorkflowRunningStatus,
} from '../types'
import { useWorkflow } from './use-workflow'
import { useWorkflowInteractions } from './use-workflow-interactions'
import { useStore as useAppStore } from '@/app/components/app/store'
import type { IOtherOptions } from '@/service/base'
import { ssePost } from '@/service/base'
@ -24,7 +25,8 @@ export const useWorkflowRun = () => {
const workflowStore = useWorkflowStore()
const reactflow = useReactFlow()
const featuresStore = useFeaturesStore()
const { renderTreeFromRecord } = useWorkflow()
const { doSyncWorkflowDraft } = useNodesSyncDraft()
const { handleUpdateWorkflowCanvas } = useWorkflowInteractions()
const handleBackupDraft = useCallback(() => {
const {
@ -45,15 +47,11 @@ export const useWorkflowRun = () => {
viewport: getViewport(),
features,
})
doSyncWorkflowDraft()
}
}, [reactflow, workflowStore, store, featuresStore])
}, [reactflow, workflowStore, store, featuresStore, doSyncWorkflowDraft])
const handleLoadBackupDraft = useCallback(() => {
const {
setNodes,
setEdges,
} = store.getState()
const { setViewport } = reactflow
const {
backupDraft,
setBackupDraft,
@ -66,64 +64,32 @@ export const useWorkflowRun = () => {
viewport,
features,
} = backupDraft
setNodes(nodes)
setEdges(edges)
setViewport(viewport)
handleUpdateWorkflowCanvas({
nodes,
edges,
viewport,
})
featuresStore!.setState({ features })
setBackupDraft(undefined)
}
}, [store, reactflow, workflowStore, featuresStore])
}, [handleUpdateWorkflowCanvas, workflowStore, featuresStore])
const handleRunSetting = useCallback((shouldClear?: boolean) => {
if (shouldClear) {
workflowStore.setState({
workflowRunningData: undefined,
historyWorkflowData: undefined,
showInputsPanel: false,
})
}
else {
workflowStore.setState({
workflowRunningData: {
result: {
status: shouldClear ? '' : WorkflowRunningStatus.Waiting,
},
tracing: [],
},
})
}
const {
setNodes,
getNodes,
edges,
setEdges,
} = store.getState()
if (shouldClear) {
handleLoadBackupDraft()
}
else {
handleBackupDraft()
const newNodes = produce(getNodes(), (draft) => {
draft.forEach((node) => {
node.data._runningStatus = NodeRunningStatus.Waiting
})
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
draft.forEach((edge) => {
edge.data._runned = false
})
})
setEdges(newEdges)
}
}, [store, handleLoadBackupDraft, handleBackupDraft, workflowStore])
const handleRun = useCallback((
const handleRun = useCallback(async (
params: any,
callback?: IOtherOptions,
) => {
const {
getNodes,
setNodes,
} = store.getState()
const newNodes = produce(getNodes(), (draft) => {
draft.forEach((node) => {
node.data.selected = false
})
})
setNodes(newNodes)
await doSyncWorkflowDraft()
const {
onWorkflowStarted,
onWorkflowFinished,
@ -151,15 +117,14 @@ export const useWorkflowRun = () => {
let prevNodeId = ''
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.result = {
...draft?.result,
setWorkflowRunningData({
result: {
status: WorkflowRunningStatus.Running,
}
}))
},
tracing: [],
})
ssePost(
url,
@ -174,8 +139,6 @@ export const useWorkflowRun = () => {
setWorkflowRunningData,
} = workflowStore.getState()
const {
getNodes,
setNodes,
edges,
setEdges,
} = store.getState()
@ -188,12 +151,6 @@ export const useWorkflowRun = () => {
}
}))
const newNodes = produce(getNodes(), (draft) => {
draft.forEach((node) => {
node.data._runningStatus = NodeRunningStatus.Waiting
})
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
draft.forEach((edge) => {
edge.data = {
@ -253,6 +210,7 @@ export const useWorkflowRun = () => {
setNodes,
edges,
setEdges,
transform,
} = store.getState()
const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
@ -268,12 +226,12 @@ export const useWorkflowRun = () => {
const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id)
const currentNode = nodes[currentNodeIndex]
const position = currentNode.position
const zoom = 1
const zoom = transform[2]
setViewport({
x: (clientWidth - 400 - currentNode.width!) / 2 - position.x,
y: (clientHeight - currentNode.height!) / 2 - position.y,
zoom,
x: (clientWidth - 400 - currentNode.width! * zoom) / 2 - position.x * zoom,
y: (clientHeight - currentNode.height! * zoom) / 2 - position.y * zoom,
zoom: transform[2],
})
const newNodes = produce(nodes, (draft) => {
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
@ -329,7 +287,7 @@ export const useWorkflowRun = () => {
...restCallback,
},
)
}, [store, reactflow, workflowStore])
}, [store, reactflow, workflowStore, doSyncWorkflowDraft])
const handleStopRun = useCallback((taskId: string) => {
const appId = useAppStore.getState().appDetail?.id
@ -344,18 +302,21 @@ export const useWorkflowRun = () => {
if (publishedWorkflow) {
const nodes = publishedWorkflow.graph.nodes
const edges = publishedWorkflow.graph.edges
const viewport = publishedWorkflow.graph.viewport
const viewport = publishedWorkflow.graph.viewport!
renderTreeFromRecord(nodes, edges, viewport)
handleUpdateWorkflowCanvas({
nodes,
edges,
viewport,
})
featuresStore?.setState({ features: publishedWorkflow.features })
workflowStore.getState().setPublishedAt(publishedWorkflow.created_at)
}
}, [featuresStore, workflowStore, renderTreeFromRecord])
}, [featuresStore, handleUpdateWorkflowCanvas, workflowStore])
return {
handleBackupDraft,
handleLoadBackupDraft,
handleRunSetting,
handleRun,
handleStopRun,
handleRestoreFromPublishedWorkflow,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,10 +3,17 @@ import {
useRef,
} from 'react'
import { useKeyPress } from 'ahooks'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import {
useEdgesInteractions,
useNodesInteractions,
useWorkflowInteractions,
} from '../../hooks'
import ChatWrapper from './chat-wrapper'
import Button from '@/app/components/base/button'
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
export type ChatWrapperRefType = {
handleRestart: () => void
@ -14,33 +21,56 @@ export type ChatWrapperRefType = {
const DebugAndPreview = () => {
const { t } = useTranslation()
const chatRef = useRef({ handleRestart: () => {} })
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
const { handleNodeCancelRunningStatus } = useNodesInteractions()
const { handleEdgeCancelRunningStatus } = useEdgesInteractions()
const handleRestartChat = () => {
handleNodeCancelRunningStatus()
handleEdgeCancelRunningStatus()
chatRef.current.handleRestart()
}
useKeyPress('shift.r', () => {
chatRef.current.handleRestart()
handleRestartChat()
}, {
exactMatch: true,
})
return (
<div
className={`
flex flex-col w-[400px] rounded-l-2xl h-full border border-black/[0.02] shadow-xl
`}
className={cn(
'flex flex-col w-[400px] rounded-l-2xl h-full border border-black/[0.02]',
)}
style={{
background: 'linear-gradient(156deg, rgba(242, 244, 247, 0.80) 0%, rgba(242, 244, 247, 0.00) 99.43%), var(--white, #FFF)',
}}
>
<div className='shrink-0 flex items-center justify-between px-4 pt-3 pb-2 font-semibold text-gray-900'>
<div className='shrink-0 flex items-center justify-between pl-4 pr-3 pt-3 pb-2 font-semibold text-gray-900'>
{t('workflow.common.debugAndPreview').toLocaleUpperCase()}
<Button
className='pl-2.5 pr-[7px] h-8 bg-white border-[0.5px] border-gray-200 shadow-xs rounded-lg text-[13px] text-primary-600 font-semibold'
onClick={() => chatRef.current.handleRestart()}
>
<RefreshCcw01 className='mr-1 w-3.5 h-3.5' />
{t('common.operation.refresh')}
<div className='ml-2 px-1 leading-[18px] rounded-md border border-gray-200 bg-gray-50 text-[11px] text-gray-500 font-medium'>Shift</div>
<div className='ml-0.5 px-1 leading-[18px] rounded-md border border-gray-200 bg-gray-50 text-[11px] text-gray-500 font-medium'>R</div>
</Button>
<div className='flex items-center'>
<Button
className='px-2 h-8 bg-white border-[0.5px] border-gray-200 shadow-xs rounded-lg text-xs text-gray-700 font-medium'
onClick={() => handleRestartChat()}
>
<RefreshCcw01 className='shrink-0 mr-1 w-3 h-3 text-gray-500' />
<div
className='grow truncate uppercase'
title={t('common.operation.refresh') || ''}
>
{t('common.operation.refresh')}
</div>
<div className='shrink-0 ml-1 px-1 leading-[18px] rounded-md border border-gray-200 bg-gray-50 text-[11px] text-gray-500 font-medium'>Shift</div>
<div className='shrink-0 ml-0.5 px-1 leading-[18px] rounded-md border border-gray-200 bg-gray-50 text-[11px] text-gray-500 font-medium'>R</div>
</Button>
<div className='mx-3 w-[1px] h-3.5 bg-gray-200'></div>
<div
className='flex items-center justify-center w-6 h-6 cursor-pointer'
onClick={handleCancelDebugAndPreviewPanel}
>
<XClose className='w-4 h-4 text-gray-500' />
</div>
</div>
</div>
<div className='grow rounded-b-2xl overflow-y-auto'>
<ChatWrapper ref={chatRef} />

View File

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

View File

@ -1,9 +1,7 @@
import type { FC } from 'react'
import {
memo,
useMemo,
} from 'react'
import { memo } from 'react'
import { useNodes } from 'reactflow'
import cn from 'classnames'
import { useShallow } from 'zustand/react/shallow'
import type { CommonNodeType } from '../types'
import { Panel as NodePanel } from '../nodes'
@ -23,9 +21,8 @@ const Panel: FC = () => {
const nodes = useNodes<CommonNodeType>()
const isChatMode = useIsChatMode()
const selectedNode = nodes.find(node => node.data.selected)
const showInputsPanel = useStore(s => s.showInputsPanel)
const workflowRunningData = useStore(s => s.workflowRunningData)
const historyWorkflowData = useStore(s => s.historyWorkflowData)
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
const isRestoring = useStore(s => s.isRestoring)
const {
enableShortcuts,
@ -37,28 +34,13 @@ const Panel: FC = () => {
showMessageLogModal: state.showMessageLogModal,
setShowMessageLogModal: state.setShowMessageLogModal,
})))
const {
showNodePanel,
showDebugAndPreviewPanel,
showWorkflowPreview,
} = useMemo(() => {
return {
showNodePanel: !!selectedNode && !workflowRunningData && !historyWorkflowData && !showInputsPanel,
showDebugAndPreviewPanel: isChatMode && workflowRunningData && !historyWorkflowData,
showWorkflowPreview: !isChatMode && !historyWorkflowData && (workflowRunningData || showInputsPanel),
}
}, [
showInputsPanel,
selectedNode,
isChatMode,
workflowRunningData,
historyWorkflowData,
])
return (
<div
tabIndex={-1}
className='absolute top-14 right-0 bottom-2 flex z-10 outline-none'
className={cn(
'absolute top-14 right-0 bottom-2 flex z-10 outline-none',
)}
onFocus={disableShortcuts}
onBlur={enableShortcuts}
key={`${isRestoring}`}
@ -76,6 +58,11 @@ const Panel: FC = () => {
/>
)
}
{
!!selectedNode && (
<NodePanel {...selectedNode!} />
)
}
{
historyWorkflowData && !isChatMode && (
<Record />
@ -87,20 +74,15 @@ const Panel: FC = () => {
)
}
{
showDebugAndPreviewPanel && (
showDebugAndPreviewPanel && isChatMode && (
<DebugAndPreview />
)
}
{
showWorkflowPreview && (
showDebugAndPreviewPanel && !isChatMode && (
<WorkflowPreview />
)
}
{
showNodePanel && (
<NodePanel {...selectedNode!} />
)
}
</div>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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