diff --git a/web/app/components/workflow/note-node/note-editor/context.tsx b/web/app/components/workflow/note-node/note-editor/context.tsx index 0349746332..c9e7eb56c8 100644 --- a/web/app/components/workflow/note-node/note-editor/context.tsx +++ b/web/app/components/workflow/note-node/note-editor/context.tsx @@ -20,10 +20,12 @@ const NoteEditorContext = createContext(null) type NoteEditorContextProviderProps = { value: string children: React.JSX.Element | string | (React.JSX.Element | string)[] + editable?: boolean } export const NoteEditorContextProvider = memo(({ value, children, + editable = true, }: NoteEditorContextProviderProps) => { const storeRef = useRef(undefined) @@ -50,6 +52,7 @@ export const NoteEditorContextProvider = memo(({ throw error }, theme, + editable, } return ( diff --git a/web/app/components/workflow/workflow-preview/components/custom-edge.tsx b/web/app/components/workflow/workflow-preview/components/custom-edge.tsx new file mode 100644 index 0000000000..eb660fb7b8 --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/custom-edge.tsx @@ -0,0 +1,101 @@ +import { + memo, + useMemo, +} from 'react' +import type { EdgeProps } from 'reactflow' +import { + BaseEdge, + Position, + getBezierPath, +} from 'reactflow' +import { NodeRunningStatus } from '@/app/components/workflow/types' +import { getEdgeColor } from '@/app/components/workflow/utils' +import CustomEdgeLinearGradientRender from '@/app/components/workflow/custom-edge-linear-gradient-render' +import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types' + +const CustomEdge = ({ + id, + data, + sourceHandleId, + sourceX, + sourceY, + targetX, + targetY, + selected, +}: EdgeProps) => { + const [ + edgePath, + ] = getBezierPath({ + sourceX: sourceX - 8, + sourceY, + sourcePosition: Position.Right, + targetX: targetX + 8, + targetY, + targetPosition: Position.Left, + curvature: 0.16, + }) + const { + _sourceRunningStatus, + _targetRunningStatus, + } = data + + const linearGradientId = useMemo(() => { + if ( + ( + _sourceRunningStatus === NodeRunningStatus.Succeeded + || _sourceRunningStatus === NodeRunningStatus.Failed + || _sourceRunningStatus === NodeRunningStatus.Exception + ) && ( + _targetRunningStatus === NodeRunningStatus.Succeeded + || _targetRunningStatus === NodeRunningStatus.Failed + || _targetRunningStatus === NodeRunningStatus.Exception + || _targetRunningStatus === NodeRunningStatus.Running + ) + ) + return id + }, [_sourceRunningStatus, _targetRunningStatus, id]) + + const stroke = useMemo(() => { + if (selected) + return getEdgeColor(NodeRunningStatus.Running) + + if (linearGradientId) + return `url(#${linearGradientId})` + + if (data?._connectedNodeIsHovering) + return getEdgeColor(NodeRunningStatus.Running, sourceHandleId === ErrorHandleTypeEnum.failBranch) + + return getEdgeColor() + }, [data._connectedNodeIsHovering, linearGradientId, selected, sourceHandleId]) + + return ( + <> + { + linearGradientId && ( + + ) + } + + + ) +} + +export default memo(CustomEdge) diff --git a/web/app/components/workflow/workflow-preview/components/error-handle-on-node.tsx b/web/app/components/workflow/workflow-preview/components/error-handle-on-node.tsx new file mode 100644 index 0000000000..3f1e2120a5 --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/error-handle-on-node.tsx @@ -0,0 +1,66 @@ +import { useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { useUpdateNodeInternals } from 'reactflow' +import { NodeSourceHandle } from './node-handle' +import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types' +import type { Node } from '@/app/components/workflow/types' +import { NodeRunningStatus } from '@/app/components/workflow/types' +import cn from '@/utils/classnames' + +type ErrorHandleOnNodeProps = Pick +const ErrorHandleOnNode = ({ + id, + data, +}: ErrorHandleOnNodeProps) => { + const { t } = useTranslation() + const { error_strategy } = data + const updateNodeInternals = useUpdateNodeInternals() + + useEffect(() => { + if (error_strategy === ErrorHandleTypeEnum.failBranch) + updateNodeInternals(id) + }, [error_strategy, id, updateNodeInternals]) + + if (!error_strategy) + return null + + return ( +
+
+
+ {t('workflow.common.onFailure')} +
+
+ { + error_strategy === ErrorHandleTypeEnum.defaultValue && ( + t('workflow.nodes.common.errorHandle.defaultValue.output') + ) + } + { + error_strategy === ErrorHandleTypeEnum.failBranch && ( + t('workflow.nodes.common.errorHandle.failBranch.title') + ) + } +
+ { + error_strategy === ErrorHandleTypeEnum.failBranch && ( + + ) + } +
+
+ ) +} + +export default ErrorHandleOnNode diff --git a/web/app/components/workflow/workflow-preview/components/node-handle.tsx b/web/app/components/workflow/workflow-preview/components/node-handle.tsx new file mode 100644 index 0000000000..4ff08354be --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/node-handle.tsx @@ -0,0 +1,70 @@ +import { + memo, +} from 'react' +import { + Handle, + Position, +} from 'reactflow' +import { + BlockEnum, +} from '@/app/components/workflow/types' +import type { Node } from '@/app/components/workflow/types' +import cn from '@/utils/classnames' + +type NodeHandleProps = { + handleId: string + handleClassName?: string +} & Pick + +export const NodeTargetHandle = memo(({ + data, + handleId, + handleClassName, +}: NodeHandleProps) => { + const connected = data._connectedTargetHandleIds?.includes(handleId) + + return ( + <> + + + + ) +}) +NodeTargetHandle.displayName = 'NodeTargetHandle' + +export const NodeSourceHandle = memo(({ + data, + handleId, + handleClassName, +}: NodeHandleProps) => { + const connected = data._connectedSourceHandleIds?.includes(handleId) + + return ( + + + ) +}) +NodeSourceHandle.displayName = 'NodeSourceHandle' diff --git a/web/app/components/workflow/workflow-preview/components/nodes/base.tsx b/web/app/components/workflow/workflow-preview/components/nodes/base.tsx new file mode 100644 index 0000000000..55dfac467e --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/nodes/base.tsx @@ -0,0 +1,139 @@ +import type { + ReactElement, +} from 'react' +import { + cloneElement, + memo, +} from 'react' +import { useTranslation } from 'react-i18next' +import cn from '@/utils/classnames' +import BlockIcon from '@/app/components/workflow/block-icon' +import type { + NodeProps, +} from '@/app/components/workflow/types' +import { + BlockEnum, +} from '@/app/components/workflow/types' +import { hasErrorHandleNode } from '@/app/components/workflow/utils' +import Tooltip from '@/app/components/base/tooltip' +import type { IterationNodeType } from '@/app/components/workflow/nodes/iteration/types' +import { + NodeSourceHandle, + NodeTargetHandle, +} from '../node-handle' +import ErrorHandleOnNode from '../error-handle-on-node' + +type NodeCardProps = NodeProps & { + children?: ReactElement +} + +const BaseCard = ({ + id, + data, + children, +}: NodeCardProps) => { + const { t } = useTranslation() + + return ( +
+
+
+ + { + data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && ( + + ) + } + +
+
+ {data.title} +
+ { + data.type === BlockEnum.Iteration && (data as IterationNodeType).is_parallel && ( + +
+ {t('workflow.nodes.iteration.parallelModeEnableTitle')} +
+ {t('workflow.nodes.iteration.parallelModeEnableDesc')} +
} + > +
+ {t('workflow.nodes.iteration.parallelModeUpper')} +
+ + ) + } +
+
+ { + data.type !== BlockEnum.Iteration && data.type !== BlockEnum.Loop && children && ( + cloneElement(children, { id, data }) + ) + } + { + (data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop) && children && ( +
+ {cloneElement(children, { id, data })} +
+ ) + } + { + hasErrorHandleNode(data.type) && ( + + ) + } + { + data.desc && data.type !== BlockEnum.Iteration && data.type !== BlockEnum.Loop && ( +
+ {data.desc} +
+ ) + } +
+ + ) +} + +export default memo(BaseCard) diff --git a/web/app/components/workflow/workflow-preview/components/nodes/constants.ts b/web/app/components/workflow/workflow-preview/components/nodes/constants.ts new file mode 100644 index 0000000000..2a6b01d561 --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/nodes/constants.ts @@ -0,0 +1,12 @@ +import { BlockEnum } from '@/app/components/workflow/types' +import QuestionClassifierNode from './question-classifier/node' +import IfElseNode from './if-else/node' +import IterationNode from './iteration/node' +import LoopNode from './loop/node' + +export const NodeComponentMap: Record = { + [BlockEnum.QuestionClassifier]: QuestionClassifierNode, + [BlockEnum.IfElse]: IfElseNode, + [BlockEnum.Iteration]: IterationNode, + [BlockEnum.Loop]: LoopNode, +} diff --git a/web/app/components/workflow/workflow-preview/components/nodes/if-else/node.tsx b/web/app/components/workflow/workflow-preview/components/nodes/if-else/node.tsx new file mode 100644 index 0000000000..8d9189fcc1 --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/nodes/if-else/node.tsx @@ -0,0 +1,103 @@ +import type { FC } from 'react' +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import type { NodeProps } from 'reactflow' +import { NodeSourceHandle } from '../../node-handle' +import { isEmptyRelatedOperator } from '@/app/components/workflow/nodes/if-else/utils' +import type { Condition, IfElseNodeType } from '@/app/components/workflow/nodes/if-else/types' +import ConditionValue from '@/app/components/workflow/nodes/if-else/components/condition-value' +import ConditionFilesListValue from '@/app/components/workflow/nodes/if-else/components/condition-files-list-value' +const i18nPrefix = 'workflow.nodes.ifElse' + +const IfElseNode: FC> = (props) => { + const { data } = props + const { t } = useTranslation() + const { cases } = data + const casesLength = cases.length + const checkIsConditionSet = useCallback((condition: Condition) => { + if (!condition.variable_selector || condition.variable_selector.length === 0) + return false + + if (condition.sub_variable_condition) { + const isSet = condition.sub_variable_condition.conditions.every((c) => { + if (!c.comparison_operator) + return false + + if (isEmptyRelatedOperator(c.comparison_operator!)) + return true + + return !!c.value + }) + return isSet + } + else { + if (isEmptyRelatedOperator(condition.comparison_operator!)) + return true + + return !!condition.value + } + }, []) + const conditionNotSet = (
+ {t(`${i18nPrefix}.conditionNotSetup`)} +
) + + return ( +
+ { + cases.map((caseItem, index) => ( +
+
+
+
+ {casesLength > 1 && `CASE ${index + 1}`} +
+
{index === 0 ? 'IF' : 'ELIF'}
+
+ +
+
+ {caseItem.conditions.map((condition, i) => ( +
+ { + checkIsConditionSet(condition) + ? ( + (!isEmptyRelatedOperator(condition.comparison_operator!) && condition.sub_variable_condition) + ? ( + + ) + : ( + + ) + + ) + : conditionNotSet} + {i !== caseItem.conditions.length - 1 && ( +
{t(`${i18nPrefix}.${caseItem.logical_operator}`)}
+ )} +
+ ))} +
+
+ )) + } +
+
ELSE
+ +
+
+ ) +} + +export default React.memo(IfElseNode) diff --git a/web/app/components/workflow/workflow-preview/components/nodes/index.tsx b/web/app/components/workflow/workflow-preview/components/nodes/index.tsx new file mode 100644 index 0000000000..e496d8440d --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/nodes/index.tsx @@ -0,0 +1,18 @@ +import type { NodeProps } from 'reactflow' +import BaseNode from './base' +import { NodeComponentMap } from './constants' + +const CustomNode = (props: NodeProps) => { + const nodeData = props.data + const NodeComponent = NodeComponentMap[nodeData.type] + + return ( + <> + + { NodeComponent && } + + + ) +} + +export default CustomNode diff --git a/web/app/components/workflow/workflow-preview/components/nodes/iteration-start/index.tsx b/web/app/components/workflow/workflow-preview/components/nodes/iteration-start/index.tsx new file mode 100644 index 0000000000..e4c7b42fca --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/nodes/iteration-start/index.tsx @@ -0,0 +1,28 @@ +import { memo } from 'react' +import { useTranslation } from 'react-i18next' +import type { NodeProps } from 'reactflow' +import { RiHome5Fill } from '@remixicon/react' +import Tooltip from '@/app/components/base/tooltip' +import { NodeSourceHandle } from '../../node-handle' + +const IterationStartNode = ({ id, data }: NodeProps) => { + const { t } = useTranslation() + + return ( +
+ +
+ +
+
+ +
+ ) +} + +export default memo(IterationStartNode) diff --git a/web/app/components/workflow/workflow-preview/components/nodes/iteration/node.tsx b/web/app/components/workflow/workflow-preview/components/nodes/iteration/node.tsx new file mode 100644 index 0000000000..fbbbfc716f --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/nodes/iteration/node.tsx @@ -0,0 +1,33 @@ +import type { FC } from 'react' +import { + memo, +} from 'react' +import { + Background, + useViewport, +} from 'reactflow' +import type { IterationNodeType } from '@/app/components/workflow/nodes/iteration/types' +import cn from '@/utils/classnames' +import type { NodeProps } from '@/app/components/workflow/types' + +const Node: FC> = ({ + id, +}) => { + const { zoom } = useViewport() + + return ( +
+ +
+ ) +} + +export default memo(Node) diff --git a/web/app/components/workflow/workflow-preview/components/nodes/loop-start/index.tsx b/web/app/components/workflow/workflow-preview/components/nodes/loop-start/index.tsx new file mode 100644 index 0000000000..d9dce9b0ee --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/nodes/loop-start/index.tsx @@ -0,0 +1,28 @@ +import { memo } from 'react' +import { useTranslation } from 'react-i18next' +import type { NodeProps } from 'reactflow' +import { RiHome5Fill } from '@remixicon/react' +import Tooltip from '@/app/components/base/tooltip' +import { NodeSourceHandle } from '../../node-handle' + +const LoopStartNode = ({ id, data }: NodeProps) => { + const { t } = useTranslation() + + return ( +
+ +
+ +
+
+ +
+ ) +} + +export default memo(LoopStartNode) diff --git a/web/app/components/workflow/workflow-preview/components/nodes/loop/hooks.ts b/web/app/components/workflow/workflow-preview/components/nodes/loop/hooks.ts new file mode 100644 index 0000000000..ad671d5453 --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/nodes/loop/hooks.ts @@ -0,0 +1,69 @@ +import { useCallback } from 'react' +import produce from 'immer' +import { useStoreApi } from 'reactflow' +import type { + Node, +} from '@/app/components/workflow/types' +import { + LOOP_PADDING, +} from '@/app/components/workflow/constants' + +export const useNodeLoopInteractions = () => { + const store = useStoreApi() + + const handleNodeLoopRerender = useCallback((nodeId: string) => { + const { + getNodes, + setNodes, + } = store.getState() + + const nodes = getNodes() + const currentNode = nodes.find(n => n.id === nodeId)! + const childrenNodes = nodes.filter(n => n.parentId === nodeId) + let rightNode: Node + let bottomNode: Node + + childrenNodes.forEach((n) => { + if (rightNode) { + if (n.position.x + n.width! > rightNode.position.x + rightNode.width!) + rightNode = n + } + else { + rightNode = n + } + if (bottomNode) { + if (n.position.y + n.height! > bottomNode.position.y + bottomNode.height!) + bottomNode = n + } + else { + bottomNode = n + } + }) + + const widthShouldExtend = rightNode! && currentNode.width! < rightNode.position.x + rightNode.width! + const heightShouldExtend = bottomNode! && currentNode.height! < bottomNode.position.y + bottomNode.height! + + if (widthShouldExtend || heightShouldExtend) { + const newNodes = produce(nodes, (draft) => { + draft.forEach((n) => { + if (n.id === nodeId) { + if (widthShouldExtend) { + n.data.width = rightNode.position.x + rightNode.width! + LOOP_PADDING.right + n.width = rightNode.position.x + rightNode.width! + LOOP_PADDING.right + } + if (heightShouldExtend) { + n.data.height = bottomNode.position.y + bottomNode.height! + LOOP_PADDING.bottom + n.height = bottomNode.position.y + bottomNode.height! + LOOP_PADDING.bottom + } + } + }) + }) + + setNodes(newNodes) + } + }, [store]) + + return { + handleNodeLoopRerender, + } +} diff --git a/web/app/components/workflow/workflow-preview/components/nodes/loop/node.tsx b/web/app/components/workflow/workflow-preview/components/nodes/loop/node.tsx new file mode 100644 index 0000000000..4c0080ec70 --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/nodes/loop/node.tsx @@ -0,0 +1,48 @@ +import type { FC } from 'react' +import { + memo, + useEffect, +} from 'react' +import { + Background, + useNodesInitialized, + useViewport, +} from 'reactflow' +import type { LoopNodeType } from '@/app/components/workflow/nodes/loop/types' +import cn from '@/utils/classnames' +import type { NodeProps } from '@/app/components/workflow/types' +import { useNodeLoopInteractions } from './hooks' + +const Node: FC> = ({ + id, + data, +}) => { + const { zoom } = useViewport() + const nodesInitialized = useNodesInitialized() + const { handleNodeLoopRerender } = useNodeLoopInteractions() + + useEffect(() => { + if (nodesInitialized) + handleNodeLoopRerender(id) + }, [nodesInitialized, id, handleNodeLoopRerender]) + + return ( +
+ +
+ ) +} + +export default memo(Node) diff --git a/web/app/components/workflow/workflow-preview/components/nodes/question-classifier/node.tsx b/web/app/components/workflow/workflow-preview/components/nodes/question-classifier/node.tsx new file mode 100644 index 0000000000..7c24ff54c3 --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/nodes/question-classifier/node.tsx @@ -0,0 +1,44 @@ +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' +import type { NodeProps } from 'reactflow' +import InfoPanel from '@/app/components/workflow/nodes/_base/components/info-panel' +import type { QuestionClassifierNodeType } from '@/app/components/workflow/nodes/question-classifier/types' +import { NodeSourceHandle } from '../../node-handle' + +const i18nPrefix = 'workflow.nodes.questionClassifiers' + +const Node: FC> = (props) => { + const { t } = useTranslation() + const { data } = props + const topics = data.classes + + return ( +
+ { + !!topics.length && ( +
+ {topics.map((topic, index) => ( +
+ + +
+ ))} +
+ ) + } +
+ ) +} + +export default React.memo(Node) diff --git a/web/app/components/workflow/workflow-preview/components/note-node/index.tsx b/web/app/components/workflow/workflow-preview/components/note-node/index.tsx new file mode 100644 index 0000000000..4cbba996f3 --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/note-node/index.tsx @@ -0,0 +1,68 @@ +import { + memo, + useRef, +} from 'react' +import { useTranslation } from 'react-i18next' +import type { NodeProps } from 'reactflow' +import { + NoteEditor, + NoteEditorContextProvider, +} from '@/app/components/workflow/note-node/note-editor' +import { THEME_MAP } from '@/app/components/workflow/note-node/constants' +import type { NoteNodeType } from '@/app/components/workflow/note-node/types' +import cn from '@/utils/classnames' + +const NoteNode = ({ + data, +}: NodeProps) => { + const { t } = useTranslation() + const ref = useRef(null) + const theme = data.theme + + return ( +
+ + <> +
+
+
+ +
+
+ { + data.showAuthor && ( +
+ {data.author} +
+ ) + } + +
+
+ ) +} + +export default memo(NoteNode) diff --git a/web/app/components/workflow/workflow-preview/components/zoom-in-out.tsx b/web/app/components/workflow/workflow-preview/components/zoom-in-out.tsx new file mode 100644 index 0000000000..0576f317a0 --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/zoom-in-out.tsx @@ -0,0 +1,212 @@ +import type { FC } from 'react' +import { + Fragment, + memo, + useCallback, + useState, +} from 'react' +import { + RiZoomInLine, + RiZoomOutLine, +} from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import { + useReactFlow, + useViewport, +} from 'reactflow' +import ShortcutsName from '@/app/components/workflow/shortcuts-name' +import Divider from '@/app/components/base/divider' +import TipPopup from '@/app/components/workflow/operator/tip-popup' +import cn from '@/utils/classnames' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' + +enum ZoomType { + zoomIn = 'zoomIn', + zoomOut = 'zoomOut', + zoomToFit = 'zoomToFit', + zoomTo25 = 'zoomTo25', + zoomTo50 = 'zoomTo50', + zoomTo75 = 'zoomTo75', + zoomTo100 = 'zoomTo100', + zoomTo200 = 'zoomTo200', +} + +const ZoomInOut: FC = () => { + const { t } = useTranslation() + const { + zoomIn, + zoomOut, + zoomTo, + fitView, + } = useReactFlow() + const { zoom } = useViewport() + const [open, setOpen] = useState(false) + + const ZOOM_IN_OUT_OPTIONS = [ + [ + { + key: ZoomType.zoomTo200, + text: '200%', + }, + { + key: ZoomType.zoomTo100, + text: '100%', + }, + { + key: ZoomType.zoomTo75, + text: '75%', + }, + { + key: ZoomType.zoomTo50, + text: '50%', + }, + { + key: ZoomType.zoomTo25, + text: '25%', + }, + ], + [ + { + key: ZoomType.zoomToFit, + text: t('workflow.operator.zoomToFit'), + }, + ], + ] + + const handleZoom = (type: string) => { + if (type === ZoomType.zoomToFit) + fitView() + + if (type === ZoomType.zoomTo25) + zoomTo(0.25) + + if (type === ZoomType.zoomTo50) + zoomTo(0.5) + + if (type === ZoomType.zoomTo75) + zoomTo(0.75) + + if (type === ZoomType.zoomTo100) + zoomTo(1) + + if (type === ZoomType.zoomTo200) + zoomTo(2) + } + + const handleTrigger = useCallback(() => { + setOpen(v => !v) + }, []) + + return ( + + +
+
+ +
{ + if (zoom <= 0.25) + return + + e.stopPropagation() + zoomOut() + }} + > + +
+
+
{Number.parseFloat(`${zoom * 100}`).toFixed(0)}%
+ +
= 2 ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-black/5'}`} + onClick={(e) => { + if (zoom >= 2) + return + + e.stopPropagation() + zoomIn() + }} + > + +
+
+
+
+
+ +
+ { + ZOOM_IN_OUT_OPTIONS.map((options, i) => ( + + { + i !== 0 && ( + + ) + } +
+ { + options.map(option => ( +
handleZoom(option.key)} + > + {option.text} +
+ { + option.key === ZoomType.zoomToFit && ( + + ) + } + { + option.key === ZoomType.zoomTo50 && ( + + ) + } + { + option.key === ZoomType.zoomTo100 && ( + + ) + } +
+
+ )) + } +
+
+ )) + } +
+
+
+ ) +} + +export default memo(ZoomInOut) diff --git a/web/app/components/workflow/workflow-preview/index.tsx b/web/app/components/workflow/workflow-preview/index.tsx new file mode 100644 index 0000000000..d25b30aa10 --- /dev/null +++ b/web/app/components/workflow/workflow-preview/index.tsx @@ -0,0 +1,147 @@ +'use client' + +import { + useCallback, + useState, +} from 'react' +import ReactFlow, { + Background, + MiniMap, + ReactFlowProvider, + SelectionMode, + applyEdgeChanges, + applyNodeChanges, +} from 'reactflow' +import type { + EdgeChange, + NodeChange, + Viewport, +} from 'reactflow' +import 'reactflow/dist/style.css' +import '../style.css' +import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants' +import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants' +import { CUSTOM_SIMPLE_NODE } from '@/app/components/workflow/simple-node/constants' +import CustomConnectionLine from '@/app/components/workflow/custom-connection-line' +import { + CUSTOM_EDGE, + CUSTOM_NODE, + ITERATION_CHILDREN_Z_INDEX, +} from '@/app/components/workflow/constants' +import cn from '@/utils/classnames' +import { + initialEdges, + initialNodes, +} from '@/app/components/workflow/utils/workflow-init' +import type { + Edge, + Node, +} from '@/app/components/workflow/types' +import { CUSTOM_NOTE_NODE } from '@/app/components/workflow/note-node/constants' +import { WorkflowHistoryProvider } from '@/app/components/workflow/workflow-history-store' +import CustomNode from './components/nodes' +import CustomEdge from './components/custom-edge' +import ZoomInOut from './components/zoom-in-out' +import IterationStartNode from './components/nodes/iteration-start' +import LoopStartNode from './components/nodes/loop-start' +import CustomNoteNode from './components/note-node' + +const nodeTypes = { + [CUSTOM_NODE]: CustomNode, + [CUSTOM_NOTE_NODE]: CustomNoteNode, + [CUSTOM_SIMPLE_NODE]: CustomNode, + [CUSTOM_ITERATION_START_NODE]: IterationStartNode, + [CUSTOM_LOOP_START_NODE]: LoopStartNode, +} +const edgeTypes = { + [CUSTOM_EDGE]: CustomEdge, +} + +type WorkflowPreviewProps = { + nodes: Node[] + edges: Edge[] + viewport: Viewport +} +const WorkflowPreview = ({ + nodes, + edges, + viewport, +}: WorkflowPreviewProps) => { + const [nodesData, setNodesData] = useState(initialNodes(nodes, edges)) + const [edgesData, setEdgesData] = useState(initialEdges(edges, nodes)) + + const onNodesChange = useCallback( + (changes: NodeChange[]) => setNodesData(nds => applyNodeChanges(changes, nds)), + [], + ) + const onEdgesChange = useCallback( + (changes: EdgeChange[]) => setEdgesData(eds => applyEdgeChanges(changes, eds)), + [], + ) + + return ( +
+ <> + +
+ +
+ + + + +
+ ) +} + +const WorkflowPreviewWrapper = (props: WorkflowPreviewProps) => { + return ( + + + + + + ) +} + +export default WorkflowPreviewWrapper